fix: can build

This commit is contained in:
sim1222 2023-01-15 01:21:04 +09:00
parent 378748a643
commit 34eb30164c
No known key found for this signature in database
GPG Key ID: 04EF48D01BEB0298
21 changed files with 545 additions and 6411 deletions

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 {
// mousedownonClick
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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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",

View File

@ -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;

View File

@ -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';

View File

@ -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';

View 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>

View File

@ -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';

View File

@ -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>

View File

@ -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),
}); });
}); });

View File

@ -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');

View File

@ -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: