mirror of
https://github.com/sim1222/misskey.git
synced 2025-04-29 02:37:22 +09:00
fix: can build
This commit is contained in:
parent
378748a643
commit
34eb30164c
@ -1,90 +0,0 @@
|
|||||||
{
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"watch": "vite build --watch --mode development",
|
|
||||||
"build": "vite build",
|
|
||||||
"lint": "eslint --quiet \"src/**/*.{ts,vue}\""
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"chokidar": "^3.3.1",
|
|
||||||
"lodash": "^4.17.21"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@discordapp/twemoji": "14.0.2",
|
|
||||||
"@fortawesome/fontawesome-free": "6.1.2",
|
|
||||||
"@rollup/plugin-alias": "3.1.9",
|
|
||||||
"@rollup/plugin-json": "4.1.0",
|
|
||||||
"@syuilo/aiscript": "0.11.1",
|
|
||||||
"@vitejs/plugin-vue": "3.1.0",
|
|
||||||
"@vue/compiler-sfc": "3.2.39",
|
|
||||||
"autobind-decorator": "2.4.0",
|
|
||||||
"autosize": "5.0.1",
|
|
||||||
"blurhash": "1.1.5",
|
|
||||||
"broadcast-channel": "4.17.0",
|
|
||||||
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2",
|
|
||||||
"chart.js": "3.9.1",
|
|
||||||
"chartjs-adapter-date-fns": "2.0.0",
|
|
||||||
"chartjs-plugin-gradient": "0.5.1",
|
|
||||||
"chartjs-plugin-zoom": "1.2.1",
|
|
||||||
"compare-versions": "5.0.1",
|
|
||||||
"cropperjs": "2.0.0-beta",
|
|
||||||
"date-fns": "2.29.3",
|
|
||||||
"escape-regexp": "0.0.1",
|
|
||||||
"eventemitter3": "4.0.7",
|
|
||||||
"idb-keyval": "6.2.0",
|
|
||||||
"insert-text-at-cursor": "0.3.0",
|
|
||||||
"json5": "2.2.1",
|
|
||||||
"katex": "0.15.6",
|
|
||||||
"matter-js": "0.18.0",
|
|
||||||
"mfm-js": "git+https://github.com/sim1222/mfm.js.git",
|
|
||||||
"misetehoshii": "https://github.com/melt-adzuki/misetehoshii",
|
|
||||||
"misskey-js": "0.0.14",
|
|
||||||
"photoswipe": "5.3.2",
|
|
||||||
"prismjs": "1.29.0",
|
|
||||||
"punycode": "2.1.1",
|
|
||||||
"querystring": "0.2.1",
|
|
||||||
"rndstr": "1.0.0",
|
|
||||||
"s-age": "1.1.2",
|
|
||||||
"sass": "1.54.9",
|
|
||||||
"seedrandom": "3.0.5",
|
|
||||||
"strict-event-emitter-types": "2.0.0",
|
|
||||||
"stringz": "2.1.0",
|
|
||||||
"syuilo-password-strength": "0.0.1",
|
|
||||||
"textarea-caret": "3.1.0",
|
|
||||||
"three": "0.144.0",
|
|
||||||
"throttle-debounce": "5.0.0",
|
|
||||||
"tinycolor2": "1.4.2",
|
|
||||||
"tsc-alias": "1.7.0",
|
|
||||||
"tsconfig-paths": "4.1.0",
|
|
||||||
"twemoji-parser": "14.0.0",
|
|
||||||
"typescript": "4.8.3",
|
|
||||||
"uuid": "9.0.0",
|
|
||||||
"vanilla-tilt": "1.7.2",
|
|
||||||
"vite": "3.1.3",
|
|
||||||
"vue": "3.2.39",
|
|
||||||
"vue-prism-editor": "2.0.0-alpha.2",
|
|
||||||
"vuedraggable": "4.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/escape-regexp": "0.0.1",
|
|
||||||
"@types/glob": "8.0.0",
|
|
||||||
"@types/gulp": "4.0.9",
|
|
||||||
"@types/gulp-rename": "2.0.1",
|
|
||||||
"@types/katex": "0.14.0",
|
|
||||||
"@types/matter-js": "0.18.2",
|
|
||||||
"@types/punycode": "2.1.0",
|
|
||||||
"@types/seedrandom": "3.0.2",
|
|
||||||
"@types/throttle-debounce": "5.0.0",
|
|
||||||
"@types/tinycolor2": "1.4.3",
|
|
||||||
"@types/uuid": "8.3.4",
|
|
||||||
"@typescript-eslint/eslint-plugin": "5.38.0",
|
|
||||||
"@typescript-eslint/parser": "5.38.0",
|
|
||||||
"cross-env": "7.0.3",
|
|
||||||
"cypress": "10.8.0",
|
|
||||||
"eslint": "8.23.1",
|
|
||||||
"eslint-plugin-import": "2.26.0",
|
|
||||||
"eslint-plugin-vue": "9.5.1",
|
|
||||||
"rollup": "2.79.0",
|
|
||||||
"start-server-and-test": "1.14.0"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,658 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="!muted"
|
|
||||||
v-show="!isDeleted"
|
|
||||||
ref="el"
|
|
||||||
v-hotkey="keymap"
|
|
||||||
v-size="{ max: [500, 450, 350, 300] }"
|
|
||||||
class="tkcbzcuz"
|
|
||||||
:tabindex="!isDeleted ? '-1' : null"
|
|
||||||
:class="{ renote: isRenote }"
|
|
||||||
>
|
|
||||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
|
|
||||||
<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.ts.pinnedNote }}</div>
|
|
||||||
<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
|
|
||||||
<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.ts.featured }}</div>
|
|
||||||
<div v-if="isRenote" class="renote">
|
|
||||||
<MkAvatar class="avatar" :user="note.user"/>
|
|
||||||
<i class="fas fa-retweet"></i>
|
|
||||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
|
||||||
<template #user>
|
|
||||||
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
|
|
||||||
<MkUserName :user="note.user"/>
|
|
||||||
</MkA>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<div class="info">
|
|
||||||
<button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
|
|
||||||
<i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i>
|
|
||||||
<MkTime :time="note.createdAt"/>
|
|
||||||
</button>
|
|
||||||
<MkVisibility :note="note"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<article class="article" @contextmenu.stop="onContextmenu">
|
|
||||||
<MkAvatar class="avatar" :user="appearNote.user"/>
|
|
||||||
<div class="main">
|
|
||||||
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
|
|
||||||
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
|
|
||||||
<div class="body">
|
|
||||||
<p v-if="appearNote.cw != null" class="cw">
|
|
||||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
|
||||||
<XCwButton v-model="showContent" :note="appearNote"/>
|
|
||||||
</p>
|
|
||||||
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }">
|
|
||||||
<div class="text">
|
|
||||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
|
||||||
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
|
|
||||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
|
||||||
<a v-if="appearNote.renote != null" class="rp">RN:</a>
|
|
||||||
<div v-if="translating || translation" class="translation">
|
|
||||||
<MkLoading v-if="translating" mini/>
|
|
||||||
<div v-else class="translated">
|
|
||||||
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
|
||||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="appearNote.files.length > 0" class="files">
|
|
||||||
<XMediaList :media-list="appearNote.files"/>
|
|
||||||
</div>
|
|
||||||
<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
|
|
||||||
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
|
|
||||||
<button v-if="isLong && collapsed" class="fade _button" @click="collapsed = false">
|
|
||||||
<span>{{ i18n.ts.showMore }}</span>
|
|
||||||
</button>
|
|
||||||
<button v-else-if="isLong && !collapsed" class="showLess _button" @click="collapsed = true">
|
|
||||||
<span>{{ i18n.ts.showLess }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
|
|
||||||
</div>
|
|
||||||
<footer class="footer">
|
|
||||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
|
||||||
<button class="button _button" @click="reply()">
|
|
||||||
<template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
|
|
||||||
<template v-else><i class="fas fa-reply"></i></template>
|
|
||||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
|
||||||
</button>
|
|
||||||
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
|
||||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
|
|
||||||
<i class="fas fa-plus"></i>
|
|
||||||
</button>
|
|
||||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
|
||||||
<i class="fas fa-minus"></i>
|
|
||||||
</button>
|
|
||||||
<button ref="menuButton" class="button _button" @click="menu()">
|
|
||||||
<i class="fas fa-ellipsis-h"></i>
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
<div v-else class="muted" @click="muted = false">
|
|
||||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
|
||||||
<template #name>
|
|
||||||
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
|
|
||||||
<MkUserName :user="appearNote.user"/>
|
|
||||||
</MkA>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, inject, onMounted, onUnmounted, reactive, ref, Ref } from 'vue';
|
|
||||||
import * as mfm from 'mfm-js';
|
|
||||||
import * as misskey from 'misskey-js';
|
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
|
||||||
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
|
||||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
|
||||||
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
|
||||||
import XMediaList from '@/components/MkMediaList.vue';
|
|
||||||
import XCwButton from '@/components/MkCwButton.vue';
|
|
||||||
import XPoll from '@/components/MkPoll.vue';
|
|
||||||
import XRenoteButton from '@/components/MkRenoteButton.vue';
|
|
||||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
|
||||||
import MkVisibility from '@/components/MkVisibility.vue';
|
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
|
||||||
import { userPage } from '@/filters/user';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { defaultStore, noteViewInterruptors } from '@/store';
|
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker';
|
|
||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
|
||||||
import { $i } from '@/account';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
|
||||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
note: misskey.entities.Note;
|
|
||||||
pinned?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const inChannel = inject('inChannel', null);
|
|
||||||
|
|
||||||
let note = $ref(JSON.parse(JSON.stringify(props.note)));
|
|
||||||
|
|
||||||
// plugin
|
|
||||||
if (noteViewInterruptors.length > 0) {
|
|
||||||
onMounted(async () => {
|
|
||||||
let result = JSON.parse(JSON.stringify(note));
|
|
||||||
for (const interruptor of noteViewInterruptors) {
|
|
||||||
result = await interruptor.handler(result);
|
|
||||||
}
|
|
||||||
note = result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRenote = (
|
|
||||||
note.renote != null &&
|
|
||||||
note.text == null &&
|
|
||||||
note.fileIds.length === 0 &&
|
|
||||||
note.poll == null
|
|
||||||
);
|
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
|
||||||
const menuButton = ref<HTMLElement>();
|
|
||||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
|
||||||
const renoteTime = ref<HTMLElement>();
|
|
||||||
const reactButton = ref<HTMLElement>();
|
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
|
||||||
const showContent = ref(false);
|
|
||||||
const isLong = (appearNote.cw == null && appearNote.text != null && (
|
|
||||||
(appearNote.text.split('\n').length > 9) ||
|
|
||||||
(appearNote.text.length > 500)
|
|
||||||
));
|
|
||||||
const collapsed = ref(appearNote.cw == null && isLong);
|
|
||||||
const isDeleted = ref(false);
|
|
||||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
|
||||||
const translation = ref(null);
|
|
||||||
const translating = ref(false);
|
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
|
||||||
|
|
||||||
const keymap = {
|
|
||||||
'r': () => reply(true),
|
|
||||||
'e|a|plus': () => react(true),
|
|
||||||
'q': () => renoteButton.value.renote(true),
|
|
||||||
'up|k|shift+tab': focusBefore,
|
|
||||||
'down|j|tab': focusAfter,
|
|
||||||
'esc': blur,
|
|
||||||
'm|o': () => menu(true),
|
|
||||||
's': () => showContent.value !== showContent.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
useNoteCapture({
|
|
||||||
rootEl: el,
|
|
||||||
note: $$(appearNote),
|
|
||||||
isDeletedRef: isDeleted,
|
|
||||||
});
|
|
||||||
|
|
||||||
function reply(viaKeyboard = false): void {
|
|
||||||
pleaseLogin();
|
|
||||||
os.post({
|
|
||||||
reply: appearNote,
|
|
||||||
animation: !viaKeyboard,
|
|
||||||
}, () => {
|
|
||||||
focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function react(viaKeyboard = false): void {
|
|
||||||
pleaseLogin();
|
|
||||||
blur();
|
|
||||||
reactionPicker.show(reactButton.value, results => {
|
|
||||||
os.api('notes/reactions/create', {
|
|
||||||
noteId: appearNote.id,
|
|
||||||
reaction: results.reaction,
|
|
||||||
});
|
|
||||||
if (results.withRenote) {
|
|
||||||
os.api('notes/create', {
|
|
||||||
renoteId: appearNote.id,
|
|
||||||
isRenote: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, () => {
|
|
||||||
focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function undoReact(note): void {
|
|
||||||
const oldReaction = note.myReaction;
|
|
||||||
if (!oldReaction) return;
|
|
||||||
os.api('notes/reactions/delete', {
|
|
||||||
noteId: note.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null);
|
|
||||||
|
|
||||||
function onContextmenu(ev: MouseEvent): void {
|
|
||||||
const isLink = (el: HTMLElement) => {
|
|
||||||
if (el.tagName === 'A') return true;
|
|
||||||
if (el.parentElement) {
|
|
||||||
return isLink(el.parentElement);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (isLink(ev.target)) return;
|
|
||||||
if (window.getSelection().toString() !== '') return;
|
|
||||||
|
|
||||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
|
||||||
ev.preventDefault();
|
|
||||||
react();
|
|
||||||
} else {
|
|
||||||
os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), ev).then(focus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function menu(viaKeyboard = false): void {
|
|
||||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
|
|
||||||
viaKeyboard,
|
|
||||||
}).then(focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showRenoteMenu(viaKeyboard = false): void {
|
|
||||||
if (!isMyRenote) return;
|
|
||||||
os.popupMenu([{
|
|
||||||
text: i18n.ts.unrenote,
|
|
||||||
icon: 'fas fa-trash-alt',
|
|
||||||
danger: true,
|
|
||||||
action: () => {
|
|
||||||
os.api('notes/delete', {
|
|
||||||
noteId: note.id,
|
|
||||||
});
|
|
||||||
isDeleted.value = true;
|
|
||||||
},
|
|
||||||
}], renoteTime.value, {
|
|
||||||
viaKeyboard: viaKeyboard,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function focus() {
|
|
||||||
el.value.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function blur() {
|
|
||||||
el.value.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusBefore() {
|
|
||||||
focusPrev(el.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusAfter() {
|
|
||||||
focusNext(el.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readPromo() {
|
|
||||||
os.api('promo/read', {
|
|
||||||
noteId: appearNote.id,
|
|
||||||
});
|
|
||||||
isDeleted.value = true;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.tkcbzcuz {
|
|
||||||
position: relative;
|
|
||||||
transition: box-shadow 0.1s ease;
|
|
||||||
font-size: 1.05em;
|
|
||||||
overflow: clip;
|
|
||||||
contain: content;
|
|
||||||
|
|
||||||
// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
|
|
||||||
// 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
|
|
||||||
// ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
|
|
||||||
// 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
|
|
||||||
// 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
|
|
||||||
//content-visibility: auto;
|
|
||||||
//contain-intrinsic-size: 0 128px;
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
pointer-events: none;
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: auto;
|
|
||||||
width: calc(100% - 8px);
|
|
||||||
height: calc(100% - 8px);
|
|
||||||
border: dashed 1px var(--focus);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover > .article > .main > .footer > .button {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px 32px 8px 32px;
|
|
||||||
line-height: 24px;
|
|
||||||
font-size: 90%;
|
|
||||||
white-space: pre;
|
|
||||||
color: #d28a3f;
|
|
||||||
|
|
||||||
> i {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .hide {
|
|
||||||
margin-left: auto;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .info + .article {
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .reply-to {
|
|
||||||
opacity: 0.7;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .renote {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px 32px 8px 32px;
|
|
||||||
line-height: 28px;
|
|
||||||
white-space: pre;
|
|
||||||
color: var(--renote);
|
|
||||||
|
|
||||||
> .avatar {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: inline-block;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
margin: 0 8px 0 0;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> i {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> span {
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 1;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
> .name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .info {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 0.9em;
|
|
||||||
|
|
||||||
> .time {
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: inherit;
|
|
||||||
|
|
||||||
> .dropdownIcon {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .renote + .article {
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .article {
|
|
||||||
display: flex;
|
|
||||||
padding: 28px 32px 18px;
|
|
||||||
|
|
||||||
> .avatar {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: block;
|
|
||||||
margin: 0 14px 8px 0;
|
|
||||||
width: 58px;
|
|
||||||
height: 58px;
|
|
||||||
position: sticky;
|
|
||||||
top: calc(22px + var(--stickyTop, 0px));
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .main {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
> .body {
|
|
||||||
> .cw {
|
|
||||||
cursor: default;
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
|
|
||||||
> .text {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .content {
|
|
||||||
&.isLong {
|
|
||||||
> .showLess {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 1em;
|
|
||||||
position: sticky;
|
|
||||||
bottom: 1em;
|
|
||||||
|
|
||||||
> span {
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--popup);
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
border-radius: 999px;
|
|
||||||
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.collapsed {
|
|
||||||
position: relative;
|
|
||||||
max-height: 9em;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
> .fade {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 64px;
|
|
||||||
background: linear-gradient(0deg, var(--panel), var(--X15));
|
|
||||||
|
|
||||||
> span {
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--panel);
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
border-radius: 999px;
|
|
||||||
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
> span {
|
|
||||||
background: var(--panelHighlight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .text {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
|
|
||||||
> .reply {
|
|
||||||
color: var(--accent);
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .rp {
|
|
||||||
margin-left: 4px;
|
|
||||||
font-style: oblique;
|
|
||||||
color: var(--renote);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .translation {
|
|
||||||
border: solid 0.5px var(--divider);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 12px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .url-preview {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .poll {
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .renote {
|
|
||||||
padding: 8px 0;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
padding: 16px;
|
|
||||||
border: dashed 1px var(--renote);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .channel {
|
|
||||||
opacity: 0.7;
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .footer {
|
|
||||||
> .button {
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px;
|
|
||||||
opacity: 0.7;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--fgHighlighted);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .count {
|
|
||||||
display: inline;
|
|
||||||
margin: 0 0 0 8px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.reacted {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .reply {
|
|
||||||
border-top: solid 0.5px var(--divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.max-width_500px {
|
|
||||||
font-size: 0.9em;
|
|
||||||
|
|
||||||
> .article {
|
|
||||||
> .avatar {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.max-width_450px {
|
|
||||||
> .renote {
|
|
||||||
padding: 8px 16px 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .info {
|
|
||||||
padding: 8px 16px 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .article {
|
|
||||||
padding: 14px 16px 9px;
|
|
||||||
|
|
||||||
> .avatar {
|
|
||||||
margin: 0 10px 8px 0;
|
|
||||||
width: 46px;
|
|
||||||
height: 46px;
|
|
||||||
top: calc(14px + var(--stickyTop, 0px));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.max-width_350px {
|
|
||||||
> .article {
|
|
||||||
> .main {
|
|
||||||
> .footer {
|
|
||||||
> .button {
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.max-width_300px {
|
|
||||||
> .article {
|
|
||||||
> .avatar {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .main {
|
|
||||||
> .footer {
|
|
||||||
> .button {
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.muted {
|
|
||||||
padding: 8px;
|
|
||||||
text-align: center;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selecting {
|
|
||||||
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,25 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="jmgmzlwq _block blur"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a class="link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
href: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.jmgmzlwq {
|
|
||||||
font-size: 0.8em;
|
|
||||||
padding: 16px;
|
|
||||||
background: var(--infoWarnBg);
|
|
||||||
color: var(--infoWarnFg);
|
|
||||||
|
|
||||||
> .link {
|
|
||||||
margin-left: 4px;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,73 +0,0 @@
|
|||||||
<template>
|
|
||||||
<img v-if="customEmoji" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt" decoding="async"/>
|
|
||||||
<img v-else-if="char && !useOsNativeEmojis" class="mk-emoji" :src="url" :alt="alt" :title="alt" decoding="async"/>
|
|
||||||
<span v-else-if="char && useOsNativeEmojis">{{ char }}</span>
|
|
||||||
<span v-else>{{ emoji }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { CustomEmoji } from 'misskey-js/built/entities';
|
|
||||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
|
||||||
import { char2filePath } from '@/scripts/twemoji-base';
|
|
||||||
import { defaultStore } from '@/store';
|
|
||||||
import { instance } from '@/instance';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
emoji: string;
|
|
||||||
normal?: boolean;
|
|
||||||
noStyle?: boolean;
|
|
||||||
customEmojis?: CustomEmoji[];
|
|
||||||
isReaction?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const isCustom = computed(() => props.emoji.startsWith(':'));
|
|
||||||
const char = computed(() => isCustom.value ? null : props.emoji);
|
|
||||||
const useOsNativeEmojis = computed(() => defaultStore.state.useOsNativeEmojis && !props.isReaction);
|
|
||||||
const ce = computed(() => props.customEmojis ?? instance.emojis ?? []);
|
|
||||||
const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : null);
|
|
||||||
const imageSrc = defaultStore.state.disableShowingAnimatedImages
|
|
||||||
? getStaticImageUrl(customEmoji.value?.url)
|
|
||||||
: customEmoji.value?.url;
|
|
||||||
const url = char.value ?
|
|
||||||
char2filePath(char.value)
|
|
||||||
:
|
|
||||||
defaultStore.state.mediaProxy ?
|
|
||||||
defaultStore.state.mediaProxy + '?url=' + imageSrc
|
|
||||||
:
|
|
||||||
imageSrc;
|
|
||||||
console.log(imageSrc);
|
|
||||||
console.log(url);
|
|
||||||
|
|
||||||
const alt = computed(() => customEmoji.value ? `:${customEmoji.value.name}:` : char.value);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.mk-emoji {
|
|
||||||
height: 1.25em;
|
|
||||||
vertical-align: -0.25em;
|
|
||||||
|
|
||||||
&.custom {
|
|
||||||
height: 2.5em;
|
|
||||||
vertical-align: middle;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.normal {
|
|
||||||
height: 1.25em;
|
|
||||||
vertical-align: -0.25em;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.noStyle {
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,368 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="show" ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick">
|
|
||||||
<div v-if="narrow" class="buttons left">
|
|
||||||
<MkAvatar v-if="props.displayMyAvatar && $i" class="avatar" :user="$i" :disable-preview="true"/>
|
|
||||||
</div>
|
|
||||||
<template v-if="metadata">
|
|
||||||
<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup">
|
|
||||||
<MkAvatar v-if="metadata.avatar" class="avatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/>
|
|
||||||
<i v-else-if="metadata.icon" class="icon" :class="metadata.icon"></i>
|
|
||||||
|
|
||||||
<div class="title">
|
|
||||||
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true" class="title"/>
|
|
||||||
<div v-else-if="metadata.title" class="title">{{ metadata.title }}</div>
|
|
||||||
<div v-if="!narrow && metadata.subtitle" class="subtitle">
|
|
||||||
{{ metadata.subtitle }}
|
|
||||||
</div>
|
|
||||||
<div v-if="narrow && hasTabs" class="subtitle activeTab">
|
|
||||||
{{ tabs.find(tab => tab.key === props.tab)?.title }}
|
|
||||||
<i class="chevron fas fa-chevron-down"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!narrow || hideTitle" class="tabs">
|
|
||||||
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
|
|
||||||
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
|
|
||||||
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
|
|
||||||
</button>
|
|
||||||
<div ref="tabHighlightEl" class="highlight"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="buttons right">
|
|
||||||
<button v-tooltip.noDelay="i18n.ts.reload" class="_button button" onclick="location.reload();">
|
|
||||||
<i class="fa-solid fa-arrow-rotate-right"></i>
|
|
||||||
</button>
|
|
||||||
<template v-for="action in actions">
|
|
||||||
<button v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, onUnmounted, ref, inject, watch, shallowReactive, nextTick, reactive } from 'vue';
|
|
||||||
import tinycolor from 'tinycolor2';
|
|
||||||
import { popupMenu } from '@/os';
|
|
||||||
import { scrollToTop } from '@/scripts/scroll';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
import { globalEvents } from '@/events';
|
|
||||||
import { injectPageMetadata } from '@/scripts/page-metadata';
|
|
||||||
import { $i } from '@/account';
|
|
||||||
|
|
||||||
type Tab = {
|
|
||||||
key?: string | null;
|
|
||||||
title: string;
|
|
||||||
icon?: string;
|
|
||||||
iconOnly?: boolean;
|
|
||||||
onClick?: (ev: MouseEvent) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
tabs?: Tab[];
|
|
||||||
tab?: string;
|
|
||||||
actions?: {
|
|
||||||
text: string;
|
|
||||||
icon: string;
|
|
||||||
handler: (ev: MouseEvent) => void;
|
|
||||||
}[];
|
|
||||||
thin?: boolean;
|
|
||||||
displayMyAvatar?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: 'update:tab', key: string);
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const metadata = injectPageMetadata();
|
|
||||||
|
|
||||||
const hideTitle = inject('shouldOmitHeaderTitle', false);
|
|
||||||
const thin_ = props.thin || inject('shouldHeaderThin', false);
|
|
||||||
|
|
||||||
const el = $ref<HTMLElement | null>(null);
|
|
||||||
const tabRefs = {};
|
|
||||||
const tabHighlightEl = $ref<HTMLElement | null>(null);
|
|
||||||
const bg = ref(null);
|
|
||||||
let narrow = $ref(false);
|
|
||||||
const height = ref(0);
|
|
||||||
const hasTabs = $computed(() => props.tabs && props.tabs.length > 0);
|
|
||||||
const hasActions = $computed(() => props.actions && props.actions.length > 0);
|
|
||||||
const show = $computed(() => {
|
|
||||||
return !hideTitle || hasTabs || hasActions;
|
|
||||||
});
|
|
||||||
|
|
||||||
const showTabsPopup = (ev: MouseEvent) => {
|
|
||||||
if (!hasTabs) return;
|
|
||||||
if (!narrow) return;
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
const menu = props.tabs.map(tab => ({
|
|
||||||
text: tab.title,
|
|
||||||
icon: tab.icon,
|
|
||||||
active: tab.key != null && tab.key === props.tab,
|
|
||||||
action: (ev) => {
|
|
||||||
onTabClick(tab, ev);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
popupMenu(menu, ev.currentTarget ?? ev.target);
|
|
||||||
};
|
|
||||||
|
|
||||||
const preventDrag = (ev: TouchEvent) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
scrollToTop(el, { behavior: 'smooth' });
|
|
||||||
};
|
|
||||||
|
|
||||||
function onTabMousedown(tab: Tab, ev: MouseEvent): void {
|
|
||||||
// ユーザビリティの観点からmousedown時にはonClickは呼ばない
|
|
||||||
if (tab.key) {
|
|
||||||
emit('update:tab', tab.key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTabClick(tab: Tab, ev: MouseEvent): void {
|
|
||||||
if (tab.onClick) {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
tab.onClick(ev);
|
|
||||||
}
|
|
||||||
if (tab.key) {
|
|
||||||
emit('update:tab', tab.key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const calcBg = () => {
|
|
||||||
const rawBg = metadata?.bg || 'var(--bg)';
|
|
||||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
|
||||||
tinyBg.setAlpha(0.85);
|
|
||||||
bg.value = tinyBg.toRgbString();
|
|
||||||
};
|
|
||||||
|
|
||||||
let ro: ResizeObserver | null;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
calcBg();
|
|
||||||
globalEvents.on('themeChanged', calcBg);
|
|
||||||
|
|
||||||
watch(() => [props.tab, props.tabs], () => {
|
|
||||||
nextTick(() => {
|
|
||||||
const tabEl = tabRefs[props.tab];
|
|
||||||
if (tabEl && tabHighlightEl) {
|
|
||||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
|
||||||
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
|
||||||
const parentRect = tabEl.parentElement.getBoundingClientRect();
|
|
||||||
const rect = tabEl.getBoundingClientRect();
|
|
||||||
tabHighlightEl.style.width = rect.width + 'px';
|
|
||||||
tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, {
|
|
||||||
immediate: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (el && el.parentElement) {
|
|
||||||
narrow = el.parentElement.offsetWidth < 500;
|
|
||||||
ro = new ResizeObserver((entries, observer) => {
|
|
||||||
if (el.parentElement && document.body.contains(el)) {
|
|
||||||
narrow = el.parentElement.offsetWidth < 500;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ro.observe(el.parentElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
globalEvents.off('themeChanged', calcBg);
|
|
||||||
if (ro) ro.disconnect();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.fdidabkb {
|
|
||||||
--height: 55px;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
|
||||||
border-bottom: solid 0.5px var(--divider);
|
|
||||||
contain: strict;
|
|
||||||
height: var(--height);
|
|
||||||
|
|
||||||
&.thin {
|
|
||||||
--height: 45px;
|
|
||||||
|
|
||||||
> .buttons {
|
|
||||||
> .button {
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.slim {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
> .titleContainer {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
> *:first-child {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
> *:last-child {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .buttons {
|
|
||||||
--margin: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-width: var(--height);
|
|
||||||
height: var(--height);
|
|
||||||
margin: 0 var(--margin);
|
|
||||||
|
|
||||||
&.left {
|
|
||||||
margin-right: auto;
|
|
||||||
|
|
||||||
> .avatar {
|
|
||||||
$size: 32px;
|
|
||||||
display: inline-block;
|
|
||||||
width: $size;
|
|
||||||
height: $size;
|
|
||||||
vertical-align: bottom;
|
|
||||||
margin: 0 8px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.right {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
width: var(--height);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: calc(var(--height) - (var(--margin) * 2));
|
|
||||||
width: calc(var(--height) - (var(--margin) * 2));
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: relative;
|
|
||||||
border-radius: 5px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.highlighted {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .fullButton {
|
|
||||||
& + .fullButton {
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .titleContainer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 400px;
|
|
||||||
overflow: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: bold;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-left: 24px;
|
|
||||||
|
|
||||||
> .avatar {
|
|
||||||
$size: 32px;
|
|
||||||
display: inline-block;
|
|
||||||
width: $size;
|
|
||||||
height: $size;
|
|
||||||
vertical-align: bottom;
|
|
||||||
margin: 0 8px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
width: 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .title {
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 1.1;
|
|
||||||
|
|
||||||
> .subtitle {
|
|
||||||
opacity: 0.6;
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: normal;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
&.activeTab {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
> .chevron {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .tabs {
|
|
||||||
position: relative;
|
|
||||||
margin-left: 16px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
overflow: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
> .tab {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
padding: 0 10px;
|
|
||||||
height: 100%;
|
|
||||||
font-weight: normal;
|
|
||||||
opacity: 0.7;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon + .title {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .highlight {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
height: 3px;
|
|
||||||
background: var(--accent);
|
|
||||||
border-radius: 999px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,637 +0,0 @@
|
|||||||
<template>
|
|
||||||
<MkSpacer :content-max="900">
|
|
||||||
<div ref="rootEl" v-size="{ max: [740] }" class="edbbcaef">
|
|
||||||
<div class="left">
|
|
||||||
<div v-if="stats" class="container stats">
|
|
||||||
<div class="title">Stats</div>
|
|
||||||
<div class="body">
|
|
||||||
<div class="number _panel blur">
|
|
||||||
<div class="label">Users</div>
|
|
||||||
<div class="value _monospace">
|
|
||||||
{{ number(stats.originalUsersCount) }}
|
|
||||||
<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="number _panel blur">
|
|
||||||
<div class="label">Notes</div>
|
|
||||||
<div class="value _monospace">
|
|
||||||
{{ number(stats.originalNotesCount) }}
|
|
||||||
<MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container queue">
|
|
||||||
<div class="title">Job queue</div>
|
|
||||||
<div class="body">
|
|
||||||
<div class="chart deliver">
|
|
||||||
<div class="title">Deliver</div>
|
|
||||||
<XQueueChart :connection="queueStatsConnection" domain="deliver"/>
|
|
||||||
</div>
|
|
||||||
<div class="chart inbox">
|
|
||||||
<div class="title">Inbox</div>
|
|
||||||
<XQueueChart :connection="queueStatsConnection" domain="inbox"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container users">
|
|
||||||
<div class="title">New users</div>
|
|
||||||
<div v-if="newUsers" class="body">
|
|
||||||
<XUser v-for="user in newUsers" :key="user.id" class="user" :user="user"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container files">
|
|
||||||
<div class="title">Recent files</div>
|
|
||||||
<div class="body">
|
|
||||||
<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container env">
|
|
||||||
<div class="title">Enviroment</div>
|
|
||||||
<div class="body">
|
|
||||||
<div class="number _panel blur">
|
|
||||||
<div class="label">Misskey</div>
|
|
||||||
<div class="value _monospace">{{ version }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="serverInfo" class="number _panel blur">
|
|
||||||
<div class="label">Node.js</div>
|
|
||||||
<div class="value _monospace">{{ serverInfo.node }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="serverInfo" class="number _panel blur">
|
|
||||||
<div class="label">PostgreSQL</div>
|
|
||||||
<div class="value _monospace">{{ serverInfo.psql }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="serverInfo" class="number _panel blur">
|
|
||||||
<div class="label">Redis</div>
|
|
||||||
<div class="value _monospace">{{ serverInfo.redis }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="number _panel blur">
|
|
||||||
<div class="label">Vue</div>
|
|
||||||
<div class="value _monospace">{{ vueVersion }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="right">
|
|
||||||
<div class="container charts">
|
|
||||||
<div class="title">Active users</div>
|
|
||||||
<div class="body">
|
|
||||||
<canvas ref="chartEl"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container federation">
|
|
||||||
<div class="title">Active instances</div>
|
|
||||||
<div class="body">
|
|
||||||
<XFederation/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="stats" class="container federationStats">
|
|
||||||
<div class="title">Federation</div>
|
|
||||||
<div class="body">
|
|
||||||
<div class="number _panel blur">
|
|
||||||
<div class="label">Sub</div>
|
|
||||||
<div class="value _monospace">
|
|
||||||
{{ number(federationSubActive) }}
|
|
||||||
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="number _panel blur">
|
|
||||||
<div class="label">Pub</div>
|
|
||||||
<div class="value _monospace">
|
|
||||||
{{ number(federationPubActive) }}
|
|
||||||
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container tagCloud">
|
|
||||||
<div class="body">
|
|
||||||
<MkTagCloud v-if="activeInstances">
|
|
||||||
<li v-for="instance in activeInstances">
|
|
||||||
<a @click.prevent="onInstanceClick(instance)">
|
|
||||||
<img style="width: 32px;" :src="instance.iconUrl">
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</MkTagCloud>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="topSubInstancesForPie && topPubInstancesForPie" class="container federationPies">
|
|
||||||
<div class="body">
|
|
||||||
<div class="chart deliver">
|
|
||||||
<div class="title">Sub</div>
|
|
||||||
<XPie :data="topSubInstancesForPie"/>
|
|
||||||
<div class="subTitle">Top 10</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart inbox">
|
|
||||||
<div class="title">Pub</div>
|
|
||||||
<XPie :data="topPubInstancesForPie"/>
|
|
||||||
<div class="subTitle">Top 10</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MkSpacer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
|
||||||
import {
|
|
||||||
Chart,
|
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
} from 'chart.js';
|
|
||||||
import { enUS } from 'date-fns/locale';
|
|
||||||
import tinycolor from 'tinycolor2';
|
|
||||||
import MagicGrid from 'magic-grid';
|
|
||||||
import XMetrics from './metrics.vue';
|
|
||||||
import XFederation from './overview.federation.vue';
|
|
||||||
import XQueueChart from './overview.queue-chart.vue';
|
|
||||||
import XUser from './overview.user.vue';
|
|
||||||
import XPie from './overview.pie.vue';
|
|
||||||
import MkNumberDiff from '@/components/MkNumberDiff.vue';
|
|
||||||
import MkTagCloud from '@/components/MkTagCloud.vue';
|
|
||||||
import { version, url } from '@/config';
|
|
||||||
import number from '@/filters/number';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { stream } from '@/stream';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
|
||||||
import 'chartjs-adapter-date-fns';
|
|
||||||
import { defaultStore } from '@/store';
|
|
||||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
|
||||||
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
|
||||||
|
|
||||||
Chart.register(
|
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
//gradient,
|
|
||||||
);
|
|
||||||
|
|
||||||
const rootEl = $ref<HTMLElement>();
|
|
||||||
const chartEl = $ref<HTMLCanvasElement>(null);
|
|
||||||
let stats: any = $ref(null);
|
|
||||||
let serverInfo: any = $ref(null);
|
|
||||||
let topSubInstancesForPie: any = $ref(null);
|
|
||||||
let topPubInstancesForPie: any = $ref(null);
|
|
||||||
let usersComparedToThePrevDay: any = $ref(null);
|
|
||||||
let notesComparedToThePrevDay: any = $ref(null);
|
|
||||||
let federationPubActive = $ref<number | null>(null);
|
|
||||||
let federationPubActiveDiff = $ref<number | null>(null);
|
|
||||||
let federationSubActive = $ref<number | null>(null);
|
|
||||||
let federationSubActiveDiff = $ref<number | null>(null);
|
|
||||||
let newUsers = $ref(null);
|
|
||||||
let activeInstances = $shallowRef(null);
|
|
||||||
const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
|
|
||||||
const now = new Date();
|
|
||||||
let chartInstance: Chart = null;
|
|
||||||
const chartLimit = 30;
|
|
||||||
const filesPagination = {
|
|
||||||
endpoint: 'admin/drive/files' as const,
|
|
||||||
limit: 9,
|
|
||||||
noPaging: 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/active-users', { limit: chartLimit, span: 'day' });
|
|
||||||
|
|
||||||
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
|
||||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
|
||||||
|
|
||||||
// フォントカラー
|
|
||||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
|
||||||
|
|
||||||
const color = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
|
|
||||||
|
|
||||||
chartInstance = new Chart(chartEl, {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
//labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
|
|
||||||
datasets: [{
|
|
||||||
parsing: false,
|
|
||||||
label: 'a',
|
|
||||||
data: format(raw.readWrite).slice().reverse(),
|
|
||||||
tension: 0.3,
|
|
||||||
pointRadius: 0,
|
|
||||||
borderWidth: 0,
|
|
||||||
borderJoinStyle: 'round',
|
|
||||||
borderRadius: 3,
|
|
||||||
backgroundColor: color,
|
|
||||||
/*gradient: props.bar ? undefined : {
|
|
||||||
backgroundColor: {
|
|
||||||
axis: 'y',
|
|
||||||
colors: {
|
|
||||||
0: alpha(x.color ? x.color : getColor(i), 0),
|
|
||||||
[maxes[i]]: alpha(x.color ? x.color : getColor(i), 0.2),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},*/
|
|
||||||
barPercentage: 0.9,
|
|
||||||
categoryPercentage: 0.9,
|
|
||||||
clip: 8,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
aspectRatio: 2.5,
|
|
||||||
layout: {
|
|
||||||
padding: {
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time',
|
|
||||||
display: false,
|
|
||||||
stacked: true,
|
|
||||||
offset: false,
|
|
||||||
time: {
|
|
||||||
stepSize: 1,
|
|
||||||
unit: 'month',
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
adapters: {
|
|
||||||
date: {
|
|
||||||
locale: enUS,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
min: getDate(chartLimit).getTime(),
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
display: false,
|
|
||||||
position: 'left',
|
|
||||||
stacked: true,
|
|
||||||
grid: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
display: false,
|
|
||||||
//mirror: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
interaction: {
|
|
||||||
intersect: false,
|
|
||||||
mode: 'index',
|
|
||||||
},
|
|
||||||
elements: {
|
|
||||||
point: {
|
|
||||||
hoverRadius: 5,
|
|
||||||
hoverBorderWidth: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animation: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
enabled: false,
|
|
||||||
mode: 'index',
|
|
||||||
animation: {
|
|
||||||
duration: 0,
|
|
||||||
},
|
|
||||||
external: externalTooltipHandler,
|
|
||||||
},
|
|
||||||
//gradient,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [{
|
|
||||||
id: 'vLine',
|
|
||||||
beforeDraw(chart, args, options) {
|
|
||||||
if (chart.tooltip?._active?.length) {
|
|
||||||
const activePoint = chart.tooltip._active[0];
|
|
||||||
const ctx = chart.ctx;
|
|
||||||
const x = activePoint.element.x;
|
|
||||||
const topY = chart.scales.y.top;
|
|
||||||
const bottomY = chart.scales.y.bottom;
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, bottomY);
|
|
||||||
ctx.lineTo(x, topY);
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.strokeStyle = vLineColor;
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onInstanceClick(i) {
|
|
||||||
os.pageWindow(`/instance-info/${i.host}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
/*
|
|
||||||
const magicGrid = new MagicGrid({
|
|
||||||
container: rootEl,
|
|
||||||
static: true,
|
|
||||||
animate: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
magicGrid.listen();
|
|
||||||
*/
|
|
||||||
|
|
||||||
renderChart();
|
|
||||||
|
|
||||||
os.api('stats', {}).then(statsResponse => {
|
|
||||||
stats = statsResponse;
|
|
||||||
|
|
||||||
os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
|
|
||||||
usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1];
|
|
||||||
});
|
|
||||||
|
|
||||||
os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
|
|
||||||
notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
os.apiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => {
|
|
||||||
federationPubActive = chart.pubActive[0];
|
|
||||||
federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1];
|
|
||||||
federationSubActive = chart.subActive[0];
|
|
||||||
federationSubActiveDiff = chart.subActive[0] - chart.subActive[1];
|
|
||||||
});
|
|
||||||
|
|
||||||
os.apiGet('federation/stats', { limit: 10 }).then(res => {
|
|
||||||
topSubInstancesForPie = res.topSubInstances.map(x => ({
|
|
||||||
name: x.host,
|
|
||||||
color: x.themeColor,
|
|
||||||
value: x.followersCount,
|
|
||||||
onClick: () => {
|
|
||||||
os.pageWindow(`/instance-info/${x.host}`);
|
|
||||||
},
|
|
||||||
})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]);
|
|
||||||
topPubInstancesForPie = res.topPubInstances.map(x => ({
|
|
||||||
name: x.host,
|
|
||||||
color: x.themeColor,
|
|
||||||
value: x.followingCount,
|
|
||||||
onClick: () => {
|
|
||||||
os.pageWindow(`/instance-info/${x.host}`);
|
|
||||||
},
|
|
||||||
})).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowingCount }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
os.api('admin/server-info').then(serverInfoResponse => {
|
|
||||||
serverInfo = serverInfoResponse;
|
|
||||||
});
|
|
||||||
|
|
||||||
os.api('admin/show-users', {
|
|
||||||
limit: 5,
|
|
||||||
sort: '+createdAt',
|
|
||||||
}).then(res => {
|
|
||||||
newUsers = res;
|
|
||||||
});
|
|
||||||
|
|
||||||
os.api('federation/instances', {
|
|
||||||
sort: '+lastCommunicatedAt',
|
|
||||||
limit: 25,
|
|
||||||
}).then(res => {
|
|
||||||
activeInstances = res;
|
|
||||||
});
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
queueStatsConnection.send('requestLog', {
|
|
||||||
id: Math.random().toString().substr(2, 8),
|
|
||||||
length: 100,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
queueStatsConnection.dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
|
||||||
|
|
||||||
definePageMetadata({
|
|
||||||
title: i18n.ts.dashboard,
|
|
||||||
icon: 'fas fa-tachometer-alt',
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.edbbcaef {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
> .left, > .right {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 50%;
|
|
||||||
|
|
||||||
> .container {
|
|
||||||
margin: 32px 0;
|
|
||||||
|
|
||||||
> .title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.stats, &.federationStats {
|
|
||||||
> .body {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: 16px;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
|
|
||||||
> .number {
|
|
||||||
padding: 14px 20px;
|
|
||||||
|
|
||||||
> .label {
|
|
||||||
opacity: 0.7;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .value {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.5em;
|
|
||||||
|
|
||||||
> .diff {
|
|
||||||
font-size: 0.7em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.env {
|
|
||||||
> .body {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: 16px;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
|
|
||||||
> .number {
|
|
||||||
padding: 14px 20px;
|
|
||||||
|
|
||||||
> .label {
|
|
||||||
opacity: 0.7;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .value {
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.charts {
|
|
||||||
> .body {
|
|
||||||
padding: 32px;
|
|
||||||
background: var(--panel);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.users {
|
|
||||||
> .body {
|
|
||||||
background: var(--panel);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
|
|
||||||
> .user {
|
|
||||||
padding: 16px 20px;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
border-bottom: solid 0.5px var(--divider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.federation {
|
|
||||||
> .body {
|
|
||||||
background: var(--panel);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
overflow: clip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.queue {
|
|
||||||
> .body {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: 16px;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
|
|
||||||
> .chart {
|
|
||||||
position: relative;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--panel);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
|
|
||||||
> .title {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: 20px;
|
|
||||||
font-size: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.federationPies {
|
|
||||||
> .body {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: 16px;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
|
|
||||||
> .chart {
|
|
||||||
position: relative;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--panel);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
|
|
||||||
> .title {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: 20px;
|
|
||||||
font-size: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .subTitle {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 20px;
|
|
||||||
right: 20px;
|
|
||||||
font-size: 85%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.tagCloud {
|
|
||||||
> .body {
|
|
||||||
background: var(--panel);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
overflow: clip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .left {
|
|
||||||
padding-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .right {
|
|
||||||
padding-left: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,137 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="iltifgqe">
|
|
||||||
<div class="editor _panel blur _gap">
|
|
||||||
<PrismEditor v-model="code" class="_code code" :highlight="highlighter" :line-numbers="false"/>
|
|
||||||
<MkButton style="position: absolute; top: 8px; right: 8px;" primary @click="run()"><i class="fas fa-play"></i></MkButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MkContainer :foldable="true" class="_gap">
|
|
||||||
<template #header>{{ i18n.ts.output }}</template>
|
|
||||||
<div class="bepmlvbi">
|
|
||||||
<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
|
|
||||||
</div>
|
|
||||||
</MkContainer>
|
|
||||||
|
|
||||||
<div class="_gap">
|
|
||||||
{{ i18n.ts.scratchpadDescription }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
import 'prismjs';
|
|
||||||
import { highlight, languages } from 'prismjs/components/prism-core';
|
|
||||||
import 'prismjs/components/prism-clike';
|
|
||||||
import 'prismjs/components/prism-javascript';
|
|
||||||
import 'prismjs/themes/prism-okaidia.css';
|
|
||||||
import { PrismEditor } from 'vue-prism-editor';
|
|
||||||
import 'vue-prism-editor/dist/prismeditor.min.css';
|
|
||||||
import { AiScript, parse, utils } from '@syuilo/aiscript';
|
|
||||||
import MkContainer from '@/components/MkContainer.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
|
||||||
import { createAiScriptEnv } from '@/scripts/aiscript/api';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { $i } from '@/account';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
|
||||||
|
|
||||||
const code = ref('');
|
|
||||||
const logs = ref<any[]>([]);
|
|
||||||
|
|
||||||
const saved = localStorage.getItem('scratchpad');
|
|
||||||
if (saved) {
|
|
||||||
code.value = saved;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(code, () => {
|
|
||||||
localStorage.setItem('scratchpad', code.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
logs.value = [];
|
|
||||||
const aiscript = new AiScript(createAiScriptEnv({
|
|
||||||
storageKey: 'scratchpad',
|
|
||||||
token: $i?.token,
|
|
||||||
}), {
|
|
||||||
in: (q) => {
|
|
||||||
return new Promise(ok => {
|
|
||||||
os.inputText({
|
|
||||||
title: q,
|
|
||||||
}).then(({ canceled, result: a }) => {
|
|
||||||
ok(a);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
out: (value) => {
|
|
||||||
logs.value.push({
|
|
||||||
id: Math.random(),
|
|
||||||
text: value.type === 'str' ? value.value : utils.valToString(value),
|
|
||||||
print: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
log: (type, params) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'end': logs.value.push({
|
|
||||||
id: Math.random(),
|
|
||||||
text: utils.valToString(params.val, true),
|
|
||||||
print: false,
|
|
||||||
}); break;
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let ast;
|
|
||||||
try {
|
|
||||||
ast = parse(code.value);
|
|
||||||
} catch (error) {
|
|
||||||
os.alert({
|
|
||||||
type: 'error',
|
|
||||||
text: 'Syntax error :(',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await aiscript.exec(ast);
|
|
||||||
} catch (error: any) {
|
|
||||||
os.alert({
|
|
||||||
type: 'error',
|
|
||||||
text: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function highlighter(code) {
|
|
||||||
return highlight(code, languages.js, 'javascript');
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
|
||||||
|
|
||||||
definePageMetadata({
|
|
||||||
title: i18n.ts.scratchpad,
|
|
||||||
icon: 'fas fa-terminal',
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.iltifgqe {
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
> .editor {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bepmlvbi {
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
> .log {
|
|
||||||
&:not(.print) {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,226 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="_card tbkwesmv">
|
|
||||||
<div class="_title"><i class="fas fa-info-circle"></i> {{ i18n.ts._tutorial.title }}</div>
|
|
||||||
<div v-if="tutorial === 0" class="_content">
|
|
||||||
<div>{{ i18n.ts._tutorial.step1_1 }}</div>
|
|
||||||
<div>{{ i18n.ts._tutorial.step1_2 }}</div>
|
|
||||||
<div>{{ i18n.ts._tutorial.step1_3 }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="tutorial === 1" class="_content">
|
|
||||||
<div>{{ i18n.ts._tutorial.step2_1 }}</div>
|
|
||||||
<div>{{ i18n.ts._tutorial.step2_2 }}</div>
|
|
||||||
<MkA class="_link" to="/settings/profile">{{ i18n.ts.editProfile }}</MkA>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="tutorial === 2" class="_content">
|
|
||||||
<div>{{ i18n.ts._tutorial.step3_1 }}</div>
|
|
||||||
<div>{{ i18n.ts._tutorial.step3_2 }}</div>
|
|
||||||
<div>{{ i18n.ts._tutorial.step3_3 }}</div>
|
|
||||||
<small>{{ i18n.ts._tutorial.step3_4 }}</small>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="tutorial === 3" class="_content">
|
|
||||||
<div>{{ i18n.ts._tutorial.step4_1 }}</div>
|
|
||||||
<div>{{ i18n.ts._tutorial.step4_2 }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="tutorial === 4" class="_content">
|
|
||||||
<div>{{ i18n.ts._tutorial.step5_1 }}</div>
|
|
||||||
<I18n :src="i18n.ts._tutorial.step5_2" tag="div">
|
|
||||||
<template #featured>
|
|
||||||
<MkA class="_link" to="/featured">{{ i18n.ts.featured }}</MkA>
|
|
||||||
</template>
|
|
||||||
<template #explore>
|
|
||||||
<MkA class="_link" to="/explore">{{ i18n.ts.explore }}</MkA>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<div>{{ i18n.ts._tutorial.step5_3 }}</div>
|
|
||||||
<small>{{ i18n.ts._tutorial.step5_4 }}</small>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="tutorial === 5" class="_content">
|
|
||||||
<div>{{ i18n.ts._tutorial.step6_1 }}</div>
|
|
||||||
<div>{{ i18n.ts._tutorial.step6_2 }}</div>
|
|
||||||
<div>{{ i18n.ts._tutorial.step6_3 }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="tutorial === 6" class="_content">
|
|
||||||
<div>{{ i18n.ts._tutorial.step7_1 }}</div>
|
|
||||||
<I18n :src="i18n.ts._tutorial.step7_2" tag="div">
|
|
||||||
<template #help>
|
|
||||||
<a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
|
|
||||||
</template>
|
|
||||||
</I18n>
|
|
||||||
<div>{{ i18n.ts._tutorial.step7_3 }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="tutorial === 7" class="_content">
|
|
||||||
<div>{{ $ts._tutorial.step8_1 }}</div>
|
|
||||||
<div>{{ $ts._tutorial.step8_2 }}</div>
|
|
||||||
<div>{{ $ts._tutorial.step8_3 }}</div>
|
|
||||||
<div>{{ $ts._tutorial.step8_4 }}</div>
|
|
||||||
<FormSwitch v-model="useBlurEffect" class="_formBlock">{{ i18n.ts.useBlurEffect }}</FormSwitch>
|
|
||||||
<FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{ i18n.ts.useBlurEffectForModal }}</FormSwitch>
|
|
||||||
<template v-if="darkMode">
|
|
||||||
<FormSelect v-model="darkThemeId" class="_formBlock">
|
|
||||||
<template #label>{{ $ts.themeForDarkMode }}</template>
|
|
||||||
<template #prefix><i class="fas fa-moon"></i></template>
|
|
||||||
<optgroup :label="$ts.darkThemes">
|
|
||||||
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup :label="$ts.lightThemes">
|
|
||||||
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
|
||||||
</optgroup>
|
|
||||||
</FormSelect>
|
|
||||||
<FormSelect v-model="lightThemeId" class="_formBlock">
|
|
||||||
<template #label>{{ $ts.themeForLightMode }}</template>
|
|
||||||
<template #prefix><i class="fas fa-sun"></i></template>
|
|
||||||
<optgroup :label="$ts.lightThemes">
|
|
||||||
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup :label="$ts.darkThemes">
|
|
||||||
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
|
||||||
</optgroup>
|
|
||||||
</FormSelect>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<FormSelect v-model="lightThemeId" class="_formBlock">
|
|
||||||
<template #label>{{ $ts.themeForLightMode }}</template>
|
|
||||||
<template #prefix><i class="fas fa-sun"></i></template>
|
|
||||||
<optgroup :label="$ts.lightThemes">
|
|
||||||
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup :label="$ts.darkThemes">
|
|
||||||
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
|
||||||
</optgroup>
|
|
||||||
</FormSelect>
|
|
||||||
<FormSelect v-model="darkThemeId" class="_formBlock">
|
|
||||||
<template #label>{{ $ts.themeForDarkMode }}</template>
|
|
||||||
<template #prefix><i class="fas fa-moon"></i></template>
|
|
||||||
<optgroup :label="$ts.darkThemes">
|
|
||||||
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup :label="$ts.lightThemes">
|
|
||||||
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
|
||||||
</optgroup>
|
|
||||||
</FormSelect>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="_footer navigation">
|
|
||||||
<div class="step">
|
|
||||||
<button class="arrow _button" :disabled="tutorial === 0" @click="tutorial--">
|
|
||||||
<i class="fas fa-chevron-left"></i>
|
|
||||||
</button>
|
|
||||||
<span>{{ tutorial + 1 }} / 8</span>
|
|
||||||
<button class="arrow _button" :disabled="tutorial === 7" @click="tutorial++">
|
|
||||||
<i class="fas fa-chevron-right"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<MkButton v-if="tutorial === 7" class="ok" primary @click="tutorial = -1"><i class="fas fa-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
|
||||||
<MkButton v-else class="ok" primary @click="tutorial++"><i class="fas fa-check"></i> {{ i18n.ts.next }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onActivated, ref, watch } from 'vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
import { defaultStore, ColdDeviceStorage } from '@/store';
|
|
||||||
import FormSwitch from '@/components/form/switch.vue';
|
|
||||||
import FormSelect from '@/components/form/select.vue';
|
|
||||||
import { fetchThemes, getThemes } from '@/theme-store';
|
|
||||||
import { getBuiltinThemesRef } from '@/scripts/theme';
|
|
||||||
import { uniqueBy } from '@/scripts/array';
|
|
||||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
|
||||||
|
|
||||||
const tutorial = computed({
|
|
||||||
get() { return defaultStore.reactiveState.tutorial.value || 0; },
|
|
||||||
set(value) { defaultStore.set('tutorial', value); },
|
|
||||||
});
|
|
||||||
|
|
||||||
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
|
||||||
const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
|
|
||||||
|
|
||||||
const installedThemes = ref(getThemes());
|
|
||||||
const builtinThemes = getBuiltinThemesRef();
|
|
||||||
const instanceThemes = [];
|
|
||||||
|
|
||||||
const themes = computed(() => uniqueBy([...instanceThemes, ...builtinThemes.value, ...installedThemes.value], theme => theme.id));
|
|
||||||
const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
|
|
||||||
const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light'));
|
|
||||||
const darkTheme = ColdDeviceStorage.ref('darkTheme');
|
|
||||||
const darkThemeId = computed({
|
|
||||||
get() {
|
|
||||||
return darkTheme.value.id;
|
|
||||||
},
|
|
||||||
set(id) {
|
|
||||||
ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const lightTheme = ColdDeviceStorage.ref('lightTheme');
|
|
||||||
const lightThemeId = computed({
|
|
||||||
get() {
|
|
||||||
return lightTheme.value.id;
|
|
||||||
},
|
|
||||||
set(id) {
|
|
||||||
ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
|
||||||
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
|
||||||
|
|
||||||
watch(syncDeviceDarkMode, () => {
|
|
||||||
if (syncDeviceDarkMode.value) {
|
|
||||||
defaultStore.set('darkMode', isDeviceDarkmode());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
fetchThemes().then(() => {
|
|
||||||
installedThemes.value = getThemes();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchThemes().then(() => {
|
|
||||||
installedThemes.value = getThemes();
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.tbkwesmv {
|
|
||||||
> ._content {
|
|
||||||
> small {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .navigation {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: baseline;
|
|
||||||
|
|
||||||
> .step {
|
|
||||||
> .arrow {
|
|
||||||
padding: 4px;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
padding-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
padding-left: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> span {
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .ok {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,32 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
|
|
||||||
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel blur _gap">
|
|
||||||
<b>{{ item.name }}</b>
|
|
||||||
<div v-if="item.description" class="description">{{ item.description }}</div>
|
|
||||||
</MkA>
|
|
||||||
</MkPagination>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import * as misskey from 'misskey-js';
|
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
user: misskey.entities.User;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const pagination = {
|
|
||||||
endpoint: 'users/clips' as const,
|
|
||||||
limit: 20,
|
|
||||||
params: computed(() => ({
|
|
||||||
userId: props.user.id,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
@ -1,437 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="mk-deck" :class="[{ isMobile }]"
|
|
||||||
>
|
|
||||||
<XSidebar v-if="!isMobile"/>
|
|
||||||
|
|
||||||
<div class="main">
|
|
||||||
<XStatusBars class="statusbars"/>
|
|
||||||
<div ref="columnsEl" class="columns" :class="deckStore.reactiveState.columnAlign.value" @contextmenu.self.prevent="onContextmenu">
|
|
||||||
<template v-for="ids in layout">
|
|
||||||
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
|
|
||||||
<section
|
|
||||||
v-if="ids.length > 1"
|
|
||||||
class="folder column"
|
|
||||||
:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
|
|
||||||
>
|
|
||||||
<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
|
|
||||||
</section>
|
|
||||||
<DeckColumnCore
|
|
||||||
v-else
|
|
||||||
:ref="ids[0]"
|
|
||||||
:key="ids[0]"
|
|
||||||
class="column"
|
|
||||||
:column="columns.find(c => c.id === ids[0])"
|
|
||||||
:is-stacked="false"
|
|
||||||
:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
|
|
||||||
@parent-focus="moveFocus(ids[0], $event)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<div v-if="layout.length === 0" class="intro _panel blur">
|
|
||||||
<div>{{ i18n.ts._deck.introduction }}</div>
|
|
||||||
<MkButton primary class="add" @click="addColumn">{{ i18n.ts._deck.addColumn }}</MkButton>
|
|
||||||
<div>{{ i18n.ts._deck.introduction2 }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="sideMenu">
|
|
||||||
<div class="top">
|
|
||||||
<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" class="_button button" @click="changeProfile"><i class="fas fa-caret-down"></i></button>
|
|
||||||
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" class="_button button" @click="deleteProfile"><i class="fas fa-trash-can"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="middle">
|
|
||||||
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" class="_button button" @click="addColumn"><i class="fas fa-plus"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="bottom">
|
|
||||||
<button v-tooltip.noDelay.left="i18n.ts.settings" class="_button button settings" @click="showSettings"><i class="fas fa-cog"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="isMobile" class="buttons">
|
|
||||||
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
|
|
||||||
<button class="button home _button" @click="mainRouter.push('/')"><i class="fas fa-home"></i></button>
|
|
||||||
<button class="button notifications _button" @click="mainRouter.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
|
|
||||||
<button class="button post _button" @click="os.post()"><i class="fas fa-pencil-alt"></i></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<transition :name="$store.state.animation ? 'menu-back' : ''">
|
|
||||||
<div
|
|
||||||
v-if="drawerMenuShowing"
|
|
||||||
class="menu-back _modalBg"
|
|
||||||
@click="drawerMenuShowing = false"
|
|
||||||
@touchstart.passive="drawerMenuShowing = false"
|
|
||||||
></div>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<transition :name="$store.state.animation ? 'menu' : ''">
|
|
||||||
<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<XCommon/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, defineAsyncComponent, onMounted, provide, ref, watch } from 'vue';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import XCommon from './_common_/common.vue';
|
|
||||||
import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store';
|
|
||||||
import DeckColumnCore from '@/ui/deck/column-core.vue';
|
|
||||||
import XSidebar from '@/ui/_common_/navbar.vue';
|
|
||||||
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
|
||||||
import { getScrollContainer } from '@/scripts/scroll';
|
|
||||||
import * as os from '@/os';
|
|
||||||
import { navbarItemDef } from '@/navbar';
|
|
||||||
import { $i } from '@/account';
|
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
import { mainRouter } from '@/router';
|
|
||||||
import { unisonReload } from '@/scripts/unison-reload';
|
|
||||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
|
||||||
|
|
||||||
mainRouter.navHook = (path, flag): boolean => {
|
|
||||||
if (flag === 'forcePage') return false;
|
|
||||||
const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main');
|
|
||||||
if (deckStore.state.navWindow || noMainColumn) {
|
|
||||||
os.pageWindow(path);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isMobile = ref(window.innerWidth <= 500);
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
isMobile.value = window.innerWidth <= 500;
|
|
||||||
});
|
|
||||||
|
|
||||||
const drawerMenuShowing = ref(false);
|
|
||||||
|
|
||||||
const route = 'TODO';
|
|
||||||
watch(route, () => {
|
|
||||||
drawerMenuShowing.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns = deckStore.reactiveState.columns;
|
|
||||||
const layout = deckStore.reactiveState.layout;
|
|
||||||
const menuIndicated = computed(() => {
|
|
||||||
if ($i == null) return false;
|
|
||||||
for (const def in navbarItemDef) {
|
|
||||||
if (navbarItemDef[def].indicated) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
function showSettings() {
|
|
||||||
os.pageWindow('/settings/deck');
|
|
||||||
}
|
|
||||||
|
|
||||||
let columnsEl = $ref<HTMLElement>();
|
|
||||||
|
|
||||||
const addColumn = async (ev) => {
|
|
||||||
const columns = [
|
|
||||||
'main',
|
|
||||||
'widgets',
|
|
||||||
'notifications',
|
|
||||||
'tl',
|
|
||||||
'antenna',
|
|
||||||
'list',
|
|
||||||
'mentions',
|
|
||||||
'direct',
|
|
||||||
];
|
|
||||||
|
|
||||||
const { canceled, result: column } = await os.select({
|
|
||||||
title: i18n.ts._deck.addColumn,
|
|
||||||
items: columns.map(column => ({
|
|
||||||
value: column, text: i18n.t('_deck._columns.' + column),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
|
|
||||||
addColumnToStore({
|
|
||||||
type: column,
|
|
||||||
id: uuid(),
|
|
||||||
name: i18n.t('_deck._columns.' + column),
|
|
||||||
width: 330,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onContextmenu = (ev) => {
|
|
||||||
os.contextMenu([{
|
|
||||||
text: i18n.ts._deck.addColumn,
|
|
||||||
action: addColumn,
|
|
||||||
}], ev);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.documentElement.style.overflowY = 'hidden';
|
|
||||||
document.documentElement.style.scrollBehavior = 'auto';
|
|
||||||
window.addEventListener('wheel', (ev) => {
|
|
||||||
if (ev.target === columnsEl && ev.deltaX === 0) {
|
|
||||||
columnsEl.scrollLeft += ev.deltaY;
|
|
||||||
} else if (getScrollContainer(ev.target as HTMLElement) == null && ev.deltaX === 0) {
|
|
||||||
columnsEl.scrollLeft += ev.deltaY;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
loadDeck();
|
|
||||||
|
|
||||||
function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
|
|
||||||
// TODO??
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeProfile(ev: MouseEvent) {
|
|
||||||
const items = ref([{
|
|
||||||
text: deckStore.state.profile,
|
|
||||||
active: true.valueOf,
|
|
||||||
}]);
|
|
||||||
getProfiles().then(profiles => {
|
|
||||||
items.value = [{
|
|
||||||
text: deckStore.state.profile,
|
|
||||||
active: true.valueOf,
|
|
||||||
}, ...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({
|
|
||||||
text: k,
|
|
||||||
action: () => {
|
|
||||||
deckStore.set('profile', k);
|
|
||||||
unisonReload();
|
|
||||||
},
|
|
||||||
}))), null, {
|
|
||||||
text: i18n.ts._deck.newProfile,
|
|
||||||
icon: 'fas fa-plus',
|
|
||||||
action: async () => {
|
|
||||||
const { canceled, result: name } = await os.inputText({
|
|
||||||
title: i18n.ts._deck.profile,
|
|
||||||
allowEmpty: false,
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
|
|
||||||
deckStore.set('profile', name);
|
|
||||||
unisonReload();
|
|
||||||
},
|
|
||||||
}];
|
|
||||||
});
|
|
||||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteProfile() {
|
|
||||||
const { canceled } = await os.confirm({
|
|
||||||
type: 'warning',
|
|
||||||
text: i18n.t('deleteAreYouSure', { x: deckStore.state.profile }),
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
|
|
||||||
deleteProfile_(deckStore.state.profile);
|
|
||||||
deckStore.set('profile', 'default');
|
|
||||||
unisonReload();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.menu-enter-active,
|
|
||||||
.menu-leave-active {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
.menu-enter-from,
|
|
||||||
.menu-leave-active {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-240px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-back-enter-active,
|
|
||||||
.menu-back-leave-active {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
||||||
}
|
|
||||||
.menu-back-enter-from,
|
|
||||||
.menu-back-leave-active {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mk-deck {
|
|
||||||
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
|
|
||||||
|
|
||||||
// TODO: ここではなくて、各カラムで自身の幅に応じて上書きするようにしたい
|
|
||||||
--margin: var(--marginHalf);
|
|
||||||
|
|
||||||
--deckDividerThickness: 5px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
|
||||||
box-sizing: border-box;
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
&.isMobile {
|
|
||||||
padding-bottom: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .main {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
> .columns {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: clip;
|
|
||||||
|
|
||||||
&.center {
|
|
||||||
> .column:first-of-type {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .column:last-of-type {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .column {
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-right: solid var(--deckDividerThickness) var(--deckDivider);
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
border-left: solid var(--deckDividerThickness) var(--deckDivider);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.folder {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
> *:not(:last-of-type) {
|
|
||||||
border-bottom: solid var(--deckDividerThickness) var(--deckDivider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .intro {
|
|
||||||
padding: 32px;
|
|
||||||
height: min-content;
|
|
||||||
text-align: center;
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
> .add {
|
|
||||||
margin: 1em auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .sideMenu {
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
margin-left: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
width: 32px;
|
|
||||||
|
|
||||||
> .top, > .middle, > .bottom {
|
|
||||||
> .button {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .top {
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .middle {
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .bottom {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .buttons {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1000;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
> .button {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
padding: 0;
|
|
||||||
margin: auto;
|
|
||||||
height: 64px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--panel);
|
|
||||||
color: var(--fg);
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
|
||||||
height: 60px;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--X2);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
color: var(--indicator);
|
|
||||||
font-size: 16px;
|
|
||||||
animation: blink 1s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
> * {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .menu-back {
|
|
||||||
z-index: 1001;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .menu {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1001;
|
|
||||||
// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
|
||||||
width: 240px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
contain: strict;
|
|
||||||
overflow: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
background: var(--navBg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
File diff suppressed because it is too large
Load Diff
@ -37,7 +37,8 @@
|
|||||||
"is-file-animated": "1.0.2",
|
"is-file-animated": "1.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"matter-js": "0.18.0",
|
"matter-js": "0.18.0",
|
||||||
"mfm-js": "0.23.3",
|
"mfm-js": "git+https://github.com/sim1222/mfm.js.git",
|
||||||
|
"misetehoshii": "https://github.com/melt-adzuki/misetehoshii",
|
||||||
"misskey-js": "0.0.14",
|
"misskey-js": "0.0.14",
|
||||||
"photoswipe": "5.3.4",
|
"photoswipe": "5.3.4",
|
||||||
"prismjs": "1.29.0",
|
"prismjs": "1.29.0",
|
||||||
|
@ -90,7 +90,7 @@ import { instance } from '@/instance';
|
|||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { getCustomEmojiCategories, customEmojis } from '@/custom-emojis';
|
import { getCustomEmojiCategories, customEmojis } from '@/custom-emojis';
|
||||||
import MkSwitch from '@/components/form/switch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
|
@ -26,12 +26,12 @@ const instance = props.instance ?? {
|
|||||||
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
|
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
|
||||||
};
|
};
|
||||||
|
|
||||||
const faviconUrl = $computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
|
const iconUrl = $computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
|
||||||
|
|
||||||
const faviconUrl = defaultStore.state.mediaProxy ?
|
const faviconUrl = defaultStore.state.mediaProxy ?
|
||||||
defaultStore.state.mediaProxy + '?url=' + instance.faviconUrl
|
defaultStore.state.mediaProxy + '?url=' + iconUrl
|
||||||
:
|
:
|
||||||
instance.faviconUrl;
|
iconUrl;
|
||||||
|
|
||||||
const themeColor = instance.themeColor ?? '#777777';
|
const themeColor = instance.themeColor ?? '#777777';
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import { NoteReaction, UserLite } from 'misskey-js/built/entities';
|
import { NoteReaction, UserLite } from 'misskey-js/built/entities';
|
||||||
import FormFolder from '@/components/form/folder.vue';
|
import FormFolder from '@/components/MkFolder.vue';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
384
packages/frontend/src/pages/admin/emojigen.vue
Normal file
384
packages/frontend/src/pages/admin/emojigen.vue
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
<template>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<template #header>
|
||||||
|
<XHeader v-model:tab="tab" :tabs="headerTabs" />
|
||||||
|
</template>
|
||||||
|
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||||
|
<div class="cwepdizn _formRoot">
|
||||||
|
<div v-if="tab === 'string'" class="local">
|
||||||
|
<FormSection>
|
||||||
|
<template #label>{{ $ts.preview }}</template>
|
||||||
|
<p><img :src="previewUrl" class="img" :alt="emojiName" /></p>
|
||||||
|
</FormSection>
|
||||||
|
<FormButton primary class="_formBlock" @click="uploadEmoji('url')">{{ $ts.emojiApproval }}</FormButton>
|
||||||
|
<FormSection>
|
||||||
|
<template #label>{{ $ts.settings }}</template>
|
||||||
|
<FormInput v-model="emojiName" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.emojiName }}</template>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormTextarea v-model="text" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.text }}</template>
|
||||||
|
</FormTextarea>
|
||||||
|
|
||||||
|
<FormRadios v-model="emojiAlign" class="_formBlock">
|
||||||
|
<template #label>{{ $ts._simkey.emojiAlign }}</template>
|
||||||
|
<option value="left"><i class="fas fa-align-left" /></option>
|
||||||
|
<option value="center"><i class="fas fa-align-center"></i></option>
|
||||||
|
<option value="right"><i class="fas fa-align-right" /></option>
|
||||||
|
</FormRadios>
|
||||||
|
|
||||||
|
<FormFolder :default-open="false" class="_formBlock">
|
||||||
|
<template #label>{{ $ts._simkey.emojiSizeSetting }}</template>
|
||||||
|
<FormSection>
|
||||||
|
<FormSwitch v-model="emojiSizeFixed" class="_formBlock">
|
||||||
|
<template #label>{{ $ts._simkey.emojiSizeFixed }}</template>
|
||||||
|
</FormSwitch>
|
||||||
|
|
||||||
|
<FormSwitch v-model="emojiStretch" class="_formBlock">
|
||||||
|
<template #label>{{ $ts._simkey.emojiStretch }}</template>
|
||||||
|
</FormSwitch>
|
||||||
|
</FormSection>
|
||||||
|
</FormFolder>
|
||||||
|
|
||||||
|
<FormFolder :default-open="false" class="_formBlock">
|
||||||
|
<template #label>{{ $ts._pages.font }}</template>
|
||||||
|
<FormRadios v-model="font" class="_formBlock">
|
||||||
|
<option value="notosans-mono-bold">Noto Sans Mono CJK JP Bold</option>
|
||||||
|
<option value="mplus-1p-black">M+ 1p black</option>
|
||||||
|
<option value="rounded-x-mplus-1p-black">Rounded M+ 1p black</option>
|
||||||
|
<option value="ipamjm">IPAmj明朝</option>
|
||||||
|
<option value="aoyagireisyoshimo">青柳隷書しも</option>
|
||||||
|
<option value="LinLibertine_RBah">LinLibertine Bold</option>
|
||||||
|
</FormRadios>
|
||||||
|
</FormFolder>
|
||||||
|
|
||||||
|
|
||||||
|
<FormFolder :default-open="false" class="_formBlock">
|
||||||
|
<template #label>{{ $ts._simkey.emojiColor }}</template>
|
||||||
|
<FormSection>
|
||||||
|
<div class="cwepdizn-colors">
|
||||||
|
<div class="row">
|
||||||
|
<button v-for="color in accentColors" :key="color" class="color rounded _button"
|
||||||
|
@click="setAccentColor(color)">
|
||||||
|
<div class="preview" :style="{ background: color }"></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormInput v-model="emojiColor" class="_formBlock" :style="{ color: '#' + emojiColor }">
|
||||||
|
<template #prefix><i class="fas fa-palette"></i></template>
|
||||||
|
<template #label @click="colorPick()">{{ $ts._simkey.emojiColor }}</template>
|
||||||
|
<template #caption>#RRGGBB</template>
|
||||||
|
</FormInput>
|
||||||
|
<FormButton @click="colorPick()">{{ $ts._simkey.colorPicker }}</FormButton>
|
||||||
|
</FormSection>
|
||||||
|
</FormFolder>
|
||||||
|
</FormSection>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="tab === 'misetehoshii'" class="remote">
|
||||||
|
<FormSection>
|
||||||
|
<template #label>{{ $ts.preview }}</template>
|
||||||
|
<canvas class="preview__content" ref="canvas" width="64" height="30" />
|
||||||
|
</FormSection>
|
||||||
|
<FormButton primary class="_formBlock" @click="uploadEmoji('')">{{ $ts._simkey.emojiApproval }}</FormButton>
|
||||||
|
<FormSection>
|
||||||
|
<template #label>{{ $ts.settings }}</template>
|
||||||
|
<FormInput v-model="emojiName" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.emojiName }}</template>
|
||||||
|
</FormInput>
|
||||||
|
|
||||||
|
<FormTextarea v-model="text" value="見せてほしい" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.text }}</template>
|
||||||
|
</FormTextarea>
|
||||||
|
|
||||||
|
<FormRadios v-model="backColor" class="_formBlock">
|
||||||
|
<template #label>{{ $ts._theme.color }}</template>
|
||||||
|
<option v-for="c in backColors" v-bind:value="c">{{ c }}</option>
|
||||||
|
</FormRadios>
|
||||||
|
</FormSection>
|
||||||
|
<FormSection>
|
||||||
|
<template #label>見せてほしいメーカー</template>
|
||||||
|
<div>Powered by <MkLink url="https://github.com/melt-adzuki/misetehoshii">misetehoshii</MkLink>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { stream } from '@/stream';
|
||||||
|
import { uploadFile } from '@/scripts/upload';
|
||||||
|
import XHeader from './_header_.vue';
|
||||||
|
import MkTab from '@/components/MkTab.vue';
|
||||||
|
import MkLink from '@/components/MkLink.vue';
|
||||||
|
import FormSection from '@/components/form/section.vue';
|
||||||
|
import FormInput from '@/components/MkInput.vue';
|
||||||
|
import FormSwitch from '@/components/MkSwitch.vue';
|
||||||
|
import FormButton from '@/components/MkButton.vue';
|
||||||
|
import FormRadios from '@/components/MkRadios.vue';
|
||||||
|
import FormTextarea from '@/components/MkTextarea.vue';
|
||||||
|
import FormFolder from '@/components/MkFolder.vue';
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { buttonImages } from "misetehoshii/src/assets";
|
||||||
|
import { draw } from "misetehoshii/src/canvas";
|
||||||
|
|
||||||
|
definePageMetadata(computed(() => ({
|
||||||
|
title: i18n.ts._simkey.emojiGen,
|
||||||
|
icon: 'fas fa-kiss-wink-heart',
|
||||||
|
})));
|
||||||
|
|
||||||
|
const tab = ref('string');
|
||||||
|
const headerTabs = $computed(() => [{
|
||||||
|
key: 'string',
|
||||||
|
title: i18n.ts._simkey.emojiNormal,
|
||||||
|
}, {
|
||||||
|
key: 'misetehoshii',
|
||||||
|
title: i18n.ts._simkey.emojiIfilter,
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const font = ref('rounded-x-mplus-1p-black');
|
||||||
|
const text = ref('');
|
||||||
|
const emojiName = ref('');
|
||||||
|
const emojiAlign = ref('center');
|
||||||
|
const emojiSizeFixed = ref(false);
|
||||||
|
const emojiStretch = ref(false);
|
||||||
|
const emojiColor = ref('90ee90');
|
||||||
|
const previewUrl = ref('');
|
||||||
|
const accentColors = [
|
||||||
|
'#ff00ff',
|
||||||
|
'#ff0080',
|
||||||
|
'#D55353',
|
||||||
|
'#ff8080',
|
||||||
|
'#ff8040',
|
||||||
|
'#ffff80',
|
||||||
|
'#80ff80',
|
||||||
|
'#90ee90',
|
||||||
|
'#80ffff',
|
||||||
|
'#0080ff',
|
||||||
|
'#8080ff',
|
||||||
|
];
|
||||||
|
|
||||||
|
const backColor = ref('blue');
|
||||||
|
const canvas = ref<HTMLCanvasElement>();
|
||||||
|
const buttonImage = computed(() => buttonImages[backColor.value]);
|
||||||
|
const backColors = [
|
||||||
|
'blue',
|
||||||
|
'peacockGreen',
|
||||||
|
'green',
|
||||||
|
'yellow',
|
||||||
|
'red',
|
||||||
|
'pink',
|
||||||
|
'disabled',
|
||||||
|
];
|
||||||
|
|
||||||
|
const updateCanvas = (): void => {
|
||||||
|
const context = canvas.value!.getContext("2d")!;
|
||||||
|
draw({ context, text: text.value, button: buttonImage.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
watch([font, text, emojiAlign, emojiSizeFixed, emojiStretch, emojiColor, backColor], () => {
|
||||||
|
preview();
|
||||||
|
updateCanvas();
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorPick = (): void => {
|
||||||
|
const input = document.createElement('input') as HTMLInputElement;
|
||||||
|
input.type = 'color';
|
||||||
|
input.value = `#${emojiColor.value}`;
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
emojiColor.value = input.value.replace('#', '');
|
||||||
|
});
|
||||||
|
(window as any).__misskey_input_ref__ = input;
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeUrl = (): string => {
|
||||||
|
const API_URL = 'https://emoji-gen.ninja/emoji';
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
text: encodeURI(text.value),
|
||||||
|
color: emojiColor.value + 'FF',
|
||||||
|
back_color: '00000000',
|
||||||
|
font: font.value,
|
||||||
|
size_fixed: !emojiSizeFixed.value ? 'true' : 'false',
|
||||||
|
align: 'center',
|
||||||
|
stretch: !emojiStretch.value ? 'true' : 'false',
|
||||||
|
public_fg: 'false',
|
||||||
|
locale: 'ja',
|
||||||
|
};
|
||||||
|
|
||||||
|
return API_URL + '?' + Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&');
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview = (): void => {
|
||||||
|
previewUrl.value = makeUrl();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAccentColor = (color) => {
|
||||||
|
emojiColor.value = color.replace('#', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadFileFromUrlWithId = (url: string) => new Promise<string>(async resolve => {
|
||||||
|
const marker = uuid();
|
||||||
|
|
||||||
|
// 先にコネクションを貼り、アップロードが完了したかどうかを監視する
|
||||||
|
const connection = stream.useChannel('main');
|
||||||
|
|
||||||
|
connection.on('urlUploadFinished', async response => {
|
||||||
|
if (response.marker === marker) {
|
||||||
|
resolve(response.file.id);
|
||||||
|
connection.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 絵文字をアップロード
|
||||||
|
await os.api('drive/files/upload-from-url', {
|
||||||
|
url,
|
||||||
|
folderId: defaultStore.state.uploadFolder,
|
||||||
|
marker,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const getEmojiObject = emojiId => new Promise<Record<string, any> | null>(async resolve => {
|
||||||
|
const sinceId = await os.api('admin/emoji/list', {
|
||||||
|
limit: 1,
|
||||||
|
untilId: emojiId.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sinceId || !sinceId[0] || !sinceId[0].id) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = await os.api('admin/emoji/list', {
|
||||||
|
limit: 1,
|
||||||
|
sinceId: sinceId[0].id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!id || !id[0]) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(id[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadEmoji = async (type: string) => {
|
||||||
|
|
||||||
|
if (type == 'url') {
|
||||||
|
const emojiUrl = makeUrl();
|
||||||
|
const fileId = await uploadFileFromUrlWithId(emojiUrl);
|
||||||
|
|
||||||
|
// ドライブにアップロードされたファイルをリネーム
|
||||||
|
await os.api('drive/files/update', {
|
||||||
|
fileId,
|
||||||
|
name: emojiName.value + '.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emojiId = await os.api('admin/emoji/add', { fileId });
|
||||||
|
const emoji = await getEmojiObject(emojiId);
|
||||||
|
|
||||||
|
if (!emoji) {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: i18n.ts.failedToUploadEmoji,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), { emoji });
|
||||||
|
} else {
|
||||||
|
const canvasImg = new Promise<Blob>(resolve => {
|
||||||
|
canvas.value!.toBlob(blob => resolve(blob?.slice(0, blob.size, 'image/png') as Blob));
|
||||||
|
});
|
||||||
|
const file = new File([await canvasImg], emojiName.value + '.png', { type: 'image/png' });
|
||||||
|
const fileId = (await uploadFile(file, defaultStore.state.uploadFolder, undefined, true)).id;
|
||||||
|
|
||||||
|
// ドライブにアップロードされたファイルをリネーム
|
||||||
|
await os.api('drive/files/update', {
|
||||||
|
fileId,
|
||||||
|
name: emojiName.value + '.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emojiId = await os.api('admin/emoji/add', { fileId });
|
||||||
|
const emoji = await getEmojiObject(emojiId);
|
||||||
|
|
||||||
|
if (!emoji) {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: i18n.ts.failedToUploadEmoji,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), { emoji });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.preview-img {
|
||||||
|
height: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cwepdizn {
|
||||||
|
::v-deep(.cwepdizn-colors) {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
>.row {
|
||||||
|
>.color {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
>.preview {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 4px rgb(0 0 0 / 30%);
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
>.preview {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
box-shadow: 0 0 0 2px var(--divider) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rounded {
|
||||||
|
border-radius: 999px;
|
||||||
|
|
||||||
|
>.preview {
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.char {
|
||||||
|
line-height: 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import FormInput from '@/components/form/input.vue';
|
import FormInput from '@/components/MkInput.vue';
|
||||||
import FormButton from '@/components/MkButton.vue';
|
import FormButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
@ -64,6 +64,58 @@
|
|||||||
<div>{{ i18n.ts._tutorial.step8_2 }}</div>
|
<div>{{ i18n.ts._tutorial.step8_2 }}</div>
|
||||||
<small :class="$style.small">{{ i18n.ts._tutorial.step8_3 }}</small>
|
<small :class="$style.small">{{ i18n.ts._tutorial.step8_3 }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="tutorial === 8" :class="$style.body">
|
||||||
|
<div>{{ $ts._tutorial.step9_1 }}</div>
|
||||||
|
<div>{{ $ts._tutorial.step9_2 }}</div>
|
||||||
|
<div>{{ $ts._tutorial.step9_3 }}</div>
|
||||||
|
<div>{{ $ts._tutorial.step9_4 }}</div>
|
||||||
|
<FormSwitch v-model="useBlurEffect" class="_formBlock">{{ i18n.ts.useBlurEffect }}</FormSwitch>
|
||||||
|
<FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{ i18n.ts.useBlurEffectForModal }}</FormSwitch>
|
||||||
|
<template v-if="darkMode">
|
||||||
|
<FormSelect v-model="darkThemeId" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.themeForDarkMode }}</template>
|
||||||
|
<template #prefix><i class="fas fa-moon"></i></template>
|
||||||
|
<optgroup :label="$ts.darkThemes">
|
||||||
|
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup :label="$ts.lightThemes">
|
||||||
|
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
</FormSelect>
|
||||||
|
<FormSelect v-model="lightThemeId" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.themeForLightMode }}</template>
|
||||||
|
<template #prefix><i class="fas fa-sun"></i></template>
|
||||||
|
<optgroup :label="$ts.lightThemes">
|
||||||
|
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup :label="$ts.darkThemes">
|
||||||
|
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
</FormSelect>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<FormSelect v-model="lightThemeId" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.themeForLightMode }}</template>
|
||||||
|
<template #prefix><i class="fas fa-sun"></i></template>
|
||||||
|
<optgroup :label="$ts.lightThemes">
|
||||||
|
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup :label="$ts.darkThemes">
|
||||||
|
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
</FormSelect>
|
||||||
|
<FormSelect v-model="darkThemeId" class="_formBlock">
|
||||||
|
<template #label>{{ $ts.themeForDarkMode }}</template>
|
||||||
|
<template #prefix><i class="fas fa-moon"></i></template>
|
||||||
|
<optgroup :label="$ts.darkThemes">
|
||||||
|
<option v-for="x in darkThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup :label="$ts.lightThemes">
|
||||||
|
<option v-for="x in lightThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
|
||||||
|
</optgroup>
|
||||||
|
</FormSelect>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div :class="$style.footer">
|
<div :class="$style.footer">
|
||||||
<template v-if="tutorial === tutorialsNumber - 1">
|
<template v-if="tutorial === tutorialsNumber - 1">
|
||||||
@ -78,18 +130,74 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed, onActivated, ref, watch } from 'vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore, ColdDeviceStorage } from '@/store';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import FormSwitch from '@/components/MkSwitch.vue';
|
||||||
|
import FormSelect from '@/components/MkSelect.vue';
|
||||||
|
import { getBuiltinThemesRef } from '@/scripts/theme';
|
||||||
|
import { fetchThemes, getThemes } from '@/theme-store';
|
||||||
|
import { uniqueBy } from '@/scripts/array';
|
||||||
|
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||||
|
|
||||||
const tutorialsNumber = 8;
|
|
||||||
|
const tutorialsNumber = 9;
|
||||||
|
|
||||||
const tutorial = computed({
|
const tutorial = computed({
|
||||||
get() { return defaultStore.reactiveState.tutorial.value || 0; },
|
get() { return defaultStore.reactiveState.tutorial.value || 0; },
|
||||||
set(value) { defaultStore.set('tutorial', value); },
|
set(value) { defaultStore.set('tutorial', value); },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
||||||
|
const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
|
||||||
|
|
||||||
|
const installedThemes = ref(getThemes());
|
||||||
|
const builtinThemes = getBuiltinThemesRef();
|
||||||
|
const instanceThemes = [];
|
||||||
|
|
||||||
|
const themes = computed(() => uniqueBy([...instanceThemes, ...builtinThemes.value, ...installedThemes.value], theme => theme.id));
|
||||||
|
const darkThemes = computed(() => themes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
|
||||||
|
const lightThemes = computed(() => themes.value.filter(t => t.base === 'light' || t.kind === 'light'));
|
||||||
|
const darkTheme = ColdDeviceStorage.ref('darkTheme');
|
||||||
|
const darkThemeId = computed({
|
||||||
|
get() {
|
||||||
|
return darkTheme.value.id;
|
||||||
|
},
|
||||||
|
set(id) {
|
||||||
|
ColdDeviceStorage.set('darkTheme', themes.value.find(x => x.id === id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const lightTheme = ColdDeviceStorage.ref('lightTheme');
|
||||||
|
const lightThemeId = computed({
|
||||||
|
get() {
|
||||||
|
return lightTheme.value.id;
|
||||||
|
},
|
||||||
|
set(id) {
|
||||||
|
ColdDeviceStorage.set('lightTheme', themes.value.find(x => x.id === id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||||
|
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
||||||
|
|
||||||
|
watch(syncDeviceDarkMode, () => {
|
||||||
|
if (syncDeviceDarkMode.value) {
|
||||||
|
defaultStore.set('darkMode', isDeviceDarkmode());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
fetchThemes().then(() => {
|
||||||
|
installedThemes.value = getThemes();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchThemes().then(() => {
|
||||||
|
installedThemes.value = getThemes();
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -65,7 +65,7 @@ export async function openReactionImportMenu(ev: MouseEvent, reaction: string, n
|
|||||||
emojiId: emojiId,
|
emojiId: emojiId,
|
||||||
}).then(async emoji => {
|
}).then(async emoji => {
|
||||||
if (skip) return;
|
if (skip) return;
|
||||||
os.popup(defineAsyncComponent(() => import('@/pages/admin/emoji-edit-dialog.vue')), {
|
os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), {
|
||||||
emoji: await getEmojiObject(emoji),
|
emoji: await getEmojiObject(emoji),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -23,6 +23,7 @@ import * as sound from '@/scripts/sound';
|
|||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { swInject } from './sw-inject';
|
import { swInject } from './sw-inject';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@ -31,17 +32,24 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
let notifications = $ref<misskey.entities.Notification[]>([]);
|
||||||
|
|
||||||
const onNotification = notification => {
|
const onNotification = notification => {
|
||||||
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
||||||
|
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
stream.send('readNotification', {
|
stream.send('readNotification', {
|
||||||
id: notification.id
|
id: notification.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
popup(defineAsyncComponent(() => import('@/components/MkNotificationToast.vue')), {
|
notifications.unshift(notification);
|
||||||
notification
|
window.setTimeout(() => {
|
||||||
}, {}, 'closed');
|
if (notifications.length > 3) notifications.pop();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
notifications = notifications.filter(x => x.id !== notification.id);
|
||||||
|
}, 6000);
|
||||||
}
|
}
|
||||||
|
|
||||||
sound.play('notification');
|
sound.play('notification');
|
||||||
|
31
yarn.lock
31
yarn.lock
@ -6145,6 +6145,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"destyle.css@npm:^3.0.2":
|
||||||
|
version: 3.0.2
|
||||||
|
resolution: "destyle.css@npm:3.0.2"
|
||||||
|
checksum: bf4c26d2f996476fb43a8b5f9144200ba421fd2f342da0e32ba5795e836c5cd78d799c56f71bf3989897f7247fbdca9d3d0e2d6b9d0e8187948e3c039af5b733
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"detect-file@npm:^1.0.0":
|
"detect-file@npm:^1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "detect-file@npm:1.0.0"
|
resolution: "detect-file@npm:1.0.0"
|
||||||
@ -8077,7 +8084,8 @@ __metadata:
|
|||||||
is-file-animated: 1.0.2
|
is-file-animated: 1.0.2
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
matter-js: 0.18.0
|
matter-js: 0.18.0
|
||||||
mfm-js: 0.23.3
|
mfm-js: "git+https://github.com/sim1222/mfm.js.git"
|
||||||
|
misetehoshii: "https://github.com/melt-adzuki/misetehoshii"
|
||||||
misskey-js: 0.0.14
|
misskey-js: 0.0.14
|
||||||
photoswipe: 5.3.4
|
photoswipe: 5.3.4
|
||||||
prismjs: 1.29.0
|
prismjs: 1.29.0
|
||||||
@ -11566,6 +11574,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"mfm-js@git+https://github.com/sim1222/mfm.js.git":
|
||||||
|
version: 0.23.0
|
||||||
|
resolution: "mfm-js@https://github.com/sim1222/mfm.js.git#commit=adba7ad3bfbd1779b46314f3eef52f301fd0d6c9"
|
||||||
|
dependencies:
|
||||||
|
twemoji-parser: 14.0.0
|
||||||
|
checksum: 2c3fc61503ed6382e5cb24d869034956968e9c60d8bb7929daa5c1b41a5adcb8e8974af610c00052f5c54f4eddea58a0988d4fd57097a636368bd96739895142
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"mfm-js@npm:0.23.3":
|
"mfm-js@npm:0.23.3":
|
||||||
version: 0.23.3
|
version: 0.23.3
|
||||||
resolution: "mfm-js@npm:0.23.3"
|
resolution: "mfm-js@npm:0.23.3"
|
||||||
@ -11796,6 +11813,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"misetehoshii@https://github.com/melt-adzuki/misetehoshii":
|
||||||
|
version: 0.0.0
|
||||||
|
resolution: "misetehoshii@https://github.com/melt-adzuki/misetehoshii.git#commit=d00f51529e390808f92172000f960b4f1314f37c"
|
||||||
|
dependencies:
|
||||||
|
destyle.css: ^3.0.2
|
||||||
|
vue: ^3.2.37
|
||||||
|
checksum: d9e522e7628202f13573d638ba43ba416751929881cdd235a1881143113bf3acb1aba0af70342bcdca4601f99baf24b2d48696f821ed98036edfc247fa5451d9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"misskey-js@npm:0.0.14":
|
"misskey-js@npm:0.0.14":
|
||||||
version: 0.0.14
|
version: 0.0.14
|
||||||
resolution: "misskey-js@npm:0.0.14"
|
resolution: "misskey-js@npm:0.0.14"
|
||||||
@ -17158,7 +17185,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"vue@npm:3.2.45":
|
"vue@npm:3.2.45, vue@npm:^3.2.37":
|
||||||
version: 3.2.45
|
version: 3.2.45
|
||||||
resolution: "vue@npm:3.2.45"
|
resolution: "vue@npm:3.2.45"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user