Merge remote-tracking branch 'misskey-original/develop' into develop
# Conflicts: # packages/backend/src/models/RepositoryModule.ts # packages/backend/src/models/_.ts # packages/backend/src/postgres.ts # packages/frontend/src/components/MkDrive.file.vue # packages/frontend/src/components/MkEmojiPicker.vue # packages/frontend/src/pages/admin/index.vue # packages/frontend/src/pages/user/home.vue # packages/frontend/src/router.ts # packages/frontend/src/ui/universal.vue
This commit is contained in:
commit
33507e24ff
157 changed files with 3973 additions and 2059 deletions
BIN
packages/frontend/assets/drop-and-fusion/bgm_1.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/bgm_1.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/drop-and-fusion/bubble2.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/bubble2.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/drop-and-fusion/click.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/click.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/drop-and-fusion/gameover.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/gameover.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/drop-and-fusion/hold.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/hold.mp3
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 248 KiB |
BIN
packages/frontend/assets/drop-and-fusion/poi1.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/poi1.mp3
Normal file
Binary file not shown.
BIN
packages/frontend/assets/drop-and-fusion/poi2.mp3
Normal file
BIN
packages/frontend/assets/drop-and-fusion/poi2.mp3
Normal file
Binary file not shown.
|
|
@ -4,7 +4,7 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"watch": "vite",
|
||||
"dev": "vite --config vite.config.local-dev.ts",
|
||||
"dev": "vite --config vite.config.local-dev.ts --debug hmr",
|
||||
"build": "vite build",
|
||||
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
|
||||
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "5.0.5",
|
||||
"@rollup/pluginutils": "5.1.0",
|
||||
"@syuilo/aiscript": "0.16.0",
|
||||
"@syuilo/aiscript": "0.17.0",
|
||||
"@tabler/icons-webfont": "2.44.0",
|
||||
"@twemoji/parser": "15.0.0",
|
||||
"@vitejs/plugin-vue": "5.0.2",
|
||||
|
|
@ -58,6 +58,7 @@
|
|||
"rollup": "4.9.1",
|
||||
"sanitize-html": "2.11.0",
|
||||
"sass": "1.69.5",
|
||||
"seedrandom": "^3.0.5",
|
||||
"shiki": "0.14.7",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
|||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||
import { setupRouter } from '@/global/router/definition.js';
|
||||
|
||||
export async function common(createVue: () => App<Element>) {
|
||||
console.info(`Misskey v${version}`);
|
||||
|
|
@ -241,6 +242,8 @@ export async function common(createVue: () => App<Element>) {
|
|||
|
||||
const app = createVue();
|
||||
|
||||
setupRouter(app);
|
||||
|
||||
if (_DEV_) {
|
||||
app.config.performance = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,23 +3,23 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { createApp, markRaw, defineAsyncComponent } from 'vue';
|
||||
import { createApp, defineAsyncComponent, markRaw } from 'vue';
|
||||
import { common } from './common.js';
|
||||
import { ui } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { confirm, alert, post, popup, toast } from '@/os.js';
|
||||
import { alert, confirm, popup, post, toast } from '@/os.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { $i, updateAccount, signout } from '@/account.js';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||
import { $i, signout, updateAccount } from '@/account.js';
|
||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||
import { makeHotkey } from '@/scripts/hotkey.js';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { initializeSw } from '@/scripts/initialize-sw.js';
|
||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
export async function mainBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
|
|
@ -271,7 +271,7 @@ export async function mainBoot() {
|
|||
|
||||
main.on('unreadAntenna', () => {
|
||||
updateAccount({ hasUnreadAntenna: true });
|
||||
sound.play('antenna');
|
||||
sound.playMisskeySfx('antenna');
|
||||
});
|
||||
|
||||
main.on('readAllAnnouncements', () => {
|
||||
|
|
|
|||
|
|
@ -266,15 +266,24 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30):
|
|||
}
|
||||
|
||||
const matched = new Map<string, EmojiScore>();
|
||||
|
||||
// 前方一致(エイリアスなし)
|
||||
// 完全一致(エイリアス込み)
|
||||
emojiDb.some(x => {
|
||||
if (x.name.startsWith(query) && !x.aliasOf) {
|
||||
matched.set(x.name, { emoji: x, score: query.length + 1 });
|
||||
if (x.name === query && !matched.has(x.aliasOf ?? x.name)) {
|
||||
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
|
||||
// 前方一致(エイリアスなし)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
if (x.name.startsWith(query) && !x.aliasOf) {
|
||||
matched.set(x.name, { emoji: x, score: query.length + 1 });
|
||||
}
|
||||
return matched.size === max;
|
||||
});
|
||||
}
|
||||
|
||||
// 前方一致(エイリアス込み)
|
||||
if (matched.size < max) {
|
||||
emojiDb.some(x => {
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ import bytes from '@/filters/bytes.js';
|
|||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { getDriveFileMenu, getDriveMultiFileMenu } from '@/scripts/get-drive-file-menu.js';
|
||||
import { isTouchUsing } from '@/scripts/touch.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -226,7 +226,18 @@ watch(q, () => {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
for (const emoji of emojis) {
|
||||
if (customEmojisMap.has(newQ)) {
|
||||
matches.add(customEmojisMap.get(newQ)!);
|
||||
}
|
||||
if (matches.size >= max) return matches;
|
||||
|
||||
for (const emoji of emojis) {
|
||||
if (emoji.aliases.some(alias => alias === newQ)) {
|
||||
matches.add(emoji);
|
||||
if (matches.size >= max) break;
|
||||
}
|
||||
}
|
||||
if (matches.size >= max) return matches;for (const emoji of emojis) {
|
||||
if (emoji.name.startsWith(newQ)) {
|
||||
matches.add(emoji);
|
||||
if (matches.size >= max) break;
|
||||
|
|
|
|||
|
|
@ -352,7 +352,7 @@ function react(viaKeyboard = false): void {
|
|||
pleaseLogin();
|
||||
showMovedDialog();
|
||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
sound.play('reaction');
|
||||
sound.playMisskeySfx('reaction');
|
||||
|
||||
if (props.mock) {
|
||||
return;
|
||||
|
|
@ -372,7 +372,7 @@ function react(viaKeyboard = false): void {
|
|||
} else {
|
||||
blur();
|
||||
reactionPicker.show(reactButton.value, reaction => {
|
||||
sound.play('reaction');
|
||||
sound.playMisskeySfx('reaction');
|
||||
|
||||
if (props.mock) {
|
||||
emit('reaction', reaction);
|
||||
|
|
|
|||
|
|
@ -396,7 +396,7 @@ function react(viaKeyboard = false): void {
|
|||
pleaseLogin();
|
||||
showMovedDialog();
|
||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
sound.play('reaction');
|
||||
sound.playMisskeySfx('reaction');
|
||||
|
||||
misskeyApi('notes/reactions/create', {
|
||||
noteId: appearNote.value.id,
|
||||
|
|
@ -412,7 +412,7 @@ function react(viaKeyboard = false): void {
|
|||
} else {
|
||||
blur();
|
||||
reactionPicker.show(reactButton.value, reaction => {
|
||||
sound.play('reaction');
|
||||
sound.playMisskeySfx('reaction');
|
||||
|
||||
misskeyApi('notes/reactions/create', {
|
||||
noteId: appearNote.value.id,
|
||||
|
|
|
|||
|
|
@ -23,26 +23,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<div ref="contents" :class="$style.root" style="container-type: inline-size;">
|
||||
<RouterView :key="reloadCount" :router="router"/>
|
||||
<RouterView :key="reloadCount" :router="windowRouter"/>
|
||||
</div>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ComputedRef, onMounted, onUnmounted, provide, shallowRef, ref, computed } from 'vue';
|
||||
import { computed, ComputedRef, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
|
||||
import RouterView from '@/components/global/RouterView.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { popout as _popout } from '@/scripts/popout.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@/config.js';
|
||||
import { mainRouter, routes, page } from '@/router.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { Router, useScrollPositionManager } from '@/nirax.js';
|
||||
import { useScrollPositionManager } from '@/nirax.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import { openingWindowsCount } from '@/os.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||
import { useRouterFactory } from '@/global/router/supplier.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
const props = defineProps<{
|
||||
initialPath: string;
|
||||
|
|
@ -52,14 +52,15 @@ defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue')));
|
||||
const routerFactory = useRouterFactory();
|
||||
const windowRouter = routerFactory(props.initialPath);
|
||||
|
||||
const contents = shallowRef<HTMLElement>();
|
||||
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
||||
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
|
||||
const history = ref<{ path: string; key: any; }[]>([{
|
||||
path: router.getCurrentPath(),
|
||||
key: router.getCurrentKey(),
|
||||
path: windowRouter.getCurrentPath(),
|
||||
key: windowRouter.getCurrentKey(),
|
||||
}]);
|
||||
const buttonsLeft = computed(() => {
|
||||
const buttons = [];
|
||||
|
|
@ -88,11 +89,11 @@ const buttonsRight = computed(() => {
|
|||
});
|
||||
const reloadCount = ref(0);
|
||||
|
||||
router.addListener('push', ctx => {
|
||||
windowRouter.addListener('push', ctx => {
|
||||
history.value.push({ path: ctx.path, key: ctx.key });
|
||||
});
|
||||
|
||||
provide('router', router);
|
||||
provide('router', windowRouter);
|
||||
provideMetadataReceiver((info) => {
|
||||
pageMetadata.value = info;
|
||||
});
|
||||
|
|
@ -112,20 +113,20 @@ const contextmenu = computed(() => ([{
|
|||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(url + router.getCurrentPath(), '_blank', 'noopener');
|
||||
window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener');
|
||||
windowEl.value.close();
|
||||
},
|
||||
}, {
|
||||
icon: 'ti ti-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(url + router.getCurrentPath());
|
||||
copyToClipboard(url + windowRouter.getCurrentPath());
|
||||
},
|
||||
}]));
|
||||
|
||||
function back() {
|
||||
history.value.pop();
|
||||
router.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
|
||||
windowRouter.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
|
|
@ -137,16 +138,16 @@ function close() {
|
|||
}
|
||||
|
||||
function expand() {
|
||||
mainRouter.push(router.getCurrentPath(), 'forcePage');
|
||||
mainRouter.push(windowRouter.getCurrentPath(), 'forcePage');
|
||||
windowEl.value.close();
|
||||
}
|
||||
|
||||
function popout() {
|
||||
_popout(router.getCurrentPath(), windowEl.value.$el);
|
||||
_popout(windowRouter.getCurrentPath(), windowEl.value.$el);
|
||||
windowEl.value.close();
|
||||
}
|
||||
|
||||
useScrollPositionManager(() => getScrollContainer(contents.value), router);
|
||||
useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter);
|
||||
|
||||
onMounted(() => {
|
||||
openingWindowsCount.value++;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: number): void;
|
||||
(ev: 'dragEnded', value: number): void;
|
||||
}>();
|
||||
|
||||
const containerEl = shallowRef<HTMLElement>();
|
||||
|
|
@ -147,6 +148,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
|
|||
// 値が変わってたら通知
|
||||
if (beforeValue !== finalValue.value) {
|
||||
emit('update:modelValue', finalValue.value);
|
||||
emit('dragEnded', finalValue.value);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ async function toggleReaction() {
|
|||
if (confirm.canceled) return;
|
||||
|
||||
if (oldReaction !== props.reaction) {
|
||||
sound.play('reaction');
|
||||
sound.playMisskeySfx('reaction');
|
||||
}
|
||||
|
||||
if (mock) {
|
||||
|
|
@ -83,7 +83,7 @@ async function toggleReaction() {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
sound.play('reaction');
|
||||
sound.playMisskeySfx('reaction');
|
||||
|
||||
if (mock) {
|
||||
emit('reactionToggled', props.reaction, (props.count + 1));
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ function prepend(note) {
|
|||
emit('note');
|
||||
|
||||
if (props.sound) {
|
||||
sound.play($i && (note.userId === $i.id) ? 'noteMy' : 'note');
|
||||
sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||
</div>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-for="kind in Object.keys(permissions)" :key="kind" v-model="permissions[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
|
||||
<MkSwitch v-for="kind in Object.keys(permissionSwitches)" :key="kind" v-model="permissionSwitches[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
|
||||
</div>
|
||||
<div v-if="iAmAdmin" :class="$style.adminPermissions">
|
||||
<div :class="$style.adminPermissionsHeader"><b>{{ i18n.ts.adminPermission }}</b></div>
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
|
@ -49,6 +55,7 @@ import MkButton from './MkButton.vue';
|
|||
import MkInfo from './MkInfo.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { iAmAdmin } from '@/account.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
title?: string | null;
|
||||
|
|
@ -68,37 +75,76 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const defaultPermissions = Misskey.permissions.filter(p => !p.startsWith('read:admin') && !p.startsWith('write:admin'));
|
||||
const adminPermissions = Misskey.permissions.filter(p => p.startsWith('read:admin') || p.startsWith('write:admin'));
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const name = ref(props.initialName);
|
||||
const permissions = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
|
||||
const permissionSwitches = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
|
||||
const permissionSwitchesForAdmin = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
|
||||
|
||||
if (props.initialPermissions) {
|
||||
for (const kind of props.initialPermissions) {
|
||||
permissions.value[kind] = true;
|
||||
permissionSwitches.value[kind] = true;
|
||||
}
|
||||
} else {
|
||||
for (const kind of defaultPermissions) {
|
||||
permissions.value[kind] = false;
|
||||
permissionSwitches.value[kind] = false;
|
||||
}
|
||||
|
||||
if (iAmAdmin) {
|
||||
for (const kind of adminPermissions) {
|
||||
permissionSwitchesForAdmin.value[kind] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ok(): void {
|
||||
emit('done', {
|
||||
name: name.value,
|
||||
permissions: Object.keys(permissions.value).filter(p => permissions.value[p]),
|
||||
permissions: [
|
||||
...Object.keys(permissionSwitches.value).filter(p => permissionSwitches.value[p]),
|
||||
...(iAmAdmin ? Object.keys(permissionSwitchesForAdmin.value).filter(p => permissionSwitchesForAdmin.value[p]) : []),
|
||||
],
|
||||
});
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
function disableAll(): void {
|
||||
for (const p in permissions.value) {
|
||||
permissions.value[p] = false;
|
||||
for (const p in permissionSwitches.value) {
|
||||
permissionSwitches.value[p] = false;
|
||||
}
|
||||
if (iAmAdmin) {
|
||||
for (const p in permissionSwitchesForAdmin.value) {
|
||||
permissionSwitchesForAdmin.value[p] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function enableAll(): void {
|
||||
for (const p in permissions.value) {
|
||||
permissions.value[p] = true;
|
||||
for (const p in permissionSwitches.value) {
|
||||
permissionSwitches.value[p] = true;
|
||||
}
|
||||
if (iAmAdmin) {
|
||||
for (const p in permissionSwitchesForAdmin.value) {
|
||||
permissionSwitchesForAdmin.value[p] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.adminPermissions {
|
||||
margin: 8px -6px 0;
|
||||
padding: 24px 6px 6px;
|
||||
border: 2px solid var(--error);
|
||||
border-radius: calc(var(--radius) / 2);
|
||||
}
|
||||
|
||||
.adminPermissionsHeader {
|
||||
margin: -34px 0 6px 12px;
|
||||
padding: 0 4px;
|
||||
width: fit-content;
|
||||
color: var(--error);
|
||||
background: var(--panel);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import * as os from '@/os.js';
|
|||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
to: string;
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ function onClick(ev: MouseEvent) {
|
|||
icon: 'ti ti-plus',
|
||||
action: () => {
|
||||
react(`:${props.name}:`);
|
||||
sound.play('reaction');
|
||||
sound.playMisskeySfx('reaction');
|
||||
},
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function onClick(ev: MouseEvent) {
|
|||
icon: 'ti ti-plus',
|
||||
action: () => {
|
||||
react(props.emoji);
|
||||
sound.play('reaction');
|
||||
sound.playMisskeySfx('reaction');
|
||||
},
|
||||
}] : [])], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue';
|
||||
import { Resolved, Router } from '@/nirax.js';
|
||||
import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue';
|
||||
import { IRouter, Resolved } from '@/nirax.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
router?: Router;
|
||||
router?: IRouter;
|
||||
}>();
|
||||
|
||||
const router = props.router ?? inject('router');
|
||||
|
|
|
|||
571
packages/frontend/src/global/router/definition.ts
Normal file
571
packages/frontend/src/global/router/definition.ts
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue';
|
||||
import { IRouter, Router } from '@/nirax.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import MkLoading from '@/pages/_loading_.vue';
|
||||
import MkError from '@/pages/_error_.vue';
|
||||
import { setMainRouter } from '@/global/router/main.js';
|
||||
|
||||
const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
|
||||
loader: loader,
|
||||
loadingComponent: MkLoading,
|
||||
errorComponent: MkError,
|
||||
});
|
||||
const routes = [{
|
||||
path: '/@:initUser/pages/:initPageName/view-source',
|
||||
component: page(() => import('@/pages/page-editor/page-editor.vue')),
|
||||
}, {
|
||||
path: '/@:username/pages/:pageName',
|
||||
component: page(() => import('@/pages/page.vue')),
|
||||
}, {
|
||||
path: '/@:acct/following',
|
||||
component: page(() => import('@/pages/user/following.vue')),
|
||||
}, {
|
||||
path: '/@:acct/followers',
|
||||
component: page(() => import('@/pages/user/followers.vue')),
|
||||
}, {
|
||||
name: 'user',
|
||||
path: '/@:acct/:page?',
|
||||
component: page(() => import('@/pages/user/index.vue')),
|
||||
}, {
|
||||
name: 'note',
|
||||
path: '/notes/:noteId',
|
||||
component: page(() => import('@/pages/note.vue')),
|
||||
}, {
|
||||
name: 'list',
|
||||
path: '/list/:listId',
|
||||
component: page(() => import('@/pages/list.vue')),
|
||||
}, {
|
||||
path: '/clips/:clipId',
|
||||
component: page(() => import('@/pages/clip.vue')),
|
||||
}, {
|
||||
path: '/instance-info/:host',
|
||||
component: page(() => import('@/pages/instance-info.vue')),
|
||||
}, {
|
||||
name: 'settings',
|
||||
path: '/settings',
|
||||
component: page(() => import('@/pages/settings/index.vue')),
|
||||
loginRequired: true,
|
||||
children: [{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
component: page(() => import('@/pages/settings/profile.vue')),
|
||||
}, {
|
||||
path: '/avatar-decoration',
|
||||
name: 'avatarDecoration',
|
||||
component: page(() => import('@/pages/settings/avatar-decoration.vue')),
|
||||
}, {
|
||||
path: '/roles',
|
||||
name: 'roles',
|
||||
component: page(() => import('@/pages/settings/roles.vue')),
|
||||
}, {
|
||||
path: '/privacy',
|
||||
name: 'privacy',
|
||||
component: page(() => import('@/pages/settings/privacy.vue')),
|
||||
}, {
|
||||
path: '/emoji-picker',
|
||||
name: 'emojiPicker',
|
||||
component: page(() => import('@/pages/settings/emoji-picker.vue')),
|
||||
}, {
|
||||
path: '/drive',
|
||||
name: 'drive',
|
||||
component: page(() => import('@/pages/settings/drive.vue')),
|
||||
}, {
|
||||
path: '/drive/cleaner',
|
||||
name: 'drive',
|
||||
component: page(() => import('@/pages/settings/drive-cleaner.vue')),
|
||||
}, {
|
||||
path: '/notifications',
|
||||
name: 'notifications',
|
||||
component: page(() => import('@/pages/settings/notifications.vue')),
|
||||
}, {
|
||||
path: '/email',
|
||||
name: 'email',
|
||||
component: page(() => import('@/pages/settings/email.vue')),
|
||||
}, {
|
||||
path: '/security',
|
||||
name: 'security',
|
||||
component: page(() => import('@/pages/settings/security.vue')),
|
||||
}, {
|
||||
path: '/general',
|
||||
name: 'general',
|
||||
component: page(() => import('@/pages/settings/general.vue')),
|
||||
}, {
|
||||
path: '/theme/install',
|
||||
name: 'theme',
|
||||
component: page(() => import('@/pages/settings/theme.install.vue')),
|
||||
}, {
|
||||
path: '/theme/manage',
|
||||
name: 'theme',
|
||||
component: page(() => import('@/pages/settings/theme.manage.vue')),
|
||||
}, {
|
||||
path: '/theme',
|
||||
name: 'theme',
|
||||
component: page(() => import('@/pages/settings/theme.vue')),
|
||||
}, {
|
||||
path: '/navbar',
|
||||
name: 'navbar',
|
||||
component: page(() => import('@/pages/settings/navbar.vue')),
|
||||
}, {
|
||||
path: '/statusbar',
|
||||
name: 'statusbar',
|
||||
component: page(() => import('@/pages/settings/statusbar.vue')),
|
||||
}, {
|
||||
path: '/sounds',
|
||||
name: 'sounds',
|
||||
component: page(() => import('@/pages/settings/sounds.vue')),
|
||||
}, {
|
||||
path: '/plugin/install',
|
||||
name: 'plugin',
|
||||
component: page(() => import('@/pages/settings/plugin.install.vue')),
|
||||
}, {
|
||||
path: '/plugin',
|
||||
name: 'plugin',
|
||||
component: page(() => import('@/pages/settings/plugin.vue')),
|
||||
}, {
|
||||
path: '/import-export',
|
||||
name: 'import-export',
|
||||
component: page(() => import('@/pages/settings/import-export.vue')),
|
||||
}, {
|
||||
path: '/mute-block',
|
||||
name: 'mute-block',
|
||||
component: page(() => import('@/pages/settings/mute-block.vue')),
|
||||
}, {
|
||||
path: '/api',
|
||||
name: 'api',
|
||||
component: page(() => import('@/pages/settings/api.vue')),
|
||||
}, {
|
||||
path: '/apps',
|
||||
name: 'api',
|
||||
component: page(() => import('@/pages/settings/apps.vue')),
|
||||
}, {
|
||||
path: '/webhook/edit/:webhookId',
|
||||
name: 'webhook',
|
||||
component: page(() => import('@/pages/settings/webhook.edit.vue')),
|
||||
}, {
|
||||
path: '/webhook/new',
|
||||
name: 'webhook',
|
||||
component: page(() => import('@/pages/settings/webhook.new.vue')),
|
||||
}, {
|
||||
path: '/webhook',
|
||||
name: 'webhook',
|
||||
component: page(() => import('@/pages/settings/webhook.vue')),
|
||||
}, {
|
||||
path: '/deck',
|
||||
name: 'deck',
|
||||
component: page(() => import('@/pages/settings/deck.vue')),
|
||||
}, {
|
||||
path: '/preferences-backups',
|
||||
name: 'preferences-backups',
|
||||
component: page(() => import('@/pages/settings/preferences-backups.vue')),
|
||||
}, {
|
||||
path: '/migration',
|
||||
name: 'migration',
|
||||
component: page(() => import('@/pages/settings/migration.vue')),
|
||||
}, {
|
||||
path: '/custom-css',
|
||||
name: 'general',
|
||||
component: page(() => import('@/pages/settings/custom-css.vue')),
|
||||
}, {
|
||||
path: '/accounts',
|
||||
name: 'profile',
|
||||
component: page(() => import('@/pages/settings/accounts.vue')),
|
||||
}, {
|
||||
path: '/other',
|
||||
name: 'other',
|
||||
component: page(() => import('@/pages/settings/other.vue')),
|
||||
}, {
|
||||
path: '/',
|
||||
component: page(() => import('@/pages/_empty_.vue')),
|
||||
}],
|
||||
}, {
|
||||
path: '/reset-password/:token?',
|
||||
component: page(() => import('@/pages/reset-password.vue')),
|
||||
}, {
|
||||
path: '/signup-complete/:code',
|
||||
component: page(() => import('@/pages/signup-complete.vue')),
|
||||
}, {
|
||||
path: '/announcements',
|
||||
component: page(() => import('@/pages/announcements.vue')),
|
||||
}, {
|
||||
path: '/about',
|
||||
component: page(() => import('@/pages/about.vue')),
|
||||
hash: 'initialTab',
|
||||
}, {
|
||||
path: '/about-misskey',
|
||||
component: page(() => import('@/pages/about-misskey.vue')),
|
||||
}, {
|
||||
path: '/invite',
|
||||
name: 'invite',
|
||||
component: page(() => import('@/pages/invite.vue')),
|
||||
}, {
|
||||
path: '/ads',
|
||||
component: page(() => import('@/pages/ads.vue')),
|
||||
}, {
|
||||
path: '/theme-editor',
|
||||
component: page(() => import('@/pages/theme-editor.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/roles/:role',
|
||||
component: page(() => import('@/pages/role.vue')),
|
||||
}, {
|
||||
path: '/user-tags/:tag',
|
||||
component: page(() => import('@/pages/user-tag.vue')),
|
||||
}, {
|
||||
path: '/explore',
|
||||
component: page(() => import('@/pages/explore.vue')),
|
||||
hash: 'initialTab',
|
||||
}, {
|
||||
path: '/search',
|
||||
component: page(() => import('@/pages/search.vue')),
|
||||
query: {
|
||||
q: 'query',
|
||||
channel: 'channel',
|
||||
type: 'type',
|
||||
origin: 'origin',
|
||||
},
|
||||
}, {
|
||||
path: '/authorize-follow',
|
||||
component: page(() => import('@/pages/follow.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/share',
|
||||
component: page(() => import('@/pages/share.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/api-console',
|
||||
component: page(() => import('@/pages/api-console.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/scratchpad',
|
||||
component: page(() => import('@/pages/scratchpad.vue')),
|
||||
}, {
|
||||
path: '/auth/:token',
|
||||
component: page(() => import('@/pages/auth.vue')),
|
||||
}, {
|
||||
path: '/miauth/:session',
|
||||
component: page(() => import('@/pages/miauth.vue')),
|
||||
query: {
|
||||
callback: 'callback',
|
||||
name: 'name',
|
||||
icon: 'icon',
|
||||
permission: 'permission',
|
||||
},
|
||||
}, {
|
||||
path: '/oauth/authorize',
|
||||
component: page(() => import('@/pages/oauth.vue')),
|
||||
}, {
|
||||
path: '/tags/:tag',
|
||||
component: page(() => import('@/pages/tag.vue')),
|
||||
}, {
|
||||
path: '/pages/new',
|
||||
component: page(() => import('@/pages/page-editor/page-editor.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/pages/edit/:initPageId',
|
||||
component: page(() => import('@/pages/page-editor/page-editor.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/pages',
|
||||
component: page(() => import('@/pages/pages.vue')),
|
||||
}, {
|
||||
path: '/play/:id/edit',
|
||||
component: page(() => import('@/pages/flash/flash-edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/play/new',
|
||||
component: page(() => import('@/pages/flash/flash-edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/play/:id',
|
||||
component: page(() => import('@/pages/flash/flash.vue')),
|
||||
}, {
|
||||
path: '/play',
|
||||
component: page(() => import('@/pages/flash/flash-index.vue')),
|
||||
}, {
|
||||
path: '/gallery/:postId/edit',
|
||||
component: page(() => import('@/pages/gallery/edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/gallery/new',
|
||||
component: page(() => import('@/pages/gallery/edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/gallery/:postId',
|
||||
component: page(() => import('@/pages/gallery/post.vue')),
|
||||
}, {
|
||||
path: '/gallery',
|
||||
component: page(() => import('@/pages/gallery/index.vue')),
|
||||
}, {
|
||||
path: '/channels/:channelId/edit',
|
||||
component: page(() => import('@/pages/channel-editor.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/channels/new',
|
||||
component: page(() => import('@/pages/channel-editor.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/channels/:channelId',
|
||||
component: page(() => import('@/pages/channel.vue')),
|
||||
}, {
|
||||
path: '/channels',
|
||||
component: page(() => import('@/pages/channels.vue')),
|
||||
}, {
|
||||
path: '/custom-emojis-manager',
|
||||
component: page(() => import('@/pages/custom-emojis-manager.vue')),
|
||||
}, {
|
||||
path: '/avatar-decorations',
|
||||
name: 'avatarDecorations',
|
||||
component: page(() => import('@/pages/avatar-decorations.vue')),
|
||||
}, {
|
||||
path: '/registry/keys/:domain/:path(*)?',
|
||||
component: page(() => import('@/pages/registry.keys.vue')),
|
||||
}, {
|
||||
path: '/registry/value/:domain/:path(*)?',
|
||||
component: page(() => import('@/pages/registry.value.vue')),
|
||||
}, {
|
||||
path: '/registry',
|
||||
component: page(() => import('@/pages/registry.vue')),
|
||||
}, {
|
||||
path: '/install-extentions',
|
||||
component: page(() => import('@/pages/install-extentions.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/admin/user/:userId',
|
||||
component: iAmModerator ? page(() => import('@/pages/admin-user.vue')) : page(() => import('@/pages/not-found.vue')),
|
||||
}, {
|
||||
path: '/admin/file/:fileId',
|
||||
component: iAmModerator ? page(() => import('@/pages/admin-file.vue')) : page(() => import('@/pages/not-found.vue')),
|
||||
}, {
|
||||
path: '/admin',
|
||||
component: iAmModerator ? page(() => import('@/pages/admin/index.vue')) : page(() => import('@/pages/not-found.vue')),
|
||||
children: [{
|
||||
path: '/overview',
|
||||
name: 'overview',
|
||||
component: page(() => import('@/pages/admin/overview.vue')),
|
||||
}, {
|
||||
path: '/users',
|
||||
name: 'users',
|
||||
component: page(() => import('@/pages/admin/users.vue')),
|
||||
}, {
|
||||
path: '/emojis',
|
||||
name: 'emojis',
|
||||
component: page(() => import('@/pages/custom-emojis-manager.vue')),
|
||||
}, {
|
||||
path: '/avatar-decorations',
|
||||
name: 'avatarDecorations',
|
||||
component: page(() => import('@/pages/avatar-decorations.vue')),
|
||||
}, {
|
||||
path: '/queue',
|
||||
name: 'queue',
|
||||
component: page(() => import('@/pages/admin/queue.vue')),
|
||||
}, {
|
||||
path: '/files',
|
||||
name: 'files',
|
||||
component: page(() => import('@/pages/admin/files.vue')),
|
||||
}, {
|
||||
path: '/federation',
|
||||
name: 'federation',
|
||||
component: page(() => import('@/pages/admin/federation.vue')),
|
||||
}, {
|
||||
path: '/announcements',
|
||||
name: 'announcements',
|
||||
component: page(() => import('@/pages/admin/announcements.vue')),
|
||||
}, {
|
||||
path: '/ads',
|
||||
name: 'ads',
|
||||
component: page(() => import('@/pages/admin/ads.vue')),
|
||||
}, {
|
||||
path: '/roles/:id/edit',
|
||||
name: 'roles',
|
||||
component: page(() => import('@/pages/admin/roles.edit.vue')),
|
||||
}, {
|
||||
path: '/roles/new',
|
||||
name: 'roles',
|
||||
component: page(() => import('@/pages/admin/roles.edit.vue')),
|
||||
}, {
|
||||
path: '/roles/:id',
|
||||
name: 'roles',
|
||||
component: page(() => import('@/pages/admin/roles.role.vue')),
|
||||
}, {
|
||||
path: '/roles',
|
||||
name: 'roles',
|
||||
component: page(() => import('@/pages/admin/roles.vue')),
|
||||
}, {
|
||||
path: '/database',
|
||||
name: 'database',
|
||||
component: page(() => import('@/pages/admin/database.vue')),
|
||||
}, {
|
||||
path: '/abuses',
|
||||
name: 'abuses',
|
||||
component: page(() => import('@/pages/admin/abuses.vue')),
|
||||
}, {
|
||||
path: '/modlog',
|
||||
name: 'modlog',
|
||||
component: page(() => import('@/pages/admin/modlog.vue')),
|
||||
}, {
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: page(() => import('@/pages/admin/settings.vue')),
|
||||
}, {
|
||||
path: '/branding',
|
||||
name: 'branding',
|
||||
component: page(() => import('@/pages/admin/branding.vue')),
|
||||
}, {
|
||||
path: '/moderation',
|
||||
name: 'moderation',
|
||||
component: page(() => import('@/pages/admin/moderation.vue')),
|
||||
}, {
|
||||
path: '/email-settings',
|
||||
name: 'email-settings',
|
||||
component: page(() => import('@/pages/admin/email-settings.vue')),
|
||||
}, {
|
||||
path: '/object-storage',
|
||||
name: 'object-storage',
|
||||
component: page(() => import('@/pages/admin/object-storage.vue')),
|
||||
}, {
|
||||
path: '/security',
|
||||
name: 'security',
|
||||
component: page(() => import('@/pages/admin/security.vue')),
|
||||
}, {
|
||||
path: '/relays',
|
||||
name: 'relays',
|
||||
component: page(() => import('@/pages/admin/relays.vue')),
|
||||
}, {
|
||||
path: '/instance-block',
|
||||
name: 'instance-block',
|
||||
component: page(() => import('@/pages/admin/instance-block.vue')),
|
||||
}, {
|
||||
path: '/proxy-account',
|
||||
name: 'proxy-account',
|
||||
component: page(() => import('@/pages/admin/proxy-account.vue')),
|
||||
}, {
|
||||
path: '/external-services',
|
||||
name: 'external-services',
|
||||
component: page(() => import('@/pages/admin/external-services.vue')),
|
||||
}, {
|
||||
path: '/other-settings',
|
||||
name: 'other-settings',
|
||||
component: page(() => import('@/pages/admin/other-settings.vue')),
|
||||
}, {
|
||||
path: '/server-rules',
|
||||
name: 'server-rules',
|
||||
component: page(() => import('@/pages/admin/server-rules.vue')),
|
||||
}, {
|
||||
path: '/invites',
|
||||
name: 'invites',
|
||||
component: page(() => import('@/pages/admin/invites.vue')),
|
||||
}, {
|
||||
path: '/',
|
||||
component: page(() => import('@/pages/_empty_.vue')),
|
||||
}],
|
||||
}, {
|
||||
path: '/my/notifications',
|
||||
component: page(() => import('@/pages/notifications.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/favorites',
|
||||
component: page(() => import('@/pages/favorites.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/achievements',
|
||||
component: page(() => import('@/pages/achievements.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/drive/folder/:folder',
|
||||
component: page(() => import('@/pages/drive.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/drive',
|
||||
component: page(() => import('@/pages/drive.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/drive/file/:fileId',
|
||||
component: page(() => import('@/pages/drive.file.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/follow-requests',
|
||||
component: page(() => import('@/pages/follow-requests.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/lists/:listId',
|
||||
component: page(() => import('@/pages/my-lists/list.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/lists',
|
||||
component: page(() => import('@/pages/my-lists/index.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/clips',
|
||||
component: page(() => import('@/pages/my-clips/index.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/antennas/create',
|
||||
component: page(() => import('@/pages/my-antennas/create.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/antennas/:antennaId',
|
||||
component: page(() => import('@/pages/my-antennas/edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/antennas',
|
||||
component: page(() => import('@/pages/my-antennas/index.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/timeline/list/:listId',
|
||||
component: page(() => import('@/pages/user-list-timeline.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/timeline/antenna/:antennaId',
|
||||
component: page(() => import('@/pages/antenna-timeline.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/clicker',
|
||||
component: page(() => import('@/pages/clicker.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/bubble-game',
|
||||
component: page(() => import('@/pages/drop-and-fusion.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/timeline',
|
||||
component: page(() => import('@/pages/timeline.vue')),
|
||||
}, {
|
||||
name: 'index',
|
||||
path: '/',
|
||||
component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
|
||||
globalCacheKey: 'index',
|
||||
}, {
|
||||
path: '/:(*)',
|
||||
component: page(() => import('@/pages/not-found.vue')),
|
||||
}];
|
||||
|
||||
function createRouterImpl(path: string): IRouter {
|
||||
return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue')));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。
|
||||
* また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能)
|
||||
*/
|
||||
export function setupRouter(app: App) {
|
||||
app.provide('routerFactory', createRouterImpl);
|
||||
|
||||
const mainRouter = createRouterImpl(location.pathname + location.search + location.hash);
|
||||
|
||||
window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
|
||||
|
||||
window.addEventListener('popstate', (event) => {
|
||||
mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
|
||||
});
|
||||
|
||||
mainRouter.addListener('push', ctx => {
|
||||
window.history.pushState({ key: ctx.key }, '', ctx.path);
|
||||
});
|
||||
|
||||
setMainRouter(mainRouter);
|
||||
}
|
||||
163
packages/frontend/src/global/router/main.ts
Normal file
163
packages/frontend/src/global/router/main.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { ShallowRef } from 'vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
|
||||
|
||||
function getMainRouter(): IRouter {
|
||||
const router = mainRouterHolder;
|
||||
if (!router) {
|
||||
throw new Error('mainRouter is not found.');
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* メインルータを設定する。一度設定すると、それ以降は変更できない。
|
||||
* {@link setupRouter}から呼び出されることのみを想定している。
|
||||
*/
|
||||
export function setMainRouter(router: IRouter) {
|
||||
if (mainRouterHolder) {
|
||||
throw new Error('mainRouter is already exists.');
|
||||
}
|
||||
|
||||
mainRouterHolder = router;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link mainRouter}用のプロキシ実装。
|
||||
* {@link mainRouter}は起動シーケンスの一部にて初期化されるため、僅かにundefinedになる期間がある。
|
||||
* その僅かな期間のためだけに型をundefined込みにしたくないのでこのクラスを緩衝材として使用する。
|
||||
*/
|
||||
class MainRouterProxy implements IRouter {
|
||||
private supplier: () => IRouter;
|
||||
|
||||
constructor(supplier: () => IRouter) {
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
get current(): Resolved {
|
||||
return this.supplier().current;
|
||||
}
|
||||
|
||||
get currentRef(): ShallowRef<Resolved> {
|
||||
return this.supplier().currentRef;
|
||||
}
|
||||
|
||||
get currentRoute(): ShallowRef<RouteDef> {
|
||||
return this.supplier().currentRoute;
|
||||
}
|
||||
|
||||
get navHook(): ((path: string, flag?: any) => boolean) | null {
|
||||
return this.supplier().navHook;
|
||||
}
|
||||
|
||||
set navHook(value) {
|
||||
this.supplier().navHook = value;
|
||||
}
|
||||
|
||||
getCurrentKey(): string {
|
||||
return this.supplier().getCurrentKey();
|
||||
}
|
||||
|
||||
getCurrentPath(): any {
|
||||
return this.supplier().getCurrentPath();
|
||||
}
|
||||
|
||||
push(path: string, flag?: any): void {
|
||||
this.supplier().push(path, flag);
|
||||
}
|
||||
|
||||
replace(path: string, key?: string | null): void {
|
||||
this.supplier().replace(path, key);
|
||||
}
|
||||
|
||||
resolve(path: string): Resolved | null {
|
||||
return this.supplier().resolve(path);
|
||||
}
|
||||
|
||||
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
|
||||
return this.supplier().eventNames();
|
||||
}
|
||||
|
||||
listeners<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
): Array<EventEmitter.EventListener<RouterEvent, T>> {
|
||||
return this.supplier().listeners(event);
|
||||
}
|
||||
|
||||
listenerCount(
|
||||
event: EventEmitter.EventNames<RouterEvent>,
|
||||
): number {
|
||||
return this.supplier().listenerCount(event);
|
||||
}
|
||||
|
||||
emit<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
...args: EventEmitter.EventArgs<RouterEvent, T>
|
||||
): boolean {
|
||||
return this.supplier().emit(event, ...args);
|
||||
}
|
||||
|
||||
on<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||
context?: any,
|
||||
): this {
|
||||
this.supplier().on(event, fn, context);
|
||||
return this;
|
||||
}
|
||||
|
||||
addListener<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||
context?: any,
|
||||
): this {
|
||||
this.supplier().addListener(event, fn, context);
|
||||
return this;
|
||||
}
|
||||
|
||||
once<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||
context?: any,
|
||||
): this {
|
||||
this.supplier().once(event, fn, context);
|
||||
return this;
|
||||
}
|
||||
|
||||
removeListener<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
fn?: EventEmitter.EventListener<RouterEvent, T>,
|
||||
context?: any,
|
||||
once?: boolean,
|
||||
): this {
|
||||
this.supplier().removeListener(event, fn, context, once);
|
||||
return this;
|
||||
}
|
||||
|
||||
off<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
fn?: EventEmitter.EventListener<RouterEvent, T>,
|
||||
context?: any,
|
||||
once?: boolean,
|
||||
): this {
|
||||
this.supplier().off(event, fn, context, once);
|
||||
return this;
|
||||
}
|
||||
|
||||
removeAllListeners(
|
||||
event?: EventEmitter.EventNames<RouterEvent>,
|
||||
): this {
|
||||
this.supplier().removeAllListeners(event);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
let mainRouterHolder: IRouter | null = null;
|
||||
|
||||
export const mainRouter: IRouter = new MainRouterProxy(getMainRouter);
|
||||
30
packages/frontend/src/global/router/supplier.ts
Normal file
30
packages/frontend/src/global/router/supplier.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { inject } from 'vue';
|
||||
import { IRouter, Router } from '@/nirax.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
/**
|
||||
* メインの{@link Router}を取得する。
|
||||
* あらかじめ{@link setupRouter}を実行しておく必要がある({@link provide}により{@link IRouter}のインスタンスを注入可能であるならばこの限りではない)
|
||||
*/
|
||||
export function useRouter(): IRouter {
|
||||
return inject<Router | null>('router', null) ?? mainRouter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任意の{@link Router}を取得するためのファクトリを取得する。
|
||||
* あらかじめ{@link setupRouter}を実行しておく必要がある。
|
||||
*/
|
||||
export function useRouterFactory(): (path: string) => IRouter {
|
||||
const factory = inject<(path: string) => IRouter>('routerFactory');
|
||||
if (!factory) {
|
||||
console.error('routerFactory is not defined.');
|
||||
throw new Error('routerFactory is not defined.');
|
||||
}
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
worker-src 'self';
|
||||
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com;
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||
img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||
connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
|
||||
// NIRAX --- A lightweight router
|
||||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
|
||||
|
||||
type RouteDef = {
|
||||
export type RouteDef = {
|
||||
path: string;
|
||||
component: Component;
|
||||
query?: Record<string, string>;
|
||||
|
|
@ -27,6 +27,27 @@ type ParsedPath = (string | {
|
|||
optional?: boolean;
|
||||
})[];
|
||||
|
||||
export type RouterEvent = {
|
||||
change: (ctx: {
|
||||
beforePath: string;
|
||||
path: string;
|
||||
resolved: Resolved;
|
||||
key: string;
|
||||
}) => void;
|
||||
replace: (ctx: {
|
||||
path: string;
|
||||
key: string;
|
||||
}) => void;
|
||||
push: (ctx: {
|
||||
beforePath: string;
|
||||
path: string;
|
||||
route: RouteDef | null;
|
||||
props: Map<string, string> | null;
|
||||
key: string;
|
||||
}) => void;
|
||||
same: () => void;
|
||||
}
|
||||
|
||||
export type Resolved = { route: RouteDef; props: Map<string, string | boolean>; child?: Resolved; };
|
||||
|
||||
function parsePath(path: string): ParsedPath {
|
||||
|
|
@ -54,26 +75,85 @@ function parsePath(path: string): ParsedPath {
|
|||
return res;
|
||||
}
|
||||
|
||||
export class Router extends EventEmitter<{
|
||||
change: (ctx: {
|
||||
beforePath: string;
|
||||
path: string;
|
||||
resolved: Resolved;
|
||||
key: string;
|
||||
}) => void;
|
||||
replace: (ctx: {
|
||||
path: string;
|
||||
key: string;
|
||||
}) => void;
|
||||
push: (ctx: {
|
||||
beforePath: string;
|
||||
path: string;
|
||||
route: RouteDef | null;
|
||||
props: Map<string, string> | null;
|
||||
key: string;
|
||||
}) => void;
|
||||
same: () => void;
|
||||
}> {
|
||||
export interface IRouter extends EventEmitter<RouterEvent> {
|
||||
current: Resolved;
|
||||
currentRef: ShallowRef<Resolved>;
|
||||
currentRoute: ShallowRef<RouteDef>;
|
||||
navHook: ((path: string, flag?: any) => boolean) | null;
|
||||
|
||||
resolve(path: string): Resolved | null;
|
||||
|
||||
getCurrentPath(): any;
|
||||
|
||||
getCurrentKey(): string;
|
||||
|
||||
push(path: string, flag?: any): void;
|
||||
|
||||
replace(path: string, key?: string | null): void;
|
||||
|
||||
/** @see EventEmitter */
|
||||
eventNames(): Array<EventEmitter.EventNames<RouterEvent>>;
|
||||
|
||||
/** @see EventEmitter */
|
||||
listeners<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T
|
||||
): Array<EventEmitter.EventListener<RouterEvent, T>>;
|
||||
|
||||
/** @see EventEmitter */
|
||||
listenerCount(
|
||||
event: EventEmitter.EventNames<RouterEvent>
|
||||
): number;
|
||||
|
||||
/** @see EventEmitter */
|
||||
emit<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
...args: EventEmitter.EventArgs<RouterEvent, T>
|
||||
): boolean;
|
||||
|
||||
/** @see EventEmitter */
|
||||
on<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||
context?: any
|
||||
): this;
|
||||
|
||||
/** @see EventEmitter */
|
||||
addListener<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||
context?: any
|
||||
): this;
|
||||
|
||||
/** @see EventEmitter */
|
||||
once<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
fn: EventEmitter.EventListener<RouterEvent, T>,
|
||||
context?: any
|
||||
): this;
|
||||
|
||||
/** @see EventEmitter */
|
||||
removeListener<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
fn?: EventEmitter.EventListener<RouterEvent, T>,
|
||||
context?: any,
|
||||
once?: boolean | undefined
|
||||
): this;
|
||||
|
||||
/** @see EventEmitter */
|
||||
off<T extends EventEmitter.EventNames<RouterEvent>>(
|
||||
event: T,
|
||||
fn?: EventEmitter.EventListener<RouterEvent, T>,
|
||||
context?: any,
|
||||
once?: boolean | undefined
|
||||
): this;
|
||||
|
||||
/** @see EventEmitter */
|
||||
removeAllListeners(
|
||||
event?: EventEmitter.EventNames<RouterEvent>
|
||||
): this;
|
||||
}
|
||||
|
||||
export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
||||
private routes: RouteDef[];
|
||||
public current: Resolved;
|
||||
public currentRef: ShallowRef<Resolved> = shallowRef();
|
||||
|
|
@ -277,7 +357,7 @@ export class Router extends EventEmitter<{
|
|||
}
|
||||
}
|
||||
|
||||
export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: Router) {
|
||||
export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: IRouter) {
|
||||
const scrollPosStore = new Map<string, number>();
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ import { instance } from '@/instance.js';
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import {bannerDark, bannerLight, defaultStore, iconDark, iconLight} from "@/store.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ import * as os from '@/os.js';
|
|||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { rolesCache } from '@/cache.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -70,12 +70,12 @@ import * as os from '@/os.js';
|
|||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -261,9 +261,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { ROLE_POLICIES } from '@/const.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
const baseRoleQ = ref('');
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ import MkTimeline from '@/components/MkTimeline.vue';
|
|||
import { scroll } from '@/scripts/scroll.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -77,12 +77,12 @@ import MkColorInput from '@/components/MkColorInput.vue';
|
|||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ import MkTimeline from '@/components/MkTimeline.vue';
|
|||
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
|
@ -92,6 +91,7 @@ import { PageHeaderItem } from '@/types/page-header.js';
|
|||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -58,9 +58,9 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ import { infoImageUrl } from '@/instance.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
1221
packages/frontend/src/pages/drop-and-fusion.game.vue
Normal file
1221
packages/frontend/src/pages/drop-and-fusion.game.vue
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -45,7 +45,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||
import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const PRESET_DEFAULT = `/// @ 0.16.0
|
||||
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ import { computed, ref } from 'vue';
|
|||
import MkFlashPreview from '@/components/MkFlashPreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import { } from 'vue';
|
|||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
async function follow(user): Promise<void> {
|
||||
const { canceled } = await os.confirm({
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ import FormSuspense from '@/components/form/suspense.vue';
|
|||
import { selectFiles } from '@/scripts/select-file.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ import MkPagination from '@/components/MkPagination.vue';
|
|||
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -72,13 +72,13 @@ import MkPagination from '@/components/MkPagination.vue';
|
|||
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
import { url } from '@/config.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import { ref } from 'vue';
|
|||
import XAntenna from './editor.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { antennasCache } from '@/cache.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ import * as Misskey from 'misskey-js';
|
|||
import XAntenna from './editor.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { antennasCache } from '@/cache.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ import * as Misskey from 'misskey-js';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
|
|
@ -70,6 +69,7 @@ import { userListsCache } from '@/cache.js';
|
|||
import { signinRequired } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
|
|
|
|||
|
|
@ -73,10 +73,10 @@ import { url } from '@/config.js';
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
const props = defineProps<{
|
||||
initPageId?: string;
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ import { computed, ref } from 'vue';
|
|||
import MkPagePreview from '@/components/MkPagePreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
const props = defineProps<{
|
||||
token?: string;
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ import { i18n } from '@/i18n.js';
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import { i18n } from '@/i18n.js';
|
|||
import * as os from '@/os.js';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
|||
import { signout, $i } from '@/account.js';
|
||||
import { clearCache } from '@/scripts/clear-cache.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import * as os from '@/os.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const indexInfo = {
|
||||
title: i18n.ts.settings,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import MkRange from '@/components/MkRange.vue';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { playFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
|
||||
import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
|
||||
import { selectFile } from '@/scripts/select-file.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -119,7 +119,7 @@ function listen() {
|
|||
return;
|
||||
}
|
||||
|
||||
playFile(type.value === '_driveFile_' ? {
|
||||
playMisskeySfxFile(type.value === '_driveFile_' ? {
|
||||
type: '_driveFile_',
|
||||
fileId: fileId.value as string,
|
||||
fileUrl: fileUrl.value as string,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ import * as os from '@/os.js';
|
|||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ import * as Misskey from 'misskey-js';
|
|||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import { scroll } from '@/scripts/scroll.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ import {getUserMenu} from '@/scripts/get-user-menu.js';
|
|||
import number from '@/filters/number.js';
|
||||
import {userPage} from '@/filters/user.js';
|
||||
import * as os from '@/os.js';
|
||||
import {useRouter} from '@/router.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
import {i18n} from '@/i18n.js';
|
||||
import {$i, iAmModerator} from '@/account.js';
|
||||
import {dateString} from '@/filters/date.js';
|
||||
|
|
|
|||
|
|
@ -1,565 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue';
|
||||
import { Router } from '@/nirax.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import MkLoading from '@/pages/_loading_.vue';
|
||||
import MkError from '@/pages/_error_.vue';
|
||||
|
||||
export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
|
||||
loader: loader,
|
||||
loadingComponent: MkLoading,
|
||||
errorComponent: MkError,
|
||||
});
|
||||
|
||||
export const routes = [{
|
||||
path: '/@:initUser/pages/:initPageName/view-source',
|
||||
component: page(() => import('./pages/page-editor/page-editor.vue')),
|
||||
}, {
|
||||
path: '/@:username/pages/:pageName',
|
||||
component: page(() => import('./pages/page.vue')),
|
||||
}, {
|
||||
path: '/@:acct/following',
|
||||
component: page(() => import('./pages/user/following.vue')),
|
||||
}, {
|
||||
path: '/@:acct/followers',
|
||||
component: page(() => import('./pages/user/followers.vue')),
|
||||
}, {
|
||||
name: 'user',
|
||||
path: '/@:acct/:page?',
|
||||
component: page(() => import('./pages/user/index.vue')),
|
||||
}, {
|
||||
name: 'note',
|
||||
path: '/notes/:noteId',
|
||||
component: page(() => import('./pages/note.vue')),
|
||||
}, {
|
||||
name: 'list',
|
||||
path: '/list/:listId',
|
||||
component: page(() => import('./pages/list.vue')),
|
||||
}, {
|
||||
path: '/clips/:clipId',
|
||||
component: page(() => import('./pages/clip.vue')),
|
||||
}, {
|
||||
path: '/instance-info/:host',
|
||||
component: page(() => import('./pages/instance-info.vue')),
|
||||
}, {
|
||||
name: 'settings',
|
||||
path: '/settings',
|
||||
component: page(() => import('./pages/settings/index.vue')),
|
||||
loginRequired: true,
|
||||
children: [{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
component: page(() => import('./pages/settings/profile.vue')),
|
||||
}, {
|
||||
path: '/avatar-decoration',
|
||||
name: 'avatarDecoration',
|
||||
component: page(() => import('./pages/settings/avatar-decoration.vue')),
|
||||
}, {
|
||||
path: '/roles',
|
||||
name: 'roles',
|
||||
component: page(() => import('./pages/settings/roles.vue')),
|
||||
}, {
|
||||
path: '/privacy',
|
||||
name: 'privacy',
|
||||
component: page(() => import('./pages/settings/privacy.vue')),
|
||||
}, {
|
||||
path: '/emoji-picker',
|
||||
name: 'emojiPicker',
|
||||
component: page(() => import('./pages/settings/emoji-picker.vue')),
|
||||
}, {
|
||||
path: '/drive',
|
||||
name: 'drive',
|
||||
component: page(() => import('./pages/settings/drive.vue')),
|
||||
}, {
|
||||
path: '/drive/cleaner',
|
||||
name: 'drive',
|
||||
component: page(() => import('./pages/settings/drive-cleaner.vue')),
|
||||
}, {
|
||||
path: '/notifications',
|
||||
name: 'notifications',
|
||||
component: page(() => import('./pages/settings/notifications.vue')),
|
||||
}, {
|
||||
path: '/email',
|
||||
name: 'email',
|
||||
component: page(() => import('./pages/settings/email.vue')),
|
||||
}, {
|
||||
path: '/security',
|
||||
name: 'security',
|
||||
component: page(() => import('./pages/settings/security.vue')),
|
||||
}, {
|
||||
path: '/general',
|
||||
name: 'general',
|
||||
component: page(() => import('./pages/settings/general.vue')),
|
||||
}, {
|
||||
path: '/theme/install',
|
||||
name: 'theme',
|
||||
component: page(() => import('./pages/settings/theme.install.vue')),
|
||||
}, {
|
||||
path: '/theme/manage',
|
||||
name: 'theme',
|
||||
component: page(() => import('./pages/settings/theme.manage.vue')),
|
||||
}, {
|
||||
path: '/theme',
|
||||
name: 'theme',
|
||||
component: page(() => import('./pages/settings/theme.vue')),
|
||||
}, {
|
||||
path: '/navbar',
|
||||
name: 'navbar',
|
||||
component: page(() => import('./pages/settings/navbar.vue')),
|
||||
}, {
|
||||
path: '/statusbar',
|
||||
name: 'statusbar',
|
||||
component: page(() => import('./pages/settings/statusbar.vue')),
|
||||
}, {
|
||||
path: '/sounds',
|
||||
name: 'sounds',
|
||||
component: page(() => import('./pages/settings/sounds.vue')),
|
||||
}, {
|
||||
path: '/plugin/install',
|
||||
name: 'plugin',
|
||||
component: page(() => import('./pages/settings/plugin.install.vue')),
|
||||
}, {
|
||||
path: '/plugin',
|
||||
name: 'plugin',
|
||||
component: page(() => import('./pages/settings/plugin.vue')),
|
||||
}, {
|
||||
path: '/import-export',
|
||||
name: 'import-export',
|
||||
component: page(() => import('./pages/settings/import-export.vue')),
|
||||
}, {
|
||||
path: '/mute-block',
|
||||
name: 'mute-block',
|
||||
component: page(() => import('./pages/settings/mute-block.vue')),
|
||||
}, {
|
||||
path: '/api',
|
||||
name: 'api',
|
||||
component: page(() => import('./pages/settings/api.vue')),
|
||||
}, {
|
||||
path: '/apps',
|
||||
name: 'api',
|
||||
component: page(() => import('./pages/settings/apps.vue')),
|
||||
}, {
|
||||
path: '/webhook/edit/:webhookId',
|
||||
name: 'webhook',
|
||||
component: page(() => import('./pages/settings/webhook.edit.vue')),
|
||||
}, {
|
||||
path: '/webhook/new',
|
||||
name: 'webhook',
|
||||
component: page(() => import('./pages/settings/webhook.new.vue')),
|
||||
}, {
|
||||
path: '/webhook',
|
||||
name: 'webhook',
|
||||
component: page(() => import('./pages/settings/webhook.vue')),
|
||||
}, {
|
||||
path: '/deck',
|
||||
name: 'deck',
|
||||
component: page(() => import('./pages/settings/deck.vue')),
|
||||
}, {
|
||||
path: '/preferences-backups',
|
||||
name: 'preferences-backups',
|
||||
component: page(() => import('./pages/settings/preferences-backups.vue')),
|
||||
}, {
|
||||
path: '/migration',
|
||||
name: 'migration',
|
||||
component: page(() => import('./pages/settings/migration.vue')),
|
||||
}, {
|
||||
path: '/custom-css',
|
||||
name: 'general',
|
||||
component: page(() => import('./pages/settings/custom-css.vue')),
|
||||
}, {
|
||||
path: '/accounts',
|
||||
name: 'profile',
|
||||
component: page(() => import('./pages/settings/accounts.vue')),
|
||||
}, {
|
||||
path: '/other',
|
||||
name: 'other',
|
||||
component: page(() => import('./pages/settings/other.vue')),
|
||||
},{
|
||||
path: '/account-stats',
|
||||
name: 'other',
|
||||
component: page(() => import('./pages/settings/account-stats.vue')),
|
||||
},{
|
||||
path: '/',
|
||||
component: page(() => import('./pages/_empty_.vue')),
|
||||
}],
|
||||
}, {
|
||||
path: '/reset-password/:token?',
|
||||
component: page(() => import('./pages/reset-password.vue')),
|
||||
}, {
|
||||
path: '/signup-complete/:code',
|
||||
component: page(() => import('./pages/signup-complete.vue')),
|
||||
}, {
|
||||
path: '/announcements',
|
||||
component: page(() => import('./pages/announcements.vue')),
|
||||
}, {
|
||||
path: '/about',
|
||||
component: page(() => import('./pages/about.vue')),
|
||||
hash: 'initialTab',
|
||||
}, {
|
||||
path: '/about-misskey',
|
||||
component: page(() => import('./pages/about-misskey.vue')),
|
||||
}, {
|
||||
path: '/invite',
|
||||
name: 'invite',
|
||||
component: page(() => import('./pages/invite.vue')),
|
||||
}, {
|
||||
path: '/ads',
|
||||
component: page(() => import('./pages/ads.vue')),
|
||||
}, {
|
||||
path: '/theme-editor',
|
||||
component: page(() => import('./pages/theme-editor.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/roles/:role',
|
||||
component: page(() => import('./pages/role.vue')),
|
||||
}, {
|
||||
path: '/user-tags/:tag',
|
||||
component: page(() => import('./pages/user-tag.vue')),
|
||||
}, {
|
||||
path: '/explore',
|
||||
component: page(() => import('./pages/explore.vue')),
|
||||
hash: 'initialTab',
|
||||
}, {
|
||||
path: '/search',
|
||||
component: page(() => import('./pages/search.vue')),
|
||||
query: {
|
||||
q: 'query',
|
||||
channel: 'channel',
|
||||
type: 'type',
|
||||
origin: 'origin',
|
||||
},
|
||||
}, {
|
||||
path: '/authorize-follow',
|
||||
component: page(() => import('./pages/follow.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/share',
|
||||
component: page(() => import('./pages/share.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/api-console',
|
||||
component: page(() => import('./pages/api-console.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/scratchpad',
|
||||
component: page(() => import('./pages/scratchpad.vue')),
|
||||
}, {
|
||||
path: '/auth/:token',
|
||||
component: page(() => import('./pages/auth.vue')),
|
||||
}, {
|
||||
path: '/miauth/:session',
|
||||
component: page(() => import('./pages/miauth.vue')),
|
||||
query: {
|
||||
callback: 'callback',
|
||||
name: 'name',
|
||||
icon: 'icon',
|
||||
permission: 'permission',
|
||||
},
|
||||
}, {
|
||||
path: '/oauth/authorize',
|
||||
component: page(() => import('./pages/oauth.vue')),
|
||||
}, {
|
||||
path: '/tags/:tag',
|
||||
component: page(() => import('./pages/tag.vue')),
|
||||
}, {
|
||||
path: '/pages/new',
|
||||
component: page(() => import('./pages/page-editor/page-editor.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/pages/edit/:initPageId',
|
||||
component: page(() => import('./pages/page-editor/page-editor.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/pages',
|
||||
component: page(() => import('./pages/pages.vue')),
|
||||
}, {
|
||||
path: '/play/:id/edit',
|
||||
component: page(() => import('./pages/flash/flash-edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/play/new',
|
||||
component: page(() => import('./pages/flash/flash-edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/play/:id',
|
||||
component: page(() => import('./pages/flash/flash.vue')),
|
||||
}, {
|
||||
path: '/play',
|
||||
component: page(() => import('./pages/flash/flash-index.vue')),
|
||||
}, {
|
||||
path: '/gallery/:postId/edit',
|
||||
component: page(() => import('./pages/gallery/edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/gallery/new',
|
||||
component: page(() => import('./pages/gallery/edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/gallery/:postId',
|
||||
component: page(() => import('./pages/gallery/post.vue')),
|
||||
}, {
|
||||
path: '/gallery',
|
||||
component: page(() => import('./pages/gallery/index.vue')),
|
||||
}, {
|
||||
path: '/channels/:channelId/edit',
|
||||
component: page(() => import('./pages/channel-editor.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/channels/new',
|
||||
component: page(() => import('./pages/channel-editor.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/channels/:channelId',
|
||||
component: page(() => import('./pages/channel.vue')),
|
||||
}, {
|
||||
path: '/channels',
|
||||
component: page(() => import('./pages/channels.vue')),
|
||||
}, {
|
||||
path: '/custom-emojis-manager',
|
||||
component: page(() => import('./pages/custom-emojis-manager.vue')),
|
||||
}, {
|
||||
path: '/avatar-decorations',
|
||||
name: 'avatarDecorations',
|
||||
component: page(() => import('./pages/avatar-decorations.vue')),
|
||||
}, {
|
||||
path: '/registry/keys/:domain/:path(*)?',
|
||||
component: page(() => import('./pages/registry.keys.vue')),
|
||||
}, {
|
||||
path: '/registry/value/:domain/:path(*)?',
|
||||
component: page(() => import('./pages/registry.value.vue')),
|
||||
}, {
|
||||
path: '/registry',
|
||||
component: page(() => import('./pages/registry.vue')),
|
||||
}, {
|
||||
path: '/install-extentions',
|
||||
component: page(() => import('./pages/install-extentions.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/admin/user/:userId',
|
||||
component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.vue')),
|
||||
}, {
|
||||
path: '/admin/file/:fileId',
|
||||
component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')),
|
||||
}, {
|
||||
path: '/admin',
|
||||
component: iAmModerator ? page(() => import('./pages/admin/index.vue')) : page(() => import('./pages/not-found.vue')),
|
||||
children: [{
|
||||
path: '/overview',
|
||||
name: 'overview',
|
||||
component: page(() => import('./pages/admin/overview.vue')),
|
||||
}, {
|
||||
path: '/users',
|
||||
name: 'users',
|
||||
component: page(() => import('./pages/admin/users.vue')),
|
||||
}, {
|
||||
path: '/emojis',
|
||||
name: 'emojis',
|
||||
component: page(() => import('./pages/custom-emojis-manager.vue')),
|
||||
}, {
|
||||
path: '/avatar-decorations',
|
||||
name: 'avatarDecorations',
|
||||
component: page(() => import('./pages/avatar-decorations.vue')),
|
||||
}, {
|
||||
path: '/queue',
|
||||
name: 'queue',
|
||||
component: page(() => import('./pages/admin/queue.vue')),
|
||||
}, {
|
||||
path: '/files',
|
||||
name: 'files',
|
||||
component: page(() => import('./pages/admin/files.vue')),
|
||||
}, {
|
||||
path: '/federation',
|
||||
name: 'federation',
|
||||
component: page(() => import('./pages/admin/federation.vue')),
|
||||
}, {
|
||||
path: '/announcements',
|
||||
name: 'announcements',
|
||||
component: page(() => import('./pages/admin/announcements.vue')),
|
||||
}, {
|
||||
path: '/ads',
|
||||
name: 'ads',
|
||||
component: page(() => import('./pages/admin/ads.vue')),
|
||||
}, {
|
||||
path: '/roles/:id/edit',
|
||||
name: 'roles',
|
||||
component: page(() => import('./pages/admin/roles.edit.vue')),
|
||||
}, {
|
||||
path: '/roles/new',
|
||||
name: 'roles',
|
||||
component: page(() => import('./pages/admin/roles.edit.vue')),
|
||||
}, {
|
||||
path: '/roles/:id',
|
||||
name: 'roles',
|
||||
component: page(() => import('./pages/admin/roles.role.vue')),
|
||||
}, {
|
||||
path: '/roles',
|
||||
name: 'roles',
|
||||
component: page(() => import('./pages/admin/roles.vue')),
|
||||
}, {
|
||||
path: '/database',
|
||||
name: 'database',
|
||||
component: page(() => import('./pages/admin/database.vue')),
|
||||
}, {
|
||||
path: '/abuses',
|
||||
name: 'abuses',
|
||||
component: page(() => import('./pages/admin/abuses.vue')),
|
||||
}, {
|
||||
path: '/modlog',
|
||||
name: 'modlog',
|
||||
component: page(() => import('./pages/admin/modlog.vue')),
|
||||
}, {
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: page(() => import('./pages/admin/settings.vue')),
|
||||
}, {
|
||||
path: '/branding',
|
||||
name: 'branding',
|
||||
component: page(() => import('./pages/admin/branding.vue')),
|
||||
}, {
|
||||
path: '/moderation',
|
||||
name: 'moderation',
|
||||
component: page(() => import('./pages/admin/moderation.vue')),
|
||||
}, {
|
||||
path: '/email-settings',
|
||||
name: 'email-settings',
|
||||
component: page(() => import('./pages/admin/email-settings.vue')),
|
||||
}, {
|
||||
path: '/object-storage',
|
||||
name: 'object-storage',
|
||||
component: page(() => import('./pages/admin/object-storage.vue')),
|
||||
}, {
|
||||
path: '/security',
|
||||
name: 'security',
|
||||
component: page(() => import('./pages/admin/security.vue')),
|
||||
}, {
|
||||
path: '/relays',
|
||||
name: 'relays',
|
||||
component: page(() => import('./pages/admin/relays.vue')),
|
||||
}, {
|
||||
path: '/instance-block',
|
||||
name: 'instance-block',
|
||||
component: page(() => import('./pages/admin/instance-block.vue')),
|
||||
}, {
|
||||
path: '/proxy-account',
|
||||
name: 'proxy-account',
|
||||
component: page(() => import('./pages/admin/proxy-account.vue')),
|
||||
}, {
|
||||
path: '/external-services',
|
||||
name: 'external-services',
|
||||
component: page(() => import('./pages/admin/external-services.vue')),
|
||||
}, {
|
||||
path: '/other-settings',
|
||||
name: 'other-settings',
|
||||
component: page(() => import('./pages/admin/other-settings.vue')),
|
||||
}, {
|
||||
path: '/server-rules',
|
||||
name: 'server-rules',
|
||||
component: page(() => import('./pages/admin/server-rules.vue')),
|
||||
}, {
|
||||
path: '/invites',
|
||||
name: 'invites',
|
||||
component: page(() => import('./pages/admin/invites.vue')),
|
||||
}, {
|
||||
path: '/',
|
||||
component: page(() => import('./pages/_empty_.vue')),
|
||||
}],
|
||||
}, {
|
||||
path: '/my/notifications',
|
||||
component: page(() => import('./pages/notifications.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/favorites',
|
||||
component: page(() => import('./pages/favorites.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/achievements',
|
||||
component: page(() => import('./pages/achievements.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/drive/folder/:folder',
|
||||
component: page(() => import('./pages/drive.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/drive',
|
||||
component: page(() => import('./pages/drive.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/drive/file/:fileId',
|
||||
component: page(() => import('./pages/drive.file.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/follow-requests',
|
||||
component: page(() => import('./pages/follow-requests.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/lists/:listId',
|
||||
component: page(() => import('./pages/my-lists/list.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/lists',
|
||||
component: page(() => import('./pages/my-lists/index.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/clips',
|
||||
component: page(() => import('./pages/my-clips/index.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/antennas/create',
|
||||
component: page(() => import('./pages/my-antennas/create.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/antennas/:antennaId',
|
||||
component: page(() => import('./pages/my-antennas/edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/my/antennas',
|
||||
component: page(() => import('./pages/my-antennas/index.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/timeline/list/:listId',
|
||||
component: page(() => import('./pages/user-list-timeline.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/timeline/antenna/:antennaId',
|
||||
component: page(() => import('./pages/antenna-timeline.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/clicker',
|
||||
component: page(() => import('./pages/clicker.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/bubble-game',
|
||||
component: page(() => import('./pages/drop-and-fusion.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/timeline',
|
||||
component: page(() => import('./pages/timeline.vue')),
|
||||
}, {
|
||||
name: 'index',
|
||||
path: '/',
|
||||
component: $i ? page(() => import('./pages/timeline.vue')) : page(() => import('./pages/welcome.vue')),
|
||||
globalCacheKey: 'index',
|
||||
}, {
|
||||
path: '/:(*)',
|
||||
component: page(() => import('./pages/not-found.vue')),
|
||||
}];
|
||||
|
||||
export const mainRouter = new Router(routes, location.pathname + location.search + location.hash, !!$i, page(() => import('@/pages/not-found.vue')));
|
||||
|
||||
window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
|
||||
|
||||
mainRouter.addListener('push', ctx => {
|
||||
window.history.pushState({ key: ctx.key }, '', ctx.path);
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', (event) => {
|
||||
mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
|
||||
});
|
||||
|
||||
export function useRouter(): Router {
|
||||
return inject<Router | null>('router', null) ?? mainRouter;
|
||||
}
|
||||
|
|
@ -83,6 +83,8 @@ export const ACHIEVEMENT_TYPES = [
|
|||
'brainDiver',
|
||||
'smashTestNotificationButton',
|
||||
'tutorialCompleted',
|
||||
'bubbleGameExplodingHead',
|
||||
'bubbleGameDoubleExplodingHead',
|
||||
] as const;
|
||||
|
||||
export const ACHIEVEMENT_BADGES = {
|
||||
|
|
@ -471,6 +473,16 @@ export const ACHIEVEMENT_BADGES = {
|
|||
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
|
||||
frame: 'bronze',
|
||||
},
|
||||
'bubbleGameExplodingHead': {
|
||||
img: '/fluent-emoji/1f92f.png',
|
||||
bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
|
||||
frame: 'bronze',
|
||||
},
|
||||
'bubbleGameDoubleExplodingHead': {
|
||||
img: '/fluent-emoji/1f92f.png',
|
||||
bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
|
||||
frame: 'silver',
|
||||
},
|
||||
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
|
||||
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
|
||||
img: string;
|
||||
|
|
|
|||
468
packages/frontend/src/scripts/drop-and-fusion-engine.ts
Normal file
468
packages/frontend/src/scripts/drop-and-fusion-engine.ts
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import * as Matter from 'matter-js';
|
||||
import seedrandom from 'seedrandom';
|
||||
|
||||
export type Mono = {
|
||||
id: string;
|
||||
level: number;
|
||||
size: number;
|
||||
shape: 'circle' | 'rectangle';
|
||||
score: number;
|
||||
dropCandidate: boolean;
|
||||
sfxPitch: number;
|
||||
img: string;
|
||||
imgSize: number;
|
||||
spriteScale: number;
|
||||
};
|
||||
|
||||
type Log = {
|
||||
frame: number;
|
||||
operation: 'drop';
|
||||
x: number;
|
||||
} | {
|
||||
frame: number;
|
||||
operation: 'hold';
|
||||
} | {
|
||||
frame: number;
|
||||
operation: 'surrender';
|
||||
};
|
||||
|
||||
export class DropAndFusionGame extends EventEmitter<{
|
||||
changeScore: (newScore: number) => void;
|
||||
changeCombo: (newCombo: number) => void;
|
||||
changeStock: (newStock: { id: string; mono: Mono }[]) => void;
|
||||
changeHolding: (newHolding: { id: string; mono: Mono } | null) => void;
|
||||
dropped: (x: number) => void;
|
||||
fusioned: (x: number, y: number, scoreDelta: number) => void;
|
||||
monoAdded: (mono: Mono) => void;
|
||||
gameOver: () => void;
|
||||
sfx(type: string, params: { volume: number; pan: number; pitch: number; }): void;
|
||||
}> {
|
||||
private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
|
||||
private COMBO_INTERVAL = 60; // frame
|
||||
public readonly GAME_VERSION = 1;
|
||||
public readonly GAME_WIDTH = 450;
|
||||
public readonly GAME_HEIGHT = 600;
|
||||
public readonly DROP_INTERVAL = 500;
|
||||
public readonly PLAYAREA_MARGIN = 25;
|
||||
private STOCK_MAX = 4;
|
||||
private TICK_DELTA = 1000 / 60; // 60fps
|
||||
|
||||
public frame = 0;
|
||||
public engine: Matter.Engine;
|
||||
private tickCallbackQueue: { frame: number; callback: () => void; }[] = [];
|
||||
private overflowCollider: Matter.Body;
|
||||
private isGameOver = false;
|
||||
private monoDefinitions: Mono[] = [];
|
||||
private rng: () => number;
|
||||
private logs: Log[] = [];
|
||||
private replaying = false;
|
||||
|
||||
/**
|
||||
* フィールドに出ていて、かつ合体の対象となるアイテム
|
||||
*/
|
||||
private activeBodyIds: Matter.Body['id'][] = [];
|
||||
|
||||
/**
|
||||
* fusion予約アイテムのペア
|
||||
* TODO: これらのモノは光らせるなどの演出をすると視覚的に楽しそう
|
||||
*/
|
||||
private fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = [];
|
||||
|
||||
private latestDroppedBodyId: Matter.Body['id'] | null = null;
|
||||
|
||||
private latestDroppedAt = 0;
|
||||
private latestFusionedAt = 0; // frame
|
||||
private stock: { id: string; mono: Mono }[] = [];
|
||||
private holding: { id: string; mono: Mono } | null = null;
|
||||
|
||||
private _combo = 0;
|
||||
private get combo() {
|
||||
return this._combo;
|
||||
}
|
||||
private set combo(value: number) {
|
||||
this._combo = value;
|
||||
this.emit('changeCombo', value);
|
||||
}
|
||||
|
||||
private _score = 0;
|
||||
private get score() {
|
||||
return this._score;
|
||||
}
|
||||
private set score(value: number) {
|
||||
this._score = value;
|
||||
this.emit('changeScore', value);
|
||||
}
|
||||
|
||||
public replayPlaybackRate = 1;
|
||||
|
||||
constructor(env: { monoDefinitions: Mono[]; seed: string; replaying?: boolean }) {
|
||||
super();
|
||||
|
||||
this.replaying = !!env.replaying;
|
||||
this.monoDefinitions = env.monoDefinitions;
|
||||
this.rng = seedrandom(env.seed);
|
||||
|
||||
this.tick = this.tick.bind(this);
|
||||
|
||||
this.engine = Matter.Engine.create({
|
||||
constraintIterations: 2 * this.PHYSICS_QUALITY_FACTOR,
|
||||
positionIterations: 6 * this.PHYSICS_QUALITY_FACTOR,
|
||||
velocityIterations: 4 * this.PHYSICS_QUALITY_FACTOR,
|
||||
gravity: {
|
||||
x: 0,
|
||||
y: 1,
|
||||
},
|
||||
timing: {
|
||||
timeScale: 2,
|
||||
},
|
||||
enableSleeping: false,
|
||||
});
|
||||
|
||||
this.engine.world.bodies = [];
|
||||
|
||||
//#region walls
|
||||
const WALL_OPTIONS: Matter.IChamferableBodyDefinition = {
|
||||
label: '_wall_',
|
||||
isStatic: true,
|
||||
friction: 0.7,
|
||||
slop: 1.0,
|
||||
render: {
|
||||
strokeStyle: 'transparent',
|
||||
fillStyle: 'transparent',
|
||||
},
|
||||
};
|
||||
|
||||
const thickness = 100;
|
||||
Matter.Composite.add(this.engine.world, [
|
||||
Matter.Bodies.rectangle(this.GAME_WIDTH / 2, this.GAME_HEIGHT + (thickness / 2) - this.PLAYAREA_MARGIN, this.GAME_WIDTH, thickness, WALL_OPTIONS),
|
||||
Matter.Bodies.rectangle(this.GAME_WIDTH + (thickness / 2) - this.PLAYAREA_MARGIN, this.GAME_HEIGHT / 2, thickness, this.GAME_HEIGHT, WALL_OPTIONS),
|
||||
Matter.Bodies.rectangle(-((thickness / 2) - this.PLAYAREA_MARGIN), this.GAME_HEIGHT / 2, thickness, this.GAME_HEIGHT, WALL_OPTIONS),
|
||||
]);
|
||||
//#endregion
|
||||
|
||||
this.overflowCollider = Matter.Bodies.rectangle(this.GAME_WIDTH / 2, 0, this.GAME_WIDTH, 200, {
|
||||
isStatic: true,
|
||||
isSensor: true,
|
||||
render: {
|
||||
strokeStyle: 'transparent',
|
||||
fillStyle: 'transparent',
|
||||
},
|
||||
});
|
||||
Matter.Composite.add(this.engine.world, this.overflowCollider);
|
||||
}
|
||||
|
||||
private msToFrame(ms: number) {
|
||||
return Math.round(ms / this.TICK_DELTA);
|
||||
}
|
||||
|
||||
private createBody(mono: Mono, x: number, y: number) {
|
||||
const options: Matter.IBodyDefinition = {
|
||||
label: mono.id,
|
||||
//density: 0.0005,
|
||||
density: mono.size / 1000,
|
||||
restitution: 0.2,
|
||||
frictionAir: 0.01,
|
||||
friction: 0.7,
|
||||
frictionStatic: 5,
|
||||
slop: 1.0,
|
||||
//mass: 0,
|
||||
render: {
|
||||
sprite: {
|
||||
texture: mono.img,
|
||||
xScale: (mono.size / mono.imgSize) * mono.spriteScale,
|
||||
yScale: (mono.size / mono.imgSize) * mono.spriteScale,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (mono.shape === 'circle') {
|
||||
return Matter.Bodies.circle(x, y, mono.size / 2, options);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
} else if (mono.shape === 'rectangle') {
|
||||
return Matter.Bodies.rectangle(x, y, mono.size, mono.size, options);
|
||||
} else {
|
||||
throw new Error('unrecognized shape');
|
||||
}
|
||||
}
|
||||
|
||||
private fusion(bodyA: Matter.Body, bodyB: Matter.Body) {
|
||||
if (this.latestFusionedAt > this.frame - this.COMBO_INTERVAL) {
|
||||
this.combo++;
|
||||
} else {
|
||||
this.combo = 1;
|
||||
}
|
||||
this.latestFusionedAt = this.frame;
|
||||
|
||||
// TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する?
|
||||
const newX = (bodyA.position.x + bodyB.position.x) / 2;
|
||||
const newY = (bodyA.position.y + bodyB.position.y) / 2;
|
||||
|
||||
Matter.Composite.remove(this.engine.world, [bodyA, bodyB]);
|
||||
this.activeBodyIds = this.activeBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id);
|
||||
|
||||
const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!;
|
||||
const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1);
|
||||
|
||||
if (nextMono) {
|
||||
const body = this.createBody(nextMono, newX, newY);
|
||||
Matter.Composite.add(this.engine.world, body);
|
||||
|
||||
// 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする
|
||||
this.tickCallbackQueue.push({
|
||||
frame: this.frame + this.msToFrame(100),
|
||||
callback: () => {
|
||||
this.activeBodyIds.push(body.id);
|
||||
},
|
||||
});
|
||||
|
||||
const comboBonus = 1 + ((this.combo - 1) / 5);
|
||||
const additionalScore = Math.round(currentMono.score * comboBonus);
|
||||
this.score += additionalScore;
|
||||
|
||||
this.emit('monoAdded', nextMono);
|
||||
this.emit('fusioned', newX, newY, additionalScore);
|
||||
|
||||
const panV = newX - this.PLAYAREA_MARGIN;
|
||||
const panW = this.GAME_WIDTH - this.PLAYAREA_MARGIN - this.PLAYAREA_MARGIN;
|
||||
const pan = ((panV / panW) - 0.5) * 2;
|
||||
this.emit('sfx', 'fusion', { volume: 1, pan, pitch: nextMono.sfxPitch });
|
||||
} else {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
|
||||
private onCollision(event: Matter.IEventCollision<Matter.Engine>) {
|
||||
const minCollisionEnergyForSound = 2.5;
|
||||
const maxCollisionEnergyForSound = 9;
|
||||
const soundPitchMax = 4;
|
||||
const soundPitchMin = 0.5;
|
||||
|
||||
for (const pairs of event.pairs) {
|
||||
const { bodyA, bodyB } = pairs;
|
||||
|
||||
if (bodyA.id === this.overflowCollider.id || bodyB.id === this.overflowCollider.id) {
|
||||
if (bodyA.id === this.latestDroppedBodyId || bodyB.id === this.latestDroppedBodyId) {
|
||||
continue;
|
||||
}
|
||||
this.gameOver();
|
||||
break;
|
||||
}
|
||||
|
||||
const shouldFusion = (bodyA.label === bodyB.label) &&
|
||||
!this.fusionReservedPairs.some(x =>
|
||||
x.bodyA.id === bodyA.id ||
|
||||
x.bodyA.id === bodyB.id ||
|
||||
x.bodyB.id === bodyA.id ||
|
||||
x.bodyB.id === bodyB.id);
|
||||
|
||||
if (shouldFusion) {
|
||||
if (this.activeBodyIds.includes(bodyA.id) && this.activeBodyIds.includes(bodyB.id)) {
|
||||
this.fusion(bodyA, bodyB);
|
||||
} else {
|
||||
this.fusionReservedPairs.push({ bodyA, bodyB });
|
||||
this.tickCallbackQueue.push({
|
||||
frame: this.frame + this.msToFrame(100),
|
||||
callback: () => {
|
||||
this.fusionReservedPairs = this.fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id);
|
||||
this.fusion(bodyA, bodyB);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const energy = pairs.collision.depth;
|
||||
if (energy > minCollisionEnergyForSound) {
|
||||
const volume = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4;
|
||||
const panV =
|
||||
pairs.bodyA.label === '_wall_' ? bodyB.position.x - this.PLAYAREA_MARGIN :
|
||||
pairs.bodyB.label === '_wall_' ? bodyA.position.x - this.PLAYAREA_MARGIN :
|
||||
((bodyA.position.x + bodyB.position.x) / 2) - this.PLAYAREA_MARGIN;
|
||||
const panW = this.GAME_WIDTH - this.PLAYAREA_MARGIN - this.PLAYAREA_MARGIN;
|
||||
const pan = ((panV / panW) - 0.5) * 2;
|
||||
const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10)));
|
||||
this.emit('sfx', 'collision', { volume, pan, pitch });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public surrender() {
|
||||
this.logs.push({
|
||||
frame: this.frame,
|
||||
operation: 'surrender',
|
||||
});
|
||||
|
||||
this.gameOver();
|
||||
}
|
||||
|
||||
private gameOver() {
|
||||
this.isGameOver = true;
|
||||
this.emit('gameOver');
|
||||
}
|
||||
|
||||
public start() {
|
||||
for (let i = 0; i < this.STOCK_MAX; i++) {
|
||||
this.stock.push({
|
||||
id: this.rng().toString(),
|
||||
mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
|
||||
});
|
||||
}
|
||||
this.emit('changeStock', this.stock);
|
||||
|
||||
Matter.Events.on(this.engine, 'collisionStart', this.onCollision.bind(this));
|
||||
}
|
||||
|
||||
public getLogs() {
|
||||
return this.logs;
|
||||
}
|
||||
|
||||
public tick() {
|
||||
this.frame++;
|
||||
|
||||
if (this.latestFusionedAt < this.frame - this.COMBO_INTERVAL) {
|
||||
this.combo = 0;
|
||||
}
|
||||
|
||||
this.tickCallbackQueue = this.tickCallbackQueue.filter(x => {
|
||||
if (x.frame === this.frame) {
|
||||
x.callback();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
Matter.Engine.update(this.engine, this.TICK_DELTA);
|
||||
|
||||
const hasNextTick = !this.isGameOver;
|
||||
|
||||
return hasNextTick;
|
||||
}
|
||||
|
||||
public getActiveMonos() {
|
||||
return this.engine.world.bodies.map(x => this.monoDefinitions.find((mono) => mono.id === x.label)!).filter(x => x !== undefined);
|
||||
}
|
||||
|
||||
public drop(_x: number) {
|
||||
if (this.isGameOver) return;
|
||||
// TODO: フレームで計算するようにすればリプレイかどうかのチェックは不要になる
|
||||
if (!this.replaying && (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL)) return;
|
||||
|
||||
const head = this.stock.shift()!;
|
||||
this.stock.push({
|
||||
id: this.rng().toString(),
|
||||
mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
|
||||
});
|
||||
this.emit('changeStock', this.stock);
|
||||
|
||||
const inputX = Math.round(_x);
|
||||
const x = Math.min(this.GAME_WIDTH - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), inputX));
|
||||
const body = this.createBody(head.mono, x, 50 + head.mono.size / 2);
|
||||
this.logs.push({
|
||||
frame: this.frame,
|
||||
operation: 'drop',
|
||||
x: inputX,
|
||||
});
|
||||
Matter.Composite.add(this.engine.world, body);
|
||||
this.activeBodyIds.push(body.id);
|
||||
this.latestDroppedBodyId = body.id;
|
||||
this.latestDroppedAt = Date.now();
|
||||
this.emit('dropped', x);
|
||||
this.emit('monoAdded', head.mono);
|
||||
}
|
||||
|
||||
public hold() {
|
||||
if (this.isGameOver) return;
|
||||
|
||||
this.logs.push({
|
||||
frame: this.frame,
|
||||
operation: 'hold',
|
||||
});
|
||||
|
||||
if (this.holding) {
|
||||
const head = this.stock.shift()!;
|
||||
this.stock.unshift(this.holding);
|
||||
this.holding = head;
|
||||
this.emit('changeHolding', this.holding);
|
||||
this.emit('changeStock', this.stock);
|
||||
} else {
|
||||
const head = this.stock.shift()!;
|
||||
this.holding = head;
|
||||
this.stock.push({
|
||||
id: this.rng().toString(),
|
||||
mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
|
||||
});
|
||||
this.emit('changeHolding', this.holding);
|
||||
this.emit('changeStock', this.stock);
|
||||
}
|
||||
}
|
||||
|
||||
public static serializeLogs(logs: Log[]) {
|
||||
const _logs: number[][] = [];
|
||||
|
||||
for (let i = 0; i < logs.length; i++) {
|
||||
const log = logs[i];
|
||||
const frameDelta = i === 0 ? log.frame : log.frame - logs[i - 1].frame;
|
||||
|
||||
switch (log.operation) {
|
||||
case 'drop':
|
||||
_logs.push([frameDelta, 0, log.x]);
|
||||
break;
|
||||
case 'hold':
|
||||
_logs.push([frameDelta, 1]);
|
||||
break;
|
||||
case 'surrender':
|
||||
_logs.push([frameDelta, 2]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return _logs;
|
||||
}
|
||||
|
||||
public static deserializeLogs(logs: number[][]) {
|
||||
const _logs: Log[] = [];
|
||||
|
||||
let frame = 0;
|
||||
|
||||
for (const log of logs) {
|
||||
const frameDelta = log[0];
|
||||
frame += frameDelta;
|
||||
|
||||
const operation = log[1];
|
||||
|
||||
switch (operation) {
|
||||
case 0:
|
||||
_logs.push({
|
||||
frame,
|
||||
operation: 'drop',
|
||||
x: log[2],
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
_logs.push({
|
||||
frame,
|
||||
operation: 'hold',
|
||||
});
|
||||
break;
|
||||
case 2:
|
||||
_logs.push({
|
||||
frame,
|
||||
operation: 'surrender',
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return _logs;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
Matter.World.clear(this.engine.world, false);
|
||||
Matter.Engine.clear(this.engine);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,11 +13,11 @@ import * as os from '@/os.js';
|
|||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore, userActions } from '@/store.js';
|
||||
import { $i, iAmModerator } from '@/account.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { Router } from '@/nirax.js';
|
||||
import { IRouter } from '@/nirax.js';
|
||||
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) {
|
||||
export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
|
||||
const meId = $i ? $i.id : null;
|
||||
|
||||
const cleanups = [] as (() => void)[];
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { Router } from '@/nirax.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
export async function lookup(router?: Router) {
|
||||
const _router = router ?? mainRouter;
|
||||
|
|
|
|||
|
|
@ -10,12 +10,17 @@ import { $i } from '@/account.js';
|
|||
export const pendingApiRequestsCount = ref(0);
|
||||
|
||||
// Implements Misskey.api.ApiClient.request
|
||||
export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
|
||||
export function misskeyApi<
|
||||
ResT = void,
|
||||
E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
|
||||
P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
|
||||
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
|
||||
>(
|
||||
endpoint: E,
|
||||
data: P = {} as any,
|
||||
token?: string | null | undefined,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
|
||||
): Promise<_ResT> {
|
||||
if (endpoint.includes('://')) throw new Error('invalid endpoint');
|
||||
pendingApiRequestsCount.value++;
|
||||
|
||||
|
|
@ -23,7 +28,7 @@ export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey.
|
|||
pendingApiRequestsCount.value--;
|
||||
};
|
||||
|
||||
const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => {
|
||||
const promise = new Promise<_ResT>((resolve, reject) => {
|
||||
// Append a credential
|
||||
if ($i) (data as any).i = $i.token;
|
||||
if (token !== undefined) (data as any).i = token;
|
||||
|
|
@ -44,7 +49,7 @@ export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey.
|
|||
if (res.status === 200) {
|
||||
resolve(body);
|
||||
} else if (res.status === 204) {
|
||||
resolve();
|
||||
resolve(undefined as _ResT); // void -> undefined
|
||||
} else {
|
||||
reject(body.error);
|
||||
}
|
||||
|
|
@ -57,10 +62,15 @@ export function misskeyApi<E extends keyof Misskey.Endpoints, P extends Misskey.
|
|||
}
|
||||
|
||||
// Implements Misskey.api.ApiClient.request
|
||||
export function misskeyApiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
|
||||
export function misskeyApiGet<
|
||||
ResT = void,
|
||||
E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
|
||||
P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
|
||||
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
|
||||
>(
|
||||
endpoint: E,
|
||||
data: P = {} as any,
|
||||
): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
|
||||
): Promise<_ResT> {
|
||||
pendingApiRequestsCount.value++;
|
||||
|
||||
const onFinally = () => {
|
||||
|
|
@ -69,7 +79,7 @@ export function misskeyApiGet<E extends keyof Misskey.Endpoints, P extends Missk
|
|||
|
||||
const query = new URLSearchParams(data as any);
|
||||
|
||||
const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => {
|
||||
const promise = new Promise<_ResT>((resolve, reject) => {
|
||||
// Send request
|
||||
window.fetch(`${apiUrl}/${endpoint}?${query}`, {
|
||||
method: 'GET',
|
||||
|
|
@ -81,7 +91,7 @@ export function misskeyApiGet<E extends keyof Misskey.Endpoints, P extends Missk
|
|||
if (res.status === 200) {
|
||||
resolve(body);
|
||||
} else if (res.status === 204) {
|
||||
resolve();
|
||||
resolve(undefined as _ResT); // void -> undefined
|
||||
} else {
|
||||
reject(body.error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
import type { SoundStore } from '@/store.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
||||
let ctx: AudioContext;
|
||||
const cache = new Map<string, AudioBuffer>();
|
||||
|
|
@ -89,69 +88,33 @@ export type OperationType = typeof operationTypes[number];
|
|||
|
||||
/**
|
||||
* 音声を読み込む
|
||||
* @param soundStore サウンド設定
|
||||
* @param url url
|
||||
* @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
|
||||
*/
|
||||
export async function loadAudio(soundStore: {
|
||||
type: Exclude<SoundType, '_driveFile_'>;
|
||||
} | {
|
||||
type: '_driveFile_';
|
||||
fileId: string;
|
||||
fileUrl: string;
|
||||
}, options?: { useCache?: boolean; }) {
|
||||
if (_DEV_) console.log('loading audio. opts:', options);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
|
||||
return;
|
||||
}
|
||||
export async function loadAudio(url: string, options?: { useCache?: boolean; }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (ctx == null) {
|
||||
ctx = new AudioContext();
|
||||
}
|
||||
if (options?.useCache ?? true) {
|
||||
if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) {
|
||||
if (_DEV_) console.log('use cache');
|
||||
return cache.get(soundStore.fileId) as AudioBuffer;
|
||||
} else if (cache.has(soundStore.type)) {
|
||||
if (_DEV_) console.log('use cache');
|
||||
return cache.get(soundStore.type) as AudioBuffer;
|
||||
if (cache.has(url)) {
|
||||
return cache.get(url) as AudioBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
|
||||
if (soundStore.type === '_driveFile_') {
|
||||
try {
|
||||
response = await fetch(soundStore.fileUrl);
|
||||
} catch (err) {
|
||||
try {
|
||||
// URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック
|
||||
const apiRes = await misskeyApi('drive/files/show', {
|
||||
fileId: soundStore.fileId,
|
||||
});
|
||||
response = await fetch(apiRes.url);
|
||||
} catch (fbErr) {
|
||||
// それでも無理なら諦める
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
response = await fetch(url);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
||||
|
||||
if (options?.useCache ?? true) {
|
||||
if (soundStore.type === '_driveFile_') {
|
||||
cache.set(soundStore.fileId, audioBuffer);
|
||||
} else {
|
||||
cache.set(soundStore.type, audioBuffer);
|
||||
}
|
||||
cache.set(url, audioBuffer);
|
||||
}
|
||||
|
||||
return audioBuffer;
|
||||
|
|
@ -161,13 +124,12 @@ export async function loadAudio(soundStore: {
|
|||
* 既定のスプライトを再生する
|
||||
* @param type スプライトの種類を指定
|
||||
*/
|
||||
export function play(operationType: OperationType) {
|
||||
export function playMisskeySfx(operationType: OperationType) {
|
||||
const sound = defaultStore.state[`sound_${operationType}`];
|
||||
if (_DEV_) console.log('play', operationType, sound);
|
||||
if (sound.type == null || !canPlay) return;
|
||||
|
||||
canPlay = false;
|
||||
playFile(sound).finally(() => {
|
||||
playMisskeySfxFile(sound).finally(() => {
|
||||
// ごく短時間に音が重複しないように
|
||||
setTimeout(() => {
|
||||
canPlay = true;
|
||||
|
|
@ -179,39 +141,59 @@ export function play(operationType: OperationType) {
|
|||
* サウンド設定形式で指定された音声を再生する
|
||||
* @param soundStore サウンド設定
|
||||
*/
|
||||
export async function playFile(soundStore: SoundStore) {
|
||||
const buffer = await loadAudio(soundStore);
|
||||
if (!buffer) return;
|
||||
createSourceNode(buffer, soundStore.volume)?.start();
|
||||
}
|
||||
|
||||
export async function playRaw(type: Exclude<SoundType, '_driveFile_'>, volume = 1, pan = 0, playbackRate = 1) {
|
||||
const buffer = await loadAudio({ type });
|
||||
if (!buffer) return;
|
||||
createSourceNode(buffer, volume, pan, playbackRate)?.start();
|
||||
}
|
||||
|
||||
export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1) : AudioBufferSourceNode | null {
|
||||
const masterVolume = defaultStore.state.sound_masterVolume;
|
||||
if (isMute() || masterVolume === 0 || volume === 0) {
|
||||
return null;
|
||||
export async function playMisskeySfxFile(soundStore: SoundStore) {
|
||||
if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
|
||||
return;
|
||||
}
|
||||
const masterVolume = defaultStore.state.sound_masterVolume;
|
||||
if (isMute() || masterVolume === 0 || soundStore.volume === 0) {
|
||||
return;
|
||||
}
|
||||
const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`;
|
||||
const buffer = await loadAudio(url);
|
||||
if (!buffer) return;
|
||||
const volume = soundStore.volume * masterVolume;
|
||||
createSourceNode(buffer, { volume }).soundSource.start();
|
||||
}
|
||||
|
||||
export async function playUrl(url: string, opts: {
|
||||
volume?: number;
|
||||
pan?: number;
|
||||
playbackRate?: number;
|
||||
}) {
|
||||
if (opts.volume === 0) {
|
||||
return;
|
||||
}
|
||||
const buffer = await loadAudio(url);
|
||||
if (!buffer) return;
|
||||
createSourceNode(buffer, opts).soundSource.start();
|
||||
}
|
||||
|
||||
export function createSourceNode(buffer: AudioBuffer, opts: {
|
||||
volume?: number;
|
||||
pan?: number;
|
||||
playbackRate?: number;
|
||||
}): {
|
||||
soundSource: AudioBufferSourceNode;
|
||||
panNode: StereoPannerNode;
|
||||
gainNode: GainNode;
|
||||
} {
|
||||
const panNode = ctx.createStereoPanner();
|
||||
panNode.pan.value = pan;
|
||||
panNode.pan.value = opts.pan ?? 0;
|
||||
|
||||
const gainNode = ctx.createGain();
|
||||
gainNode.gain.value = masterVolume * volume;
|
||||
|
||||
gainNode.gain.value = opts.volume ?? 1;
|
||||
|
||||
const soundSource = ctx.createBufferSource();
|
||||
soundSource.buffer = buffer;
|
||||
soundSource.playbackRate.value = playbackRate;
|
||||
soundSource.playbackRate.value = opts.playbackRate ?? 1;
|
||||
soundSource
|
||||
.connect(panNode)
|
||||
.connect(gainNode)
|
||||
.connect(ctx.destination);
|
||||
|
||||
return soundSource;
|
||||
return { soundSource, panNode, gainNode };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -511,6 +511,13 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
dropAndFusion: {
|
||||
where: 'device',
|
||||
default: {
|
||||
bgmVolume: 0.25,
|
||||
sfxVolume: 1,
|
||||
},
|
||||
},
|
||||
|
||||
sound_masterVolume: {
|
||||
where: 'device',
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
|
|||
}, 6000);
|
||||
}
|
||||
|
||||
sound.play('notification');
|
||||
sound.playMisskeySfx('notification');
|
||||
}
|
||||
|
||||
if ($i) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import { post } from '@/os.js';
|
|||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { $i, login } from '@/account.js';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
export function swInject() {
|
||||
navigator.serviceWorker.addEventListener('message', async ev => {
|
||||
|
|
|
|||
|
|
@ -52,11 +52,11 @@ import XCommon from './_common_/common.vue';
|
|||
import { instanceName } from '@/config.js';
|
||||
import { StickySidebar } from '@/scripts/sticky-sidebar.js';
|
||||
import * as os from '@/os.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue'));
|
||||
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,6 @@ import * as os from '@/os.js';
|
|||
import { navbarItemDef } from '@/navbar.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
|
@ -117,6 +116,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue';
|
|||
import XMentionsColumn from '@/ui/deck/mentions-column.vue';
|
||||
import XDirectColumn from '@/ui/deck/direct-column.vue';
|
||||
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
|
||||
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
|
||||
const indicatorCounterToggle = computed(defaultStore.makeGetterSetter('indicatorCounterToggle'));
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ import XColumn from './column.vue';
|
|||
import { deckStore, Column } from '@/ui/deck/deck-store.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import { useScrollPositionManager } from '@/nirax.js';
|
||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
defineProps<{
|
||||
column: Column;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { provide, ComputedRef, ref } from 'vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import { instanceName } from '@/config.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ import {defaultStore} from '@/store';
|
|||
import {navbarItemDef} from '@/navbar';
|
||||
import {i18n} from '@/i18n';
|
||||
import {$i} from '@/account';
|
||||
import {mainRouter} from '@/router';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
import {PageMetadata, provideMetadataReceiver} from '@/scripts/page-metadata';
|
||||
import {deviceKind} from '@/scripts/device-kind';
|
||||
import {miLocalStorage} from '@/local-storage';
|
||||
|
|
|
|||
|
|
@ -79,10 +79,10 @@ import { instance } from '@/instance.js';
|
|||
import XSigninDialog from '@/components/MkSigninDialog.vue';
|
||||
import XSignupDialog from '@/components/MkSignupDialog.vue';
|
||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
const DESKTOP_THRESHOLD = 1100;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { provide, ComputedRef, ref } from 'vue';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import { instanceName, ui } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
||||
|
||||
|
|
|
|||
|
|
@ -104,10 +104,7 @@ const jammedAudioBuffer = ref<AudioBuffer | null>(null);
|
|||
const jammedSoundNodePlaying = ref<boolean>(false);
|
||||
|
||||
if (defaultStore.state.sound_masterVolume) {
|
||||
sound.loadAudio({
|
||||
type: 'syuilo/queue-jammed',
|
||||
volume: 1,
|
||||
}).then(buf => {
|
||||
sound.loadAudio('/client-assets/sounds/syuilo/queue-jammed.mp3').then(buf => {
|
||||
if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer');
|
||||
jammedAudioBuffer.value = buf;
|
||||
});
|
||||
|
|
@ -126,7 +123,7 @@ const onStats = (stats) => {
|
|||
current[domain].delayed = stats[domain].delayed;
|
||||
|
||||
if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) {
|
||||
const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1);
|
||||
const soundNode = sound.createSourceNode(jammedAudioBuffer.value, {}).soundSource;
|
||||
if (soundNode) {
|
||||
jammedSoundNodePlaying.value = true;
|
||||
soundNode.onended = () => jammedSoundNodePlaying.value = false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue