From 66f1aaf5f7ba793500123320ac723b5c3fbe80d6 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Wed, 20 Jul 2022 19:59:27 +0900 Subject: [PATCH] =?UTF-8?q?enhance(client):=20=E3=83=8D=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=83=AB=E3=83=BC=E3=83=86=E3=82=A3=E3=83=B3?= =?UTF-8?q?=E3=82=B0=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/global/router-view.vue | 38 +++- .../client/src/components/page-window.vue | 2 +- packages/client/src/nirax.ts | 190 +++++++++++------- packages/client/src/pages/_empty_.vue | 7 + packages/client/src/pages/admin/index.vue | 99 +++------ packages/client/src/pages/settings/index.vue | 144 ++++--------- ....statusbar.vue => statusbar.statusbar.vue} | 0 .../{statusbars.vue => statusbar.vue} | 2 +- packages/client/src/router.ts | 174 +++++++++++++++- 9 files changed, 391 insertions(+), 265 deletions(-) create mode 100644 packages/client/src/pages/_empty_.vue rename packages/client/src/pages/settings/{statusbars.statusbar.vue => statusbar.statusbar.vue} (100%) rename packages/client/src/pages/settings/{statusbars.vue => statusbar.vue} (96%) diff --git a/packages/client/src/components/global/router-view.vue b/packages/client/src/components/global/router-view.vue index cd1e780196..1d841e050c 100644 --- a/packages/client/src/components/global/router-view.vue +++ b/packages/client/src/components/global/router-view.vue @@ -11,8 +11,8 @@ </template> <script lang="ts" setup> -import { inject, nextTick, onMounted, onUnmounted, watch } from 'vue'; -import { Router } from '@/nirax'; +import { inject, nextTick, onBeforeUnmount, onMounted, onUnmounted, provide, watch } from 'vue'; +import { Resolved, Router } from '@/nirax'; import { defaultStore } from '@/store'; const props = defineProps<{ @@ -25,19 +25,37 @@ if (router == null) { throw new Error('no router provided'); } -let currentPageComponent = $shallowRef(router.getCurrentComponent()); -let currentPageProps = $ref(router.getCurrentProps()); -let key = $ref(router.getCurrentKey()); +const currentDepth = inject('routerCurrentDepth', 0); +provide('routerCurrentDepth', currentDepth + 1); -function onChange({ route, props: newProps, key: newKey }) { - currentPageComponent = route.component; - currentPageProps = newProps; - key = newKey; +function resolveNested(current: Resolved, d = 0): Resolved | null { + if (d === currentDepth) { + return current; + } else { + if (current.child) { + return resolveNested(current.child, d + 1); + } else { + return null; + } + } +} + +const current = resolveNested(router.current)!; +let currentPageComponent = $shallowRef(current.route.component); +let currentPageProps = $ref(current.props); +let key = $ref(current.route.path + JSON.stringify(Object.fromEntries(current.props))); + +function onChange({ resolved, key: newKey }) { + const current = resolveNested(resolved); + if (current == null) return; + currentPageComponent = current.route.component; + currentPageProps = current.props; + key = current.route.path + JSON.stringify(Object.fromEntries(current.props)); } router.addListener('change', onChange); -onUnmounted(() => { +onBeforeUnmount(() => { router.removeListener('change', onChange); }); </script> diff --git a/packages/client/src/components/page-window.vue b/packages/client/src/components/page-window.vue index 98140b95c0..43d75b0cf9 100644 --- a/packages/client/src/components/page-window.vue +++ b/packages/client/src/components/page-window.vue @@ -114,7 +114,7 @@ function menu(ev) { function back() { history.pop(); - router.change(history[history.length - 1].path, history[history.length - 1].key); + router.replace(history[history.length - 1].path, history[history.length - 1].key); } function close() { diff --git a/packages/client/src/nirax.ts b/packages/client/src/nirax.ts index 4ba1fe70f6..0ee39bf473 100644 --- a/packages/client/src/nirax.ts +++ b/packages/client/src/nirax.ts @@ -13,6 +13,7 @@ type RouteDef = { name?: string; hash?: string; globalCacheKey?: string; + children?: RouteDef[]; }; type ParsedPath = (string | { @@ -22,6 +23,8 @@ type ParsedPath = (string | { optional?: boolean; })[]; +export type Resolved = { route: RouteDef; props: Map<string, string>; child?: Resolved; }; + function parsePath(path: string): ParsedPath { const res = [] as ParsedPath; @@ -51,8 +54,11 @@ export class Router extends EventEmitter<{ change: (ctx: { beforePath: string; path: string; - route: RouteDef | null; - props: Map<string, string> | null; + resolved: Resolved; + key: string; + }) => void; + replace: (ctx: { + path: string; key: string; }) => void; push: (ctx: { @@ -65,12 +71,12 @@ export class Router extends EventEmitter<{ same: () => void; }> { private routes: RouteDef[]; + public current: Resolved; + public currentRef: ShallowRef<Resolved> = shallowRef(); + public currentRoute: ShallowRef<RouteDef> = shallowRef(); private currentPath: string; - private currentComponent: Component | null = null; - private currentProps: Map<string, string> | null = null; private currentKey = Date.now().toString(); - public currentRoute: ShallowRef<RouteDef | null> = shallowRef(null); public navHook: ((path: string, flag?: any) => boolean) | null = null; constructor(routes: Router['routes'], currentPath: Router['currentPath']) { @@ -78,10 +84,10 @@ export class Router extends EventEmitter<{ this.routes = routes; this.currentPath = currentPath; - this.navigate(currentPath, null, true); + this.navigate(currentPath, null, false); } - public resolve(path: string): { route: RouteDef; props: Map<string, string>; } | null { + public resolve(path: string): Resolved | null { let queryString: string | null = null; let hash: string | null = null; if (path[0] === '/') path = path.substring(1); @@ -96,77 +102,108 @@ export class Router extends EventEmitter<{ if (_DEV_) console.log('Routing: ', path, queryString); - const _parts = path.split('/').filter(part => part.length !== 0); + function check(routes: RouteDef[], _parts: string[]): Resolved | null { + forEachRouteLoop: + for (const route of routes) { + let parts = [ ..._parts ]; + const props = new Map<string, string>(); - forEachRouteLoop: - for (const route of this.routes) { - let parts = [ ..._parts ]; - const props = new Map<string, string>(); - - pathMatchLoop: - for (const p of parsePath(route.path)) { - if (typeof p === 'string') { - if (p === parts[0]) { - parts.shift(); - } else { - continue forEachRouteLoop; - } - } else { - if (parts[0] == null && !p.optional) { - continue forEachRouteLoop; - } - if (p.wildcard) { - if (parts.length !== 0) { - props.set(p.name, safeURIDecode(parts.join('/'))); - parts = []; - } - break pathMatchLoop; - } else { - if (p.startsWith) { - if (parts[0] == null || !parts[0].startsWith(p.startsWith)) continue forEachRouteLoop; - - props.set(p.name, safeURIDecode(parts[0].substring(p.startsWith.length))); + pathMatchLoop: + for (const p of parsePath(route.path)) { + if (typeof p === 'string') { + if (p === parts[0]) { parts.shift(); } else { - if (parts[0]) { - props.set(p.name, safeURIDecode(parts[0])); + continue forEachRouteLoop; + } + } else { + if (parts[0] == null && !p.optional) { + continue forEachRouteLoop; + } + if (p.wildcard) { + if (parts.length !== 0) { + props.set(p.name, safeURIDecode(parts.join('/'))); + parts = []; + } + break pathMatchLoop; + } else { + if (p.startsWith) { + if (parts[0] == null || !parts[0].startsWith(p.startsWith)) continue forEachRouteLoop; + + props.set(p.name, safeURIDecode(parts[0].substring(p.startsWith.length))); + parts.shift(); + } else { + if (parts[0]) { + props.set(p.name, safeURIDecode(parts[0])); + } + parts.shift(); } - parts.shift(); } } } - } - if (parts.length !== 0) continue forEachRouteLoop; + if (parts.length === 0) { + if (route.children) { + const child = check(route.children, []); + if (child) { + return { + route, + props, + child, + }; + } else { + continue forEachRouteLoop; + } + } - if (route.hash != null && hash != null) { - props.set(route.hash, safeURIDecode(hash)); - } - - if (route.query != null && queryString != null) { - const queryObject = [...new URLSearchParams(queryString).entries()] - .reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}); - - for (const q in route.query) { - const as = route.query[q]; - if (queryObject[q]) { - props.set(as, safeURIDecode(queryObject[q])); + if (route.hash != null && hash != null) { + props.set(route.hash, safeURIDecode(hash)); + } + + if (route.query != null && queryString != null) { + const queryObject = [...new URLSearchParams(queryString).entries()] + .reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}); + + for (const q in route.query) { + const as = route.query[q]; + if (queryObject[q]) { + props.set(as, safeURIDecode(queryObject[q])); + } + } + } + + return { + route, + props, + }; + } else { + if (route.children) { + const child = check(route.children, parts); + if (child) { + return { + route, + props, + child, + }; + } else { + continue forEachRouteLoop; + } + } else { + continue forEachRouteLoop; } } } - return { - route, - props, - }; + return null; } - return null; + const _parts = path.split('/').filter(part => part.length !== 0); + + return check(this.routes, _parts); } - private navigate(path: string, key: string | null | undefined, initial = false) { + private navigate(path: string, key: string | null | undefined, emitChange = true) { const beforePath = this.currentPath; - const beforeRoute = this.currentRoute.value; this.currentPath = path; const res = this.resolve(this.currentPath); @@ -181,28 +218,21 @@ export class Router extends EventEmitter<{ const isSamePath = beforePath === path; if (isSamePath && key == null) key = this.currentKey; - this.currentComponent = res.route.component; - this.currentProps = res.props; + this.current = res; + this.currentRef.value = res; this.currentRoute.value = res.route; - this.currentKey = this.currentRoute.value.globalCacheKey ?? key ?? Date.now().toString(); + this.currentKey = res.route.globalCacheKey ?? key ?? path; - if (!initial) { + if (emitChange) { this.emit('change', { beforePath, path, - route: this.currentRoute.value, - props: this.currentProps, + resolved: res, key: this.currentKey, }); } - } - public getCurrentComponent() { - return this.currentComponent; - } - - public getCurrentProps() { - return this.currentProps; + return res; } public getCurrentPath() { @@ -223,17 +253,23 @@ export class Router extends EventEmitter<{ const cancel = this.navHook(path, flag); if (cancel) return; } - this.navigate(path, null); + const res = this.navigate(path, null); this.emit('push', { beforePath, path, - route: this.currentRoute.value, - props: this.currentProps, + route: res.route, + props: res.props, key: this.currentKey, }); } - public change(path: string, key?: string | null) { + public replace(path: string, key?: string | null, emitEvent = true) { this.navigate(path, key); + if (emitEvent) { + this.emit('replace', { + path, + key: this.currentKey, + }); + } } } diff --git a/packages/client/src/pages/_empty_.vue b/packages/client/src/pages/_empty_.vue new file mode 100644 index 0000000000..000b6decc9 --- /dev/null +++ b/packages/client/src/pages/_empty_.vue @@ -0,0 +1,7 @@ +<template> +<div></div> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +</script> diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue index d82880c34a..2ff55d351b 100644 --- a/packages/client/src/pages/admin/index.vue +++ b/packages/client/src/pages/admin/index.vue @@ -1,6 +1,6 @@ <template> <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> - <div v-if="!narrow || initialPage == null" class="nav"> + <div v-if="!narrow || currentPage?.route.name == null" class="nav"> <MkSpacer :content-max="700" :margin-min="16"> <div class="lxpfedzu"> <div class="banner"> @@ -12,12 +12,12 @@ <MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ $ts.configure }}</MkA></MkInfo> <MkInfo v-if="noEmailServer" warn class="info">{{ $ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ $ts.configure }}</MkA></MkInfo> - <MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu> + <MkSuperMenu :def="menuDef" :grid="currentPage?.route.name == null"></MkSuperMenu> </div> </MkSpacer> </div> - <div v-if="!(narrow && initialPage == null)" class="main"> - <component :is="component" :key="initialPage" v-bind="pageProps"/> + <div v-if="!(narrow && currentPage?.route.name == null)" class="main"> + <RouterView/> </div> </div> </template> @@ -44,15 +44,10 @@ const indexInfo = { hideHeader: true, }; -const props = defineProps<{ - initialPage?: string, -}>(); - provide('shouldOmitHeaderTitle', false); let INFO = $ref(indexInfo); let childInfo = $ref(null); -let page = $ref(props.initialPage); let narrow = $ref(false); let view = $ref(null); let el = $ref(null); @@ -61,6 +56,7 @@ let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instan let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha; let noEmailServer = !instance.enableEmail; let thereIsUnresolvedAbuseReport = $ref(false); +let currentPage = $computed(() => router.currentRef.value.child); os.api('admin/abuse-user-reports', { state: 'unresolved', @@ -94,47 +90,47 @@ const menuDef = $computed(() => [{ icon: 'fas fa-tachometer-alt', text: i18n.ts.dashboard, to: '/admin/overview', - active: props.initialPage === 'overview', + active: currentPage?.route.name === 'overview', }, { icon: 'fas fa-users', text: i18n.ts.users, to: '/admin/users', - active: props.initialPage === 'users', + active: currentPage?.route.name === 'users', }, { icon: 'fas fa-laugh', text: i18n.ts.customEmojis, to: '/admin/emojis', - active: props.initialPage === 'emojis', + active: currentPage?.route.name === 'emojis', }, { icon: 'fas fa-globe', text: i18n.ts.federation, to: '/about#federation', - active: props.initialPage === 'federation', + active: currentPage?.route.name === 'federation', }, { icon: 'fas fa-clipboard-list', text: i18n.ts.jobQueue, to: '/admin/queue', - active: props.initialPage === 'queue', + active: currentPage?.route.name === 'queue', }, { icon: 'fas fa-cloud', text: i18n.ts.files, to: '/admin/files', - active: props.initialPage === 'files', + active: currentPage?.route.name === 'files', }, { icon: 'fas fa-broadcast-tower', text: i18n.ts.announcements, to: '/admin/announcements', - active: props.initialPage === 'announcements', + active: currentPage?.route.name === 'announcements', }, { icon: 'fas fa-audio-description', text: i18n.ts.ads, to: '/admin/ads', - active: props.initialPage === 'ads', + active: currentPage?.route.name === 'ads', }, { icon: 'fas fa-exclamation-circle', text: i18n.ts.abuseReports, to: '/admin/abuses', - active: props.initialPage === 'abuses', + active: currentPage?.route.name === 'abuses', }], }, { title: i18n.ts.settings, @@ -142,47 +138,47 @@ const menuDef = $computed(() => [{ icon: 'fas fa-cog', text: i18n.ts.general, to: '/admin/settings', - active: props.initialPage === 'settings', + active: currentPage?.route.name === 'settings', }, { icon: 'fas fa-envelope', text: i18n.ts.emailServer, to: '/admin/email-settings', - active: props.initialPage === 'email-settings', + active: currentPage?.route.name === 'email-settings', }, { icon: 'fas fa-cloud', text: i18n.ts.objectStorage, to: '/admin/object-storage', - active: props.initialPage === 'object-storage', + active: currentPage?.route.name === 'object-storage', }, { icon: 'fas fa-lock', text: i18n.ts.security, to: '/admin/security', - active: props.initialPage === 'security', + active: currentPage?.route.name === 'security', }, { icon: 'fas fa-globe', text: i18n.ts.relays, to: '/admin/relays', - active: props.initialPage === 'relays', + active: currentPage?.route.name === 'relays', }, { icon: 'fas fa-share-alt', text: i18n.ts.integration, to: '/admin/integrations', - active: props.initialPage === 'integrations', + active: currentPage?.route.name === 'integrations', }, { icon: 'fas fa-ban', text: i18n.ts.instanceBlocking, to: '/admin/instance-block', - active: props.initialPage === 'instance-block', + active: currentPage?.route.name === 'instance-block', }, { icon: 'fas fa-ghost', text: i18n.ts.proxyAccount, to: '/admin/proxy-account', - active: props.initialPage === 'proxy-account', + active: currentPage?.route.name === 'proxy-account', }, { icon: 'fas fa-cogs', text: i18n.ts.other, to: '/admin/other-settings', - active: props.initialPage === 'other-settings', + active: currentPage?.route.name === 'other-settings', }], }, { title: i18n.ts.info, @@ -190,55 +186,12 @@ const menuDef = $computed(() => [{ icon: 'fas fa-database', text: i18n.ts.database, to: '/admin/database', - active: props.initialPage === 'database', + active: currentPage?.route.name === 'database', }], }]); -const component = $computed(() => { - if (props.initialPage == null) return null; - switch (props.initialPage) { - case 'overview': return defineAsyncComponent(() => import('./overview.vue')); - case 'users': return defineAsyncComponent(() => import('./users.vue')); - case 'emojis': return defineAsyncComponent(() => import('./emojis.vue')); - //case 'federation': return defineAsyncComponent(() => import('../federation.vue')); - case 'queue': return defineAsyncComponent(() => import('./queue.vue')); - case 'files': return defineAsyncComponent(() => import('./files.vue')); - case 'announcements': return defineAsyncComponent(() => import('./announcements.vue')); - case 'ads': return defineAsyncComponent(() => import('./ads.vue')); - case 'database': return defineAsyncComponent(() => import('./database.vue')); - case 'abuses': return defineAsyncComponent(() => import('./abuses.vue')); - case 'settings': return defineAsyncComponent(() => import('./settings.vue')); - case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue')); - case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue')); - case 'security': return defineAsyncComponent(() => import('./security.vue')); - case 'relays': return defineAsyncComponent(() => import('./relays.vue')); - case 'integrations': return defineAsyncComponent(() => import('./integrations.vue')); - case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue')); - case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue')); - case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue')); - } -}); - -watch(component, () => { - pageProps = {}; - - nextTick(() => { - scroll(el, { top: 0 }); - }); -}, { immediate: true }); - -watch(() => props.initialPage, () => { - if (props.initialPage == null && !narrow) { - router.push('/admin/overview'); - } else { - if (props.initialPage == null) { - INFO = indexInfo; - } - } -}); - watch(narrow, () => { - if (props.initialPage == null && !narrow) { + if (currentPage?.route.name == null && !narrow) { router.push('/admin/overview'); } }); @@ -247,7 +200,7 @@ onMounted(() => { ro.observe(el); narrow = el.offsetWidth < NARROW_THRESHOLD; - if (props.initialPage == null && !narrow) { + if (currentPage?.route.name == null && !narrow) { router.push('/admin/overview'); } }); diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue index 8b1cc6c124..8964333b31 100644 --- a/packages/client/src/pages/settings/index.vue +++ b/packages/client/src/pages/settings/index.vue @@ -4,15 +4,15 @@ <MkSpacer :content-max="900" :margin-min="20" :margin-max="32"> <div ref="el" class="vvcocwet" :class="{ wide: !narrow }"> <div class="body"> - <div v-if="!narrow || initialPage == null" class="nav"> + <div v-if="!narrow || currentPage?.route.name == null" class="nav"> <div class="baaadecd"> <MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo> - <MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu> + <MkSuperMenu :def="menuDef" :grid="currentPage?.route.name == null"></MkSuperMenu> </div> </div> - <div v-if="!(narrow && initialPage == null)" class="main"> + <div v-if="!(narrow && currentPage?.route.name == null)" class="main"> <div class="bkzroven"> - <component :is="component" :key="initialPage" v-bind="pageProps"/> + <RouterView/> </div> </div> </div> @@ -22,7 +22,7 @@ </template> <script setup lang="ts"> -import { computed, defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, ref, watch } from 'vue'; +import { computed, defineAsyncComponent, inject, nextTick, onActivated, onMounted, onUnmounted, provide, ref, watch } from 'vue'; import { i18n } from '@/i18n'; import MkInfo from '@/components/ui/info.vue'; import MkSuperMenu from '@/components/ui/super-menu.vue'; @@ -34,11 +34,6 @@ import { useRouter } from '@/router'; import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; import * as os from '@/os'; -const props = withDefaults(defineProps<{ - initialPage?: string; -}>(), { -}); - const indexInfo = { title: i18n.ts.settings, icon: 'fas fa-cog', @@ -50,12 +45,14 @@ const childInfo = ref(null); const router = useRouter(); -const narrow = ref(false); +let narrow = $ref(false); const NARROW_THRESHOLD = 600; +let currentPage = $computed(() => router.currentRef.value.child); + const ro = new ResizeObserver((entries, observer) => { if (entries.length === 0) return; - narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; + narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; }); const menuDef = computed(() => [{ @@ -64,42 +61,42 @@ const menuDef = computed(() => [{ icon: 'fas fa-user', text: i18n.ts.profile, to: '/settings/profile', - active: props.initialPage === 'profile', + active: currentPage?.route.name === 'profile', }, { icon: 'fas fa-lock-open', text: i18n.ts.privacy, to: '/settings/privacy', - active: props.initialPage === 'privacy', + active: currentPage?.route.name === 'privacy', }, { icon: 'fas fa-laugh', text: i18n.ts.reaction, to: '/settings/reaction', - active: props.initialPage === 'reaction', + active: currentPage?.route.name === 'reaction', }, { icon: 'fas fa-cloud', text: i18n.ts.drive, to: '/settings/drive', - active: props.initialPage === 'drive', + active: currentPage?.route.name === 'drive', }, { icon: 'fas fa-bell', text: i18n.ts.notifications, to: '/settings/notifications', - active: props.initialPage === 'notifications', + active: currentPage?.route.name === 'notifications', }, { icon: 'fas fa-envelope', text: i18n.ts.email, to: '/settings/email', - active: props.initialPage === 'email', + active: currentPage?.route.name === 'email', }, { icon: 'fas fa-share-alt', text: i18n.ts.integration, to: '/settings/integration', - active: props.initialPage === 'integration', + active: currentPage?.route.name === 'integration', }, { icon: 'fas fa-lock', text: i18n.ts.security, to: '/settings/security', - active: props.initialPage === 'security', + active: currentPage?.route.name === 'security', }], }, { title: i18n.ts.clientSettings, @@ -107,32 +104,32 @@ const menuDef = computed(() => [{ icon: 'fas fa-cogs', text: i18n.ts.general, to: '/settings/general', - active: props.initialPage === 'general', + active: currentPage?.route.name === 'general', }, { icon: 'fas fa-palette', text: i18n.ts.theme, to: '/settings/theme', - active: props.initialPage === 'theme', + active: currentPage?.route.name === 'theme', }, { icon: 'fas fa-bars', text: i18n.ts.navbar, to: '/settings/navbar', - active: props.initialPage === 'navbar', + active: currentPage?.route.name === 'navbar', }, { icon: 'fas fa-bars-progress', text: i18n.ts.statusbar, - to: '/settings/statusbars', - active: props.initialPage === 'statusbars', + to: '/settings/statusbar', + active: currentPage?.route.name === 'statusbar', }, { icon: 'fas fa-music', text: i18n.ts.sounds, to: '/settings/sounds', - active: props.initialPage === 'sounds', + active: currentPage?.route.name === 'sounds', }, { icon: 'fas fa-plug', text: i18n.ts.plugins, to: '/settings/plugin', - active: props.initialPage === 'plugin', + active: currentPage?.route.name === 'plugin', }], }, { title: i18n.ts.otherSettings, @@ -140,37 +137,37 @@ const menuDef = computed(() => [{ icon: 'fas fa-boxes', text: i18n.ts.importAndExport, to: '/settings/import-export', - active: props.initialPage === 'import-export', + active: currentPage?.route.name === 'import-export', }, { icon: 'fas fa-volume-mute', text: i18n.ts.instanceMute, to: '/settings/instance-mute', - active: props.initialPage === 'instance-mute', + active: currentPage?.route.name === 'instance-mute', }, { icon: 'fas fa-ban', text: i18n.ts.muteAndBlock, to: '/settings/mute-block', - active: props.initialPage === 'mute-block', + active: currentPage?.route.name === 'mute-block', }, { icon: 'fas fa-comment-slash', text: i18n.ts.wordMute, to: '/settings/word-mute', - active: props.initialPage === 'word-mute', + active: currentPage?.route.name === 'word-mute', }, { icon: 'fas fa-key', text: 'API', to: '/settings/api', - active: props.initialPage === 'api', + active: currentPage?.route.name === 'api', }, { icon: 'fas fa-bolt', text: 'Webhook', to: '/settings/webhook', - active: props.initialPage === 'webhook', + active: currentPage?.route.name === 'webhook', }, { icon: 'fas fa-ellipsis-h', text: i18n.ts.other, to: '/settings/other', - active: props.initialPage === 'other', + active: currentPage?.route.name === 'other', }], }, { items: [{ @@ -198,77 +195,24 @@ const menuDef = computed(() => [{ }], }]); -const pageProps = ref({}); -const component = computed(() => { - if (props.initialPage == null) return null; - switch (props.initialPage) { - case 'accounts': return defineAsyncComponent(() => import('./accounts.vue')); - case 'profile': return defineAsyncComponent(() => import('./profile.vue')); - case 'privacy': return defineAsyncComponent(() => import('./privacy.vue')); - case 'reaction': return defineAsyncComponent(() => import('./reaction.vue')); - case 'drive': return defineAsyncComponent(() => import('./drive.vue')); - case 'notifications': return defineAsyncComponent(() => import('./notifications.vue')); - case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue')); - case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue')); - case 'instance-mute': return defineAsyncComponent(() => import('./instance-mute.vue')); - case 'integration': return defineAsyncComponent(() => import('./integration.vue')); - case 'security': return defineAsyncComponent(() => import('./security.vue')); - case '2fa': return defineAsyncComponent(() => import('./2fa.vue')); - case 'api': return defineAsyncComponent(() => import('./api.vue')); - case 'webhook': return defineAsyncComponent(() => import('./webhook.vue')); - case 'webhook/new': return defineAsyncComponent(() => import('./webhook.new.vue')); - case 'webhook/edit': return defineAsyncComponent(() => import('./webhook.edit.vue')); - case 'apps': return defineAsyncComponent(() => import('./apps.vue')); - case 'other': return defineAsyncComponent(() => import('./other.vue')); - case 'general': return defineAsyncComponent(() => import('./general.vue')); - case 'email': return defineAsyncComponent(() => import('./email.vue')); - case 'theme': return defineAsyncComponent(() => import('./theme.vue')); - case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue')); - case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue')); - case 'navbar': return defineAsyncComponent(() => import('./navbar.vue')); - case 'statusbars': return defineAsyncComponent(() => import('./statusbars.vue')); - case 'sounds': return defineAsyncComponent(() => import('./sounds.vue')); - case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue')); - case 'deck': return defineAsyncComponent(() => import('./deck.vue')); - case 'plugin': return defineAsyncComponent(() => import('./plugin.vue')); - case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue')); - case 'import-export': return defineAsyncComponent(() => import('./import-export.vue')); - case 'account-info': return defineAsyncComponent(() => import('./account-info.vue')); - case 'delete-account': return defineAsyncComponent(() => import('./delete-account.vue')); - } - return null; -}); - -watch(component, () => { - pageProps.value = {}; - - nextTick(() => { - scroll(el.value, { top: 0 }); - }); -}, { immediate: true }); - -watch(() => props.initialPage, () => { - if (props.initialPage == null && !narrow.value) { - router.push('/settings/profile'); - } else { - if (props.initialPage == null) { - INFO.value = indexInfo; - } - } -}); - -watch(narrow, () => { - if (props.initialPage == null && !narrow.value) { - router.push('/settings/profile'); - } +watch($$(narrow), () => { }); onMounted(() => { ro.observe(el.value); - narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; - if (props.initialPage == null && !narrow.value) { - router.push('/settings/profile'); + narrow = el.value.offsetWidth < NARROW_THRESHOLD; + + if (!narrow && currentPage?.route.name == null) { + router.replace('/settings/profile'); + } +}); + +onActivated(() => { + narrow = el.value.offsetWidth < NARROW_THRESHOLD; + + if (!narrow && currentPage?.route.name == null) { + router.replace('/settings/profile'); } }); diff --git a/packages/client/src/pages/settings/statusbars.statusbar.vue b/packages/client/src/pages/settings/statusbar.statusbar.vue similarity index 100% rename from packages/client/src/pages/settings/statusbars.statusbar.vue rename to packages/client/src/pages/settings/statusbar.statusbar.vue diff --git a/packages/client/src/pages/settings/statusbars.vue b/packages/client/src/pages/settings/statusbar.vue similarity index 96% rename from packages/client/src/pages/settings/statusbars.vue rename to packages/client/src/pages/settings/statusbar.vue index c81bd7fbdf..3f23ed470c 100644 --- a/packages/client/src/pages/settings/statusbars.vue +++ b/packages/client/src/pages/settings/statusbar.vue @@ -12,7 +12,7 @@ <script lang="ts" setup> import { computed, onMounted, ref, watch } from 'vue'; import { v4 as uuid } from 'uuid'; -import XStatusbar from './statusbars.statusbar.vue'; +import XStatusbar from './statusbar.statusbar.vue'; import FormRadios from '@/components/form/radios.vue'; import FormFolder from '@/components/form/folder.vue'; import FormButton from '@/components/ui/button.vue'; diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index b61b77eeeb..f3ca521832 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -42,9 +42,97 @@ export const routes = [{ component: page(() => import('./pages/instance-info.vue')), }, { name: 'settings', - path: '/settings/:initialPage(*)?', + path: '/settings', component: page(() => import('./pages/settings/index.vue')), loginRequired: true, + children: [{ + path: '/profile', + name: 'profile', + component: page(() => import('./pages/settings/profile.vue')), + }, { + path: '/privacy', + name: 'privacy', + component: page(() => import('./pages/settings/privacy.vue')), + }, { + path: '/reaction', + name: 'reaction', + component: page(() => import('./pages/settings/reaction.vue')), + }, { + path: '/drive', + name: 'drive', + component: page(() => import('./pages/settings/drive.vue')), + }, { + path: '/notifications', + name: 'notifications', + component: page(() => import('./pages/settings/notifications.vue')), + }, { + path: '/email', + name: 'email', + component: page(() => import('./pages/settings/email.vue')), + }, { + path: '/integration', + name: 'integration', + component: page(() => import('./pages/settings/integration.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', + 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', + name: 'plugin', + component: page(() => import('./pages/settings/plugin.vue')), + }, { + path: '/import-export', + name: 'import-export', + component: page(() => import('./pages/settings/import-export.vue')), + }, { + path: '/instance-mute', + name: 'instance-mute', + component: page(() => import('./pages/settings/instance-mute.vue')), + }, { + path: '/mute-block', + name: 'mute-block', + component: page(() => import('./pages/settings/mute-block.vue')), + }, { + path: '/word-mute', + name: 'word-mute', + component: page(() => import('./pages/settings/word-mute.vue')), + }, { + path: '/api', + name: 'api', + component: page(() => import('./pages/settings/api.vue')), + }, { + path: '/webhook', + name: 'webhook', + component: page(() => import('./pages/settings/webhook.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')), @@ -166,8 +254,84 @@ export const routes = [{ path: '/admin/file/:fileId', component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), }, { - path: '/admin/:initialPage(*)?', + 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/admin/emojis.vue')), + }, { + path: '/queue', + name: 'queue', + component: page(() => import('./pages/admin/queue.vue')), + }, { + path: '/files', + name: 'files', + component: page(() => import('./pages/admin/files.vue')), + }, { + path: '/announcements', + name: 'announcements', + component: page(() => import('./pages/admin/announcements.vue')), + }, { + path: '/ads', + name: 'ads', + component: page(() => import('./pages/admin/ads.vue')), + }, { + path: '/database', + name: 'database', + component: page(() => import('./pages/admin/database.vue')), + }, { + path: '/abuses', + name: 'abuses', + component: page(() => import('./pages/admin/abuses.vue')), + }, { + path: '/settings', + name: 'settings', + component: page(() => import('./pages/admin/settings.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: '/integrations', + name: 'integrations', + component: page(() => import('./pages/admin/integrations.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: '/other-settings', + name: 'other-settings', + component: page(() => import('./pages/admin/other-settings.vue')), + }, { + path: '/', + component: page(() => import('./pages/_empty_.vue')), + }], }, { path: '/my/notifications', component: page(() => import('./pages/notifications.vue')), @@ -267,12 +431,16 @@ mainRouter.addListener('push', ctx => { } }); +mainRouter.addListener('replace', ctx => { + window.history.replaceState({ key: ctx.key }, '', ctx.path); +}); + mainRouter.addListener('same', () => { window.scroll({ top: 0, behavior: 'smooth' }); }); window.addEventListener('popstate', (event) => { - mainRouter.change(location.pathname + location.search + location.hash, event.state?.key); + mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key, false); const scrollPos = scrollPosStore.get(event.state?.key) ?? 0; window.scroll({ top: scrollPos, behavior: 'instant' }); window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール