Compare commits

...

19 Commits

Author SHA1 Message Date
syuilo
4c8dbcc20d 13.0.0-beta.31 2023-01-08 17:44:24 +09:00
syuilo
416dcf884d 🎨 2023-01-08 17:42:51 +09:00
syuilo
09d3ce444a Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-08 17:41:12 +09:00
syuilo
27c2ca5048 feat(client): 🍪👈 2023-01-08 17:41:09 +09:00
syuilo
fceeb1b108 🎨 2023-01-08 17:38:33 +09:00
tamaina
b442c38f41 enhance: Push Notification badges to Tabler Icons (#9406)
* enhance: Push Notification badges to Tabler Icons

* add receiveFollowRequest icon
2023-01-08 16:47:57 +09:00
syuilo
7c2d2676f7 refactor 2023-01-08 16:17:42 +09:00
syuilo
1f6a41cea7 13.0.0-beta.30 2023-01-08 14:30:00 +09:00
syuilo
0d7ee20a77 🎨 2023-01-08 14:29:22 +09:00
syuilo
dcca2350dd 🎨 2023-01-08 14:28:14 +09:00
syuilo
1cfdd4c41a refactor 2023-01-08 14:22:04 +09:00
syuilo
25f4ee7030 Update CHANGELOG.md 2023-01-08 14:19:32 +09:00
syuilo
5320f23017 enhance(client): improve user activity page 2023-01-08 14:17:56 +09:00
syuilo
4ffbbbe6d8 🎨 2023-01-08 13:10:01 +09:00
syuilo
132e45dff4 13.0.0-beta.29 2023-01-08 11:58:00 +09:00
syuilo
01652b72b3 🎨 2023-01-08 11:57:34 +09:00
syuilo
8b1fdb5a3b enhance(client): add theme 2023-01-08 11:55:37 +09:00
syuilo
192add376c fix MkModal animation 2023-01-08 11:47:16 +09:00
syuilo
244ea9593a tweak components 2023-01-08 11:30:40 +09:00
65 changed files with 865 additions and 193 deletions

View File

@@ -79,13 +79,16 @@ You should also include the user name that made the change.
- Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
- Client: OpenSearch support @SoniEx2 @chaoticryptidz
- Client: Support remote objects in search @SoniEx2
- Client: user activity page @syuilo
- Client: add user list widget @syuilo
- Client: add heatmap of daily active users to about page @syuilo
- Client: introduce fluent emoji @syuilo
- Client: add new theme @syuilo
- Client: show fireworks when visit user who today is birthday @syuilo
- Client: show bot warning on screen when logged in as bot account @syuilo
- Client: improve overall performance of client @syuilo
- Client: ui tweaks @syuilo
- Client: clicker game @syuilo
### Bugfixes
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468

View File

@@ -1361,6 +1361,7 @@ _widgets:
userList: "ユーザーリスト"
_userList:
chooseList: "リストを選択"
clicker: "クリッカー"
_cw:
hide: "隠す"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.0.0-beta.28",
"version": "13.0.0-beta.31",
"codename": "indigo",
"repository": {
"type": "git",

View File

@@ -1,5 +0,0 @@
Font Awesome Icons
-------------------------
Ⓒ Font Awesome
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 991 B

View File

@@ -0,0 +1,24 @@
Tabler Icons
https://github.com/tabler/tabler-icons/blob/master/LICENSE
====
MIT License
Copyright (c) 2020-2022 Paweł Kuna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 174 B

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,70 @@
<template>
<div>
<div v-if="game.ready" :class="$style.game">
<div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
<button v-click-anime class="_button" :class="$style.button" @click="onClick">
<img src="/client-assets/cookie.png" :class="$style.img">
</button>
</div>
<div v-else>
<MkLoading/>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, onMounted, onUnmounted } from 'vue';
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
import * as os from '@/os';
import { useInterval } from '@/scripts/use-interval';
import * as game from '@/scripts/clicker-game';
import number from '@/filters/number';
defineProps<{
}>();
const saveData = game.saveData;
const cookies = computed(() => saveData.value?.cookies);
function onClick(ev: MouseEvent) {
saveData.value!.cookies++;
saveData.value!.clicked++;
const x = ev.clientX;
const y = ev.clientY;
os.popup(MkPlusOneEffect, { x, y }, {}, 'end');
}
useInterval(game.save, 1000 * 5, {
immediate: false,
afterMounted: true,
});
onMounted(async () => {
await game.load();
});
onUnmounted(() => {
game.save();
});
</script>
<style lang="scss" module>
.game {
padding: 16px;
text-align: center;
}
.count {
font-size: 1.3em;
margin-bottom: 6px;
}
.button {
}
.img {
max-width: 90px;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="ssazuxis">
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
<div class="title"><slot name="header"></slot></div>
<div class="title"><div><slot name="header"></slot></div></div>
<div class="divider"></div>
<button class="_button">
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
@@ -127,14 +127,6 @@ export default defineComponent({
place-content: center;
margin: 0;
padding: 12px 16px 12px 0;
> i {
margin-right: 6px;
}
&:empty {
display: none;
}
}
> .divider {

View File

@@ -63,6 +63,7 @@ let transformOrigin = $ref('center');
let showing = $ref(true);
let content = $shallowRef<HTMLElement>();
const zIndex = os.claimZIndex(props.zPriority);
let useSendAnime = $ref(false);
const type = $computed<ModalTypes>(() => {
if (props.preferType === 'auto') {
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
@@ -76,29 +77,32 @@ const type = $computed<ModalTypes>(() => {
});
let transitionName = $computed((() =>
defaultStore.state.animation
? (type === 'drawer')
? 'modal-drawer'
: (type === 'popup')
? 'modal-popup'
: 'modal'
? useSendAnime
? 'send'
: type === 'drawer'
? 'modal-drawer'
: type === 'popup'
? 'modal-popup'
: 'modal'
: ''
));
let transitionDuration = $computed((() =>
transitionName === 'modal-popup'
? 100
: transitionName === 'modal'
? 200
: transitionName === 'modal-drawer'
transitionName === 'send'
? 400
: transitionName === 'modal-popup'
? 100
: transitionName === 'modal'
? 200
: 0
: transitionName === 'modal-drawer'
? 200
: 0
));
let contentClicking = false;
function close(opts: { useSendAnimation?: boolean } = {}) {
if (opts.useSendAnimation) {
transitionName = 'send';
transitionDuration = 400;
useSendAnime = true;
}
// eslint-disable-next-line vue/no-mutating-props

View File

@@ -1,6 +1,6 @@
<template>
<div ref="elRef" class="qglefbjs" :class="notification.type">
<div class="head">
<div v-once class="head">
<MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/>
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
@@ -15,7 +15,7 @@
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon
<MkReactionIcon
v-else-if="notification.type === 'reaction'"
ref="reactionRef"
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
@@ -31,37 +31,39 @@
<span v-else>{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
</header>
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
</MkA>
<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
<i class="ti ti-quote"></i>
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
</MkA>
<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
</MkA>
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
<span v-if="notification.type === 'app'" class="text">
<Mfm :text="notification.body" :nowrap="!full"/>
</span>
<div v-once class="content">
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
</MkA>
<MkA v-else-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
<i class="ti ti-quote"></i>
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
</MkA>
<MkA v-else-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-else-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-else-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-else-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
</MkA>
<span v-else-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
<span v-else-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<span v-else-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
<span v-else-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
<span v-else-if="notification.type === 'app'" class="text">
<Mfm :text="notification.body" :nowrap="!full"/>
</span>
</div>
</div>
</div>
</template>
@@ -69,7 +71,7 @@
<script lang="ts" setup>
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
import * as misskey from 'misskey-js';
import XReactionIcon from '@/components/MkReactionIcon.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
import { getNoteSummary } from '@/scripts/get-note-summary';
@@ -263,23 +265,25 @@ useTooltip(reactionRef, (showing) => {
}
}
> .text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
> .content {
> .text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
> i {
vertical-align: super;
font-size: 50%;
opacity: 0.5;
}
> i {
vertical-align: super;
font-size: 50%;
opacity: 0.5;
}
> i:first-child {
margin-right: 4px;
}
> i:first-child {
margin-right: 4px;
}
> i:last-child {
margin-left: 4px;
> i:last-child {
margin-left: 4px;
}
}
}
}

View File

@@ -1,18 +1,14 @@
<template>
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
<span class="text" :class="{ up }">
<XReactionIcon class="icon" :reaction="reaction"/>
</span>
<span class="text" :class="{ up }">+1</span>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import * as os from '@/os';
import XReactionIcon from '@/components/MkReactionIcon.vue';
const props = withDefaults(defineProps<{
reaction: string;
x: number;
y: number;
}>(), {
@@ -23,8 +19,8 @@ const emit = defineEmits<{
}>();
let up = $ref(false);
const zIndex = os.claimZIndex('veryLow');
const angle = (90 - (Math.random() * 180)) + 'deg';
const zIndex = os.claimZIndex('middle');
const angle = (45 - (Math.random() * 90)) + 'deg';
onMounted(() => {
window.setTimeout(() => {
@@ -55,10 +51,11 @@ onMounted(() => {
right: 0;
bottom: 0;
margin: auto;
color: var(--accent);
color: #fff;
text-shadow: 0 0 6px #000;
font-size: 18px;
font-weight: bold;
transform: translateY(-30px);
transform: translateY(0px);
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
will-change: opacity, transform;

View File

@@ -0,0 +1,72 @@
<template>
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
<span class="text" :class="{ up }">
<MkReactionIcon class="icon" :reaction="reaction"/>
</span>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import * as os from '@/os';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
const props = withDefaults(defineProps<{
reaction: string;
x: number;
y: number;
}>(), {
});
const emit = defineEmits<{
(ev: 'end'): void;
}>();
let up = $ref(false);
const zIndex = os.claimZIndex('middle');
const angle = (90 - (Math.random() * 180)) + 'deg';
onMounted(() => {
window.setTimeout(() => {
up = true;
}, 10);
window.setTimeout(() => {
emit('end');
}, 1100);
});
</script>
<style lang="scss" module>
.root {
pointer-events: none;
position: fixed;
width: 128px;
height: 128px;
&:global {
> .text {
display: block;
height: 1em;
text-align: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
color: var(--accent);
font-size: 18px;
font-weight: bold;
transform: translateY(-30px);
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
will-change: opacity, transform;
&.up {
opacity: 0;
transform: translateY(-50px) rotateZ(v-bind(angle));
}
}
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="beeadbfb">
<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
<div class="name">{{ reaction.replace('@.', '') }}</div>
</div>
</MkTooltip>
@@ -10,7 +10,7 @@
<script lang="ts" setup>
import { } from 'vue';
import MkTooltip from './MkTooltip.vue';
import XReactionIcon from '@/components/MkReactionIcon.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
defineProps<{
showing: boolean;

View File

@@ -2,7 +2,7 @@
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
<div class="bqxuuuey">
<div class="reaction">
<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
<div class="name">{{ getReactionName(reaction) }}</div>
</div>
<div class="users">
@@ -19,7 +19,7 @@
<script lang="ts" setup>
import { } from 'vue';
import MkTooltip from './MkTooltip.vue';
import XReactionIcon from '@/components/MkReactionIcon.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import { getEmojiName } from '@/scripts/emojilist';
defineProps<{

View File

@@ -1,12 +1,12 @@
<template>
<button
ref="buttonRef"
ref="buttonEl"
v-ripple="canToggle"
class="hkzvhatu _button"
:class="{ reacted: note.myReaction == reaction, canToggle }"
@click="toggleReaction()"
>
<XReactionIcon class="icon" :reaction="reaction"/>
<MkReactionIcon class="icon" :reaction="reaction"/>
<span class="count">{{ count }}</span>
</button>
</template>
@@ -15,11 +15,11 @@
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
import * as misskey from 'misskey-js';
import XDetails from '@/components/MkReactionsViewer.details.vue';
import XReactionIcon from '@/components/MkReactionIcon.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import * as os from '@/os';
import { useTooltip } from '@/scripts/use-tooltip';
import { $i } from '@/account';
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
import MkReactionEffect from '@/components/MkReactionEffect.vue';
const props = defineProps<{
reaction: string;
@@ -28,7 +28,7 @@ const props = defineProps<{
note: misskey.entities.Note;
}>();
const buttonRef = shallowRef<HTMLElement>();
const buttonEl = shallowRef<HTMLElement>();
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
@@ -58,10 +58,10 @@ const toggleReaction = () => {
const anime = () => {
if (document.hidden) return;
const rect = buttonRef.value.getBoundingClientRect();
const x = rect.left + (buttonRef.value.offsetWidth / 2);
const y = rect.top + (buttonRef.value.offsetHeight / 2);
os.popup(MkPlusOneEffect, { reaction: props.reaction, x, y }, {}, 'end');
const rect = buttonEl.value.getBoundingClientRect();
const x = rect.left + 16;
const y = rect.top + (buttonEl.value.offsetHeight / 2);
os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {}, 'end');
};
watch(() => props.count, (newCount, oldCount) => {
@@ -72,7 +72,7 @@ onMounted(() => {
if (!props.isInitial) anime();
});
useTooltip(buttonRef, async (showing) => {
useTooltip(buttonEl, async (showing) => {
const reactions = await os.apiGet('notes/reactions', {
noteId: props.note.id,
type: props.reaction,
@@ -87,7 +87,7 @@ useTooltip(buttonRef, async (showing) => {
reaction: props.reaction,
users,
count: props.count,
targetElement: buttonRef.value,
targetElement: buttonEl.value,
}, {}, 'closed');
}, 100);
</script>

View File

@@ -14,7 +14,7 @@
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
@@ -36,7 +36,7 @@
<template #label>{{ i18n.ts.token }}</template>
<template #prefix><i class="ti ti-123"></i></template>
</MkInput>
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
</div>
</div>

View File

@@ -12,6 +12,9 @@ export default {
target.classList.add('_anime_bounce_standBy');
el.addEventListener('mousedown', () => {
target.classList.remove('_anime_bounce_ready');
target.classList.remove('_anime_bounce');
target.classList.add('_anime_bounce_standBy');
target.classList.add('_anime_bounce_ready');

View File

@@ -22,7 +22,7 @@
<MkFolder v-for="category in customEmojiCategories" :key="category" class="emojis">
<template #header>{{ category || $ts.other }}</template>
<div class="zuvgdzyt">
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/>
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" v-once :key="emoji.name" class="emoji" :emoji="emoji"/>
</div>
</MkFolder>
</div>

View File

@@ -36,7 +36,7 @@
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
<div class="dqokceoi">
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`">
<MkA v-for="instance in items" v-once :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`">
<MkInstanceCardMini :instance="instance"/>
</MkA>
</div>

View File

@@ -41,7 +41,7 @@
</div>
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`">
<MkA v-for="user in items" v-once :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`">
<MkUserCardMini :user="user"/>
</MkA>
</MkPagination>

View File

@@ -0,0 +1,24 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader/></template>
<MkSpacer :content-max="800">
<MkClickerGame/>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MkClickerGame from '@/components/MkClickerGame.vue';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
definePageMetadata({
title: '🍪👈',
icon: 'ti ti-cookie',
});
</script>
<style lang="scss" module>
</style>

View File

@@ -69,7 +69,7 @@ const headerActions = $computed(() => [{
const headerTabs = $computed(() => [{
key: 'featured',
title: i18n.ts._play.featured,
icon: 'fas fa-fire-alt',
icon: 'ti ti-flare',
}, {
key: 'my',
title: i18n.ts._play.my,

View File

@@ -63,7 +63,7 @@ const headerActions = $computed(() => [{
const headerTabs = $computed(() => [{
key: 'featured',
title: i18n.ts._pages.featured,
icon: 'fas fa-fire-alt',
icon: 'ti ti-flare',
}, {
key: 'my',
title: i18n.ts._pages.my,

View File

@@ -1,18 +1,20 @@
<template>
<div class="_gaps_m">
<div class="">
<FormSuspense :p="init">
<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
<div class="_gaps">
<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
<div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)">
<div class="avatar">
<MkAvatar :user="account" class="avatar"/>
</div>
<div class="body">
<div class="name">
<MkUserName :user="account"/>
<div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)">
<div class="avatar">
<MkAvatar :user="account" class="avatar"/>
</div>
<div class="acct">
<MkAcct :user="account"/>
<div class="body">
<div class="name">
<MkUserName :user="account"/>
</div>
<div class="acct">
<MkAcct :user="account"/>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,174 @@
<template>
<div>
<MkLoading v-if="fetching"/>
<div v-show="!fetching" :class="$style.root" class="_panel">
<canvas ref="chartEl"></canvas>
<MkChartLegend ref="legendEl" style="margin-top: 8px;"/>
</div>
</div>
</template>
<script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { Chart, ChartDataset } from 'chart.js';
import tinycolor from 'tinycolor2';
import * as misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
import { satisfies } from 'compare-versions';
import * as os from '@/os';
import { defaultStore } from '@/store';
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
import { chartLegend } from '@/scripts/chart-legend';
import MkChartLegend from '@/components/MkChartLegend.vue';
initChart();
const props = defineProps<{
user: misskey.entities.User;
}>();
const chartEl = $shallowRef<HTMLCanvasElement>(null);
let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
const now = new Date();
let chartInstance: Chart = null;
const chartLimit = 50;
let fetching = $ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
async function renderChart() {
if (chartInstance) {
chartInstance.destroy();
}
const getDate = (ago: number) => {
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
return new Date(y, m, d - ago);
};
const format = (arr) => {
return arr.map((v, i) => ({
x: getDate(i).getTime(),
y: v,
}));
};
const raw = await os.api('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const colorFollowLocal = '#008FFB';
const colorFollowRemote = '#008FFB88';
const colorFollowedLocal = '#2ecc71';
const colorFollowedRemote = '#2ecc7188';
function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset {
return Object.assign({
label: label,
data: data,
parsing: false,
pointRadius: 0,
borderWidth: 0,
borderJoinStyle: 'round',
borderRadius: 4,
barPercentage: 0.9,
fill: true,
} satisfies ChartDataset, extra);
}
chartInstance = new Chart(chartEl, {
type: 'bar',
data: {
datasets: [
makeDataset('Follow (local)', format(raw.local.followings.inc).slice().reverse(), { backgroundColor: colorFollowLocal }),
makeDataset('Follow (remote)', format(raw.remote.followings.inc).slice().reverse(), { backgroundColor: colorFollowRemote }),
makeDataset('Followed (local)', format(raw.local.followers.inc).slice().reverse(), { backgroundColor: colorFollowedLocal }),
makeDataset('Followed (remote)', format(raw.remote.followers.inc).slice().reverse(), { backgroundColor: colorFollowedRemote }),
],
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 8,
top: 0,
bottom: 0,
},
},
scales: {
x: {
type: 'time',
offset: true,
stacked: true,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
month: 'Y/M',
},
},
grid: {
display: false,
},
ticks: {
display: true,
maxRotation: 0,
autoSkipPadding: 8,
},
},
y: {
position: 'left',
stacked: true,
suggestedMax: 10,
grid: {
display: true,
},
ticks: {
display: true,
//mirror: true,
},
},
},
interaction: {
intersect: false,
mode: 'index',
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
mode: 'index',
animation: {
duration: 0,
},
external: externalTooltipHandler,
},
gradient,
},
},
plugins: [chartVLine(vLineColor), chartLegend(legendEl)],
});
fetching = false;
}
onMounted(async () => {
renderChart();
});
</script>
<style lang="scss" module>
.root {
padding: 20px;
}
</style>

View File

@@ -0,0 +1,174 @@
<template>
<div>
<MkLoading v-if="fetching"/>
<div v-show="!fetching" :class="$style.root" class="_panel">
<canvas ref="chartEl"></canvas>
<MkChartLegend ref="legendEl" style="margin-top: 8px;"/>
</div>
</div>
</template>
<script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { Chart, ChartDataset } from 'chart.js';
import tinycolor from 'tinycolor2';
import * as misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
import { satisfies } from 'compare-versions';
import * as os from '@/os';
import { defaultStore } from '@/store';
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import { chartVLine } from '@/scripts/chart-vline';
import { alpha } from '@/scripts/color';
import { initChart } from '@/scripts/init-chart';
import { chartLegend } from '@/scripts/chart-legend';
import MkChartLegend from '@/components/MkChartLegend.vue';
initChart();
const props = defineProps<{
user: misskey.entities.User;
}>();
const chartEl = $shallowRef<HTMLCanvasElement>(null);
let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
const now = new Date();
let chartInstance: Chart = null;
const chartLimit = 50;
let fetching = $ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
async function renderChart() {
if (chartInstance) {
chartInstance.destroy();
}
const getDate = (ago: number) => {
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
return new Date(y, m, d - ago);
};
const format = (arr) => {
return arr.map((v, i) => ({
x: getDate(i).getTime(),
y: v,
}));
};
const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const colorNormal = '#008FFB';
const colorReply = '#FEB019';
const colorRenote = '#00E396';
const colorFile = '#e300db';
function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset {
return Object.assign({
label: label,
data: data,
parsing: false,
pointRadius: 0,
borderWidth: 0,
borderJoinStyle: 'round',
borderRadius: 4,
barPercentage: 0.9,
fill: true,
} satisfies ChartDataset, extra);
}
chartInstance = new Chart(chartEl, {
type: 'bar',
data: {
datasets: [
makeDataset('File', format(raw.diffs.withFile).slice().reverse(), { backgroundColor: colorFile }),
makeDataset('Renote', format(raw.diffs.renote).slice().reverse(), { backgroundColor: colorRenote }),
makeDataset('Reply', format(raw.diffs.reply).slice().reverse(), { backgroundColor: colorReply }),
makeDataset('Normal', format(raw.diffs.normal).slice().reverse(), { backgroundColor: colorNormal }),
],
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 8,
top: 0,
bottom: 0,
},
},
scales: {
x: {
type: 'time',
offset: true,
stacked: true,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
month: 'Y/M',
},
},
grid: {
display: false,
},
ticks: {
display: true,
maxRotation: 0,
autoSkipPadding: 8,
},
},
y: {
position: 'left',
stacked: true,
suggestedMax: 10,
grid: {
display: true,
},
ticks: {
display: true,
//mirror: true,
},
},
},
interaction: {
intersect: false,
mode: 'index',
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
mode: 'index',
animation: {
duration: 0,
},
external: externalTooltipHandler,
},
gradient,
},
},
plugins: [chartVLine(vLineColor), chartLegend(legendEl)],
});
fetching = false;
}
onMounted(async () => {
renderChart();
});
</script>
<style lang="scss" module>
.root {
padding: 20px;
}
</style>

View File

@@ -10,7 +10,7 @@
<script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { Chart } from 'chart.js';
import { Chart, ChartDataset } from 'chart.js';
import tinycolor from 'tinycolor2';
import * as misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
@@ -67,65 +67,33 @@ async function renderChart() {
const colorUser2 = '#3498db88';
const colorVisitor2 = '#2ecc7188';
function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset {
return Object.assign({
label: label,
data: data,
parsing: false,
pointRadius: 0,
borderWidth: 0,
borderJoinStyle: 'round',
borderRadius: 4,
barPercentage: 0.7,
categoryPercentage: 0.7,
fill: true,
} satisfies ChartDataset, extra);
}
chartInstance = new Chart(chartEl, {
type: 'bar',
data: {
datasets: [{
parsing: false,
label: 'UPV (user)',
data: format(raw.upv.user).slice().reverse(),
pointRadius: 0,
borderWidth: 0,
borderJoinStyle: 'round',
borderRadius: 4,
backgroundColor: colorUser,
barPercentage: 0.7,
categoryPercentage: 0.7,
fill: true,
stack: 'u',
}, {
parsing: false,
label: 'UPV (visitor)',
data: format(raw.upv.visitor).slice().reverse(),
pointRadius: 0,
borderWidth: 0,
borderJoinStyle: 'round',
borderRadius: 4,
backgroundColor: colorVisitor,
barPercentage: 0.7,
categoryPercentage: 0.7,
fill: true,
stack: 'u',
}, {
parsing: false,
label: 'NPV (user)',
data: format(raw.pv.user).slice().reverse(),
pointRadius: 0,
borderWidth: 0,
borderJoinStyle: 'round',
borderRadius: 4,
backgroundColor: colorUser2,
barPercentage: 0.7,
categoryPercentage: 0.7,
fill: true,
stack: 'n',
}, {
parsing: false,
label: 'NPV (visitor)',
data: format(raw.pv.visitor).slice().reverse(),
pointRadius: 0,
borderWidth: 0,
borderJoinStyle: 'round',
borderRadius: 4,
backgroundColor: colorVisitor2,
barPercentage: 0.7,
categoryPercentage: 0.7,
fill: true,
stack: 'n',
}],
datasets: [
makeDataset('UPV (user)', format(raw.upv.user).slice().reverse(), { backgroundColor: colorUser, stack: 'u' }),
makeDataset('UPV (visitor)', format(raw.upv.visitor).slice().reverse(), { backgroundColor: colorVisitor, stack: 'u' }),
makeDataset('NPV (user)', format(raw.pv.user).slice().reverse(), { backgroundColor: colorUser2, stack: 'n' }),
makeDataset('UPV (visitor)', format(raw.pv.visitor).slice().reverse(), { backgroundColor: colorVisitor2, stack: 'n' }),
],
},
options: {
aspectRatio: 2.5,
aspectRatio: 3,
layout: {
padding: {
left: 0,

View File

@@ -2,11 +2,19 @@
<MkSpacer :content-max="700">
<div class="_gaps">
<MkFolder class="item">
<template #header>Heatmap</template>
<template #header><i class="ti ti-activity"></i> Heatmap</template>
<XHeatmap :user="user" :src="'notes'"/>
</MkFolder>
<MkFolder class="item">
<template #header>PV</template>
<template #header><i class="ti ti-pencil"></i> Notes</template>
<XNotes :user="user"/>
</MkFolder>
<MkFolder class="item">
<template #header><i class="ti ti-users"></i> Following</template>
<XFollowing :user="user"/>
</MkFolder>
<MkFolder class="item">
<template #header><i class="ti ti-eye"></i> PV</template>
<XPv :user="user"/>
</MkFolder>
</div>
@@ -18,6 +26,8 @@ import { computed } from 'vue';
import * as misskey from 'misskey-js';
import XHeatmap from './activity.heatmap.vue';
import XPv from './activity.pv.vue';
import XNotes from './activity.notes.vue';
import XFollowing from './activity.following.vue';
import MkFolder from '@/components/MkFolder.vue';
const props = defineProps<{

View File

@@ -460,6 +460,10 @@ export const routes = [{
path: '/timeline/antenna/:antennaId',
component: page(() => import('./pages/antenna-timeline.vue')),
loginRequired: true,
}, {
path: '/clicker',
component: page(() => import('./pages/clicker.vue')),
loginRequired: true,
}, {
name: 'index',
path: '/',

View File

@@ -0,0 +1,46 @@
import { ref, computed } from 'vue';
import * as os from '@/os';
type SaveData = {
gameVersion: number;
cookies: number;
clicked: number;
};
export const saveData = ref<SaveData>();
export const ready = computed(() => saveData.value != null);
let prev = '';
export async function load() {
try {
saveData.value = await os.api('i/registry/get', {
scope: ['clickerGame'],
key: 'saveData',
});
} catch (err) {
if (err.code === 'NO_SUCH_KEY') {
saveData.value = {
gameVersion: 1,
cookies: 0,
clicked: 0,
};
save();
return;
}
throw err;
}
}
export async function save() {
const current = JSON.stringify(saveData.value);
if (current === prev) return;
await os.api('i/registry/set', {
scope: ['clickerGame'],
key: 'saveData',
value: saveData.value,
});
prev = current;
}

View File

@@ -24,6 +24,7 @@ export const getBuiltinThemes = () => Promise.all(
'l-coffee',
'l-apricot',
'l-rainy',
'l-botanical',
'l-vivid',
'l-cherry',
'l-sushi',

View File

@@ -0,0 +1,29 @@
{
id: '1100673c-f902-4ccd-93aa-7cb88be56178',
name: 'Mi Botanical Light',
author: 'ThinaticSystem',
base: 'light',
props: {
accent: '#77b58c',
bg: 'e2deda',
fg: '#3d3d3d',
fgHighlighted: '#6bc9a0',
divider: '#cfcfcf',
panel: '@X14',
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
header: ':alpha<0.7<@panel',
navBg: '@X14',
renote: '#229e92',
mention: '#da6d35',
mentionMe: '#d44c4c',
hashtag: '#4cb8d4',
link: '@accent',
buttonGradateB: ':hue<-70<@accent',
success: '#86b300',
X14: '#ebe7e5'
},
}

View File

@@ -41,6 +41,11 @@ export function openInstanceMenu(ev: MouseEvent) {
to: '/api-console',
text: 'API Console',
icon: 'ti ti-terminal-2',
}, {
type: 'link',
to: '/clicker',
text: '🍪👈',
icon: 'ti ti-cookie',
}],
}, null, {
type: 'parent',

View File

@@ -34,7 +34,7 @@
<div v-if="showMenu" class="menu">
<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA>
<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA>
<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA>
<MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA>
<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA>
<div class="action">
<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button>

View File

@@ -4,7 +4,7 @@
<div class="content">
<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA>
<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA>
<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA>
<MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA>
<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA>
<div v-if="info" class="page active link">
<div class="title">

View File

@@ -11,19 +11,19 @@
</div>
<div class="info">
<div>
<p>{{ i18n.ts.today }}: <b>{{ dayP.toFixed(1) }}%</b></p>
<p>{{ i18n.ts.today }}<b>{{ dayP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${dayP}%` }"></div>
</div>
</div>
<div>
<p>{{ i18n.ts.thisMonth }}: <b>{{ monthP.toFixed(1) }}%</b></p>
<p>{{ i18n.ts.thisMonth }}<b>{{ monthP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${monthP}%` }"></div>
</div>
</div>
<div>
<p>{{ i18n.ts.thisYear }}: <b>{{ yearP.toFixed(1) }}%</b></p>
<p>{{ i18n.ts.thisYear }}<b>{{ yearP.toFixed(1) }}%</b></p>
<div class="meter">
<div class="val" :style="{ width: `${yearP}%` }"></div>
</div>
@@ -168,13 +168,14 @@ defineExpose<WidgetComponentExpose>({
}
> p {
display: flex;
margin: 0 0 2px 0;
font-size: 0.75em;
line-height: 18px;
opacity: 0.8;
> b {
margin-left: 2px;
margin-left: auto;
}
}

View File

@@ -0,0 +1,44 @@
<template>
<MkContainer :show-header="widgetProps.showHeader" class="mkw-clicker">
<template #header><i class="ti ti-cookie"></i>Clicker</template>
<MkClickerGame/>
</MkContainer>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import { GetFormResultType } from '@/scripts/form';
import { $i } from '@/account';
import MkContainer from '@/components/MkContainer.vue';
import MkClickerGame from '@/components/MkClickerGame.vue';
const name = 'clicker';
const widgetPropsDef = {
showHeader: {
type: 'boolean' as const,
default: true,
},
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>

View File

@@ -25,6 +25,7 @@ export default function(app: App) {
app.component('MkwAiscriptApp', defineAsyncComponent(() => import('./aiscript-app.vue')));
app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue')));
app.component('MkwClicker', defineAsyncComponent(() => import('./clicker.vue')));
}
export const widgets = [
@@ -52,4 +53,5 @@ export const widgets = [
'aiscriptApp',
'aichan',
'userList',
'clicker',
];

View File

@@ -3,14 +3,21 @@
*/
import { swLang } from '@/scripts/lang';
import { cli } from '@/scripts/operations';
import { pushNotificationDataMap } from '@/types';
import { badgeNames, pushNotificationDataMap } from '@/types';
import getUserName from '@/scripts/get-user-name';
import { I18n } from '@/scripts/i18n';
import { getAccountFromId } from '@/scripts/get-account-from-id';
import { char2fileName } from '@/scripts/twemoji-base';
import * as url from '@/scripts/url';
const iconUrl = (name: string) => `/static-assets/notification-badges/${name}.png`;
const iconUrl = (name: badgeNames) => `/static-assets/tabler-badges/${name}.png`;
/* How to add a new badge:
* 1. Find the icon and download png from https://tabler-icons.io/
* 2. vips resize ~/Downloads/icon-name.png vipswork.png 0.4; vips scRGB2BW vipswork.png ~/icon-name.png"[compression=9,strip]"; rm vipswork.png;
* 3. mv ~/icon-name.png ~/misskey/packages/backend/assets/tabler-badges/
* 4. Add 'icon-name' to badgeNames
* 5. Add `badge: iconUrl('icon-name'),`
*/
export async function createNotification<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) {
const n = await composeNotification(data);
@@ -75,7 +82,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), {
body: data.body.note.text || '',
icon: data.body.user.avatarUrl,
badge: iconUrl('reply'),
badge: iconUrl('arrow-back-up'),
data,
actions: [
{
@@ -89,7 +96,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
body: data.body.note.text || '',
icon: data.body.user.avatarUrl,
badge: iconUrl('retweet'),
badge: iconUrl('repeat'),
data,
actions: [
{
@@ -103,7 +110,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
body: data.body.note.text || '',
icon: data.body.user.avatarUrl,
badge: iconUrl('quote-right'),
badge: iconUrl('quote'),
data,
actions: [
{
@@ -171,7 +178,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
case 'pollEnded':
return [t('_notification.pollEnded'), {
body: data.body.note.text || '',
badge: iconUrl('clipboard-check-solid'),
badge: iconUrl('chart-arrows'),
tag: `poll:${data.body.note.id}`,
data,
}];
@@ -179,7 +187,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
return [t('_notification.youReceivedFollowRequest'), {
body: getUserName(data.body.user),
icon: data.body.user.avatarUrl,
badge: iconUrl('clock'),
badge: iconUrl('user-plus'),
data,
actions: [
{
@@ -197,14 +205,14 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
return [t('_notification.yourFollowRequestAccepted'), {
body: getUserName(data.body.user),
icon: data.body.user.avatarUrl,
badge: iconUrl('check'),
badge: iconUrl('circle-check'),
data,
}];
case 'groupInvited':
return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.user) }), {
body: data.body.invitation.group.name,
badge: iconUrl('id-card-alt'),
badge: iconUrl('users'),
data,
actions: [
{
@@ -232,7 +240,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
if (data.body.groupId === null) {
return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), {
icon: data.body.user.avatarUrl,
badge: iconUrl('comments'),
badge: iconUrl('messages'),
tag: `messaging:user:${data.body.userId}`,
data,
renotify: true,
@@ -240,7 +248,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
}
return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), {
icon: data.body.user.avatarUrl,
badge: iconUrl('comments'),
badge: iconUrl('messages'),
tag: `messaging:group:${data.body.groupId}`,
data,
renotify: true,
@@ -249,7 +257,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
body: `${getUserName(data.body.note.user)}: ${data.body.note.text || ''}`,
icon: data.body.note.user.avatarUrl,
badge: iconUrl('satellite'),
badge: iconUrl('antenna'),
tag: `antenna:${data.body.antenna.id}`,
data,
renotify: true,

View File

@@ -36,3 +36,18 @@ export type pushNotificationData<K extends keyof pushNotificationDataSourceMap>
export type pushNotificationDataMap = {
[K in keyof pushNotificationDataSourceMap]: pushNotificationData<K>;
};
export type badgeNames =
'null'
| 'antenna'
| 'arrow-back-up'
| 'at'
| 'chart-arrows'
| 'circle-check'
| 'messages'
| 'plus'
| 'quote'
| 'repeat'
| 'user-plus'
| 'users'
;