refactor(client): Refine routing (#8846)
This commit is contained in:
		
							parent
							
								
									30a39a296d
								
							
						
					
					
						commit
						699f24f3dc
					
				
					 149 changed files with 6312 additions and 6670 deletions
				
			
		|  | @ -77,7 +77,6 @@ | |||
| 		"vite": "2.9.10", | ||||
| 		"vue": "3.2.37", | ||||
| 		"vue-prism-editor": "2.0.0-alpha.2", | ||||
| 		"vue-router": "4.0.16", | ||||
| 		"vuedraggable": "4.0.1", | ||||
| 		"websocket": "1.0.34", | ||||
| 		"ws": "8.8.0" | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import { del, get, set } from '@/scripts/idb-proxy'; | ||||
| import { defineAsyncComponent, reactive } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import { showSuspendedDialog } from './scripts/show-suspended-dialog'; | ||||
| import { i18n } from './i18n'; | ||||
| import { del, get, set } from '@/scripts/idb-proxy'; | ||||
| import { apiUrl } from '@/config'; | ||||
| import { waiting, api, popup, popupMenu, success, alert } from '@/os'; | ||||
| import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; | ||||
| import { showSuspendedDialog } from './scripts/show-suspended-dialog'; | ||||
| import { i18n } from './i18n'; | ||||
| 
 | ||||
| // TODO: 他のタブと永続化されたstateを同期
 | ||||
| 
 | ||||
|  | @ -22,13 +22,7 @@ export async function signout() { | |||
| 	waiting(); | ||||
| 	localStorage.removeItem('account'); | ||||
| 
 | ||||
| 	//#region Remove account
 | ||||
| 	const accounts = await getAccounts(); | ||||
| 	accounts.splice(accounts.findIndex(x => x.id === $i.id), 1); | ||||
| 
 | ||||
| 	if (accounts.length > 0) await set('accounts', accounts); | ||||
| 	else await del('accounts'); | ||||
| 	//#endregion
 | ||||
| 	await removeAccount($i.id); | ||||
| 
 | ||||
| 	//#region Remove service worker registration
 | ||||
| 	try { | ||||
|  | @ -55,7 +49,7 @@ export async function signout() { | |||
| 	} catch (err) {} | ||||
| 	//#endregion
 | ||||
| 
 | ||||
| 	document.cookie = `igi=; path=/`; | ||||
| 	document.cookie = 'igi=; path=/'; | ||||
| 
 | ||||
| 	if (accounts.length > 0) login(accounts[0].token); | ||||
| 	else unisonReload('/'); | ||||
|  | @ -72,14 +66,22 @@ export async function addAccount(id: Account['id'], token: Account['token']) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| export async function removeAccount(id: Account['id']) { | ||||
| 	const accounts = await getAccounts(); | ||||
| 	accounts.splice(accounts.findIndex(x => x.id === id), 1); | ||||
| 
 | ||||
| 	if (accounts.length > 0) await set('accounts', accounts); | ||||
| 	else await del('accounts'); | ||||
| } | ||||
| 
 | ||||
| function fetchAccount(token: string): Promise<Account> { | ||||
| 	return new Promise((done, fail) => { | ||||
| 		// Fetch user
 | ||||
| 		fetch(`${apiUrl}/i`, { | ||||
| 			method: 'POST', | ||||
| 			body: JSON.stringify({ | ||||
| 				i: token | ||||
| 			}) | ||||
| 				i: token, | ||||
| 			}), | ||||
| 		}) | ||||
| 		.then(res => res.json()) | ||||
| 		.then(res => { | ||||
|  | @ -216,13 +218,13 @@ export async function openAccountMenu(opts: { | |||
| 			type: 'link', | ||||
| 			icon: 'fas fa-users', | ||||
| 			text: i18n.ts.manageAccounts, | ||||
| 			to: `/settings/accounts`, | ||||
| 			to: '/settings/accounts', | ||||
| 		}]], ev.currentTarget ?? ev.target, { | ||||
| 			align: 'left' | ||||
| 			align: 'left', | ||||
| 		}); | ||||
| 	} else { | ||||
| 		popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, { | ||||
| 			align: 'left' | ||||
| 			align: 'left', | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ | |||
|   id-denylist violation when setting it. This is causing about 60+ lint issues. | ||||
|   As this is part of Chart.js's API it makes sense to disable the check here. | ||||
| */ | ||||
| import { defineProps, onMounted, ref, watch, PropType, onUnmounted } from 'vue'; | ||||
| import { onMounted, ref, watch, PropType, onUnmounted } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
|  | @ -53,7 +53,7 @@ const props = defineProps({ | |||
| 	limit: { | ||||
| 		type: Number, | ||||
| 		required: false, | ||||
| 		default: 90 | ||||
| 		default: 90, | ||||
| 	}, | ||||
| 	span: { | ||||
| 		type: String as PropType<'hour' | 'day'>, | ||||
|  | @ -62,22 +62,22 @@ const props = defineProps({ | |||
| 	detailed: { | ||||
| 		type: Boolean, | ||||
| 		required: false, | ||||
| 		default: false | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	stacked: { | ||||
| 		type: Boolean, | ||||
| 		required: false, | ||||
| 		default: false | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	bar: { | ||||
| 		type: Boolean, | ||||
| 		required: false, | ||||
| 		default: false | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	aspectRatio: { | ||||
| 		type: Number, | ||||
| 		required: false, | ||||
| 		default: null | ||||
| 		default: null, | ||||
| 	}, | ||||
| }); | ||||
| 
 | ||||
|  | @ -156,7 +156,7 @@ const getDate = (ago: number) => { | |||
| const format = (arr) => { | ||||
| 	return arr.map((v, i) => ({ | ||||
| 		x: getDate(i).getTime(), | ||||
| 		y: v | ||||
| 		y: v, | ||||
| 	})); | ||||
| }; | ||||
| 
 | ||||
|  | @ -343,7 +343,7 @@ const render = () => { | |||
| 							min: 'original', | ||||
| 							max: 'original', | ||||
| 						}, | ||||
| 					} | ||||
| 					}, | ||||
| 				} : undefined, | ||||
| 				//gradient, | ||||
| 			}, | ||||
|  | @ -367,8 +367,8 @@ const render = () => { | |||
| 					ctx.stroke(); | ||||
| 					ctx.restore(); | ||||
| 				} | ||||
| 			} | ||||
| 		}] | ||||
| 			}, | ||||
| 		}], | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
|  | @ -433,18 +433,18 @@ const fetchApRequestChart = async (): Promise<typeof chartData> => { | |||
| 			name: 'In', | ||||
| 			type: 'area', | ||||
| 			color: '#008FFB', | ||||
| 			data: format(raw.inboxReceived) | ||||
| 			data: format(raw.inboxReceived), | ||||
| 		}, { | ||||
| 			name: 'Out (succ)', | ||||
| 			type: 'area', | ||||
| 			color: '#00E396', | ||||
| 			data: format(raw.deliverSucceeded) | ||||
| 			data: format(raw.deliverSucceeded), | ||||
| 		}, { | ||||
| 			name: 'Out (fail)', | ||||
| 			type: 'area', | ||||
| 			color: '#FEB019', | ||||
| 			data: format(raw.deliverFailed) | ||||
| 		}] | ||||
| 			data: format(raw.deliverFailed), | ||||
| 		}], | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
|  | @ -456,7 +456,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { | |||
| 			type: 'line', | ||||
| 			data: format(type === 'combined' | ||||
| 				? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)) | ||||
| 				: sum(raw[type].inc, negate(raw[type].dec)) | ||||
| 				: sum(raw[type].inc, negate(raw[type].dec)), | ||||
| 			), | ||||
| 			color: '#888888', | ||||
| 		}, { | ||||
|  | @ -464,7 +464,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { | |||
| 			type: 'area', | ||||
| 			data: format(type === 'combined' | ||||
| 				? sum(raw.local.diffs.renote, raw.remote.diffs.renote) | ||||
| 				: raw[type].diffs.renote | ||||
| 				: raw[type].diffs.renote, | ||||
| 			), | ||||
| 			color: colors.green, | ||||
| 		}, { | ||||
|  | @ -472,7 +472,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { | |||
| 			type: 'area', | ||||
| 			data: format(type === 'combined' | ||||
| 				? sum(raw.local.diffs.reply, raw.remote.diffs.reply) | ||||
| 				: raw[type].diffs.reply | ||||
| 				: raw[type].diffs.reply, | ||||
| 			), | ||||
| 			color: colors.yellow, | ||||
| 		}, { | ||||
|  | @ -480,7 +480,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { | |||
| 			type: 'area', | ||||
| 			data: format(type === 'combined' | ||||
| 				? sum(raw.local.diffs.normal, raw.remote.diffs.normal) | ||||
| 				: raw[type].diffs.normal | ||||
| 				: raw[type].diffs.normal, | ||||
| 			), | ||||
| 			color: colors.blue, | ||||
| 		}, { | ||||
|  | @ -488,7 +488,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { | |||
| 			type: 'area', | ||||
| 			data: format(type === 'combined' | ||||
| 				? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile) | ||||
| 				: raw[type].diffs.withFile | ||||
| 				: raw[type].diffs.withFile, | ||||
| 			), | ||||
| 			color: colors.purple, | ||||
| 		}], | ||||
|  | @ -522,21 +522,21 @@ const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => { | |||
| 			type: 'line', | ||||
| 			data: format(total | ||||
| 				? sum(raw.local.total, raw.remote.total) | ||||
| 				: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)) | ||||
| 				: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)), | ||||
| 			), | ||||
| 		}, { | ||||
| 			name: 'Local', | ||||
| 			type: 'area', | ||||
| 			data: format(total | ||||
| 				? raw.local.total | ||||
| 				: sum(raw.local.inc, negate(raw.local.dec)) | ||||
| 				: sum(raw.local.inc, negate(raw.local.dec)), | ||||
| 			), | ||||
| 		}, { | ||||
| 			name: 'Remote', | ||||
| 			type: 'area', | ||||
| 			data: format(total | ||||
| 				? raw.remote.total | ||||
| 				: sum(raw.remote.inc, negate(raw.remote.dec)) | ||||
| 				: sum(raw.remote.inc, negate(raw.remote.dec)), | ||||
| 			), | ||||
| 		}], | ||||
| 	}; | ||||
|  | @ -607,8 +607,8 @@ const fetchDriveChart = async (): Promise<typeof chartData> => { | |||
| 					raw.local.incSize, | ||||
| 					negate(raw.local.decSize), | ||||
| 					raw.remote.incSize, | ||||
| 					negate(raw.remote.decSize) | ||||
| 				) | ||||
| 					negate(raw.remote.decSize), | ||||
| 				), | ||||
| 			), | ||||
| 		}, { | ||||
| 			name: 'Local +', | ||||
|  | @ -642,8 +642,8 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => { | |||
| 					raw.local.incCount, | ||||
| 					negate(raw.local.decCount), | ||||
| 					raw.remote.incCount, | ||||
| 					negate(raw.remote.decCount) | ||||
| 				) | ||||
| 					negate(raw.remote.decCount), | ||||
| 				), | ||||
| 			), | ||||
| 		}, { | ||||
| 			name: 'Local +', | ||||
|  | @ -672,18 +672,18 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => { | |||
| 			name: 'In', | ||||
| 			type: 'area', | ||||
| 			color: '#008FFB', | ||||
| 			data: format(raw.requests.received) | ||||
| 			data: format(raw.requests.received), | ||||
| 		}, { | ||||
| 			name: 'Out (succ)', | ||||
| 			type: 'area', | ||||
| 			color: '#00E396', | ||||
| 			data: format(raw.requests.succeeded) | ||||
| 			data: format(raw.requests.succeeded), | ||||
| 		}, { | ||||
| 			name: 'Out (fail)', | ||||
| 			type: 'area', | ||||
| 			color: '#FEB019', | ||||
| 			data: format(raw.requests.failed) | ||||
| 		}] | ||||
| 			data: format(raw.requests.failed), | ||||
| 		}], | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
|  | @ -696,9 +696,9 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData | |||
| 			color: '#008FFB', | ||||
| 			data: format(total | ||||
| 				? raw.users.total | ||||
| 				: sum(raw.users.inc, negate(raw.users.dec)) | ||||
| 			) | ||||
| 		}] | ||||
| 				: sum(raw.users.inc, negate(raw.users.dec)), | ||||
| 			), | ||||
| 		}], | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
|  | @ -711,9 +711,9 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData | |||
| 			color: '#008FFB', | ||||
| 			data: format(total | ||||
| 				? raw.notes.total | ||||
| 				: sum(raw.notes.inc, negate(raw.notes.dec)) | ||||
| 			) | ||||
| 		}] | ||||
| 				: sum(raw.notes.inc, negate(raw.notes.dec)), | ||||
| 			), | ||||
| 		}], | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
|  | @ -726,17 +726,17 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> = | |||
| 			color: '#008FFB', | ||||
| 			data: format(total | ||||
| 				? raw.following.total | ||||
| 				: sum(raw.following.inc, negate(raw.following.dec)) | ||||
| 			) | ||||
| 				: sum(raw.following.inc, negate(raw.following.dec)), | ||||
| 			), | ||||
| 		}, { | ||||
| 			name: 'Followers', | ||||
| 			type: 'area', | ||||
| 			color: '#00E396', | ||||
| 			data: format(total | ||||
| 				? raw.followers.total | ||||
| 				: sum(raw.followers.inc, negate(raw.followers.dec)) | ||||
| 			) | ||||
| 		}] | ||||
| 				: sum(raw.followers.inc, negate(raw.followers.dec)), | ||||
| 			), | ||||
| 		}], | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
|  | @ -750,9 +750,9 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char | |||
| 			color: '#008FFB', | ||||
| 			data: format(total | ||||
| 				? raw.drive.totalUsage | ||||
| 				: sum(raw.drive.incUsage, negate(raw.drive.decUsage)) | ||||
| 			) | ||||
| 		}] | ||||
| 				: sum(raw.drive.incUsage, negate(raw.drive.decUsage)), | ||||
| 			), | ||||
| 		}], | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
|  | @ -765,9 +765,9 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char | |||
| 			color: '#008FFB', | ||||
| 			data: format(total | ||||
| 				? raw.drive.totalFiles | ||||
| 				: sum(raw.drive.incFiles, negate(raw.drive.decFiles)) | ||||
| 			) | ||||
| 		}] | ||||
| 				: sum(raw.drive.incFiles, negate(raw.drive.decFiles)), | ||||
| 			), | ||||
| 		}], | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -57,7 +57,7 @@ const isThumbnailAvailable = computed(() => { | |||
| .zdjebgpv { | ||||
| 	position: relative; | ||||
| 	display: flex; | ||||
| 	background: #e1e1e1; | ||||
| 	background: var(--panel); | ||||
| 	border-radius: 8px; | ||||
| 	overflow: clip; | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,13 +9,13 @@ | |||
| 			<i v-else class="fas fa-angle-down icon"></i> | ||||
| 		</span> | ||||
| 	</div> | ||||
| 	<keep-alive> | ||||
| 	<KeepAlive> | ||||
| 		<div v-if="openedAtLeastOnce" v-show="opened" class="body"> | ||||
| 			<MkSpacer :margin-min="14" :margin-max="22"> | ||||
| 				<slot></slot> | ||||
| 			</MkSpacer> | ||||
| 		</div> | ||||
| 	</keep-alive> | ||||
| 	</KeepAlive> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,13 +5,13 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { inject } from 'vue'; | ||||
| import * as os from '@/os'; | ||||
| import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||
| import { router } from '@/router'; | ||||
| import { url } from '@/config'; | ||||
| import { popout as popout_ } from '@/scripts/popout'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { MisskeyNavigator } from '@/scripts/navigate'; | ||||
| import { useRouter } from '@/router'; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	to: string; | ||||
|  | @ -22,15 +22,16 @@ const props = withDefaults(defineProps<{ | |||
| 	behavior: null, | ||||
| }); | ||||
| 
 | ||||
| const mkNav = new MisskeyNavigator(); | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| const active = $computed(() => { | ||||
| 	if (props.activeClass == null) return false; | ||||
| 	const resolved = router.resolve(props.to); | ||||
| 	if (resolved.path === router.currentRoute.value.path) return true; | ||||
| 	if (resolved.name == null) return false; | ||||
| 	if (resolved == null) return false; | ||||
| 	if (resolved.route.path === router.currentRoute.value.path) return true; | ||||
| 	if (resolved.route.name == null) return false; | ||||
| 	if (router.currentRoute.value.name == null) return false; | ||||
| 	return resolved.name === router.currentRoute.value.name; | ||||
| 	return resolved.route.name === router.currentRoute.value.name; | ||||
| }); | ||||
| 
 | ||||
| function onContextmenu(ev) { | ||||
|  | @ -44,31 +45,25 @@ function onContextmenu(ev) { | |||
| 		text: i18n.ts.openInWindow, | ||||
| 		action: () => { | ||||
| 			os.pageWindow(props.to); | ||||
| 		} | ||||
| 	}, mkNav.sideViewHook ? { | ||||
| 		icon: 'fas fa-columns', | ||||
| 		text: i18n.ts.openInSideView, | ||||
| 		action: () => { | ||||
| 			if (mkNav.sideViewHook) mkNav.sideViewHook(props.to); | ||||
| 		} | ||||
| 	} : undefined, { | ||||
| 		}, | ||||
| 	}, { | ||||
| 		icon: 'fas fa-expand-alt', | ||||
| 		text: i18n.ts.showInPage, | ||||
| 		action: () => { | ||||
| 			router.push(props.to); | ||||
| 		} | ||||
| 		}, | ||||
| 	}, null, { | ||||
| 		icon: 'fas fa-external-link-alt', | ||||
| 		text: i18n.ts.openInNewTab, | ||||
| 		action: () => { | ||||
| 			window.open(props.to, '_blank'); | ||||
| 		} | ||||
| 		}, | ||||
| 	}, { | ||||
| 		icon: 'fas fa-link', | ||||
| 		text: i18n.ts.copyLink, | ||||
| 		action: () => { | ||||
| 			copyToClipboard(`${url}${props.to}`); | ||||
| 		} | ||||
| 		}, | ||||
| 	}], ev); | ||||
| } | ||||
| 
 | ||||
|  | @ -98,6 +93,6 @@ function nav() { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	mkNav.push(props.to); | ||||
| 	router.push(props.to); | ||||
| } | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,361 +0,0 @@ | |||
| <template> | ||||
| <div ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick"> | ||||
| 	<template v-if="info"> | ||||
| 		<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup"> | ||||
| 			<MkAvatar v-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/> | ||||
| 			<i v-else-if="info.icon" class="icon" :class="info.icon"></i> | ||||
| 
 | ||||
| 			<div class="title"> | ||||
| 				<MkUserName v-if="info.userName" :user="info.userName" :nowrap="true" class="title"/> | ||||
| 				<div v-else-if="info.title" class="title">{{ info.title }}</div> | ||||
| 				<div v-if="!narrow && info.subtitle" class="subtitle"> | ||||
| 					{{ info.subtitle }} | ||||
| 				</div> | ||||
| 				<div v-if="narrow && hasTabs" class="subtitle activeTab"> | ||||
| 					{{ info.tabs.find(tab => tab.active)?.title }} | ||||
| 					<i class="chevron fas fa-chevron-down"></i> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div v-if="!narrow || hideTitle" class="tabs"> | ||||
| 			<button v-for="tab in info.tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick"> | ||||
| 				<i v-if="tab.icon" class="icon" :class="tab.icon"></i> | ||||
| 				<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> | ||||
| 			</button> | ||||
| 		</div> | ||||
| 	</template> | ||||
| 	<div class="buttons right"> | ||||
| 		<template v-if="info && info.actions && !narrow"> | ||||
| 			<template v-for="action in info.actions"> | ||||
| 				<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton> | ||||
| 				<button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> | ||||
| 			</template> | ||||
| 		</template> | ||||
| 		<button v-if="shouldShowMenu" v-tooltip="$ts.menu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag"><i class="fas fa-ellipsis-h"></i></button> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, inject } from 'vue'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import { popupMenu } from '@/os'; | ||||
| import { url } from '@/config'; | ||||
| import { scrollToTop } from '@/scripts/scroll'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { globalEvents } from '@/events'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		info: { | ||||
| 			type: Object as PropType<{ | ||||
| 				actions?: {}[]; | ||||
| 				tabs?: {}[]; | ||||
| 			}>, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		menu: { | ||||
| 			required: false | ||||
| 		}, | ||||
| 		thin: { | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	setup(props) { | ||||
| 		const el = ref<HTMLElement>(null); | ||||
| 		const bg = ref(null); | ||||
| 		const narrow = ref(false); | ||||
| 		const height = ref(0); | ||||
| 		const hasTabs = computed(() => { | ||||
| 			return props.info.tabs && props.info.tabs.length > 0; | ||||
| 		}); | ||||
| 		const shouldShowMenu = computed(() => { | ||||
| 			if (props.info == null) return false; | ||||
| 			if (props.info.actions != null && narrow.value) return true; | ||||
| 			if (props.info.menu != null) return true; | ||||
| 			if (props.info.share != null) return true; | ||||
| 			if (props.menu != null) return true; | ||||
| 			return false; | ||||
| 		}); | ||||
| 
 | ||||
| 		const share = () => { | ||||
| 			navigator.share({ | ||||
| 				url: url + props.info.path, | ||||
| 				...props.info.share, | ||||
| 			}); | ||||
| 		}; | ||||
| 
 | ||||
| 		const showMenu = (ev: MouseEvent) => { | ||||
| 			let menu = props.info.menu ? props.info.menu() : []; | ||||
| 			if (narrow.value && props.info.actions) { | ||||
| 				menu = [...props.info.actions.map(x => ({ | ||||
| 					text: x.text, | ||||
| 					icon: x.icon, | ||||
| 					action: x.handler | ||||
| 				})), menu.length > 0 ? null : undefined, ...menu]; | ||||
| 			} | ||||
| 			if (props.info.share) { | ||||
| 				if (menu.length > 0) menu.push(null); | ||||
| 				menu.push({ | ||||
| 					text: i18n.ts.share, | ||||
| 					icon: 'fas fa-share-alt', | ||||
| 					action: share | ||||
| 				}); | ||||
| 			} | ||||
| 			if (props.menu) { | ||||
| 				if (menu.length > 0) menu.push(null); | ||||
| 				menu = menu.concat(props.menu); | ||||
| 			} | ||||
| 			popupMenu(menu, ev.currentTarget ?? ev.target); | ||||
| 		}; | ||||
| 
 | ||||
| 		const showTabsPopup = (ev: MouseEvent) => { | ||||
| 			if (!hasTabs.value) return; | ||||
| 			if (!narrow.value) return; | ||||
| 			ev.preventDefault(); | ||||
| 			ev.stopPropagation(); | ||||
| 			const menu = props.info.tabs.map(tab => ({ | ||||
| 				text: tab.title, | ||||
| 				icon: tab.icon, | ||||
| 				action: tab.onClick, | ||||
| 			})); | ||||
| 			popupMenu(menu, ev.currentTarget ?? ev.target); | ||||
| 		}; | ||||
| 
 | ||||
| 		const preventDrag = (ev: TouchEvent) => { | ||||
| 			ev.stopPropagation(); | ||||
| 		}; | ||||
| 
 | ||||
| 		const onClick = () => { | ||||
| 			scrollToTop(el.value, { behavior: 'smooth' }); | ||||
| 		}; | ||||
| 
 | ||||
| 		const calcBg = () => { | ||||
| 			const rawBg = props.info?.bg || 'var(--bg)'; | ||||
| 			const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); | ||||
| 			tinyBg.setAlpha(0.85); | ||||
| 			bg.value = tinyBg.toRgbString(); | ||||
| 		}; | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			calcBg(); | ||||
| 			globalEvents.on('themeChanged', calcBg); | ||||
| 			onUnmounted(() => { | ||||
| 				globalEvents.off('themeChanged', calcBg); | ||||
| 			}); | ||||
| 		 | ||||
| 			if (el.value.parentElement) { | ||||
| 				narrow.value = el.value.parentElement.offsetWidth < 500; | ||||
| 				const ro = new ResizeObserver((entries, observer) => { | ||||
| 					if (el.value) { | ||||
| 						narrow.value = el.value.parentElement.offsetWidth < 500; | ||||
| 					} | ||||
| 				}); | ||||
| 				ro.observe(el.value.parentElement); | ||||
| 				onUnmounted(() => { | ||||
| 					ro.disconnect(); | ||||
| 				}); | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			el, | ||||
| 			bg, | ||||
| 			narrow, | ||||
| 			height, | ||||
| 			hasTabs, | ||||
| 			shouldShowMenu, | ||||
| 			share, | ||||
| 			showMenu, | ||||
| 			showTabsPopup, | ||||
| 			preventDrag, | ||||
| 			onClick, | ||||
| 			hideTitle: inject('shouldOmitHeaderTitle', false), | ||||
| 			thin_: props.thin || inject('shouldHeaderThin', false) | ||||
| 		}; | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fdidabkb { | ||||
| 	--height: 60px; | ||||
| 	display: flex; | ||||
| 	position: sticky; | ||||
| 	top: var(--stickyTop, 0); | ||||
| 	z-index: 1000; | ||||
| 	width: 100%; | ||||
| 	-webkit-backdrop-filter: var(--blur, blur(15px)); | ||||
| 	backdrop-filter: var(--blur, blur(15px)); | ||||
| 	border-bottom: solid 0.5px var(--divider); | ||||
| 
 | ||||
| 	&.thin { | ||||
| 		--height: 50px; | ||||
| 
 | ||||
| 		> .buttons { | ||||
| 			> .button { | ||||
| 				font-size: 0.9em; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.slim { | ||||
| 		text-align: center; | ||||
| 
 | ||||
| 		> .titleContainer { | ||||
| 			flex: 1; | ||||
| 			margin: 0 auto; | ||||
| 			margin-left: var(--height); | ||||
| 
 | ||||
| 			> *:first-child { | ||||
| 				margin-left: auto; | ||||
| 			} | ||||
| 
 | ||||
| 			> *:last-child { | ||||
| 				margin-right: auto; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .buttons { | ||||
| 		--margin: 8px; | ||||
| 		display: flex; | ||||
|     align-items: center; | ||||
| 		height: var(--height); | ||||
| 		margin: 0 var(--margin); | ||||
| 
 | ||||
| 		&.right { | ||||
| 			margin-left: auto; | ||||
| 		} | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			width: var(--height); | ||||
| 		} | ||||
| 
 | ||||
| 		> .button { | ||||
| 			display: flex; | ||||
| 			align-items: center; | ||||
| 			justify-content: center; | ||||
| 			height: calc(var(--height) - (var(--margin) * 2)); | ||||
| 			width: calc(var(--height) - (var(--margin) * 2)); | ||||
| 			box-sizing: border-box; | ||||
| 			position: relative; | ||||
| 			border-radius: 5px; | ||||
| 
 | ||||
| 			&:hover { | ||||
| 				background: rgba(0, 0, 0, 0.05); | ||||
| 			} | ||||
| 
 | ||||
| 			&.highlighted { | ||||
| 				color: var(--accent); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .fullButton { | ||||
| 			& + .fullButton { | ||||
| 				margin-left: 12px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .titleContainer { | ||||
| 		display: flex; | ||||
| 		align-items: center; | ||||
| 		max-width: 400px; | ||||
| 		overflow: auto; | ||||
| 		white-space: nowrap; | ||||
| 		text-align: left; | ||||
| 		font-weight: bold; | ||||
| 		flex-shrink: 0; | ||||
| 		margin-left: 24px; | ||||
| 
 | ||||
| 		> .avatar { | ||||
| 			$size: 32px; | ||||
| 			display: inline-block; | ||||
| 			width: $size; | ||||
| 			height: $size; | ||||
| 			vertical-align: bottom; | ||||
| 			margin: 0 8px; | ||||
| 			pointer-events: none; | ||||
| 		} | ||||
| 
 | ||||
| 		> .icon { | ||||
| 			margin-right: 8px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .title { | ||||
| 			min-width: 0; | ||||
| 			overflow: hidden; | ||||
| 			text-overflow: ellipsis; | ||||
| 			white-space: nowrap; | ||||
| 			line-height: 1.1; | ||||
| 
 | ||||
| 			> .subtitle { | ||||
| 				opacity: 0.6; | ||||
| 				font-size: 0.8em; | ||||
| 				font-weight: normal; | ||||
| 				white-space: nowrap; | ||||
| 				overflow: hidden; | ||||
| 				text-overflow: ellipsis; | ||||
| 
 | ||||
| 				&.activeTab { | ||||
| 					text-align: center; | ||||
| 
 | ||||
| 					> .chevron { | ||||
| 						display: inline-block; | ||||
| 						margin-left: 6px; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .tabs { | ||||
| 		margin-left: 16px; | ||||
| 		font-size: 0.8em; | ||||
| 		overflow: auto; | ||||
| 		white-space: nowrap; | ||||
| 
 | ||||
| 		> .tab { | ||||
| 			display: inline-block; | ||||
| 			position: relative; | ||||
| 			padding: 0 10px; | ||||
| 			height: 100%; | ||||
| 			font-weight: normal; | ||||
| 			opacity: 0.7; | ||||
| 
 | ||||
| 			&:hover { | ||||
| 				opacity: 1; | ||||
| 			} | ||||
| 
 | ||||
| 			&.active { | ||||
| 				opacity: 1; | ||||
| 
 | ||||
| 				&:after { | ||||
| 					content: ""; | ||||
| 					display: block; | ||||
| 					position: absolute; | ||||
| 					bottom: 0; | ||||
| 					left: 0; | ||||
| 					right: 0; | ||||
| 					margin: 0 auto; | ||||
| 					width: 100%; | ||||
| 					height: 3px; | ||||
| 					background: var(--accent); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			> .icon + .title { | ||||
| 				margin-left: 8px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										300
									
								
								packages/client/src/components/global/page-header.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								packages/client/src/components/global/page-header.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,300 @@ | |||
| <template> | ||||
| <div v-if="show" ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick"> | ||||
| 	<template v-if="metadata"> | ||||
| 		<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup"> | ||||
| 			<MkAvatar v-if="metadata.avatar" class="avatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/> | ||||
| 			<i v-else-if="metadata.icon" class="icon" :class="metadata.icon"></i> | ||||
| 
 | ||||
| 			<div class="title"> | ||||
| 				<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true" class="title"/> | ||||
| 				<div v-else-if="metadata.title" class="title">{{ metadata.title }}</div> | ||||
| 				<div v-if="!narrow && metadata.subtitle" class="subtitle"> | ||||
| 					{{ metadata.subtitle }} | ||||
| 				</div> | ||||
| 				<div v-if="narrow && hasTabs" class="subtitle activeTab"> | ||||
| 					{{ tabs.find(tab => tab.active)?.title }} | ||||
| 					<i class="chevron fas fa-chevron-down"></i> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div v-if="!narrow || hideTitle" class="tabs"> | ||||
| 			<button v-for="tab in tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick"> | ||||
| 				<i v-if="tab.icon" class="icon" :class="tab.icon"></i> | ||||
| 				<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> | ||||
| 			</button> | ||||
| 		</div> | ||||
| 	</template> | ||||
| 	<div class="buttons right"> | ||||
| 		<template v-for="action in actions"> | ||||
| 			<button v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> | ||||
| 		</template> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed, onMounted, onUnmounted, ref, inject } from 'vue'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import { popupMenu } from '@/os'; | ||||
| import { scrollToTop } from '@/scripts/scroll'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { globalEvents } from '@/events'; | ||||
| import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	tabs?: { | ||||
| 		title: string; | ||||
| 		active: boolean; | ||||
| 		icon?: string; | ||||
| 		iconOnly?: boolean; | ||||
| 		onClick: () => void; | ||||
| 	}[]; | ||||
| 	actions?: { | ||||
| 		text: string; | ||||
| 		icon: string; | ||||
| 		handler: (ev: MouseEvent) => void; | ||||
| 	}[]; | ||||
| 	thin?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| const metadata = injectPageMetadata(); | ||||
| 
 | ||||
| const hideTitle = inject('shouldOmitHeaderTitle', false); | ||||
| const thin_ = props.thin || inject('shouldHeaderThin', false); | ||||
| 
 | ||||
| const el = $ref<HTMLElement | null>(null); | ||||
| const bg = ref(null); | ||||
| let narrow = $ref(false); | ||||
| const height = ref(0); | ||||
| const hasTabs = $computed(() => props.tabs && props.tabs.length > 0); | ||||
| const hasActions = $computed(() => props.actions && props.actions.length > 0); | ||||
| const show = $computed(() => { | ||||
| 	return !hideTitle || hasTabs || hasActions; | ||||
| }); | ||||
| 
 | ||||
| const showTabsPopup = (ev: MouseEvent) => { | ||||
| 	if (!hasTabs) return; | ||||
| 	if (!narrow) return; | ||||
| 	ev.preventDefault(); | ||||
| 	ev.stopPropagation(); | ||||
| 	const menu = props.tabs.map(tab => ({ | ||||
| 		text: tab.title, | ||||
| 		icon: tab.icon, | ||||
| 		action: tab.onClick, | ||||
| 	})); | ||||
| 	popupMenu(menu, ev.currentTarget ?? ev.target); | ||||
| }; | ||||
| 
 | ||||
| const preventDrag = (ev: TouchEvent) => { | ||||
| 	ev.stopPropagation(); | ||||
| }; | ||||
| 
 | ||||
| const onClick = () => { | ||||
| 	scrollToTop(el, { behavior: 'smooth' }); | ||||
| }; | ||||
| 
 | ||||
| const calcBg = () => { | ||||
| 	const rawBg = metadata?.bg || 'var(--bg)'; | ||||
| 	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); | ||||
| 	tinyBg.setAlpha(0.85); | ||||
| 	bg.value = tinyBg.toRgbString(); | ||||
| }; | ||||
| 
 | ||||
| let ro: ResizeObserver | null; | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	calcBg(); | ||||
| 	globalEvents.on('themeChanged', calcBg); | ||||
| 
 | ||||
| 	if (el && el.parentElement) { | ||||
| 		narrow = el.parentElement.offsetWidth < 500; | ||||
| 		ro = new ResizeObserver((entries, observer) => { | ||||
| 			if (el.parentElement) { | ||||
| 				narrow = el.parentElement.offsetWidth < 500; | ||||
| 			} | ||||
| 		}); | ||||
| 		ro.observe(el.parentElement); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| onUnmounted(() => { | ||||
| 	globalEvents.off('themeChanged', calcBg); | ||||
| 	if (ro) ro.disconnect(); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fdidabkb { | ||||
| 	--height: 60px; | ||||
| 	display: flex; | ||||
| 	position: sticky; | ||||
| 	top: var(--stickyTop, 0); | ||||
| 	z-index: 1000; | ||||
| 	width: 100%; | ||||
| 	-webkit-backdrop-filter: var(--blur, blur(15px)); | ||||
| 	backdrop-filter: var(--blur, blur(15px)); | ||||
| 	border-bottom: solid 0.5px var(--divider); | ||||
| 
 | ||||
| 	&.thin { | ||||
| 		--height: 50px; | ||||
| 
 | ||||
| 		> .buttons { | ||||
| 			> .button { | ||||
| 				font-size: 0.9em; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.slim { | ||||
| 		text-align: center; | ||||
| 
 | ||||
| 		> .titleContainer { | ||||
| 			flex: 1; | ||||
| 			margin: 0 auto; | ||||
| 			margin-left: var(--height); | ||||
| 
 | ||||
| 			> *:first-child { | ||||
| 				margin-left: auto; | ||||
| 			} | ||||
| 
 | ||||
| 			> *:last-child { | ||||
| 				margin-right: auto; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .buttons { | ||||
| 		--margin: 8px; | ||||
| 		display: flex; | ||||
|     align-items: center; | ||||
| 		height: var(--height); | ||||
| 		margin: 0 var(--margin); | ||||
| 
 | ||||
| 		&.right { | ||||
| 			margin-left: auto; | ||||
| 		} | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			width: var(--height); | ||||
| 		} | ||||
| 
 | ||||
| 		> .button { | ||||
| 			display: flex; | ||||
| 			align-items: center; | ||||
| 			justify-content: center; | ||||
| 			height: calc(var(--height) - (var(--margin) * 2)); | ||||
| 			width: calc(var(--height) - (var(--margin) * 2)); | ||||
| 			box-sizing: border-box; | ||||
| 			position: relative; | ||||
| 			border-radius: 5px; | ||||
| 
 | ||||
| 			&:hover { | ||||
| 				background: rgba(0, 0, 0, 0.05); | ||||
| 			} | ||||
| 
 | ||||
| 			&.highlighted { | ||||
| 				color: var(--accent); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .fullButton { | ||||
| 			& + .fullButton { | ||||
| 				margin-left: 12px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .titleContainer { | ||||
| 		display: flex; | ||||
| 		align-items: center; | ||||
| 		max-width: 400px; | ||||
| 		overflow: auto; | ||||
| 		white-space: nowrap; | ||||
| 		text-align: left; | ||||
| 		font-weight: bold; | ||||
| 		flex-shrink: 0; | ||||
| 		margin-left: 24px; | ||||
| 
 | ||||
| 		> .avatar { | ||||
| 			$size: 32px; | ||||
| 			display: inline-block; | ||||
| 			width: $size; | ||||
| 			height: $size; | ||||
| 			vertical-align: bottom; | ||||
| 			margin: 0 8px; | ||||
| 			pointer-events: none; | ||||
| 		} | ||||
| 
 | ||||
| 		> .icon { | ||||
| 			margin-right: 8px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .title { | ||||
| 			min-width: 0; | ||||
| 			overflow: hidden; | ||||
| 			text-overflow: ellipsis; | ||||
| 			white-space: nowrap; | ||||
| 			line-height: 1.1; | ||||
| 
 | ||||
| 			> .subtitle { | ||||
| 				opacity: 0.6; | ||||
| 				font-size: 0.8em; | ||||
| 				font-weight: normal; | ||||
| 				white-space: nowrap; | ||||
| 				overflow: hidden; | ||||
| 				text-overflow: ellipsis; | ||||
| 
 | ||||
| 				&.activeTab { | ||||
| 					text-align: center; | ||||
| 
 | ||||
| 					> .chevron { | ||||
| 						display: inline-block; | ||||
| 						margin-left: 6px; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .tabs { | ||||
| 		margin-left: 16px; | ||||
| 		font-size: 0.8em; | ||||
| 		overflow: auto; | ||||
| 		white-space: nowrap; | ||||
| 
 | ||||
| 		> .tab { | ||||
| 			display: inline-block; | ||||
| 			position: relative; | ||||
| 			padding: 0 10px; | ||||
| 			height: 100%; | ||||
| 			font-weight: normal; | ||||
| 			opacity: 0.7; | ||||
| 
 | ||||
| 			&:hover { | ||||
| 				opacity: 1; | ||||
| 			} | ||||
| 
 | ||||
| 			&.active { | ||||
| 				opacity: 1; | ||||
| 
 | ||||
| 				&:after { | ||||
| 					content: ""; | ||||
| 					display: block; | ||||
| 					position: absolute; | ||||
| 					bottom: 0; | ||||
| 					left: 0; | ||||
| 					right: 0; | ||||
| 					margin: 0 auto; | ||||
| 					width: 100%; | ||||
| 					height: 3px; | ||||
| 					background: var(--accent); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			> .icon + .title { | ||||
| 				margin-left: 8px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										39
									
								
								packages/client/src/components/global/router-view.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								packages/client/src/components/global/router-view.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| <template> | ||||
| <KeepAlive max="5"> | ||||
| 	<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/> | ||||
| </KeepAlive> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { inject, nextTick, onMounted, onUnmounted, watch } from 'vue'; | ||||
| import { Router } from '@/nirax'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	router?: Router; | ||||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| }>(); | ||||
| 
 | ||||
| const router = props.router ?? inject('router'); | ||||
| 
 | ||||
| if (router == null) { | ||||
| 	throw new Error('no router provided'); | ||||
| } | ||||
| 
 | ||||
| let currentPageComponent = $ref(router.getCurrentComponent()); | ||||
| let currentPageProps = $ref(router.getCurrentProps()); | ||||
| let key = $ref(router.getCurrentKey()); | ||||
| 
 | ||||
| function onChange({ route, props: newProps, key: newKey }) { | ||||
| 	currentPageComponent = route.component; | ||||
| 	currentPageProps = newProps; | ||||
| 	key = newKey; | ||||
| } | ||||
| 
 | ||||
| router.addListener('change', onChange); | ||||
| 
 | ||||
| onUnmounted(() => { | ||||
| 	router.removeListener('change', onChange); | ||||
| }); | ||||
| </script> | ||||
|  | @ -10,15 +10,17 @@ import MkEllipsis from './global/ellipsis.vue'; | |||
| import MkTime from './global/time.vue'; | ||||
| import MkUrl from './global/url.vue'; | ||||
| import I18n from './global/i18n'; | ||||
| import RouterView from './global/router-view.vue'; | ||||
| import MkLoading from './global/loading.vue'; | ||||
| import MkError from './global/error.vue'; | ||||
| import MkAd from './global/ad.vue'; | ||||
| import MkHeader from './global/header.vue'; | ||||
| import MkPageHeader from './global/page-header.vue'; | ||||
| import MkSpacer from './global/spacer.vue'; | ||||
| import MkStickyContainer from './global/sticky-container.vue'; | ||||
| 
 | ||||
| export default function(app: App) { | ||||
| 	app.component('I18n', I18n); | ||||
| 	app.component('RouterView', RouterView); | ||||
| 	app.component('Mfm', Mfm); | ||||
| 	app.component('MkA', MkA); | ||||
| 	app.component('MkAcct', MkAcct); | ||||
|  | @ -31,7 +33,7 @@ export default function(app: App) { | |||
| 	app.component('MkLoading', MkLoading); | ||||
| 	app.component('MkError', MkError); | ||||
| 	app.component('MkAd', MkAd); | ||||
| 	app.component('MkHeader', MkHeader); | ||||
| 	app.component('MkPageHeader', MkPageHeader); | ||||
| 	app.component('MkSpacer', MkSpacer); | ||||
| 	app.component('MkStickyContainer', MkStickyContainer); | ||||
| } | ||||
|  | @ -39,6 +41,7 @@ export default function(app: App) { | |||
| declare module '@vue/runtime-core' { | ||||
| 	export interface GlobalComponents { | ||||
| 		I18n: typeof I18n; | ||||
| 		RouterView: typeof RouterView; | ||||
| 		Mfm: typeof Mfm; | ||||
| 		MkA: typeof MkA; | ||||
| 		MkAcct: typeof MkAcct; | ||||
|  | @ -51,7 +54,7 @@ declare module '@vue/runtime-core' { | |||
| 		MkLoading: typeof MkLoading; | ||||
| 		MkError: typeof MkError; | ||||
| 		MkAd: typeof MkAd; | ||||
| 		MkHeader: typeof MkHeader; | ||||
| 		MkPageHeader: typeof MkPageHeader; | ||||
| 		MkSpacer: typeof MkSpacer; | ||||
| 		MkStickyContainer: typeof MkStickyContainer; | ||||
| 	} | ||||
|  |  | |||
|  | @ -1,163 +1,118 @@ | |||
| <template> | ||||
| <MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')"> | ||||
| 	<div class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }"> | ||||
| 	<div ref="rootEl" class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }"> | ||||
| 		<div class="header" @contextmenu="onContextmenu"> | ||||
| 			<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button> | ||||
| 			<span v-else style="display: inline-block; width: 20px"></span> | ||||
| 			<span v-if="pageInfo" class="title"> | ||||
| 				<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i> | ||||
| 				<span>{{ pageInfo.title }}</span> | ||||
| 			<span v-if="pageMetadata?.value" class="title"> | ||||
| 				<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i> | ||||
| 				<span>{{ pageMetadata?.value.title }}</span> | ||||
| 			</span> | ||||
| 			<button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button> | ||||
| 		</div> | ||||
| 		<div class="body"> | ||||
| 			<MkStickyContainer> | ||||
| 				<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template> | ||||
| 				<keep-alive> | ||||
| 					<component :is="component" v-bind="props" :ref="changePage"/> | ||||
| 				</keep-alive> | ||||
| 				<template #header><MkPageHeader v-if="pageMetadata?.value && !pageMetadata?.value.hideHeader" :info="pageMetadata?.value"/></template> | ||||
| 				<RouterView :router="router"/> | ||||
| 			</MkStickyContainer> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkModal> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { ComputedRef, provide } from 'vue'; | ||||
| import MkModal from '@/components/ui/modal.vue'; | ||||
| import { popout } from '@/scripts/popout'; | ||||
| import { popout as _popout } from '@/scripts/popout'; | ||||
| import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||
| import { resolve } from '@/router'; | ||||
| import { url } from '@/config'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import * as os from '@/os'; | ||||
| import { mainRouter, routes } from '@/router'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; | ||||
| import { Router } from '@/nirax'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkModal, | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	initialPath: string; | ||||
| }>(); | ||||
| 
 | ||||
| 	inject: { | ||||
| 		sideViewHook: { | ||||
| 			default: null, | ||||
| 		}, | ||||
| 	}, | ||||
| defineEmits<{ | ||||
| 	(ev: 'closed'): void; | ||||
| 	(ev: 'click'): void; | ||||
| }>(); | ||||
| 
 | ||||
| 	provide() { | ||||
| 		return { | ||||
| 			navHook: (path) => { | ||||
| 				this.navigate(path); | ||||
| 			}, | ||||
| 			shouldHeaderThin: true, | ||||
| 		}; | ||||
| 	}, | ||||
| const router = new Router(routes, props.initialPath); | ||||
| 
 | ||||
| 	props: { | ||||
| 		initialPath: { | ||||
| 			type: String, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		initialComponent: { | ||||
| 			type: Object, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		initialProps: { | ||||
| 			type: Object, | ||||
| 			required: false, | ||||
| 			default: () => {}, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['closed'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			width: 860, | ||||
| 			height: 660, | ||||
| 			pageInfo: null, | ||||
| 			path: this.initialPath, | ||||
| 			component: this.initialComponent, | ||||
| 			props: this.initialProps, | ||||
| 			history: [], | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		url(): string { | ||||
| 			return url + this.path; | ||||
| 		}, | ||||
| 
 | ||||
| 		contextmenu() { | ||||
| 			return [{ | ||||
| 				type: 'label', | ||||
| 				text: this.path, | ||||
| 			}, { | ||||
| 				icon: 'fas fa-expand-alt', | ||||
| 				text: this.$ts.showInPage, | ||||
| 				action: this.expand, | ||||
| 			}, this.sideViewHook ? { | ||||
| 				icon: 'fas fa-columns', | ||||
| 				text: this.$ts.openInSideView, | ||||
| 				action: () => { | ||||
| 					this.sideViewHook(this.path); | ||||
| 					this.$refs.window.close(); | ||||
| 				}, | ||||
| 			} : undefined, { | ||||
| 				icon: 'fas fa-external-link-alt', | ||||
| 				text: this.$ts.popout, | ||||
| 				action: this.popout, | ||||
| 			}, null, { | ||||
| 				icon: 'fas fa-external-link-alt', | ||||
| 				text: this.$ts.openInNewTab, | ||||
| 				action: () => { | ||||
| 					window.open(this.url, '_blank'); | ||||
| 					this.$refs.window.close(); | ||||
| 				}, | ||||
| 			}, { | ||||
| 				icon: 'fas fa-link', | ||||
| 				text: this.$ts.copyLink, | ||||
| 				action: () => { | ||||
| 					copyToClipboard(this.url); | ||||
| 				}, | ||||
| 			}]; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		changePage(page) { | ||||
| 			if (page == null) return; | ||||
| 			if (page[symbols.PAGE_INFO]) { | ||||
| 				this.pageInfo = page[symbols.PAGE_INFO]; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		navigate(path, record = true) { | ||||
| 			if (record) this.history.push(this.path); | ||||
| 			this.path = path; | ||||
| 			const { component, props } = resolve(path); | ||||
| 			this.component = component; | ||||
| 			this.props = props; | ||||
| 		}, | ||||
| 
 | ||||
| 		back() { | ||||
| 			this.navigate(this.history.pop(), false); | ||||
| 		}, | ||||
| 
 | ||||
| 		expand() { | ||||
| 			this.$router.push(this.path); | ||||
| 			this.$refs.window.close(); | ||||
| 		}, | ||||
| 
 | ||||
| 		popout() { | ||||
| 			popout(this.path, this.$el); | ||||
| 			this.$refs.window.close(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onContextmenu(ev: MouseEvent) { | ||||
| 			os.contextMenu(this.contextmenu, ev); | ||||
| 		}, | ||||
| 	}, | ||||
| router.addListener('push', ctx => { | ||||
| 	 | ||||
| }); | ||||
| 
 | ||||
| let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); | ||||
| let rootEl = $ref(); | ||||
| let modal = $ref<InstanceType<typeof MkModal>>(); | ||||
| let path = $ref(props.initialPath); | ||||
| let width = $ref(860); | ||||
| let height = $ref(660); | ||||
| const history = []; | ||||
| 
 | ||||
| provide('router', router); | ||||
| provideMetadataReceiver((info) => { | ||||
| 	pageMetadata = info; | ||||
| }); | ||||
| provide('shouldOmitHeaderTitle', true); | ||||
| provide('shouldHeaderThin', true); | ||||
| 
 | ||||
| const pageUrl = $computed(() => url + path); | ||||
| const contextmenu = $computed(() => { | ||||
| 	return [{ | ||||
| 		type: 'label', | ||||
| 		text: path, | ||||
| 	}, { | ||||
| 		icon: 'fas fa-expand-alt', | ||||
| 		text: i18n.ts.showInPage, | ||||
| 		action: expand, | ||||
| 	}, { | ||||
| 		icon: 'fas fa-external-link-alt', | ||||
| 		text: i18n.ts.popout, | ||||
| 		action: popout, | ||||
| 	}, null, { | ||||
| 		icon: 'fas fa-external-link-alt', | ||||
| 		text: i18n.ts.openInNewTab, | ||||
| 		action: () => { | ||||
| 			window.open(pageUrl, '_blank'); | ||||
| 			modal.close(); | ||||
| 		}, | ||||
| 	}, { | ||||
| 		icon: 'fas fa-link', | ||||
| 		text: i18n.ts.copyLink, | ||||
| 		action: () => { | ||||
| 			copyToClipboard(pageUrl); | ||||
| 		}, | ||||
| 	}]; | ||||
| }); | ||||
| 
 | ||||
| function navigate(path, record = true) { | ||||
| 	if (record) history.push(router.getCurrentPath()); | ||||
| 	router.push(path); | ||||
| } | ||||
| 
 | ||||
| function back() { | ||||
| 	navigate(history.pop(), false); | ||||
| } | ||||
| 
 | ||||
| function expand() { | ||||
| 	mainRouter.push(path); | ||||
| 	modal.close(); | ||||
| } | ||||
| 
 | ||||
| function popout() { | ||||
| 	_popout(path, rootEl); | ||||
| 	modal.close(); | ||||
| } | ||||
| 
 | ||||
| function onContextmenu(ev: MouseEvent) { | ||||
| 	os.contextMenu(contextmenu, ev); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -225,7 +225,7 @@ function undoReact(note): void { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| const currentClipPage = inject<Ref<misskey.entities.Clip>>('currentClipPage'); | ||||
| const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null); | ||||
| 
 | ||||
| function onContextmenu(ev: MouseEvent): void { | ||||
| 	const isLink = (el: HTMLElement) => { | ||||
|  |  | |||
|  | @ -1,186 +1,135 @@ | |||
| <template> | ||||
| <XWindow ref="window" | ||||
| <XWindow | ||||
| 	ref="windowEl" | ||||
| 	:initial-width="500" | ||||
| 	:initial-height="500" | ||||
| 	:can-resize="true" | ||||
| 	:close-button="true" | ||||
| 	:buttons-left="buttonsLeft" | ||||
| 	:buttons-right="buttonsRight" | ||||
| 	:contextmenu="contextmenu" | ||||
| 	@closed="$emit('closed')" | ||||
| > | ||||
| 	<template #header> | ||||
| 		<template v-if="pageInfo"> | ||||
| 			<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i> | ||||
| 			<span>{{ pageInfo.title }}</span> | ||||
| 		<template v-if="pageMetadata?.value"> | ||||
| 			<i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i> | ||||
| 			<span>{{ pageMetadata.value.title }}</span> | ||||
| 		</template> | ||||
| 	</template> | ||||
| 	<template #headerLeft> | ||||
| 		<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button> | ||||
| 	</template> | ||||
| 	<template #headerRight> | ||||
| 		<button v-tooltip="$ts.showInPage" class="_button" @click="expand()"><i class="fas fa-expand-alt"></i></button> | ||||
| 		<button v-tooltip="$ts.popout" class="_button" @click="popout()"><i class="fas fa-external-link-alt"></i></button> | ||||
| 		<button class="_button" @click="menu"><i class="fas fa-ellipsis-h"></i></button> | ||||
| 	</template> | ||||
| 
 | ||||
| 	<div class="yrolvcoq" :style="{ background: pageInfo?.bg }"> | ||||
| 		<MkStickyContainer> | ||||
| 			<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template> | ||||
| 			<component :is="component" v-bind="props" :ref="changePage"/> | ||||
| 		</MkStickyContainer> | ||||
| 	<div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }"> | ||||
| 		<RouterView :router="router"/> | ||||
| 	</div> | ||||
| </XWindow> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { ComputedRef, inject, provide } from 'vue'; | ||||
| import RouterView from './global/router-view.vue'; | ||||
| import XWindow from '@/components/ui/window.vue'; | ||||
| import { popout } from '@/scripts/popout'; | ||||
| import { popout as _popout } from '@/scripts/popout'; | ||||
| import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||
| import { resolve } from '@/router'; | ||||
| import { url } from '@/config'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import * as os from '@/os'; | ||||
| import { mainRouter, routes } from '@/router'; | ||||
| import { Router } from '@/nirax'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XWindow, | ||||
| const props = defineProps<{ | ||||
| 	initialPath: string; | ||||
| }>(); | ||||
| 
 | ||||
| defineEmits<{ | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| 
 | ||||
| const router = new Router(routes, props.initialPath); | ||||
| 
 | ||||
| let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); | ||||
| let windowEl = $ref<InstanceType<typeof XWindow>>(); | ||||
| const history = $ref<string[]>([props.initialPath]); | ||||
| const buttonsLeft = $computed(() => { | ||||
| 	const buttons = []; | ||||
| 
 | ||||
| 	if (history.length > 1) { | ||||
| 		buttons.push({ | ||||
| 			icon: 'fas fa-arrow-left', | ||||
| 			onClick: back, | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	return buttons; | ||||
| }); | ||||
| const buttonsRight = $computed(() => { | ||||
| 	const buttons = [{ | ||||
| 		icon: 'fas fa-expand-alt', | ||||
| 		title: i18n.ts.showInPage, | ||||
| 		onClick: expand, | ||||
| 	}]; | ||||
| 
 | ||||
| 	return buttons; | ||||
| }); | ||||
| 
 | ||||
| router.addListener('push', ctx => { | ||||
| 	history.push(router.getCurrentPath()); | ||||
| }); | ||||
| 
 | ||||
| provide('router', router); | ||||
| provideMetadataReceiver((info) => { | ||||
| 	pageMetadata = info; | ||||
| }); | ||||
| provide('shouldOmitHeaderTitle', true); | ||||
| provide('shouldHeaderThin', true); | ||||
| 
 | ||||
| const contextmenu = $computed(() => ([{ | ||||
| 	icon: 'fas fa-expand-alt', | ||||
| 	text: i18n.ts.showInPage, | ||||
| 	action: expand, | ||||
| }, { | ||||
| 	icon: 'fas fa-external-link-alt', | ||||
| 	text: i18n.ts.popout, | ||||
| 	action: popout, | ||||
| }, { | ||||
| 	icon: 'fas fa-external-link-alt', | ||||
| 	text: i18n.ts.openInNewTab, | ||||
| 	action: () => { | ||||
| 		window.open(url + router.getCurrentPath(), '_blank'); | ||||
| 		windowEl.close(); | ||||
| 	}, | ||||
| 
 | ||||
| 	inject: { | ||||
| 		sideViewHook: { | ||||
| 			default: null | ||||
| 		} | ||||
| }, { | ||||
| 	icon: 'fas fa-link', | ||||
| 	text: i18n.ts.copyLink, | ||||
| 	action: () => { | ||||
| 		copyToClipboard(url + router.getCurrentPath()); | ||||
| 	}, | ||||
| }])); | ||||
| 
 | ||||
| 	provide() { | ||||
| 		return { | ||||
| 			navHook: (path) => { | ||||
| 				this.navigate(path); | ||||
| 			}, | ||||
| 			shouldHeaderThin: true, | ||||
| 		}; | ||||
| 	}, | ||||
| function menu(ev) { | ||||
| 	os.popupMenu(contextmenu, ev.currentTarget ?? ev.target); | ||||
| } | ||||
| 
 | ||||
| 	props: { | ||||
| 		initialPath: { | ||||
| 			type: String, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		initialComponent: { | ||||
| 			type: Object, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		initialProps: { | ||||
| 			type: Object, | ||||
| 			required: false, | ||||
| 			default: () => {}, | ||||
| 		}, | ||||
| 	}, | ||||
| function back() { | ||||
| 	history.pop(); | ||||
| 	router.change(history[history.length - 1]); | ||||
| } | ||||
| 
 | ||||
| 	emits: ['closed'], | ||||
| function close() { | ||||
| 	windowEl.close(); | ||||
| } | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			pageInfo: null, | ||||
| 			path: this.initialPath, | ||||
| 			component: this.initialComponent, | ||||
| 			props: this.initialProps, | ||||
| 			history: [], | ||||
| 		}; | ||||
| 	}, | ||||
| function expand() { | ||||
| 	mainRouter.push(router.getCurrentPath()); | ||||
| 	windowEl.close(); | ||||
| } | ||||
| 
 | ||||
| 	computed: { | ||||
| 		url(): string { | ||||
| 			return url + this.path; | ||||
| 		}, | ||||
| function popout() { | ||||
| 	_popout(router.getCurrentPath(), windowEl.$el); | ||||
| 	windowEl.close(); | ||||
| } | ||||
| 
 | ||||
| 		contextmenu() { | ||||
| 			return [{ | ||||
| 				type: 'label', | ||||
| 				text: this.path, | ||||
| 			}, { | ||||
| 				icon: 'fas fa-expand-alt', | ||||
| 				text: this.$ts.showInPage, | ||||
| 				action: this.expand | ||||
| 			}, this.sideViewHook ? { | ||||
| 				icon: 'fas fa-columns', | ||||
| 				text: this.$ts.openInSideView, | ||||
| 				action: () => { | ||||
| 					this.sideViewHook(this.path); | ||||
| 					this.$refs.window.close(); | ||||
| 				} | ||||
| 			} : undefined, { | ||||
| 				icon: 'fas fa-external-link-alt', | ||||
| 				text: this.$ts.popout, | ||||
| 				action: this.popout | ||||
| 			}, null, { | ||||
| 				icon: 'fas fa-external-link-alt', | ||||
| 				text: this.$ts.openInNewTab, | ||||
| 				action: () => { | ||||
| 					window.open(this.url, '_blank'); | ||||
| 					this.$refs.window.close(); | ||||
| 				} | ||||
| 			}, { | ||||
| 				icon: 'fas fa-link', | ||||
| 				text: this.$ts.copyLink, | ||||
| 				action: () => { | ||||
| 					copyToClipboard(this.url); | ||||
| 				} | ||||
| 			}]; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		changePage(page) { | ||||
| 			if (page == null) return; | ||||
| 			if (page[symbols.PAGE_INFO]) { | ||||
| 				this.pageInfo = page[symbols.PAGE_INFO]; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		navigate(path, record = true) { | ||||
| 			if (record) this.history.push(this.path); | ||||
| 			this.path = path; | ||||
| 			const { component, props } = resolve(path); | ||||
| 			this.component = component; | ||||
| 			this.props = props; | ||||
| 		}, | ||||
| 
 | ||||
| 		menu(ev) { | ||||
| 			os.popupMenu([{ | ||||
| 				icon: 'fas fa-external-link-alt', | ||||
| 				text: this.$ts.openInNewTab, | ||||
| 				action: () => { | ||||
| 					window.open(this.url, '_blank'); | ||||
| 					this.$refs.window.close(); | ||||
| 				} | ||||
| 			}, { | ||||
| 				icon: 'fas fa-link', | ||||
| 				text: this.$ts.copyLink, | ||||
| 				action: () => { | ||||
| 					copyToClipboard(this.url); | ||||
| 				} | ||||
| 			}], ev.currentTarget ?? ev.target); | ||||
| 		}, | ||||
| 
 | ||||
| 		back() { | ||||
| 			this.navigate(this.history.pop(), false); | ||||
| 		}, | ||||
| 
 | ||||
| 		close() { | ||||
| 			this.$refs.window.close(); | ||||
| 		}, | ||||
| 
 | ||||
| 		expand() { | ||||
| 			this.$router.push(this.path); | ||||
| 			this.$refs.window.close(); | ||||
| 		}, | ||||
| 
 | ||||
| 		popout() { | ||||
| 			popout(this.path, this.$el); | ||||
| 			this.$refs.window.close(); | ||||
| 		}, | ||||
| 	}, | ||||
| defineExpose({ | ||||
| 	close, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,14 +4,14 @@ | |||
| 		<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown"> | ||||
| 			<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu"> | ||||
| 				<span class="left"> | ||||
| 					<slot name="headerLeft"></slot> | ||||
| 					<button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button> | ||||
| 				</span> | ||||
| 				<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown"> | ||||
| 					<slot name="header"></slot> | ||||
| 				</span> | ||||
| 				<span class="right"> | ||||
| 					<slot name="headerRight"></slot> | ||||
| 					<button v-if="closeButton" class="_button" @click="close()"><i class="fas fa-times"></i></button> | ||||
| 					<button v-for="button in buttonsRight" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button> | ||||
| 					<button v-if="closeButton" class="button _button" @click="close()"><i class="fas fa-times"></i></button> | ||||
| 				</span> | ||||
| 			</div> | ||||
| 			<div v-if="padding" class="body"> | ||||
|  | @ -46,41 +46,41 @@ const minHeight = 50; | |||
| const minWidth = 250; | ||||
| 
 | ||||
| function dragListen(fn) { | ||||
| 	window.addEventListener('mousemove',  fn); | ||||
| 	window.addEventListener('touchmove',  fn); | ||||
| 	window.addEventListener('mousemove', fn); | ||||
| 	window.addEventListener('touchmove', fn); | ||||
| 	window.addEventListener('mouseleave', dragClear.bind(null, fn)); | ||||
| 	window.addEventListener('mouseup',    dragClear.bind(null, fn)); | ||||
| 	window.addEventListener('touchend',   dragClear.bind(null, fn)); | ||||
| 	window.addEventListener('mouseup', dragClear.bind(null, fn)); | ||||
| 	window.addEventListener('touchend', dragClear.bind(null, fn)); | ||||
| } | ||||
| 
 | ||||
| function dragClear(fn) { | ||||
| 	window.removeEventListener('mousemove',  fn); | ||||
| 	window.removeEventListener('touchmove',  fn); | ||||
| 	window.removeEventListener('mousemove', fn); | ||||
| 	window.removeEventListener('touchmove', fn); | ||||
| 	window.removeEventListener('mouseleave', dragClear); | ||||
| 	window.removeEventListener('mouseup',    dragClear); | ||||
| 	window.removeEventListener('touchend',   dragClear); | ||||
| 	window.removeEventListener('mouseup', dragClear); | ||||
| 	window.removeEventListener('touchend', dragClear); | ||||
| } | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	provide: { | ||||
| 		inWindow: true | ||||
| 		inWindow: true, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		padding: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 			default: false, | ||||
| 		}, | ||||
| 		initialWidth: { | ||||
| 			type: Number, | ||||
| 			required: false, | ||||
| 			default: 400 | ||||
| 			default: 400, | ||||
| 		}, | ||||
| 		initialHeight: { | ||||
| 			type: Number, | ||||
| 			required: false, | ||||
| 			default: null | ||||
| 			default: null, | ||||
| 		}, | ||||
| 		canResize: { | ||||
| 			type: Boolean, | ||||
|  | @ -105,7 +105,17 @@ export default defineComponent({ | |||
| 		contextmenu: { | ||||
| 			type: Array, | ||||
| 			required: false, | ||||
| 		} | ||||
| 		}, | ||||
| 		buttonsLeft: { | ||||
| 			type: Array, | ||||
| 			required: false, | ||||
| 			default: [], | ||||
| 		}, | ||||
| 		buttonsRight: { | ||||
| 			type: Array, | ||||
| 			required: false, | ||||
| 			default: [], | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['closed'], | ||||
|  | @ -162,7 +172,10 @@ export default defineComponent({ | |||
| 			this.top(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onHeaderMousedown(evt) { | ||||
| 		onHeaderMousedown(evt: MouseEvent) { | ||||
| 			// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視 | ||||
| 			if (evt.button === 2) return; | ||||
| 
 | ||||
| 			const main = this.$el as any; | ||||
| 
 | ||||
| 			if (!contains(main, document.activeElement)) main.focus(); | ||||
|  | @ -356,12 +369,12 @@ export default defineComponent({ | |||
| 			const browserHeight = window.innerHeight; | ||||
| 			const windowWidth = main.offsetWidth; | ||||
| 			const windowHeight = main.offsetHeight; | ||||
| 			if (position.left < 0) main.style.left = 0;     // 左はみ出し | ||||
| 			if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px';  // 下はみ出し | ||||
| 			if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px';    // 右はみ出し | ||||
| 			if (position.top < 0) main.style.top = 0;       // 上はみ出し | ||||
| 		} | ||||
| 	} | ||||
| 			if (position.left < 0) main.style.left = 0; // 左はみ出し | ||||
| 			if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し | ||||
| 			if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し | ||||
| 			if (position.top < 0) main.style.top = 0; // 上はみ出し | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  | @ -404,17 +417,25 @@ export default defineComponent({ | |||
| 			border-bottom: solid 1px var(--divider); | ||||
| 
 | ||||
| 			> .left, > .right { | ||||
| 				> ::v-deep(button) { | ||||
| 				> .button { | ||||
| 					height: var(--height); | ||||
| 					width: var(--height); | ||||
| 
 | ||||
| 					&:hover { | ||||
| 						color: var(--fgHighlighted); | ||||
| 					} | ||||
| 
 | ||||
| 					&.highlighted { | ||||
| 						color: var(--accent); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			> .left { | ||||
| 				margin-right: 16px; | ||||
| 			} | ||||
| 
 | ||||
| 			> .right { | ||||
| 				min-width: 16px; | ||||
| 			} | ||||
| 
 | ||||
|  |  | |||
|  | @ -21,7 +21,6 @@ import widgets from '@/widgets'; | |||
| import directives from '@/directives'; | ||||
| import components from '@/components'; | ||||
| import { version, ui, lang, host } from '@/config'; | ||||
| import { router } from '@/router'; | ||||
| import { applyTheme } from '@/scripts/theme'; | ||||
| import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | @ -170,11 +169,10 @@ fetchInstanceMetaPromise.then(() => { | |||
| 
 | ||||
| const app = createApp( | ||||
| 	window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) : | ||||
| 	!$i                               ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : | ||||
| 	ui === 'deck'                     ? defineAsyncComponent(() => import('@/ui/deck.vue')) : | ||||
| 	ui === 'desktop'                  ? defineAsyncComponent(() => import('@/ui/desktop.vue')) : | ||||
| 	ui === 'classic'                  ? defineAsyncComponent(() => import('@/ui/classic.vue')) : | ||||
| 	defineAsyncComponent(() => import('@/ui/universal.vue')) | ||||
| 	!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : | ||||
| 	ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : | ||||
| 	ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : | ||||
| 	defineAsyncComponent(() => import('@/ui/universal.vue')), | ||||
| ); | ||||
| 
 | ||||
| if (_DEV_) { | ||||
|  | @ -189,14 +187,10 @@ app.config.globalProperties = { | |||
| 	$ts: i18n.ts, | ||||
| }; | ||||
| 
 | ||||
| app.use(router); | ||||
| 
 | ||||
| widgets(app); | ||||
| directives(app); | ||||
| components(app); | ||||
| 
 | ||||
| await router.isReady(); | ||||
| 
 | ||||
| const splash = document.getElementById('splash'); | ||||
| // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
 | ||||
| if (splash) splash.addEventListener('transitionend', () => { | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import { computed, ref, reactive } from 'vue'; | ||||
| import { $i } from './account'; | ||||
| import { mainRouter } from '@/router'; | ||||
| import { search } from '@/scripts/search'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { ui } from '@/config'; | ||||
| import { $i } from './account'; | ||||
| import { unisonReload } from '@/scripts/unison-reload'; | ||||
| import { router } from './router'; | ||||
| 
 | ||||
| export const menuDef = reactive({ | ||||
| 	notifications: { | ||||
|  | @ -60,16 +60,16 @@ export const menuDef = reactive({ | |||
| 		title: 'lists', | ||||
| 		icon: 'fas fa-list-ul', | ||||
| 		show: computed(() => $i != null), | ||||
| 		active: computed(() => router.currentRoute.value.path.startsWith('/timeline/list/') || router.currentRoute.value.path === '/my/lists' || router.currentRoute.value.path.startsWith('/my/lists/')), | ||||
| 		active: computed(() => mainRouter.currentRoute.value.path.startsWith('/timeline/list/') || mainRouter.currentRoute.value.path === '/my/lists' || mainRouter.currentRoute.value.path.startsWith('/my/lists/')), | ||||
| 		action: (ev) => { | ||||
| 			const items = ref([{ | ||||
| 				type: 'pending' | ||||
| 				type: 'pending', | ||||
| 			}]); | ||||
| 			os.api('users/lists/list').then(lists => { | ||||
| 				const _items = [...lists.map(list => ({ | ||||
| 					type: 'link', | ||||
| 					text: list.name, | ||||
| 					to: `/timeline/list/${list.id}` | ||||
| 					to: `/timeline/list/${list.id}`, | ||||
| 				})), null, { | ||||
| 					type: 'link', | ||||
| 					to: '/my/lists', | ||||
|  | @ -91,16 +91,16 @@ export const menuDef = reactive({ | |||
| 		title: 'antennas', | ||||
| 		icon: 'fas fa-satellite', | ||||
| 		show: computed(() => $i != null), | ||||
| 		active: computed(() => router.currentRoute.value.path.startsWith('/timeline/antenna/') || router.currentRoute.value.path === '/my/antennas' || router.currentRoute.value.path.startsWith('/my/antennas/')), | ||||
| 		active: computed(() => mainRouter.currentRoute.value.path.startsWith('/timeline/antenna/') || mainRouter.currentRoute.value.path === '/my/antennas' || mainRouter.currentRoute.value.path.startsWith('/my/antennas/')), | ||||
| 		action: (ev) => { | ||||
| 			const items = ref([{ | ||||
| 				type: 'pending' | ||||
| 				type: 'pending', | ||||
| 			}]); | ||||
| 			os.api('antennas/list').then(antennas => { | ||||
| 				const _items = [...antennas.map(antenna => ({ | ||||
| 					type: 'link', | ||||
| 					text: antenna.name, | ||||
| 					to: `/timeline/antenna/${antenna.id}` | ||||
| 					to: `/timeline/antenna/${antenna.id}`, | ||||
| 				})), null, { | ||||
| 					type: 'link', | ||||
| 					to: '/my/antennas', | ||||
|  | @ -178,29 +178,22 @@ export const menuDef = reactive({ | |||
| 				action: () => { | ||||
| 					localStorage.setItem('ui', 'default'); | ||||
| 					unisonReload(); | ||||
| 				} | ||||
| 				}, | ||||
| 			}, { | ||||
| 				text: i18n.ts.deck, | ||||
| 				active: ui === 'deck', | ||||
| 				action: () => { | ||||
| 					localStorage.setItem('ui', 'deck'); | ||||
| 					unisonReload(); | ||||
| 				} | ||||
| 				}, | ||||
| 			}, { | ||||
| 				text: i18n.ts.classic, | ||||
| 				active: ui === 'classic', | ||||
| 				action: () => { | ||||
| 					localStorage.setItem('ui', 'classic'); | ||||
| 					unisonReload(); | ||||
| 				} | ||||
| 			}, /*{ | ||||
| 				text: i18n.ts.desktop + ' (β)', | ||||
| 				active: ui === 'desktop', | ||||
| 				action: () => { | ||||
| 					localStorage.setItem('ui', 'desktop'); | ||||
| 					unisonReload(); | ||||
| 				} | ||||
| 			}*/], ev.currentTarget ?? ev.target); | ||||
| 				}, | ||||
| 			}], ev.currentTarget ?? ev.target); | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										200
									
								
								packages/client/src/nirax.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								packages/client/src/nirax.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,200 @@ | |||
| import { EventEmitter } from 'eventemitter3'; | ||||
| import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue'; | ||||
| 
 | ||||
| type RouteDef = { | ||||
| 	path: string; | ||||
| 	component: Component; | ||||
| 	query?: Record<string, string>; | ||||
| 	name?: string; | ||||
| 	globalCacheKey?: string; | ||||
| }; | ||||
| 
 | ||||
| type ParsedPath = (string | { | ||||
| 	name: string; | ||||
| 	startsWith?: string; | ||||
| 	wildcard?: boolean; | ||||
| 	optional?: boolean; | ||||
| })[]; | ||||
| 
 | ||||
| function parsePath(path: string): ParsedPath { | ||||
| 	const res = [] as ParsedPath; | ||||
| 
 | ||||
| 	path = path.substring(1); | ||||
| 
 | ||||
| 	for (const part of path.split('/')) { | ||||
| 		if (part.includes(':')) { | ||||
| 			const prefix = part.substring(0, part.indexOf(':')); | ||||
| 			const placeholder = part.substring(part.indexOf(':') + 1); | ||||
| 			const wildcard = placeholder.includes('(*)'); | ||||
| 			const optional = placeholder.endsWith('?'); | ||||
| 			res.push({ | ||||
| 				name: placeholder.replace('(*)', '').replace('?', ''), | ||||
| 				startsWith: prefix !== '' ? prefix : undefined, | ||||
| 				wildcard, | ||||
| 				optional, | ||||
| 			}); | ||||
| 		} else { | ||||
| 			res.push(part); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| } | ||||
| 
 | ||||
| export class Router extends EventEmitter<{ | ||||
| 	change: (ctx: { | ||||
| 		beforePath: string; | ||||
| 		path: string; | ||||
| 		route: RouteDef | null; | ||||
| 		props: Map<string, string> | null; | ||||
| 		key: string; | ||||
| 	}) => void; | ||||
| 	push: (ctx: { | ||||
| 		beforePath: string; | ||||
| 		path: string; | ||||
| 		route: RouteDef | null; | ||||
| 		props: Map<string, string> | null; | ||||
| 		key: string; | ||||
| 	}) => void; | ||||
| }> { | ||||
| 	private routes: RouteDef[]; | ||||
| 	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); | ||||
| 
 | ||||
| 	constructor(routes: Router['routes'], currentPath: Router['currentPath']) { | ||||
| 		super(); | ||||
| 
 | ||||
| 		this.routes = routes; | ||||
| 		this.currentPath = currentPath; | ||||
| 		this.navigate(currentPath, null, true); | ||||
| 	} | ||||
| 
 | ||||
| 	public resolve(path: string): { route: RouteDef; props: Map<string, string>; } | null { | ||||
| 		let queryString: string | null = null; | ||||
| 		if (path[0] === '/') path = path.substring(1); | ||||
| 		if (path.includes('?')) { | ||||
| 			queryString = path.substring(path.indexOf('?') + 1); | ||||
| 			path = path.substring(0, path.indexOf('?')); | ||||
| 		} | ||||
| 
 | ||||
| 		if (_DEV_) console.log('Routing: ', path, queryString); | ||||
| 
 | ||||
| 		forEachRouteLoop: | ||||
| 		for (const route of this.routes) { | ||||
| 			let parts = path.split('/'); | ||||
| 			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, parts.join('/')); | ||||
| 							parts = []; | ||||
| 						} | ||||
| 						break pathMatchLoop; | ||||
| 					} else { | ||||
| 						if (p.startsWith && (parts[0] == null || !parts[0].startsWith(p.startsWith))) continue forEachRouteLoop; | ||||
| 
 | ||||
| 						props.set(p.name, parts[0]); | ||||
| 						parts.shift(); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if (parts.length !== 0) continue forEachRouteLoop; | ||||
| 
 | ||||
| 			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, queryObject[q]); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 			return { | ||||
| 				route, | ||||
| 				props, | ||||
| 			}; | ||||
| 		} | ||||
| 
 | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	private navigate(path: string, key: string | null | undefined, initial = false) { | ||||
| 		const beforePath = this.currentPath; | ||||
| 		const beforeRoute = this.currentRoute.value; | ||||
| 		this.currentPath = path; | ||||
| 
 | ||||
| 		const res = this.resolve(this.currentPath); | ||||
| 
 | ||||
| 		if (res == null) { | ||||
| 			throw new Error('no route found for: ' + path); | ||||
| 		} | ||||
| 
 | ||||
| 		const isSamePath = beforePath === path; | ||||
| 		if (isSamePath && key == null) key = this.currentKey; | ||||
| 		this.currentComponent = res.route.component; | ||||
| 		this.currentProps = res.props; | ||||
| 		this.currentRoute.value = res.route; | ||||
| 		this.currentKey = this.currentRoute.value.globalCacheKey ?? key ?? Date.now().toString(); | ||||
| 
 | ||||
| 		if (!initial) { | ||||
| 			this.emit('change', { | ||||
| 				beforePath, | ||||
| 				path, | ||||
| 				route: this.currentRoute.value, | ||||
| 				props: this.currentProps, | ||||
| 				key: this.currentKey, | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public getCurrentComponent() { | ||||
| 		return this.currentComponent; | ||||
| 	} | ||||
| 
 | ||||
| 	public getCurrentProps() { | ||||
| 		return this.currentProps; | ||||
| 	} | ||||
| 
 | ||||
| 	public getCurrentPath() { | ||||
| 		return this.currentPath; | ||||
| 	} | ||||
| 
 | ||||
| 	public getCurrentKey() { | ||||
| 		return this.currentKey; | ||||
| 	} | ||||
| 
 | ||||
| 	public push(path: string) { | ||||
| 		const beforePath = this.currentPath; | ||||
| 		this.navigate(path, null); | ||||
| 		this.emit('push', { | ||||
| 			beforePath, | ||||
| 			path, | ||||
| 			route: this.currentRoute.value, | ||||
| 			props: this.currentProps, | ||||
| 			key: this.currentKey, | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	public change(path: string, key?: string | null) { | ||||
| 		this.navigate(path, key); | ||||
| 	} | ||||
| } | ||||
|  | @ -8,7 +8,6 @@ import { apiUrl, url } from '@/config'; | |||
| import MkPostFormDialog from '@/components/post-form-dialog.vue'; | ||||
| import MkWaitingDialog from '@/components/waiting-dialog.vue'; | ||||
| import { MenuItem } from '@/types/menu'; | ||||
| import { resolve } from '@/router'; | ||||
| import { $i } from '@/account'; | ||||
| 
 | ||||
| export const pendingApiRequestsCount = ref(0); | ||||
|  | @ -155,20 +154,14 @@ export async function popup(component: Component, props: Record<string, any>, ev | |||
| } | ||||
| 
 | ||||
| export function pageWindow(path: string) { | ||||
| 	const { component, props } = resolve(path); | ||||
| 	popup(defineAsyncComponent(() => import('@/components/page-window.vue')), { | ||||
| 		initialPath: path, | ||||
| 		initialComponent: markRaw(component), | ||||
| 		initialProps: props, | ||||
| 	}, {}, 'closed'); | ||||
| } | ||||
| 
 | ||||
| export function modalPageWindow(path: string) { | ||||
| 	const { component, props } = resolve(path); | ||||
| 	popup(defineAsyncComponent(() => import('@/components/modal-page-window.vue')), { | ||||
| 		initialPath: path, | ||||
| 		initialComponent: markRaw(component), | ||||
| 		initialProps: props, | ||||
| 	}, {}, 'closed'); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -21,11 +21,11 @@ | |||
| import { } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { version } from '@/config'; | ||||
| import * as os from '@/os'; | ||||
| import { unisonReload } from '@/scripts/unison-reload'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	error?: Error; | ||||
|  | @ -52,11 +52,13 @@ function reload() { | |||
| 	unisonReload(); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.error, | ||||
| 		icon: 'fas fa-exclamation-triangle', | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.error, | ||||
| 	icon: 'fas fa-exclamation-triangle', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,62 +1,65 @@ | |||
| <template> | ||||
| <div style="overflow: clip;"> | ||||
| 	<MkSpacer :content-max="600" :margin-min="20"> | ||||
| 		<div class="_formRoot znqjceqz"> | ||||
| 			<div id="debug"></div> | ||||
| 			<div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }"> | ||||
| 				<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/> | ||||
| 				<div class="misskey">Misskey</div> | ||||
| 				<div class="version">v{{ version }}</div> | ||||
| 				<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span> | ||||
| 			</div> | ||||
| 			<div class="_formBlock" style="text-align: center;"> | ||||
| 				{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a> | ||||
| 			</div> | ||||
| 			<div class="_formBlock" style="text-align: center;"> | ||||
| 				<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton> | ||||
| 			</div> | ||||
| 			<FormSection> | ||||
| 				<div class="_formLinks"> | ||||
| 					<FormLink to="https://github.com/misskey-dev/misskey" external> | ||||
| 						<template #icon><i class="fas fa-code"></i></template> | ||||
| 						{{ i18n.ts._aboutMisskey.source }} | ||||
| 						<template #suffix>GitHub</template> | ||||
| 					</FormLink> | ||||
| 					<FormLink to="https://crowdin.com/project/misskey" external> | ||||
| 						<template #icon><i class="fas fa-language"></i></template> | ||||
| 						{{ i18n.ts._aboutMisskey.translation }} | ||||
| 						<template #suffix>Crowdin</template> | ||||
| 					</FormLink> | ||||
| 					<FormLink to="https://www.patreon.com/syuilo" external> | ||||
| 						<template #icon><i class="fas fa-hand-holding-medical"></i></template> | ||||
| 						{{ i18n.ts._aboutMisskey.donate }} | ||||
| 						<template #suffix>Patreon</template> | ||||
| 					</FormLink> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<div style="overflow: clip;"> | ||||
| 		<MkSpacer :content-max="600" :margin-min="20"> | ||||
| 			<div class="_formRoot znqjceqz"> | ||||
| 				<div id="debug"></div> | ||||
| 				<div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }"> | ||||
| 					<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/> | ||||
| 					<div class="misskey">Misskey</div> | ||||
| 					<div class="version">v{{ version }}</div> | ||||
| 					<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span> | ||||
| 				</div> | ||||
| 			</FormSection> | ||||
| 			<FormSection> | ||||
| 				<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template> | ||||
| 				<div class="_formLinks"> | ||||
| 					<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink> | ||||
| 					<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink> | ||||
| 					<FormLink to="https://github.com/mei23" external>@mei23</FormLink> | ||||
| 					<FormLink to="https://github.com/acid-chicken" external>@acid-chicken</FormLink> | ||||
| 					<FormLink to="https://github.com/tamaina" external>@tamaina</FormLink> | ||||
| 					<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink> | ||||
| 					<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink> | ||||
| 					<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink> | ||||
| 					<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink> | ||||
| 				<div class="_formBlock" style="text-align: center;"> | ||||
| 					{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a> | ||||
| 				</div> | ||||
| 				<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template> | ||||
| 			</FormSection> | ||||
| 			<FormSection> | ||||
| 				<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template> | ||||
| 				<div v-for="patron in patrons" :key="patron">{{ patron }}</div> | ||||
| 				<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template> | ||||
| 			</FormSection> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </div> | ||||
| 				<div class="_formBlock" style="text-align: center;"> | ||||
| 					<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton> | ||||
| 				</div> | ||||
| 				<FormSection> | ||||
| 					<div class="_formLinks"> | ||||
| 						<FormLink to="https://github.com/misskey-dev/misskey" external> | ||||
| 							<template #icon><i class="fas fa-code"></i></template> | ||||
| 							{{ i18n.ts._aboutMisskey.source }} | ||||
| 							<template #suffix>GitHub</template> | ||||
| 						</FormLink> | ||||
| 						<FormLink to="https://crowdin.com/project/misskey" external> | ||||
| 							<template #icon><i class="fas fa-language"></i></template> | ||||
| 							{{ i18n.ts._aboutMisskey.translation }} | ||||
| 							<template #suffix>Crowdin</template> | ||||
| 						</FormLink> | ||||
| 						<FormLink to="https://www.patreon.com/syuilo" external> | ||||
| 							<template #icon><i class="fas fa-hand-holding-medical"></i></template> | ||||
| 							{{ i18n.ts._aboutMisskey.donate }} | ||||
| 							<template #suffix>Patreon</template> | ||||
| 						</FormLink> | ||||
| 					</div> | ||||
| 				</FormSection> | ||||
| 				<FormSection> | ||||
| 					<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template> | ||||
| 					<div class="_formLinks"> | ||||
| 						<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink> | ||||
| 						<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink> | ||||
| 						<FormLink to="https://github.com/mei23" external>@mei23</FormLink> | ||||
| 						<FormLink to="https://github.com/acid-chicken" external>@acid-chicken</FormLink> | ||||
| 						<FormLink to="https://github.com/tamaina" external>@tamaina</FormLink> | ||||
| 						<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink> | ||||
| 						<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink> | ||||
| 						<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink> | ||||
| 						<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink> | ||||
| 					</div> | ||||
| 					<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template> | ||||
| 				</FormSection> | ||||
| 				<FormSection> | ||||
| 					<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template> | ||||
| 					<div v-for="patron in patrons" :key="patron">{{ patron }}</div> | ||||
| 					<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template> | ||||
| 				</FormSection> | ||||
| 			</div> | ||||
| 		</MkSpacer> | ||||
| 	</div> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -67,10 +70,10 @@ import FormSection from '@/components/form/section.vue'; | |||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkLink from '@/components/link.vue'; | ||||
| import { physics } from '@/scripts/physics'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import * as os from '@/os'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const patrons = [ | ||||
| 	'まっちゃとーにゅ', | ||||
|  | @ -194,12 +197,14 @@ onBeforeUnmount(() => { | |||
| 	} | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.aboutMisskey, | ||||
| 		icon: null, | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.aboutMisskey, | ||||
| 	icon: null, | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,78 +1,81 @@ | |||
| <template> | ||||
| <MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20"> | ||||
| 	<div class="_formRoot"> | ||||
| 		<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"> | ||||
| 			<div class="content"> | ||||
| 				<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/> | ||||
| 				<div class="name"> | ||||
| 					<b>{{ $instance.name || host }}</b> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20"> | ||||
| 		<div class="_formRoot"> | ||||
| 			<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"> | ||||
| 				<div class="content"> | ||||
| 					<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/> | ||||
| 					<div class="name"> | ||||
| 						<b>{{ $instance.name || host }}</b> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<MkKeyValue class="_formBlock"> | ||||
| 			<template #key>{{ $ts.description }}</template> | ||||
| 			<template #value>{{ $instance.description }}</template> | ||||
| 		</MkKeyValue> | ||||
| 
 | ||||
| 		<FormSection> | ||||
| 			<MkKeyValue class="_formBlock" :copy="version"> | ||||
| 				<template #key>Misskey</template> | ||||
| 				<template #value>{{ version }}</template> | ||||
| 			<MkKeyValue class="_formBlock"> | ||||
| 				<template #key>{{ $ts.description }}</template> | ||||
| 				<template #value>{{ $instance.description }}</template> | ||||
| 			</MkKeyValue> | ||||
| 			<FormLink to="/about-misskey">{{ $ts.aboutMisskey }}</FormLink> | ||||
| 		</FormSection> | ||||
| 
 | ||||
| 		<FormSection> | ||||
| 			<FormSplit> | ||||
| 				<MkKeyValue class="_formBlock"> | ||||
| 					<template #key>{{ $ts.administrator }}</template> | ||||
| 					<template #value>{{ $instance.maintainerName }}</template> | ||||
| 				</MkKeyValue> | ||||
| 				<MkKeyValue class="_formBlock"> | ||||
| 					<template #key>{{ $ts.contact }}</template> | ||||
| 					<template #value>{{ $instance.maintainerEmail }}</template> | ||||
| 				</MkKeyValue> | ||||
| 			</FormSplit> | ||||
| 			<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink> | ||||
| 		</FormSection> | ||||
| 
 | ||||
| 		<FormSuspense :p="initStats"> | ||||
| 			<FormSection> | ||||
| 				<template #label>{{ $ts.statistics }}</template> | ||||
| 				<MkKeyValue class="_formBlock" :copy="version"> | ||||
| 					<template #key>Misskey</template> | ||||
| 					<template #value>{{ version }}</template> | ||||
| 				</MkKeyValue> | ||||
| 				<FormLink to="/about-misskey">{{ $ts.aboutMisskey }}</FormLink> | ||||
| 			</FormSection> | ||||
| 
 | ||||
| 			<FormSection> | ||||
| 				<FormSplit> | ||||
| 					<MkKeyValue class="_formBlock"> | ||||
| 						<template #key>{{ $ts.users }}</template> | ||||
| 						<template #value>{{ number(stats.originalUsersCount) }}</template> | ||||
| 						<template #key>{{ $ts.administrator }}</template> | ||||
| 						<template #value>{{ $instance.maintainerName }}</template> | ||||
| 					</MkKeyValue> | ||||
| 					<MkKeyValue class="_formBlock"> | ||||
| 						<template #key>{{ $ts.notes }}</template> | ||||
| 						<template #value>{{ number(stats.originalNotesCount) }}</template> | ||||
| 						<template #key>{{ $ts.contact }}</template> | ||||
| 						<template #value>{{ $instance.maintainerEmail }}</template> | ||||
| 					</MkKeyValue> | ||||
| 				</FormSplit> | ||||
| 				<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink> | ||||
| 			</FormSection> | ||||
| 		</FormSuspense> | ||||
| 
 | ||||
| 		<FormSection> | ||||
| 			<template #label>Well-known resources</template> | ||||
| 			<div class="_formLinks"> | ||||
| 				<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink> | ||||
| 				<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink> | ||||
| 				<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink> | ||||
| 				<FormLink :to="`/robots.txt`" external>robots.txt</FormLink> | ||||
| 				<FormLink :to="`/manifest.json`" external>manifest.json</FormLink> | ||||
| 			</div> | ||||
| 		</FormSection> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| <MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20"> | ||||
| 	<MkInstanceStats :chart-limit="500" :detailed="true"/> | ||||
| </MkSpacer> | ||||
| 			<FormSuspense :p="initStats"> | ||||
| 				<FormSection> | ||||
| 					<template #label>{{ $ts.statistics }}</template> | ||||
| 					<FormSplit> | ||||
| 						<MkKeyValue class="_formBlock"> | ||||
| 							<template #key>{{ $ts.users }}</template> | ||||
| 							<template #value>{{ number(stats.originalUsersCount) }}</template> | ||||
| 						</MkKeyValue> | ||||
| 						<MkKeyValue class="_formBlock"> | ||||
| 							<template #key>{{ $ts.notes }}</template> | ||||
| 							<template #value>{{ number(stats.originalNotesCount) }}</template> | ||||
| 						</MkKeyValue> | ||||
| 					</FormSplit> | ||||
| 				</FormSection> | ||||
| 			</FormSuspense> | ||||
| 
 | ||||
| 			<FormSection> | ||||
| 				<template #label>Well-known resources</template> | ||||
| 				<div class="_formLinks"> | ||||
| 					<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink> | ||||
| 					<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink> | ||||
| 					<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink> | ||||
| 					<FormLink :to="`/robots.txt`" external>robots.txt</FormLink> | ||||
| 					<FormLink :to="`/manifest.json`" external>manifest.json</FormLink> | ||||
| 				</div> | ||||
| 			</FormSection> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| 	<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20"> | ||||
| 		<MkInstanceStats :chart-limit="500" :detailed="true"/> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { ref, computed } from 'vue'; | ||||
| import { version, instanceName } from '@/config'; | ||||
| import { version, instanceName , host } from '@/config'; | ||||
| import FormLink from '@/components/form/link.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
|  | @ -81,9 +84,8 @@ import MkKeyValue from '@/components/key-value.vue'; | |||
| import MkInstanceStats from '@/components/instance-stats.vue'; | ||||
| import * as os from '@/os'; | ||||
| import number from '@/filters/number'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { host } from '@/config'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let stats = $ref(null); | ||||
| let tab = $ref('overview'); | ||||
|  | @ -93,23 +95,24 @@ const initStats = () => os.api('stats', { | |||
| 	stats = res; | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.ts.instanceInfo, | ||||
| 		icon: 'fas fa-info-circle', | ||||
| 		bg: 'var(--bg)', | ||||
| 		tabs: [{ | ||||
| 			active: tab === 'overview', | ||||
| 			title: i18n.ts.overview, | ||||
| 			onClick: () => { tab = 'overview'; }, | ||||
| 		}, { | ||||
| 			active: tab === 'charts', | ||||
| 			title: i18n.ts.charts, | ||||
| 			icon: 'fas fa-chart-bar', | ||||
| 			onClick: () => { tab = 'charts'; }, | ||||
| 		},], | ||||
| 	})), | ||||
| }); | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => [{ | ||||
| 	active: tab === 'overview', | ||||
| 	title: i18n.ts.overview, | ||||
| 	onClick: () => { tab = 'overview'; }, | ||||
| }, { | ||||
| 	active: tab === 'charts', | ||||
| 	title: i18n.ts.charts, | ||||
| 	icon: 'fas fa-chart-bar', | ||||
| 	onClick: () => { tab = 'charts'; }, | ||||
| }]); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: i18n.ts.instanceInfo, | ||||
| 	icon: 'fas fa-info-circle', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,30 +1,33 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="500" :margin-min="16" :margin-max="32"> | ||||
| 	<div v-if="file" class="cxqhhsmd _formRoot"> | ||||
| 		<div class="_formBlock"> | ||||
| 			<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||
| 			<div class="info"> | ||||
| 				<span style="margin-right: 1em;">{{ file.type }}</span> | ||||
| 				<span>{{ bytes(file.size) }}</span> | ||||
| 				<MkTime :time="file.createdAt" mode="detail" style="display: block;"/> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="500" :margin-min="16" :margin-max="32"> | ||||
| 		<div v-if="file" class="cxqhhsmd _formRoot"> | ||||
| 			<div class="_formBlock"> | ||||
| 				<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||
| 				<div class="info"> | ||||
| 					<span style="margin-right: 1em;">{{ file.type }}</span> | ||||
| 					<span>{{ bytes(file.size) }}</span> | ||||
| 					<MkTime :time="file.createdAt" mode="detail" style="display: block;"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="_formBlock"> | ||||
| 				<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch> | ||||
| 			</div> | ||||
| 			<div class="_formBlock"> | ||||
| 				<MkButton full @click="showUser"><i class="fas fa-external-link-square-alt"></i> {{ $ts.user }}</MkButton> | ||||
| 			</div> | ||||
| 			<div class="_formBlock"> | ||||
| 				<MkButton full danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton> | ||||
| 			</div> | ||||
| 			<div v-if="info" class="_formBlock"> | ||||
| 				<details class="_content rawdata"> | ||||
| 					<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre> | ||||
| 				</details> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="_formBlock"> | ||||
| 			<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch> | ||||
| 		</div> | ||||
| 		<div class="_formBlock"> | ||||
| 			<MkButton full @click="showUser"><i class="fas fa-external-link-square-alt"></i> {{ $ts.user }}</MkButton> | ||||
| 		</div> | ||||
| 		<div class="_formBlock"> | ||||
| 			<MkButton full danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton> | ||||
| 		</div> | ||||
| 		<div v-if="info" class="_formBlock"> | ||||
| 			<details class="_content rawdata"> | ||||
| 				<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre> | ||||
| 			</details> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -35,7 +38,7 @@ import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; | |||
| import bytes from '@/filters/bytes'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let file: any = $ref(null); | ||||
| let info: any = $ref(null); | ||||
|  | @ -74,13 +77,15 @@ async function toggleIsSensitive(v) { | |||
| 	isSensitive = v; | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file, | ||||
| 		icon: 'fas fa-file', | ||||
| 		bg: 'var(--bg)', | ||||
| 	})), | ||||
| }); | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file, | ||||
| 	icon: 'fas fa-file', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
							
								
								
									
										249
									
								
								packages/client/src/pages/admin/_header_.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								packages/client/src/pages/admin/_header_.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,249 @@ | |||
| <template> | ||||
| <div ref="el" class="fdidabkc" :style="{ background: bg }" @click="onClick"> | ||||
| 	<template v-if="metadata"> | ||||
| 		<div class="titleContainer" @click="showTabsPopup"> | ||||
| 			<i v-if="metadata.icon" class="icon" :class="metadata.icon"></i> | ||||
| 
 | ||||
| 			<div class="title"> | ||||
| 				<div class="title">{{ metadata.title }}</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="tabs"> | ||||
| 			<button v-for="tab in tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick"> | ||||
| 				<i v-if="tab.icon" class="icon" :class="tab.icon"></i> | ||||
| 				<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> | ||||
| 			</button> | ||||
| 		</div> | ||||
| 	</template> | ||||
| 	<div class="buttons right"> | ||||
| 		<template v-if="actions"> | ||||
| 			<template v-for="action in actions"> | ||||
| 				<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton> | ||||
| 				<button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> | ||||
| 			</template> | ||||
| 		</template> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed, onMounted, onUnmounted, ref, inject } from 'vue'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import { popupMenu } from '@/os'; | ||||
| import { url } from '@/config'; | ||||
| import { scrollToTop } from '@/scripts/scroll'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { globalEvents } from '@/events'; | ||||
| import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	tabs?: { | ||||
| 		title: string; | ||||
| 		active: boolean; | ||||
| 		icon?: string; | ||||
| 		iconOnly?: boolean; | ||||
| 		onClick: () => void; | ||||
| 	}[]; | ||||
| 	actions?: { | ||||
| 		text: string; | ||||
| 		icon: string; | ||||
| 		asFullButton?: boolean; | ||||
| 		handler: (ev: MouseEvent) => void; | ||||
| 	}[]; | ||||
| 	thin?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| const metadata = injectPageMetadata(); | ||||
| 
 | ||||
| const el = ref<HTMLElement>(null); | ||||
| const bg = ref(null); | ||||
| const height = ref(0); | ||||
| const hasTabs = computed(() => { | ||||
| 	return props.tabs && props.tabs.length > 0; | ||||
| }); | ||||
| 
 | ||||
| const showTabsPopup = (ev: MouseEvent) => { | ||||
| 	if (!hasTabs.value) return; | ||||
| 	if (!narrow.value) return; | ||||
| 	ev.preventDefault(); | ||||
| 	ev.stopPropagation(); | ||||
| 	const menu = props.tabs.map(tab => ({ | ||||
| 		text: tab.title, | ||||
| 		icon: tab.icon, | ||||
| 		action: tab.onClick, | ||||
| 	})); | ||||
| 	popupMenu(menu, ev.currentTarget ?? ev.target); | ||||
| }; | ||||
| 
 | ||||
| const preventDrag = (ev: TouchEvent) => { | ||||
| 	ev.stopPropagation(); | ||||
| }; | ||||
| 
 | ||||
| const onClick = () => { | ||||
| 	scrollToTop(el.value, { behavior: 'smooth' }); | ||||
| }; | ||||
| 
 | ||||
| const calcBg = () => { | ||||
| 	const rawBg = metadata?.bg || 'var(--bg)'; | ||||
| 	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); | ||||
| 	tinyBg.setAlpha(0.85); | ||||
| 	bg.value = tinyBg.toRgbString(); | ||||
| }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	calcBg(); | ||||
| 	globalEvents.on('themeChanged', calcBg); | ||||
| }); | ||||
| 
 | ||||
| onUnmounted(() => { | ||||
| 	globalEvents.off('themeChanged', calcBg); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fdidabkc { | ||||
| 	--height: 60px; | ||||
| 	display: flex; | ||||
| 	position: sticky; | ||||
| 	top: var(--stickyTop, 0); | ||||
| 	z-index: 1000; | ||||
| 	width: 100%; | ||||
| 	-webkit-backdrop-filter: var(--blur, blur(15px)); | ||||
| 	backdrop-filter: var(--blur, blur(15px)); | ||||
| 
 | ||||
| 	> .buttons { | ||||
| 		--margin: 8px; | ||||
| 		display: flex; | ||||
|     align-items: center; | ||||
| 		height: var(--height); | ||||
| 		margin: 0 var(--margin); | ||||
| 
 | ||||
| 		&.right { | ||||
| 			margin-left: auto; | ||||
| 		} | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			width: var(--height); | ||||
| 		} | ||||
| 
 | ||||
| 		> .button { | ||||
| 			display: flex; | ||||
| 			align-items: center; | ||||
| 			justify-content: center; | ||||
| 			height: calc(var(--height) - (var(--margin) * 2)); | ||||
| 			width: calc(var(--height) - (var(--margin) * 2)); | ||||
| 			box-sizing: border-box; | ||||
| 			position: relative; | ||||
| 			border-radius: 5px; | ||||
| 
 | ||||
| 			&:hover { | ||||
| 				background: rgba(0, 0, 0, 0.05); | ||||
| 			} | ||||
| 
 | ||||
| 			&.highlighted { | ||||
| 				color: var(--accent); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .fullButton { | ||||
| 			& + .fullButton { | ||||
| 				margin-left: 12px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .titleContainer { | ||||
| 		display: flex; | ||||
| 		align-items: center; | ||||
| 		max-width: 400px; | ||||
| 		overflow: auto; | ||||
| 		white-space: nowrap; | ||||
| 		text-align: left; | ||||
| 		font-weight: bold; | ||||
| 		flex-shrink: 0; | ||||
| 		margin-left: 24px; | ||||
| 
 | ||||
| 		> .avatar { | ||||
| 			$size: 32px; | ||||
| 			display: inline-block; | ||||
| 			width: $size; | ||||
| 			height: $size; | ||||
| 			vertical-align: bottom; | ||||
| 			margin: 0 8px; | ||||
| 			pointer-events: none; | ||||
| 		} | ||||
| 
 | ||||
| 		> .icon { | ||||
| 			margin-right: 8px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .title { | ||||
| 			min-width: 0; | ||||
| 			overflow: hidden; | ||||
| 			text-overflow: ellipsis; | ||||
| 			white-space: nowrap; | ||||
| 			line-height: 1.1; | ||||
| 
 | ||||
| 			> .subtitle { | ||||
| 				opacity: 0.6; | ||||
| 				font-size: 0.8em; | ||||
| 				font-weight: normal; | ||||
| 				white-space: nowrap; | ||||
| 				overflow: hidden; | ||||
| 				text-overflow: ellipsis; | ||||
| 
 | ||||
| 				&.activeTab { | ||||
| 					text-align: center; | ||||
| 
 | ||||
| 					> .chevron { | ||||
| 						display: inline-block; | ||||
| 						margin-left: 6px; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .tabs { | ||||
| 		margin-left: 16px; | ||||
| 		font-size: 0.8em; | ||||
| 		overflow: auto; | ||||
| 		white-space: nowrap; | ||||
| 
 | ||||
| 		> .tab { | ||||
| 			display: inline-block; | ||||
| 			position: relative; | ||||
| 			padding: 0 10px; | ||||
| 			height: 100%; | ||||
| 			font-weight: normal; | ||||
| 			opacity: 0.7; | ||||
| 
 | ||||
| 			&:hover { | ||||
| 				opacity: 1; | ||||
| 			} | ||||
| 
 | ||||
| 			&.active { | ||||
| 				opacity: 1; | ||||
| 
 | ||||
| 				&:after { | ||||
| 					content: ""; | ||||
| 					display: block; | ||||
| 					position: absolute; | ||||
| 					bottom: 0; | ||||
| 					left: 0; | ||||
| 					right: 0; | ||||
| 					margin: 0 auto; | ||||
| 					width: 100%; | ||||
| 					height: 3px; | ||||
| 					background: var(--accent); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			> .icon + .title { | ||||
| 				margin-left: 8px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -1,28 +1,31 @@ | |||
| <template> | ||||
| <div class="lcixvhis"> | ||||
| 	<div class="_section reports"> | ||||
| 		<div class="_content"> | ||||
| 			<div class="inputs" style="display: flex;"> | ||||
| 				<MkSelect v-model="state" style="margin: 0; flex: 1;"> | ||||
| 					<template #label>{{ $ts.state }}</template> | ||||
| 					<option value="all">{{ $ts.all }}</option> | ||||
| 					<option value="unresolved">{{ $ts.unresolved }}</option> | ||||
| 					<option value="resolved">{{ $ts.resolved }}</option> | ||||
| 				</MkSelect> | ||||
| 				<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;"> | ||||
| 					<template #label>{{ $ts.reporteeOrigin }}</template> | ||||
| 					<option value="combined">{{ $ts.all }}</option> | ||||
| 					<option value="local">{{ $ts.local }}</option> | ||||
| 					<option value="remote">{{ $ts.remote }}</option> | ||||
| 				</MkSelect> | ||||
| 				<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;"> | ||||
| 					<template #label>{{ $ts.reporterOrigin }}</template> | ||||
| 					<option value="combined">{{ $ts.all }}</option> | ||||
| 					<option value="local">{{ $ts.local }}</option> | ||||
| 					<option value="remote">{{ $ts.remote }}</option> | ||||
| 				</MkSelect> | ||||
| 			</div> | ||||
| 			<!-- TODO | ||||
| <MkStickyContainer> | ||||
| 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="900"> | ||||
| 		<div class="lcixvhis"> | ||||
| 			<div class="_section reports"> | ||||
| 				<div class="_content"> | ||||
| 					<div class="inputs" style="display: flex;"> | ||||
| 						<MkSelect v-model="state" style="margin: 0; flex: 1;"> | ||||
| 							<template #label>{{ $ts.state }}</template> | ||||
| 							<option value="all">{{ $ts.all }}</option> | ||||
| 							<option value="unresolved">{{ $ts.unresolved }}</option> | ||||
| 							<option value="resolved">{{ $ts.resolved }}</option> | ||||
| 						</MkSelect> | ||||
| 						<MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;"> | ||||
| 							<template #label>{{ $ts.reporteeOrigin }}</template> | ||||
| 							<option value="combined">{{ $ts.all }}</option> | ||||
| 							<option value="local">{{ $ts.local }}</option> | ||||
| 							<option value="remote">{{ $ts.remote }}</option> | ||||
| 						</MkSelect> | ||||
| 						<MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;"> | ||||
| 							<template #label>{{ $ts.reporterOrigin }}</template> | ||||
| 							<option value="combined">{{ $ts.all }}</option> | ||||
| 							<option value="local">{{ $ts.local }}</option> | ||||
| 							<option value="remote">{{ $ts.remote }}</option> | ||||
| 						</MkSelect> | ||||
| 					</div> | ||||
| 					<!-- TODO | ||||
| 			<div class="inputs" style="display: flex; padding-top: 1.2em;"> | ||||
| 				<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false"> | ||||
| 					<span>{{ $ts.username }}</span> | ||||
|  | @ -33,24 +36,27 @@ | |||
| 			</div> | ||||
| 			--> | ||||
| 
 | ||||
| 			<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);"> | ||||
| 				<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> | ||||
| 			</MkPagination> | ||||
| 					<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);"> | ||||
| 						<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> | ||||
| 					</MkPagination> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed } from 'vue'; | ||||
| 
 | ||||
| import XHeader from './_header_.vue'; | ||||
| import MkInput from '@/components/form/input.vue'; | ||||
| import MkSelect from '@/components/form/select.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import XAbuseReport from '@/components/abuse-report.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let reports = $ref<InstanceType<typeof MkPagination>>(); | ||||
| 
 | ||||
|  | @ -74,12 +80,14 @@ function resolved(reportId) { | |||
| 	reports.removeItem(item => item.id === reportId); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.abuseReports, | ||||
| 		icon: 'fas fa-exclamation-circle', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.abuseReports, | ||||
| 	icon: 'fas fa-exclamation-circle', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,21 +1,23 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="900"> | ||||
| 	<div class="uqshojas"> | ||||
| 		<div v-for="ad in ads" class="_panel _formRoot ad"> | ||||
| 			<MkAd v-if="ad.url" :specify="ad"/> | ||||
| 			<MkInput v-model="ad.url" type="url" class="_formBlock"> | ||||
| 				<template #label>URL</template> | ||||
| 			</MkInput> | ||||
| 			<MkInput v-model="ad.imageUrl" class="_formBlock"> | ||||
| 				<template #label>{{ i18n.ts.imageUrl }}</template> | ||||
| 			</MkInput> | ||||
| 			<FormRadios v-model="ad.place" class="_formBlock"> | ||||
| 				<template #label>Form</template> | ||||
| 				<option value="square">square</option> | ||||
| 				<option value="horizontal">horizontal</option> | ||||
| 				<option value="horizontal-big">horizontal-big</option> | ||||
| 			</FormRadios> | ||||
| 			<!-- | ||||
| <MkStickyContainer> | ||||
| 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="900"> | ||||
| 		<div class="uqshojas"> | ||||
| 			<div v-for="ad in ads" class="_panel _formRoot ad"> | ||||
| 				<MkAd v-if="ad.url" :specify="ad"/> | ||||
| 				<MkInput v-model="ad.url" type="url" class="_formBlock"> | ||||
| 					<template #label>URL</template> | ||||
| 				</MkInput> | ||||
| 				<MkInput v-model="ad.imageUrl" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.imageUrl }}</template> | ||||
| 				</MkInput> | ||||
| 				<FormRadios v-model="ad.place" class="_formBlock"> | ||||
| 					<template #label>Form</template> | ||||
| 					<option value="square">square</option> | ||||
| 					<option value="horizontal">horizontal</option> | ||||
| 					<option value="horizontal-big">horizontal-big</option> | ||||
| 				</FormRadios> | ||||
| 				<!-- | ||||
| 			<div style="margin: 32px 0;"> | ||||
| 				{{ i18n.ts.priority }} | ||||
| 				<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio> | ||||
|  | @ -23,36 +25,38 @@ | |||
| 				<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio> | ||||
| 			</div> | ||||
| 			--> | ||||
| 			<FormSplit> | ||||
| 				<MkInput v-model="ad.ratio" type="number"> | ||||
| 					<template #label>{{ i18n.ts.ratio }}</template> | ||||
| 				</MkInput> | ||||
| 				<MkInput v-model="ad.expiresAt" type="date"> | ||||
| 					<template #label>{{ i18n.ts.expiration }}</template> | ||||
| 				</MkInput> | ||||
| 			</FormSplit> | ||||
| 			<MkTextarea v-model="ad.memo" class="_formBlock"> | ||||
| 				<template #label>{{ i18n.ts.memo }}</template> | ||||
| 			</MkTextarea> | ||||
| 			<div class="buttons _formBlock"> | ||||
| 				<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> | ||||
| 				<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> | ||||
| 				<FormSplit> | ||||
| 					<MkInput v-model="ad.ratio" type="number"> | ||||
| 						<template #label>{{ i18n.ts.ratio }}</template> | ||||
| 					</MkInput> | ||||
| 					<MkInput v-model="ad.expiresAt" type="date"> | ||||
| 						<template #label>{{ i18n.ts.expiration }}</template> | ||||
| 					</MkInput> | ||||
| 				</FormSplit> | ||||
| 				<MkTextarea v-model="ad.memo" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.memo }}</template> | ||||
| 				</MkTextarea> | ||||
| 				<div class="buttons _formBlock"> | ||||
| 					<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> | ||||
| 					<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkInput from '@/components/form/input.vue'; | ||||
| import MkTextarea from '@/components/form/textarea.vue'; | ||||
| import FormRadios from '@/components/form/radios.vue'; | ||||
| import FormSplit from '@/components/form/split.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let ads: any[] = $ref([]); | ||||
| 
 | ||||
|  | @ -81,7 +85,7 @@ function remove(ad) { | |||
| 		if (canceled) return; | ||||
| 		ads = ads.filter(x => x !== ad); | ||||
| 		os.apiWithDialog('admin/ad/delete', { | ||||
| 			id: ad.id | ||||
| 			id: ad.id, | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
|  | @ -90,28 +94,29 @@ function save(ad) { | |||
| 	if (ad.id == null) { | ||||
| 		os.apiWithDialog('admin/ad/create', { | ||||
| 			...ad, | ||||
| 			expiresAt: new Date(ad.expiresAt).getTime() | ||||
| 			expiresAt: new Date(ad.expiresAt).getTime(), | ||||
| 		}); | ||||
| 	} else { | ||||
| 		os.apiWithDialog('admin/ad/update', { | ||||
| 			...ad, | ||||
| 			expiresAt: new Date(ad.expiresAt).getTime() | ||||
| 			expiresAt: new Date(ad.expiresAt).getTime(), | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.ads, | ||||
| 		icon: 'fas fa-audio-description', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-plus', | ||||
| 			text: i18n.ts.add, | ||||
| 			handler: add, | ||||
| 		}], | ||||
| 	} | ||||
| const headerActions = $computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	icon: 'fas fa-plus', | ||||
| 	text: i18n.ts.add, | ||||
| 	handler: add, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.ads, | ||||
| 	icon: 'fas fa-audio-description', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,34 +1,40 @@ | |||
| <template> | ||||
| <div class="ztgjmzrw"> | ||||
| 	<section v-for="announcement in announcements" class="_card _gap announcements"> | ||||
| 		<div class="_content announcement"> | ||||
| 			<MkInput v-model="announcement.title"> | ||||
| 				<template #label>{{ i18n.ts.title }}</template> | ||||
| 			</MkInput> | ||||
| 			<MkTextarea v-model="announcement.text"> | ||||
| 				<template #label>{{ i18n.ts.text }}</template> | ||||
| 			</MkTextarea> | ||||
| 			<MkInput v-model="announcement.imageUrl"> | ||||
| 				<template #label>{{ i18n.ts.imageUrl }}</template> | ||||
| 			</MkInput> | ||||
| 			<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p> | ||||
| 			<div class="buttons"> | ||||
| 				<MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> | ||||
| 				<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> | ||||
| 			</div> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="900"> | ||||
| 		<div class="ztgjmzrw"> | ||||
| 			<section v-for="announcement in announcements" class="_card _gap announcements"> | ||||
| 				<div class="_content announcement"> | ||||
| 					<MkInput v-model="announcement.title"> | ||||
| 						<template #label>{{ i18n.ts.title }}</template> | ||||
| 					</MkInput> | ||||
| 					<MkTextarea v-model="announcement.text"> | ||||
| 						<template #label>{{ i18n.ts.text }}</template> | ||||
| 					</MkTextarea> | ||||
| 					<MkInput v-model="announcement.imageUrl"> | ||||
| 						<template #label>{{ i18n.ts.imageUrl }}</template> | ||||
| 					</MkInput> | ||||
| 					<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p> | ||||
| 					<div class="buttons"> | ||||
| 						<MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> | ||||
| 						<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</section> | ||||
| 		</div> | ||||
| 	</section> | ||||
| </div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkInput from '@/components/form/input.vue'; | ||||
| import MkTextarea from '@/components/form/textarea.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let announcements: any[] = $ref([]); | ||||
| 
 | ||||
|  | @ -41,7 +47,7 @@ function add() { | |||
| 		id: null, | ||||
| 		title: '', | ||||
| 		text: '', | ||||
| 		imageUrl: null | ||||
| 		imageUrl: null, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | @ -61,41 +67,42 @@ function save(announcement) { | |||
| 		os.api('admin/announcements/create', announcement).then(() => { | ||||
| 			os.alert({ | ||||
| 				type: 'success', | ||||
| 				text: i18n.ts.saved | ||||
| 				text: i18n.ts.saved, | ||||
| 			}); | ||||
| 		}).catch(err => { | ||||
| 			os.alert({ | ||||
| 				type: 'error', | ||||
| 				text: err | ||||
| 				text: err, | ||||
| 			}); | ||||
| 		}); | ||||
| 	} else { | ||||
| 		os.api('admin/announcements/update', announcement).then(() => { | ||||
| 			os.alert({ | ||||
| 				type: 'success', | ||||
| 				text: i18n.ts.saved | ||||
| 				text: i18n.ts.saved, | ||||
| 			}); | ||||
| 		}).catch(err => { | ||||
| 			os.alert({ | ||||
| 				type: 'error', | ||||
| 				text: err | ||||
| 				text: err, | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.announcements, | ||||
| 		icon: 'fas fa-broadcast-tower', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-plus', | ||||
| 			text: i18n.ts.add, | ||||
| 			handler: add, | ||||
| 		}], | ||||
| 	} | ||||
| const headerActions = $computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	icon: 'fas fa-plus', | ||||
| 	text: i18n.ts.add, | ||||
| 	handler: add, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.announcements, | ||||
| 	icon: 'fas fa-broadcast-tower', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -51,7 +51,6 @@ import FormButton from '@/components/ui/button.vue'; | |||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import FormSlot from '@/components/form/slot.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { fetchInstance } from '@/instance'; | ||||
| 
 | ||||
| const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue')); | ||||
|  |  | |||
|  | @ -1,12 +1,13 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> | ||||
| 	<FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory"> | ||||
| 		<MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;"> | ||||
| 			<template #key>{{ table[0] }}</template> | ||||
| 			<template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template> | ||||
| 		</MkKeyValue> | ||||
| 	</FormSuspense> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -14,18 +15,20 @@ import { } from 'vue'; | |||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import MkKeyValue from '@/components/key-value.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import bytes from '@/filters/bytes'; | ||||
| import number from '@/filters/number'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.database, | ||||
| 		icon: 'fas fa-database', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.database, | ||||
| 	icon: 'fas fa-database', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,49 +1,53 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<div class="_formRoot"> | ||||
| 			<FormSwitch v-model="enableEmail" class="_formBlock"> | ||||
| 				<template #label>{{ i18n.ts.enableEmail }}</template> | ||||
| 				<template #caption>{{ i18n.ts.emailConfigInfo }}</template> | ||||
| 			</FormSwitch> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 		<FormSuspense :p="init"> | ||||
| 			<div class="_formRoot"> | ||||
| 				<FormSwitch v-model="enableEmail" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.enableEmail }}</template> | ||||
| 					<template #caption>{{ i18n.ts.emailConfigInfo }}</template> | ||||
| 				</FormSwitch> | ||||
| 
 | ||||
| 			<template v-if="enableEmail"> | ||||
| 				<FormInput v-model="email" type="email" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.emailAddress }}</template> | ||||
| 				</FormInput> | ||||
| 				<template v-if="enableEmail"> | ||||
| 					<FormInput v-model="email" type="email" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.emailAddress }}</template> | ||||
| 					</FormInput> | ||||
| 
 | ||||
| 				<FormSection> | ||||
| 					<template #label>{{ i18n.ts.smtpConfig }}</template> | ||||
| 					<FormSplit :min-width="280"> | ||||
| 						<FormInput v-model="smtpHost" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.smtpHost }}</template> | ||||
| 						</FormInput> | ||||
| 						<FormInput v-model="smtpPort" type="number" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.smtpPort }}</template> | ||||
| 						</FormInput> | ||||
| 					</FormSplit> | ||||
| 					<FormSplit :min-width="280"> | ||||
| 						<FormInput v-model="smtpUser" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.smtpUser }}</template> | ||||
| 						</FormInput> | ||||
| 						<FormInput v-model="smtpPass" type="password" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.smtpPass }}</template> | ||||
| 						</FormInput> | ||||
| 					</FormSplit> | ||||
| 					<FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo> | ||||
| 					<FormSwitch v-model="smtpSecure" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.smtpSecure }}</template> | ||||
| 						<template #caption>{{ i18n.ts.smtpSecureInfo }}</template> | ||||
| 					</FormSwitch> | ||||
| 				</FormSection> | ||||
| 			</template> | ||||
| 		</div> | ||||
| 	</FormSuspense> | ||||
| </MkSpacer> | ||||
| 					<FormSection> | ||||
| 						<template #label>{{ i18n.ts.smtpConfig }}</template> | ||||
| 						<FormSplit :min-width="280"> | ||||
| 							<FormInput v-model="smtpHost" class="_formBlock"> | ||||
| 								<template #label>{{ i18n.ts.smtpHost }}</template> | ||||
| 							</FormInput> | ||||
| 							<FormInput v-model="smtpPort" type="number" class="_formBlock"> | ||||
| 								<template #label>{{ i18n.ts.smtpPort }}</template> | ||||
| 							</FormInput> | ||||
| 						</FormSplit> | ||||
| 						<FormSplit :min-width="280"> | ||||
| 							<FormInput v-model="smtpUser" class="_formBlock"> | ||||
| 								<template #label>{{ i18n.ts.smtpUser }}</template> | ||||
| 							</FormInput> | ||||
| 							<FormInput v-model="smtpPass" type="password" class="_formBlock"> | ||||
| 								<template #label>{{ i18n.ts.smtpPass }}</template> | ||||
| 							</FormInput> | ||||
| 						</FormSplit> | ||||
| 						<FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo> | ||||
| 						<FormSwitch v-model="smtpSecure" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.smtpSecure }}</template> | ||||
| 							<template #caption>{{ i18n.ts.smtpSecureInfo }}</template> | ||||
| 						</FormSwitch> | ||||
| 					</FormSection> | ||||
| 				</template> | ||||
| 			</div> | ||||
| 		</FormSuspense> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import FormInput from '@/components/form/input.vue'; | ||||
| import FormInfo from '@/components/ui/info.vue'; | ||||
|  | @ -51,9 +55,9 @@ import FormSuspense from '@/components/form/suspense.vue'; | |||
| import FormSplit from '@/components/form/split.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { fetchInstance, instance } from '@/instance'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let enableEmail: boolean = $ref(false); | ||||
| let email: any = $ref(null); | ||||
|  | @ -78,13 +82,13 @@ async function testEmail() { | |||
| 	const { canceled, result: destination } = await os.inputText({ | ||||
| 		title: i18n.ts.destination, | ||||
| 		type: 'email', | ||||
| 		placeholder: instance.maintainerEmail | ||||
| 		placeholder: instance.maintainerEmail, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 	os.apiWithDialog('admin/send-email', { | ||||
| 		to: destination, | ||||
| 		subject: 'Test email', | ||||
| 		text: 'Yo' | ||||
| 		text: 'Yo', | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | @ -102,21 +106,22 @@ function save() { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.emailServer, | ||||
| 		icon: 'fas fa-envelope', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			asFullButton: true, | ||||
| 			text: i18n.ts.testEmail, | ||||
| 			handler: testEmail, | ||||
| 		}, { | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-check', | ||||
| 			text: i18n.ts.save, | ||||
| 			handler: save, | ||||
| 		}], | ||||
| 	} | ||||
| const headerActions = $computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	text: i18n.ts.testEmail, | ||||
| 	handler: testEmail, | ||||
| }, { | ||||
| 	asFullButton: true, | ||||
| 	icon: 'fas fa-check', | ||||
| 	text: i18n.ts.save, | ||||
| 	handler: save, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.emailServer, | ||||
| 	icon: 'fas fa-envelope', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,69 +1,75 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="900"> | ||||
| 	<div class="ogwlenmc"> | ||||
| 		<div v-if="tab === 'local'" class="local"> | ||||
| 			<MkInput v-model="query" :debounce="true" type="search"> | ||||
| 				<template #prefix><i class="fas fa-search"></i></template> | ||||
| 				<template #label>{{ $ts.search }}</template> | ||||
| 			</MkInput> | ||||
| 			<MkSwitch v-model="selectMode" style="margin: 8px 0;"> | ||||
| 				<template #label>Select mode</template> | ||||
| 			</MkSwitch> | ||||
| 			<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> | ||||
| 				<MkButton inline @click="selectAll">Select all</MkButton> | ||||
| 				<MkButton inline @click="setCategoryBulk">Set category</MkButton> | ||||
| 				<MkButton inline @click="addTagBulk">Add tag</MkButton> | ||||
| 				<MkButton inline @click="removeTagBulk">Remove tag</MkButton> | ||||
| 				<MkButton inline @click="setTagBulk">Set tag</MkButton> | ||||
| 				<MkButton inline danger @click="delBulk">Delete</MkButton> | ||||
| 			</div> | ||||
| 			<MkPagination ref="emojisPaginationComponent" :pagination="pagination"> | ||||
| 				<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | ||||
| 				<template v-slot="{items}"> | ||||
| 					<div class="ldhfsamy"> | ||||
| 						<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)"> | ||||
| 							<img :src="emoji.url" class="img" :alt="emoji.name"/> | ||||
| 							<div class="body"> | ||||
| 								<div class="name _monospace">{{ emoji.name }}</div> | ||||
| 								<div class="info">{{ emoji.category }}</div> | ||||
| 							</div> | ||||
| 						</button> | ||||
| <div> | ||||
| 	<MkStickyContainer> | ||||
| 		<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="900"> | ||||
| 			<div class="ogwlenmc"> | ||||
| 				<div v-if="tab === 'local'" class="local"> | ||||
| 					<MkInput v-model="query" :debounce="true" type="search"> | ||||
| 						<template #prefix><i class="fas fa-search"></i></template> | ||||
| 						<template #label>{{ $ts.search }}</template> | ||||
| 					</MkInput> | ||||
| 					<MkSwitch v-model="selectMode" style="margin: 8px 0;"> | ||||
| 						<template #label>Select mode</template> | ||||
| 					</MkSwitch> | ||||
| 					<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> | ||||
| 						<MkButton inline @click="selectAll">Select all</MkButton> | ||||
| 						<MkButton inline @click="setCategoryBulk">Set category</MkButton> | ||||
| 						<MkButton inline @click="addTagBulk">Add tag</MkButton> | ||||
| 						<MkButton inline @click="removeTagBulk">Remove tag</MkButton> | ||||
| 						<MkButton inline @click="setTagBulk">Set tag</MkButton> | ||||
| 						<MkButton inline danger @click="delBulk">Delete</MkButton> | ||||
| 					</div> | ||||
| 				</template> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 					<MkPagination ref="emojisPaginationComponent" :pagination="pagination"> | ||||
| 						<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | ||||
| 						<template #default="{items}"> | ||||
| 							<div class="ldhfsamy"> | ||||
| 								<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)"> | ||||
| 									<img :src="emoji.url" class="img" :alt="emoji.name"/> | ||||
| 									<div class="body"> | ||||
| 										<div class="name _monospace">{{ emoji.name }}</div> | ||||
| 										<div class="info">{{ emoji.category }}</div> | ||||
| 									</div> | ||||
| 								</button> | ||||
| 							</div> | ||||
| 						</template> | ||||
| 					</MkPagination> | ||||
| 				</div> | ||||
| 
 | ||||
| 		<div v-else-if="tab === 'remote'" class="remote"> | ||||
| 			<FormSplit> | ||||
| 				<MkInput v-model="queryRemote" :debounce="true" type="search"> | ||||
| 					<template #prefix><i class="fas fa-search"></i></template> | ||||
| 					<template #label>{{ $ts.search }}</template> | ||||
| 				</MkInput> | ||||
| 				<MkInput v-model="host" :debounce="true"> | ||||
| 					<template #label>{{ $ts.host }}</template> | ||||
| 				</MkInput> | ||||
| 			</FormSplit> | ||||
| 			<MkPagination :pagination="remotePagination"> | ||||
| 				<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | ||||
| 				<template v-slot="{items}"> | ||||
| 					<div class="ldhfsamy"> | ||||
| 						<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)"> | ||||
| 							<img :src="emoji.url" class="img" :alt="emoji.name"/> | ||||
| 							<div class="body"> | ||||
| 								<div class="name _monospace">{{ emoji.name }}</div> | ||||
| 								<div class="info">{{ emoji.host }}</div> | ||||
| 				<div v-else-if="tab === 'remote'" class="remote"> | ||||
| 					<FormSplit> | ||||
| 						<MkInput v-model="queryRemote" :debounce="true" type="search"> | ||||
| 							<template #prefix><i class="fas fa-search"></i></template> | ||||
| 							<template #label>{{ $ts.search }}</template> | ||||
| 						</MkInput> | ||||
| 						<MkInput v-model="host" :debounce="true"> | ||||
| 							<template #label>{{ $ts.host }}</template> | ||||
| 						</MkInput> | ||||
| 					</FormSplit> | ||||
| 					<MkPagination :pagination="remotePagination"> | ||||
| 						<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | ||||
| 						<template #default="{items}"> | ||||
| 							<div class="ldhfsamy"> | ||||
| 								<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)"> | ||||
| 									<img :src="emoji.url" class="img" :alt="emoji.name"/> | ||||
| 									<div class="body"> | ||||
| 										<div class="name _monospace">{{ emoji.name }}</div> | ||||
| 										<div class="info">{{ emoji.host }}</div> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</template> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 						</template> | ||||
| 					</MkPagination> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</MkSpacer> | ||||
| 	</MkStickyContainer> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkInput from '@/components/form/input.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
|  | @ -72,8 +78,8 @@ import MkSwitch from '@/components/form/switch.vue'; | |||
| import FormSplit from '@/components/form/split.vue'; | ||||
| import { selectFile, selectFiles } from '@/scripts/select-file'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>(); | ||||
| 
 | ||||
|  | @ -131,13 +137,13 @@ const add = async (ev: MouseEvent) => { | |||
| 
 | ||||
| const edit = (emoji) => { | ||||
| 	os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { | ||||
| 		emoji: emoji | ||||
| 		emoji: emoji, | ||||
| 	}, { | ||||
| 		done: result => { | ||||
| 			if (result.updated) { | ||||
| 				emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ | ||||
| 					...oldEmoji, | ||||
| 					...result.updated | ||||
| 					...result.updated, | ||||
| 				})); | ||||
| 			} else if (result.deleted) { | ||||
| 				emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id); | ||||
|  | @ -159,7 +165,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => { | |||
| 	}, { | ||||
| 		text: i18n.ts.import, | ||||
| 		icon: 'fas fa-plus', | ||||
| 		action: () => { im(emoji); } | ||||
| 		action: () => { im(emoji); }, | ||||
| 	}], ev.currentTarget ?? ev.target); | ||||
| }; | ||||
| 
 | ||||
|  | @ -181,7 +187,7 @@ const menu = (ev: MouseEvent) => { | |||
| 					text: err.message, | ||||
| 				}); | ||||
| 			}); | ||||
| 		} | ||||
| 		}, | ||||
| 	}, { | ||||
| 		icon: 'fas fa-upload', | ||||
| 		text: i18n.ts.import, | ||||
|  | @ -201,7 +207,7 @@ const menu = (ev: MouseEvent) => { | |||
| 					text: err.message, | ||||
| 				}); | ||||
| 			}); | ||||
| 		} | ||||
| 		}, | ||||
| 	}], ev.currentTarget ?? ev.target); | ||||
| }; | ||||
| 
 | ||||
|  | @ -265,31 +271,31 @@ const delBulk = async () => { | |||
| 	emojisPaginationComponent.value.reload(); | ||||
| }; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.ts.customEmojis, | ||||
| 		icon: 'fas fa-laugh', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-plus', | ||||
| 			text: i18n.ts.addEmoji, | ||||
| 			handler: add, | ||||
| 		}, { | ||||
| 			icon: 'fas fa-ellipsis-h', | ||||
| 			handler: menu, | ||||
| 		}], | ||||
| 		tabs: [{ | ||||
| 			active: tab.value === 'local', | ||||
| 			title: i18n.ts.local, | ||||
| 			onClick: () => { tab.value = 'local'; }, | ||||
| 		}, { | ||||
| 			active: tab.value === 'remote', | ||||
| 			title: i18n.ts.remote, | ||||
| 			onClick: () => { tab.value = 'remote'; }, | ||||
| 		},] | ||||
| 	})), | ||||
| }); | ||||
| const headerActions = $computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	icon: 'fas fa-plus', | ||||
| 	text: i18n.ts.addEmoji, | ||||
| 	handler: add, | ||||
| }, { | ||||
| 	icon: 'fas fa-ellipsis-h', | ||||
| 	handler: menu, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => [{ | ||||
| 	active: tab.value === 'local', | ||||
| 	title: i18n.ts.local, | ||||
| 	onClick: () => { tab.value = 'local'; }, | ||||
| }, { | ||||
| 	active: tab.value === 'remote', | ||||
| 	title: i18n.ts.remote, | ||||
| 	onClick: () => { tab.value = 'remote'; }, | ||||
| }]); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: i18n.ts.customEmojis, | ||||
| 	icon: 'fas fa-laugh', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,50 +1,58 @@ | |||
| <template> | ||||
| <div class="xrmjdkdw"> | ||||
| 	<div> | ||||
| 		<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> | ||||
| 			<MkSelect v-model="origin" style="margin: 0; flex: 1;"> | ||||
| 				<template #label>{{ $ts.instance }}</template> | ||||
| 				<option value="combined">{{ $ts.all }}</option> | ||||
| 				<option value="local">{{ $ts.local }}</option> | ||||
| 				<option value="remote">{{ $ts.remote }}</option> | ||||
| 			</MkSelect> | ||||
| 			<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'"> | ||||
| 				<template #label>{{ $ts.host }}</template> | ||||
| 			</MkInput> | ||||
| 		</div> | ||||
| 		<div class="inputs" style="display: flex; padding-top: 1.2em;"> | ||||
| 			<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> | ||||
| 				<template #label>MIME type</template> | ||||
| 			</MkInput> | ||||
| 		</div> | ||||
| 		<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }"> | ||||
| 			<button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _panel _button" @click="show(file, $event)"> | ||||
| 				<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||
| 				<div v-if="viewMode === 'list'" class="body"> | ||||
| 					<div> | ||||
| 						<small style="opacity: 0.7;">{{ file.name }}</small> | ||||
| <div> | ||||
| 	<MkStickyContainer> | ||||
| 		<template #header><XHeader :actions="headerActions"/></template> | ||||
| 		<MkSpacer :content-max="900"> | ||||
| 			<div class="xrmjdkdw"> | ||||
| 				<div> | ||||
| 					<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> | ||||
| 						<MkSelect v-model="origin" style="margin: 0; flex: 1;"> | ||||
| 							<template #label>{{ $ts.instance }}</template> | ||||
| 							<option value="combined">{{ $ts.all }}</option> | ||||
| 							<option value="local">{{ $ts.local }}</option> | ||||
| 							<option value="remote">{{ $ts.remote }}</option> | ||||
| 						</MkSelect> | ||||
| 						<MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'"> | ||||
| 							<template #label>{{ $ts.host }}</template> | ||||
| 						</MkInput> | ||||
| 					</div> | ||||
| 					<div> | ||||
| 						<MkAcct v-if="file.user" :user="file.user"/> | ||||
| 						<div v-else>{{ $ts.system }}</div> | ||||
| 					</div> | ||||
| 					<div> | ||||
| 						<span style="margin-right: 1em;">{{ file.type }}</span> | ||||
| 						<span>{{ bytes(file.size) }}</span> | ||||
| 					</div> | ||||
| 					<div> | ||||
| 						<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> | ||||
| 					<div class="inputs" style="display: flex; padding-top: 1.2em;"> | ||||
| 						<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> | ||||
| 							<template #label>MIME type</template> | ||||
| 						</MkInput> | ||||
| 					</div> | ||||
| 					<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }"> | ||||
| 						<button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _button" @click="show(file, $event)"> | ||||
| 							<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||
| 							<div v-if="viewMode === 'list'" class="body"> | ||||
| 								<div> | ||||
| 									<small style="opacity: 0.7;">{{ file.name }}</small> | ||||
| 								</div> | ||||
| 								<div> | ||||
| 									<MkAcct v-if="file.user" :user="file.user"/> | ||||
| 									<div v-else>{{ $ts.system }}</div> | ||||
| 								</div> | ||||
| 								<div> | ||||
| 									<span style="margin-right: 1em;">{{ file.type }}</span> | ||||
| 									<span>{{ bytes(file.size) }}</span> | ||||
| 								</div> | ||||
| 								<div> | ||||
| 									<span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</button> | ||||
| 					</MkPagination> | ||||
| 				</div> | ||||
| 			</button> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| 			</div> | ||||
| 		</MkSpacer> | ||||
| 	</MkStickyContainer> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineAsyncComponent } from 'vue'; | ||||
| import * as Acct from 'misskey-js/built/acct'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkInput from '@/components/form/input.vue'; | ||||
| import MkSelect from '@/components/form/select.vue'; | ||||
|  | @ -53,8 +61,8 @@ import MkContainer from '@/components/ui/container.vue'; | |||
| import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; | ||||
| import bytes from '@/filters/bytes'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let origin = $ref('local'); | ||||
| let type = $ref(null); | ||||
|  | @ -82,7 +90,7 @@ function clear() { | |||
| } | ||||
| 
 | ||||
| function show(file) { | ||||
| 	os.pageWindow(`/admin-file/${file.id}`); | ||||
| 	os.pageWindow(`/admin/file/${file.id}`); | ||||
| } | ||||
| 
 | ||||
| async function find() { | ||||
|  | @ -104,22 +112,23 @@ async function find() { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.ts.files, | ||||
| 		icon: 'fas fa-cloud', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			text: i18n.ts.lookup, | ||||
| 			icon: 'fas fa-search', | ||||
| 			handler: find, | ||||
| 		}, { | ||||
| 			text: i18n.ts.clearCachedFiles, | ||||
| 			icon: 'fas fa-trash-alt', | ||||
| 			handler: clear, | ||||
| 		}], | ||||
| 	})), | ||||
| }); | ||||
| const headerActions = $computed(() => [{ | ||||
| 	text: i18n.ts.lookup, | ||||
| 	icon: 'fas fa-search', | ||||
| 	handler: find, | ||||
| }, { | ||||
| 	text: i18n.ts.clearCachedFiles, | ||||
| 	icon: 'fas fa-trash-alt', | ||||
| 	handler: clear, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: i18n.ts.files, | ||||
| 	icon: 'fas fa-cloud', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,8 +1,6 @@ | |||
| <template> | ||||
| <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> | ||||
| 	<div v-if="!narrow || initialPage == null" class="nav"> | ||||
| 		<MkHeader :info="header"></MkHeader> | ||||
| 	 | ||||
| 	<div v-if="!narrow || initialPage == null" class="nav">	 | ||||
| 		<MkSpacer :content-max="700" :margin-min="16"> | ||||
| 			<div class="lxpfedzu"> | ||||
| 				<div class="banner"> | ||||
|  | @ -17,29 +15,26 @@ | |||
| 		</MkSpacer> | ||||
| 	</div> | ||||
| 	<div v-if="!(narrow && initialPage == null)" class="main"> | ||||
| 		<MkStickyContainer> | ||||
| 			<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template> | ||||
| 			<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/> | ||||
| 		</MkStickyContainer> | ||||
| 		<component :is="component" :key="initialPage" v-bind="pageProps"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { defineAsyncComponent, nextTick, onMounted, onUnmounted, provide, watch } from 'vue'; | ||||
| import { defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, watch } from 'vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import MkSuperMenu from '@/components/ui/super-menu.vue'; | ||||
| import MkInfo from '@/components/ui/info.vue'; | ||||
| import { scroll } from '@/scripts/scroll'; | ||||
| import { instance } from '@/instance'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import * as os from '@/os'; | ||||
| import { lookupUser } from '@/scripts/lookup-user'; | ||||
| import { MisskeyNavigator } from '@/scripts/navigate'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const isEmpty = (x: string | null) => x == null || x === ''; | ||||
| 
 | ||||
| const nav = new MisskeyNavigator(); | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| const indexInfo = { | ||||
| 	title: i18n.ts.controlPanel, | ||||
|  | @ -224,7 +219,7 @@ watch(component, () => { | |||
| 
 | ||||
| watch(() => props.initialPage, () => { | ||||
| 	if (props.initialPage == null && !narrow) { | ||||
| 		nav.push('/admin/overview'); | ||||
| 		router.push('/admin/overview'); | ||||
| 	} else { | ||||
| 		if (props.initialPage == null) { | ||||
| 			INFO = indexInfo; | ||||
|  | @ -234,7 +229,7 @@ watch(() => props.initialPage, () => { | |||
| 
 | ||||
| watch(narrow, () => { | ||||
| 	if (props.initialPage == null && !narrow) { | ||||
| 		nav.push('/admin/overview'); | ||||
| 		router.push('/admin/overview'); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
|  | @ -243,7 +238,7 @@ onMounted(() => { | |||
| 
 | ||||
| 	narrow = el.offsetWidth < NARROW_THRESHOLD; | ||||
| 	if (props.initialPage == null && !narrow) { | ||||
| 		nav.push('/admin/overview'); | ||||
| 		router.push('/admin/overview'); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
|  | @ -251,19 +246,19 @@ onUnmounted(() => { | |||
| 	ro.disconnect(); | ||||
| }); | ||||
| 
 | ||||
| const pageChanged = (page) => { | ||||
| 	if (page == null) { | ||||
| provideMetadataReceiver((info) => { | ||||
| 	if (info == null) { | ||||
| 		childInfo = null; | ||||
| 	} else { | ||||
| 		childInfo = page[symbols.PAGE_INFO]; | ||||
| 		childInfo = info; | ||||
| 	} | ||||
| }; | ||||
| }); | ||||
| 
 | ||||
| const invite = () => { | ||||
| 	os.api('admin/invite').then(x => { | ||||
| 		os.alert({ | ||||
| 			type: 'info', | ||||
| 			text: x.code | ||||
| 			text: x.code, | ||||
| 		}); | ||||
| 	}).catch(err => { | ||||
| 		os.alert({ | ||||
|  | @ -279,33 +274,38 @@ const lookup = (ev) => { | |||
| 		icon: 'fas fa-user', | ||||
| 		action: () => { | ||||
| 			lookupUser(); | ||||
| 		} | ||||
| 		}, | ||||
| 	}, { | ||||
| 		text: i18n.ts.note, | ||||
| 		icon: 'fas fa-pencil-alt', | ||||
| 		action: () => { | ||||
| 			alert('TODO'); | ||||
| 		} | ||||
| 		}, | ||||
| 	}, { | ||||
| 		text: i18n.ts.file, | ||||
| 		icon: 'fas fa-cloud', | ||||
| 		action: () => { | ||||
| 			alert('TODO'); | ||||
| 		} | ||||
| 		}, | ||||
| 	}, { | ||||
| 		text: i18n.ts.instance, | ||||
| 		icon: 'fas fa-globe', | ||||
| 		action: () => { | ||||
| 			alert('TODO'); | ||||
| 		} | ||||
| 		}, | ||||
| 	}], ev.currentTarget ?? ev.target); | ||||
| }; | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(INFO); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: INFO, | ||||
| 	header: { | ||||
| 		title: i18n.ts.controlPanel, | ||||
| 	} | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,25 +1,29 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<FormTextarea v-model="blockedHosts" class="_formBlock"> | ||||
| 			<span>{{ i18n.ts.blockedInstances }}</span> | ||||
| 			<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> | ||||
| 		</FormTextarea> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 		<FormSuspense :p="init"> | ||||
| 			<FormTextarea v-model="blockedHosts" class="_formBlock"> | ||||
| 				<span>{{ i18n.ts.blockedInstances }}</span> | ||||
| 				<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> | ||||
| 			</FormTextarea> | ||||
| 
 | ||||
| 		<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> | ||||
| 	</FormSuspense> | ||||
| </MkSpacer> | ||||
| 			<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> | ||||
| 		</FormSuspense> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import FormButton from '@/components/ui/button.vue'; | ||||
| import FormTextarea from '@/components/form/textarea.vue'; | ||||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { fetchInstance } from '@/instance'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let blockedHosts: string = $ref(''); | ||||
| 
 | ||||
|  | @ -36,11 +40,13 @@ function save() { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.instanceBlocking, | ||||
| 		icon: 'fas fa-ban', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.instanceBlocking, | ||||
| 	icon: 'fas fa-ban', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<FormFolder class="_formBlock"> | ||||
| 			<template #icon><i class="fab fa-twitter"></i></template> | ||||
|  | @ -20,19 +21,19 @@ | |||
| 			<XDiscord/> | ||||
| 		</FormFolder> | ||||
| 	</FormSuspense> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import FormFolder from '@/components/form/folder.vue'; | ||||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import XTwitter from './integrations.twitter.vue'; | ||||
| import XGithub from './integrations.github.vue'; | ||||
| import XDiscord from './integrations.discord.vue'; | ||||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import FormFolder from '@/components/form/folder.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let enableTwitterIntegration: boolean = $ref(false); | ||||
| let enableGithubIntegration: boolean = $ref(false); | ||||
|  | @ -45,11 +46,13 @@ async function init() { | |||
| 	enableDiscordIntegration = meta.enableDiscordIntegration; | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.integration, | ||||
| 		icon: 'fas fa-share-alt', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.integration, | ||||
| 	icon: 'fas fa-share-alt', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,72 +1,76 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<div class="_formRoot"> | ||||
| 			<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 		<FormSuspense :p="init"> | ||||
| 			<div class="_formRoot"> | ||||
| 				<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch> | ||||
| 
 | ||||
| 			<template v-if="useObjectStorage"> | ||||
| 				<FormInput v-model="objectStorageBaseUrl" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template> | ||||
| 					<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template> | ||||
| 				</FormInput> | ||||
| 
 | ||||
| 				<FormInput v-model="objectStorageBucket" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.objectStorageBucket }}</template> | ||||
| 					<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template> | ||||
| 				</FormInput> | ||||
| 
 | ||||
| 				<FormInput v-model="objectStoragePrefix" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.objectStoragePrefix }}</template> | ||||
| 					<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template> | ||||
| 				</FormInput> | ||||
| 
 | ||||
| 				<FormInput v-model="objectStorageEndpoint" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.objectStorageEndpoint }}</template> | ||||
| 					<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template> | ||||
| 				</FormInput> | ||||
| 
 | ||||
| 				<FormInput v-model="objectStorageRegion" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.objectStorageRegion }}</template> | ||||
| 					<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template> | ||||
| 				</FormInput> | ||||
| 
 | ||||
| 				<FormSplit :min-width="280"> | ||||
| 					<FormInput v-model="objectStorageAccessKey" class="_formBlock"> | ||||
| 						<template #prefix><i class="fas fa-key"></i></template> | ||||
| 						<template #label>Access key</template> | ||||
| 				<template v-if="useObjectStorage"> | ||||
| 					<FormInput v-model="objectStorageBaseUrl" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template> | ||||
| 						<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template> | ||||
| 					</FormInput> | ||||
| 
 | ||||
| 					<FormInput v-model="objectStorageSecretKey" class="_formBlock"> | ||||
| 						<template #prefix><i class="fas fa-key"></i></template> | ||||
| 						<template #label>Secret key</template> | ||||
| 					<FormInput v-model="objectStorageBucket" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.objectStorageBucket }}</template> | ||||
| 						<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template> | ||||
| 					</FormInput> | ||||
| 				</FormSplit> | ||||
| 
 | ||||
| 				<FormSwitch v-model="objectStorageUseSSL" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.objectStorageUseSSL }}</template> | ||||
| 					<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template> | ||||
| 				</FormSwitch> | ||||
| 					<FormInput v-model="objectStoragePrefix" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.objectStoragePrefix }}</template> | ||||
| 						<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template> | ||||
| 					</FormInput> | ||||
| 
 | ||||
| 				<FormSwitch v-model="objectStorageUseProxy" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.objectStorageUseProxy }}</template> | ||||
| 					<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template> | ||||
| 				</FormSwitch> | ||||
| 					<FormInput v-model="objectStorageEndpoint" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.objectStorageEndpoint }}</template> | ||||
| 						<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template> | ||||
| 					</FormInput> | ||||
| 
 | ||||
| 				<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template> | ||||
| 				</FormSwitch> | ||||
| 					<FormInput v-model="objectStorageRegion" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.objectStorageRegion }}</template> | ||||
| 						<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template> | ||||
| 					</FormInput> | ||||
| 
 | ||||
| 				<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock"> | ||||
| 					<template #label>s3ForcePathStyle</template> | ||||
| 				</FormSwitch> | ||||
| 			</template> | ||||
| 		</div> | ||||
| 	</FormSuspense> | ||||
| </MkSpacer> | ||||
| 					<FormSplit :min-width="280"> | ||||
| 						<FormInput v-model="objectStorageAccessKey" class="_formBlock"> | ||||
| 							<template #prefix><i class="fas fa-key"></i></template> | ||||
| 							<template #label>Access key</template> | ||||
| 						</FormInput> | ||||
| 
 | ||||
| 						<FormInput v-model="objectStorageSecretKey" class="_formBlock"> | ||||
| 							<template #prefix><i class="fas fa-key"></i></template> | ||||
| 							<template #label>Secret key</template> | ||||
| 						</FormInput> | ||||
| 					</FormSplit> | ||||
| 
 | ||||
| 					<FormSwitch v-model="objectStorageUseSSL" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.objectStorageUseSSL }}</template> | ||||
| 						<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template> | ||||
| 					</FormSwitch> | ||||
| 
 | ||||
| 					<FormSwitch v-model="objectStorageUseProxy" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.objectStorageUseProxy }}</template> | ||||
| 						<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template> | ||||
| 					</FormSwitch> | ||||
| 
 | ||||
| 					<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template> | ||||
| 					</FormSwitch> | ||||
| 
 | ||||
| 					<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock"> | ||||
| 						<template #label>s3ForcePathStyle</template> | ||||
| 					</FormSwitch> | ||||
| 				</template> | ||||
| 			</div> | ||||
| 		</FormSuspense> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import FormInput from '@/components/form/input.vue'; | ||||
| import FormGroup from '@/components/form/group.vue'; | ||||
|  | @ -74,9 +78,9 @@ import FormSuspense from '@/components/form/suspense.vue'; | |||
| import FormSplit from '@/components/form/split.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { fetchInstance } from '@/instance'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let useObjectStorage: boolean = $ref(false); | ||||
| let objectStorageBaseUrl: string | null = $ref(null); | ||||
|  | @ -129,17 +133,18 @@ function save() { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
|   [symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.objectStorage, | ||||
| 		icon: 'fas fa-cloud', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-check', | ||||
| 			text: i18n.ts.save, | ||||
| 			handler: save, | ||||
| 		}], | ||||
| 	} | ||||
| const headerActions = $computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	icon: 'fas fa-check', | ||||
| 	text: i18n.ts.save, | ||||
| 	handler: save, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.objectStorage, | ||||
| 	icon: 'fas fa-cloud', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,18 +1,22 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		none | ||||
| 	</FormSuspense> | ||||
| </MkSpacer> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 		<FormSuspense :p="init"> | ||||
| 			none | ||||
| 		</FormSuspense> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { fetchInstance } from '@/instance'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| async function init() { | ||||
| 	await os.api('admin/meta'); | ||||
|  | @ -24,17 +28,18 @@ function save() { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
|   [symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.other, | ||||
| 		icon: 'fas fa-cogs', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-check', | ||||
| 			text: i18n.ts.save, | ||||
| 			handler: save, | ||||
| 		}], | ||||
| 	} | ||||
| const headerActions = $computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	icon: 'fas fa-check', | ||||
| 	text: i18n.ts.save, | ||||
| 	handler: save, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.other, | ||||
| 	icon: 'fas fa-cogs', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ | |||
| 		</MkContainer> | ||||
| 	</div> | ||||
| 
 | ||||
| 		<!--<XMetrics/>--> | ||||
| 	<!--<XMetrics/>--> | ||||
| 
 | ||||
| 	<MkFolder style="margin: var(--margin)"> | ||||
| 		<template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template> | ||||
|  | @ -67,6 +67,7 @@ | |||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; | ||||
| import XMetrics from './metrics.vue'; | ||||
| import MkInstanceStats from '@/components/instance-stats.vue'; | ||||
| import MkNumberDiff from '@/components/number-diff.vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
|  | @ -74,11 +75,10 @@ import MkFolder from '@/components/ui/folder.vue'; | |||
| import MkQueueChart from '@/components/queue-chart.vue'; | ||||
| import { version, url } from '@/config'; | ||||
| import number from '@/filters/number'; | ||||
| import XMetrics from './metrics.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { stream } from '@/stream'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let stats: any = $ref(null); | ||||
| let serverInfo: any = $ref(null); | ||||
|  | @ -106,7 +106,7 @@ onMounted(async () => { | |||
| 	nextTick(() => { | ||||
| 		queueStatsConnection.send('requestLog', { | ||||
| 			id: Math.random().toString().substr(2, 8), | ||||
| 			length: 200 | ||||
| 			length: 200, | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
|  | @ -115,12 +115,14 @@ onBeforeUnmount(() => { | |||
| 	queueStatsConnection.dispose(); | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.dashboard, | ||||
| 		icon: 'fas fa-tachometer-alt', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.dashboard, | ||||
| 	icon: 'fas fa-tachometer-alt', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo> | ||||
| 		<MkKeyValue class="_formBlock"> | ||||
|  | @ -9,7 +10,7 @@ | |||
| 
 | ||||
| 		<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton> | ||||
| 	</FormSuspense> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -19,9 +20,9 @@ import FormButton from '@/components/ui/button.vue'; | |||
| import MkInfo from '@/components/ui/info.vue'; | ||||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { fetchInstance } from '@/instance'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let proxyAccount: any = $ref(null); | ||||
| let proxyAccountId: any = $ref(null); | ||||
|  | @ -50,11 +51,13 @@ function save() { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.proxyAccount, | ||||
| 		icon: 'fas fa-ghost', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.proxyAccount, | ||||
| 	icon: 'fas fa-ghost', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,24 +1,28 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800"> | ||||
| 	<XQueue :connection="connection" domain="inbox"> | ||||
| 		<template #title>In</template> | ||||
| 	</XQueue> | ||||
| 	<XQueue :connection="connection" domain="deliver"> | ||||
| 		<template #title>Out</template> | ||||
| 	</XQueue> | ||||
| 	<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton> | ||||
| </MkSpacer> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="800"> | ||||
| 		<XQueue :connection="connection" domain="inbox"> | ||||
| 			<template #title>In</template> | ||||
| 		</XQueue> | ||||
| 		<XQueue :connection="connection" domain="deliver"> | ||||
| 			<template #title>Out</template> | ||||
| 		</XQueue> | ||||
| 		<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import XQueue from './queue.chart.vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { stream } from '@/stream'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import * as config from '@/config'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const connection = markRaw(stream.useChannel('queueStats')); | ||||
| 
 | ||||
|  | @ -38,7 +42,7 @@ onMounted(() => { | |||
| 	nextTick(() => { | ||||
| 		connection.send('requestLog', { | ||||
| 			id: Math.random().toString().substr(2, 8), | ||||
| 			length: 200 | ||||
| 			length: 200, | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
|  | @ -47,19 +51,20 @@ onBeforeUnmount(() => { | |||
| 	connection.dispose(); | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.jobQueue, | ||||
| 		icon: 'fas fa-clipboard-list', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-up-right-from-square', | ||||
| 			text: i18n.ts.dashboard, | ||||
| 			handler: () => { | ||||
| 				window.open(config.url + '/queue', '_blank'); | ||||
| 			}, | ||||
| 		}], | ||||
| 	} | ||||
| const headerActions = $computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	icon: 'fas fa-up-right-from-square', | ||||
| 	text: i18n.ts.dashboard, | ||||
| 	handler: () => { | ||||
| 		window.open(config.url + '/queue', '_blank'); | ||||
| 	}, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.jobQueue, | ||||
| 	icon: 'fas fa-clipboard-list', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,24 +1,28 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800"> | ||||
| 	<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;"> | ||||
| 		<div>{{ relay.inbox }}</div> | ||||
| 		<div class="status"> | ||||
| 			<i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i> | ||||
| 			<i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i> | ||||
| 			<i v-else class="fas fa-clock icon requesting"></i> | ||||
| 			<span>{{ $t(`_relayStatus.${relay.status}`) }}</span> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="800"> | ||||
| 		<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;"> | ||||
| 			<div>{{ relay.inbox }}</div> | ||||
| 			<div class="status"> | ||||
| 				<i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i> | ||||
| 				<i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i> | ||||
| 				<i v-else class="fas fa-clock icon requesting"></i> | ||||
| 				<span>{{ $t(`_relayStatus.${relay.status}`) }}</span> | ||||
| 			</div> | ||||
| 			<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> | ||||
| 		</div> | ||||
| 		<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let relays: any[] = $ref([]); | ||||
| 
 | ||||
|  | @ -26,30 +30,30 @@ async function addRelay() { | |||
| 	const { canceled, result: inbox } = await os.inputText({ | ||||
| 		title: i18n.ts.addRelay, | ||||
| 		type: 'url', | ||||
| 		placeholder: i18n.ts.inboxUrl | ||||
| 		placeholder: i18n.ts.inboxUrl, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 	os.api('admin/relays/add', { | ||||
| 		inbox | ||||
| 		inbox, | ||||
| 	}).then((relay: any) => { | ||||
| 		refresh(); | ||||
| 	}).catch((err: any) => { | ||||
| 		os.alert({ | ||||
| 			type: 'error', | ||||
| 			text: err.message || err | ||||
| 			text: err.message || err, | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function remove(inbox: string) { | ||||
| 	os.api('admin/relays/remove', { | ||||
| 		inbox | ||||
| 		inbox, | ||||
| 	}).then(() => { | ||||
| 		refresh(); | ||||
| 	}).catch((err: any) => { | ||||
| 		os.alert({ | ||||
| 			type: 'error', | ||||
| 			text: err.message || err | ||||
| 			text: err.message || err, | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
|  | @ -62,18 +66,19 @@ function refresh() { | |||
| 
 | ||||
| refresh(); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.relays, | ||||
| 		icon: 'fas fa-globe', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-plus', | ||||
| 			text: i18n.ts.addRelay, | ||||
| 			handler: addRelay, | ||||
| 		}], | ||||
| 	} | ||||
| const headerActions = $computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	icon: 'fas fa-plus', | ||||
| 	text: i18n.ts.addRelay, | ||||
| 	handler: addRelay, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.relays, | ||||
| 	icon: 'fas fa-globe', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,36 +1,41 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<div class="_formRoot"> | ||||
| 			<FormFolder class="_formBlock"> | ||||
| 				<template #icon><i class="fas fa-shield-alt"></i></template> | ||||
| 				<template #label>{{ i18n.ts.botProtection }}</template> | ||||
| 				<template v-if="enableHcaptcha" #suffix>hCaptcha</template> | ||||
| 				<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> | ||||
| 				<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 		<FormSuspense :p="init"> | ||||
| 			<div class="_formRoot"> | ||||
| 				<FormFolder class="_formBlock"> | ||||
| 					<template #icon><i class="fas fa-shield-alt"></i></template> | ||||
| 					<template #label>{{ i18n.ts.botProtection }}</template> | ||||
| 					<template v-if="enableHcaptcha" #suffix>hCaptcha</template> | ||||
| 					<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> | ||||
| 					<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> | ||||
| 
 | ||||
| 				<XBotProtection/> | ||||
| 			</FormFolder> | ||||
| 					<XBotProtection/> | ||||
| 				</FormFolder> | ||||
| 
 | ||||
| 			<FormFolder class="_formBlock"> | ||||
| 				<template #label>Summaly Proxy</template> | ||||
| 				<FormFolder class="_formBlock"> | ||||
| 					<template #label>Summaly Proxy</template> | ||||
| 
 | ||||
| 				<div class="_formRoot"> | ||||
| 					<FormInput v-model="summalyProxy" class="_formBlock"> | ||||
| 						<template #prefix><i class="fas fa-link"></i></template> | ||||
| 						<template #label>Summaly Proxy URL</template> | ||||
| 					</FormInput> | ||||
| 					<div class="_formRoot"> | ||||
| 						<FormInput v-model="summalyProxy" class="_formBlock"> | ||||
| 							<template #prefix><i class="fas fa-link"></i></template> | ||||
| 							<template #label>Summaly Proxy URL</template> | ||||
| 						</FormInput> | ||||
| 
 | ||||
| 					<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> | ||||
| 				</div> | ||||
| 			</FormFolder> | ||||
| 		</div> | ||||
| 	</FormSuspense> | ||||
| </MkSpacer> | ||||
| 						<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> | ||||
| 					</div> | ||||
| 				</FormFolder> | ||||
| 			</div> | ||||
| 		</FormSuspense> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import XBotProtection from './bot-protection.vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import FormFolder from '@/components/form/folder.vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import FormInfo from '@/components/ui/info.vue'; | ||||
|  | @ -38,11 +43,10 @@ import FormSuspense from '@/components/form/suspense.vue'; | |||
| import FormSection from '@/components/form/section.vue'; | ||||
| import FormInput from '@/components/form/input.vue'; | ||||
| import FormButton from '@/components/ui/button.vue'; | ||||
| import XBotProtection from './bot-protection.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { fetchInstance } from '@/instance'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let summalyProxy: string = $ref(''); | ||||
| let enableHcaptcha: boolean = $ref(false); | ||||
|  | @ -63,11 +67,13 @@ function save() { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.security, | ||||
| 		icon: 'fas fa-lock', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.security, | ||||
| 	icon: 'fas fa-lock', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,149 +1,155 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<div class="_formRoot"> | ||||
| 			<FormInput v-model="name" class="_formBlock"> | ||||
| 				<template #label>{{ i18n.ts.instanceName }}</template> | ||||
| 			</FormInput> | ||||
| 
 | ||||
| 			<FormTextarea v-model="description" class="_formBlock"> | ||||
| 				<template #label>{{ i18n.ts.instanceDescription }}</template> | ||||
| 			</FormTextarea> | ||||
| 
 | ||||
| 			<FormInput v-model="tosUrl" class="_formBlock"> | ||||
| 				<template #prefix><i class="fas fa-link"></i></template> | ||||
| 				<template #label>{{ i18n.ts.tosUrl }}</template> | ||||
| 			</FormInput> | ||||
| 
 | ||||
| 			<FormSplit :min-width="300"> | ||||
| 				<FormInput v-model="maintainerName" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.maintainerName }}</template> | ||||
| 				</FormInput> | ||||
| 
 | ||||
| 				<FormInput v-model="maintainerEmail" type="email" class="_formBlock"> | ||||
| 					<template #prefix><i class="fas fa-envelope"></i></template> | ||||
| 					<template #label>{{ i18n.ts.maintainerEmail }}</template> | ||||
| 				</FormInput> | ||||
| 			</FormSplit> | ||||
| 
 | ||||
| 			<FormTextarea v-model="pinnedUsers" class="_formBlock"> | ||||
| 				<template #label>{{ i18n.ts.pinnedUsers }}</template> | ||||
| 				<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> | ||||
| 			</FormTextarea> | ||||
| 
 | ||||
| 			<FormSection> | ||||
| 				<FormSwitch v-model="enableRegistration" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.enableRegistration }}</template> | ||||
| 				</FormSwitch> | ||||
| 
 | ||||
| 				<FormSwitch v-model="emailRequiredForSignup" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.emailRequiredForSignup }}</template> | ||||
| 				</FormSwitch> | ||||
| 			</FormSection> | ||||
| 
 | ||||
| 			<FormSection> | ||||
| 				<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch> | ||||
| 				<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch> | ||||
| 				<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo> | ||||
| 			</FormSection> | ||||
| 
 | ||||
| 			<FormSection> | ||||
| 				<template #label>{{ i18n.ts.theme }}</template> | ||||
| 
 | ||||
| 				<FormInput v-model="iconUrl" class="_formBlock"> | ||||
| 					<template #prefix><i class="fas fa-link"></i></template> | ||||
| 					<template #label>{{ i18n.ts.iconUrl }}</template> | ||||
| 				</FormInput> | ||||
| 
 | ||||
| 				<FormInput v-model="bannerUrl" class="_formBlock"> | ||||
| 					<template #prefix><i class="fas fa-link"></i></template> | ||||
| 					<template #label>{{ i18n.ts.bannerUrl }}</template> | ||||
| 				</FormInput> | ||||
| 
 | ||||
| 				<FormInput v-model="backgroundImageUrl" class="_formBlock"> | ||||
| 					<template #prefix><i class="fas fa-link"></i></template> | ||||
| 					<template #label>{{ i18n.ts.backgroundImageUrl }}</template> | ||||
| 				</FormInput> | ||||
| 
 | ||||
| 				<FormInput v-model="themeColor" class="_formBlock"> | ||||
| 					<template #prefix><i class="fas fa-palette"></i></template> | ||||
| 					<template #label>{{ i18n.ts.themeColor }}</template> | ||||
| 					<template #caption>#RRGGBB</template> | ||||
| 				</FormInput> | ||||
| 
 | ||||
| 				<FormTextarea v-model="defaultLightTheme" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template> | ||||
| 					<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> | ||||
| 				</FormTextarea> | ||||
| 
 | ||||
| 				<FormTextarea v-model="defaultDarkTheme" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template> | ||||
| 					<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> | ||||
| 				</FormTextarea> | ||||
| 			</FormSection> | ||||
| 
 | ||||
| 			<FormSection> | ||||
| 				<template #label>{{ i18n.ts.files }}</template> | ||||
| 
 | ||||
| 				<FormSwitch v-model="cacheRemoteFiles" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.cacheRemoteFiles }}</template> | ||||
| 					<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template> | ||||
| 				</FormSwitch> | ||||
| 
 | ||||
| 				<FormSplit :min-width="280"> | ||||
| 					<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template> | ||||
| 						<template #suffix>MB</template> | ||||
| 						<template #caption>{{ i18n.ts.inMb }}</template> | ||||
| <div> | ||||
| 	<MkStickyContainer> | ||||
| 		<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 			<FormSuspense :p="init"> | ||||
| 				<div class="_formRoot"> | ||||
| 					<FormInput v-model="name" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.instanceName }}</template> | ||||
| 					</FormInput> | ||||
| 
 | ||||
| 					<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template> | ||||
| 						<template #suffix>MB</template> | ||||
| 						<template #caption>{{ i18n.ts.inMb }}</template> | ||||
| 					</FormInput> | ||||
| 				</FormSplit> | ||||
| 			</FormSection> | ||||
| 					<FormTextarea v-model="description" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.instanceDescription }}</template> | ||||
| 					</FormTextarea> | ||||
| 
 | ||||
| 			<FormSection> | ||||
| 				<template #label>ServiceWorker</template> | ||||
| 
 | ||||
| 				<FormSwitch v-model="enableServiceWorker" class="_formBlock"> | ||||
| 					<template #label>{{ i18n.ts.enableServiceworker }}</template> | ||||
| 					<template #caption>{{ i18n.ts.serviceworkerInfo }}</template> | ||||
| 				</FormSwitch> | ||||
| 
 | ||||
| 				<template v-if="enableServiceWorker"> | ||||
| 					<FormInput v-model="swPublicKey" class="_formBlock"> | ||||
| 						<template #prefix><i class="fas fa-key"></i></template> | ||||
| 						<template #label>Public key</template> | ||||
| 					<FormInput v-model="tosUrl" class="_formBlock"> | ||||
| 						<template #prefix><i class="fas fa-link"></i></template> | ||||
| 						<template #label>{{ i18n.ts.tosUrl }}</template> | ||||
| 					</FormInput> | ||||
| 
 | ||||
| 					<FormInput v-model="swPrivateKey" class="_formBlock"> | ||||
| 						<template #prefix><i class="fas fa-key"></i></template> | ||||
| 						<template #label>Private key</template> | ||||
| 					</FormInput> | ||||
| 				</template> | ||||
| 			</FormSection> | ||||
| 					<FormSplit :min-width="300"> | ||||
| 						<FormInput v-model="maintainerName" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.maintainerName }}</template> | ||||
| 						</FormInput> | ||||
| 
 | ||||
| 			<FormSection> | ||||
| 				<template #label>DeepL Translation</template> | ||||
| 						<FormInput v-model="maintainerEmail" type="email" class="_formBlock"> | ||||
| 							<template #prefix><i class="fas fa-envelope"></i></template> | ||||
| 							<template #label>{{ i18n.ts.maintainerEmail }}</template> | ||||
| 						</FormInput> | ||||
| 					</FormSplit> | ||||
| 
 | ||||
| 				<FormInput v-model="deeplAuthKey" class="_formBlock"> | ||||
| 					<template #prefix><i class="fas fa-key"></i></template> | ||||
| 					<template #label>DeepL Auth Key</template> | ||||
| 				</FormInput> | ||||
| 				<FormSwitch v-model="deeplIsPro" class="_formBlock"> | ||||
| 					<template #label>Pro account</template> | ||||
| 				</FormSwitch> | ||||
| 			</FormSection> | ||||
| 		</div> | ||||
| 	</FormSuspense> | ||||
| </MkSpacer> | ||||
| 					<FormTextarea v-model="pinnedUsers" class="_formBlock"> | ||||
| 						<template #label>{{ i18n.ts.pinnedUsers }}</template> | ||||
| 						<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> | ||||
| 					</FormTextarea> | ||||
| 
 | ||||
| 					<FormSection> | ||||
| 						<FormSwitch v-model="enableRegistration" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.enableRegistration }}</template> | ||||
| 						</FormSwitch> | ||||
| 
 | ||||
| 						<FormSwitch v-model="emailRequiredForSignup" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.emailRequiredForSignup }}</template> | ||||
| 						</FormSwitch> | ||||
| 					</FormSection> | ||||
| 
 | ||||
| 					<FormSection> | ||||
| 						<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch> | ||||
| 						<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch> | ||||
| 						<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo> | ||||
| 					</FormSection> | ||||
| 
 | ||||
| 					<FormSection> | ||||
| 						<template #label>{{ i18n.ts.theme }}</template> | ||||
| 
 | ||||
| 						<FormInput v-model="iconUrl" class="_formBlock"> | ||||
| 							<template #prefix><i class="fas fa-link"></i></template> | ||||
| 							<template #label>{{ i18n.ts.iconUrl }}</template> | ||||
| 						</FormInput> | ||||
| 
 | ||||
| 						<FormInput v-model="bannerUrl" class="_formBlock"> | ||||
| 							<template #prefix><i class="fas fa-link"></i></template> | ||||
| 							<template #label>{{ i18n.ts.bannerUrl }}</template> | ||||
| 						</FormInput> | ||||
| 
 | ||||
| 						<FormInput v-model="backgroundImageUrl" class="_formBlock"> | ||||
| 							<template #prefix><i class="fas fa-link"></i></template> | ||||
| 							<template #label>{{ i18n.ts.backgroundImageUrl }}</template> | ||||
| 						</FormInput> | ||||
| 
 | ||||
| 						<FormInput v-model="themeColor" class="_formBlock"> | ||||
| 							<template #prefix><i class="fas fa-palette"></i></template> | ||||
| 							<template #label>{{ i18n.ts.themeColor }}</template> | ||||
| 							<template #caption>#RRGGBB</template> | ||||
| 						</FormInput> | ||||
| 
 | ||||
| 						<FormTextarea v-model="defaultLightTheme" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template> | ||||
| 							<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> | ||||
| 						</FormTextarea> | ||||
| 
 | ||||
| 						<FormTextarea v-model="defaultDarkTheme" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template> | ||||
| 							<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> | ||||
| 						</FormTextarea> | ||||
| 					</FormSection> | ||||
| 
 | ||||
| 					<FormSection> | ||||
| 						<template #label>{{ i18n.ts.files }}</template> | ||||
| 
 | ||||
| 						<FormSwitch v-model="cacheRemoteFiles" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.cacheRemoteFiles }}</template> | ||||
| 							<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template> | ||||
| 						</FormSwitch> | ||||
| 
 | ||||
| 						<FormSplit :min-width="280"> | ||||
| 							<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock"> | ||||
| 								<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template> | ||||
| 								<template #suffix>MB</template> | ||||
| 								<template #caption>{{ i18n.ts.inMb }}</template> | ||||
| 							</FormInput> | ||||
| 
 | ||||
| 							<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock"> | ||||
| 								<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template> | ||||
| 								<template #suffix>MB</template> | ||||
| 								<template #caption>{{ i18n.ts.inMb }}</template> | ||||
| 							</FormInput> | ||||
| 						</FormSplit> | ||||
| 					</FormSection> | ||||
| 
 | ||||
| 					<FormSection> | ||||
| 						<template #label>ServiceWorker</template> | ||||
| 
 | ||||
| 						<FormSwitch v-model="enableServiceWorker" class="_formBlock"> | ||||
| 							<template #label>{{ i18n.ts.enableServiceworker }}</template> | ||||
| 							<template #caption>{{ i18n.ts.serviceworkerInfo }}</template> | ||||
| 						</FormSwitch> | ||||
| 
 | ||||
| 						<template v-if="enableServiceWorker"> | ||||
| 							<FormInput v-model="swPublicKey" class="_formBlock"> | ||||
| 								<template #prefix><i class="fas fa-key"></i></template> | ||||
| 								<template #label>Public key</template> | ||||
| 							</FormInput> | ||||
| 
 | ||||
| 							<FormInput v-model="swPrivateKey" class="_formBlock"> | ||||
| 								<template #prefix><i class="fas fa-key"></i></template> | ||||
| 								<template #label>Private key</template> | ||||
| 							</FormInput> | ||||
| 						</template> | ||||
| 					</FormSection> | ||||
| 
 | ||||
| 					<FormSection> | ||||
| 						<template #label>DeepL Translation</template> | ||||
| 
 | ||||
| 						<FormInput v-model="deeplAuthKey" class="_formBlock"> | ||||
| 							<template #prefix><i class="fas fa-key"></i></template> | ||||
| 							<template #label>DeepL Auth Key</template> | ||||
| 						</FormInput> | ||||
| 						<FormSwitch v-model="deeplIsPro" class="_formBlock"> | ||||
| 							<template #label>Pro account</template> | ||||
| 						</FormSwitch> | ||||
| 					</FormSection> | ||||
| 				</div> | ||||
| 			</FormSuspense> | ||||
| 		</MkSpacer> | ||||
| 	</MkStickyContainer> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import FormInput from '@/components/form/input.vue'; | ||||
| import FormTextarea from '@/components/form/textarea.vue'; | ||||
|  | @ -152,9 +158,9 @@ import FormSection from '@/components/form/section.vue'; | |||
| import FormSplit from '@/components/form/split.vue'; | ||||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { fetchInstance } from '@/instance'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let name: string | null = $ref(null); | ||||
| let description: string | null = $ref(null); | ||||
|  | @ -240,17 +246,18 @@ function save() { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.general, | ||||
| 		icon: 'fas fa-cog', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-check', | ||||
| 			text: i18n.ts.save, | ||||
| 			handler: save, | ||||
| 		}], | ||||
| 	} | ||||
| const headerActions = $computed(() => [{ | ||||
| 	asFullButton: true, | ||||
| 	icon: 'fas fa-check', | ||||
| 	text: i18n.ts.save, | ||||
| 	handler: save, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.general, | ||||
| 	icon: 'fas fa-cog', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,76 +1,84 @@ | |||
| <template> | ||||
| <div class="lknzcolw"> | ||||
| 	<div class="users"> | ||||
| 		<div class="inputs"> | ||||
| 			<MkSelect v-model="sort" style="flex: 1;"> | ||||
| 				<template #label>{{ $ts.sort }}</template> | ||||
| 				<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option> | ||||
| 				<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option> | ||||
| 			</MkSelect> | ||||
| 			<MkSelect v-model="state" style="flex: 1;"> | ||||
| 				<template #label>{{ $ts.state }}</template> | ||||
| 				<option value="all">{{ $ts.all }}</option> | ||||
| 				<option value="available">{{ $ts.normal }}</option> | ||||
| 				<option value="admin">{{ $ts.administrator }}</option> | ||||
| 				<option value="moderator">{{ $ts.moderator }}</option> | ||||
| 				<option value="silenced">{{ $ts.silence }}</option> | ||||
| 				<option value="suspended">{{ $ts.suspend }}</option> | ||||
| 			</MkSelect> | ||||
| 			<MkSelect v-model="origin" style="flex: 1;"> | ||||
| 				<template #label>{{ $ts.instance }}</template> | ||||
| 				<option value="combined">{{ $ts.all }}</option> | ||||
| 				<option value="local">{{ $ts.local }}</option> | ||||
| 				<option value="remote">{{ $ts.remote }}</option> | ||||
| 			</MkSelect> | ||||
| 		</div> | ||||
| 		<div class="inputs"> | ||||
| 			<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()"> | ||||
| 				<template #prefix>@</template> | ||||
| 				<template #label>{{ $ts.username }}</template> | ||||
| 			</MkInput> | ||||
| 			<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()"> | ||||
| 				<template #prefix>@</template> | ||||
| 				<template #label>{{ $ts.host }}</template> | ||||
| 			</MkInput> | ||||
| 		</div> | ||||
| <div> | ||||
| 	<MkStickyContainer> | ||||
| 		<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="900"> | ||||
| 			<div class="lknzcolw"> | ||||
| 				<div class="users"> | ||||
| 					<div class="inputs"> | ||||
| 						<MkSelect v-model="sort" style="flex: 1;"> | ||||
| 							<template #label>{{ $ts.sort }}</template> | ||||
| 							<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option> | ||||
| 							<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option> | ||||
| 							<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option> | ||||
| 							<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option> | ||||
| 						</MkSelect> | ||||
| 						<MkSelect v-model="state" style="flex: 1;"> | ||||
| 							<template #label>{{ $ts.state }}</template> | ||||
| 							<option value="all">{{ $ts.all }}</option> | ||||
| 							<option value="available">{{ $ts.normal }}</option> | ||||
| 							<option value="admin">{{ $ts.administrator }}</option> | ||||
| 							<option value="moderator">{{ $ts.moderator }}</option> | ||||
| 							<option value="silenced">{{ $ts.silence }}</option> | ||||
| 							<option value="suspended">{{ $ts.suspend }}</option> | ||||
| 						</MkSelect> | ||||
| 						<MkSelect v-model="origin" style="flex: 1;"> | ||||
| 							<template #label>{{ $ts.instance }}</template> | ||||
| 							<option value="combined">{{ $ts.all }}</option> | ||||
| 							<option value="local">{{ $ts.local }}</option> | ||||
| 							<option value="remote">{{ $ts.remote }}</option> | ||||
| 						</MkSelect> | ||||
| 					</div> | ||||
| 					<div class="inputs"> | ||||
| 						<MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()"> | ||||
| 							<template #prefix>@</template> | ||||
| 							<template #label>{{ $ts.username }}</template> | ||||
| 						</MkInput> | ||||
| 						<MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()"> | ||||
| 							<template #prefix>@</template> | ||||
| 							<template #label>{{ $ts.host }}</template> | ||||
| 						</MkInput> | ||||
| 					</div> | ||||
| 
 | ||||
| 		<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users"> | ||||
| 			<button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)"> | ||||
| 				<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/> | ||||
| 				<div class="body"> | ||||
| 					<header> | ||||
| 						<MkUserName class="name" :user="user"/> | ||||
| 						<span class="acct">@{{ acct(user) }}</span> | ||||
| 						<span v-if="user.isAdmin" class="staff"><i class="fas fa-bookmark"></i></span> | ||||
| 						<span v-if="user.isModerator" class="staff"><i class="far fa-bookmark"></i></span> | ||||
| 						<span v-if="user.isSilenced" class="punished"><i class="fas fa-microphone-slash"></i></span> | ||||
| 						<span v-if="user.isSuspended" class="punished"><i class="fas fa-snowflake"></i></span> | ||||
| 					</header> | ||||
| 					<div> | ||||
| 						<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span> | ||||
| 					</div> | ||||
| 					<div> | ||||
| 						<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span> | ||||
| 					</div> | ||||
| 					<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users"> | ||||
| 						<button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)"> | ||||
| 							<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/> | ||||
| 							<div class="body"> | ||||
| 								<header> | ||||
| 									<MkUserName class="name" :user="user"/> | ||||
| 									<span class="acct">@{{ acct(user) }}</span> | ||||
| 									<span v-if="user.isAdmin" class="staff"><i class="fas fa-bookmark"></i></span> | ||||
| 									<span v-if="user.isModerator" class="staff"><i class="far fa-bookmark"></i></span> | ||||
| 									<span v-if="user.isSilenced" class="punished"><i class="fas fa-microphone-slash"></i></span> | ||||
| 									<span v-if="user.isSuspended" class="punished"><i class="fas fa-snowflake"></i></span> | ||||
| 								</header> | ||||
| 								<div> | ||||
| 									<span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span> | ||||
| 								</div> | ||||
| 								<div> | ||||
| 									<span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</button> | ||||
| 					</MkPagination> | ||||
| 				</div> | ||||
| 			</button> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| 			</div> | ||||
| 		</MkSpacer> | ||||
| 	</MkStickyContainer> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed } from 'vue'; | ||||
| import XHeader from './_header_.vue'; | ||||
| import MkInput from '@/components/form/input.vue'; | ||||
| import MkSelect from '@/components/form/select.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import { acct } from '@/filters/user'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { lookupUser } from '@/scripts/lookup-user'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let paginationComponent = $ref<InstanceType<typeof MkPagination>>(); | ||||
| 
 | ||||
|  | @ -89,7 +97,7 @@ const pagination = { | |||
| 		username: searchUsername, | ||||
| 		hostname: searchHost, | ||||
| 	})), | ||||
| 	offsetMode: true | ||||
| 	offsetMode: true, | ||||
| }; | ||||
| 
 | ||||
| function searchUser() { | ||||
|  | @ -106,7 +114,7 @@ async function addUser() { | |||
| 
 | ||||
| 	const { canceled: canceled2, result: password } = await os.inputText({ | ||||
| 		title: i18n.ts.password, | ||||
| 		type: 'password' | ||||
| 		type: 'password', | ||||
| 	}); | ||||
| 	if (canceled2) return; | ||||
| 
 | ||||
|  | @ -122,34 +130,34 @@ function show(user) { | |||
| 	os.pageWindow(`/user-info/${user.id}`); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.ts.users, | ||||
| 		icon: 'fas fa-users', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			icon: 'fas fa-search', | ||||
| 			text: i18n.ts.search, | ||||
| 			handler: searchUser | ||||
| 		}, { | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-plus', | ||||
| 			text: i18n.ts.addUser, | ||||
| 			handler: addUser | ||||
| 		}, { | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-search', | ||||
| 			text: i18n.ts.lookup, | ||||
| 			handler: lookupUser | ||||
| 		}], | ||||
| 	})), | ||||
| }); | ||||
| const headerActions = $computed(() => [{ | ||||
| 	icon: 'fas fa-search', | ||||
| 	text: i18n.ts.search, | ||||
| 	handler: searchUser, | ||||
| }, { | ||||
| 	asFullButton: true, | ||||
| 	icon: 'fas fa-plus', | ||||
| 	text: i18n.ts.addUser, | ||||
| 	handler: addUser, | ||||
| }, { | ||||
| 	asFullButton: true, | ||||
| 	icon: 'fas fa-search', | ||||
| 	text: i18n.ts.lookup, | ||||
| 	handler: lookupUser, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: i18n.ts.users, | ||||
| 	icon: 'fas fa-users', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .lknzcolw { | ||||
| 	> .users { | ||||
| 		margin: var(--margin); | ||||
| 
 | ||||
| 		> .inputs { | ||||
| 			display: flex; | ||||
|  |  | |||
|  | @ -1,57 +1,53 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800"> | ||||
| 	<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content"> | ||||
| 		<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement"> | ||||
| 			<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> | ||||
| 			<div class="_content"> | ||||
| 				<Mfm :text="announcement.text"/> | ||||
| 				<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/> | ||||
| 			</div> | ||||
| 			<div v-if="$i && !announcement.isRead" class="_footer"> | ||||
| 				<MkButton primary @click="read(items, announcement, i)"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton> | ||||
| 			</div> | ||||
| 		</section> | ||||
| 	</MkPagination> | ||||
| </MkSpacer> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="800"> | ||||
| 		<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content"> | ||||
| 			<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement"> | ||||
| 				<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> | ||||
| 				<div class="_content"> | ||||
| 					<Mfm :text="announcement.text"/> | ||||
| 					<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/> | ||||
| 				</div> | ||||
| 				<div v-if="$i && !announcement.isRead" class="_footer"> | ||||
| 					<MkButton primary @click="read(items, announcement, i)"><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton> | ||||
| 				</div> | ||||
| 			</section> | ||||
| 		</MkPagination> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkPagination, | ||||
| 		MkButton | ||||
| 	}, | ||||
| const pagination = { | ||||
| 	endpoint: 'announcements' as const, | ||||
| 	limit: 10, | ||||
| }; | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.announcements, | ||||
| 				icon: 'fas fa-broadcast-tower', | ||||
| 				bg: 'var(--bg)', | ||||
| 			}, | ||||
| 			pagination: { | ||||
| 				endpoint: 'announcements' as const, | ||||
| 				limit: 10, | ||||
| 			}, | ||||
| 		}; | ||||
| 	}, | ||||
| // TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい | ||||
| function read(items, announcement, i) { | ||||
| 	items[i] = { | ||||
| 		...announcement, | ||||
| 		isRead: true, | ||||
| 	}; | ||||
| 	os.api('i/read-announcement', { announcementId: announcement.id }); | ||||
| } | ||||
| 
 | ||||
| 	methods: { | ||||
| 		// TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい | ||||
| 		read(items, announcement, i) { | ||||
| 			items[i] = { | ||||
| 				...announcement, | ||||
| 				isRead: true, | ||||
| 			}; | ||||
| 			os.api('i/read-announcement', { announcementId: announcement.id }); | ||||
| 		}, | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.announcements, | ||||
| 	icon: 'fas fa-broadcast-tower', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,9 @@ | |||
| <template> | ||||
| <div v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks"> | ||||
| <div ref="rootEl" v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks"> | ||||
| 	<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> | ||||
| 	<div class="tl _block"> | ||||
| 		<XTimeline ref="tl" :key="antennaId" | ||||
| 		<XTimeline | ||||
| 			ref="tlEl" :key="antennaId" | ||||
| 			class="tl" | ||||
| 			src="antenna" | ||||
| 			:antenna="antennaId" | ||||
|  | @ -13,92 +14,78 @@ | |||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, defineAsyncComponent, computed } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, inject, watch } from 'vue'; | ||||
| import XTimeline from '@/components/timeline.vue'; | ||||
| import { scroll } from '@/scripts/scroll'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import i18n from '@/components/global/i18n'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XTimeline, | ||||
| 	}, | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| 	props: { | ||||
| 		antennaId: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	antennaId: string; | ||||
| }>(); | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			antenna: null, | ||||
| 			queue: 0, | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.antenna ? { | ||||
| 				title: this.antenna.name, | ||||
| 				icon: 'fas fa-satellite', | ||||
| 				bg: 'var(--bg)', | ||||
| 				actions: [{ | ||||
| 					icon: 'fas fa-calendar-alt', | ||||
| 					text: this.$ts.jumpToSpecifiedDate, | ||||
| 					handler: this.timetravel | ||||
| 				}, { | ||||
| 					icon: 'fas fa-cog', | ||||
| 					text: this.$ts.settings, | ||||
| 					handler: this.settings | ||||
| 				}], | ||||
| 			} : null), | ||||
| 		}; | ||||
| 	}, | ||||
| let antenna = $ref(null); | ||||
| let queue = $ref(0); | ||||
| let rootEl = $ref<HTMLElement>(); | ||||
| let tlEl = $ref<InstanceType<typeof XTimeline>>(); | ||||
| const keymap = $computed(() => ({ | ||||
| 	't': focus, | ||||
| })); | ||||
| 
 | ||||
| 	computed: { | ||||
| 		keymap(): any { | ||||
| 			return { | ||||
| 				't': this.focus | ||||
| 			}; | ||||
| 		}, | ||||
| 	}, | ||||
| function queueUpdated(q) { | ||||
| 	queue = q; | ||||
| } | ||||
| 
 | ||||
| 	watch: { | ||||
| 		antennaId: { | ||||
| 			async handler() { | ||||
| 				this.antenna = await os.api('antennas/show', { | ||||
| 					antennaId: this.antennaId | ||||
| 				}); | ||||
| 			}, | ||||
| 			immediate: true | ||||
| 		} | ||||
| 	}, | ||||
| function top() { | ||||
| 	scroll(rootEl, { top: 0 }); | ||||
| } | ||||
| 
 | ||||
| 	methods: { | ||||
| 		queueUpdated(q) { | ||||
| 			this.queue = q; | ||||
| 		}, | ||||
| async function timetravel() { | ||||
| 	const { canceled, result: date } = await os.inputDate({ | ||||
| 		title: i18n.ts.date, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 
 | ||||
| 		top() { | ||||
| 			scroll(this.$el, { top: 0 }); | ||||
| 		}, | ||||
| 	tlEl.timetravel(date); | ||||
| } | ||||
| 
 | ||||
| 		async timetravel() { | ||||
| 			const { canceled, result: date } = await os.inputDate({ | ||||
| 				title: this.$ts.date, | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| function settings() { | ||||
| 	router.push(`/my/antennas/${props.antennaId}`); | ||||
| } | ||||
| 
 | ||||
| 			this.$refs.tl.timetravel(date); | ||||
| 		}, | ||||
| function focus() { | ||||
| 	tlEl.focus(); | ||||
| } | ||||
| 
 | ||||
| 		settings() { | ||||
| 			this.$router.push(`/my/antennas/${this.antennaId}`); | ||||
| 		}, | ||||
| watch(() => props.antennaId, async () => { | ||||
| 	antenna = await os.api('antennas/show', { | ||||
| 		antennaId: props.antennaId, | ||||
| 	}); | ||||
| }, { immediate: true }); | ||||
| 
 | ||||
| 		focus() { | ||||
| 			(this.$refs.tl as any).focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => antenna ? { | ||||
| 	title: antenna.name, | ||||
| 	icon: 'fas fa-satellite', | ||||
| 	bg: 'var(--bg)', | ||||
| 	actions: [{ | ||||
| 		icon: 'fas fa-calendar-alt', | ||||
| 		text: i18n.ts.jumpToSpecifiedDate, | ||||
| 		handler: timetravel, | ||||
| 	}, { | ||||
| 		icon: 'fas fa-cog', | ||||
| 		text: i18n.ts.settings, | ||||
| 		handler: settings, | ||||
| 	}], | ||||
| } : null)); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,40 +1,43 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| 	<div class="_formRoot"> | ||||
| 		<div class="_formBlock"> | ||||
| 			<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()"> | ||||
| 				<template #label>Endpoint</template> | ||||
| 			</MkInput> | ||||
| 			<MkTextarea v-model="body" class="_formBlock" code> | ||||
| 				<template #label>Params (JSON or JSON5)</template> | ||||
| 			</MkTextarea> | ||||
| 			<MkSwitch v-model="withCredential" class="_formBlock"> | ||||
| 				With credential | ||||
| 			</MkSwitch> | ||||
| 			<MkButton class="_formBlock" primary :disabled="sending" @click="send"> | ||||
| 				<template v-if="sending"><MkEllipsis/></template> | ||||
| 				<template v-else><i class="fas fa-paper-plane"></i> Send</template> | ||||
| 			</MkButton> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="700"> | ||||
| 		<div class="_formRoot"> | ||||
| 			<div class="_formBlock"> | ||||
| 				<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()"> | ||||
| 					<template #label>Endpoint</template> | ||||
| 				</MkInput> | ||||
| 				<MkTextarea v-model="body" class="_formBlock" code> | ||||
| 					<template #label>Params (JSON or JSON5)</template> | ||||
| 				</MkTextarea> | ||||
| 				<MkSwitch v-model="withCredential" class="_formBlock"> | ||||
| 					With credential | ||||
| 				</MkSwitch> | ||||
| 				<MkButton class="_formBlock" primary :disabled="sending" @click="send"> | ||||
| 					<template v-if="sending"><MkEllipsis/></template> | ||||
| 					<template v-else><i class="fas fa-paper-plane"></i> Send</template> | ||||
| 				</MkButton> | ||||
| 			</div> | ||||
| 			<div v-if="res" class="_formBlock"> | ||||
| 				<MkTextarea v-model="res" code readonly tall> | ||||
| 					<template #label>Response</template> | ||||
| 				</MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div v-if="res" class="_formBlock"> | ||||
| 			<MkTextarea v-model="res" code readonly tall> | ||||
| 				<template #label>Response</template> | ||||
| 			</MkTextarea> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { ref } from 'vue'; | ||||
| import JSON5 from 'json5'; | ||||
| import { Endpoints } from 'misskey-js'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkInput from '@/components/form/input.vue'; | ||||
| import MkTextarea from '@/components/form/textarea.vue'; | ||||
| import MkSwitch from '@/components/form/switch.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { Endpoints } from 'misskey-js'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const body = ref('{}'); | ||||
| const endpoint = ref(''); | ||||
|  | @ -75,10 +78,12 @@ function onEndpointChange() { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: 'API console', | ||||
| 		icon: 'fas fa-terminal' | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: 'API console', | ||||
| 	icon: 'fas fa-terminal', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ | |||
| 		<h1>{{ $ts._auth.denied }}</h1> | ||||
| 	</div> | ||||
| 	<div v-if="state == 'accepted'" class="accepted"> | ||||
| 		<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$ts.allowed }}</h1> | ||||
| 		<h1>{{ session.app.isAuthorized ? $t('already-authorized') : $ts.allowed }}</h1> | ||||
| 		<p v-if="session.app.callbackUrl">{{ $ts._auth.callback }}<MkEllipsis/></p> | ||||
| 		<p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p> | ||||
| 	</div> | ||||
|  | @ -40,24 +40,20 @@ export default defineComponent({ | |||
| 		XForm, | ||||
| 		MkSignin, | ||||
| 	}, | ||||
| 	props: ['token'], | ||||
| 	data() { | ||||
| 		return { | ||||
| 			state: null, | ||||
| 			session: null, | ||||
| 			fetching: true | ||||
| 			fetching: true, | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		token(): string { | ||||
| 			return this.$route.params.token; | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		if (!this.$i) return; | ||||
| 
 | ||||
| 		// Fetch session | ||||
| 		os.api('auth/session/show', { | ||||
| 			token: this.token | ||||
| 			token: this.token, | ||||
| 		}).then(session => { | ||||
| 			this.session = session; | ||||
| 			this.fetching = false; | ||||
|  | @ -65,7 +61,7 @@ export default defineComponent({ | |||
| 			// 既に連携していた場合 | ||||
| 			if (this.session.app.isAuthorized) { | ||||
| 				os.api('auth/accept', { | ||||
| 					token: this.session.token | ||||
| 					token: this.session.token, | ||||
| 				}).then(() => { | ||||
| 					this.accepted(); | ||||
| 				}); | ||||
|  | @ -85,8 +81,8 @@ export default defineComponent({ | |||
| 			} | ||||
| 		}, onLogin(res) { | ||||
| 			login(res.i); | ||||
| 		} | ||||
| 	} | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,127 +1,122 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| 	<div class="_formRoot"> | ||||
| 		<MkInput v-model="name" class="_formBlock"> | ||||
| 			<template #label>{{ $ts.name }}</template> | ||||
| 		</MkInput> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="700"> | ||||
| 		<div class="_formRoot"> | ||||
| 			<MkInput v-model="name" class="_formBlock"> | ||||
| 				<template #label>{{ $ts.name }}</template> | ||||
| 			</MkInput> | ||||
| 
 | ||||
| 		<MkTextarea v-model="description" class="_formBlock"> | ||||
| 			<template #label>{{ $ts.description }}</template> | ||||
| 		</MkTextarea> | ||||
| 			<MkTextarea v-model="description" class="_formBlock"> | ||||
| 				<template #label>{{ $ts.description }}</template> | ||||
| 			</MkTextarea> | ||||
| 
 | ||||
| 		<div class="banner"> | ||||
| 			<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton> | ||||
| 			<div v-else-if="bannerUrl"> | ||||
| 				<img :src="bannerUrl" style="width: 100%;"/> | ||||
| 				<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton> | ||||
| 			<div class="banner"> | ||||
| 				<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="fas fa-plus"></i> {{ $ts._channel.setBanner }}</MkButton> | ||||
| 				<div v-else-if="bannerUrl"> | ||||
| 					<img :src="bannerUrl" style="width: 100%;"/> | ||||
| 					<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="_formBlock"> | ||||
| 				<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="_formBlock"> | ||||
| 			<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, inject, watch } from 'vue'; | ||||
| import MkTextarea from '@/components/form/textarea.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkInput from '@/components/form/input.vue'; | ||||
| import { selectFile } from '@/scripts/select-file'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { i18n } from '@/i18n'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkTextarea, MkButton, MkInput, | ||||
| 	}, | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| 	props: { | ||||
| 		channelId: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	channelId?: string; | ||||
| }>(); | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.channelId ? { | ||||
| 				title: this.$ts._channel.edit, | ||||
| 				icon: 'fas fa-satellite-dish', | ||||
| 				bg: 'var(--bg)', | ||||
| 			} : { | ||||
| 				title: this.$ts._channel.create, | ||||
| 				icon: 'fas fa-satellite-dish', | ||||
| 				bg: 'var(--bg)', | ||||
| 			}), | ||||
| 			channel: null, | ||||
| 			name: null, | ||||
| 			description: null, | ||||
| 			bannerUrl: null, | ||||
| 			bannerId: null, | ||||
| 		}; | ||||
| 	}, | ||||
| let channel = $ref(null); | ||||
| let name = $ref(null); | ||||
| let description = $ref(null); | ||||
| let bannerUrl = $ref<string | null>(null); | ||||
| let bannerId = $ref<string | null>(null); | ||||
| 
 | ||||
| 	watch: { | ||||
| 		async bannerId() { | ||||
| 			if (this.bannerId == null) { | ||||
| 				this.bannerUrl = null; | ||||
| 			} else { | ||||
| 				this.bannerUrl = (await os.api('drive/files/show', { | ||||
| 					fileId: this.bannerId, | ||||
| 				})).url; | ||||
| 			} | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	async created() { | ||||
| 		if (this.channelId) { | ||||
| 			this.channel = await os.api('channels/show', { | ||||
| 				channelId: this.channelId, | ||||
| 			}); | ||||
| 
 | ||||
| 			this.name = this.channel.name; | ||||
| 			this.description = this.channel.description; | ||||
| 			this.bannerId = this.channel.bannerId; | ||||
| 			this.bannerUrl = this.channel.bannerUrl; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		save() { | ||||
| 			const params = { | ||||
| 				name: this.name, | ||||
| 				description: this.description, | ||||
| 				bannerId: this.bannerId, | ||||
| 			}; | ||||
| 
 | ||||
| 			if (this.channelId) { | ||||
| 				params.channelId = this.channelId; | ||||
| 				os.api('channels/update', params) | ||||
| 				.then(channel => { | ||||
| 					os.success(); | ||||
| 				}); | ||||
| 			} else { | ||||
| 				os.api('channels/create', params) | ||||
| 				.then(channel => { | ||||
| 					os.success(); | ||||
| 					this.$router.push(`/channels/${channel.id}`); | ||||
| 				}); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		setBannerImage(evt) { | ||||
| 			selectFile(evt.currentTarget ?? evt.target, null).then(file => { | ||||
| 				this.bannerId = file.id; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		removeBannerImage() { | ||||
| 			this.bannerId = null; | ||||
| 		} | ||||
| watch(() => bannerId, async () => { | ||||
| 	if (bannerId == null) { | ||||
| 		bannerUrl = null; | ||||
| 	} else { | ||||
| 		bannerUrl = (await os.api('drive/files/show', { | ||||
| 			fileId: bannerId, | ||||
| 		})).url; | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| async function fetchChannel() { | ||||
| 	if (props.channelId == null) return; | ||||
| 
 | ||||
| 	channel = await os.api('channels/show', { | ||||
| 		channelId: props.channelId, | ||||
| 	}); | ||||
| 
 | ||||
| 	name = channel.name; | ||||
| 	description = channel.description; | ||||
| 	bannerId = channel.bannerId; | ||||
| 	bannerUrl = channel.bannerUrl; | ||||
| } | ||||
| 
 | ||||
| fetchChannel(); | ||||
| 
 | ||||
| function save() { | ||||
| 	const params = { | ||||
| 		name: name, | ||||
| 		description: description, | ||||
| 		bannerId: bannerId, | ||||
| 	}; | ||||
| 
 | ||||
| 	if (props.channelId) { | ||||
| 		params.channelId = props.channelId; | ||||
| 		os.api('channels/update', params).then(() => { | ||||
| 			os.success(); | ||||
| 		}); | ||||
| 	} else { | ||||
| 		os.api('channels/create', params).then(created => { | ||||
| 			os.success(); | ||||
| 			router.push(`/channels/${created.id}`); | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| function setBannerImage(evt) { | ||||
| 	selectFile(evt.currentTarget ?? evt.target, null).then(file => { | ||||
| 		bannerId = file.id; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function removeBannerImage() { | ||||
| 	bannerId = null; | ||||
| } | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => props.channelId ? { | ||||
| 	title: i18n.ts._channel.edit, | ||||
| 	icon: 'fas fa-satellite-dish', | ||||
| 	bg: 'var(--bg)', | ||||
| } : { | ||||
| 	title: i18n.ts._channel.create, | ||||
| 	icon: 'fas fa-satellite-dish', | ||||
| 	bg: 'var(--bg)', | ||||
| })); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,98 +1,87 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| 	<div v-if="channel"> | ||||
| 		<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }"> | ||||
| 			<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/> | ||||
| 			<button class="_button toggle" @click="() => showBanner = !showBanner"> | ||||
| 				<template v-if="showBanner"><i class="fas fa-angle-up"></i></template> | ||||
| 				<template v-else><i class="fas fa-angle-down"></i></template> | ||||
| 			</button> | ||||
| 			<div v-if="!showBanner" class="hideOverlay"> | ||||
| 			</div> | ||||
| 			<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner"> | ||||
| 				<div class="status"> | ||||
| 					<div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> | ||||
| 					<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="700"> | ||||
| 		<div v-if="channel"> | ||||
| 			<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }"> | ||||
| 				<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/> | ||||
| 				<button class="_button toggle" @click="() => showBanner = !showBanner"> | ||||
| 					<template v-if="showBanner"><i class="fas fa-angle-up"></i></template> | ||||
| 					<template v-else><i class="fas fa-angle-down"></i></template> | ||||
| 				</button> | ||||
| 				<div v-if="!showBanner" class="hideOverlay"> | ||||
| 				</div> | ||||
| 				<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner"> | ||||
| 					<div class="status"> | ||||
| 						<div><i class="fas fa-users fa-fw"></i><I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> | ||||
| 						<div><i class="fas fa-pencil-alt fa-fw"></i><I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> | ||||
| 					</div> | ||||
| 					<div class="fade"></div> | ||||
| 				</div> | ||||
| 				<div v-if="channel.description" class="description"> | ||||
| 					<Mfm :text="channel.description" :is-note="false" :i="$i"/> | ||||
| 				</div> | ||||
| 				<div class="fade"></div> | ||||
| 			</div> | ||||
| 			<div v-if="channel.description" class="description"> | ||||
| 				<Mfm :text="channel.description" :is-note="false" :i="$i"/> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<XPostForm v-if="$i" :channel="channel" class="post-form _panel _gap" fixed/> | ||||
| 
 | ||||
| 			<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<XPostForm v-if="$i" :channel="channel" class="post-form _panel _gap" fixed/> | ||||
| 
 | ||||
| 		<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, inject, watch } from 'vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import XPostForm from '@/components/post-form.vue'; | ||||
| import XTimeline from '@/components/timeline.vue'; | ||||
| import XChannelFollowButton from '@/components/channel-follow-button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { $i } from '@/account'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		XPostForm, | ||||
| 		XTimeline, | ||||
| 		XChannelFollowButton | ||||
| 	}, | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| 	props: { | ||||
| 		channelId: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	channelId: string; | ||||
| }>(); | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.channel ? { | ||||
| 				title: this.channel.name, | ||||
| 				icon: 'fas fa-satellite-dish', | ||||
| 				bg: 'var(--bg)', | ||||
| 				actions: [...(this.$i && this.$i.id === this.channel.userId ? [{ | ||||
| 					icon: 'fas fa-cog', | ||||
| 					text: this.$ts.edit, | ||||
| 					handler: this.edit, | ||||
| 				}] : [])], | ||||
| 			} : null), | ||||
| 			channel: null, | ||||
| 			showBanner: true, | ||||
| 			pagination: { | ||||
| 				endpoint: 'channels/timeline' as const, | ||||
| 				limit: 10, | ||||
| 				params: computed(() => ({ | ||||
| 					channelId: this.channelId, | ||||
| 				})) | ||||
| 			}, | ||||
| 		}; | ||||
| 	}, | ||||
| let channel = $ref(null); | ||||
| let showBanner = $ref(true); | ||||
| const pagination = { | ||||
| 	endpoint: 'channels/timeline' as const, | ||||
| 	limit: 10, | ||||
| 	params: computed(() => ({ | ||||
| 		channelId: props.channelId, | ||||
| 	})), | ||||
| }; | ||||
| 
 | ||||
| 	watch: { | ||||
| 		channelId: { | ||||
| 			async handler() { | ||||
| 				this.channel = await os.api('channels/show', { | ||||
| 					channelId: this.channelId, | ||||
| 				}); | ||||
| 			}, | ||||
| 			immediate: true | ||||
| 		} | ||||
| 	}, | ||||
| watch(() => props.channelId, async () => { | ||||
| 	channel = await os.api('channels/show', { | ||||
| 		channelId: props.channelId, | ||||
| 	}); | ||||
| }, { immediate: true }); | ||||
| 
 | ||||
| 	methods: { | ||||
| 		edit() { | ||||
| 			this.$router.push(`/channels/${this.channel.id}/edit`); | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
| function edit() { | ||||
| 	router.push(`/channels/${channel.id}/edit`); | ||||
| } | ||||
| 
 | ||||
| const headerActions = $computed(() => channel && channel.userId ? [{ | ||||
| 	icon: 'fas fa-cog', | ||||
| 	text: i18n.ts.edit, | ||||
| 	handler: edit, | ||||
| }] : null); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => channel ? { | ||||
| 	title: channel.name, | ||||
| 	icon: 'fas fa-satellite-dish', | ||||
| 	bg: 'var(--bg)', | ||||
| } : null)); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,82 +1,83 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| 	<div v-if="tab === 'featured'" class="_content grwlizim featured"> | ||||
| 		<MkPagination v-slot="{items}" :pagination="featuredPagination"> | ||||
| 			<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| 	<div v-else-if="tab === 'following'" class="_content grwlizim following"> | ||||
| 		<MkPagination v-slot="{items}" :pagination="followingPagination"> | ||||
| 			<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| 	<div v-else-if="tab === 'owned'" class="_content grwlizim owned"> | ||||
| 		<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> | ||||
| 		<MkPagination v-slot="{items}" :pagination="ownedPagination"> | ||||
| 			<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="700"> | ||||
| 		<div v-if="tab === 'featured'" class="_content grwlizim featured"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="featuredPagination"> | ||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'following'" class="_content grwlizim following"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="followingPagination"> | ||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'owned'" class="_content grwlizim owned"> | ||||
| 			<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> | ||||
| 			<MkPagination v-slot="{items}" :pagination="ownedPagination"> | ||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineComponent, inject } from 'vue'; | ||||
| import MkChannelPreview from '@/components/channel-preview.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { i18n } from '@/i18n'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkChannelPreview, MkPagination, MkButton, | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 				title: this.$ts.channel, | ||||
| 				icon: 'fas fa-satellite-dish', | ||||
| 				bg: 'var(--bg)', | ||||
| 				actions: [{ | ||||
| 					icon: 'fas fa-plus', | ||||
| 					text: this.$ts.create, | ||||
| 					handler: this.create, | ||||
| 				}], | ||||
| 				tabs: [{ | ||||
| 					active: this.tab === 'featured', | ||||
| 					title: this.$ts._channel.featured, | ||||
| 					icon: 'fas fa-fire-alt', | ||||
| 					onClick: () => { this.tab = 'featured'; }, | ||||
| 				}, { | ||||
| 					active: this.tab === 'following', | ||||
| 					title: this.$ts._channel.following, | ||||
| 					icon: 'fas fa-heart', | ||||
| 					onClick: () => { this.tab = 'following'; }, | ||||
| 				}, { | ||||
| 					active: this.tab === 'owned', | ||||
| 					title: this.$ts._channel.owned, | ||||
| 					icon: 'fas fa-edit', | ||||
| 					onClick: () => { this.tab = 'owned'; }, | ||||
| 				},] | ||||
| 			})), | ||||
| 			tab: 'featured', | ||||
| 			featuredPagination: { | ||||
| 				endpoint: 'channels/featured' as const, | ||||
| 				noPaging: true, | ||||
| 			}, | ||||
| 			followingPagination: { | ||||
| 				endpoint: 'channels/followed' as const, | ||||
| 				limit: 5, | ||||
| 			}, | ||||
| 			ownedPagination: { | ||||
| 				endpoint: 'channels/owned' as const, | ||||
| 				limit: 5, | ||||
| 			}, | ||||
| 		}; | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		create() { | ||||
| 			this.$router.push(`/channels/new`); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| let tab = $ref('featured'); | ||||
| 
 | ||||
| const featuredPagination = { | ||||
| 	endpoint: 'channels/featured' as const, | ||||
| 	noPaging: true, | ||||
| }; | ||||
| const followingPagination = { | ||||
| 	endpoint: 'channels/followed' as const, | ||||
| 	limit: 5, | ||||
| }; | ||||
| const ownedPagination = { | ||||
| 	endpoint: 'channels/owned' as const, | ||||
| 	limit: 5, | ||||
| }; | ||||
| 
 | ||||
| function create() { | ||||
| 	router.push('/channels/new'); | ||||
| } | ||||
| 
 | ||||
| const headerActions = $computed(() => [{ | ||||
| 	icon: 'fas fa-plus', | ||||
| 	text: i18n.ts.create, | ||||
| 	handler: create, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => [{ | ||||
| 	active: tab === 'featured', | ||||
| 	title: i18n.ts._channel.featured, | ||||
| 	icon: 'fas fa-fire-alt', | ||||
| 	onClick: () => { tab = 'featured'; }, | ||||
| }, { | ||||
| 	active: tab === 'following', | ||||
| 	title: i18n.ts._channel.following, | ||||
| 	icon: 'fas fa-heart', | ||||
| 	onClick: () => { tab = 'following'; }, | ||||
| }, { | ||||
| 	active: tab === 'owned', | ||||
| 	title: i18n.ts._channel.owned, | ||||
| 	icon: 'fas fa-edit', | ||||
| 	onClick: () => { tab = 'owned'; }, | ||||
| }]); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: i18n.ts.channel, | ||||
| 	icon: 'fas fa-satellite-dish', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,18 +1,21 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800"> | ||||
| 	<div v-if="clip"> | ||||
| 		<div class="okzinsic _panel"> | ||||
| 			<div v-if="clip.description" class="description"> | ||||
| 				<Mfm :text="clip.description" :is-note="false" :i="$i"/> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions"/></template> | ||||
| 		<MkSpacer :content-max="800"> | ||||
| 		<div v-if="clip"> | ||||
| 			<div class="okzinsic _panel"> | ||||
| 				<div v-if="clip.description" class="description"> | ||||
| 					<Mfm :text="clip.description" :is-note="false" :i="$i"/> | ||||
| 				</div> | ||||
| 				<div class="user"> | ||||
| 					<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="user"> | ||||
| 				<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<XNotes :pagination="pagination" :detail="true"/> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 			<XNotes :pagination="pagination" :detail="true"/> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -22,7 +25,7 @@ import XNotes from '@/components/notes.vue'; | |||
| import { $i } from '@/account'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	clipId: string, | ||||
|  | @ -49,59 +52,58 @@ watch(() => props.clipId, async () => { | |||
| 
 | ||||
| provide('currentClipPage', $$(clip)); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => clip ? { | ||||
| 		title: clip.name, | ||||
| 		icon: 'fas fa-paperclip', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: isOwned ? [{ | ||||
| 			icon: 'fas fa-pencil-alt', | ||||
| 			text: i18n.ts.edit, | ||||
| 			handler: async (): Promise<void> => { | ||||
| 				const { canceled, result } = await os.form(clip.name, { | ||||
| 					name: { | ||||
| 						type: 'string', | ||||
| 						label: i18n.ts.name, | ||||
| 						default: clip.name, | ||||
| 					}, | ||||
| 					description: { | ||||
| 						type: 'string', | ||||
| 						required: false, | ||||
| 						multiline: true, | ||||
| 						label: i18n.ts.description, | ||||
| 						default: clip.description, | ||||
| 					}, | ||||
| 					isPublic: { | ||||
| 						type: 'boolean', | ||||
| 						label: i18n.ts.public, | ||||
| 						default: clip.isPublic, | ||||
| 					}, | ||||
| 				}); | ||||
| 				if (canceled) return; | ||||
| 
 | ||||
| 				os.apiWithDialog('clips/update', { | ||||
| 					clipId: clip.id, | ||||
| 					...result, | ||||
| 				}); | ||||
| const headerActions = $computed(() => clip && isOwned ? [{ | ||||
| 	icon: 'fas fa-pencil-alt', | ||||
| 	text: i18n.ts.edit, | ||||
| 	handler: async (): Promise<void> => { | ||||
| 		const { canceled, result } = await os.form(clip.name, { | ||||
| 			name: { | ||||
| 				type: 'string', | ||||
| 				label: i18n.ts.name, | ||||
| 				default: clip.name, | ||||
| 			}, | ||||
| 		}, { | ||||
| 			icon: 'fas fa-trash-alt', | ||||
| 			text: i18n.ts.delete, | ||||
| 			danger: true, | ||||
| 			handler: async (): Promise<void> => { | ||||
| 				const { canceled } = await os.confirm({ | ||||
| 					type: 'warning', | ||||
| 					text: i18n.t('deleteAreYouSure', { x: clip.name }), | ||||
| 				}); | ||||
| 				if (canceled) return; | ||||
| 
 | ||||
| 				await os.apiWithDialog('clips/delete', { | ||||
| 					clipId: clip.id, | ||||
| 				}); | ||||
| 			description: { | ||||
| 				type: 'string', | ||||
| 				required: false, | ||||
| 				multiline: true, | ||||
| 				label: i18n.ts.description, | ||||
| 				default: clip.description, | ||||
| 			}, | ||||
| 		}] : [], | ||||
| 	} : null), | ||||
| }); | ||||
| 			isPublic: { | ||||
| 				type: 'boolean', | ||||
| 				label: i18n.ts.public, | ||||
| 				default: clip.isPublic, | ||||
| 			}, | ||||
| 		}); | ||||
| 		if (canceled) return; | ||||
| 
 | ||||
| 		os.apiWithDialog('clips/update', { | ||||
| 			clipId: clip.id, | ||||
| 			...result, | ||||
| 		}); | ||||
| 	}, | ||||
| }, { | ||||
| 	icon: 'fas fa-trash-alt', | ||||
| 	text: i18n.ts.delete, | ||||
| 	danger: true, | ||||
| 	handler: async (): Promise<void> => { | ||||
| 		const { canceled } = await os.confirm({ | ||||
| 			type: 'warning', | ||||
| 			text: i18n.t('deleteAreYouSure', { x: clip.name }), | ||||
| 		}); | ||||
| 		if (canceled) return; | ||||
| 
 | ||||
| 		await os.apiWithDialog('clips/delete', { | ||||
| 			clipId: clip.id, | ||||
| 		}); | ||||
| 	}, | ||||
| }] : null); | ||||
| 
 | ||||
| definePageMetadata(computed(() => clip ? { | ||||
| 	title: clip.name, | ||||
| 	icon: 'fas fa-paperclip', | ||||
| 	bg: 'var(--bg)', | ||||
| } : null)); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -8,17 +8,19 @@ | |||
| import { computed } from 'vue'; | ||||
| import XDrive from '@/components/drive.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let folder = $ref(null); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: folder ? folder.name : i18n.ts.drive, | ||||
| 		icon: 'fas fa-cloud', | ||||
| 		bg: 'var(--bg)', | ||||
| 		hideHeader: true, | ||||
| 	})), | ||||
| }); | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: folder ? folder.name : i18n.ts.drive, | ||||
| 	icon: 'fas fa-cloud', | ||||
| 	bg: 'var(--bg)', | ||||
| 	hideHeader: true, | ||||
| }))); | ||||
| </script> | ||||
|  |  | |||
|  | @ -36,7 +36,6 @@ import MkSelect from '@/components/form/select.vue'; | |||
| import MkFolder from '@/components/ui/folder.vue'; | ||||
| import MkTab from '@/components/tab.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { emojiCategories, emojiTags } from '@/instance'; | ||||
| import XEmoji from './emojis.emoji.vue'; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,15 +1,18 @@ | |||
| <template> | ||||
| <div :class="$style.root"> | ||||
| 	<XCategory v-if="tab === 'category'"/> | ||||
| </div> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<div :class="$style.root"> | ||||
| 		<XCategory v-if="tab === 'category'"/> | ||||
| 	</div> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { ref, computed } from 'vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import XCategory from './emojis.category.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const tab = ref('category'); | ||||
| 
 | ||||
|  | @ -31,20 +34,21 @@ function menu(ev) { | |||
| 					text: err.message, | ||||
| 				}); | ||||
| 			}); | ||||
| 		} | ||||
| 		}, | ||||
| 	}], ev.currentTarget ?? ev.target); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.customEmojis, | ||||
| 		icon: 'fas fa-laugh', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			icon: 'fas fa-ellipsis-h', | ||||
| 			handler: menu, | ||||
| 		}], | ||||
| 	}, | ||||
| const headerActions = $computed(() => [{ | ||||
| 	icon: 'fas fa-ellipsis-h', | ||||
| 	handler: menu, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.customEmojis, | ||||
| 	icon: 'fas fa-laugh', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,11 +1,12 @@ | |||
| <template> | ||||
| <div> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="1200"> | ||||
| 		<div class="lznhrdub"> | ||||
| 			<div v-if="tab === 'local'"> | ||||
| 				<div v-if="meta && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> | ||||
| 					<header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header> | ||||
| 					<div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div> | ||||
| 				<div v-if="instance && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: instance.bannerUrl ? `url(${instance.bannerUrl})` : null }"> | ||||
| 					<header><span>{{ $t('explore', { host: instance.name || 'Misskey' }) }}</span></header> | ||||
| 					<div><span>{{ $t('exploreUsersCount', { count: number(stats.originalUsersCount) }) }}</span></div> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<template v-if="tag == null"> | ||||
|  | @ -32,7 +33,7 @@ | |||
| 					<header><span>{{ $ts.exploreFediverse }}</span></header> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<MkFolder ref="tags" :foldable="true" :expanded="false" class="_gap"> | ||||
| 				<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap"> | ||||
| 					<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template> | ||||
| 
 | ||||
| 					<div class="vxjfqztj"> | ||||
|  | @ -74,147 +75,127 @@ | |||
| 					</MkRadios> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<XUserList v-if="searchQuery" ref="search" class="_gap" :pagination="searchPagination"/> | ||||
| 				<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </div> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineComponent, watch } from 'vue'; | ||||
| import XUserList from '@/components/user-list.vue'; | ||||
| import MkFolder from '@/components/ui/folder.vue'; | ||||
| import MkInput from '@/components/form/input.vue'; | ||||
| import MkRadios from '@/components/form/radios.vue'; | ||||
| import number from '@/filters/number'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { instance } from '@/instance'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XUserList, | ||||
| 		MkFolder, | ||||
| 		MkInput, | ||||
| 		MkRadios, | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	tag?: string; | ||||
| }>(); | ||||
| 
 | ||||
| 	props: { | ||||
| 		tag: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| let tab = $ref('local'); | ||||
| let tagsEl = $ref<InstanceType<typeof MkFolder>>(); | ||||
| let tagsLocal = $ref([]); | ||||
| let tagsRemote = $ref([]); | ||||
| let stats = $ref(null); | ||||
| let searchQuery = $ref(null); | ||||
| let searchOrigin = $ref('combined'); | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 				title: this.$ts.explore, | ||||
| 				icon: 'fas fa-hashtag', | ||||
| 				bg: 'var(--bg)', | ||||
| 				tabs: [{ | ||||
| 					active: this.tab === 'local', | ||||
| 					title: this.$ts.local, | ||||
| 					onClick: () => { this.tab = 'local'; }, | ||||
| 				}, { | ||||
| 					active: this.tab === 'remote', | ||||
| 					title: this.$ts.remote, | ||||
| 					onClick: () => { this.tab = 'remote'; }, | ||||
| 				}, { | ||||
| 					active: this.tab === 'search', | ||||
| 					title: this.$ts.search, | ||||
| 					onClick: () => { this.tab = 'search'; }, | ||||
| 				},] | ||||
| 			})), | ||||
| 			tab: 'local', | ||||
| 			pinnedUsers: { endpoint: 'pinned-users' }, | ||||
| 			popularUsers: { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 				state: 'alive', | ||||
| 				origin: 'local', | ||||
| 				sort: '+follower', | ||||
| 			} }, | ||||
| 			recentlyUpdatedUsers: { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 				origin: 'local', | ||||
| 				sort: '+updatedAt', | ||||
| 			} }, | ||||
| 			recentlyRegisteredUsers: { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 				origin: 'local', | ||||
| 				state: 'alive', | ||||
| 				sort: '+createdAt', | ||||
| 			} }, | ||||
| 			popularUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 				state: 'alive', | ||||
| 				origin: 'remote', | ||||
| 				sort: '+follower', | ||||
| 			} }, | ||||
| 			recentlyUpdatedUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 				origin: 'combined', | ||||
| 				sort: '+updatedAt', | ||||
| 			} }, | ||||
| 			recentlyRegisteredUsersF: { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 				origin: 'combined', | ||||
| 				sort: '+createdAt', | ||||
| 			} }, | ||||
| 			searchPagination: { | ||||
| 				endpoint: 'users/search' as const, | ||||
| 				limit: 10, | ||||
| 				params: computed(() => (this.searchQuery && this.searchQuery !== '') ? { | ||||
| 					query: this.searchQuery, | ||||
| 					origin: this.searchOrigin, | ||||
| 				} : null) | ||||
| 			}, | ||||
| 			tagsLocal: [], | ||||
| 			tagsRemote: [], | ||||
| 			stats: null, | ||||
| 			searchQuery: null, | ||||
| 			searchOrigin: 'combined', | ||||
| 			num: number, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		meta() { | ||||
| 			return this.$instance; | ||||
| 		}, | ||||
| 		tagUsers(): any { | ||||
| 			return { | ||||
| 				endpoint: 'hashtags/users' as const, | ||||
| 				limit: 30, | ||||
| 				params: { | ||||
| 					tag: this.tag, | ||||
| 					origin: 'combined', | ||||
| 					sort: '+follower', | ||||
| 				} | ||||
| 			}; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		tag() { | ||||
| 			if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		os.api('hashtags/list', { | ||||
| 			sort: '+attachedLocalUsers', | ||||
| 			attachedToLocalUserOnly: true, | ||||
| 			limit: 30 | ||||
| 		}).then(tags => { | ||||
| 			this.tagsLocal = tags; | ||||
| 		}); | ||||
| 		os.api('hashtags/list', { | ||||
| 			sort: '+attachedRemoteUsers', | ||||
| 			attachedToRemoteUserOnly: true, | ||||
| 			limit: 30 | ||||
| 		}).then(tags => { | ||||
| 			this.tagsRemote = tags; | ||||
| 		}); | ||||
| 		os.api('stats').then(stats => { | ||||
| 			this.stats = stats; | ||||
| 		}); | ||||
| 	}, | ||||
| watch(() => props.tag, () => { | ||||
| 	if (tagsEl) tagsEl.toggleContent(props.tag == null); | ||||
| }); | ||||
| 
 | ||||
| const tagUsers = $computed(() => ({ | ||||
| 	endpoint: 'hashtags/users' as const, | ||||
| 	limit: 30, | ||||
| 	params: { | ||||
| 		tag: props.tag, | ||||
| 		origin: 'combined', | ||||
| 		sort: '+follower', | ||||
| 	}, | ||||
| })); | ||||
| 
 | ||||
| const pinnedUsers = { endpoint: 'pinned-users' }; | ||||
| const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 	state: 'alive', | ||||
| 	origin: 'local', | ||||
| 	sort: '+follower', | ||||
| } }; | ||||
| const recentlyUpdatedUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 	origin: 'local', | ||||
| 	sort: '+updatedAt', | ||||
| } }; | ||||
| const recentlyRegisteredUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 	origin: 'local', | ||||
| 	state: 'alive', | ||||
| 	sort: '+createdAt', | ||||
| } }; | ||||
| const popularUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 	state: 'alive', | ||||
| 	origin: 'remote', | ||||
| 	sort: '+follower', | ||||
| } }; | ||||
| const recentlyUpdatedUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 	origin: 'combined', | ||||
| 	sort: '+updatedAt', | ||||
| } }; | ||||
| const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||
| 	origin: 'combined', | ||||
| 	sort: '+createdAt', | ||||
| } }; | ||||
| const searchPagination = { | ||||
| 	endpoint: 'users/search' as const, | ||||
| 	limit: 10, | ||||
| 	params: computed(() => (searchQuery && searchQuery !== '') ? { | ||||
| 		query: searchQuery, | ||||
| 		origin: searchOrigin, | ||||
| 	} : null), | ||||
| }; | ||||
| 
 | ||||
| os.api('hashtags/list', { | ||||
| 	sort: '+attachedLocalUsers', | ||||
| 	attachedToLocalUserOnly: true, | ||||
| 	limit: 30, | ||||
| }).then(tags => { | ||||
| 	tagsLocal = tags; | ||||
| }); | ||||
| os.api('hashtags/list', { | ||||
| 	sort: '+attachedRemoteUsers', | ||||
| 	attachedToRemoteUserOnly: true, | ||||
| 	limit: 30, | ||||
| }).then(tags => { | ||||
| 	tagsRemote = tags; | ||||
| }); | ||||
| os.api('stats').then(_stats => { | ||||
| 	stats = _stats; | ||||
| }); | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => [{ | ||||
| 	active: tab === 'local', | ||||
| 	title: i18n.ts.local, | ||||
| 	onClick: () => { tab = 'local'; }, | ||||
| }, { | ||||
| 	active: tab === 'remote', | ||||
| 	title: i18n.ts.remote, | ||||
| 	onClick: () => { tab = 'remote'; }, | ||||
| }, { | ||||
| 	active: tab === 'search', | ||||
| 	title: i18n.ts.search, | ||||
| 	onClick: () => { tab = 'search'; }, | ||||
| }]); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: i18n.ts.explore, | ||||
| 	icon: 'fas fa-hashtag', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,20 +1,23 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800"> | ||||
| 	<MkPagination ref="pagingComponent" :pagination="pagination"> | ||||
| 		<template #empty> | ||||
| 			<div class="_fullinfo"> | ||||
| 				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 				<div>{{ $ts.noNotes }}</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader/></template> | ||||
| 	<MkSpacer :content-max="800"> | ||||
| 		<MkPagination ref="pagingComponent" :pagination="pagination"> | ||||
| 			<template #empty> | ||||
| 				<div class="_fullinfo"> | ||||
| 					<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 					<div>{{ $ts.noNotes }}</div> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 
 | ||||
| 		<template #default="{ items }"> | ||||
| 			<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false"> | ||||
| 				<XNote :key="item.id" :note="item.note" :class="$style.note"/> | ||||
| 			</XList> | ||||
| 		</template> | ||||
| 	</MkPagination> | ||||
| </MkSpacer> | ||||
| 			<template #default="{ items }"> | ||||
| 				<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false"> | ||||
| 					<XNote :key="item.id" :note="item.note" :class="$style.note"/> | ||||
| 				</XList> | ||||
| 			</template> | ||||
| 		</MkPagination> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -22,8 +25,8 @@ import { ref } from 'vue'; | |||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import XNote from '@/components/note.vue'; | ||||
| import XList from '@/components/date-separated-list.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const pagination = { | ||||
| 	endpoint: 'i/favorites' as const, | ||||
|  | @ -32,12 +35,10 @@ const pagination = { | |||
| 
 | ||||
| const pagingComponent = ref<InstanceType<typeof MkPagination>>(); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.favorites, | ||||
| 		icon: 'fas fa-star', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.favorites, | ||||
| 	icon: 'fas fa-star', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,13 +1,16 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800"> | ||||
| 	<XNotes ref="notes" :pagination="pagination"/> | ||||
| </MkSpacer> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader/></template> | ||||
| 	<MkSpacer :content-max="800"> | ||||
| 		<XNotes ref="notes" :pagination="pagination"/> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import XNotes from '@/components/notes.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const pagination = { | ||||
| 	endpoint: 'notes/featured' as const, | ||||
|  | @ -15,11 +18,9 @@ const pagination = { | |||
| 	offsetMode: true, | ||||
| }; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.featured, | ||||
| 		icon: 'fas fa-fire-alt', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.featured, | ||||
| 	icon: 'fas fa-fire-alt', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,94 +1,97 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="1000"> | ||||
| 	<div class="taeiyria"> | ||||
| 		<div class="query"> | ||||
| 			<MkInput v-model="host" :debounce="true" class=""> | ||||
| 				<template #prefix><i class="fas fa-search"></i></template> | ||||
| 				<template #label>{{ $ts.host }}</template> | ||||
| 			</MkInput> | ||||
| 			<FormSplit style="margin-top: var(--margin);"> | ||||
| 				<MkSelect v-model="state"> | ||||
| 					<template #label>{{ $ts.state }}</template> | ||||
| 					<option value="all">{{ $ts.all }}</option> | ||||
| 					<option value="federating">{{ $ts.federating }}</option> | ||||
| 					<option value="subscribing">{{ $ts.subscribing }}</option> | ||||
| 					<option value="publishing">{{ $ts.publishing }}</option> | ||||
| 					<option value="suspended">{{ $ts.suspended }}</option> | ||||
| 					<option value="blocked">{{ $ts.blocked }}</option> | ||||
| 					<option value="notResponding">{{ $ts.notResponding }}</option> | ||||
| 				</MkSelect> | ||||
| 				<MkSelect v-model="sort"> | ||||
| 					<template #label>{{ $ts.sort }}</template> | ||||
| 					<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option> | ||||
| 					<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option> | ||||
| 				</MkSelect> | ||||
| 			</FormSplit> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> | ||||
| 			<div class="dqokceoi"> | ||||
| 				<MkA v-for="instance in items" :key="instance.id" class="instance" :to="`/instance-info/${instance.host}`"> | ||||
| 					<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div> | ||||
| 					<div class="table"> | ||||
| 						<div class="cell"> | ||||
| 							<div class="key">{{ $ts.registeredAt }}</div> | ||||
| 							<div class="value"><MkTime :time="instance.caughtAt"/></div> | ||||
| 						</div> | ||||
| 						<div class="cell"> | ||||
| 							<div class="key">{{ $ts.software }}</div> | ||||
| 							<div class="value">{{ instance.softwareName || `(${$ts.unknown})` }}</div> | ||||
| 						</div> | ||||
| 						<div class="cell"> | ||||
| 							<div class="key">{{ $ts.version }}</div> | ||||
| 							<div class="value">{{ instance.softwareVersion || `(${$ts.unknown})` }}</div> | ||||
| 						</div> | ||||
| 						<div class="cell"> | ||||
| 							<div class="key">{{ $ts.users }}</div> | ||||
| 							<div class="value">{{ instance.usersCount }}</div> | ||||
| 						</div> | ||||
| 						<div class="cell"> | ||||
| 							<div class="key">{{ $ts.notes }}</div> | ||||
| 							<div class="value">{{ instance.notesCount }}</div> | ||||
| 						</div> | ||||
| 						<div class="cell"> | ||||
| 							<div class="key">{{ $ts.sent }}</div> | ||||
| 							<div class="value"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div> | ||||
| 						</div> | ||||
| 						<div class="cell"> | ||||
| 							<div class="key">{{ $ts.received }}</div> | ||||
| 							<div class="value"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div class="footer"> | ||||
| 						<span class="status" :class="getStatus(instance)">{{ getStatus(instance) }}</span> | ||||
| 						<span class="pubSub"> | ||||
| 							<span v-if="instance.followersCount > 0" class="sub"><i class="fas fa-caret-down icon"></i>Sub</span> | ||||
| 							<span v-else class="sub"><i class="fas fa-caret-down icon"></i>-</span> | ||||
| 							<span v-if="instance.followingCount > 0" class="pub"><i class="fas fa-caret-up icon"></i>Pub</span> | ||||
| 							<span v-else class="pub"><i class="fas fa-caret-up icon"></i>-</span> | ||||
| 						</span> | ||||
| 						<span class="right"> | ||||
| 							<span class="latestStatus">{{ instance.latestStatus || '-' }}</span> | ||||
| 							<span class="lastCommunicatedAt"><MkTime :time="instance.lastCommunicatedAt"/></span> | ||||
| 						</span> | ||||
| 					</div> | ||||
| 				</MkA> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="1000"> | ||||
| 		<div class="taeiyria"> | ||||
| 			<div class="query"> | ||||
| 				<MkInput v-model="host" :debounce="true" class=""> | ||||
| 					<template #prefix><i class="fas fa-search"></i></template> | ||||
| 					<template #label>{{ $ts.host }}</template> | ||||
| 				</MkInput> | ||||
| 				<FormSplit style="margin-top: var(--margin);"> | ||||
| 					<MkSelect v-model="state"> | ||||
| 						<template #label>{{ $ts.state }}</template> | ||||
| 						<option value="all">{{ $ts.all }}</option> | ||||
| 						<option value="federating">{{ $ts.federating }}</option> | ||||
| 						<option value="subscribing">{{ $ts.subscribing }}</option> | ||||
| 						<option value="publishing">{{ $ts.publishing }}</option> | ||||
| 						<option value="suspended">{{ $ts.suspended }}</option> | ||||
| 						<option value="blocked">{{ $ts.blocked }}</option> | ||||
| 						<option value="notResponding">{{ $ts.notResponding }}</option> | ||||
| 					</MkSelect> | ||||
| 					<MkSelect v-model="sort"> | ||||
| 						<template #label>{{ $ts.sort }}</template> | ||||
| 						<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option> | ||||
| 						<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option> | ||||
| 						<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option> | ||||
| 						<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option> | ||||
| 						<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option> | ||||
| 						<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option> | ||||
| 						<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option> | ||||
| 						<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option> | ||||
| 						<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option> | ||||
| 						<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option> | ||||
| 						<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option> | ||||
| 						<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option> | ||||
| 						<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option> | ||||
| 						<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option> | ||||
| 					</MkSelect> | ||||
| 				</FormSplit> | ||||
| 			</div> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 
 | ||||
| 			<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> | ||||
| 				<div class="dqokceoi"> | ||||
| 					<MkA v-for="instance in items" :key="instance.id" class="instance" :to="`/instance-info/${instance.host}`"> | ||||
| 						<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div> | ||||
| 						<div class="table"> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.registeredAt }}</div> | ||||
| 								<div class="value"><MkTime :time="instance.caughtAt"/></div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.software }}</div> | ||||
| 								<div class="value">{{ instance.softwareName || `(${$ts.unknown})` }}</div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.version }}</div> | ||||
| 								<div class="value">{{ instance.softwareVersion || `(${$ts.unknown})` }}</div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.users }}</div> | ||||
| 								<div class="value">{{ instance.usersCount }}</div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.notes }}</div> | ||||
| 								<div class="value">{{ instance.notesCount }}</div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.sent }}</div> | ||||
| 								<div class="value"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div> | ||||
| 							</div> | ||||
| 							<div class="cell"> | ||||
| 								<div class="key">{{ $ts.received }}</div> | ||||
| 								<div class="value"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 						<div class="footer"> | ||||
| 							<span class="status" :class="getStatus(instance)">{{ getStatus(instance) }}</span> | ||||
| 							<span class="pubSub"> | ||||
| 								<span v-if="instance.followersCount > 0" class="sub"><i class="fas fa-caret-down icon"></i>Sub</span> | ||||
| 								<span v-else class="sub"><i class="fas fa-caret-down icon"></i>-</span> | ||||
| 								<span v-if="instance.followingCount > 0" class="pub"><i class="fas fa-caret-up icon"></i>Pub</span> | ||||
| 								<span v-else class="pub"><i class="fas fa-caret-up icon"></i>-</span> | ||||
| 							</span> | ||||
| 							<span class="right"> | ||||
| 								<span class="latestStatus">{{ instance.latestStatus || '-' }}</span> | ||||
| 								<span class="lastCommunicatedAt"><MkTime :time="instance.lastCommunicatedAt"/></span> | ||||
| 							</span> | ||||
| 						</div> | ||||
| 					</MkA> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -99,8 +102,8 @@ import MkSelect from '@/components/form/select.vue'; | |||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import FormSplit from '@/components/form/split.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let host = $ref(''); | ||||
| let state = $ref('federating'); | ||||
|  | @ -119,8 +122,8 @@ const pagination = { | |||
| 			state === 'suspended' ? { suspended: true } : | ||||
| 			state === 'blocked' ? { blocked: true } : | ||||
| 			state === 'notResponding' ? { notResponding: true } : | ||||
| 			{}) | ||||
| 	})) | ||||
| 			{}), | ||||
| 	})), | ||||
| }; | ||||
| 
 | ||||
| function getStatus(instance) { | ||||
|  | @ -129,12 +132,14 @@ function getStatus(instance) { | |||
| 	return 'alive'; | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.federation, | ||||
| 		icon: 'fas fa-globe', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.federation, | ||||
| 	icon: 'fas fa-globe', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
| 				<div>{{ $ts.noFollowRequests }}</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 		<template v-slot="{items}"> | ||||
| 		<template #default="{items}"> | ||||
| 			<div class="mk-follow-requests"> | ||||
| 				<div v-for="req in items" :key="req.id" class="user _panel"> | ||||
| 					<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/> | ||||
|  | @ -36,8 +36,8 @@ import { ref, computed } from 'vue'; | |||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import { userPage, acct } from '@/filters/user'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const paginationComponent = ref<InstanceType<typeof MkPagination>>(); | ||||
| 
 | ||||
|  | @ -58,13 +58,15 @@ function reject(user) { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.ts.followRequests, | ||||
| 		icon: 'fas fa-user-clock', | ||||
| 		bg: 'var(--bg)', | ||||
| 	})), | ||||
| }); | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: i18n.ts.followRequests, | ||||
| 	icon: 'fas fa-user-clock', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -5,8 +5,9 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as Acct from 'misskey-js/built/acct'; | ||||
| import * as os from '@/os'; | ||||
| import { mainRouter } from '@/router'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	created() { | ||||
|  | @ -17,17 +18,17 @@ export default defineComponent({ | |||
| 
 | ||||
| 		if (acct.startsWith('https://')) { | ||||
| 			promise = os.api('ap/show', { | ||||
| 				uri: acct | ||||
| 				uri: acct, | ||||
| 			}); | ||||
| 			promise.then(res => { | ||||
| 				if (res.type === 'User') { | ||||
| 					this.follow(res.object); | ||||
| 				} else if (res.type === 'Note') { | ||||
| 					this.$router.push(`/notes/${res.object.id}`); | ||||
| 					mainRouter.push(`/notes/${res.object.id}`); | ||||
| 				} else { | ||||
| 					os.alert({ | ||||
| 						type: 'error', | ||||
| 						text: 'Not a user' | ||||
| 						text: 'Not a user', | ||||
| 					}).then(() => { | ||||
| 						window.close(); | ||||
| 					}); | ||||
|  | @ -56,9 +57,9 @@ export default defineComponent({ | |||
| 			} | ||||
| 			 | ||||
| 			os.apiWithDialog('following/create', { | ||||
| 				userId: user.id | ||||
| 				userId: user.id, | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -27,8 +27,8 @@ | |||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, inject, watch } from 'vue'; | ||||
| import FormButton from '@/components/ui/button.vue'; | ||||
| import FormInput from '@/components/form/input.vue'; | ||||
| import FormTextarea from '@/components/form/textarea.vue'; | ||||
|  | @ -37,104 +37,87 @@ import FormGroup from '@/components/form/group.vue'; | |||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import { selectFiles } from '@/scripts/select-file'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { i18n } from '@/i18n'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormButton, | ||||
| 		FormInput, | ||||
| 		FormTextarea, | ||||
| 		FormSwitch, | ||||
| 		FormGroup, | ||||
| 		FormSuspense, | ||||
| 	}, | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| 	props: { | ||||
| 		postId: { | ||||
| 			type: String, | ||||
| 			required: false, | ||||
| 			default: null, | ||||
| 		} | ||||
| 	}, | ||||
| 	 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.postId ? { | ||||
| 				title: this.$ts.edit, | ||||
| 				icon: 'fas fa-pencil-alt' | ||||
| 			} : { | ||||
| 				title: this.$ts.postToGallery, | ||||
| 				icon: 'fas fa-pencil-alt' | ||||
| 			}), | ||||
| 			init: null, | ||||
| 			files: [], | ||||
| 			description: null, | ||||
| 			title: null, | ||||
| 			isSensitive: false, | ||||
| 		}; | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	postId?: string; | ||||
| }>(); | ||||
| 
 | ||||
| 	watch: { | ||||
| 		postId: { | ||||
| 			handler() { | ||||
| 				this.init = () => this.postId ? os.api('gallery/posts/show', { | ||||
| 					postId: this.postId | ||||
| 				}).then(post => { | ||||
| 					this.files = post.files; | ||||
| 					this.title = post.title; | ||||
| 					this.description = post.description; | ||||
| 					this.isSensitive = post.isSensitive; | ||||
| 				}) : Promise.resolve(null); | ||||
| 			}, | ||||
| 			immediate: true, | ||||
| 		} | ||||
| 	}, | ||||
| let init = $ref(null); | ||||
| let files = $ref([]); | ||||
| let description = $ref(null); | ||||
| let title = $ref(null); | ||||
| let isSensitive = $ref(false); | ||||
| 
 | ||||
| 	methods: { | ||||
| 		selectFile(evt) { | ||||
| 			selectFiles(evt.currentTarget ?? evt.target, null).then(files => { | ||||
| 				this.files = this.files.concat(files); | ||||
| 			}); | ||||
| 		}, | ||||
| function selectFile(evt) { | ||||
| 	selectFiles(evt.currentTarget ?? evt.target, null).then(selected => { | ||||
| 		files = files.concat(selected); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| 		remove(file) { | ||||
| 			this.files = this.files.filter(f => f.id !== file.id); | ||||
| 		}, | ||||
| function remove(file) { | ||||
| 	files = files.filter(f => f.id !== file.id); | ||||
| } | ||||
| 
 | ||||
| 		async save() { | ||||
| 			if (this.postId) { | ||||
| 				await os.apiWithDialog('gallery/posts/update', { | ||||
| 					postId: this.postId, | ||||
| 					title: this.title, | ||||
| 					description: this.description, | ||||
| 					fileIds: this.files.map(file => file.id), | ||||
| 					isSensitive: this.isSensitive, | ||||
| 				}); | ||||
| 				this.$router.push(`/gallery/${this.postId}`); | ||||
| 			} else { | ||||
| 				const post = await os.apiWithDialog('gallery/posts/create', { | ||||
| 					title: this.title, | ||||
| 					description: this.description, | ||||
| 					fileIds: this.files.map(file => file.id), | ||||
| 					isSensitive: this.isSensitive, | ||||
| 				}); | ||||
| 				this.$router.push(`/gallery/${post.id}`); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		async del() { | ||||
| 			const { canceled } = await os.confirm({ | ||||
| 				type: 'warning', | ||||
| 				text: this.$ts.deleteConfirm, | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 			await os.apiWithDialog('gallery/posts/delete', { | ||||
| 				postId: this.postId, | ||||
| 			}); | ||||
| 			this.$router.push(`/gallery`); | ||||
| 		} | ||||
| async function save() { | ||||
| 	if (props.postId) { | ||||
| 		await os.apiWithDialog('gallery/posts/update', { | ||||
| 			postId: props.postId, | ||||
| 			title: title, | ||||
| 			description: description, | ||||
| 			fileIds: files.map(file => file.id), | ||||
| 			isSensitive: isSensitive, | ||||
| 		}); | ||||
| 		mainRouter.push(`/gallery/${props.postId}`); | ||||
| 	} else { | ||||
| 		const created = await os.apiWithDialog('gallery/posts/create', { | ||||
| 			title: title, | ||||
| 			description: description, | ||||
| 			fileIds: files.map(file => file.id), | ||||
| 			isSensitive: isSensitive, | ||||
| 		}); | ||||
| 		router.push(`/gallery/${created.id}`); | ||||
| 	} | ||||
| }); | ||||
| } | ||||
| 
 | ||||
| async function del() { | ||||
| 	const { canceled } = await os.confirm({ | ||||
| 		type: 'warning', | ||||
| 		text: i18n.ts.deleteConfirm, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 	await os.apiWithDialog('gallery/posts/delete', { | ||||
| 		postId: props.postId, | ||||
| 	}); | ||||
| 	mainRouter.push('/gallery'); | ||||
| } | ||||
| 
 | ||||
| watch(() => props.postId, () => { | ||||
| 	init = () => props.postId ? os.api('gallery/posts/show', { | ||||
| 		postId: props.postId, | ||||
| 	}).then(post => { | ||||
| 		files = post.files; | ||||
| 		title = post.title; | ||||
| 		description = post.description; | ||||
| 		isSensitive = post.isSensitive; | ||||
| 	}) : Promise.resolve(null); | ||||
| }, { immediate: true }); | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => props.postId ? { | ||||
| 	title: i18n.ts.edit, | ||||
| 	icon: 'fas fa-pencil-alt', | ||||
| } : { | ||||
| 	title: i18n.ts.postToGallery, | ||||
| 	icon: 'fas fa-pencil-alt', | ||||
| })); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,49 +1,54 @@ | |||
| <template> | ||||
| <div class="xprsixdl _root"> | ||||
| 	<MkTab v-if="$i" v-model="tab"> | ||||
| 		<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option> | ||||
| 		<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option> | ||||
| 		<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option> | ||||
| 	</MkTab> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="1400"> | ||||
| 		<div class="_root"> | ||||
| 			<MkTab v-if="$i" v-model="tab"> | ||||
| 				<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option> | ||||
| 				<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option> | ||||
| 				<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option> | ||||
| 			</MkTab> | ||||
| 
 | ||||
| 	<div v-if="tab === 'explore'"> | ||||
| 		<MkFolder class="_gap"> | ||||
| 			<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template> | ||||
| 			<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true"> | ||||
| 				<div class="vfpdbgtk"> | ||||
| 					<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 		</MkFolder> | ||||
| 		<MkFolder class="_gap"> | ||||
| 			<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template> | ||||
| 			<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true"> | ||||
| 				<div class="vfpdbgtk"> | ||||
| 					<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> | ||||
| 				</div> | ||||
| 			</MkPagination> | ||||
| 		</MkFolder> | ||||
| 	</div> | ||||
| 	<div v-else-if="tab === 'liked'"> | ||||
| 		<MkPagination v-slot="{items}" :pagination="likedPostsPagination"> | ||||
| 			<div class="vfpdbgtk"> | ||||
| 				<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/> | ||||
| 			<div v-if="tab === 'explore'"> | ||||
| 				<MkFolder class="_gap"> | ||||
| 					<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template> | ||||
| 					<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true"> | ||||
| 						<div class="vfpdbgtk"> | ||||
| 							<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> | ||||
| 						</div> | ||||
| 					</MkPagination> | ||||
| 				</MkFolder> | ||||
| 				<MkFolder class="_gap"> | ||||
| 					<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template> | ||||
| 					<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true"> | ||||
| 						<div class="vfpdbgtk"> | ||||
| 							<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> | ||||
| 						</div> | ||||
| 					</MkPagination> | ||||
| 				</MkFolder> | ||||
| 			</div> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| 	<div v-else-if="tab === 'my'"> | ||||
| 		<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA> | ||||
| 		<MkPagination v-slot="{items}" :pagination="myPostsPagination"> | ||||
| 			<div class="vfpdbgtk"> | ||||
| 				<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> | ||||
| 			<div v-else-if="tab === 'liked'"> | ||||
| 				<MkPagination v-slot="{items}" :pagination="likedPostsPagination"> | ||||
| 					<div class="vfpdbgtk"> | ||||
| 						<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/> | ||||
| 					</div> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| </div> | ||||
| 			<div v-else-if="tab === 'my'"> | ||||
| 				<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA> | ||||
| 				<MkPagination v-slot="{items}" :pagination="myPostsPagination"> | ||||
| 					<div class="vfpdbgtk"> | ||||
| 						<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> | ||||
| 					</div> | ||||
| 				</MkPagination> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineComponent, watch } from 'vue'; | ||||
| import XUserList from '@/components/user-list.vue'; | ||||
| import MkFolder from '@/components/ui/folder.vue'; | ||||
| import MkInput from '@/components/form/input.vue'; | ||||
|  | @ -53,92 +58,60 @@ import MkPagination from '@/components/ui/pagination.vue'; | |||
| import MkGalleryPostPreview from '@/components/gallery-post-preview.vue'; | ||||
| import number from '@/filters/number'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { i18n } from '@/i18n'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XUserList, | ||||
| 		MkFolder, | ||||
| 		MkInput, | ||||
| 		MkButton, | ||||
| 		MkTab, | ||||
| 		MkPagination, | ||||
| 		MkGalleryPostPreview, | ||||
| const props = defineProps<{ | ||||
| 	tag?: string; | ||||
| }>(); | ||||
| 
 | ||||
| let tab = $ref('explore'); | ||||
| let tags = $ref([]); | ||||
| let tagsRef = $ref(); | ||||
| 
 | ||||
| const recentPostsPagination = { | ||||
| 	endpoint: 'gallery/posts' as const, | ||||
| 	limit: 6, | ||||
| }; | ||||
| const popularPostsPagination = { | ||||
| 	endpoint: 'gallery/featured' as const, | ||||
| 	limit: 5, | ||||
| }; | ||||
| const myPostsPagination = { | ||||
| 	endpoint: 'i/gallery/posts' as const, | ||||
| 	limit: 5, | ||||
| }; | ||||
| const likedPostsPagination = { | ||||
| 	endpoint: 'i/gallery/likes' as const, | ||||
| 	limit: 5, | ||||
| }; | ||||
| 
 | ||||
| const tagUsersPagination = $computed(() => ({ | ||||
| 	endpoint: 'hashtags/users' as const, | ||||
| 	limit: 30, | ||||
| 	params: { | ||||
| 		tag: this.tag, | ||||
| 		origin: 'combined', | ||||
| 		sort: '+follower', | ||||
| 	}, | ||||
| })); | ||||
| 
 | ||||
| 	props: { | ||||
| 		tag: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		} | ||||
| 	}, | ||||
| watch(() => props.tag, () => { | ||||
| 	if (tagsRef) tagsRef.tags.toggleContent(props.tag == null); | ||||
| }); | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.gallery, | ||||
| 				icon: 'fas fa-icons' | ||||
| 			}, | ||||
| 			tab: 'explore', | ||||
| 			recentPostsPagination: { | ||||
| 				endpoint: 'gallery/posts' as const, | ||||
| 				limit: 6, | ||||
| 			}, | ||||
| 			popularPostsPagination: { | ||||
| 				endpoint: 'gallery/featured' as const, | ||||
| 				limit: 5, | ||||
| 			}, | ||||
| 			myPostsPagination: { | ||||
| 				endpoint: 'i/gallery/posts' as const, | ||||
| 				limit: 5, | ||||
| 			}, | ||||
| 			likedPostsPagination: { | ||||
| 				endpoint: 'i/gallery/likes' as const, | ||||
| 				limit: 5, | ||||
| 			}, | ||||
| 			tags: [], | ||||
| 		}; | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| 	computed: { | ||||
| 		meta() { | ||||
| 			return this.$instance; | ||||
| 		}, | ||||
| 		tagUsers(): any { | ||||
| 			return { | ||||
| 				endpoint: 'hashtags/users' as const, | ||||
| 				limit: 30, | ||||
| 				params: { | ||||
| 					tag: this.tag, | ||||
| 					origin: 'combined', | ||||
| 					sort: '+follower', | ||||
| 				} | ||||
| 			}; | ||||
| 		}, | ||||
| 	}, | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| 	watch: { | ||||
| 		tag() { | ||||
| 			if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 
 | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 
 | ||||
| 	} | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.gallery, | ||||
| 	icon: 'fas fa-icons', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .xprsixdl { | ||||
| 	max-width: 1400px; | ||||
| 	margin: 0 auto; | ||||
| } | ||||
| 
 | ||||
| .vfpdbgtk { | ||||
| 	display: grid; | ||||
| 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); | ||||
|  |  | |||
|  | @ -49,123 +49,108 @@ | |||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineComponent, inject, watch } from 'vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkGalleryPostPreview from '@/components/gallery-post-preview.vue'; | ||||
| import MkFollowButton from '@/components/follow-button.vue'; | ||||
| import { url } from '@/config'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		ImgWithBlurhash, | ||||
| 		MkPagination, | ||||
| 		MkGalleryPostPreview, | ||||
| 		MkButton, | ||||
| 		MkFollowButton, | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	postId: string; | ||||
| }>(); | ||||
| 
 | ||||
| const post = $ref(null); | ||||
| const error = $ref(null); | ||||
| const otherPostsPagination = { | ||||
| 	endpoint: 'users/gallery/posts' as const, | ||||
| 	limit: 6, | ||||
| 	params: computed(() => ({ | ||||
| 		userId: post.user.id, | ||||
| 	})), | ||||
| }; | ||||
| 
 | ||||
| function fetchPost() { | ||||
| 	post = null; | ||||
| 	os.api('gallery/posts/show', { | ||||
| 		postId: props.postId, | ||||
| 	}).then(_post => { | ||||
| 		post = _post; | ||||
| 	}).catch(_error => { | ||||
| 		error = _error; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function share() { | ||||
| 	navigator.share({ | ||||
| 		title: post.title, | ||||
| 		text: post.description, | ||||
| 		url: `${url}/gallery/${post.id}`, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function shareWithNote() { | ||||
| 	os.post({ | ||||
| 		initialText: `${post.title} ${url}/gallery/${post.id}`, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function like() { | ||||
| 	os.apiWithDialog('gallery/posts/like', { | ||||
| 		postId: props.postId, | ||||
| 	}).then(() => { | ||||
| 		post.isLiked = true; | ||||
| 		post.likedCount++; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function unlike() { | ||||
| 	const confirm = await os.confirm({ | ||||
| 		type: 'warning', | ||||
| 		text: i18n.ts.unlikeConfirm, | ||||
| 	}); | ||||
| 	if (confirm.canceled) return; | ||||
| 	os.apiWithDialog('gallery/posts/unlike', { | ||||
| 		postId: props.postId, | ||||
| 	}).then(() => { | ||||
| 		post.isLiked = false; | ||||
| 		post.likedCount--; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function edit() { | ||||
| 	router.push(`/gallery/${post.id}/edit`); | ||||
| } | ||||
| 
 | ||||
| watch(() => props.postId, fetchPost, { immediate: true }); | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => post ? { | ||||
| 	title: post.title, | ||||
| 	avatar: post.user, | ||||
| 	path: `/gallery/${post.id}`, | ||||
| 	share: { | ||||
| 		title: post.title, | ||||
| 		text: post.description, | ||||
| 	}, | ||||
| 	props: { | ||||
| 		postId: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.post ? { | ||||
| 				title: this.post.title, | ||||
| 				avatar: this.post.user, | ||||
| 				path: `/gallery/${this.post.id}`, | ||||
| 				share: { | ||||
| 					title: this.post.title, | ||||
| 					text: this.post.description, | ||||
| 				}, | ||||
| 				actions: [{ | ||||
| 					icon: 'fas fa-pencil-alt', | ||||
| 					text: this.$ts.edit, | ||||
| 					handler: this.edit | ||||
| 				}] | ||||
| 			} : null), | ||||
| 			otherPostsPagination: { | ||||
| 				endpoint: 'users/gallery/posts' as const, | ||||
| 				limit: 6, | ||||
| 				params: computed(() => ({ | ||||
| 					userId: this.post.user.id | ||||
| 				})), | ||||
| 			}, | ||||
| 			post: null, | ||||
| 			error: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		postId: 'fetch' | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.post = null; | ||||
| 			os.api('gallery/posts/show', { | ||||
| 				postId: this.postId | ||||
| 			}).then(post => { | ||||
| 				this.post = post; | ||||
| 			}).catch(err => { | ||||
| 				this.error = err; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		share() { | ||||
| 			navigator.share({ | ||||
| 				title: this.post.title, | ||||
| 				text: this.post.description, | ||||
| 				url: `${url}/gallery/${this.post.id}` | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		shareWithNote() { | ||||
| 			os.post({ | ||||
| 				initialText: `${this.post.title} ${url}/gallery/${this.post.id}` | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		like() { | ||||
| 			os.apiWithDialog('gallery/posts/like', { | ||||
| 				postId: this.postId, | ||||
| 			}).then(() => { | ||||
| 				this.post.isLiked = true; | ||||
| 				this.post.likedCount++; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async unlike() { | ||||
| 			const confirm = await os.confirm({ | ||||
| 				type: 'warning', | ||||
| 				text: this.$ts.unlikeConfirm, | ||||
| 			}); | ||||
| 			if (confirm.canceled) return; | ||||
| 			os.apiWithDialog('gallery/posts/unlike', { | ||||
| 				postId: this.postId, | ||||
| 			}).then(() => { | ||||
| 				this.post.isLiked = false; | ||||
| 				this.post.likedCount--; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		edit() { | ||||
| 			this.$router.push(`/gallery/${this.post.id}/edit`); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| 	actions: [{ | ||||
| 		icon: 'fas fa-pencil-alt', | ||||
| 		text: i18n.ts.edit, | ||||
| 		handler: edit, | ||||
| 	}], | ||||
| } : null)); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="600" :margin-min="16" :margin-max="32"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="600" :margin-min="16" :margin-max="32"> | ||||
| 	<div v-if="instance" class="_formRoot"> | ||||
| 		<div class="fnfelxur"> | ||||
| 			<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/> | ||||
|  | @ -102,7 +103,7 @@ | |||
| 			<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink> | ||||
| 		</FormSection> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -120,8 +121,8 @@ import FormSwitch from '@/components/form/switch.vue'; | |||
| import * as os from '@/os'; | ||||
| import number from '@/filters/number'; | ||||
| import bytes from '@/filters/bytes'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { iAmModerator } from '@/account'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	host: string; | ||||
|  | @ -146,7 +147,7 @@ async function fetch() { | |||
| async function toggleBlock(ev) { | ||||
| 	if (meta == null) return; | ||||
| 	await os.api('admin/update-meta', { | ||||
| 		blockedHosts: isBlocked ? meta.blockedHosts.concat([instance.host]) : meta.blockedHosts.filter(x => x !== instance.host) | ||||
| 		blockedHosts: isBlocked ? meta.blockedHosts.concat([instance.host]) : meta.blockedHosts.filter(x => x !== instance.host), | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | @ -168,19 +169,21 @@ function refreshMetadata() { | |||
| 
 | ||||
| fetch(); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: props.host, | ||||
| 		icon: 'fas fa-info-circle', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			text: `https://${props.host}`, | ||||
| 			icon: 'fas fa-external-link-alt', | ||||
| 			handler: () => { | ||||
| 				window.open(`https://${props.host}`, '_blank'); | ||||
| 			} | ||||
| 		}], | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: props.host, | ||||
| 	icon: 'fas fa-info-circle', | ||||
| 	bg: 'var(--bg)', | ||||
| 	actions: [{ | ||||
| 		text: `https://${props.host}`, | ||||
| 		icon: 'fas fa-external-link-alt', | ||||
| 		handler: () => { | ||||
| 			window.open(`https://${props.host}`, '_blank'); | ||||
| 		}, | ||||
| 	}], | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,24 +1,27 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="800"> | ||||
| 	<XNotes :pagination="pagination"/> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import XNotes from '@/components/notes.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const pagination = { | ||||
| 	endpoint: 'notes/mentions' as const, | ||||
| 	limit: 10, | ||||
| }; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.mentions, | ||||
| 		icon: 'fas fa-at', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.mentions, | ||||
| 	icon: 'fas fa-at', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,27 +1,30 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="800"> | ||||
| 	<XNotes :pagination="pagination"/> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import XNotes from '@/components/notes.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const pagination = { | ||||
| 	endpoint: 'notes/mentions' as const, | ||||
| 	limit: 10, | ||||
| 	params: { | ||||
| 		visibility: 'specified' | ||||
| 		visibility: 'specified', | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.directNotes, | ||||
| 		icon: 'fas fa-envelope', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.directNotes, | ||||
| 	icon: 'fas fa-envelope', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,165 +1,165 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800"> | ||||
| 	<div v-size="{ max: [400] }" class="yweeujhr"> | ||||
| 		<MkButton primary class="start" @click="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="800"> | ||||
| 		<div v-size="{ max: [400] }" class="yweeujhr"> | ||||
| 			<MkButton primary class="start" @click="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton> | ||||
| 
 | ||||
| 		<div v-if="messages.length > 0" class="history"> | ||||
| 			<MkA v-for="(message, i) in messages" | ||||
| 				:key="message.id" | ||||
| 				v-anim="i" | ||||
| 				class="message _block" | ||||
| 				:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }" | ||||
| 				:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" | ||||
| 				:data-index="i" | ||||
| 			> | ||||
| 				<div> | ||||
| 					<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/> | ||||
| 					<header v-if="message.groupId"> | ||||
| 						<span class="name">{{ message.group.name }}</span> | ||||
| 						<MkTime :time="message.createdAt" class="time"/> | ||||
| 					</header> | ||||
| 					<header v-else> | ||||
| 						<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span> | ||||
| 						<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span> | ||||
| 						<MkTime :time="message.createdAt" class="time"/> | ||||
| 					</header> | ||||
| 					<div class="body"> | ||||
| 						<p class="text"><span v-if="isMe(message)" class="me">{{ $ts.you }}:</span>{{ message.text }}</p> | ||||
| 			<div v-if="messages.length > 0" class="history"> | ||||
| 				<MkA | ||||
| 					v-for="(message, i) in messages" | ||||
| 					:key="message.id" | ||||
| 					v-anim="i" | ||||
| 					class="message _block" | ||||
| 					:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }" | ||||
| 					:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`" | ||||
| 					:data-index="i" | ||||
| 				> | ||||
| 					<div> | ||||
| 						<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/> | ||||
| 						<header v-if="message.groupId"> | ||||
| 							<span class="name">{{ message.group.name }}</span> | ||||
| 							<MkTime :time="message.createdAt" class="time"/> | ||||
| 						</header> | ||||
| 						<header v-else> | ||||
| 							<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span> | ||||
| 							<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span> | ||||
| 							<MkTime :time="message.createdAt" class="time"/> | ||||
| 						</header> | ||||
| 						<div class="body"> | ||||
| 							<p class="text"><span v-if="isMe(message)" class="me">{{ $ts.you }}:</span>{{ message.text }}</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</MkA> | ||||
| 				</MkA> | ||||
| 			</div> | ||||
| 			<div v-if="!fetching && messages.length == 0" class="_fullinfo"> | ||||
| 				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 				<div>{{ $ts.noHistory }}</div> | ||||
| 			</div> | ||||
| 			<MkLoading v-if="fetching"/> | ||||
| 		</div> | ||||
| 		<div v-if="!fetching && messages.length == 0" class="_fullinfo"> | ||||
| 			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 			<div>{{ $ts.noHistory }}</div> | ||||
| 		</div> | ||||
| 		<MkLoading v-if="fetching"/> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { defineAsyncComponent, defineComponent, inject, markRaw, onMounted, onUnmounted } from 'vue'; | ||||
| import * as Acct from 'misskey-js/built/acct'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import { acct } from '@/filters/user'; | ||||
| import * as os from '@/os'; | ||||
| import { stream } from '@/stream'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { $i } from '@/account'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton | ||||
| 	}, | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.messaging, | ||||
| 				icon: 'fas fa-comments', | ||||
| 				bg: 'var(--bg)', | ||||
| 			}, | ||||
| 			fetching: true, | ||||
| 			moreFetching: false, | ||||
| 			messages: [], | ||||
| 			connection: null, | ||||
| 		}; | ||||
| 	}, | ||||
| let fetching = $ref(true); | ||||
| let moreFetching = $ref(false); | ||||
| let messages = $ref([]); | ||||
| let connection = $ref(null); | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.connection = markRaw(stream.useChannel('messagingIndex')); | ||||
| const getAcct = Acct.toString; | ||||
| 
 | ||||
| 		this.connection.on('message', this.onMessage); | ||||
| 		this.connection.on('read', this.onRead); | ||||
| function isMe(message) { | ||||
| 	return message.userId === $i.id; | ||||
| } | ||||
| 
 | ||||
| 		os.api('messaging/history', { group: false }).then(userMessages => { | ||||
| 			os.api('messaging/history', { group: true }).then(groupMessages => { | ||||
| 				const messages = userMessages.concat(groupMessages); | ||||
| 				messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); | ||||
| 				this.messages = messages; | ||||
| 				this.fetching = false; | ||||
| 			}); | ||||
| 		}); | ||||
| 	}, | ||||
| function onMessage(message) { | ||||
| 	if (message.recipientId) { | ||||
| 		messages = messages.filter(m => !( | ||||
| 			(m.recipientId === message.recipientId && m.userId === message.userId) || | ||||
| 			(m.recipientId === message.userId && m.userId === message.recipientId))); | ||||
| 
 | ||||
| 	beforeUnmount() { | ||||
| 		this.connection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		getAcct: Acct.toString, | ||||
| 
 | ||||
| 		isMe(message) { | ||||
| 			return message.userId === this.$i.id; | ||||
| 		}, | ||||
| 
 | ||||
| 		onMessage(message) { | ||||
| 			if (message.recipientId) { | ||||
| 				this.messages = this.messages.filter(m => !( | ||||
| 					(m.recipientId === message.recipientId && m.userId === message.userId) || | ||||
| 					(m.recipientId === message.userId && m.userId === message.recipientId))); | ||||
| 
 | ||||
| 				this.messages.unshift(message); | ||||
| 			} else if (message.groupId) { | ||||
| 				this.messages = this.messages.filter(m => m.groupId !== message.groupId); | ||||
| 				this.messages.unshift(message); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		onRead(ids) { | ||||
| 			for (const id of ids) { | ||||
| 				const found = this.messages.find(m => m.id === id); | ||||
| 				if (found) { | ||||
| 					if (found.recipientId) { | ||||
| 						found.isRead = true; | ||||
| 					} else if (found.groupId) { | ||||
| 						found.reads.push(this.$i.id); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		start(ev) { | ||||
| 			os.popupMenu([{ | ||||
| 				text: this.$ts.messagingWithUser, | ||||
| 				icon: 'fas fa-user', | ||||
| 				action: () => { this.startUser(); } | ||||
| 			}, { | ||||
| 				text: this.$ts.messagingWithGroup, | ||||
| 				icon: 'fas fa-users', | ||||
| 				action: () => { this.startGroup(); } | ||||
| 			}], ev.currentTarget ?? ev.target); | ||||
| 		}, | ||||
| 
 | ||||
| 		async startUser() { | ||||
| 			os.selectUser().then(user => { | ||||
| 				this.$router.push(`/my/messaging/${Acct.toString(user)}`); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async startGroup() { | ||||
| 			const groups1 = await os.api('users/groups/owned'); | ||||
| 			const groups2 = await os.api('users/groups/joined'); | ||||
| 			if (groups1.length === 0 && groups2.length === 0) { | ||||
| 				os.alert({ | ||||
| 					type: 'warning', | ||||
| 					title: this.$ts.youHaveNoGroups, | ||||
| 					text: this.$ts.joinOrCreateGroup, | ||||
| 				}); | ||||
| 				return; | ||||
| 			} | ||||
| 			const { canceled, result: group } = await os.select({ | ||||
| 				title: this.$ts.group, | ||||
| 				items: groups1.concat(groups2).map(group => ({ | ||||
| 					value: group, text: group.name | ||||
| 				})) | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 			this.$router.push(`/my/messaging/group/${group.id}`); | ||||
| 		}, | ||||
| 
 | ||||
| 		acct | ||||
| 		messages.unshift(message); | ||||
| 	} else if (message.groupId) { | ||||
| 		messages = messages.filter(m => m.groupId !== message.groupId); | ||||
| 		messages.unshift(message); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| function onRead(ids) { | ||||
| 	for (const id of ids) { | ||||
| 		const found = messages.find(m => m.id === id); | ||||
| 		if (found) { | ||||
| 			if (found.recipientId) { | ||||
| 				found.isRead = true; | ||||
| 			} else if (found.groupId) { | ||||
| 				found.reads.push($i.id); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| function start(ev) { | ||||
| 	os.popupMenu([{ | ||||
| 		text: i18n.ts.messagingWithUser, | ||||
| 		icon: 'fas fa-user', | ||||
| 		action: () => { startUser(); }, | ||||
| 	}, { | ||||
| 		text: i18n.ts.messagingWithGroup, | ||||
| 		icon: 'fas fa-users', | ||||
| 		action: () => { startGroup(); }, | ||||
| 	}], ev.currentTarget ?? ev.target); | ||||
| } | ||||
| 
 | ||||
| async function startUser() { | ||||
| 	os.selectUser().then(user => { | ||||
| 		router.push(`/my/messaging/${Acct.toString(user)}`); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function startGroup() { | ||||
| 	const groups1 = await os.api('users/groups/owned'); | ||||
| 	const groups2 = await os.api('users/groups/joined'); | ||||
| 	if (groups1.length === 0 && groups2.length === 0) { | ||||
| 		os.alert({ | ||||
| 			type: 'warning', | ||||
| 			title: i18n.ts.youHaveNoGroups, | ||||
| 			text: i18n.ts.joinOrCreateGroup, | ||||
| 		}); | ||||
| 		return; | ||||
| 	} | ||||
| 	const { canceled, result: group } = await os.select({ | ||||
| 		title: i18n.ts.group, | ||||
| 		items: groups1.concat(groups2).map(group => ({ | ||||
| 			value: group, text: group.name, | ||||
| 		})), | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 	router.push(`/my/messaging/group/${group.id}`); | ||||
| } | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	connection = markRaw(stream.useChannel('messagingIndex')); | ||||
| 
 | ||||
| 	connection.on('message', onMessage); | ||||
| 	connection.on('read', onRead); | ||||
| 
 | ||||
| 	os.api('messaging/history', { group: false }).then(userMessages => { | ||||
| 		os.api('messaging/history', { group: true }).then(groupMessages => { | ||||
| 			const _messages = userMessages.concat(groupMessages); | ||||
| 			_messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); | ||||
| 			messages = _messages; | ||||
| 			fetching = false; | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| onUnmounted(() => { | ||||
| 	if (connection) connection.dispose(); | ||||
| }); | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.messaging, | ||||
| 	icon: 'fas fa-comments', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -61,10 +61,10 @@ import { isBottomVisible, onScrollBottom, scrollToBottom } from '@/scripts/scrol | |||
| import * as os from '@/os'; | ||||
| import { stream } from '@/stream'; | ||||
| import * as sound from '@/scripts/sound'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { $i } from '@/account'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	userAcct?: string; | ||||
|  | @ -280,15 +280,13 @@ onBeforeUnmount(() => { | |||
| 	if (scrollRemove) scrollRemove(); | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => !fetching ? user ? { | ||||
| 		userName: user, | ||||
| 		avatar: user, | ||||
| 	} : { | ||||
| 		title: group?.name, | ||||
| 		icon: 'fas fa-users', | ||||
| 	} : null), | ||||
| }); | ||||
| definePageMetadata(computed(() => !fetching ? user ? { | ||||
| 	userName: user, | ||||
| 	avatar: user, | ||||
| } : { | ||||
| 	title: group?.name, | ||||
| 	icon: 'fas fa-users', | ||||
| } : null)); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,127 +1,129 @@ | |||
| <template> | ||||
| <div class="mwysmxbg"> | ||||
| 	<div class="_isolated">{{ $ts._mfm.intro }}</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.mention }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.mentionDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_mention"/> | ||||
| 				<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader/></template> | ||||
| 	<div class="mwysmxbg"> | ||||
| 		<div class="_isolated">{{ $ts._mfm.intro }}</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.mention }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.mentionDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_mention"/> | ||||
| 					<MkTextarea v-model="preview_mention"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.hashtag }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.hashtagDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_hashtag"/> | ||||
| 				<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.hashtag }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.hashtagDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_hashtag"/> | ||||
| 					<MkTextarea v-model="preview_hashtag"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.url }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.urlDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_url"/> | ||||
| 				<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.url }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.urlDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_url"/> | ||||
| 					<MkTextarea v-model="preview_url"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.link }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.linkDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_link"/> | ||||
| 				<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.link }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.linkDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_link"/> | ||||
| 					<MkTextarea v-model="preview_link"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.emoji }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.emojiDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_emoji"/> | ||||
| 				<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.emoji }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.emojiDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_emoji"/> | ||||
| 					<MkTextarea v-model="preview_emoji"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.bold }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.boldDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_bold"/> | ||||
| 				<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.bold }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.boldDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_bold"/> | ||||
| 					<MkTextarea v-model="preview_bold"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.small }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.smallDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_small"/> | ||||
| 				<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.small }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.smallDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_small"/> | ||||
| 					<MkTextarea v-model="preview_small"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.quote }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.quoteDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_quote"/> | ||||
| 				<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.quote }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.quoteDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_quote"/> | ||||
| 					<MkTextarea v-model="preview_quote"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.center }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.centerDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_center"/> | ||||
| 				<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.center }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.centerDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_center"/> | ||||
| 					<MkTextarea v-model="preview_center"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.inlineCode }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.inlineCodeDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_inlineCode"/> | ||||
| 				<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.inlineCode }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.inlineCodeDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_inlineCode"/> | ||||
| 					<MkTextarea v-model="preview_inlineCode"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.blockCode }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.blockCodeDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_blockCode"/> | ||||
| 				<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.blockCode }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.blockCodeDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_blockCode"/> | ||||
| 					<MkTextarea v-model="preview_blockCode"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.inlineMath }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.inlineMathDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_inlineMath"/> | ||||
| 				<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.inlineMath }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.inlineMathDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_inlineMath"/> | ||||
| 					<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<!-- deprecated | ||||
| 		<!-- deprecated | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.search }}</div> | ||||
| 		<div class="content"> | ||||
|  | @ -133,216 +135,210 @@ | |||
| 		</div> | ||||
| 	</div> | ||||
| 	--> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.flip }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.flipDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_flip"/> | ||||
| 				<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.flip }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.flipDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_flip"/> | ||||
| 					<MkTextarea v-model="preview_flip"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.font }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.fontDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_font"/> | ||||
| 					<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.x2 }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.x2Description }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_x2"/> | ||||
| 					<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.x3 }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.x3Description }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_x3"/> | ||||
| 					<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.x4 }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.x4Description }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_x4"/> | ||||
| 					<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.blur }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.blurDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_blur"/> | ||||
| 					<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.jelly }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.jellyDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_jelly"/> | ||||
| 					<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.tada }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.tadaDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_tada"/> | ||||
| 					<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.jump }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.jumpDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_jump"/> | ||||
| 					<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.bounce }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.bounceDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_bounce"/> | ||||
| 					<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.spin }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.spinDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_spin"/> | ||||
| 					<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.shake }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.shakeDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_shake"/> | ||||
| 					<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.twitch }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.twitchDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_twitch"/> | ||||
| 					<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.rainbow }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.rainbowDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_rainbow"/> | ||||
| 					<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.sparkle }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.sparkleDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_sparkle"/> | ||||
| 					<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="section _block"> | ||||
| 			<div class="title">{{ $ts._mfm.rotate }}</div> | ||||
| 			<div class="content"> | ||||
| 				<p>{{ $ts._mfm.rotateDescription }}</p> | ||||
| 				<div class="preview"> | ||||
| 					<Mfm :text="preview_rotate"/> | ||||
| 					<MkTextarea v-model="preview_rotate"><span>MFM</span></MkTextarea> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.font }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.fontDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_font"/> | ||||
| 				<MkTextarea v-model="preview_font"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.x2 }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.x2Description }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_x2"/> | ||||
| 				<MkTextarea v-model="preview_x2"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.x3 }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.x3Description }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_x3"/> | ||||
| 				<MkTextarea v-model="preview_x3"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.x4 }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.x4Description }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_x4"/> | ||||
| 				<MkTextarea v-model="preview_x4"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.blur }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.blurDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_blur"/> | ||||
| 				<MkTextarea v-model="preview_blur"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.jelly }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.jellyDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_jelly"/> | ||||
| 				<MkTextarea v-model="preview_jelly"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.tada }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.tadaDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_tada"/> | ||||
| 				<MkTextarea v-model="preview_tada"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.jump }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.jumpDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_jump"/> | ||||
| 				<MkTextarea v-model="preview_jump"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.bounce }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.bounceDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_bounce"/> | ||||
| 				<MkTextarea v-model="preview_bounce"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.spin }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.spinDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_spin"/> | ||||
| 				<MkTextarea v-model="preview_spin"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.shake }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.shakeDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_shake"/> | ||||
| 				<MkTextarea v-model="preview_shake"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.twitch }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.twitchDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_twitch"/> | ||||
| 				<MkTextarea v-model="preview_twitch"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.rainbow }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.rainbowDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_rainbow"/> | ||||
| 				<MkTextarea v-model="preview_rainbow"><template #label>MFM</template></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.sparkle }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.sparkleDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_sparkle"/> | ||||
| 				<MkTextarea v-model="preview_sparkle"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.rotate }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.rotateDescription }}</p> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_rotate"/> | ||||
| 				<MkTextarea v-model="preview_rotate"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| <script lang="ts" setup> | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkTextarea from '@/components/form/textarea.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { instance } from '@/instance'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkTextarea | ||||
| 	}, | ||||
| const preview_mention = '@example'; | ||||
| const preview_hashtag = '#test'; | ||||
| const preview_url = 'https://example.com'; | ||||
| const preview_link = `[${i18n.ts._mfm.dummy}](https://example.com)`; | ||||
| const preview_emoji = instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:'; | ||||
| const preview_bold = `**${i18n.ts._mfm.dummy}**`; | ||||
| const preview_small = `<small>${i18n.ts._mfm.dummy}</small>`; | ||||
| const preview_center = `<center>${i18n.ts._mfm.dummy}</center>`; | ||||
| const preview_inlineCode = '`<: "Hello, world!"`'; | ||||
| const preview_blockCode = '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```'; | ||||
| const preview_inlineMath = '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)'; | ||||
| const preview_quote = `> ${i18n.ts._mfm.dummy}`; | ||||
| const preview_search = `${i18n.ts._mfm.dummy} 検索`; | ||||
| const preview_jelly = '$[jelly 🍮] $[jelly.speed=5s 🍮]'; | ||||
| const preview_tada = '$[tada 🍮] $[tada.speed=5s 🍮]'; | ||||
| const preview_jump = '$[jump 🍮] $[jump.speed=5s 🍮]'; | ||||
| const preview_bounce = '$[bounce 🍮] $[bounce.speed=5s 🍮]'; | ||||
| const preview_shake = '$[shake 🍮] $[shake.speed=5s 🍮]'; | ||||
| const preview_twitch = '$[twitch 🍮] $[twitch.speed=5s 🍮]'; | ||||
| const preview_spin = '$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]'; | ||||
| const preview_flip = `$[flip ${i18n.ts._mfm.dummy}]\n$[flip.v ${i18n.ts._mfm.dummy}]\n$[flip.h,v ${i18n.ts._mfm.dummy}]`; | ||||
| const preview_font = `$[font.serif ${i18n.ts._mfm.dummy}]\n$[font.monospace ${i18n.ts._mfm.dummy}]\n$[font.cursive ${i18n.ts._mfm.dummy}]\n$[font.fantasy ${i18n.ts._mfm.dummy}]`; | ||||
| const preview_x2 = '$[x2 🍮]'; | ||||
| const preview_x3 = '$[x3 🍮]'; | ||||
| const preview_x4 = '$[x4 🍮]'; | ||||
| const preview_blur = `$[blur ${i18n.ts._mfm.dummy}]`; | ||||
| const preview_rainbow = '$[rainbow 🍮] $[rainbow.speed=5s 🍮]'; | ||||
| const preview_sparkle = '$[sparkle 🍮]'; | ||||
| const preview_rotate = '$[rotate 🍮]'; | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts._mfm.cheatSheet, | ||||
| 				icon: 'fas fa-question-circle', | ||||
| 			}, | ||||
| 			preview_mention: '@example', | ||||
| 			preview_hashtag: '#test', | ||||
| 			preview_url: `https://example.com`, | ||||
| 			preview_link: `[${this.$ts._mfm.dummy}](https://example.com)`, | ||||
| 			preview_emoji: this.$instance.emojis.length ? `:${this.$instance.emojis[0].name}:` : `:emojiname:`, | ||||
| 			preview_bold: `**${this.$ts._mfm.dummy}**`, | ||||
| 			preview_small: `<small>${this.$ts._mfm.dummy}</small>`, | ||||
| 			preview_center: `<center>${this.$ts._mfm.dummy}</center>`, | ||||
| 			preview_inlineCode: '`<: "Hello, world!"`', | ||||
| 			preview_blockCode: '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```', | ||||
| 			preview_inlineMath: '\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)', | ||||
| 			preview_quote: `> ${this.$ts._mfm.dummy}`, | ||||
| 			preview_search: `${this.$ts._mfm.dummy} 検索`, | ||||
| 			preview_jelly: `$[jelly 🍮] $[jelly.speed=5s 🍮]`, | ||||
| 			preview_tada: `$[tada 🍮] $[tada.speed=5s 🍮]`, | ||||
| 			preview_jump: `$[jump 🍮] $[jump.speed=5s 🍮]`, | ||||
| 			preview_bounce: `$[bounce 🍮] $[bounce.speed=5s 🍮]`, | ||||
| 			preview_shake: `$[shake 🍮] $[shake.speed=5s 🍮]`, | ||||
| 			preview_twitch: `$[twitch 🍮] $[twitch.speed=5s 🍮]`, | ||||
| 			preview_spin: `$[spin 🍮] $[spin.left 🍮] $[spin.alternate 🍮]\n$[spin.x 🍮] $[spin.x,left 🍮] $[spin.x,alternate 🍮]\n$[spin.y 🍮] $[spin.y,left 🍮] $[spin.y,alternate 🍮]\n\n$[spin.speed=5s 🍮]`, | ||||
| 			preview_flip: `$[flip ${this.$ts._mfm.dummy}]\n$[flip.v ${this.$ts._mfm.dummy}]\n$[flip.h,v ${this.$ts._mfm.dummy}]`, | ||||
| 			preview_font: `$[font.serif ${this.$ts._mfm.dummy}]\n$[font.monospace ${this.$ts._mfm.dummy}]\n$[font.cursive ${this.$ts._mfm.dummy}]\n$[font.fantasy ${this.$ts._mfm.dummy}]`, | ||||
| 			preview_x2: `$[x2 🍮]`, | ||||
| 			preview_x3: `$[x3 🍮]`, | ||||
| 			preview_x4: `$[x4 🍮]`, | ||||
| 			preview_blur: `$[blur ${this.$ts._mfm.dummy}]`, | ||||
| 			preview_rainbow: `$[rainbow 🍮] $[rainbow.speed=5s 🍮]`, | ||||
| 			preview_sparkle: `$[sparkle 🍮]`, | ||||
| 			preview_rotate: `$[rotate 🍮]`, | ||||
| 		}; | ||||
| 	}, | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts._mfm.cheatSheet, | ||||
| 	icon: 'fas fa-question-circle', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -49,28 +49,12 @@ export default defineComponent({ | |||
| 		MkSignin, | ||||
| 		MkButton, | ||||
| 	}, | ||||
| 	props: ['session', 'callback', 'name', 'icon', 'permission'], | ||||
| 	data() { | ||||
| 		return { | ||||
| 			state: null | ||||
| 			state: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		session(): string { | ||||
| 			return this.$route.params.session; | ||||
| 		}, | ||||
| 		callback(): string { | ||||
| 			return this.$route.query.callback; | ||||
| 		}, | ||||
| 		name(): string { | ||||
| 			return this.$route.query.name; | ||||
| 		}, | ||||
| 		icon(): string { | ||||
| 			return this.$route.query.icon; | ||||
| 		}, | ||||
| 		permission(): string[] { | ||||
| 			return this.$route.query.permission ? this.$route.query.permission.split(',') : []; | ||||
| 		}, | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		async accept() { | ||||
| 			this.state = 'waiting'; | ||||
|  | @ -84,7 +68,7 @@ export default defineComponent({ | |||
| 			this.state = 'accepted'; | ||||
| 			if (this.callback) { | ||||
| 				location.href = appendQuery(this.callback, query({ | ||||
| 					session: this.session | ||||
| 					session: this.session, | ||||
| 				})); | ||||
| 			} | ||||
| 		}, | ||||
|  | @ -93,8 +77,8 @@ export default defineComponent({ | |||
| 		}, | ||||
| 		onLogin(res) { | ||||
| 			login(res.i); | ||||
| 		} | ||||
| 	} | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,11 +5,13 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import { inject } from 'vue'; | ||||
| import XAntenna from './editor.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { router } from '@/router'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { useRouter } from '@/router'; | ||||
| 
 | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| let draft = $ref({ | ||||
| 	name: '', | ||||
|  | @ -22,19 +24,21 @@ let draft = $ref({ | |||
| 	withReplies: false, | ||||
| 	caseSensitive: false, | ||||
| 	withFile: false, | ||||
| 	notify: false | ||||
| 	notify: false, | ||||
| }); | ||||
| 
 | ||||
| function onAntennaCreated() { | ||||
| 	router.push('/my/antennas'); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.manageAntennas, | ||||
| 		icon: 'fas fa-satellite', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.manageAntennas, | ||||
| 	icon: 'fas fa-satellite', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,14 +5,14 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { watch } from 'vue'; | ||||
| import { inject, watch } from 'vue'; | ||||
| import XAntenna from './editor.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import * as os from '@/os'; | ||||
| import { MisskeyNavigator } from '@/scripts/navigate'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const nav = new MisskeyNavigator(); | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| let antenna: any = $ref(null); | ||||
| 
 | ||||
|  | @ -21,18 +21,20 @@ const props = defineProps<{ | |||
| }>(); | ||||
| 
 | ||||
| function onAntennaUpdated() { | ||||
| 	nav.push('/my/antennas'); | ||||
| 	router.push('/my/antennas'); | ||||
| } | ||||
| 
 | ||||
| os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => { | ||||
| 	antenna = antennaResponse; | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.manageAntennas, | ||||
| 		icon: 'fas fa-satellite', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.manageAntennas, | ||||
| 	icon: 'fas fa-satellite', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="700"> | ||||
| 	<div class="ieepwinx"> | ||||
| 		<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton> | ||||
| 
 | ||||
|  | @ -11,27 +12,29 @@ | |||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const pagination = { | ||||
| 	endpoint: 'antennas/list' as const, | ||||
| 	limit: 10, | ||||
| }; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.manageAntennas, | ||||
| 		icon: 'fas fa-satellite', | ||||
| 		bg: 'var(--bg)' | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.manageAntennas, | ||||
| 	icon: 'fas fa-satellite', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="700"> | ||||
| 	<div class="qtcaoidl"> | ||||
| 		<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> | ||||
| 
 | ||||
|  | @ -10,7 +11,7 @@ | |||
| 			</MkA> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -18,8 +19,8 @@ import { } from 'vue'; | |||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const pagination = { | ||||
| 	endpoint: 'clips/list' as const, | ||||
|  | @ -61,15 +62,17 @@ function onClipDeleted() { | |||
| 	pagingComponent.reload(); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.clip, | ||||
| 		icon: 'fas fa-paperclip', | ||||
| 		bg: 'var(--bg)', | ||||
| 		action: { | ||||
| 			icon: 'fas fa-plus', | ||||
| 			handler: create | ||||
| 		}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.clip, | ||||
| 	icon: 'fas fa-paperclip', | ||||
| 	bg: 'var(--bg)', | ||||
| 	action: { | ||||
| 		icon: 'fas fa-plus', | ||||
| 		handler: create, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,178 +0,0 @@ | |||
| <template> | ||||
| <div class="mk-group-page"> | ||||
| 	<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> | ||||
| 		<div v-if="group" class="_section"> | ||||
| 			<div class="_content" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> | ||||
| 				<MkButton inline @click="invite()">{{ $ts.invite }}</MkButton> | ||||
| 				<MkButton inline @click="renameGroup()">{{ $ts.rename }}</MkButton> | ||||
| 				<MkButton inline @click="transfer()">{{ $ts.transfer }}</MkButton> | ||||
| 				<MkButton inline @click="deleteGroup()">{{ $ts.delete }}</MkButton> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</transition> | ||||
| 
 | ||||
| 	<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> | ||||
| 		<div v-if="group" class="_section members _gap"> | ||||
| 			<div class="_title">{{ $ts.members }}</div> | ||||
| 			<div class="_content"> | ||||
| 				<div class="users"> | ||||
| 					<div v-for="user in users" :key="user.id" class="user _panel"> | ||||
| 						<MkAvatar :user="user" class="avatar" :show-indicator="true"/> | ||||
| 						<div class="body"> | ||||
| 							<MkUserName :user="user" class="name"/> | ||||
| 							<MkAcct :user="user" class="acct"/> | ||||
| 						</div> | ||||
| 						<div class="action"> | ||||
| 							<button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</transition> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		groupId: { | ||||
| 			type: String, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.group ? { | ||||
| 				title: this.group.name, | ||||
| 				icon: 'fas fa-users', | ||||
| 			} : null), | ||||
| 			group: null, | ||||
| 			users: [], | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		groupId: 'fetch', | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			os.api('users/groups/show', { | ||||
| 				groupId: this.groupId | ||||
| 			}).then(group => { | ||||
| 				this.group = group; | ||||
| 				os.api('users/show', { | ||||
| 					userIds: this.group.userIds | ||||
| 				}).then(users => { | ||||
| 					this.users = users; | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		invite() { | ||||
| 			os.selectUser().then(user => { | ||||
| 				os.apiWithDialog('users/groups/invite', { | ||||
| 					groupId: this.group.id, | ||||
| 					userId: user.id | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		removeUser(user) { | ||||
| 			os.api('users/groups/pull', { | ||||
| 				groupId: this.group.id, | ||||
| 				userId: user.id | ||||
| 			}).then(() => { | ||||
| 				this.users = this.users.filter(x => x.id !== user.id); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async renameGroup() { | ||||
| 			const { canceled, result: name } = await os.inputText({ | ||||
| 				title: this.$ts.groupName, | ||||
| 				default: this.group.name | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 
 | ||||
| 			await os.api('users/groups/update', { | ||||
| 				groupId: this.group.id, | ||||
| 				name: name | ||||
| 			}); | ||||
| 
 | ||||
| 			this.group.name = name; | ||||
| 		}, | ||||
| 
 | ||||
| 		transfer() { | ||||
| 			os.selectUser().then(user => { | ||||
| 				os.apiWithDialog('users/groups/transfer', { | ||||
| 					groupId: this.group.id, | ||||
| 					userId: user.id | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async deleteGroup() { | ||||
| 			const { canceled } = await os.confirm({ | ||||
| 				type: 'warning', | ||||
| 				text: this.$t('removeAreYouSure', { x: this.group.name }), | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 
 | ||||
| 			await os.apiWithDialog('users/groups/delete', { | ||||
| 				groupId: this.group.id | ||||
| 			}); | ||||
| 			this.$router.push('/my/groups'); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .mk-group-page { | ||||
| 	> .members { | ||||
| 		> ._content { | ||||
| 			> .users { | ||||
| 				> .user { | ||||
| 					display: flex; | ||||
| 					align-items: center; | ||||
| 					padding: 16px; | ||||
| 
 | ||||
| 					> .avatar { | ||||
| 						width: 50px; | ||||
| 						height: 50px; | ||||
| 					} | ||||
| 
 | ||||
| 					> .body { | ||||
| 						flex: 1; | ||||
| 						padding: 8px; | ||||
| 
 | ||||
| 						> .name { | ||||
| 							display: block; | ||||
| 							font-weight: bold; | ||||
| 						} | ||||
| 
 | ||||
| 						> .acct { | ||||
| 							opacity: 0.5; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -1,147 +0,0 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| 	<div v-if="tab === 'owned'" class="_content"> | ||||
| 		<MkButton primary style="margin: 0 auto var(--margin) auto;" @click="create"><i class="fas fa-plus"></i> {{ $ts.createGroup }}</MkButton> | ||||
| 
 | ||||
| 		<MkPagination v-slot="{items}" ref="owned" :pagination="ownedPagination"> | ||||
| 			<div v-for="group in items" :key="group.id" class="_card"> | ||||
| 				<div class="_title"><MkA :to="`/my/groups/${ group.id }`" class="_link">{{ group.name }}</MkA></div> | ||||
| 				<div class="_content"><MkAvatars :user-ids="group.userIds"/></div> | ||||
| 			</div> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div v-else-if="tab === 'joined'" class="_content"> | ||||
| 		<MkPagination v-slot="{items}" ref="joined" :pagination="joinedPagination"> | ||||
| 			<div v-for="group in items" :key="group.id" class="_card"> | ||||
| 				<div class="_title">{{ group.name }}</div> | ||||
| 				<div class="_content"><MkAvatars :user-ids="group.userIds"/></div> | ||||
| 				<div class="_footer"> | ||||
| 					<MkButton danger @click="leave(group)">{{ $ts.leaveGroup }}</MkButton> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div v-else-if="tab === 'invites'" class="_content"> | ||||
| 		<MkPagination v-slot="{items}" ref="invitations" :pagination="invitationPagination"> | ||||
| 			<div v-for="invitation in items" :key="invitation.id" class="_card"> | ||||
| 				<div class="_title">{{ invitation.group.name }}</div> | ||||
| 				<div class="_content"><MkAvatars :user-ids="invitation.group.userIds"/></div> | ||||
| 				<div class="_footer"> | ||||
| 					<MkButton primary inline @click="acceptInvite(invitation)"><i class="fas fa-check"></i> {{ $ts.accept }}</MkButton> | ||||
| 					<MkButton primary inline @click="rejectInvite(invitation)"><i class="fas fa-ban"></i> {{ $ts.reject }}</MkButton> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, computed } from 'vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import MkAvatars from '@/components/avatars.vue'; | ||||
| import MkTab from '@/components/tab.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkPagination, | ||||
| 		MkButton, | ||||
| 		MkContainer, | ||||
| 		MkTab, | ||||
| 		MkAvatars, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 				title: this.$ts.groups, | ||||
| 				icon: 'fas fa-users', | ||||
| 				bg: 'var(--bg)', | ||||
| 				actions: [{ | ||||
| 					icon: 'fas fa-plus', | ||||
| 					text: this.$ts.createGroup, | ||||
| 					handler: this.create, | ||||
| 				}], | ||||
| 				tabs: [{ | ||||
| 					active: this.tab === 'owned', | ||||
| 					title: this.$ts.ownedGroups, | ||||
| 					icon: 'fas fa-user-tie', | ||||
| 					onClick: () => { this.tab = 'owned'; }, | ||||
| 				}, { | ||||
| 					active: this.tab === 'joined', | ||||
| 					title: this.$ts.joinedGroups, | ||||
| 					icon: 'fas fa-id-badge', | ||||
| 					onClick: () => { this.tab = 'joined'; }, | ||||
| 				}, { | ||||
| 					active: this.tab === 'invites', | ||||
| 					title: this.$ts.invites, | ||||
| 					icon: 'fas fa-envelope-open-text', | ||||
| 					onClick: () => { this.tab = 'invites'; }, | ||||
| 				},] | ||||
| 			})), | ||||
| 			tab: 'owned', | ||||
| 			ownedPagination: { | ||||
| 				endpoint: 'users/groups/owned' as const, | ||||
| 				limit: 10, | ||||
| 			}, | ||||
| 			joinedPagination: { | ||||
| 				endpoint: 'users/groups/joined' as const, | ||||
| 				limit: 10, | ||||
| 			}, | ||||
| 			invitationPagination: { | ||||
| 				endpoint: 'i/user-group-invites' as const, | ||||
| 				limit: 10, | ||||
| 			}, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async create() { | ||||
| 			const { canceled, result: name } = await os.inputText({ | ||||
| 				title: this.$ts.groupName, | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 			await os.api('users/groups/create', { name: name }); | ||||
| 			this.$refs.owned.reload(); | ||||
| 			os.success(); | ||||
| 		}, | ||||
| 		acceptInvite(invitation) { | ||||
| 			os.api('users/groups/invitations/accept', { | ||||
| 				invitationId: invitation.id | ||||
| 			}).then(() => { | ||||
| 				os.success(); | ||||
| 				this.$refs.invitations.reload(); | ||||
| 				this.$refs.joined.reload(); | ||||
| 			}); | ||||
| 		}, | ||||
| 		rejectInvite(invitation) { | ||||
| 			os.api('users/groups/invitations/reject', { | ||||
| 				invitationId: invitation.id | ||||
| 			}).then(() => { | ||||
| 				this.$refs.invitations.reload(); | ||||
| 			}); | ||||
| 		}, | ||||
| 		async leave(group) { | ||||
| 			const { canceled } = await os.confirm({ | ||||
| 				type: 'warning', | ||||
| 				text: this.$t('leaveGroupConfirm', { name: group.name }), | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 			os.apiWithDialog('users/groups/leave', { | ||||
| 				groupId: group.id, | ||||
| 			}).then(() => { | ||||
| 				this.$refs.joined.reload(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| </style> | ||||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="700"> | ||||
| 	<div class="qkcjvfiv"> | ||||
| 		<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton> | ||||
| 
 | ||||
|  | @ -10,7 +11,7 @@ | |||
| 			</MkA> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -19,8 +20,8 @@ import MkPagination from '@/components/ui/pagination.vue'; | |||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkAvatars from '@/components/avatars.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); | ||||
| 
 | ||||
|  | @ -38,15 +39,17 @@ async function create() { | |||
| 	pagingComponent.reload(); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.manageLists, | ||||
| 		icon: 'fas fa-list-ul', | ||||
| 		bg: 'var(--bg)', | ||||
| 		action: { | ||||
| 			icon: 'fas fa-plus', | ||||
| 			handler: create, | ||||
| 		}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.manageLists, | ||||
| 	icon: 'fas fa-list-ul', | ||||
| 	bg: 'var(--bg)', | ||||
| 	action: { | ||||
| 		icon: 'fas fa-plus', | ||||
| 		handler: create, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="700"> | ||||
| 	<div class="mk-list-page"> | ||||
| 		<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> | ||||
| 			<div v-if="list" class="_section"> | ||||
|  | @ -31,104 +32,96 @@ | |||
| 			</div> | ||||
| 		</transition> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineComponent, watch } from 'vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { mainRouter } from '@/router'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	listId: string; | ||||
| }>(); | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.list ? { | ||||
| 				title: this.list.name, | ||||
| 				icon: 'fas fa-list-ul', | ||||
| 				bg: 'var(--bg)', | ||||
| 			} : null), | ||||
| 			list: null, | ||||
| 			users: [], | ||||
| 		}; | ||||
| 	}, | ||||
| let list = $ref(null); | ||||
| let users = $ref([]); | ||||
| 
 | ||||
| 	watch: { | ||||
| 		$route: 'fetch' | ||||
| 	}, | ||||
| function fetchList() { | ||||
| 	os.api('users/lists/show', { | ||||
| 		listId: props.listId, | ||||
| 	}).then(_list => { | ||||
| 		list = _list; | ||||
| 		os.api('users/show', { | ||||
| 			userIds: list.userIds, | ||||
| 		}).then(_users => { | ||||
| 			users = _users; | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| function addUser() { | ||||
| 	os.selectUser().then(user => { | ||||
| 		os.apiWithDialog('users/lists/push', { | ||||
| 			listId: list.id, | ||||
| 			userId: user.id, | ||||
| 		}).then(() => { | ||||
| 			users.push(user); | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			os.api('users/lists/show', { | ||||
| 				listId: this.$route.params.list | ||||
| 			}).then(list => { | ||||
| 				this.list = list; | ||||
| 				os.api('users/show', { | ||||
| 					userIds: this.list.userIds | ||||
| 				}).then(users => { | ||||
| 					this.users = users; | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| function removeUser(user) { | ||||
| 	os.api('users/lists/pull', { | ||||
| 		listId: list.id, | ||||
| 		userId: user.id, | ||||
| 	}).then(() => { | ||||
| 		users = users.filter(x => x.id !== user.id); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| 		addUser() { | ||||
| 			os.selectUser().then(user => { | ||||
| 				os.apiWithDialog('users/lists/push', { | ||||
| 					listId: this.list.id, | ||||
| 					userId: user.id | ||||
| 				}).then(() => { | ||||
| 					this.users.push(user); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| async function renameList() { | ||||
| 	const { canceled, result: name } = await os.inputText({ | ||||
| 		title: i18n.ts.enterListName, | ||||
| 		default: list.name, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 
 | ||||
| 		removeUser(user) { | ||||
| 			os.api('users/lists/pull', { | ||||
| 				listId: this.list.id, | ||||
| 				userId: user.id | ||||
| 			}).then(() => { | ||||
| 				this.users = this.users.filter(x => x.id !== user.id); | ||||
| 			}); | ||||
| 		}, | ||||
| 	await os.api('users/lists/update', { | ||||
| 		listId: list.id, | ||||
| 		name: name, | ||||
| 	}); | ||||
| 
 | ||||
| 		async renameList() { | ||||
| 			const { canceled, result: name } = await os.inputText({ | ||||
| 				title: this.$ts.enterListName, | ||||
| 				default: this.list.name | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 	list.name = name; | ||||
| } | ||||
| 
 | ||||
| 			await os.api('users/lists/update', { | ||||
| 				listId: this.list.id, | ||||
| 				name: name | ||||
| 			}); | ||||
| async function deleteList() { | ||||
| 	const { canceled } = await os.confirm({ | ||||
| 		type: 'warning', | ||||
| 		text: i18n.t('removeAreYouSure', { x: list.name }), | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 
 | ||||
| 			this.list.name = name; | ||||
| 		}, | ||||
| 	await os.api('users/lists/delete', { | ||||
| 		listId: list.id, | ||||
| 	}); | ||||
| 	os.success(); | ||||
| 	mainRouter.push('/my/lists'); | ||||
| } | ||||
| 
 | ||||
| 		async deleteList() { | ||||
| 			const { canceled } = await os.confirm({ | ||||
| 				type: 'warning', | ||||
| 				text: this.$t('removeAreYouSure', { x: this.list.name }), | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| watch(() => props.listId, fetchList, { immediate: true }); | ||||
| 
 | ||||
| 			await os.api('users/lists/delete', { | ||||
| 				listId: this.list.id | ||||
| 			}); | ||||
| 			os.success(); | ||||
| 			this.$router.push('/my/lists'); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => list ? { | ||||
| 	title: list.name, | ||||
| 	icon: 'fas fa-list-ul', | ||||
| 	bg: 'var(--bg)', | ||||
| } : null)); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -8,14 +8,16 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.notFound, | ||||
| 		icon: 'fas fa-exclamation-triangle', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.notFound, | ||||
| 	icon: 'fas fa-exclamation-triangle', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,10 +1,11 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="800"> | ||||
| 	<div class="fcuexfpr"> | ||||
| 		<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> | ||||
| 			<div v-if="note" class="note"> | ||||
| 				<div v-if="showNext" class="_gap"> | ||||
| 					<XNotes class="_content" :pagination="next" :no-gap="true"/> | ||||
| 					<XNotes class="_content" :pagination="nextPagination" :no-gap="true"/> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<div class="main _gap"> | ||||
|  | @ -27,121 +28,112 @@ | |||
| 				</div> | ||||
| 
 | ||||
| 				<div v-if="showPrev" class="_gap"> | ||||
| 					<XNotes class="_content" :pagination="prev" :no-gap="true"/> | ||||
| 					<XNotes class="_content" :pagination="prevPagination" :no-gap="true"/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<MkError v-else-if="error" @retry="fetch()"/> | ||||
| 			<MkLoading v-else/> | ||||
| 		</transition> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineComponent, watch } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import XNote from '@/components/note.vue'; | ||||
| import XNoteDetailed from '@/components/note-detailed.vue'; | ||||
| import XNotes from '@/components/notes.vue'; | ||||
| import MkRemoteCaution from '@/components/remote-caution.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { i18n } from '@/i18n'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XNote, | ||||
| 		XNoteDetailed, | ||||
| 		XNotes, | ||||
| 		MkRemoteCaution, | ||||
| 		MkButton, | ||||
| 	}, | ||||
| 	props: { | ||||
| 		noteId: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.note ? { | ||||
| 				title: this.$ts.note, | ||||
| 				subtitle: new Date(this.note.createdAt).toLocaleString(), | ||||
| 				avatar: this.note.user, | ||||
| 				path: `/notes/${this.note.id}`, | ||||
| 				share: { | ||||
| 					title: this.$t('noteOf', { user: this.note.user.name }), | ||||
| 					text: this.note.text, | ||||
| 				}, | ||||
| 				bg: 'var(--bg)', | ||||
| 			} : null), | ||||
| 			note: null, | ||||
| 			clips: null, | ||||
| 			hasPrev: false, | ||||
| 			hasNext: false, | ||||
| 			showPrev: false, | ||||
| 			showNext: false, | ||||
| 			error: null, | ||||
| 			prev: { | ||||
| 				endpoint: 'users/notes' as const, | ||||
| 				limit: 10, | ||||
| 				params: computed(() => ({ | ||||
| 					userId: this.note.userId, | ||||
| 					untilId: this.note.id, | ||||
| 				})), | ||||
| 			}, | ||||
| 			next: { | ||||
| 				reversed: true, | ||||
| 				endpoint: 'users/notes' as const, | ||||
| 				limit: 10, | ||||
| 				params: computed(() => ({ | ||||
| 					userId: this.note.userId, | ||||
| 					sinceId: this.note.id, | ||||
| 				})), | ||||
| 			}, | ||||
| 		}; | ||||
| 	}, | ||||
| 	watch: { | ||||
| 		noteId: 'fetch' | ||||
| 	}, | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.hasPrev = false; | ||||
| 			this.hasNext = false; | ||||
| 			this.showPrev = false; | ||||
| 			this.showNext = false; | ||||
| 			this.note = null; | ||||
| 			os.api('notes/show', { | ||||
| 				noteId: this.noteId | ||||
| 			}).then(note => { | ||||
| 				this.note = note; | ||||
| 				Promise.all([ | ||||
| 					os.api('notes/clips', { | ||||
| 						noteId: note.id, | ||||
| 					}), | ||||
| 					os.api('users/notes', { | ||||
| 						userId: note.userId, | ||||
| 						untilId: note.id, | ||||
| 						limit: 1, | ||||
| 					}), | ||||
| 					os.api('users/notes', { | ||||
| 						userId: note.userId, | ||||
| 						sinceId: note.id, | ||||
| 						limit: 1, | ||||
| 					}), | ||||
| 				]).then(([clips, prev, next]) => { | ||||
| 					this.clips = clips; | ||||
| 					this.hasPrev = prev.length !== 0; | ||||
| 					this.hasNext = next.length !== 0; | ||||
| 				}); | ||||
| 			}).catch(err => { | ||||
| 				this.error = err; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| const props = defineProps<{ | ||||
| 	noteId: string; | ||||
| }>(); | ||||
| 
 | ||||
| let note = $ref<null | misskey.entities.Note>(); | ||||
| let clips = $ref(); | ||||
| let hasPrev = $ref(false); | ||||
| let hasNext = $ref(false); | ||||
| let showPrev = $ref(false); | ||||
| let showNext = $ref(false); | ||||
| let error = $ref(); | ||||
| 
 | ||||
| const prevPagination = { | ||||
| 	endpoint: 'users/notes' as const, | ||||
| 	limit: 10, | ||||
| 	params: computed(() => note ? ({ | ||||
| 		userId: note.userId, | ||||
| 		untilId: note.id, | ||||
| 	}) : null), | ||||
| }; | ||||
| 
 | ||||
| const nextPagination = { | ||||
| 	reversed: true, | ||||
| 	endpoint: 'users/notes' as const, | ||||
| 	limit: 10, | ||||
| 	params: computed(() => note ? ({ | ||||
| 		userId: note.userId, | ||||
| 		sinceId: note.id, | ||||
| 	}) : null), | ||||
| }; | ||||
| 
 | ||||
| function fetchNote() { | ||||
| 	hasPrev = false; | ||||
| 	hasNext = false; | ||||
| 	showPrev = false; | ||||
| 	showNext = false; | ||||
| 	note = null; | ||||
| 	os.api('notes/show', { | ||||
| 		noteId: props.noteId, | ||||
| 	}).then(res => { | ||||
| 		note = res; | ||||
| 		Promise.all([ | ||||
| 			os.api('notes/clips', { | ||||
| 				noteId: note.id, | ||||
| 			}), | ||||
| 			os.api('users/notes', { | ||||
| 				userId: note.userId, | ||||
| 				untilId: note.id, | ||||
| 				limit: 1, | ||||
| 			}), | ||||
| 			os.api('users/notes', { | ||||
| 				userId: note.userId, | ||||
| 				sinceId: note.id, | ||||
| 				limit: 1, | ||||
| 			}), | ||||
| 		]).then(([_clips, prev, next]) => { | ||||
| 			clips = _clips; | ||||
| 			hasPrev = prev.length !== 0; | ||||
| 			hasNext = next.length !== 0; | ||||
| 		}); | ||||
| 	}).catch(err => { | ||||
| 		error = err; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| watch(() => props.noteId, fetchNote, { | ||||
| 	immediate: true, | ||||
| }); | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => note ? { | ||||
| 	title: i18n.ts.note, | ||||
| 	subtitle: new Date(note.createdAt).toLocaleString(), | ||||
| 	avatar: note.user, | ||||
| 	path: `/notes/${note.id}`, | ||||
| 	share: { | ||||
| 		title: i18n.t('noteOf', { user: note.user.name }), | ||||
| 		text: note.text, | ||||
| 	}, | ||||
| 	bg: 'var(--bg)', | ||||
| } : null)); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,18 +1,21 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="800"> | ||||
| 	<div class="clupoqwt"> | ||||
| 		<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="800"> | ||||
| 		<div class="clupoqwt"> | ||||
| 			<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed } from 'vue'; | ||||
| import { notificationTypes } from 'misskey-js'; | ||||
| import XNotifications from '@/components/notifications.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { notificationTypes } from 'misskey-js'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| let tab = $ref('all'); | ||||
| let includeTypes = $ref<string[] | null>(null); | ||||
|  | @ -23,46 +26,46 @@ function setFilter(ev) { | |||
| 		active: includeTypes && includeTypes.includes(t), | ||||
| 		action: () => { | ||||
| 			includeTypes = [t]; | ||||
| 		} | ||||
| 		}, | ||||
| 	})); | ||||
| 	const items = includeTypes != null ? [{ | ||||
| 		icon: 'fas fa-times', | ||||
| 		text: i18n.ts.clear, | ||||
| 		action: () => { | ||||
| 			includeTypes = null; | ||||
| 		} | ||||
| 		}, | ||||
| 	}, null, ...typeItems] : typeItems; | ||||
| 	os.popupMenu(items, ev.currentTarget ?? ev.target); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.ts.notifications, | ||||
| 		icon: 'fas fa-bell', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			text: i18n.ts.filter, | ||||
| 			icon: 'fas fa-filter', | ||||
| 			highlighted: includeTypes != null, | ||||
| 			handler: setFilter, | ||||
| 		}, { | ||||
| 			text: i18n.ts.markAllAsRead, | ||||
| 			icon: 'fas fa-check', | ||||
| 			handler: () => { | ||||
| 				os.apiWithDialog('notifications/mark-all-as-read'); | ||||
| 			}, | ||||
| 		}], | ||||
| 		tabs: [{ | ||||
| 			active: tab === 'all', | ||||
| 			title: i18n.ts.all, | ||||
| 			onClick: () => { tab = 'all'; }, | ||||
| 		}, { | ||||
| 			active: tab === 'unread', | ||||
| 			title: i18n.ts.unread, | ||||
| 			onClick: () => { tab = 'unread'; }, | ||||
| 		},] | ||||
| 	})), | ||||
| }); | ||||
| const headerActions = $computed(() => [{ | ||||
| 	text: i18n.ts.filter, | ||||
| 	icon: 'fas fa-filter', | ||||
| 	highlighted: includeTypes != null, | ||||
| 	handler: setFilter, | ||||
| }, { | ||||
| 	text: i18n.ts.markAllAsRead, | ||||
| 	icon: 'fas fa-check', | ||||
| 	handler: () => { | ||||
| 		os.apiWithDialog('notifications/mark-all-as-read'); | ||||
| 	}, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => [{ | ||||
| 	active: tab === 'all', | ||||
| 	title: i18n.ts.all, | ||||
| 	onClick: () => { tab = 'all'; }, | ||||
| }, { | ||||
| 	active: tab === 'unread', | ||||
| 	title: i18n.ts.unread, | ||||
| 	onClick: () => { tab = 'unread'; }, | ||||
| }]); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: i18n.ts.notifications, | ||||
| 	icon: 'fas fa-bell', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="700"> | ||||
| 	<div class="jqqmcavi"> | ||||
| 		<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkButton> | ||||
| 		<MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> | ||||
|  | @ -55,7 +56,7 @@ | |||
| 			<XDraggable v-show="variables.length > 0" v-model="variables" tag="div" class="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> | ||||
| 				<template #item="{element}"> | ||||
| 					<XVariable | ||||
| 						:modelValue="element" | ||||
| 						:model-value="element" | ||||
| 						:removable="true" | ||||
| 						:hpml="hpml" | ||||
| 						:name="element.name" | ||||
|  | @ -75,11 +76,11 @@ | |||
| 			<MkTextarea v-model="script" class="_code"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, defineAsyncComponent, computed } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { defineComponent, defineAsyncComponent, computed, provide, watch } from 'vue'; | ||||
| import 'prismjs'; | ||||
| import { highlight, languages } from 'prismjs/components/prism-core'; | ||||
| import 'prismjs/components/prism-clike'; | ||||
|  | @ -101,367 +102,349 @@ import { url } from '@/config'; | |||
| import { collectPageVars } from '@/scripts/collect-page-vars'; | ||||
| import * as os from '@/os'; | ||||
| import { selectFile } from '@/scripts/select-file'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { mainRouter } from '@/router'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { $i } from '@/account'; | ||||
| const XDraggable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), | ||||
| 		XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput, | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	initPageId?: string; | ||||
| 	initPageName?: string; | ||||
| 	initUser?: string; | ||||
| }>(); | ||||
| 
 | ||||
| 	provide() { | ||||
| 		return { | ||||
| 			readonly: this.readonly, | ||||
| 			getScriptBlockList: this.getScriptBlockList, | ||||
| 			getPageBlockList: this.getPageBlockList | ||||
| 		}; | ||||
| 	}, | ||||
| let tab = $ref('settings'); | ||||
| let author = $ref($i); | ||||
| let readonly = $ref(false); | ||||
| let page = $ref(null); | ||||
| let pageId = $ref(null); | ||||
| let currentName = $ref(null); | ||||
| let title = $ref(''); | ||||
| let summary = $ref(null); | ||||
| let name = $ref(Date.now().toString()); | ||||
| let eyeCatchingImage = $ref(null); | ||||
| let eyeCatchingImageId = $ref(null); | ||||
| let font = $ref('sans-serif'); | ||||
| let content = $ref([]); | ||||
| let alignCenter = $ref(false); | ||||
| let hideTitleWhenPinned = $ref(false); | ||||
| let variables = $ref([]); | ||||
| let hpml = $ref(null); | ||||
| let script = $ref(''); | ||||
| 
 | ||||
| 	props: { | ||||
| 		initPageId: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		initPageName: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		initUser: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 	}, | ||||
| provide('readonly', readonly); | ||||
| provide('getScriptBlockList', getScriptBlockList); | ||||
| provide('getPageBlockList', getPageBlockList); | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => { | ||||
| 				let title = this.$ts._pages.newPage; | ||||
| 				if (this.initPageId) { | ||||
| 					title = this.$ts._pages.editPage; | ||||
| 				} | ||||
| 				else if (this.initPageName && this.initUser) { | ||||
| 					title = this.$ts._pages.readPage; | ||||
| 				} | ||||
| 				return { | ||||
| 					title: title, | ||||
| 					icon: 'fas fa-pencil-alt', | ||||
| 					bg: 'var(--bg)', | ||||
| 					tabs: [{ | ||||
| 						active: this.tab === 'settings', | ||||
| 						title: this.$ts._pages.pageSetting, | ||||
| 						icon: 'fas fa-cog', | ||||
| 						onClick: () => { this.tab = 'settings'; }, | ||||
| 					}, { | ||||
| 						active: this.tab === 'contents', | ||||
| 						title: this.$ts._pages.contents, | ||||
| 						icon: 'fas fa-sticky-note', | ||||
| 						onClick: () => { this.tab = 'contents'; }, | ||||
| 					}, { | ||||
| 						active: this.tab === 'variables', | ||||
| 						title: this.$ts._pages.variables, | ||||
| 						icon: 'fas fa-magic', | ||||
| 						onClick: () => { this.tab = 'variables'; }, | ||||
| 					}, { | ||||
| 						active: this.tab === 'script', | ||||
| 						title: this.$ts.script, | ||||
| 						icon: 'fas fa-code', | ||||
| 						onClick: () => { this.tab = 'script'; }, | ||||
| 					}], | ||||
| 				}; | ||||
| 			}), | ||||
| 			tab: 'settings', | ||||
| 			author: this.$i, | ||||
| 			readonly: false, | ||||
| 			page: null, | ||||
| 			pageId: null, | ||||
| 			currentName: null, | ||||
| 			title: '', | ||||
| 			summary: null, | ||||
| 			name: Date.now().toString(), | ||||
| 			eyeCatchingImage: null, | ||||
| 			eyeCatchingImageId: null, | ||||
| 			font: 'sans-serif', | ||||
| 			content: [], | ||||
| 			alignCenter: false, | ||||
| 			hideTitleWhenPinned: false, | ||||
| 			variables: [], | ||||
| 			hpml: null, | ||||
| 			script: '', | ||||
| 			url, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		async eyeCatchingImageId() { | ||||
| 			if (this.eyeCatchingImageId == null) { | ||||
| 				this.eyeCatchingImage = null; | ||||
| 			} else { | ||||
| 				this.eyeCatchingImage = await os.api('drive/files/show', { | ||||
| 					fileId: this.eyeCatchingImageId, | ||||
| 				}); | ||||
| 			} | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	async created() { | ||||
| 		this.hpml = new HpmlTypeChecker(); | ||||
| 
 | ||||
| 		this.$watch('variables', () => { | ||||
| 			this.hpml.variables = this.variables; | ||||
| 		}, { deep: true }); | ||||
| 
 | ||||
| 		this.$watch('content', () => { | ||||
| 			this.hpml.pageVars = collectPageVars(this.content); | ||||
| 		}, { deep: true }); | ||||
| 
 | ||||
| 		if (this.initPageId) { | ||||
| 			this.page = await os.api('pages/show', { | ||||
| 				pageId: this.initPageId, | ||||
| 			}); | ||||
| 		} else if (this.initPageName && this.initUser) { | ||||
| 			this.page = await os.api('pages/show', { | ||||
| 				name: this.initPageName, | ||||
| 				username: this.initUser, | ||||
| 			}); | ||||
| 			this.readonly = true; | ||||
| 		} | ||||
| 
 | ||||
| 		if (this.page) { | ||||
| 			this.author = this.page.user; | ||||
| 			this.pageId = this.page.id; | ||||
| 			this.title = this.page.title; | ||||
| 			this.name = this.page.name; | ||||
| 			this.currentName = this.page.name; | ||||
| 			this.summary = this.page.summary; | ||||
| 			this.font = this.page.font; | ||||
| 			this.script = this.page.script; | ||||
| 			this.hideTitleWhenPinned = this.page.hideTitleWhenPinned; | ||||
| 			this.alignCenter = this.page.alignCenter; | ||||
| 			this.content = this.page.content; | ||||
| 			this.variables = this.page.variables; | ||||
| 			this.eyeCatchingImageId = this.page.eyeCatchingImageId; | ||||
| 		} else { | ||||
| 			const id = uuid(); | ||||
| 			this.content = [{ | ||||
| 				id, | ||||
| 				type: 'text', | ||||
| 				text: 'Hello World!' | ||||
| 			}]; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		getSaveOptions() { | ||||
| 			return { | ||||
| 				title: this.title.trim(), | ||||
| 				name: this.name.trim(), | ||||
| 				summary: this.summary, | ||||
| 				font: this.font, | ||||
| 				script: this.script, | ||||
| 				hideTitleWhenPinned: this.hideTitleWhenPinned, | ||||
| 				alignCenter: this.alignCenter, | ||||
| 				content: this.content, | ||||
| 				variables: this.variables, | ||||
| 				eyeCatchingImageId: this.eyeCatchingImageId, | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		save() { | ||||
| 			const options = this.getSaveOptions(); | ||||
| 
 | ||||
| 			const onError = err => { | ||||
| 				if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') { | ||||
| 					if (err.info.param == 'name') { | ||||
| 						os.alert({ | ||||
| 							type: 'error', | ||||
| 							title: this.$ts._pages.invalidNameTitle, | ||||
| 							text: this.$ts._pages.invalidNameText | ||||
| 						}); | ||||
| 					} | ||||
| 				} else if (err.code == 'NAME_ALREADY_EXISTS') { | ||||
| 					os.alert({ | ||||
| 						type: 'error', | ||||
| 						text: this.$ts._pages.nameAlreadyExists | ||||
| 					}); | ||||
| 				} | ||||
| 			}; | ||||
| 
 | ||||
| 			if (this.pageId) { | ||||
| 				options.pageId = this.pageId; | ||||
| 				os.api('pages/update', options) | ||||
| 				.then(page => { | ||||
| 					this.currentName = this.name.trim(); | ||||
| 					os.alert({ | ||||
| 						type: 'success', | ||||
| 						text: this.$ts._pages.updated | ||||
| 					}); | ||||
| 				}).catch(onError); | ||||
| 			} else { | ||||
| 				os.api('pages/create', options) | ||||
| 				.then(page => { | ||||
| 					this.pageId = page.id; | ||||
| 					this.currentName = this.name.trim(); | ||||
| 					os.alert({ | ||||
| 						type: 'success', | ||||
| 						text: this.$ts._pages.created | ||||
| 					}); | ||||
| 					this.$router.push(`/pages/edit/${this.pageId}`); | ||||
| 				}).catch(onError); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		del() { | ||||
| 			os.confirm({ | ||||
| 				type: 'warning', | ||||
| 				text: this.$t('removeAreYouSure', { x: this.title.trim() }), | ||||
| 			}).then(({ canceled }) => { | ||||
| 				if (canceled) return; | ||||
| 				os.api('pages/delete', { | ||||
| 					pageId: this.pageId, | ||||
| 				}).then(() => { | ||||
| 					os.alert({ | ||||
| 						type: 'success', | ||||
| 						text: this.$ts._pages.deleted | ||||
| 					}); | ||||
| 					this.$router.push(`/pages`); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		duplicate() { | ||||
| 			this.title = this.title + ' - copy'; | ||||
| 			this.name = this.name + '-copy'; | ||||
| 			os.api('pages/create', this.getSaveOptions()).then(page => { | ||||
| 				this.pageId = page.id; | ||||
| 				this.currentName = this.name.trim(); | ||||
| 				os.alert({ | ||||
| 					type: 'success', | ||||
| 					text: this.$ts._pages.created | ||||
| 				}); | ||||
| 				this.$router.push(`/pages/edit/${this.pageId}`); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async add() { | ||||
| 			const { canceled, result: type } = await os.select({ | ||||
| 				type: null, | ||||
| 				title: this.$ts._pages.chooseBlock, | ||||
| 				groupedItems: this.getPageBlockList() | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 
 | ||||
| 			const id = uuid(); | ||||
| 			this.content.push({ id, type }); | ||||
| 		}, | ||||
| 
 | ||||
| 		async addVariable() { | ||||
| 			let { canceled, result: name } = await os.inputText({ | ||||
| 				title: this.$ts._pages.enterVariableName, | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 
 | ||||
| 			name = name.trim(); | ||||
| 
 | ||||
| 			if (this.hpml.isUsedName(name)) { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					text: this.$ts._pages.variableNameIsAlreadyUsed | ||||
| 				}); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			const id = uuid(); | ||||
| 			this.variables.push({ id, name, type: null }); | ||||
| 		}, | ||||
| 
 | ||||
| 		removeVariable(v) { | ||||
| 			this.variables = this.variables.filter(x => x.name !== v.name); | ||||
| 		}, | ||||
| 
 | ||||
| 		getPageBlockList() { | ||||
| 			return [{ | ||||
| 				label: this.$ts._pages.contentBlocks, | ||||
| 				items: [ | ||||
| 					{ value: 'section', text: this.$ts._pages.blocks.section }, | ||||
| 					{ value: 'text', text: this.$ts._pages.blocks.text }, | ||||
| 					{ value: 'image', text: this.$ts._pages.blocks.image }, | ||||
| 					{ value: 'textarea', text: this.$ts._pages.blocks.textarea }, | ||||
| 					{ value: 'note', text: this.$ts._pages.blocks.note }, | ||||
| 					{ value: 'canvas', text: this.$ts._pages.blocks.canvas }, | ||||
| 				] | ||||
| 			}, { | ||||
| 				label: this.$ts._pages.inputBlocks, | ||||
| 				items: [ | ||||
| 					{ value: 'button', text: this.$ts._pages.blocks.button }, | ||||
| 					{ value: 'radioButton', text: this.$ts._pages.blocks.radioButton }, | ||||
| 					{ value: 'textInput', text: this.$ts._pages.blocks.textInput }, | ||||
| 					{ value: 'textareaInput', text: this.$ts._pages.blocks.textareaInput }, | ||||
| 					{ value: 'numberInput', text: this.$ts._pages.blocks.numberInput }, | ||||
| 					{ value: 'switch', text: this.$ts._pages.blocks.switch }, | ||||
| 					{ value: 'counter', text: this.$ts._pages.blocks.counter } | ||||
| 				] | ||||
| 			}, { | ||||
| 				label: this.$ts._pages.specialBlocks, | ||||
| 				items: [ | ||||
| 					{ value: 'if', text: this.$ts._pages.blocks.if }, | ||||
| 					{ value: 'post', text: this.$ts._pages.blocks.post } | ||||
| 				] | ||||
| 			}]; | ||||
| 		}, | ||||
| 
 | ||||
| 		getScriptBlockList(type: string = null) { | ||||
| 			const list = []; | ||||
| 
 | ||||
| 			const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number'); | ||||
| 
 | ||||
| 			for (const block of blocks) { | ||||
| 				const category = list.find(x => x.category === block.category); | ||||
| 				if (category) { | ||||
| 					category.items.push({ | ||||
| 						value: block.type, | ||||
| 						text: this.$t(`_pages.script.blocks.${block.type}`) | ||||
| 					}); | ||||
| 				} else { | ||||
| 					list.push({ | ||||
| 						category: block.category, | ||||
| 						label: this.$t(`_pages.script.categories.${block.category}`), | ||||
| 						items: [{ | ||||
| 							value: block.type, | ||||
| 							text: this.$t(`_pages.script.blocks.${block.type}`) | ||||
| 						}] | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			const userFns = this.variables.filter(x => x.type === 'fn'); | ||||
| 			if (userFns.length > 0) { | ||||
| 				list.unshift({ | ||||
| 					label: this.$t(`_pages.script.categories.fn`), | ||||
| 					items: userFns.map(v => ({ | ||||
| 						value: 'fn:' + v.name, | ||||
| 						text: v.name | ||||
| 					})) | ||||
| 				}); | ||||
| 			} | ||||
| 
 | ||||
| 			return list; | ||||
| 		}, | ||||
| 
 | ||||
| 		setEyeCatchingImage(e) { | ||||
| 			selectFile(e.currentTarget ?? e.target, null).then(file => { | ||||
| 				this.eyeCatchingImageId = file.id; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		removeEyeCatchingImage() { | ||||
| 			this.eyeCatchingImageId = null; | ||||
| 		}, | ||||
| 
 | ||||
| 		highlighter(code) { | ||||
| 			return highlight(code, languages.js, 'javascript'); | ||||
| 		}, | ||||
| watch($$(eyeCatchingImageId), async () => { | ||||
| 	if (eyeCatchingImageId == null) { | ||||
| 		eyeCatchingImage = null; | ||||
| 	} else { | ||||
| 		eyeCatchingImage = await os.api('drive/files/show', { | ||||
| 			fileId: eyeCatchingImageId, | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| function getSaveOptions() { | ||||
| 	return { | ||||
| 		title: tatitle.trim(), | ||||
| 		name: taname.trim(), | ||||
| 		summary: tasummary, | ||||
| 		font: tafont, | ||||
| 		script: tascript, | ||||
| 		hideTitleWhenPinned: tahideTitleWhenPinned, | ||||
| 		alignCenter: taalignCenter, | ||||
| 		content: tacontent, | ||||
| 		variables: tavariables, | ||||
| 		eyeCatchingImageId: taeyeCatchingImageId, | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| function save() { | ||||
| 	const options = tagetSaveOptions(); | ||||
| 
 | ||||
| 	const onError = err => { | ||||
| 		if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') { | ||||
| 			if (err.info.param == 'name') { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					title: i18n.ts._pages.invalidNameTitle, | ||||
| 					text: i18n.ts._pages.invalidNameText, | ||||
| 				}); | ||||
| 			} | ||||
| 		} else if (err.code == 'NAME_ALREADY_EXISTS') { | ||||
| 			os.alert({ | ||||
| 				type: 'error', | ||||
| 				text: i18n.ts._pages.nameAlreadyExists, | ||||
| 			}); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	if (tapageId) { | ||||
| 		options.pageId = tapageId; | ||||
| 		os.api('pages/update', options) | ||||
| 		.then(page => { | ||||
| 			tacurrentName = taname.trim(); | ||||
| 			os.alert({ | ||||
| 				type: 'success', | ||||
| 				text: i18n.ts._pages.updated, | ||||
| 			}); | ||||
| 		}).catch(onError); | ||||
| 	} else { | ||||
| 		os.api('pages/create', options) | ||||
| 		.then(created => { | ||||
| 			tapageId = created.id; | ||||
| 			tacurrentName = name.trim(); | ||||
| 			os.alert({ | ||||
| 				type: 'success', | ||||
| 				text: i18n.ts._pages.created, | ||||
| 			}); | ||||
| 			mainRouter.push(`/pages/edit/${pageId}`); | ||||
| 		}).catch(onError); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| function del() { | ||||
| 	os.confirm({ | ||||
| 		type: 'warning', | ||||
| 		text: i18n.t('removeAreYouSure', { x: title.trim() }), | ||||
| 	}).then(({ canceled }) => { | ||||
| 		if (canceled) return; | ||||
| 		os.api('pages/delete', { | ||||
| 			pageId: pageId, | ||||
| 		}).then(() => { | ||||
| 			os.alert({ | ||||
| 				type: 'success', | ||||
| 				text: i18n.ts._pages.deleted, | ||||
| 			}); | ||||
| 			mainRouter.push('/pages'); | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function duplicate() { | ||||
| 	tatitle = tatitle + ' - copy'; | ||||
| 	taname = taname + '-copy'; | ||||
| 	os.api('pages/create', tagetSaveOptions()).then(created => { | ||||
| 		tapageId = created.id; | ||||
| 		tacurrentName = taname.trim(); | ||||
| 		os.alert({ | ||||
| 			type: 'success', | ||||
| 			text: i18n.ts._pages.created, | ||||
| 		}); | ||||
| 		mainRouter.push(`/pages/edit/${pageId}`); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function add() { | ||||
| 	const { canceled, result: type } = await os.select({ | ||||
| 		type: null, | ||||
| 		title: i18n.ts._pages.chooseBlock, | ||||
| 		groupedItems: tagetPageBlockList(), | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 
 | ||||
| 	const id = uuid(); | ||||
| 	tacontent.push({ id, type }); | ||||
| } | ||||
| 
 | ||||
| async function addVariable() { | ||||
| 	let { canceled, result: name } = await os.inputText({ | ||||
| 		title: i18n.ts._pages.enterVariableName, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 
 | ||||
| 	name = name.trim(); | ||||
| 
 | ||||
| 	if (tahpml.isUsedName(name)) { | ||||
| 		os.alert({ | ||||
| 			type: 'error', | ||||
| 			text: i18n.ts._pages.variableNameIsAlreadyUsed, | ||||
| 		}); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const id = uuid(); | ||||
| 	tavariables.push({ id, name, type: null }); | ||||
| } | ||||
| 
 | ||||
| function removeVariable(v) { | ||||
| 	tavariables = tavariables.filter(x => x.name !== v.name); | ||||
| } | ||||
| 
 | ||||
| function getPageBlockList() { | ||||
| 	return [{ | ||||
| 		label: i18n.ts._pages.contentBlocks, | ||||
| 		items: [ | ||||
| 			{ value: 'section', text: i18n.ts._pages.blocks.section }, | ||||
| 			{ value: 'text', text: i18n.ts._pages.blocks.text }, | ||||
| 			{ value: 'image', text: i18n.ts._pages.blocks.image }, | ||||
| 			{ value: 'textarea', text: i18n.ts._pages.blocks.textarea }, | ||||
| 			{ value: 'note', text: i18n.ts._pages.blocks.note }, | ||||
| 			{ value: 'canvas', text: i18n.ts._pages.blocks.canvas }, | ||||
| 		], | ||||
| 	}, { | ||||
| 		label: i18n.ts._pages.inputBlocks, | ||||
| 		items: [ | ||||
| 			{ value: 'button', text: i18n.ts._pages.blocks.button }, | ||||
| 			{ value: 'radioButton', text: i18n.ts._pages.blocks.radioButton }, | ||||
| 			{ value: 'textInput', text: i18n.ts._pages.blocks.textInput }, | ||||
| 			{ value: 'textareaInput', text: i18n.ts._pages.blocks.textareaInput }, | ||||
| 			{ value: 'numberInput', text: i18n.ts._pages.blocks.numberInput }, | ||||
| 			{ value: 'switch', text: i18n.ts._pages.blocks.switch }, | ||||
| 			{ value: 'counter', text: i18n.ts._pages.blocks.counter }, | ||||
| 		], | ||||
| 	}, { | ||||
| 		label: i18n.ts._pages.specialBlocks, | ||||
| 		items: [ | ||||
| 			{ value: 'if', text: i18n.ts._pages.blocks.if }, | ||||
| 			{ value: 'post', text: i18n.ts._pages.blocks.post }, | ||||
| 		], | ||||
| 	}]; | ||||
| } | ||||
| 
 | ||||
| function getScriptBlockList(type: string = null) { | ||||
| 	const list = []; | ||||
| 
 | ||||
| 	const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number'); | ||||
| 
 | ||||
| 	for (const block of blocks) { | ||||
| 		const category = list.find(x => x.category === block.category); | ||||
| 		if (category) { | ||||
| 			category.items.push({ | ||||
| 				value: block.type, | ||||
| 				text: i18n.t(`_pages.script.blocks.${block.type}`), | ||||
| 			}); | ||||
| 		} else { | ||||
| 			list.push({ | ||||
| 				category: block.category, | ||||
| 				label: i18n.t(`_pages.script.categories.${block.category}`), | ||||
| 				items: [{ | ||||
| 					value: block.type, | ||||
| 					text: i18n.t(`_pages.script.blocks.${block.type}`), | ||||
| 				}], | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	const userFns = variables.filter(x => x.type === 'fn'); | ||||
| 	if (userFns.length > 0) { | ||||
| 		list.unshift({ | ||||
| 			label: i18n.t('_pages.script.categories.fn'), | ||||
| 			items: userFns.map(v => ({ | ||||
| 				value: 'fn:' + v.name, | ||||
| 				text: v.name, | ||||
| 			})), | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	return list; | ||||
| } | ||||
| 
 | ||||
| function setEyeCatchingImage(e) { | ||||
| 	selectFile(e.currentTarget ?? e.target, null).then(file => { | ||||
| 		eyeCatchingImageId = file.id; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function removeEyeCatchingImage() { | ||||
| 	taeyeCatchingImageId = null; | ||||
| } | ||||
| 
 | ||||
| function highlighter(code) { | ||||
| 	return highlight(code, languages.js, 'javascript'); | ||||
| } | ||||
| 
 | ||||
| async function init() { | ||||
| 	hpml = new HpmlTypeChecker(); | ||||
| 
 | ||||
| 	watch($$(variables), () => { | ||||
| 		hpml.variables = variables; | ||||
| 	}, { deep: true }); | ||||
| 
 | ||||
| 	watch($$(content), () => { | ||||
| 		hpml.pageVars = collectPageVars(content); | ||||
| 	}, { deep: true }); | ||||
| 
 | ||||
| 	if (props.initPageId) { | ||||
| 		page = await os.api('pages/show', { | ||||
| 			pageId: props.initPageId, | ||||
| 		}); | ||||
| 	} else if (props.initPageName && props.initUser) { | ||||
| 		page = await os.api('pages/show', { | ||||
| 			name: props.initPageName, | ||||
| 			username: props.initUser, | ||||
| 		}); | ||||
| 		readonly = true; | ||||
| 	} | ||||
| 
 | ||||
| 	if (page) { | ||||
| 		author = page.user; | ||||
| 		pageId = page.id; | ||||
| 		title = page.title; | ||||
| 		name = page.name; | ||||
| 		currentName = page.name; | ||||
| 		summary = page.summary; | ||||
| 		font = page.font; | ||||
| 		script = page.script; | ||||
| 		hideTitleWhenPinned = page.hideTitleWhenPinned; | ||||
| 		alignCenter = page.alignCenter; | ||||
| 		content = page.content; | ||||
| 		variables = page.variables; | ||||
| 		eyeCatchingImageId = page.eyeCatchingImageId; | ||||
| 	} else { | ||||
| 		const id = uuid(); | ||||
| 		content = [{ | ||||
| 			id, | ||||
| 			type: 'text', | ||||
| 			text: 'Hello World!', | ||||
| 		}]; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| init(); | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => { | ||||
| 	let title = i18n.ts._pages.newPage; | ||||
| 	if (props.initPageId) { | ||||
| 		title = i18n.ts._pages.editPage; | ||||
| 	} | ||||
| 	else if (props.initPageName && props.initUser) { | ||||
| 		title = i18n.ts._pages.readPage; | ||||
| 	} | ||||
| 	return { | ||||
| 		title: title, | ||||
| 		icon: 'fas fa-pencil-alt', | ||||
| 		bg: 'var(--bg)', | ||||
| 		tabs: [{ | ||||
| 			active: tab === 'settings', | ||||
| 			title: i18n.ts._pages.pageSetting, | ||||
| 			icon: 'fas fa-cog', | ||||
| 			onClick: () => { tab = 'settings'; }, | ||||
| 		}, { | ||||
| 			active: tab === 'contents', | ||||
| 			title: i18n.ts._pages.contents, | ||||
| 			icon: 'fas fa-sticky-note', | ||||
| 			onClick: () => { tab = 'contents'; }, | ||||
| 		}, { | ||||
| 			active: tab === 'variables', | ||||
| 			title: i18n.ts._pages.variables, | ||||
| 			icon: 'fas fa-magic', | ||||
| 			onClick: () => { tab = 'variables'; }, | ||||
| 		}, { | ||||
| 			active: tab === 'script', | ||||
| 			title: i18n.ts.script, | ||||
| 			icon: 'fas fa-code', | ||||
| 			onClick: () => { tab = 'script'; }, | ||||
| 		}], | ||||
| 	}; | ||||
| })); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| <template><MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 		<MkSpacer :content-max="700"> | ||||
| 	<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> | ||||
| 		<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh"> | ||||
| 			<div class="_block main"> | ||||
|  | @ -56,138 +57,108 @@ | |||
| 		<MkError v-else-if="error" @retry="fetch()"/> | ||||
| 		<MkLoading v-else/> | ||||
| 	</transition> | ||||
| </MkSpacer> | ||||
| </MkSpacer></MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, watch } from 'vue'; | ||||
| import XPage from '@/components/page/page.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { url } from '@/config'; | ||||
| import MkFollowButton from '@/components/follow-button.vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkPagePreview from '@/components/page-preview.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XPage, | ||||
| 		MkButton, | ||||
| 		MkFollowButton, | ||||
| 		MkContainer, | ||||
| 		MkPagination, | ||||
| 		MkPagePreview, | ||||
| const props = defineProps<{ | ||||
| 	pageName: string; | ||||
| 	username: string; | ||||
| }>(); | ||||
| 
 | ||||
| let page = $ref(null); | ||||
| let error = $ref(null); | ||||
| const otherPostsPagination = { | ||||
| 	endpoint: 'users/pages' as const, | ||||
| 	limit: 6, | ||||
| 	params: computed(() => ({ | ||||
| 		userId: page.user.id, | ||||
| 	})), | ||||
| }; | ||||
| const path = $computed(() => props.username + '/' + props.pageName); | ||||
| 
 | ||||
| function fetchPage() { | ||||
| 	page = null; | ||||
| 	os.api('pages/show', { | ||||
| 		name: props.pageName, | ||||
| 		username: props.username, | ||||
| 	}).then(_page => { | ||||
| 		page = _page; | ||||
| 	}).catch(err => { | ||||
| 		error = err; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function share() { | ||||
| 	navigator.share({ | ||||
| 		title: page.title ?? page.name, | ||||
| 		text: page.summary, | ||||
| 		url: `${url}/@${page.user.username}/pages/${page.name}`, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function shareWithNote() { | ||||
| 	os.post({ | ||||
| 		initialText: `${page.title || page.name} ${url}/@${page.user.username}/pages/${page.name}`, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function like() { | ||||
| 	os.apiWithDialog('pages/like', { | ||||
| 		pageId: page.id, | ||||
| 	}).then(() => { | ||||
| 		page.isLiked = true; | ||||
| 		page.likedCount++; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function unlike() { | ||||
| 	const confirm = await os.confirm({ | ||||
| 		type: 'warning', | ||||
| 		text: i18n.ts.unlikeConfirm, | ||||
| 	}); | ||||
| 	if (confirm.canceled) return; | ||||
| 	os.apiWithDialog('pages/unlike', { | ||||
| 		pageId: page.id, | ||||
| 	}).then(() => { | ||||
| 		page.isLiked = false; | ||||
| 		page.likedCount--; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function pin(pin) { | ||||
| 	os.apiWithDialog('i/update', { | ||||
| 		pinnedPageId: pin ? page.id : null, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| watch(() => path, fetchPage, { immediate: true }); | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => page ? { | ||||
| 	title: computed(() => page.title || page.name), | ||||
| 	avatar: page.user, | ||||
| 	path: `/@${page.user.username}/pages/${page.name}`, | ||||
| 	share: { | ||||
| 		title: page.title || page.name, | ||||
| 		text: page.summary, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		pageName: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		username: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.page ? { | ||||
| 				title: computed(() => this.page.title || this.page.name), | ||||
| 				avatar: this.page.user, | ||||
| 				path: `/@${this.page.user.username}/pages/${this.page.name}`, | ||||
| 				share: { | ||||
| 					title: this.page.title || this.page.name, | ||||
| 					text: this.page.summary, | ||||
| 				}, | ||||
| 			} : null), | ||||
| 			page: null, | ||||
| 			error: null, | ||||
| 			otherPostsPagination: { | ||||
| 				endpoint: 'users/pages' as const, | ||||
| 				limit: 6, | ||||
| 				params: computed(() => ({ | ||||
| 					userId: this.page.user.id | ||||
| 				})), | ||||
| 			}, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		path(): string { | ||||
| 			return this.username + '/' + this.pageName; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		path() { | ||||
| 			this.fetch(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetch() { | ||||
| 			this.page = null; | ||||
| 			os.api('pages/show', { | ||||
| 				name: this.pageName, | ||||
| 				username: this.username, | ||||
| 			}).then(page => { | ||||
| 				this.page = page; | ||||
| 			}).catch(err => { | ||||
| 				this.error = err; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		share() { | ||||
| 			navigator.share({ | ||||
| 				title: this.page.title || this.page.name, | ||||
| 				text: this.page.summary, | ||||
| 				url: `${url}/@${this.page.user.username}/pages/${this.page.name}` | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		shareWithNote() { | ||||
| 			os.post({ | ||||
| 				initialText: `${this.page.title || this.page.name} ${url}/@${this.page.user.username}/pages/${this.page.name}` | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		like() { | ||||
| 			os.apiWithDialog('pages/like', { | ||||
| 				pageId: this.page.id, | ||||
| 			}).then(() => { | ||||
| 				this.page.isLiked = true; | ||||
| 				this.page.likedCount++; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		async unlike() { | ||||
| 			const confirm = await os.confirm({ | ||||
| 				type: 'warning', | ||||
| 				text: this.$ts.unlikeConfirm, | ||||
| 			}); | ||||
| 			if (confirm.canceled) return; | ||||
| 			os.apiWithDialog('pages/unlike', { | ||||
| 				pageId: this.page.id, | ||||
| 			}).then(() => { | ||||
| 				this.page.isLiked = false; | ||||
| 				this.page.likedCount--; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		pin(pin) { | ||||
| 			os.apiWithDialog('i/update', { | ||||
| 				pinnedPageId: pin ? this.page.id : null, | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| } : null)); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,86 +1,87 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="700"> | ||||
| 	<div v-if="tab === 'featured'" class="rknalgpo"> | ||||
| 		<MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> | ||||
| 			<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="700"> | ||||
| 		<div v-if="tab === 'featured'" class="rknalgpo"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> | ||||
| 				<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 
 | ||||
| 	<div v-else-if="tab === 'my'" class="rknalgpo my"> | ||||
| 		<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> | ||||
| 		<MkPagination v-slot="{items}" :pagination="myPagesPagination"> | ||||
| 			<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| 		<div v-else-if="tab === 'my'" class="rknalgpo my"> | ||||
| 			<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> | ||||
| 			<MkPagination v-slot="{items}" :pagination="myPagesPagination"> | ||||
| 				<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 
 | ||||
| 	<div v-else-if="tab === 'liked'" class="rknalgpo"> | ||||
| 		<MkPagination v-slot="{items}" :pagination="likedPagesPagination"> | ||||
| 			<MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/> | ||||
| 		</MkPagination> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 		<div v-else-if="tab === 'liked'" class="rknalgpo"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="likedPagesPagination"> | ||||
| 				<MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/> | ||||
| 			</MkPagination> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { computed, inject } from 'vue'; | ||||
| import MkPagePreview from '@/components/page-preview.vue'; | ||||
| import MkPagination from '@/components/ui/pagination.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkPagePreview, MkPagination, MkButton | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 				title: this.$ts.pages, | ||||
| 				icon: 'fas fa-sticky-note', | ||||
| 				bg: 'var(--bg)', | ||||
| 				actions: [{ | ||||
| 					icon: 'fas fa-plus', | ||||
| 					text: this.$ts.create, | ||||
| 					handler: this.create, | ||||
| 				}], | ||||
| 				tabs: [{ | ||||
| 					active: this.tab === 'featured', | ||||
| 					title: this.$ts._pages.featured, | ||||
| 					icon: 'fas fa-fire-alt', | ||||
| 					onClick: () => { this.tab = 'featured'; }, | ||||
| 				}, { | ||||
| 					active: this.tab === 'my', | ||||
| 					title: this.$ts._pages.my, | ||||
| 					icon: 'fas fa-edit', | ||||
| 					onClick: () => { this.tab = 'my'; }, | ||||
| 				}, { | ||||
| 					active: this.tab === 'liked', | ||||
| 					title: this.$ts._pages.liked, | ||||
| 					icon: 'fas fa-heart', | ||||
| 					onClick: () => { this.tab = 'liked'; }, | ||||
| 				},] | ||||
| 			})), | ||||
| 			tab: 'featured', | ||||
| 			featuredPagesPagination: { | ||||
| 				endpoint: 'pages/featured' as const, | ||||
| 				noPaging: true, | ||||
| 			}, | ||||
| 			myPagesPagination: { | ||||
| 				endpoint: 'i/pages' as const, | ||||
| 				limit: 5, | ||||
| 			}, | ||||
| 			likedPagesPagination: { | ||||
| 				endpoint: 'i/page-likes' as const, | ||||
| 				limit: 5, | ||||
| 			}, | ||||
| 		}; | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		create() { | ||||
| 			this.$router.push(`/pages/new`); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| let tab = $ref('featured'); | ||||
| 
 | ||||
| const featuredPagesPagination = { | ||||
| 	endpoint: 'pages/featured' as const, | ||||
| 	noPaging: true, | ||||
| }; | ||||
| const myPagesPagination = { | ||||
| 	endpoint: 'i/pages' as const, | ||||
| 	limit: 5, | ||||
| }; | ||||
| const likedPagesPagination = { | ||||
| 	endpoint: 'i/page-likes' as const, | ||||
| 	limit: 5, | ||||
| }; | ||||
| 
 | ||||
| function create() { | ||||
| 	router.push('/pages/new'); | ||||
| } | ||||
| 
 | ||||
| const headerActions = $computed(() => [{ | ||||
| 	icon: 'fas fa-plus', | ||||
| 	text: i18n.ts.create, | ||||
| 	handler: create, | ||||
| }]); | ||||
| 
 | ||||
| const headerTabs = $computed(() => [{ | ||||
| 	active: tab === 'featured', | ||||
| 	title: i18n.ts._pages.featured, | ||||
| 	icon: 'fas fa-fire-alt', | ||||
| 	onClick: () => { tab = 'featured'; }, | ||||
| }, { | ||||
| 	active: tab === 'my', | ||||
| 	title: i18n.ts._pages.my, | ||||
| 	icon: 'fas fa-edit', | ||||
| 	onClick: () => { tab = 'my'; }, | ||||
| }, { | ||||
| 	active: tab === 'liked', | ||||
| 	title: i18n.ts._pages.liked, | ||||
| 	icon: 'fas fa-heart', | ||||
| 	onClick: () => { tab = 'liked'; }, | ||||
| }]); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: i18n.ts.pages, | ||||
| 	icon: 'fas fa-sticky-note', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -7,16 +7,18 @@ | |||
| <script lang="ts" setup> | ||||
| import { computed } from 'vue'; | ||||
| import MkSample from '@/components/sample.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.ts.preview, | ||||
| 		icon: 'fas fa-eye', | ||||
| 		bg: 'var(--bg)', | ||||
| 	})), | ||||
| }); | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: i18n.ts.preview, | ||||
| 	icon: 'fas fa-eye', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,14 +1,17 @@ | |||
| <template> | ||||
| <MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 	<div class="_formRoot"> | ||||
| 		<FormInput v-model="password" type="password" class="_formBlock"> | ||||
| 			<template #prefix><i class="fas fa-lock"></i></template> | ||||
| 			<template #label>{{ i18n.ts.newPassword }}</template> | ||||
| 		</FormInput> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32"> | ||||
| 		<div class="_formRoot"> | ||||
| 			<FormInput v-model="password" type="password" class="_formBlock"> | ||||
| 				<template #prefix><i class="fas fa-lock"></i></template> | ||||
| 				<template #label>{{ i18n.ts.newPassword }}</template> | ||||
| 			</FormInput> | ||||
| 		 | ||||
| 		<FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 			<FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
|  | @ -16,9 +19,9 @@ import { defineAsyncComponent, onMounted } from 'vue'; | |||
| import FormInput from '@/components/form/input.vue'; | ||||
| import FormButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { router } from '@/router'; | ||||
| import { mainRouter } from '@/router'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	token?: string; | ||||
|  | @ -31,22 +34,24 @@ async function save() { | |||
| 		token: props.token, | ||||
| 		password: password, | ||||
| 	}); | ||||
| 	router.push('/'); | ||||
| 	mainRouter.push('/'); | ||||
| } | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	if (props.token == null) { | ||||
| 		os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {}, 'closed'); | ||||
| 		router.push('/'); | ||||
| 		mainRouter.push('/'); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.resetPassword, | ||||
| 		icon: 'fas fa-lock', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.resetPassword, | ||||
| 	icon: 'fas fa-lock', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { defineExpose, ref, watch } from 'vue'; | ||||
| import { ref, watch } from 'vue'; | ||||
| import 'prismjs'; | ||||
| import { highlight, languages } from 'prismjs/components/prism-core'; | ||||
| import 'prismjs/components/prism-clike'; | ||||
|  | @ -32,9 +32,9 @@ import MkContainer from '@/components/ui/container.vue'; | |||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import { createAiScriptEnv } from '@/scripts/aiscript/api'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { $i } from '@/account'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const code = ref(''); | ||||
| const logs = ref<any[]>([]); | ||||
|  | @ -67,7 +67,7 @@ async function run() { | |||
| 			logs.value.push({ | ||||
| 				id: Math.random(), | ||||
| 				text: value.type === 'str' ? value.value : utils.valToString(value), | ||||
| 				print: true | ||||
| 				print: true, | ||||
| 			}); | ||||
| 		}, | ||||
| 		log: (type, params) => { | ||||
|  | @ -75,11 +75,11 @@ async function run() { | |||
| 				case 'end': logs.value.push({ | ||||
| 					id: Math.random(), | ||||
| 					text: utils.valToString(params.val, true), | ||||
| 					print: false | ||||
| 					print: false, | ||||
| 				}); break; | ||||
| 				default: break; | ||||
| 			} | ||||
| 		} | ||||
| 		}, | ||||
| 	}); | ||||
| 
 | ||||
| 	let ast; | ||||
|  | @ -88,7 +88,7 @@ async function run() { | |||
| 	} catch (error) { | ||||
| 		os.alert({ | ||||
| 			type: 'error', | ||||
| 			text: 'Syntax error :(' | ||||
| 			text: 'Syntax error :(', | ||||
| 		}); | ||||
| 		return; | ||||
| 	} | ||||
|  | @ -97,7 +97,7 @@ async function run() { | |||
| 	} catch (error: any) { | ||||
| 		os.alert({ | ||||
| 			type: 'error', | ||||
| 			text: error.message | ||||
| 			text: error.message, | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  | @ -106,11 +106,13 @@ function highlighter(code) { | |||
| 	return highlight(code, languages.js, 'javascript'); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.scratchpad, | ||||
| 		icon: 'fas fa-terminal', | ||||
| 	}, | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.scratchpad, | ||||
| 	icon: 'fas fa-terminal', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,16 +1,17 @@ | |||
| <template> | ||||
| <div class="_section"> | ||||
| 	<div class="_content"> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="800"> | ||||
| 		<XNotes ref="notes" :pagination="pagination"/> | ||||
| 	</div> | ||||
| </div> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed } from 'vue'; | ||||
| import XNotes from '@/components/notes.vue'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	query: string; | ||||
|  | @ -23,14 +24,16 @@ const pagination = { | |||
| 	params: computed(() => ({ | ||||
| 		query: props.query, | ||||
| 		channelId: props.channel, | ||||
| 	})) | ||||
| 	})), | ||||
| }; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.t('searchWith', { q: props.query }), | ||||
| 		icon: 'fas fa-search', | ||||
| 		bg: 'var(--bg)', | ||||
| 	})), | ||||
| }); | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(computed(() => ({ | ||||
| 	title: i18n.t('searchWith', { q: props.query }), | ||||
| 	icon: 'fas fa-search', | ||||
| 	bg: 'var(--bg)', | ||||
| }))); | ||||
| </script> | ||||
|  |  | |||
|  | @ -127,30 +127,32 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { defineExpose, onMounted, ref } from 'vue'; | ||||
| import { onMounted, ref } from 'vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import MkKeyValue from '@/components/key-value.vue'; | ||||
| import * as os from '@/os'; | ||||
| import number from '@/filters/number'; | ||||
| import bytes from '@/filters/bytes'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { $i } from '@/account'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const stats = ref<any>({}); | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	os.api('users/stats', { | ||||
| 		userId: $i!.id | ||||
| 		userId: $i!.id, | ||||
| 	}).then(response => { | ||||
| 		stats.value = response; | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.accountInfo, | ||||
| 		icon: 'fas fa-info-circle' | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.accountInfo, | ||||
| 	icon: 'fas fa-info-circle', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -21,13 +21,13 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { defineAsyncComponent, defineExpose, ref } from 'vue'; | ||||
| import { defineAsyncComponent, ref } from 'vue'; | ||||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import FormButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { getAccounts, addAccount as addAccounts, login, $i } from '@/account'; | ||||
| import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const storedAccounts = ref<any>(null); | ||||
| const accounts = ref<any>(null); | ||||
|  | @ -39,7 +39,7 @@ const init = async () => { | |||
| 		console.log(storedAccounts.value); | ||||
| 
 | ||||
| 		return os.api('users/show', { | ||||
| 			userIds: storedAccounts.value.map(x => x.id) | ||||
| 			userIds: storedAccounts.value.map(x => x.id), | ||||
| 		}); | ||||
| 	}).then(response => { | ||||
| 		accounts.value = response; | ||||
|  | @ -70,6 +70,10 @@ function addAccount(ev) { | |||
| 	}], ev.currentTarget ?? ev.target); | ||||
| } | ||||
| 
 | ||||
| function removeAccount(account) { | ||||
| 	_removeAccount(account.id); | ||||
| } | ||||
| 
 | ||||
| function addExistingAccount() { | ||||
| 	os.popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, { | ||||
| 		done: res => { | ||||
|  | @ -98,12 +102,14 @@ function switchAccountWithToken(token: string) { | |||
| 	login(token); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.accounts, | ||||
| 		icon: 'fas fa-users', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.accounts, | ||||
| 	icon: 'fas fa-users', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,12 +7,12 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { defineAsyncComponent, defineExpose, ref } from 'vue'; | ||||
| import { defineAsyncComponent, ref } from 'vue'; | ||||
| import FormLink from '@/components/form/link.vue'; | ||||
| import FormButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const isDesktop = ref(window.innerWidth >= 1100); | ||||
| 
 | ||||
|  | @ -29,17 +29,19 @@ function generateToken() { | |||
| 			os.alert({ | ||||
| 				type: 'success', | ||||
| 				title: i18n.ts.token, | ||||
| 				text: token | ||||
| 				text: token, | ||||
| 			}); | ||||
| 		}, | ||||
| 	}, 'closed'); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: 'API', | ||||
| 		icon: 'fas fa-key', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: 'API', | ||||
| 	icon: 'fas fa-key', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
| 				<div>{{ i18n.ts.nothing }}</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 		<template v-slot="{items}"> | ||||
| 		<template #default="{items}"> | ||||
| 			<div v-for="token in items" :key="token.id" class="_panel bfomjevm"> | ||||
| 				<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/> | ||||
| 				<div class="body"> | ||||
|  | @ -38,11 +38,11 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { defineExpose, ref } from 'vue'; | ||||
| import { ref } from 'vue'; | ||||
| import FormPagination from '@/components/ui/pagination.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const list = ref<any>(null); | ||||
| 
 | ||||
|  | @ -50,8 +50,8 @@ const pagination = { | |||
| 	endpoint: 'i/apps' as const, | ||||
| 	limit: 100, | ||||
| 	params: { | ||||
| 		sort: '+lastUsedAt' | ||||
| 	} | ||||
| 		sort: '+lastUsedAt', | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| function revoke(token) { | ||||
|  | @ -60,12 +60,14 @@ function revoke(token) { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.installedApps, | ||||
| 		icon: 'fas fa-plug', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.installedApps, | ||||
| 	icon: 'fas fa-plug', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,13 +9,13 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { defineExpose, ref, watch } from 'vue'; | ||||
| import { ref, watch } from 'vue'; | ||||
| import FormTextarea from '@/components/form/textarea.vue'; | ||||
| import FormInfo from '@/components/ui/info.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { unisonReload } from '@/scripts/unison-reload'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const localCustomCss = ref(localStorage.getItem('customCss') ?? ''); | ||||
| 
 | ||||
|  | @ -35,11 +35,13 @@ watch(localCustomCss, async () => { | |||
| 	await apply(); | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.customCss, | ||||
| 		icon: 'fas fa-code', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.customCss, | ||||
| 	icon: 'fas fa-code', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineExpose, watch } from 'vue'; | ||||
| import { computed, watch } from 'vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import FormLink from '@/components/form/link.vue'; | ||||
| import FormRadios from '@/components/form/radios.vue'; | ||||
|  | @ -39,8 +39,8 @@ import FormGroup from '@/components/form/group.vue'; | |||
| import { deckStore } from '@/ui/deck/deck-store'; | ||||
| import * as os from '@/os'; | ||||
| import { unisonReload } from '@/scripts/unison-reload'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const navWindow = computed(deckStore.makeGetterSetter('navWindow')); | ||||
| const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); | ||||
|  | @ -62,7 +62,7 @@ watch(navWindow, async () => { | |||
| async function setProfile() { | ||||
| 	const { canceled, result: name } = await os.inputText({ | ||||
| 		title: i18n.ts._deck.profile, | ||||
| 		allowEmpty: false | ||||
| 		allowEmpty: false, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 	 | ||||
|  | @ -70,11 +70,13 @@ async function setProfile() { | |||
| 	unisonReload(); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.deck, | ||||
| 		icon: 'fas fa-columns', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.deck, | ||||
| 	icon: 'fas fa-columns', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -8,13 +8,12 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { defineExpose } from 'vue'; | ||||
| import FormInfo from '@/components/ui/info.vue'; | ||||
| import FormButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { signout } from '@/account'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| async function deleteAccount() { | ||||
| 	{ | ||||
|  | @ -27,12 +26,12 @@ async function deleteAccount() { | |||
| 
 | ||||
| 	const { canceled, result: password } = await os.inputText({ | ||||
| 		title: i18n.ts.password, | ||||
| 		type: 'password' | ||||
| 		type: 'password', | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 
 | ||||
| 	await os.apiWithDialog('i/delete-account', { | ||||
| 		password: password | ||||
| 		password: password, | ||||
| 	}); | ||||
| 
 | ||||
| 	await os.alert({ | ||||
|  | @ -42,11 +41,13 @@ async function deleteAccount() { | |||
| 	await signout(); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts._accountDelete.accountDelete, | ||||
| 		icon: 'fas fa-exclamation-triangle', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts._accountDelete.accountDelete, | ||||
| 	icon: 'fas fa-exclamation-triangle', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineExpose, ref } from 'vue'; | ||||
| import { computed, ref } from 'vue'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import FormLink from '@/components/form/link.vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
|  | @ -43,10 +43,10 @@ import MkKeyValue from '@/components/key-value.vue'; | |||
| import FormSplit from '@/components/form/split.vue'; | ||||
| import * as os from '@/os'; | ||||
| import bytes from '@/filters/bytes'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import MkChart from '@/components/chart.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const fetching = ref(true); | ||||
| const usage = ref<any>(null); | ||||
|  | @ -59,8 +59,8 @@ const meterStyle = computed(() => { | |||
| 		background: tinycolor({ | ||||
| 			h: 180 - (usage.value / capacity.value * 180), | ||||
| 			s: 0.7, | ||||
| 			l: 0.5 | ||||
| 		}) | ||||
| 			l: 0.5, | ||||
| 		}), | ||||
| 	}; | ||||
| }); | ||||
| 
 | ||||
|  | @ -74,7 +74,7 @@ os.api('drive').then(info => { | |||
| 
 | ||||
| if (defaultStore.state.uploadFolder) { | ||||
| 	os.api('drive/folders/show', { | ||||
| 		folderId: defaultStore.state.uploadFolder | ||||
| 		folderId: defaultStore.state.uploadFolder, | ||||
| 	}).then(response => { | ||||
| 		uploadFolder.value = response; | ||||
| 	}); | ||||
|  | @ -86,7 +86,7 @@ function chooseUploadFolder() { | |||
| 		os.success(); | ||||
| 		if (defaultStore.state.uploadFolder) { | ||||
| 			uploadFolder.value = await os.api('drive/folders/show', { | ||||
| 				folderId: defaultStore.state.uploadFolder | ||||
| 				folderId: defaultStore.state.uploadFolder, | ||||
| 			}); | ||||
| 		} else { | ||||
| 			uploadFolder.value = null; | ||||
|  | @ -94,12 +94,14 @@ function chooseUploadFolder() { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.drive, | ||||
| 		icon: 'fas fa-cloud', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.drive, | ||||
| 	icon: 'fas fa-cloud', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -40,27 +40,27 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { defineExpose, onMounted, ref, watch } from 'vue'; | ||||
| import { onMounted, ref, watch } from 'vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import FormInput from '@/components/form/input.vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { $i } from '@/account'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const emailAddress = ref($i!.email); | ||||
| 
 | ||||
| const onChangeReceiveAnnouncementEmail = (v) => { | ||||
| 	os.api('i/update', { | ||||
| 		receiveAnnouncementEmail: v | ||||
| 		receiveAnnouncementEmail: v, | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| const saveEmailAddress = () => { | ||||
| 	os.inputText({ | ||||
| 		title: i18n.ts.password, | ||||
| 		type: 'password' | ||||
| 		type: 'password', | ||||
| 	}).then(({ canceled, result: password }) => { | ||||
| 		if (canceled) return; | ||||
| 		os.apiWithDialog('i/update-email', { | ||||
|  | @ -86,7 +86,7 @@ const saveNotificationSettings = () => { | |||
| 			...[emailNotification_follow.value ? 'follow' : null], | ||||
| 			...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null], | ||||
| 			...[emailNotification_groupInvited.value ? 'groupInvited' : null], | ||||
| 		].filter(x => x != null) | ||||
| 		].filter(x => x != null), | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
|  | @ -100,11 +100,13 @@ onMounted(() => { | |||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.email, | ||||
| 		icon: 'fas fa-envelope', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.email, | ||||
| 	icon: 'fas fa-envelope', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -48,7 +48,8 @@ | |||
| 		<FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch> | ||||
| 		<FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch> | ||||
| 		<FormSwitch v-model="useSystemFont" class="_formBlock">{{ i18n.ts.useSystemFont }}</FormSwitch> | ||||
| 		<FormSwitch v-model="useOsNativeEmojis" class="_formBlock">{{ i18n.ts.useOsNativeEmojis }} | ||||
| 		<FormSwitch v-model="useOsNativeEmojis" class="_formBlock"> | ||||
| 			{{ i18n.ts.useOsNativeEmojis }} | ||||
| 			<div><Mfm :key="useOsNativeEmojis" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div> | ||||
| 		</FormSwitch> | ||||
| 		<FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch> | ||||
|  | @ -92,7 +93,7 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { computed, defineExpose, ref, watch } from 'vue'; | ||||
| import { computed, ref, watch } from 'vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import FormSelect from '@/components/form/select.vue'; | ||||
| import FormRadios from '@/components/form/radios.vue'; | ||||
|  | @ -104,8 +105,8 @@ import { langs } from '@/config'; | |||
| import { defaultStore } from '@/store'; | ||||
| import * as os from '@/os'; | ||||
| import { unisonReload } from '@/scripts/unison-reload'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const lang = ref(localStorage.getItem('lang')); | ||||
| const fontSize = ref(localStorage.getItem('fontSize')); | ||||
|  | @ -173,16 +174,18 @@ watch([ | |||
| 	aiChanMode, | ||||
| 	showGapBetweenNotesInTimeline, | ||||
| 	instanceTicker, | ||||
| 	overridedDeviceKind | ||||
| 	overridedDeviceKind, | ||||
| ], async () => { | ||||
| 	await reloadAsk(); | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.general, | ||||
| 		icon: 'fas fa-cogs', | ||||
| 		bg: 'var(--bg)' | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.general, | ||||
| 	icon: 'fas fa-cogs', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -38,15 +38,15 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { defineExpose, ref } from 'vue'; | ||||
| import { ref } from 'vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import FormGroup from '@/components/form/group.vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { selectFile } from '@/scripts/select-file'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const excludeMutingUsers = ref(false); | ||||
| const excludeInactiveUsers = ref(false); | ||||
|  | @ -116,12 +116,14 @@ const importBlocking = async (ev) => { | |||
| 	os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); | ||||
| }; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.ts.importAndExport, | ||||
| 		icon: 'fas fa-boxes', | ||||
| 		bg: 'var(--bg)', | ||||
| 	} | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.importAndExport, | ||||
| 	icon: 'fas fa-boxes', | ||||
| 	bg: 'var(--bg)', | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,46 +1,42 @@ | |||
| <template> | ||||
| <MkSpacer :content-max="900" :margin-min="20" :margin-max="32"> | ||||
| 	<div ref="el" class="vvcocwet" :class="{ wide: !narrow }"> | ||||
| 		<div class="header"> | ||||
| 			<div class="title"> | ||||
| 				<MkA v-if="narrow" to="/settings">{{ $ts.settings }}</MkA> | ||||
| 				<template v-else>{{ $ts.settings }}</template> | ||||
| 			</div> | ||||
| 			<div v-if="childInfo" class="subtitle">{{ childInfo.title }}</div> | ||||
| 		</div> | ||||
| 		<div class="body"> | ||||
| 			<div v-if="!narrow || initialPage == 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> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<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 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> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div v-if="!(narrow && initialPage == null)" class="main"> | ||||
| 				<div class="bkzroven"> | ||||
| 					<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/> | ||||
| 				<div v-if="!(narrow && initialPage == null)" class="main"> | ||||
| 					<div class="bkzroven"> | ||||
| 						<component :is="component" :key="initialPage" v-bind="pageProps"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| 	</MkSpacer> | ||||
| </mkstickycontainer> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'; | ||||
| import { computed, defineAsyncComponent, inject, nextTick, 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'; | ||||
| import { scroll } from '@/scripts/scroll'; | ||||
| import { signout } from '@/account'; | ||||
| import { signout , $i } from '@/account'; | ||||
| import { unisonReload } from '@/scripts/unison-reload'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { instance } from '@/instance'; | ||||
| import { $i } from '@/account'; | ||||
| import { MisskeyNavigator } from '@/scripts/navigate'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|   initialPage?: string | ||||
| }>(); | ||||
| const props = withDefaults(defineProps<{ | ||||
|   initialPage?: string; | ||||
| }>(), { | ||||
| }); | ||||
| 
 | ||||
| const indexInfo = { | ||||
| 	title: i18n.ts.settings, | ||||
|  | @ -52,7 +48,7 @@ const INFO = ref(indexInfo); | |||
| const el = ref<HTMLElement | null>(null); | ||||
| const childInfo = ref(null); | ||||
| 
 | ||||
| const nav = new MisskeyNavigator(); | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| const narrow = ref(false); | ||||
| const NARROW_THRESHOLD = 600; | ||||
|  | @ -189,7 +185,7 @@ const menuDef = computed(() => [{ | |||
| 			signout(); | ||||
| 		}, | ||||
| 		danger: true, | ||||
| 	},], | ||||
| 	}], | ||||
| }]); | ||||
| 
 | ||||
| const pageProps = ref({}); | ||||
|  | @ -242,7 +238,7 @@ watch(component, () => { | |||
| 
 | ||||
| watch(() => props.initialPage, () => { | ||||
| 	if (props.initialPage == null && !narrow.value) { | ||||
| 		nav.push('/settings/profile'); | ||||
| 		router.push('/settings/profile'); | ||||
| 	} else { | ||||
| 		if (props.initialPage == null) { | ||||
| 			INFO.value = indexInfo; | ||||
|  | @ -252,7 +248,7 @@ watch(() => props.initialPage, () => { | |||
| 
 | ||||
| watch(narrow, () => { | ||||
| 	if (props.initialPage == null && !narrow.value) { | ||||
| 		nav.push('/settings/profile'); | ||||
| 		router.push('/settings/profile'); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
|  | @ -261,7 +257,7 @@ onMounted(() => { | |||
| 
 | ||||
| 	narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; | ||||
| 	if (props.initialPage == null && !narrow.value) { | ||||
| 		nav.push('/settings/profile'); | ||||
| 		router.push('/settings/profile'); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
|  | @ -271,38 +267,23 @@ onUnmounted(() => { | |||
| 
 | ||||
| const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified)); | ||||
| 
 | ||||
| const pageChanged = (page) => { | ||||
| 	if (page == null) { | ||||
| provideMetadataReceiver((info) => { | ||||
| 	if (info == null) { | ||||
| 		childInfo.value = null; | ||||
| 	} else { | ||||
| 		childInfo.value = page[symbols.PAGE_INFO]; | ||||
| 		childInfo.value = info; | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: INFO, | ||||
| }); | ||||
| 
 | ||||
| const headerActions = $computed(() => []); | ||||
| 
 | ||||
| const headerTabs = $computed(() => []); | ||||
| 
 | ||||
| definePageMetadata(INFO); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .vvcocwet { | ||||
| 	> .header { | ||||
| 		display: flex; | ||||
| 		margin-bottom: 24px; | ||||
| 		font-size: 1.3em; | ||||
| 		font-weight: bold; | ||||
| 
 | ||||
| 		> .title { | ||||
| 			display: block; | ||||
| 			width: 34%; | ||||
| 		} | ||||
| 
 | ||||
| 		> .subtitle { | ||||
| 			flex: 1; | ||||
| 			min-width: 0; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .body { | ||||
| 		> .nav { | ||||
| 			.baaadecd { | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue