enhance(client): ネストしたルーティングに対応
This commit is contained in:
		
							parent
							
								
									17afbc3c46
								
							
						
					
					
						commit
						66f1aaf5f7
					
				
					 9 changed files with 391 additions and 265 deletions
				
			
		|  | @ -11,8 +11,8 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { inject, nextTick, onMounted, onUnmounted, watch } from 'vue'; | import { inject, nextTick, onBeforeUnmount, onMounted, onUnmounted, provide, watch } from 'vue'; | ||||||
| import { Router } from '@/nirax'; | import { Resolved, Router } from '@/nirax'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
|  | @ -25,19 +25,37 @@ if (router == null) { | ||||||
| 	throw new Error('no router provided'); | 	throw new Error('no router provided'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| let currentPageComponent = $shallowRef(router.getCurrentComponent()); | const currentDepth = inject('routerCurrentDepth', 0); | ||||||
| let currentPageProps = $ref(router.getCurrentProps()); | provide('routerCurrentDepth', currentDepth + 1); | ||||||
| let key = $ref(router.getCurrentKey()); |  | ||||||
| 
 | 
 | ||||||
| function onChange({ route, props: newProps, key: newKey }) { | function resolveNested(current: Resolved, d = 0): Resolved | null { | ||||||
| 	currentPageComponent = route.component; | 	if (d === currentDepth) { | ||||||
| 	currentPageProps = newProps; | 		return current; | ||||||
| 	key = newKey; | 	} 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); | router.addListener('change', onChange); | ||||||
| 
 | 
 | ||||||
| onUnmounted(() => { | onBeforeUnmount(() => { | ||||||
| 	router.removeListener('change', onChange); | 	router.removeListener('change', onChange); | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -114,7 +114,7 @@ function menu(ev) { | ||||||
| 
 | 
 | ||||||
| function back() { | function back() { | ||||||
| 	history.pop(); | 	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() { | function close() { | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ type RouteDef = { | ||||||
| 	name?: string; | 	name?: string; | ||||||
| 	hash?: string; | 	hash?: string; | ||||||
| 	globalCacheKey?: string; | 	globalCacheKey?: string; | ||||||
|  | 	children?: RouteDef[]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| type ParsedPath = (string | { | type ParsedPath = (string | { | ||||||
|  | @ -22,6 +23,8 @@ type ParsedPath = (string | { | ||||||
| 	optional?: boolean; | 	optional?: boolean; | ||||||
| })[]; | })[]; | ||||||
| 
 | 
 | ||||||
|  | export type Resolved = { route: RouteDef; props: Map<string, string>; child?: Resolved; }; | ||||||
|  | 
 | ||||||
| function parsePath(path: string): ParsedPath { | function parsePath(path: string): ParsedPath { | ||||||
| 	const res = [] as ParsedPath; | 	const res = [] as ParsedPath; | ||||||
| 
 | 
 | ||||||
|  | @ -51,8 +54,11 @@ export class Router extends EventEmitter<{ | ||||||
| 	change: (ctx: { | 	change: (ctx: { | ||||||
| 		beforePath: string; | 		beforePath: string; | ||||||
| 		path: string; | 		path: string; | ||||||
| 		route: RouteDef | null; | 		resolved: Resolved; | ||||||
| 		props: Map<string, string> | null; | 		key: string; | ||||||
|  | 	}) => void; | ||||||
|  | 	replace: (ctx: { | ||||||
|  | 		path: string; | ||||||
| 		key: string; | 		key: string; | ||||||
| 	}) => void; | 	}) => void; | ||||||
| 	push: (ctx: { | 	push: (ctx: { | ||||||
|  | @ -65,12 +71,12 @@ export class Router extends EventEmitter<{ | ||||||
| 	same: () => void; | 	same: () => void; | ||||||
| }> { | }> { | ||||||
| 	private routes: RouteDef[]; | 	private routes: RouteDef[]; | ||||||
|  | 	public current: Resolved; | ||||||
|  | 	public currentRef: ShallowRef<Resolved> = shallowRef(); | ||||||
|  | 	public currentRoute: ShallowRef<RouteDef> = shallowRef(); | ||||||
| 	private currentPath: string; | 	private currentPath: string; | ||||||
| 	private currentComponent: Component | null = null; |  | ||||||
| 	private currentProps: Map<string, string> | null = null; |  | ||||||
| 	private currentKey = Date.now().toString(); | 	private currentKey = Date.now().toString(); | ||||||
| 
 | 
 | ||||||
| 	public currentRoute: ShallowRef<RouteDef | null> = shallowRef(null); |  | ||||||
| 	public navHook: ((path: string, flag?: any) => boolean) | null = null; | 	public navHook: ((path: string, flag?: any) => boolean) | null = null; | ||||||
| 
 | 
 | ||||||
| 	constructor(routes: Router['routes'], currentPath: Router['currentPath']) { | 	constructor(routes: Router['routes'], currentPath: Router['currentPath']) { | ||||||
|  | @ -78,10 +84,10 @@ export class Router extends EventEmitter<{ | ||||||
| 
 | 
 | ||||||
| 		this.routes = routes; | 		this.routes = routes; | ||||||
| 		this.currentPath = currentPath; | 		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 queryString: string | null = null; | ||||||
| 		let hash: string | null = null; | 		let hash: string | null = null; | ||||||
| 		if (path[0] === '/') path = path.substring(1); | 		if (path[0] === '/') path = path.substring(1); | ||||||
|  | @ -96,77 +102,108 @@ export class Router extends EventEmitter<{ | ||||||
| 
 | 
 | ||||||
| 		if (_DEV_) console.log('Routing: ', path, queryString); | 		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: | 				pathMatchLoop: | ||||||
| 		for (const route of this.routes) { | 				for (const p of parsePath(route.path)) { | ||||||
| 			let parts = [ ..._parts ]; | 					if (typeof p === 'string') { | ||||||
| 			const props = new Map<string, string>(); | 						if (p === parts[0]) { | ||||||
| 
 |  | ||||||
| 			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))); |  | ||||||
| 							parts.shift(); | 							parts.shift(); | ||||||
| 						} else { | 						} else { | ||||||
| 							if (parts[0]) { | 							continue forEachRouteLoop; | ||||||
| 								props.set(p.name, safeURIDecode(parts[0])); | 						} | ||||||
|  | 					} 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) { | 					if (route.hash != null && hash != null) { | ||||||
| 				props.set(route.hash, safeURIDecode(hash)); | 						props.set(route.hash, safeURIDecode(hash)); | ||||||
| 			} | 					} | ||||||
| 
 | 	 | ||||||
| 			if (route.query != null && queryString != null) { | 					if (route.query != null && queryString != null) { | ||||||
| 				const queryObject = [...new URLSearchParams(queryString).entries()] | 						const queryObject = [...new URLSearchParams(queryString).entries()] | ||||||
| 					.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}); | 							.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}); | ||||||
| 
 | 	 | ||||||
| 				for (const q in route.query) { | 						for (const q in route.query) { | ||||||
| 					const as = route.query[q]; | 							const as = route.query[q]; | ||||||
| 					if (queryObject[q]) { | 							if (queryObject[q]) { | ||||||
| 						props.set(as, safeURIDecode(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 { | 			return null; | ||||||
| 				route, |  | ||||||
| 				props, |  | ||||||
| 			}; |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		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 beforePath = this.currentPath; | ||||||
| 		const beforeRoute = this.currentRoute.value; |  | ||||||
| 		this.currentPath = path; | 		this.currentPath = path; | ||||||
| 
 | 
 | ||||||
| 		const res = this.resolve(this.currentPath); | 		const res = this.resolve(this.currentPath); | ||||||
|  | @ -181,28 +218,21 @@ export class Router extends EventEmitter<{ | ||||||
| 
 | 
 | ||||||
| 		const isSamePath = beforePath === path; | 		const isSamePath = beforePath === path; | ||||||
| 		if (isSamePath && key == null) key = this.currentKey; | 		if (isSamePath && key == null) key = this.currentKey; | ||||||
| 		this.currentComponent = res.route.component; | 		this.current = res; | ||||||
| 		this.currentProps = res.props; | 		this.currentRef.value = res; | ||||||
| 		this.currentRoute.value = res.route; | 		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', { | 			this.emit('change', { | ||||||
| 				beforePath, | 				beforePath, | ||||||
| 				path, | 				path, | ||||||
| 				route: this.currentRoute.value, | 				resolved: res, | ||||||
| 				props: this.currentProps, |  | ||||||
| 				key: this.currentKey, | 				key: this.currentKey, | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	public getCurrentComponent() { | 		return res; | ||||||
| 		return this.currentComponent; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	public getCurrentProps() { |  | ||||||
| 		return this.currentProps; |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public getCurrentPath() { | 	public getCurrentPath() { | ||||||
|  | @ -223,17 +253,23 @@ export class Router extends EventEmitter<{ | ||||||
| 			const cancel = this.navHook(path, flag); | 			const cancel = this.navHook(path, flag); | ||||||
| 			if (cancel) return; | 			if (cancel) return; | ||||||
| 		} | 		} | ||||||
| 		this.navigate(path, null); | 		const res = this.navigate(path, null); | ||||||
| 		this.emit('push', { | 		this.emit('push', { | ||||||
| 			beforePath, | 			beforePath, | ||||||
| 			path, | 			path, | ||||||
| 			route: this.currentRoute.value, | 			route: res.route, | ||||||
| 			props: this.currentProps, | 			props: res.props, | ||||||
| 			key: this.currentKey, | 			key: this.currentKey, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public change(path: string, key?: string | null) { | 	public replace(path: string, key?: string | null, emitEvent = true) { | ||||||
| 		this.navigate(path, key); | 		this.navigate(path, key); | ||||||
|  | 		if (emitEvent) { | ||||||
|  | 			this.emit('replace', { | ||||||
|  | 				path, | ||||||
|  | 				key: this.currentKey, | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										7
									
								
								packages/client/src/pages/_empty_.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/client/src/pages/_empty_.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | <template> | ||||||
|  | <div></div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { } from 'vue'; | ||||||
|  | </script> | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> | <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"> | 		<MkSpacer :content-max="700" :margin-min="16"> | ||||||
| 			<div class="lxpfedzu"> | 			<div class="lxpfedzu"> | ||||||
| 				<div class="banner"> | 				<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="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> | 				<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> | 			</div> | ||||||
| 		</MkSpacer> | 		</MkSpacer> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div v-if="!(narrow && initialPage == null)" class="main"> | 	<div v-if="!(narrow && currentPage?.route.name == null)" class="main"> | ||||||
| 		<component :is="component" :key="initialPage" v-bind="pageProps"/> | 		<RouterView/> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  | @ -44,15 +44,10 @@ const indexInfo = { | ||||||
| 	hideHeader: true, | 	hideHeader: true, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ |  | ||||||
| 	initialPage?: string, |  | ||||||
| }>(); |  | ||||||
| 
 |  | ||||||
| provide('shouldOmitHeaderTitle', false); | provide('shouldOmitHeaderTitle', false); | ||||||
| 
 | 
 | ||||||
| let INFO = $ref(indexInfo); | let INFO = $ref(indexInfo); | ||||||
| let childInfo = $ref(null); | let childInfo = $ref(null); | ||||||
| let page = $ref(props.initialPage); |  | ||||||
| let narrow = $ref(false); | let narrow = $ref(false); | ||||||
| let view = $ref(null); | let view = $ref(null); | ||||||
| let el = $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 noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha; | ||||||
| let noEmailServer = !instance.enableEmail; | let noEmailServer = !instance.enableEmail; | ||||||
| let thereIsUnresolvedAbuseReport = $ref(false); | let thereIsUnresolvedAbuseReport = $ref(false); | ||||||
|  | let currentPage = $computed(() => router.currentRef.value.child); | ||||||
| 
 | 
 | ||||||
| os.api('admin/abuse-user-reports', { | os.api('admin/abuse-user-reports', { | ||||||
| 	state: 'unresolved', | 	state: 'unresolved', | ||||||
|  | @ -94,47 +90,47 @@ const menuDef = $computed(() => [{ | ||||||
| 		icon: 'fas fa-tachometer-alt', | 		icon: 'fas fa-tachometer-alt', | ||||||
| 		text: i18n.ts.dashboard, | 		text: i18n.ts.dashboard, | ||||||
| 		to: '/admin/overview', | 		to: '/admin/overview', | ||||||
| 		active: props.initialPage === 'overview', | 		active: currentPage?.route.name === 'overview', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-users', | 		icon: 'fas fa-users', | ||||||
| 		text: i18n.ts.users, | 		text: i18n.ts.users, | ||||||
| 		to: '/admin/users', | 		to: '/admin/users', | ||||||
| 		active: props.initialPage === 'users', | 		active: currentPage?.route.name === 'users', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-laugh', | 		icon: 'fas fa-laugh', | ||||||
| 		text: i18n.ts.customEmojis, | 		text: i18n.ts.customEmojis, | ||||||
| 		to: '/admin/emojis', | 		to: '/admin/emojis', | ||||||
| 		active: props.initialPage === 'emojis', | 		active: currentPage?.route.name === 'emojis', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-globe', | 		icon: 'fas fa-globe', | ||||||
| 		text: i18n.ts.federation, | 		text: i18n.ts.federation, | ||||||
| 		to: '/about#federation', | 		to: '/about#federation', | ||||||
| 		active: props.initialPage === 'federation', | 		active: currentPage?.route.name === 'federation', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-clipboard-list', | 		icon: 'fas fa-clipboard-list', | ||||||
| 		text: i18n.ts.jobQueue, | 		text: i18n.ts.jobQueue, | ||||||
| 		to: '/admin/queue', | 		to: '/admin/queue', | ||||||
| 		active: props.initialPage === 'queue', | 		active: currentPage?.route.name === 'queue', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-cloud', | 		icon: 'fas fa-cloud', | ||||||
| 		text: i18n.ts.files, | 		text: i18n.ts.files, | ||||||
| 		to: '/admin/files', | 		to: '/admin/files', | ||||||
| 		active: props.initialPage === 'files', | 		active: currentPage?.route.name === 'files', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-broadcast-tower', | 		icon: 'fas fa-broadcast-tower', | ||||||
| 		text: i18n.ts.announcements, | 		text: i18n.ts.announcements, | ||||||
| 		to: '/admin/announcements', | 		to: '/admin/announcements', | ||||||
| 		active: props.initialPage === 'announcements', | 		active: currentPage?.route.name === 'announcements', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-audio-description', | 		icon: 'fas fa-audio-description', | ||||||
| 		text: i18n.ts.ads, | 		text: i18n.ts.ads, | ||||||
| 		to: '/admin/ads', | 		to: '/admin/ads', | ||||||
| 		active: props.initialPage === 'ads', | 		active: currentPage?.route.name === 'ads', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-exclamation-circle', | 		icon: 'fas fa-exclamation-circle', | ||||||
| 		text: i18n.ts.abuseReports, | 		text: i18n.ts.abuseReports, | ||||||
| 		to: '/admin/abuses', | 		to: '/admin/abuses', | ||||||
| 		active: props.initialPage === 'abuses', | 		active: currentPage?.route.name === 'abuses', | ||||||
| 	}], | 	}], | ||||||
| }, { | }, { | ||||||
| 	title: i18n.ts.settings, | 	title: i18n.ts.settings, | ||||||
|  | @ -142,47 +138,47 @@ const menuDef = $computed(() => [{ | ||||||
| 		icon: 'fas fa-cog', | 		icon: 'fas fa-cog', | ||||||
| 		text: i18n.ts.general, | 		text: i18n.ts.general, | ||||||
| 		to: '/admin/settings', | 		to: '/admin/settings', | ||||||
| 		active: props.initialPage === 'settings', | 		active: currentPage?.route.name === 'settings', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-envelope', | 		icon: 'fas fa-envelope', | ||||||
| 		text: i18n.ts.emailServer, | 		text: i18n.ts.emailServer, | ||||||
| 		to: '/admin/email-settings', | 		to: '/admin/email-settings', | ||||||
| 		active: props.initialPage === 'email-settings', | 		active: currentPage?.route.name === 'email-settings', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-cloud', | 		icon: 'fas fa-cloud', | ||||||
| 		text: i18n.ts.objectStorage, | 		text: i18n.ts.objectStorage, | ||||||
| 		to: '/admin/object-storage', | 		to: '/admin/object-storage', | ||||||
| 		active: props.initialPage === 'object-storage', | 		active: currentPage?.route.name === 'object-storage', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-lock', | 		icon: 'fas fa-lock', | ||||||
| 		text: i18n.ts.security, | 		text: i18n.ts.security, | ||||||
| 		to: '/admin/security', | 		to: '/admin/security', | ||||||
| 		active: props.initialPage === 'security', | 		active: currentPage?.route.name === 'security', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-globe', | 		icon: 'fas fa-globe', | ||||||
| 		text: i18n.ts.relays, | 		text: i18n.ts.relays, | ||||||
| 		to: '/admin/relays', | 		to: '/admin/relays', | ||||||
| 		active: props.initialPage === 'relays', | 		active: currentPage?.route.name === 'relays', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-share-alt', | 		icon: 'fas fa-share-alt', | ||||||
| 		text: i18n.ts.integration, | 		text: i18n.ts.integration, | ||||||
| 		to: '/admin/integrations', | 		to: '/admin/integrations', | ||||||
| 		active: props.initialPage === 'integrations', | 		active: currentPage?.route.name === 'integrations', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-ban', | 		icon: 'fas fa-ban', | ||||||
| 		text: i18n.ts.instanceBlocking, | 		text: i18n.ts.instanceBlocking, | ||||||
| 		to: '/admin/instance-block', | 		to: '/admin/instance-block', | ||||||
| 		active: props.initialPage === 'instance-block', | 		active: currentPage?.route.name === 'instance-block', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-ghost', | 		icon: 'fas fa-ghost', | ||||||
| 		text: i18n.ts.proxyAccount, | 		text: i18n.ts.proxyAccount, | ||||||
| 		to: '/admin/proxy-account', | 		to: '/admin/proxy-account', | ||||||
| 		active: props.initialPage === 'proxy-account', | 		active: currentPage?.route.name === 'proxy-account', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-cogs', | 		icon: 'fas fa-cogs', | ||||||
| 		text: i18n.ts.other, | 		text: i18n.ts.other, | ||||||
| 		to: '/admin/other-settings', | 		to: '/admin/other-settings', | ||||||
| 		active: props.initialPage === 'other-settings', | 		active: currentPage?.route.name === 'other-settings', | ||||||
| 	}], | 	}], | ||||||
| }, { | }, { | ||||||
| 	title: i18n.ts.info, | 	title: i18n.ts.info, | ||||||
|  | @ -190,55 +186,12 @@ const menuDef = $computed(() => [{ | ||||||
| 		icon: 'fas fa-database', | 		icon: 'fas fa-database', | ||||||
| 		text: i18n.ts.database, | 		text: i18n.ts.database, | ||||||
| 		to: '/admin/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, () => { | watch(narrow, () => { | ||||||
| 	if (props.initialPage == null && !narrow) { | 	if (currentPage?.route.name == null && !narrow) { | ||||||
| 		router.push('/admin/overview'); | 		router.push('/admin/overview'); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | @ -247,7 +200,7 @@ onMounted(() => { | ||||||
| 	ro.observe(el); | 	ro.observe(el); | ||||||
| 
 | 
 | ||||||
| 	narrow = el.offsetWidth < NARROW_THRESHOLD; | 	narrow = el.offsetWidth < NARROW_THRESHOLD; | ||||||
| 	if (props.initialPage == null && !narrow) { | 	if (currentPage?.route.name == null && !narrow) { | ||||||
| 		router.push('/admin/overview'); | 		router.push('/admin/overview'); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -4,15 +4,15 @@ | ||||||
| 	<MkSpacer :content-max="900" :margin-min="20" :margin-max="32"> | 	<MkSpacer :content-max="900" :margin-min="20" :margin-max="32"> | ||||||
| 		<div ref="el" class="vvcocwet" :class="{ wide: !narrow }"> | 		<div ref="el" class="vvcocwet" :class="{ wide: !narrow }"> | ||||||
| 			<div class="body"> | 			<div class="body"> | ||||||
| 				<div v-if="!narrow || initialPage == null" class="nav"> | 				<div v-if="!narrow || currentPage?.route.name == null" class="nav"> | ||||||
| 					<div class="baaadecd"> | 					<div class="baaadecd"> | ||||||
| 						<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo> | 						<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> | 				</div> | ||||||
| 				<div v-if="!(narrow && initialPage == null)" class="main"> | 				<div v-if="!(narrow && currentPage?.route.name == null)" class="main"> | ||||||
| 					<div class="bkzroven"> | 					<div class="bkzroven"> | ||||||
| 						<component :is="component" :key="initialPage" v-bind="pageProps"/> | 						<RouterView/> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
|  | @ -22,7 +22,7 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> | <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 { i18n } from '@/i18n'; | ||||||
| import MkInfo from '@/components/ui/info.vue'; | import MkInfo from '@/components/ui/info.vue'; | ||||||
| import MkSuperMenu from '@/components/ui/super-menu.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 { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| const props = withDefaults(defineProps<{ |  | ||||||
|   initialPage?: string; |  | ||||||
| }>(), { |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const indexInfo = { | const indexInfo = { | ||||||
| 	title: i18n.ts.settings, | 	title: i18n.ts.settings, | ||||||
| 	icon: 'fas fa-cog', | 	icon: 'fas fa-cog', | ||||||
|  | @ -50,12 +45,14 @@ const childInfo = ref(null); | ||||||
| 
 | 
 | ||||||
| const router = useRouter(); | const router = useRouter(); | ||||||
| 
 | 
 | ||||||
| const narrow = ref(false); | let narrow = $ref(false); | ||||||
| const NARROW_THRESHOLD = 600; | const NARROW_THRESHOLD = 600; | ||||||
| 
 | 
 | ||||||
|  | let currentPage = $computed(() => router.currentRef.value.child); | ||||||
|  | 
 | ||||||
| const ro = new ResizeObserver((entries, observer) => { | const ro = new ResizeObserver((entries, observer) => { | ||||||
| 	if (entries.length === 0) return; | 	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(() => [{ | const menuDef = computed(() => [{ | ||||||
|  | @ -64,42 +61,42 @@ const menuDef = computed(() => [{ | ||||||
| 		icon: 'fas fa-user', | 		icon: 'fas fa-user', | ||||||
| 		text: i18n.ts.profile, | 		text: i18n.ts.profile, | ||||||
| 		to: '/settings/profile', | 		to: '/settings/profile', | ||||||
| 		active: props.initialPage === 'profile', | 		active: currentPage?.route.name === 'profile', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-lock-open', | 		icon: 'fas fa-lock-open', | ||||||
| 		text: i18n.ts.privacy, | 		text: i18n.ts.privacy, | ||||||
| 		to: '/settings/privacy', | 		to: '/settings/privacy', | ||||||
| 		active: props.initialPage === 'privacy', | 		active: currentPage?.route.name === 'privacy', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-laugh', | 		icon: 'fas fa-laugh', | ||||||
| 		text: i18n.ts.reaction, | 		text: i18n.ts.reaction, | ||||||
| 		to: '/settings/reaction', | 		to: '/settings/reaction', | ||||||
| 		active: props.initialPage === 'reaction', | 		active: currentPage?.route.name === 'reaction', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-cloud', | 		icon: 'fas fa-cloud', | ||||||
| 		text: i18n.ts.drive, | 		text: i18n.ts.drive, | ||||||
| 		to: '/settings/drive', | 		to: '/settings/drive', | ||||||
| 		active: props.initialPage === 'drive', | 		active: currentPage?.route.name === 'drive', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-bell', | 		icon: 'fas fa-bell', | ||||||
| 		text: i18n.ts.notifications, | 		text: i18n.ts.notifications, | ||||||
| 		to: '/settings/notifications', | 		to: '/settings/notifications', | ||||||
| 		active: props.initialPage === 'notifications', | 		active: currentPage?.route.name === 'notifications', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-envelope', | 		icon: 'fas fa-envelope', | ||||||
| 		text: i18n.ts.email, | 		text: i18n.ts.email, | ||||||
| 		to: '/settings/email', | 		to: '/settings/email', | ||||||
| 		active: props.initialPage === 'email', | 		active: currentPage?.route.name === 'email', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-share-alt', | 		icon: 'fas fa-share-alt', | ||||||
| 		text: i18n.ts.integration, | 		text: i18n.ts.integration, | ||||||
| 		to: '/settings/integration', | 		to: '/settings/integration', | ||||||
| 		active: props.initialPage === 'integration', | 		active: currentPage?.route.name === 'integration', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-lock', | 		icon: 'fas fa-lock', | ||||||
| 		text: i18n.ts.security, | 		text: i18n.ts.security, | ||||||
| 		to: '/settings/security', | 		to: '/settings/security', | ||||||
| 		active: props.initialPage === 'security', | 		active: currentPage?.route.name === 'security', | ||||||
| 	}], | 	}], | ||||||
| }, { | }, { | ||||||
| 	title: i18n.ts.clientSettings, | 	title: i18n.ts.clientSettings, | ||||||
|  | @ -107,32 +104,32 @@ const menuDef = computed(() => [{ | ||||||
| 		icon: 'fas fa-cogs', | 		icon: 'fas fa-cogs', | ||||||
| 		text: i18n.ts.general, | 		text: i18n.ts.general, | ||||||
| 		to: '/settings/general', | 		to: '/settings/general', | ||||||
| 		active: props.initialPage === 'general', | 		active: currentPage?.route.name === 'general', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-palette', | 		icon: 'fas fa-palette', | ||||||
| 		text: i18n.ts.theme, | 		text: i18n.ts.theme, | ||||||
| 		to: '/settings/theme', | 		to: '/settings/theme', | ||||||
| 		active: props.initialPage === 'theme', | 		active: currentPage?.route.name === 'theme', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-bars', | 		icon: 'fas fa-bars', | ||||||
| 		text: i18n.ts.navbar, | 		text: i18n.ts.navbar, | ||||||
| 		to: '/settings/navbar', | 		to: '/settings/navbar', | ||||||
| 		active: props.initialPage === 'navbar', | 		active: currentPage?.route.name === 'navbar', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-bars-progress', | 		icon: 'fas fa-bars-progress', | ||||||
| 		text: i18n.ts.statusbar, | 		text: i18n.ts.statusbar, | ||||||
| 		to: '/settings/statusbars', | 		to: '/settings/statusbar', | ||||||
| 		active: props.initialPage === 'statusbars', | 		active: currentPage?.route.name === 'statusbar', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-music', | 		icon: 'fas fa-music', | ||||||
| 		text: i18n.ts.sounds, | 		text: i18n.ts.sounds, | ||||||
| 		to: '/settings/sounds', | 		to: '/settings/sounds', | ||||||
| 		active: props.initialPage === 'sounds', | 		active: currentPage?.route.name === 'sounds', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-plug', | 		icon: 'fas fa-plug', | ||||||
| 		text: i18n.ts.plugins, | 		text: i18n.ts.plugins, | ||||||
| 		to: '/settings/plugin', | 		to: '/settings/plugin', | ||||||
| 		active: props.initialPage === 'plugin', | 		active: currentPage?.route.name === 'plugin', | ||||||
| 	}], | 	}], | ||||||
| }, { | }, { | ||||||
| 	title: i18n.ts.otherSettings, | 	title: i18n.ts.otherSettings, | ||||||
|  | @ -140,37 +137,37 @@ const menuDef = computed(() => [{ | ||||||
| 		icon: 'fas fa-boxes', | 		icon: 'fas fa-boxes', | ||||||
| 		text: i18n.ts.importAndExport, | 		text: i18n.ts.importAndExport, | ||||||
| 		to: '/settings/import-export', | 		to: '/settings/import-export', | ||||||
| 		active: props.initialPage === 'import-export', | 		active: currentPage?.route.name === 'import-export', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-volume-mute', | 		icon: 'fas fa-volume-mute', | ||||||
| 		text: i18n.ts.instanceMute, | 		text: i18n.ts.instanceMute, | ||||||
| 		to: '/settings/instance-mute', | 		to: '/settings/instance-mute', | ||||||
| 		active: props.initialPage === 'instance-mute', | 		active: currentPage?.route.name === 'instance-mute', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-ban', | 		icon: 'fas fa-ban', | ||||||
| 		text: i18n.ts.muteAndBlock, | 		text: i18n.ts.muteAndBlock, | ||||||
| 		to: '/settings/mute-block', | 		to: '/settings/mute-block', | ||||||
| 		active: props.initialPage === 'mute-block', | 		active: currentPage?.route.name === 'mute-block', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-comment-slash', | 		icon: 'fas fa-comment-slash', | ||||||
| 		text: i18n.ts.wordMute, | 		text: i18n.ts.wordMute, | ||||||
| 		to: '/settings/word-mute', | 		to: '/settings/word-mute', | ||||||
| 		active: props.initialPage === 'word-mute', | 		active: currentPage?.route.name === 'word-mute', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-key', | 		icon: 'fas fa-key', | ||||||
| 		text: 'API', | 		text: 'API', | ||||||
| 		to: '/settings/api', | 		to: '/settings/api', | ||||||
| 		active: props.initialPage === 'api', | 		active: currentPage?.route.name === 'api', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-bolt', | 		icon: 'fas fa-bolt', | ||||||
| 		text: 'Webhook', | 		text: 'Webhook', | ||||||
| 		to: '/settings/webhook', | 		to: '/settings/webhook', | ||||||
| 		active: props.initialPage === 'webhook', | 		active: currentPage?.route.name === 'webhook', | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-ellipsis-h', | 		icon: 'fas fa-ellipsis-h', | ||||||
| 		text: i18n.ts.other, | 		text: i18n.ts.other, | ||||||
| 		to: '/settings/other', | 		to: '/settings/other', | ||||||
| 		active: props.initialPage === 'other', | 		active: currentPage?.route.name === 'other', | ||||||
| 	}], | 	}], | ||||||
| }, { | }, { | ||||||
| 	items: [{ | 	items: [{ | ||||||
|  | @ -198,77 +195,24 @@ const menuDef = computed(() => [{ | ||||||
| 	}], | 	}], | ||||||
| }]); | }]); | ||||||
| 
 | 
 | ||||||
| const pageProps = ref({}); | watch($$(narrow), () => { | ||||||
| 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'); |  | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
| 	ro.observe(el.value); | 	ro.observe(el.value); | ||||||
| 
 | 
 | ||||||
| 	narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; | 	narrow = el.value.offsetWidth < NARROW_THRESHOLD; | ||||||
| 	if (props.initialPage == null && !narrow.value) { | 
 | ||||||
| 		router.push('/settings/profile'); | 	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'); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed, onMounted, ref, watch } from 'vue'; | import { computed, onMounted, ref, watch } from 'vue'; | ||||||
| import { v4 as uuid } from 'uuid'; | 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 FormRadios from '@/components/form/radios.vue'; | ||||||
| import FormFolder from '@/components/form/folder.vue'; | import FormFolder from '@/components/form/folder.vue'; | ||||||
| import FormButton from '@/components/ui/button.vue'; | import FormButton from '@/components/ui/button.vue'; | ||||||
|  | @ -42,9 +42,97 @@ export const routes = [{ | ||||||
| 	component: page(() => import('./pages/instance-info.vue')), | 	component: page(() => import('./pages/instance-info.vue')), | ||||||
| }, { | }, { | ||||||
| 	name: 'settings', | 	name: 'settings', | ||||||
| 	path: '/settings/:initialPage(*)?', | 	path: '/settings', | ||||||
| 	component: page(() => import('./pages/settings/index.vue')), | 	component: page(() => import('./pages/settings/index.vue')), | ||||||
| 	loginRequired: true, | 	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?', | 	path: '/reset-password/:token?', | ||||||
| 	component: page(() => import('./pages/reset-password.vue')), | 	component: page(() => import('./pages/reset-password.vue')), | ||||||
|  | @ -166,8 +254,84 @@ export const routes = [{ | ||||||
| 	path: '/admin/file/:fileId', | 	path: '/admin/file/:fileId', | ||||||
| 	component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), | 	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')), | 	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', | 	path: '/my/notifications', | ||||||
| 	component: page(() => import('./pages/notifications.vue')), | 	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', () => { | mainRouter.addListener('same', () => { | ||||||
| 	window.scroll({ top: 0, behavior: 'smooth' }); | 	window.scroll({ top: 0, behavior: 'smooth' }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| window.addEventListener('popstate', (event) => { | 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; | 	const scrollPos = scrollPosStore.get(event.state?.key) ?? 0; | ||||||
| 	window.scroll({ top: scrollPos, behavior: 'instant' }); | 	window.scroll({ top: scrollPos, behavior: 'instant' }); | ||||||
| 	window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
 | 	window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue