Compare commits
19 Commits
13.0.0-bet
...
13.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c8dbcc20d | ||
|
|
416dcf884d | ||
|
|
09d3ce444a | ||
|
|
27c2ca5048 | ||
|
|
fceeb1b108 | ||
|
|
b442c38f41 | ||
|
|
7c2d2676f7 | ||
|
|
1f6a41cea7 | ||
|
|
0d7ee20a77 | ||
|
|
dcca2350dd | ||
|
|
1cfdd4c41a | ||
|
|
25f4ee7030 | ||
|
|
5320f23017 | ||
|
|
4ffbbbe6d8 | ||
|
|
132e45dff4 | ||
|
|
01652b72b3 | ||
|
|
8b1fdb5a3b | ||
|
|
192add376c | ||
|
|
244ea9593a |
@@ -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: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
|
||||||
- Client: OpenSearch support @SoniEx2 @chaoticryptidz
|
- Client: OpenSearch support @SoniEx2 @chaoticryptidz
|
||||||
- Client: Support remote objects in search @SoniEx2
|
- Client: Support remote objects in search @SoniEx2
|
||||||
|
- Client: user activity page @syuilo
|
||||||
- Client: add user list widget @syuilo
|
- Client: add user list widget @syuilo
|
||||||
- Client: add heatmap of daily active users to about page @syuilo
|
- Client: add heatmap of daily active users to about page @syuilo
|
||||||
- Client: introduce fluent emoji @syuilo
|
- Client: introduce fluent emoji @syuilo
|
||||||
|
- Client: add new theme @syuilo
|
||||||
- Client: show fireworks when visit user who today is birthday @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: show bot warning on screen when logged in as bot account @syuilo
|
||||||
- Client: improve overall performance of client @syuilo
|
- Client: improve overall performance of client @syuilo
|
||||||
- Client: ui tweaks @syuilo
|
- Client: ui tweaks @syuilo
|
||||||
|
- Client: clicker game @syuilo
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
|
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
|
||||||
|
|||||||
@@ -1361,6 +1361,7 @@ _widgets:
|
|||||||
userList: "ユーザーリスト"
|
userList: "ユーザーリスト"
|
||||||
_userList:
|
_userList:
|
||||||
chooseList: "リストを選択"
|
chooseList: "リストを選択"
|
||||||
|
clicker: "クリッカー"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.0.0-beta.28",
|
"version": "13.0.0-beta.31",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
Font Awesome Icons
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
Ⓒ Font Awesome
|
|
||||||
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 577 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 844 B |
|
Before Width: | Height: | Size: 507 B |
|
Before Width: | Height: | Size: 689 B |
|
Before Width: | Height: | Size: 772 B |
|
Before Width: | Height: | Size: 930 B |
|
Before Width: | Height: | Size: 798 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 991 B |
24
packages/backend/assets/tabler-badges/LICENSE
Normal 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.
|
||||||
BIN
packages/backend/assets/tabler-badges/antenna.png
Normal file
|
After Width: | Height: | Size: 516 B |
BIN
packages/backend/assets/tabler-badges/arrow-back-up.png
Normal file
|
After Width: | Height: | Size: 952 B |
BIN
packages/backend/assets/tabler-badges/at.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
packages/backend/assets/tabler-badges/chart-arrows.png
Normal file
|
After Width: | Height: | Size: 829 B |
BIN
packages/backend/assets/tabler-badges/circle-check.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
packages/backend/assets/tabler-badges/messages.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 174 B After Width: | Height: | Size: 174 B |
BIN
packages/backend/assets/tabler-badges/plus.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
packages/backend/assets/tabler-badges/quote.png
Normal file
|
After Width: | Height: | Size: 1011 B |
BIN
packages/backend/assets/tabler-badges/repeat.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
packages/backend/assets/tabler-badges/user-plus.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/backend/assets/tabler-badges/users.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
packages/frontend/assets/cookie.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
70
packages/frontend/src/components/MkClickerGame.vue
Normal 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>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ssazuxis">
|
<div class="ssazuxis">
|
||||||
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
<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>
|
<div class="divider"></div>
|
||||||
<button class="_button">
|
<button class="_button">
|
||||||
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
|
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
|
||||||
@@ -127,14 +127,6 @@ export default defineComponent({
|
|||||||
place-content: center;
|
place-content: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px 16px 12px 0;
|
padding: 12px 16px 12px 0;
|
||||||
|
|
||||||
> i {
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .divider {
|
> .divider {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ let transformOrigin = $ref('center');
|
|||||||
let showing = $ref(true);
|
let showing = $ref(true);
|
||||||
let content = $shallowRef<HTMLElement>();
|
let content = $shallowRef<HTMLElement>();
|
||||||
const zIndex = os.claimZIndex(props.zPriority);
|
const zIndex = os.claimZIndex(props.zPriority);
|
||||||
|
let useSendAnime = $ref(false);
|
||||||
const type = $computed<ModalTypes>(() => {
|
const type = $computed<ModalTypes>(() => {
|
||||||
if (props.preferType === 'auto') {
|
if (props.preferType === 'auto') {
|
||||||
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
||||||
@@ -76,15 +77,19 @@ const type = $computed<ModalTypes>(() => {
|
|||||||
});
|
});
|
||||||
let transitionName = $computed((() =>
|
let transitionName = $computed((() =>
|
||||||
defaultStore.state.animation
|
defaultStore.state.animation
|
||||||
? (type === 'drawer')
|
? useSendAnime
|
||||||
|
? 'send'
|
||||||
|
: type === 'drawer'
|
||||||
? 'modal-drawer'
|
? 'modal-drawer'
|
||||||
: (type === 'popup')
|
: type === 'popup'
|
||||||
? 'modal-popup'
|
? 'modal-popup'
|
||||||
: 'modal'
|
: 'modal'
|
||||||
: ''
|
: ''
|
||||||
));
|
));
|
||||||
let transitionDuration = $computed((() =>
|
let transitionDuration = $computed((() =>
|
||||||
transitionName === 'modal-popup'
|
transitionName === 'send'
|
||||||
|
? 400
|
||||||
|
: transitionName === 'modal-popup'
|
||||||
? 100
|
? 100
|
||||||
: transitionName === 'modal'
|
: transitionName === 'modal'
|
||||||
? 200
|
? 200
|
||||||
@@ -97,8 +102,7 @@ let contentClicking = false;
|
|||||||
|
|
||||||
function close(opts: { useSendAnimation?: boolean } = {}) {
|
function close(opts: { useSendAnimation?: boolean } = {}) {
|
||||||
if (opts.useSendAnimation) {
|
if (opts.useSendAnimation) {
|
||||||
transitionName = 'send';
|
useSendAnime = true;
|
||||||
transitionDuration = 400;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line vue/no-mutating-props
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="elRef" class="qglefbjs" :class="notification.type">
|
<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-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/>
|
||||||
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
|
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
|
||||||
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
|
<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 === 'quote'" class="ti ti-quote"></i>
|
||||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||||
<XReactionIcon
|
<MkReactionIcon
|
||||||
v-else-if="notification.type === 'reaction'"
|
v-else-if="notification.type === 'reaction'"
|
||||||
ref="reactionRef"
|
ref="reactionRef"
|
||||||
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
||||||
@@ -31,45 +31,47 @@
|
|||||||
<span v-else>{{ notification.header }}</span>
|
<span v-else>{{ notification.header }}</span>
|
||||||
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
|
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
|
||||||
</header>
|
</header>
|
||||||
|
<div v-once class="content">
|
||||||
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<i class="ti ti-quote"></i>
|
<i class="ti ti-quote"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
<i class="ti ti-quote"></i>
|
<i class="ti ti-quote"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
<MkA v-else-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||||
<i class="ti ti-quote"></i>
|
<i class="ti ti-quote"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
|
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
|
||||||
<i class="ti ti-quote"></i>
|
<i class="ti ti-quote"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<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"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<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"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<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"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
<MkA v-else-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||||
<i class="ti ti-quote"></i>
|
<i class="ti ti-quote"></i>
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||||
<i class="ti ti-quote"></i>
|
<i class="ti ti-quote"></i>
|
||||||
</MkA>
|
</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-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-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
<span v-else-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-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-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 === '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">
|
<span v-else-if="notification.type === 'app'" class="text">
|
||||||
<Mfm :text="notification.body" :nowrap="!full"/>
|
<Mfm :text="notification.body" :nowrap="!full"/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
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 MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
|
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
|
||||||
import { getNoteSummary } from '@/scripts/get-note-summary';
|
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||||
@@ -263,6 +265,7 @@ useTooltip(reactionRef, (showing) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .content {
|
||||||
> .text {
|
> .text {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -283,6 +286,7 @@ useTooltip(reactionRef, (showing) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (max-width: 600px) {
|
@container (max-width: 600px) {
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
||||||
<span class="text" :class="{ up }">
|
<span class="text" :class="{ up }">+1</span>
|
||||||
<XReactionIcon class="icon" :reaction="reaction"/>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
reaction: string;
|
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}>(), {
|
}>(), {
|
||||||
@@ -23,8 +19,8 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
let up = $ref(false);
|
let up = $ref(false);
|
||||||
const zIndex = os.claimZIndex('veryLow');
|
const zIndex = os.claimZIndex('middle');
|
||||||
const angle = (90 - (Math.random() * 180)) + 'deg';
|
const angle = (45 - (Math.random() * 90)) + 'deg';
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
@@ -55,10 +51,11 @@ onMounted(() => {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
color: var(--accent);
|
color: #fff;
|
||||||
|
text-shadow: 0 0 6px #000;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
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);
|
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
|
||||||
will-change: opacity, transform;
|
will-change: opacity, transform;
|
||||||
|
|
||||||
|
|||||||
72
packages/frontend/src/components/MkReactionEffect.vue
Normal 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>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||||
<div class="beeadbfb">
|
<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 class="name">{{ reaction.replace('@.', '') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</MkTooltip>
|
</MkTooltip>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import MkTooltip from './MkTooltip.vue';
|
import MkTooltip from './MkTooltip.vue';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
showing: boolean;
|
showing: boolean;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||||
<div class="bqxuuuey">
|
<div class="bqxuuuey">
|
||||||
<div class="reaction">
|
<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 class="name">{{ getReactionName(reaction) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="users">
|
<div class="users">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import MkTooltip from './MkTooltip.vue';
|
import MkTooltip from './MkTooltip.vue';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import { getEmojiName } from '@/scripts/emojilist';
|
import { getEmojiName } from '@/scripts/emojilist';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
ref="buttonRef"
|
ref="buttonEl"
|
||||||
v-ripple="canToggle"
|
v-ripple="canToggle"
|
||||||
class="hkzvhatu _button"
|
class="hkzvhatu _button"
|
||||||
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
||||||
@click="toggleReaction()"
|
@click="toggleReaction()"
|
||||||
>
|
>
|
||||||
<XReactionIcon class="icon" :reaction="reaction"/>
|
<MkReactionIcon class="icon" :reaction="reaction"/>
|
||||||
<span class="count">{{ count }}</span>
|
<span class="count">{{ count }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -15,11 +15,11 @@
|
|||||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
|
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
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 * as os from '@/os';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
|
import MkReactionEffect from '@/components/MkReactionEffect.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reaction: string;
|
reaction: string;
|
||||||
@@ -28,7 +28,7 @@ const props = defineProps<{
|
|||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const buttonRef = shallowRef<HTMLElement>();
|
const buttonEl = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||||
|
|
||||||
@@ -58,10 +58,10 @@ const toggleReaction = () => {
|
|||||||
const anime = () => {
|
const anime = () => {
|
||||||
if (document.hidden) return;
|
if (document.hidden) return;
|
||||||
|
|
||||||
const rect = buttonRef.value.getBoundingClientRect();
|
const rect = buttonEl.value.getBoundingClientRect();
|
||||||
const x = rect.left + (buttonRef.value.offsetWidth / 2);
|
const x = rect.left + 16;
|
||||||
const y = rect.top + (buttonRef.value.offsetHeight / 2);
|
const y = rect.top + (buttonEl.value.offsetHeight / 2);
|
||||||
os.popup(MkPlusOneEffect, { reaction: props.reaction, x, y }, {}, 'end');
|
os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {}, 'end');
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(() => props.count, (newCount, oldCount) => {
|
watch(() => props.count, (newCount, oldCount) => {
|
||||||
@@ -72,7 +72,7 @@ onMounted(() => {
|
|||||||
if (!props.isInitial) anime();
|
if (!props.isInitial) anime();
|
||||||
});
|
});
|
||||||
|
|
||||||
useTooltip(buttonRef, async (showing) => {
|
useTooltip(buttonEl, async (showing) => {
|
||||||
const reactions = await os.apiGet('notes/reactions', {
|
const reactions = await os.apiGet('notes/reactions', {
|
||||||
noteId: props.note.id,
|
noteId: props.note.id,
|
||||||
type: props.reaction,
|
type: props.reaction,
|
||||||
@@ -87,7 +87,7 @@ useTooltip(buttonRef, async (showing) => {
|
|||||||
reaction: props.reaction,
|
reaction: props.reaction,
|
||||||
users,
|
users,
|
||||||
count: props.count,
|
count: props.count,
|
||||||
targetElement: buttonRef.value,
|
targetElement: buttonEl.value,
|
||||||
}, {}, 'closed');
|
}, {}, 'closed');
|
||||||
}, 100);
|
}, 100);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<template #prefix><i class="ti ti-lock"></i></template>
|
<template #prefix><i class="ti ti-lock"></i></template>
|
||||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
||||||
</MkInput>
|
</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>
|
||||||
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
|
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
|
||||||
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<template #label>{{ i18n.ts.token }}</template>
|
<template #label>{{ i18n.ts.token }}</template>
|
||||||
<template #prefix><i class="ti ti-123"></i></template>
|
<template #prefix><i class="ti ti-123"></i></template>
|
||||||
</MkInput>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export default {
|
|||||||
target.classList.add('_anime_bounce_standBy');
|
target.classList.add('_anime_bounce_standBy');
|
||||||
|
|
||||||
el.addEventListener('mousedown', () => {
|
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_standBy');
|
||||||
target.classList.add('_anime_bounce_ready');
|
target.classList.add('_anime_bounce_ready');
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<MkFolder v-for="category in customEmojiCategories" :key="category" class="emojis">
|
<MkFolder v-for="category in customEmojiCategories" :key="category" class="emojis">
|
||||||
<template #header>{{ category || $ts.other }}</template>
|
<template #header>{{ category || $ts.other }}</template>
|
||||||
<div class="zuvgdzyt">
|
<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>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||||
<div class="dqokceoi">
|
<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"/>
|
<MkInstanceCardMini :instance="instance"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
|
<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"/>
|
<MkUserCardMini :user="user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
|
|||||||
24
packages/frontend/src/pages/clicker.vue
Normal 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>
|
||||||
@@ -69,7 +69,7 @@ const headerActions = $computed(() => [{
|
|||||||
const headerTabs = $computed(() => [{
|
const headerTabs = $computed(() => [{
|
||||||
key: 'featured',
|
key: 'featured',
|
||||||
title: i18n.ts._play.featured,
|
title: i18n.ts._play.featured,
|
||||||
icon: 'fas fa-fire-alt',
|
icon: 'ti ti-flare',
|
||||||
}, {
|
}, {
|
||||||
key: 'my',
|
key: 'my',
|
||||||
title: i18n.ts._play.my,
|
title: i18n.ts._play.my,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const headerActions = $computed(() => [{
|
|||||||
const headerTabs = $computed(() => [{
|
const headerTabs = $computed(() => [{
|
||||||
key: 'featured',
|
key: 'featured',
|
||||||
title: i18n.ts._pages.featured,
|
title: i18n.ts._pages.featured,
|
||||||
icon: 'fas fa-fire-alt',
|
icon: 'ti ti-flare',
|
||||||
}, {
|
}, {
|
||||||
key: 'my',
|
key: 'my',
|
||||||
title: i18n.ts._pages.my,
|
title: i18n.ts._pages.my,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="_gaps_m">
|
<div class="">
|
||||||
<FormSuspense :p="init">
|
<FormSuspense :p="init">
|
||||||
|
<div class="_gaps">
|
||||||
<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
|
<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 v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)">
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
174
packages/frontend/src/pages/user/activity.following.vue
Normal 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>
|
||||||
174
packages/frontend/src/pages/user/activity.notes.vue
Normal 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>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
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 tinycolor from 'tinycolor2';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import gradient from 'chartjs-plugin-gradient';
|
import gradient from 'chartjs-plugin-gradient';
|
||||||
@@ -67,65 +67,33 @@ async function renderChart() {
|
|||||||
const colorUser2 = '#3498db88';
|
const colorUser2 = '#3498db88';
|
||||||
const colorVisitor2 = '#2ecc7188';
|
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, {
|
chartInstance = new Chart(chartEl, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
datasets: [{
|
datasets: [
|
||||||
parsing: false,
|
makeDataset('UPV (user)', format(raw.upv.user).slice().reverse(), { backgroundColor: colorUser, stack: 'u' }),
|
||||||
label: 'UPV (user)',
|
makeDataset('UPV (visitor)', format(raw.upv.visitor).slice().reverse(), { backgroundColor: colorVisitor, stack: 'u' }),
|
||||||
data: format(raw.upv.user).slice().reverse(),
|
makeDataset('NPV (user)', format(raw.pv.user).slice().reverse(), { backgroundColor: colorUser2, stack: 'n' }),
|
||||||
pointRadius: 0,
|
makeDataset('UPV (visitor)', format(raw.pv.visitor).slice().reverse(), { backgroundColor: colorVisitor2, stack: 'n' }),
|
||||||
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',
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
aspectRatio: 2.5,
|
aspectRatio: 3,
|
||||||
layout: {
|
layout: {
|
||||||
padding: {
|
padding: {
|
||||||
left: 0,
|
left: 0,
|
||||||
|
|||||||
@@ -2,11 +2,19 @@
|
|||||||
<MkSpacer :content-max="700">
|
<MkSpacer :content-max="700">
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkFolder class="item">
|
<MkFolder class="item">
|
||||||
<template #header>Heatmap</template>
|
<template #header><i class="ti ti-activity"></i> Heatmap</template>
|
||||||
<XHeatmap :user="user" :src="'notes'"/>
|
<XHeatmap :user="user" :src="'notes'"/>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<MkFolder class="item">
|
<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"/>
|
<XPv :user="user"/>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,6 +26,8 @@ import { computed } from 'vue';
|
|||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import XHeatmap from './activity.heatmap.vue';
|
import XHeatmap from './activity.heatmap.vue';
|
||||||
import XPv from './activity.pv.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';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -460,6 +460,10 @@ export const routes = [{
|
|||||||
path: '/timeline/antenna/:antennaId',
|
path: '/timeline/antenna/:antennaId',
|
||||||
component: page(() => import('./pages/antenna-timeline.vue')),
|
component: page(() => import('./pages/antenna-timeline.vue')),
|
||||||
loginRequired: true,
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/clicker',
|
||||||
|
component: page(() => import('./pages/clicker.vue')),
|
||||||
|
loginRequired: true,
|
||||||
}, {
|
}, {
|
||||||
name: 'index',
|
name: 'index',
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|||||||
46
packages/frontend/src/scripts/clicker-game.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ export const getBuiltinThemes = () => Promise.all(
|
|||||||
'l-coffee',
|
'l-coffee',
|
||||||
'l-apricot',
|
'l-apricot',
|
||||||
'l-rainy',
|
'l-rainy',
|
||||||
|
'l-botanical',
|
||||||
'l-vivid',
|
'l-vivid',
|
||||||
'l-cherry',
|
'l-cherry',
|
||||||
'l-sushi',
|
'l-sushi',
|
||||||
|
|||||||
29
packages/frontend/src/themes/l-botanical.json5
Normal 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'
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -41,6 +41,11 @@ export function openInstanceMenu(ev: MouseEvent) {
|
|||||||
to: '/api-console',
|
to: '/api-console',
|
||||||
text: 'API Console',
|
text: 'API Console',
|
||||||
icon: 'ti ti-terminal-2',
|
icon: 'ti ti-terminal-2',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
to: '/clicker',
|
||||||
|
text: '🍪👈',
|
||||||
|
icon: 'ti ti-cookie',
|
||||||
}],
|
}],
|
||||||
}, null, {
|
}, null, {
|
||||||
type: 'parent',
|
type: 'parent',
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<div v-if="showMenu" class="menu">
|
<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="/" 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="/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>
|
<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button>
|
<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA>
|
<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="/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>
|
<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 v-if="info" class="page active link">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
|||||||
@@ -11,19 +11,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div>
|
<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="meter">
|
||||||
<div class="val" :style="{ width: `${dayP}%` }"></div>
|
<div class="val" :style="{ width: `${dayP}%` }"></div>
|
||||||
</div>
|
</div>
|
||||||
</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="meter">
|
||||||
<div class="val" :style="{ width: `${monthP}%` }"></div>
|
<div class="val" :style="{ width: `${monthP}%` }"></div>
|
||||||
</div>
|
</div>
|
||||||
</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="meter">
|
||||||
<div class="val" :style="{ width: `${yearP}%` }"></div>
|
<div class="val" :style="{ width: `${yearP}%` }"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,13 +168,14 @@ defineExpose<WidgetComponentExpose>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
> p {
|
> p {
|
||||||
|
display: flex;
|
||||||
margin: 0 0 2px 0;
|
margin: 0 0 2px 0;
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
|
||||||
> b {
|
> b {
|
||||||
margin-left: 2px;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
packages/frontend/src/widgets/clicker.vue
Normal 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>
|
||||||
@@ -25,6 +25,7 @@ export default function(app: App) {
|
|||||||
app.component('MkwAiscriptApp', defineAsyncComponent(() => import('./aiscript-app.vue')));
|
app.component('MkwAiscriptApp', defineAsyncComponent(() => import('./aiscript-app.vue')));
|
||||||
app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
|
app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
|
||||||
app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue')));
|
app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue')));
|
||||||
|
app.component('MkwClicker', defineAsyncComponent(() => import('./clicker.vue')));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const widgets = [
|
export const widgets = [
|
||||||
@@ -52,4 +53,5 @@ export const widgets = [
|
|||||||
'aiscriptApp',
|
'aiscriptApp',
|
||||||
'aichan',
|
'aichan',
|
||||||
'userList',
|
'userList',
|
||||||
|
'clicker',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,14 +3,21 @@
|
|||||||
*/
|
*/
|
||||||
import { swLang } from '@/scripts/lang';
|
import { swLang } from '@/scripts/lang';
|
||||||
import { cli } from '@/scripts/operations';
|
import { cli } from '@/scripts/operations';
|
||||||
import { pushNotificationDataMap } from '@/types';
|
import { badgeNames, pushNotificationDataMap } from '@/types';
|
||||||
import getUserName from '@/scripts/get-user-name';
|
import getUserName from '@/scripts/get-user-name';
|
||||||
import { I18n } from '@/scripts/i18n';
|
import { I18n } from '@/scripts/i18n';
|
||||||
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||||
import { char2fileName } from '@/scripts/twemoji-base';
|
import { char2fileName } from '@/scripts/twemoji-base';
|
||||||
import * as url from '@/scripts/url';
|
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]) {
|
export async function createNotification<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) {
|
||||||
const n = await composeNotification(data);
|
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) }), {
|
return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), {
|
||||||
body: data.body.note.text || '',
|
body: data.body.note.text || '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('reply'),
|
badge: iconUrl('arrow-back-up'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -89,7 +96,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
|
||||||
body: data.body.note.text || '',
|
body: data.body.note.text || '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('retweet'),
|
badge: iconUrl('repeat'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -103,7 +110,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
|
||||||
body: data.body.note.text || '',
|
body: data.body.note.text || '',
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('quote-right'),
|
badge: iconUrl('quote'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -171,7 +178,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
case 'pollEnded':
|
case 'pollEnded':
|
||||||
return [t('_notification.pollEnded'), {
|
return [t('_notification.pollEnded'), {
|
||||||
body: data.body.note.text || '',
|
body: data.body.note.text || '',
|
||||||
badge: iconUrl('clipboard-check-solid'),
|
badge: iconUrl('chart-arrows'),
|
||||||
|
tag: `poll:${data.body.note.id}`,
|
||||||
data,
|
data,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
@@ -179,7 +187,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return [t('_notification.youReceivedFollowRequest'), {
|
return [t('_notification.youReceivedFollowRequest'), {
|
||||||
body: getUserName(data.body.user),
|
body: getUserName(data.body.user),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('clock'),
|
badge: iconUrl('user-plus'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -197,14 +205,14 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return [t('_notification.yourFollowRequestAccepted'), {
|
return [t('_notification.yourFollowRequestAccepted'), {
|
||||||
body: getUserName(data.body.user),
|
body: getUserName(data.body.user),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('check'),
|
badge: iconUrl('circle-check'),
|
||||||
data,
|
data,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'groupInvited':
|
case 'groupInvited':
|
||||||
return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.user) }), {
|
return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.user) }), {
|
||||||
body: data.body.invitation.group.name,
|
body: data.body.invitation.group.name,
|
||||||
badge: iconUrl('id-card-alt'),
|
badge: iconUrl('users'),
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -232,7 +240,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
if (data.body.groupId === null) {
|
if (data.body.groupId === null) {
|
||||||
return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), {
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('comments'),
|
badge: iconUrl('messages'),
|
||||||
tag: `messaging:user:${data.body.userId}`,
|
tag: `messaging:user:${data.body.userId}`,
|
||||||
data,
|
data,
|
||||||
renotify: true,
|
renotify: true,
|
||||||
@@ -240,7 +248,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
}
|
}
|
||||||
return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), {
|
return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), {
|
||||||
icon: data.body.user.avatarUrl,
|
icon: data.body.user.avatarUrl,
|
||||||
badge: iconUrl('comments'),
|
badge: iconUrl('messages'),
|
||||||
tag: `messaging:group:${data.body.groupId}`,
|
tag: `messaging:group:${data.body.groupId}`,
|
||||||
data,
|
data,
|
||||||
renotify: true,
|
renotify: true,
|
||||||
@@ -249,7 +257,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
|||||||
return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
|
return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
|
||||||
body: `${getUserName(data.body.note.user)}: ${data.body.note.text || ''}`,
|
body: `${getUserName(data.body.note.user)}: ${data.body.note.text || ''}`,
|
||||||
icon: data.body.note.user.avatarUrl,
|
icon: data.body.note.user.avatarUrl,
|
||||||
badge: iconUrl('satellite'),
|
badge: iconUrl('antenna'),
|
||||||
tag: `antenna:${data.body.antenna.id}`,
|
tag: `antenna:${data.body.antenna.id}`,
|
||||||
data,
|
data,
|
||||||
renotify: true,
|
renotify: true,
|
||||||
|
|||||||
@@ -36,3 +36,18 @@ export type pushNotificationData<K extends keyof pushNotificationDataSourceMap>
|
|||||||
export type pushNotificationDataMap = {
|
export type pushNotificationDataMap = {
|
||||||
[K in keyof pushNotificationDataSourceMap]: pushNotificationData<K>;
|
[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'
|
||||||
|
;
|
||||||
|
|||||||