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", | 		"vite": "2.9.10", | ||||||
| 		"vue": "3.2.37", | 		"vue": "3.2.37", | ||||||
| 		"vue-prism-editor": "2.0.0-alpha.2", | 		"vue-prism-editor": "2.0.0-alpha.2", | ||||||
| 		"vue-router": "4.0.16", |  | ||||||
| 		"vuedraggable": "4.0.1", | 		"vuedraggable": "4.0.1", | ||||||
| 		"websocket": "1.0.34", | 		"websocket": "1.0.34", | ||||||
| 		"ws": "8.8.0" | 		"ws": "8.8.0" | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { del, get, set } from '@/scripts/idb-proxy'; |  | ||||||
| import { defineAsyncComponent, reactive } from 'vue'; | import { defineAsyncComponent, reactive } from 'vue'; | ||||||
| import * as misskey from 'misskey-js'; | 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 { apiUrl } from '@/config'; | ||||||
| import { waiting, api, popup, popupMenu, success, alert } from '@/os'; | import { waiting, api, popup, popupMenu, success, alert } from '@/os'; | ||||||
| import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; | import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; | ||||||
| import { showSuspendedDialog } from './scripts/show-suspended-dialog'; |  | ||||||
| import { i18n } from './i18n'; |  | ||||||
| 
 | 
 | ||||||
| // TODO: 他のタブと永続化されたstateを同期
 | // TODO: 他のタブと永続化されたstateを同期
 | ||||||
| 
 | 
 | ||||||
|  | @ -22,13 +22,7 @@ export async function signout() { | ||||||
| 	waiting(); | 	waiting(); | ||||||
| 	localStorage.removeItem('account'); | 	localStorage.removeItem('account'); | ||||||
| 
 | 
 | ||||||
| 	//#region Remove account
 | 	await removeAccount($i.id); | ||||||
| 	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
 |  | ||||||
| 
 | 
 | ||||||
| 	//#region Remove service worker registration
 | 	//#region Remove service worker registration
 | ||||||
| 	try { | 	try { | ||||||
|  | @ -55,7 +49,7 @@ export async function signout() { | ||||||
| 	} catch (err) {} | 	} catch (err) {} | ||||||
| 	//#endregion
 | 	//#endregion
 | ||||||
| 
 | 
 | ||||||
| 	document.cookie = `igi=; path=/`; | 	document.cookie = 'igi=; path=/'; | ||||||
| 
 | 
 | ||||||
| 	if (accounts.length > 0) login(accounts[0].token); | 	if (accounts.length > 0) login(accounts[0].token); | ||||||
| 	else unisonReload('/'); | 	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> { | function fetchAccount(token: string): Promise<Account> { | ||||||
| 	return new Promise((done, fail) => { | 	return new Promise((done, fail) => { | ||||||
| 		// Fetch user
 | 		// Fetch user
 | ||||||
| 		fetch(`${apiUrl}/i`, { | 		fetch(`${apiUrl}/i`, { | ||||||
| 			method: 'POST', | 			method: 'POST', | ||||||
| 			body: JSON.stringify({ | 			body: JSON.stringify({ | ||||||
| 				i: token | 				i: token, | ||||||
| 			}) | 			}), | ||||||
| 		}) | 		}) | ||||||
| 		.then(res => res.json()) | 		.then(res => res.json()) | ||||||
| 		.then(res => { | 		.then(res => { | ||||||
|  | @ -216,13 +218,13 @@ export async function openAccountMenu(opts: { | ||||||
| 			type: 'link', | 			type: 'link', | ||||||
| 			icon: 'fas fa-users', | 			icon: 'fas fa-users', | ||||||
| 			text: i18n.ts.manageAccounts, | 			text: i18n.ts.manageAccounts, | ||||||
| 			to: `/settings/accounts`, | 			to: '/settings/accounts', | ||||||
| 		}]], ev.currentTarget ?? ev.target, { | 		}]], ev.currentTarget ?? ev.target, { | ||||||
| 			align: 'left' | 			align: 'left', | ||||||
| 		}); | 		}); | ||||||
| 	} else { | 	} else { | ||||||
| 		popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, { | 		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. |   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. |   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 { | import { | ||||||
| 	Chart, | 	Chart, | ||||||
| 	ArcElement, | 	ArcElement, | ||||||
|  | @ -53,7 +53,7 @@ const props = defineProps({ | ||||||
| 	limit: { | 	limit: { | ||||||
| 		type: Number, | 		type: Number, | ||||||
| 		required: false, | 		required: false, | ||||||
| 		default: 90 | 		default: 90, | ||||||
| 	}, | 	}, | ||||||
| 	span: { | 	span: { | ||||||
| 		type: String as PropType<'hour' | 'day'>, | 		type: String as PropType<'hour' | 'day'>, | ||||||
|  | @ -62,22 +62,22 @@ const props = defineProps({ | ||||||
| 	detailed: { | 	detailed: { | ||||||
| 		type: Boolean, | 		type: Boolean, | ||||||
| 		required: false, | 		required: false, | ||||||
| 		default: false | 		default: false, | ||||||
| 	}, | 	}, | ||||||
| 	stacked: { | 	stacked: { | ||||||
| 		type: Boolean, | 		type: Boolean, | ||||||
| 		required: false, | 		required: false, | ||||||
| 		default: false | 		default: false, | ||||||
| 	}, | 	}, | ||||||
| 	bar: { | 	bar: { | ||||||
| 		type: Boolean, | 		type: Boolean, | ||||||
| 		required: false, | 		required: false, | ||||||
| 		default: false | 		default: false, | ||||||
| 	}, | 	}, | ||||||
| 	aspectRatio: { | 	aspectRatio: { | ||||||
| 		type: Number, | 		type: Number, | ||||||
| 		required: false, | 		required: false, | ||||||
| 		default: null | 		default: null, | ||||||
| 	}, | 	}, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | @ -156,7 +156,7 @@ const getDate = (ago: number) => { | ||||||
| const format = (arr) => { | const format = (arr) => { | ||||||
| 	return arr.map((v, i) => ({ | 	return arr.map((v, i) => ({ | ||||||
| 		x: getDate(i).getTime(), | 		x: getDate(i).getTime(), | ||||||
| 		y: v | 		y: v, | ||||||
| 	})); | 	})); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -343,7 +343,7 @@ const render = () => { | ||||||
| 							min: 'original', | 							min: 'original', | ||||||
| 							max: 'original', | 							max: 'original', | ||||||
| 						}, | 						}, | ||||||
| 					} | 					}, | ||||||
| 				} : undefined, | 				} : undefined, | ||||||
| 				//gradient, | 				//gradient, | ||||||
| 			}, | 			}, | ||||||
|  | @ -367,8 +367,8 @@ const render = () => { | ||||||
| 					ctx.stroke(); | 					ctx.stroke(); | ||||||
| 					ctx.restore(); | 					ctx.restore(); | ||||||
| 				} | 				} | ||||||
| 			} | 			}, | ||||||
| 		}] | 		}], | ||||||
| 	}); | 	}); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -433,18 +433,18 @@ const fetchApRequestChart = async (): Promise<typeof chartData> => { | ||||||
| 			name: 'In', | 			name: 'In', | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			color: '#008FFB', | 			color: '#008FFB', | ||||||
| 			data: format(raw.inboxReceived) | 			data: format(raw.inboxReceived), | ||||||
| 		}, { | 		}, { | ||||||
| 			name: 'Out (succ)', | 			name: 'Out (succ)', | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			color: '#00E396', | 			color: '#00E396', | ||||||
| 			data: format(raw.deliverSucceeded) | 			data: format(raw.deliverSucceeded), | ||||||
| 		}, { | 		}, { | ||||||
| 			name: 'Out (fail)', | 			name: 'Out (fail)', | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			color: '#FEB019', | 			color: '#FEB019', | ||||||
| 			data: format(raw.deliverFailed) | 			data: format(raw.deliverFailed), | ||||||
| 		}] | 		}], | ||||||
| 	}; | 	}; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -456,7 +456,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { | ||||||
| 			type: 'line', | 			type: 'line', | ||||||
| 			data: format(type === 'combined' | 			data: format(type === 'combined' | ||||||
| 				? 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)) | ||||||
| 				: sum(raw[type].inc, negate(raw[type].dec)) | 				: sum(raw[type].inc, negate(raw[type].dec)), | ||||||
| 			), | 			), | ||||||
| 			color: '#888888', | 			color: '#888888', | ||||||
| 		}, { | 		}, { | ||||||
|  | @ -464,7 +464,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			data: format(type === 'combined' | 			data: format(type === 'combined' | ||||||
| 				? sum(raw.local.diffs.renote, raw.remote.diffs.renote) | 				? sum(raw.local.diffs.renote, raw.remote.diffs.renote) | ||||||
| 				: raw[type].diffs.renote | 				: raw[type].diffs.renote, | ||||||
| 			), | 			), | ||||||
| 			color: colors.green, | 			color: colors.green, | ||||||
| 		}, { | 		}, { | ||||||
|  | @ -472,7 +472,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			data: format(type === 'combined' | 			data: format(type === 'combined' | ||||||
| 				? sum(raw.local.diffs.reply, raw.remote.diffs.reply) | 				? sum(raw.local.diffs.reply, raw.remote.diffs.reply) | ||||||
| 				: raw[type].diffs.reply | 				: raw[type].diffs.reply, | ||||||
| 			), | 			), | ||||||
| 			color: colors.yellow, | 			color: colors.yellow, | ||||||
| 		}, { | 		}, { | ||||||
|  | @ -480,7 +480,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			data: format(type === 'combined' | 			data: format(type === 'combined' | ||||||
| 				? sum(raw.local.diffs.normal, raw.remote.diffs.normal) | 				? sum(raw.local.diffs.normal, raw.remote.diffs.normal) | ||||||
| 				: raw[type].diffs.normal | 				: raw[type].diffs.normal, | ||||||
| 			), | 			), | ||||||
| 			color: colors.blue, | 			color: colors.blue, | ||||||
| 		}, { | 		}, { | ||||||
|  | @ -488,7 +488,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => { | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			data: format(type === 'combined' | 			data: format(type === 'combined' | ||||||
| 				? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile) | 				? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile) | ||||||
| 				: raw[type].diffs.withFile | 				: raw[type].diffs.withFile, | ||||||
| 			), | 			), | ||||||
| 			color: colors.purple, | 			color: colors.purple, | ||||||
| 		}], | 		}], | ||||||
|  | @ -522,21 +522,21 @@ const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => { | ||||||
| 			type: 'line', | 			type: 'line', | ||||||
| 			data: format(total | 			data: format(total | ||||||
| 				? sum(raw.local.total, raw.remote.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', | 			name: 'Local', | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			data: format(total | 			data: format(total | ||||||
| 				? raw.local.total | 				? raw.local.total | ||||||
| 				: sum(raw.local.inc, negate(raw.local.dec)) | 				: sum(raw.local.inc, negate(raw.local.dec)), | ||||||
| 			), | 			), | ||||||
| 		}, { | 		}, { | ||||||
| 			name: 'Remote', | 			name: 'Remote', | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			data: format(total | 			data: format(total | ||||||
| 				? raw.remote.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, | 					raw.local.incSize, | ||||||
| 					negate(raw.local.decSize), | 					negate(raw.local.decSize), | ||||||
| 					raw.remote.incSize, | 					raw.remote.incSize, | ||||||
| 					negate(raw.remote.decSize) | 					negate(raw.remote.decSize), | ||||||
| 				) | 				), | ||||||
| 			), | 			), | ||||||
| 		}, { | 		}, { | ||||||
| 			name: 'Local +', | 			name: 'Local +', | ||||||
|  | @ -642,8 +642,8 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => { | ||||||
| 					raw.local.incCount, | 					raw.local.incCount, | ||||||
| 					negate(raw.local.decCount), | 					negate(raw.local.decCount), | ||||||
| 					raw.remote.incCount, | 					raw.remote.incCount, | ||||||
| 					negate(raw.remote.decCount) | 					negate(raw.remote.decCount), | ||||||
| 				) | 				), | ||||||
| 			), | 			), | ||||||
| 		}, { | 		}, { | ||||||
| 			name: 'Local +', | 			name: 'Local +', | ||||||
|  | @ -672,18 +672,18 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => { | ||||||
| 			name: 'In', | 			name: 'In', | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			color: '#008FFB', | 			color: '#008FFB', | ||||||
| 			data: format(raw.requests.received) | 			data: format(raw.requests.received), | ||||||
| 		}, { | 		}, { | ||||||
| 			name: 'Out (succ)', | 			name: 'Out (succ)', | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			color: '#00E396', | 			color: '#00E396', | ||||||
| 			data: format(raw.requests.succeeded) | 			data: format(raw.requests.succeeded), | ||||||
| 		}, { | 		}, { | ||||||
| 			name: 'Out (fail)', | 			name: 'Out (fail)', | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			color: '#FEB019', | 			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', | 			color: '#008FFB', | ||||||
| 			data: format(total | 			data: format(total | ||||||
| 				? raw.users.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', | 			color: '#008FFB', | ||||||
| 			data: format(total | 			data: format(total | ||||||
| 				? raw.notes.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', | 			color: '#008FFB', | ||||||
| 			data: format(total | 			data: format(total | ||||||
| 				? raw.following.total | 				? raw.following.total | ||||||
| 				: sum(raw.following.inc, negate(raw.following.dec)) | 				: sum(raw.following.inc, negate(raw.following.dec)), | ||||||
| 			) | 			), | ||||||
| 		}, { | 		}, { | ||||||
| 			name: 'Followers', | 			name: 'Followers', | ||||||
| 			type: 'area', | 			type: 'area', | ||||||
| 			color: '#00E396', | 			color: '#00E396', | ||||||
| 			data: format(total | 			data: format(total | ||||||
| 				? raw.followers.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', | 			color: '#008FFB', | ||||||
| 			data: format(total | 			data: format(total | ||||||
| 				? raw.drive.totalUsage | 				? 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', | 			color: '#008FFB', | ||||||
| 			data: format(total | 			data: format(total | ||||||
| 				? raw.drive.totalFiles | 				? 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 { | .zdjebgpv { | ||||||
| 	position: relative; | 	position: relative; | ||||||
| 	display: flex; | 	display: flex; | ||||||
| 	background: #e1e1e1; | 	background: var(--panel); | ||||||
| 	border-radius: 8px; | 	border-radius: 8px; | ||||||
| 	overflow: clip; | 	overflow: clip; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,13 +9,13 @@ | ||||||
| 			<i v-else class="fas fa-angle-down icon"></i> | 			<i v-else class="fas fa-angle-down icon"></i> | ||||||
| 		</span> | 		</span> | ||||||
| 	</div> | 	</div> | ||||||
| 	<keep-alive> | 	<KeepAlive> | ||||||
| 		<div v-if="openedAtLeastOnce" v-show="opened" class="body"> | 		<div v-if="openedAtLeastOnce" v-show="opened" class="body"> | ||||||
| 			<MkSpacer :margin-min="14" :margin-max="22"> | 			<MkSpacer :margin-min="14" :margin-max="22"> | ||||||
| 				<slot></slot> | 				<slot></slot> | ||||||
| 			</MkSpacer> | 			</MkSpacer> | ||||||
| 		</div> | 		</div> | ||||||
| 	</keep-alive> | 	</KeepAlive> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,13 +5,13 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | import { inject } from 'vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import copyToClipboard from '@/scripts/copy-to-clipboard'; | import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||||
| import { router } from '@/router'; |  | ||||||
| import { url } from '@/config'; | import { url } from '@/config'; | ||||||
| import { popout as popout_ } from '@/scripts/popout'; | import { popout as popout_ } from '@/scripts/popout'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { MisskeyNavigator } from '@/scripts/navigate'; | import { useRouter } from '@/router'; | ||||||
| 
 | 
 | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	to: string; | 	to: string; | ||||||
|  | @ -22,15 +22,16 @@ const props = withDefaults(defineProps<{ | ||||||
| 	behavior: null, | 	behavior: null, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const mkNav = new MisskeyNavigator(); | const router = useRouter(); | ||||||
| 
 | 
 | ||||||
| const active = $computed(() => { | const active = $computed(() => { | ||||||
| 	if (props.activeClass == null) return false; | 	if (props.activeClass == null) return false; | ||||||
| 	const resolved = router.resolve(props.to); | 	const resolved = router.resolve(props.to); | ||||||
| 	if (resolved.path === router.currentRoute.value.path) return true; | 	if (resolved == null) return false; | ||||||
| 	if (resolved.name == 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; | 	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) { | function onContextmenu(ev) { | ||||||
|  | @ -44,31 +45,25 @@ function onContextmenu(ev) { | ||||||
| 		text: i18n.ts.openInWindow, | 		text: i18n.ts.openInWindow, | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			os.pageWindow(props.to); | 			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', | 		icon: 'fas fa-expand-alt', | ||||||
| 		text: i18n.ts.showInPage, | 		text: i18n.ts.showInPage, | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			router.push(props.to); | 			router.push(props.to); | ||||||
| 		} | 		}, | ||||||
| 	}, null, { | 	}, null, { | ||||||
| 		icon: 'fas fa-external-link-alt', | 		icon: 'fas fa-external-link-alt', | ||||||
| 		text: i18n.ts.openInNewTab, | 		text: i18n.ts.openInNewTab, | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			window.open(props.to, '_blank'); | 			window.open(props.to, '_blank'); | ||||||
| 		} | 		}, | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-link', | 		icon: 'fas fa-link', | ||||||
| 		text: i18n.ts.copyLink, | 		text: i18n.ts.copyLink, | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			copyToClipboard(`${url}${props.to}`); | 			copyToClipboard(`${url}${props.to}`); | ||||||
| 		} | 		}, | ||||||
| 	}], ev); | 	}], ev); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -98,6 +93,6 @@ function nav() { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	mkNav.push(props.to); | 	router.push(props.to); | ||||||
| } | } | ||||||
| </script> | </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 MkTime from './global/time.vue'; | ||||||
| import MkUrl from './global/url.vue'; | import MkUrl from './global/url.vue'; | ||||||
| import I18n from './global/i18n'; | import I18n from './global/i18n'; | ||||||
|  | import RouterView from './global/router-view.vue'; | ||||||
| import MkLoading from './global/loading.vue'; | import MkLoading from './global/loading.vue'; | ||||||
| import MkError from './global/error.vue'; | import MkError from './global/error.vue'; | ||||||
| import MkAd from './global/ad.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 MkSpacer from './global/spacer.vue'; | ||||||
| import MkStickyContainer from './global/sticky-container.vue'; | import MkStickyContainer from './global/sticky-container.vue'; | ||||||
| 
 | 
 | ||||||
| export default function(app: App) { | export default function(app: App) { | ||||||
| 	app.component('I18n', I18n); | 	app.component('I18n', I18n); | ||||||
|  | 	app.component('RouterView', RouterView); | ||||||
| 	app.component('Mfm', Mfm); | 	app.component('Mfm', Mfm); | ||||||
| 	app.component('MkA', MkA); | 	app.component('MkA', MkA); | ||||||
| 	app.component('MkAcct', MkAcct); | 	app.component('MkAcct', MkAcct); | ||||||
|  | @ -31,7 +33,7 @@ export default function(app: App) { | ||||||
| 	app.component('MkLoading', MkLoading); | 	app.component('MkLoading', MkLoading); | ||||||
| 	app.component('MkError', MkError); | 	app.component('MkError', MkError); | ||||||
| 	app.component('MkAd', MkAd); | 	app.component('MkAd', MkAd); | ||||||
| 	app.component('MkHeader', MkHeader); | 	app.component('MkPageHeader', MkPageHeader); | ||||||
| 	app.component('MkSpacer', MkSpacer); | 	app.component('MkSpacer', MkSpacer); | ||||||
| 	app.component('MkStickyContainer', MkStickyContainer); | 	app.component('MkStickyContainer', MkStickyContainer); | ||||||
| } | } | ||||||
|  | @ -39,6 +41,7 @@ export default function(app: App) { | ||||||
| declare module '@vue/runtime-core' { | declare module '@vue/runtime-core' { | ||||||
| 	export interface GlobalComponents { | 	export interface GlobalComponents { | ||||||
| 		I18n: typeof I18n; | 		I18n: typeof I18n; | ||||||
|  | 		RouterView: typeof RouterView; | ||||||
| 		Mfm: typeof Mfm; | 		Mfm: typeof Mfm; | ||||||
| 		MkA: typeof MkA; | 		MkA: typeof MkA; | ||||||
| 		MkAcct: typeof MkAcct; | 		MkAcct: typeof MkAcct; | ||||||
|  | @ -51,7 +54,7 @@ declare module '@vue/runtime-core' { | ||||||
| 		MkLoading: typeof MkLoading; | 		MkLoading: typeof MkLoading; | ||||||
| 		MkError: typeof MkError; | 		MkError: typeof MkError; | ||||||
| 		MkAd: typeof MkAd; | 		MkAd: typeof MkAd; | ||||||
| 		MkHeader: typeof MkHeader; | 		MkPageHeader: typeof MkPageHeader; | ||||||
| 		MkSpacer: typeof MkSpacer; | 		MkSpacer: typeof MkSpacer; | ||||||
| 		MkStickyContainer: typeof MkStickyContainer; | 		MkStickyContainer: typeof MkStickyContainer; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1,163 +1,118 @@ | ||||||
| <template> | <template> | ||||||
| <MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')"> | <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"> | 		<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> | 			<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-else style="display: inline-block; width: 20px"></span> | ||||||
| 			<span v-if="pageInfo" class="title"> | 			<span v-if="pageMetadata?.value" class="title"> | ||||||
| 				<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i> | 				<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i> | ||||||
| 				<span>{{ pageInfo.title }}</span> | 				<span>{{ pageMetadata?.value.title }}</span> | ||||||
| 			</span> | 			</span> | ||||||
| 			<button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button> | 			<button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="body"> | 		<div class="body"> | ||||||
| 			<MkStickyContainer> | 			<MkStickyContainer> | ||||||
| 				<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template> | 				<template #header><MkPageHeader v-if="pageMetadata?.value && !pageMetadata?.value.hideHeader" :info="pageMetadata?.value"/></template> | ||||||
| 				<keep-alive> | 				<RouterView :router="router"/> | ||||||
| 					<component :is="component" v-bind="props" :ref="changePage"/> |  | ||||||
| 				</keep-alive> |  | ||||||
| 			</MkStickyContainer> | 			</MkStickyContainer> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </MkModal> | </MkModal> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent } from 'vue'; | import { ComputedRef, provide } from 'vue'; | ||||||
| import MkModal from '@/components/ui/modal.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 copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||||
| import { resolve } from '@/router'; |  | ||||||
| import { url } from '@/config'; | import { url } from '@/config'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import * as os from '@/os'; | 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({ | const props = defineProps<{ | ||||||
| 	components: { | 	initialPath: string; | ||||||
| 		MkModal, | }>(); | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	inject: { | defineEmits<{ | ||||||
| 		sideViewHook: { | 	(ev: 'closed'): void; | ||||||
| 			default: null, | 	(ev: 'click'): void; | ||||||
| 		}, | }>(); | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	provide() { | const router = new Router(routes, props.initialPath); | ||||||
| 		return { |  | ||||||
| 			navHook: (path) => { |  | ||||||
| 				this.navigate(path); |  | ||||||
| 			}, |  | ||||||
| 			shouldHeaderThin: true, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	props: { | router.addListener('push', ctx => { | ||||||
| 		initialPath: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: true, |  | ||||||
| 		}, |  | ||||||
| 		initialComponent: { |  | ||||||
| 			type: Object, |  | ||||||
| 			required: true, |  | ||||||
| 		}, |  | ||||||
| 		initialProps: { |  | ||||||
| 			type: Object, |  | ||||||
| 			required: false, |  | ||||||
| 			default: () => {}, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	 | 	 | ||||||
| 	emits: ['closed'], | }); | ||||||
| 
 | 
 | ||||||
| 	data() { | let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); | ||||||
| 		return { | let rootEl = $ref(); | ||||||
| 			width: 860, | let modal = $ref<InstanceType<typeof MkModal>>(); | ||||||
| 			height: 660, | let path = $ref(props.initialPath); | ||||||
| 			pageInfo: null, | let width = $ref(860); | ||||||
| 			path: this.initialPath, | let height = $ref(660); | ||||||
| 			component: this.initialComponent, | const history = []; | ||||||
| 			props: this.initialProps, |  | ||||||
| 			history: [], |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	computed: { | provide('router', router); | ||||||
| 		url(): string { | provideMetadataReceiver((info) => { | ||||||
| 			return url + this.path; | 	pageMetadata = info; | ||||||
| 		}, | }); | ||||||
|  | provide('shouldOmitHeaderTitle', true); | ||||||
|  | provide('shouldHeaderThin', true); | ||||||
| 
 | 
 | ||||||
| 		contextmenu() { | const pageUrl = $computed(() => url + path); | ||||||
|  | const contextmenu = $computed(() => { | ||||||
| 	return [{ | 	return [{ | ||||||
| 		type: 'label', | 		type: 'label', | ||||||
| 				text: this.path, | 		text: path, | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-expand-alt', | 		icon: 'fas fa-expand-alt', | ||||||
| 				text: this.$ts.showInPage, | 		text: i18n.ts.showInPage, | ||||||
| 				action: this.expand, | 		action: 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', | 		icon: 'fas fa-external-link-alt', | ||||||
| 				text: this.$ts.popout, | 		text: i18n.ts.popout, | ||||||
| 				action: this.popout, | 		action: popout, | ||||||
| 	}, null, { | 	}, null, { | ||||||
| 		icon: 'fas fa-external-link-alt', | 		icon: 'fas fa-external-link-alt', | ||||||
| 				text: this.$ts.openInNewTab, | 		text: i18n.ts.openInNewTab, | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 					window.open(this.url, '_blank'); | 			window.open(pageUrl, '_blank'); | ||||||
| 					this.$refs.window.close(); | 			modal.close(); | ||||||
| 		}, | 		}, | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-link', | 		icon: 'fas fa-link', | ||||||
| 				text: this.$ts.copyLink, | 		text: i18n.ts.copyLink, | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 					copyToClipboard(this.url); | 			copyToClipboard(pageUrl); | ||||||
| 		}, | 		}, | ||||||
| 	}]; | 	}]; | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	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); |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | 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> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <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 { | function onContextmenu(ev: MouseEvent): void { | ||||||
| 	const isLink = (el: HTMLElement) => { | 	const isLink = (el: HTMLElement) => { | ||||||
|  |  | ||||||
|  | @ -1,186 +1,135 @@ | ||||||
| <template> | <template> | ||||||
| <XWindow ref="window" | <XWindow | ||||||
|  | 	ref="windowEl" | ||||||
| 	:initial-width="500" | 	:initial-width="500" | ||||||
| 	:initial-height="500" | 	:initial-height="500" | ||||||
| 	:can-resize="true" | 	:can-resize="true" | ||||||
| 	:close-button="true" | 	:close-button="true" | ||||||
|  | 	:buttons-left="buttonsLeft" | ||||||
|  | 	:buttons-right="buttonsRight" | ||||||
| 	:contextmenu="contextmenu" | 	:contextmenu="contextmenu" | ||||||
| 	@closed="$emit('closed')" | 	@closed="$emit('closed')" | ||||||
| > | > | ||||||
| 	<template #header> | 	<template #header> | ||||||
| 		<template v-if="pageInfo"> | 		<template v-if="pageMetadata?.value"> | ||||||
| 			<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i> | 			<i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i> | ||||||
| 			<span>{{ pageInfo.title }}</span> | 			<span>{{ pageMetadata.value.title }}</span> | ||||||
| 		</template> | 		</template> | ||||||
| 	</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 }"> | 	<div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }"> | ||||||
| 		<MkStickyContainer> | 		<RouterView :router="router"/> | ||||||
| 			<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template> |  | ||||||
| 			<component :is="component" v-bind="props" :ref="changePage"/> |  | ||||||
| 		</MkStickyContainer> |  | ||||||
| 	</div> | 	</div> | ||||||
| </XWindow> | </XWindow> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent } from 'vue'; | import { ComputedRef, inject, provide } from 'vue'; | ||||||
|  | import RouterView from './global/router-view.vue'; | ||||||
| import XWindow from '@/components/ui/window.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 copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||||
| import { resolve } from '@/router'; |  | ||||||
| import { url } from '@/config'; | import { url } from '@/config'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import * as os from '@/os'; | 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({ | const props = defineProps<{ | ||||||
| 	components: { | 	initialPath: string; | ||||||
| 		XWindow, | }>(); | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	inject: { | defineEmits<{ | ||||||
| 		sideViewHook: { | 	(ev: 'closed'): void; | ||||||
| 			default: null | }>(); | ||||||
|  | 
 | ||||||
|  | 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, | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	provide() { | 	return buttons; | ||||||
| 		return { | }); | ||||||
| 			navHook: (path) => { | const buttonsRight = $computed(() => { | ||||||
| 				this.navigate(path); | 	const buttons = [{ | ||||||
| 			}, |  | ||||||
| 			shouldHeaderThin: true, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	props: { |  | ||||||
| 		initialPath: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: true, |  | ||||||
| 		}, |  | ||||||
| 		initialComponent: { |  | ||||||
| 			type: Object, |  | ||||||
| 			required: true, |  | ||||||
| 		}, |  | ||||||
| 		initialProps: { |  | ||||||
| 			type: Object, |  | ||||||
| 			required: false, |  | ||||||
| 			default: () => {}, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	emits: ['closed'], |  | ||||||
| 
 |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			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', | 		icon: 'fas fa-expand-alt', | ||||||
| 				text: this.$ts.showInPage, | 		title: i18n.ts.showInPage, | ||||||
| 				action: this.expand | 		onClick: 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: { | 	return buttons; | ||||||
| 		changePage(page) { | }); | ||||||
| 			if (page == null) return; |  | ||||||
| 			if (page[symbols.PAGE_INFO]) { |  | ||||||
| 				this.pageInfo = page[symbols.PAGE_INFO]; |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 
 | 
 | ||||||
| 		navigate(path, record = true) { | router.addListener('push', ctx => { | ||||||
| 			if (record) this.history.push(this.path); | 	history.push(router.getCurrentPath()); | ||||||
| 			this.path = path; | }); | ||||||
| 			const { component, props } = resolve(path); |  | ||||||
| 			this.component = component; |  | ||||||
| 			this.props = props; |  | ||||||
| 		}, |  | ||||||
| 
 | 
 | ||||||
| 		menu(ev) { | provide('router', router); | ||||||
| 			os.popupMenu([{ | 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', | 	icon: 'fas fa-external-link-alt', | ||||||
| 				text: this.$ts.openInNewTab, | 	text: i18n.ts.popout, | ||||||
|  | 	action: popout, | ||||||
|  | }, { | ||||||
|  | 	icon: 'fas fa-external-link-alt', | ||||||
|  | 	text: i18n.ts.openInNewTab, | ||||||
| 	action: () => { | 	action: () => { | ||||||
| 					window.open(this.url, '_blank'); | 		window.open(url + router.getCurrentPath(), '_blank'); | ||||||
| 					this.$refs.window.close(); | 		windowEl.close(); | ||||||
| 				} | 	}, | ||||||
| 			}, { | }, { | ||||||
| 	icon: 'fas fa-link', | 	icon: 'fas fa-link', | ||||||
| 				text: this.$ts.copyLink, | 	text: i18n.ts.copyLink, | ||||||
| 	action: () => { | 	action: () => { | ||||||
| 					copyToClipboard(this.url); | 		copyToClipboard(url + router.getCurrentPath()); | ||||||
| 				} |  | ||||||
| 			}], ev.currentTarget ?? ev.target); |  | ||||||
| 	}, | 	}, | ||||||
|  | }])); | ||||||
| 
 | 
 | ||||||
| 		back() { | function menu(ev) { | ||||||
| 			this.navigate(this.history.pop(), false); | 	os.popupMenu(contextmenu, ev.currentTarget ?? ev.target); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		close() { | function back() { | ||||||
| 			this.$refs.window.close(); | 	history.pop(); | ||||||
| 		}, | 	router.change(history[history.length - 1]); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 		expand() { | function close() { | ||||||
| 			this.$router.push(this.path); | 	windowEl.close(); | ||||||
| 			this.$refs.window.close(); | } | ||||||
| 		}, |  | ||||||
| 
 | 
 | ||||||
| 		popout() { | function expand() { | ||||||
| 			popout(this.path, this.$el); | 	mainRouter.push(router.getCurrentPath()); | ||||||
| 			this.$refs.window.close(); | 	windowEl.close(); | ||||||
| 		}, | } | ||||||
| 	}, | 
 | ||||||
|  | function popout() { | ||||||
|  | 	_popout(router.getCurrentPath(), windowEl.$el); | ||||||
|  | 	windowEl.close(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | defineExpose({ | ||||||
|  | 	close, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,14 +4,14 @@ | ||||||
| 		<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown"> | 		<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown"> | ||||||
| 			<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu"> | 			<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu"> | ||||||
| 				<span class="left"> | 				<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> | ||||||
| 				<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown"> | 				<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown"> | ||||||
| 					<slot name="header"></slot> | 					<slot name="header"></slot> | ||||||
| 				</span> | 				</span> | ||||||
| 				<span class="right"> | 				<span class="right"> | ||||||
| 					<slot name="headerRight"></slot> | 					<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" @click="close()"><i class="fas fa-times"></i></button> | 					<button v-if="closeButton" class="button _button" @click="close()"><i class="fas fa-times"></i></button> | ||||||
| 				</span> | 				</span> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div v-if="padding" class="body"> | 			<div v-if="padding" class="body"> | ||||||
|  | @ -63,24 +63,24 @@ function dragClear(fn) { | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	provide: { | 	provide: { | ||||||
| 		inWindow: true | 		inWindow: true, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
| 		padding: { | 		padding: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false, | ||||||
| 		}, | 		}, | ||||||
| 		initialWidth: { | 		initialWidth: { | ||||||
| 			type: Number, | 			type: Number, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: 400 | 			default: 400, | ||||||
| 		}, | 		}, | ||||||
| 		initialHeight: { | 		initialHeight: { | ||||||
| 			type: Number, | 			type: Number, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: null | 			default: null, | ||||||
| 		}, | 		}, | ||||||
| 		canResize: { | 		canResize: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
|  | @ -105,7 +105,17 @@ export default defineComponent({ | ||||||
| 		contextmenu: { | 		contextmenu: { | ||||||
| 			type: Array, | 			type: Array, | ||||||
| 			required: false, | 			required: false, | ||||||
| 		} | 		}, | ||||||
|  | 		buttonsLeft: { | ||||||
|  | 			type: Array, | ||||||
|  | 			required: false, | ||||||
|  | 			default: [], | ||||||
|  | 		}, | ||||||
|  | 		buttonsRight: { | ||||||
|  | 			type: Array, | ||||||
|  | 			required: false, | ||||||
|  | 			default: [], | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	emits: ['closed'], | 	emits: ['closed'], | ||||||
|  | @ -162,7 +172,10 @@ export default defineComponent({ | ||||||
| 			this.top(); | 			this.top(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onHeaderMousedown(evt) { | 		onHeaderMousedown(evt: MouseEvent) { | ||||||
|  | 			// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視 | ||||||
|  | 			if (evt.button === 2) return; | ||||||
|  | 
 | ||||||
| 			const main = this.$el as any; | 			const main = this.$el as any; | ||||||
| 
 | 
 | ||||||
| 			if (!contains(main, document.activeElement)) main.focus(); | 			if (!contains(main, document.activeElement)) main.focus(); | ||||||
|  | @ -360,8 +373,8 @@ export default defineComponent({ | ||||||
| 			if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し | 			if (position.top + windowHeight > browserHeight) main.style.top = browserHeight - windowHeight + 'px'; // 下はみ出し | ||||||
| 			if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し | 			if (position.left + windowWidth > browserWidth) main.style.left = browserWidth - windowWidth + 'px'; // 右はみ出し | ||||||
| 			if (position.top < 0) main.style.top = 0; // 上はみ出し | 			if (position.top < 0) main.style.top = 0; // 上はみ出し | ||||||
| 		} | 		}, | ||||||
| 	} | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -404,17 +417,25 @@ export default defineComponent({ | ||||||
| 			border-bottom: solid 1px var(--divider); | 			border-bottom: solid 1px var(--divider); | ||||||
| 
 | 
 | ||||||
| 			> .left, > .right { | 			> .left, > .right { | ||||||
| 				> ::v-deep(button) { | 				> .button { | ||||||
| 					height: var(--height); | 					height: var(--height); | ||||||
| 					width: var(--height); | 					width: var(--height); | ||||||
| 
 | 
 | ||||||
| 					&:hover { | 					&:hover { | ||||||
| 						color: var(--fgHighlighted); | 						color: var(--fgHighlighted); | ||||||
| 					} | 					} | ||||||
|  | 
 | ||||||
|  | 					&.highlighted { | ||||||
|  | 						color: var(--accent); | ||||||
|  | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			> .left { | 			> .left { | ||||||
|  | 				margin-right: 16px; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			> .right { | ||||||
| 				min-width: 16px; | 				min-width: 16px; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -21,7 +21,6 @@ import widgets from '@/widgets'; | ||||||
| import directives from '@/directives'; | import directives from '@/directives'; | ||||||
| import components from '@/components'; | import components from '@/components'; | ||||||
| import { version, ui, lang, host } from '@/config'; | import { version, ui, lang, host } from '@/config'; | ||||||
| import { router } from '@/router'; |  | ||||||
| import { applyTheme } from '@/scripts/theme'; | import { applyTheme } from '@/scripts/theme'; | ||||||
| import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; | import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | @ -172,9 +171,8 @@ const app = createApp( | ||||||
| 	window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) : | 	window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) : | ||||||
| 	!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : | 	!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : | ||||||
| 	ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : | 	ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : | ||||||
| 	ui === 'desktop'                  ? defineAsyncComponent(() => import('@/ui/desktop.vue')) : |  | ||||||
| 	ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : | 	ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : | ||||||
| 	defineAsyncComponent(() => import('@/ui/universal.vue')) | 	defineAsyncComponent(() => import('@/ui/universal.vue')), | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| if (_DEV_) { | if (_DEV_) { | ||||||
|  | @ -189,14 +187,10 @@ app.config.globalProperties = { | ||||||
| 	$ts: i18n.ts, | 	$ts: i18n.ts, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| app.use(router); |  | ||||||
| 
 |  | ||||||
| widgets(app); | widgets(app); | ||||||
| directives(app); | directives(app); | ||||||
| components(app); | components(app); | ||||||
| 
 | 
 | ||||||
| await router.isReady(); |  | ||||||
| 
 |  | ||||||
| const splash = document.getElementById('splash'); | const splash = document.getElementById('splash'); | ||||||
| // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
 | // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
 | ||||||
| if (splash) splash.addEventListener('transitionend', () => { | if (splash) splash.addEventListener('transitionend', () => { | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { computed, ref, reactive } from 'vue'; | import { computed, ref, reactive } from 'vue'; | ||||||
|  | import { $i } from './account'; | ||||||
|  | import { mainRouter } from '@/router'; | ||||||
| import { search } from '@/scripts/search'; | import { search } from '@/scripts/search'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { ui } from '@/config'; | import { ui } from '@/config'; | ||||||
| import { $i } from './account'; |  | ||||||
| import { unisonReload } from '@/scripts/unison-reload'; | import { unisonReload } from '@/scripts/unison-reload'; | ||||||
| import { router } from './router'; |  | ||||||
| 
 | 
 | ||||||
| export const menuDef = reactive({ | export const menuDef = reactive({ | ||||||
| 	notifications: { | 	notifications: { | ||||||
|  | @ -60,16 +60,16 @@ export const menuDef = reactive({ | ||||||
| 		title: 'lists', | 		title: 'lists', | ||||||
| 		icon: 'fas fa-list-ul', | 		icon: 'fas fa-list-ul', | ||||||
| 		show: computed(() => $i != null), | 		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) => { | 		action: (ev) => { | ||||||
| 			const items = ref([{ | 			const items = ref([{ | ||||||
| 				type: 'pending' | 				type: 'pending', | ||||||
| 			}]); | 			}]); | ||||||
| 			os.api('users/lists/list').then(lists => { | 			os.api('users/lists/list').then(lists => { | ||||||
| 				const _items = [...lists.map(list => ({ | 				const _items = [...lists.map(list => ({ | ||||||
| 					type: 'link', | 					type: 'link', | ||||||
| 					text: list.name, | 					text: list.name, | ||||||
| 					to: `/timeline/list/${list.id}` | 					to: `/timeline/list/${list.id}`, | ||||||
| 				})), null, { | 				})), null, { | ||||||
| 					type: 'link', | 					type: 'link', | ||||||
| 					to: '/my/lists', | 					to: '/my/lists', | ||||||
|  | @ -91,16 +91,16 @@ export const menuDef = reactive({ | ||||||
| 		title: 'antennas', | 		title: 'antennas', | ||||||
| 		icon: 'fas fa-satellite', | 		icon: 'fas fa-satellite', | ||||||
| 		show: computed(() => $i != null), | 		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) => { | 		action: (ev) => { | ||||||
| 			const items = ref([{ | 			const items = ref([{ | ||||||
| 				type: 'pending' | 				type: 'pending', | ||||||
| 			}]); | 			}]); | ||||||
| 			os.api('antennas/list').then(antennas => { | 			os.api('antennas/list').then(antennas => { | ||||||
| 				const _items = [...antennas.map(antenna => ({ | 				const _items = [...antennas.map(antenna => ({ | ||||||
| 					type: 'link', | 					type: 'link', | ||||||
| 					text: antenna.name, | 					text: antenna.name, | ||||||
| 					to: `/timeline/antenna/${antenna.id}` | 					to: `/timeline/antenna/${antenna.id}`, | ||||||
| 				})), null, { | 				})), null, { | ||||||
| 					type: 'link', | 					type: 'link', | ||||||
| 					to: '/my/antennas', | 					to: '/my/antennas', | ||||||
|  | @ -178,29 +178,22 @@ export const menuDef = reactive({ | ||||||
| 				action: () => { | 				action: () => { | ||||||
| 					localStorage.setItem('ui', 'default'); | 					localStorage.setItem('ui', 'default'); | ||||||
| 					unisonReload(); | 					unisonReload(); | ||||||
| 				} | 				}, | ||||||
| 			}, { | 			}, { | ||||||
| 				text: i18n.ts.deck, | 				text: i18n.ts.deck, | ||||||
| 				active: ui === 'deck', | 				active: ui === 'deck', | ||||||
| 				action: () => { | 				action: () => { | ||||||
| 					localStorage.setItem('ui', 'deck'); | 					localStorage.setItem('ui', 'deck'); | ||||||
| 					unisonReload(); | 					unisonReload(); | ||||||
| 				} | 				}, | ||||||
| 			}, { | 			}, { | ||||||
| 				text: i18n.ts.classic, | 				text: i18n.ts.classic, | ||||||
| 				active: ui === 'classic', | 				active: ui === 'classic', | ||||||
| 				action: () => { | 				action: () => { | ||||||
| 					localStorage.setItem('ui', 'classic'); | 					localStorage.setItem('ui', 'classic'); | ||||||
| 					unisonReload(); | 					unisonReload(); | ||||||
| 				} | 				}, | ||||||
| 			}, /*{ | 			}], ev.currentTarget ?? ev.target); | ||||||
| 				text: i18n.ts.desktop + ' (β)', |  | ||||||
| 				active: ui === 'desktop', |  | ||||||
| 				action: () => { |  | ||||||
| 					localStorage.setItem('ui', 'desktop'); |  | ||||||
| 					unisonReload(); |  | ||||||
| 				} |  | ||||||
| 			}*/], 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 MkPostFormDialog from '@/components/post-form-dialog.vue'; | ||||||
| import MkWaitingDialog from '@/components/waiting-dialog.vue'; | import MkWaitingDialog from '@/components/waiting-dialog.vue'; | ||||||
| import { MenuItem } from '@/types/menu'; | import { MenuItem } from '@/types/menu'; | ||||||
| import { resolve } from '@/router'; |  | ||||||
| import { $i } from '@/account'; | import { $i } from '@/account'; | ||||||
| 
 | 
 | ||||||
| export const pendingApiRequestsCount = ref(0); | 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) { | export function pageWindow(path: string) { | ||||||
| 	const { component, props } = resolve(path); |  | ||||||
| 	popup(defineAsyncComponent(() => import('@/components/page-window.vue')), { | 	popup(defineAsyncComponent(() => import('@/components/page-window.vue')), { | ||||||
| 		initialPath: path, | 		initialPath: path, | ||||||
| 		initialComponent: markRaw(component), |  | ||||||
| 		initialProps: props, |  | ||||||
| 	}, {}, 'closed'); | 	}, {}, 'closed'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function modalPageWindow(path: string) { | export function modalPageWindow(path: string) { | ||||||
| 	const { component, props } = resolve(path); |  | ||||||
| 	popup(defineAsyncComponent(() => import('@/components/modal-page-window.vue')), { | 	popup(defineAsyncComponent(() => import('@/components/modal-page-window.vue')), { | ||||||
| 		initialPath: path, | 		initialPath: path, | ||||||
| 		initialComponent: markRaw(component), |  | ||||||
| 		initialProps: props, |  | ||||||
| 	}, {}, 'closed'); | 	}, {}, 'closed'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -21,11 +21,11 @@ | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
| import * as misskey from 'misskey-js'; | import * as misskey from 'misskey-js'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { version } from '@/config'; | import { version } from '@/config'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { unisonReload } from '@/scripts/unison-reload'; | import { unisonReload } from '@/scripts/unison-reload'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	error?: Error; | 	error?: Error; | ||||||
|  | @ -52,11 +52,13 @@ function reload() { | ||||||
| 	unisonReload(); | 	unisonReload(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.error, | 	title: i18n.ts.error, | ||||||
| 	icon: 'fas fa-exclamation-triangle', | 	icon: 'fas fa-exclamation-triangle', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <div style="overflow: clip;"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<div style="overflow: clip;"> | ||||||
| 		<MkSpacer :content-max="600" :margin-min="20"> | 		<MkSpacer :content-max="600" :margin-min="20"> | ||||||
| 			<div class="_formRoot znqjceqz"> | 			<div class="_formRoot znqjceqz"> | ||||||
| 				<div id="debug"></div> | 				<div id="debug"></div> | ||||||
|  | @ -56,7 +58,8 @@ | ||||||
| 				</FormSection> | 				</FormSection> | ||||||
| 			</div> | 			</div> | ||||||
| 		</MkSpacer> | 		</MkSpacer> | ||||||
| </div> | 	</div> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -67,10 +70,10 @@ import FormSection from '@/components/form/section.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkLink from '@/components/link.vue'; | import MkLink from '@/components/link.vue'; | ||||||
| import { physics } from '@/scripts/physics'; | import { physics } from '@/scripts/physics'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const patrons = [ | const patrons = [ | ||||||
| 	'まっちゃとーにゅ', | 	'まっちゃとーにゅ', | ||||||
|  | @ -194,12 +197,14 @@ onBeforeUnmount(() => { | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.aboutMisskey, | 	title: i18n.ts.aboutMisskey, | ||||||
| 	icon: null, | 	icon: null, | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20"> | <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="_formRoot"> | ||||||
| 			<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"> | 			<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }"> | ||||||
| 				<div class="content"> | 				<div class="content"> | ||||||
|  | @ -64,15 +66,16 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 			</FormSection> | 			</FormSection> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
| <MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20"> | 	<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20"> | ||||||
| 		<MkInstanceStats :chart-limit="500" :detailed="true"/> | 		<MkInstanceStats :chart-limit="500" :detailed="true"/> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { ref, computed } from 'vue'; | import { ref, computed } from 'vue'; | ||||||
| import { version, instanceName } from '@/config'; | import { version, instanceName , host } from '@/config'; | ||||||
| import FormLink from '@/components/form/link.vue'; | import FormLink from '@/components/form/link.vue'; | ||||||
| import FormSection from '@/components/form/section.vue'; | import FormSection from '@/components/form/section.vue'; | ||||||
| import FormSuspense from '@/components/form/suspense.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 MkInstanceStats from '@/components/instance-stats.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import number from '@/filters/number'; | import number from '@/filters/number'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { host } from '@/config'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let stats = $ref(null); | let stats = $ref(null); | ||||||
| let tab = $ref('overview'); | let tab = $ref('overview'); | ||||||
|  | @ -93,23 +95,24 @@ const initStats = () => os.api('stats', { | ||||||
| 	stats = res; | 	stats = res; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | 
 | ||||||
| 		title: i18n.ts.instanceInfo, | const headerTabs = $computed(() => [{ | ||||||
| 		icon: 'fas fa-info-circle', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		tabs: [{ |  | ||||||
| 	active: tab === 'overview', | 	active: tab === 'overview', | ||||||
| 	title: i18n.ts.overview, | 	title: i18n.ts.overview, | ||||||
| 	onClick: () => { tab = 'overview'; }, | 	onClick: () => { tab = 'overview'; }, | ||||||
| 		}, { | }, { | ||||||
| 	active: tab === 'charts', | 	active: tab === 'charts', | ||||||
| 	title: i18n.ts.charts, | 	title: i18n.ts.charts, | ||||||
| 	icon: 'fas fa-chart-bar', | 	icon: 'fas fa-chart-bar', | ||||||
| 	onClick: () => { tab = 'charts'; }, | 	onClick: () => { tab = 'charts'; }, | ||||||
| 		},], | }]); | ||||||
| 	})), | 
 | ||||||
| }); | definePageMetadata(computed(() => ({ | ||||||
|  | 	title: i18n.ts.instanceInfo, | ||||||
|  | 	icon: 'fas fa-info-circle', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
|  | }))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="500" :margin-min="16" :margin-max="32"> | <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 v-if="file" class="cxqhhsmd _formRoot"> | ||||||
| 			<div class="_formBlock"> | 			<div class="_formBlock"> | ||||||
| 				<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | 				<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||||
|  | @ -24,7 +26,8 @@ | ||||||
| 				</details> | 				</details> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -35,7 +38,7 @@ import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; | ||||||
| import bytes from '@/filters/bytes'; | import bytes from '@/filters/bytes'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import * as symbols from '@/symbols'; | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let file: any = $ref(null); | let file: any = $ref(null); | ||||||
| let info: any = $ref(null); | let info: any = $ref(null); | ||||||
|  | @ -74,13 +77,15 @@ async function toggleIsSensitive(v) { | ||||||
| 	isSensitive = v; | 	isSensitive = v; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => ({ | ||||||
| 	title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file, | 	title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file, | ||||||
| 	icon: 'fas fa-file', | 	icon: 'fas fa-file', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	})), | }))); | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <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,5 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <div class="lcixvhis"> | <MkStickyContainer> | ||||||
|  | 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="900"> | ||||||
|  | 		<div class="lcixvhis"> | ||||||
| 			<div class="_section reports"> | 			<div class="_section reports"> | ||||||
| 				<div class="_content"> | 				<div class="_content"> | ||||||
| 					<div class="inputs" style="display: flex;"> | 					<div class="inputs" style="display: flex;"> | ||||||
|  | @ -38,19 +41,22 @@ | ||||||
| 					</MkPagination> | 					</MkPagination> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| </div> | 		</div> | ||||||
|  | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| 
 | 
 | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkSelect from '@/components/form/select.vue'; | import MkSelect from '@/components/form/select.vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import XAbuseReport from '@/components/abuse-report.vue'; | import XAbuseReport from '@/components/abuse-report.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let reports = $ref<InstanceType<typeof MkPagination>>(); | let reports = $ref<InstanceType<typeof MkPagination>>(); | ||||||
| 
 | 
 | ||||||
|  | @ -74,12 +80,14 @@ function resolved(reportId) { | ||||||
| 	reports.removeItem(item => item.id === reportId); | 	reports.removeItem(item => item.id === reportId); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.abuseReports, | 	title: i18n.ts.abuseReports, | ||||||
| 	icon: 'fas fa-exclamation-circle', | 	icon: 'fas fa-exclamation-circle', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="900"> | <MkStickyContainer> | ||||||
|  | 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="900"> | ||||||
| 		<div class="uqshojas"> | 		<div class="uqshojas"> | ||||||
| 			<div v-for="ad in ads" class="_panel _formRoot ad"> | 			<div v-for="ad in ads" class="_panel _formRoot ad"> | ||||||
| 				<MkAd v-if="ad.url" :specify="ad"/> | 				<MkAd v-if="ad.url" :specify="ad"/> | ||||||
|  | @ -40,19 +42,21 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkTextarea from '@/components/form/textarea.vue'; | import MkTextarea from '@/components/form/textarea.vue'; | ||||||
| import FormRadios from '@/components/form/radios.vue'; | import FormRadios from '@/components/form/radios.vue'; | ||||||
| import FormSplit from '@/components/form/split.vue'; | import FormSplit from '@/components/form/split.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let ads: any[] = $ref([]); | let ads: any[] = $ref([]); | ||||||
| 
 | 
 | ||||||
|  | @ -81,7 +85,7 @@ function remove(ad) { | ||||||
| 		if (canceled) return; | 		if (canceled) return; | ||||||
| 		ads = ads.filter(x => x !== ad); | 		ads = ads.filter(x => x !== ad); | ||||||
| 		os.apiWithDialog('admin/ad/delete', { | 		os.apiWithDialog('admin/ad/delete', { | ||||||
| 			id: ad.id | 			id: ad.id, | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
|  | @ -90,28 +94,29 @@ function save(ad) { | ||||||
| 	if (ad.id == null) { | 	if (ad.id == null) { | ||||||
| 		os.apiWithDialog('admin/ad/create', { | 		os.apiWithDialog('admin/ad/create', { | ||||||
| 			...ad, | 			...ad, | ||||||
| 			expiresAt: new Date(ad.expiresAt).getTime() | 			expiresAt: new Date(ad.expiresAt).getTime(), | ||||||
| 		}); | 		}); | ||||||
| 	} else { | 	} else { | ||||||
| 		os.apiWithDialog('admin/ad/update', { | 		os.apiWithDialog('admin/ad/update', { | ||||||
| 			...ad, | 			...ad, | ||||||
| 			expiresAt: new Date(ad.expiresAt).getTime() | 			expiresAt: new Date(ad.expiresAt).getTime(), | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
| 	[symbols.PAGE_INFO]: { |  | ||||||
| 		title: i18n.ts.ads, |  | ||||||
| 		icon: 'fas fa-audio-description', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	icon: 'fas fa-plus', | 	icon: 'fas fa-plus', | ||||||
| 	text: i18n.ts.add, | 	text: i18n.ts.add, | ||||||
| 	handler: add, | 	handler: add, | ||||||
| 		}], | }]); | ||||||
| 	} | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
|  | 	title: i18n.ts.ads, | ||||||
|  | 	icon: 'fas fa-audio-description', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <div class="ztgjmzrw"> | <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"> | 			<section v-for="announcement in announcements" class="_card _gap announcements"> | ||||||
| 				<div class="_content announcement"> | 				<div class="_content announcement"> | ||||||
| 					<MkInput v-model="announcement.title"> | 					<MkInput v-model="announcement.title"> | ||||||
|  | @ -18,17 +21,20 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 			</section> | 			</section> | ||||||
| </div> | 		</div> | ||||||
|  | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkTextarea from '@/components/form/textarea.vue'; | import MkTextarea from '@/components/form/textarea.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let announcements: any[] = $ref([]); | let announcements: any[] = $ref([]); | ||||||
| 
 | 
 | ||||||
|  | @ -41,7 +47,7 @@ function add() { | ||||||
| 		id: null, | 		id: null, | ||||||
| 		title: '', | 		title: '', | ||||||
| 		text: '', | 		text: '', | ||||||
| 		imageUrl: null | 		imageUrl: null, | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -61,41 +67,42 @@ function save(announcement) { | ||||||
| 		os.api('admin/announcements/create', announcement).then(() => { | 		os.api('admin/announcements/create', announcement).then(() => { | ||||||
| 			os.alert({ | 			os.alert({ | ||||||
| 				type: 'success', | 				type: 'success', | ||||||
| 				text: i18n.ts.saved | 				text: i18n.ts.saved, | ||||||
| 			}); | 			}); | ||||||
| 		}).catch(err => { | 		}).catch(err => { | ||||||
| 			os.alert({ | 			os.alert({ | ||||||
| 				type: 'error', | 				type: 'error', | ||||||
| 				text: err | 				text: err, | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
| 	} else { | 	} else { | ||||||
| 		os.api('admin/announcements/update', announcement).then(() => { | 		os.api('admin/announcements/update', announcement).then(() => { | ||||||
| 			os.alert({ | 			os.alert({ | ||||||
| 				type: 'success', | 				type: 'success', | ||||||
| 				text: i18n.ts.saved | 				text: i18n.ts.saved, | ||||||
| 			}); | 			}); | ||||||
| 		}).catch(err => { | 		}).catch(err => { | ||||||
| 			os.alert({ | 			os.alert({ | ||||||
| 				type: 'error', | 				type: 'error', | ||||||
| 				text: err | 				text: err, | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
| 	[symbols.PAGE_INFO]: { |  | ||||||
| 		title: i18n.ts.announcements, |  | ||||||
| 		icon: 'fas fa-broadcast-tower', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	icon: 'fas fa-plus', | 	icon: 'fas fa-plus', | ||||||
| 	text: i18n.ts.add, | 	text: i18n.ts.add, | ||||||
| 	handler: add, | 	handler: add, | ||||||
| 		}], | }]); | ||||||
| 	} | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
|  | 	title: i18n.ts.announcements, | ||||||
|  | 	icon: 'fas fa-broadcast-tower', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -51,7 +51,6 @@ import FormButton from '@/components/ui/button.vue'; | ||||||
| import FormSuspense from '@/components/form/suspense.vue'; | import FormSuspense from '@/components/form/suspense.vue'; | ||||||
| import FormSlot from '@/components/form/slot.vue'; | import FormSlot from '@/components/form/slot.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { fetchInstance } from '@/instance'; | import { fetchInstance } from '@/instance'; | ||||||
| 
 | 
 | ||||||
| const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue')); | const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue')); | ||||||
|  |  | ||||||
|  | @ -1,12 +1,13 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> | 	<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"> | 	<FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory"> | ||||||
| 		<MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;"> | 		<MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;"> | ||||||
| 			<template #key>{{ table[0] }}</template> | 			<template #key>{{ table[0] }}</template> | ||||||
| 			<template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template> | 			<template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template> | ||||||
| 		</MkKeyValue> | 		</MkKeyValue> | ||||||
| 	</FormSuspense> | 	</FormSuspense> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -14,18 +15,20 @@ import { } from 'vue'; | ||||||
| import FormSuspense from '@/components/form/suspense.vue'; | import FormSuspense from '@/components/form/suspense.vue'; | ||||||
| import MkKeyValue from '@/components/key-value.vue'; | import MkKeyValue from '@/components/key-value.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import bytes from '@/filters/bytes'; | import bytes from '@/filters/bytes'; | ||||||
| import number from '@/filters/number'; | import number from '@/filters/number'; | ||||||
| import { i18n } from '@/i18n'; | 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)); | const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.database, | 	title: i18n.ts.database, | ||||||
| 	icon: 'fas fa-database', | 	icon: 'fas fa-database', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | <MkStickyContainer> | ||||||
|  | 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||||
| 		<FormSuspense :p="init"> | 		<FormSuspense :p="init"> | ||||||
| 			<div class="_formRoot"> | 			<div class="_formRoot"> | ||||||
| 				<FormSwitch v-model="enableEmail" class="_formBlock"> | 				<FormSwitch v-model="enableEmail" class="_formBlock"> | ||||||
|  | @ -39,11 +41,13 @@ | ||||||
| 				</template> | 				</template> | ||||||
| 			</div> | 			</div> | ||||||
| 		</FormSuspense> | 		</FormSuspense> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import FormSwitch from '@/components/form/switch.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import FormInput from '@/components/form/input.vue'; | import FormInput from '@/components/form/input.vue'; | ||||||
| import FormInfo from '@/components/ui/info.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 FormSplit from '@/components/form/split.vue'; | ||||||
| import FormSection from '@/components/form/section.vue'; | import FormSection from '@/components/form/section.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { fetchInstance, instance } from '@/instance'; | import { fetchInstance, instance } from '@/instance'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let enableEmail: boolean = $ref(false); | let enableEmail: boolean = $ref(false); | ||||||
| let email: any = $ref(null); | let email: any = $ref(null); | ||||||
|  | @ -78,13 +82,13 @@ async function testEmail() { | ||||||
| 	const { canceled, result: destination } = await os.inputText({ | 	const { canceled, result: destination } = await os.inputText({ | ||||||
| 		title: i18n.ts.destination, | 		title: i18n.ts.destination, | ||||||
| 		type: 'email', | 		type: 'email', | ||||||
| 		placeholder: instance.maintainerEmail | 		placeholder: instance.maintainerEmail, | ||||||
| 	}); | 	}); | ||||||
| 	if (canceled) return; | 	if (canceled) return; | ||||||
| 	os.apiWithDialog('admin/send-email', { | 	os.apiWithDialog('admin/send-email', { | ||||||
| 		to: destination, | 		to: destination, | ||||||
| 		subject: 'Test email', | 		subject: 'Test email', | ||||||
| 		text: 'Yo' | 		text: 'Yo', | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -102,21 +106,22 @@ function save() { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
| 	[symbols.PAGE_INFO]: { |  | ||||||
| 		title: i18n.ts.emailServer, |  | ||||||
| 		icon: 'fas fa-envelope', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	text: i18n.ts.testEmail, | 	text: i18n.ts.testEmail, | ||||||
| 	handler: testEmail, | 	handler: testEmail, | ||||||
| 		}, { | }, { | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	icon: 'fas fa-check', | 	icon: 'fas fa-check', | ||||||
| 	text: i18n.ts.save, | 	text: i18n.ts.save, | ||||||
| 	handler: save, | 	handler: save, | ||||||
| 		}], | }]); | ||||||
| 	} | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
|  | 	title: i18n.ts.emailServer, | ||||||
|  | 	icon: 'fas fa-envelope', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="900"> | <div> | ||||||
|  | 	<MkStickyContainer> | ||||||
|  | 		<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="900"> | ||||||
| 			<div class="ogwlenmc"> | 			<div class="ogwlenmc"> | ||||||
| 				<div v-if="tab === 'local'" class="local"> | 				<div v-if="tab === 'local'" class="local"> | ||||||
| 					<MkInput v-model="query" :debounce="true" type="search"> | 					<MkInput v-model="query" :debounce="true" type="search"> | ||||||
|  | @ -19,7 +22,7 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 					<MkPagination ref="emojisPaginationComponent" :pagination="pagination"> | 					<MkPagination ref="emojisPaginationComponent" :pagination="pagination"> | ||||||
| 						<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | 						<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | ||||||
| 				<template v-slot="{items}"> | 						<template #default="{items}"> | ||||||
| 							<div class="ldhfsamy"> | 							<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)"> | 								<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"/> | 									<img :src="emoji.url" class="img" :alt="emoji.name"/> | ||||||
|  | @ -45,7 +48,7 @@ | ||||||
| 					</FormSplit> | 					</FormSplit> | ||||||
| 					<MkPagination :pagination="remotePagination"> | 					<MkPagination :pagination="remotePagination"> | ||||||
| 						<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | 						<template #empty><span>{{ $ts.noCustomEmojis }}</span></template> | ||||||
| 				<template v-slot="{items}"> | 						<template #default="{items}"> | ||||||
| 							<div class="ldhfsamy"> | 							<div class="ldhfsamy"> | ||||||
| 								<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)"> | 								<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"/> | 									<img :src="emoji.url" class="img" :alt="emoji.name"/> | ||||||
|  | @ -59,11 +62,14 @@ | ||||||
| 					</MkPagination> | 					</MkPagination> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| </MkSpacer> | 		</MkSpacer> | ||||||
|  | 	</MkStickyContainer> | ||||||
|  | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue'; | import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.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 FormSplit from '@/components/form/split.vue'; | ||||||
| import { selectFile, selectFiles } from '@/scripts/select-file'; | import { selectFile, selectFiles } from '@/scripts/select-file'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>(); | const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>(); | ||||||
| 
 | 
 | ||||||
|  | @ -131,13 +137,13 @@ const add = async (ev: MouseEvent) => { | ||||||
| 
 | 
 | ||||||
| const edit = (emoji) => { | const edit = (emoji) => { | ||||||
| 	os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { | 	os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { | ||||||
| 		emoji: emoji | 		emoji: emoji, | ||||||
| 	}, { | 	}, { | ||||||
| 		done: result => { | 		done: result => { | ||||||
| 			if (result.updated) { | 			if (result.updated) { | ||||||
| 				emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ | 				emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ | ||||||
| 					...oldEmoji, | 					...oldEmoji, | ||||||
| 					...result.updated | 					...result.updated, | ||||||
| 				})); | 				})); | ||||||
| 			} else if (result.deleted) { | 			} else if (result.deleted) { | ||||||
| 				emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id); | 				emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id); | ||||||
|  | @ -159,7 +165,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => { | ||||||
| 	}, { | 	}, { | ||||||
| 		text: i18n.ts.import, | 		text: i18n.ts.import, | ||||||
| 		icon: 'fas fa-plus', | 		icon: 'fas fa-plus', | ||||||
| 		action: () => { im(emoji); } | 		action: () => { im(emoji); }, | ||||||
| 	}], ev.currentTarget ?? ev.target); | 	}], ev.currentTarget ?? ev.target); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -181,7 +187,7 @@ const menu = (ev: MouseEvent) => { | ||||||
| 					text: err.message, | 					text: err.message, | ||||||
| 				}); | 				}); | ||||||
| 			}); | 			}); | ||||||
| 		} | 		}, | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-upload', | 		icon: 'fas fa-upload', | ||||||
| 		text: i18n.ts.import, | 		text: i18n.ts.import, | ||||||
|  | @ -201,7 +207,7 @@ const menu = (ev: MouseEvent) => { | ||||||
| 					text: err.message, | 					text: err.message, | ||||||
| 				}); | 				}); | ||||||
| 			}); | 			}); | ||||||
| 		} | 		}, | ||||||
| 	}], ev.currentTarget ?? ev.target); | 	}], ev.currentTarget ?? ev.target); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -265,31 +271,31 @@ const delBulk = async () => { | ||||||
| 	emojisPaginationComponent.value.reload(); | 	emojisPaginationComponent.value.reload(); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
| 	[symbols.PAGE_INFO]: computed(() => ({ |  | ||||||
| 		title: i18n.ts.customEmojis, |  | ||||||
| 		icon: 'fas fa-laugh', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	icon: 'fas fa-plus', | 	icon: 'fas fa-plus', | ||||||
| 	text: i18n.ts.addEmoji, | 	text: i18n.ts.addEmoji, | ||||||
| 	handler: add, | 	handler: add, | ||||||
| 		}, { | }, { | ||||||
| 	icon: 'fas fa-ellipsis-h', | 	icon: 'fas fa-ellipsis-h', | ||||||
| 	handler: menu, | 	handler: menu, | ||||||
| 		}], | }]); | ||||||
| 		tabs: [{ | 
 | ||||||
|  | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab.value === 'local', | 	active: tab.value === 'local', | ||||||
| 	title: i18n.ts.local, | 	title: i18n.ts.local, | ||||||
| 	onClick: () => { tab.value = 'local'; }, | 	onClick: () => { tab.value = 'local'; }, | ||||||
| 		}, { | }, { | ||||||
| 	active: tab.value === 'remote', | 	active: tab.value === 'remote', | ||||||
| 	title: i18n.ts.remote, | 	title: i18n.ts.remote, | ||||||
| 	onClick: () => { tab.value = 'remote'; }, | 	onClick: () => { tab.value = 'remote'; }, | ||||||
| 		},] | }]); | ||||||
| 	})), | 
 | ||||||
| }); | definePageMetadata(computed(() => ({ | ||||||
|  | 	title: i18n.ts.customEmojis, | ||||||
|  | 	icon: 'fas fa-laugh', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
|  | }))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,9 @@ | ||||||
| <template> | <template> | ||||||
| <div class="xrmjdkdw"> | <div> | ||||||
|  | 	<MkStickyContainer> | ||||||
|  | 		<template #header><XHeader :actions="headerActions"/></template> | ||||||
|  | 		<MkSpacer :content-max="900"> | ||||||
|  | 			<div class="xrmjdkdw"> | ||||||
| 				<div> | 				<div> | ||||||
| 					<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> | 					<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> | ||||||
| 						<MkSelect v-model="origin" style="margin: 0; flex: 1;"> | 						<MkSelect v-model="origin" style="margin: 0; flex: 1;"> | ||||||
|  | @ -18,7 +22,7 @@ | ||||||
| 						</MkInput> | 						</MkInput> | ||||||
| 					</div> | 					</div> | ||||||
| 					<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }"> | 					<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)"> | 						<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"/> | 							<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||||
| 							<div v-if="viewMode === 'list'" class="body"> | 							<div v-if="viewMode === 'list'" class="body"> | ||||||
| 								<div> | 								<div> | ||||||
|  | @ -39,12 +43,16 @@ | ||||||
| 						</button> | 						</button> | ||||||
| 					</MkPagination> | 					</MkPagination> | ||||||
| 				</div> | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</MkSpacer> | ||||||
|  | 	</MkStickyContainer> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed, defineAsyncComponent } from 'vue'; | import { computed, defineAsyncComponent } from 'vue'; | ||||||
| import * as Acct from 'misskey-js/built/acct'; | import * as Acct from 'misskey-js/built/acct'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkSelect from '@/components/form/select.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 MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; | ||||||
| import bytes from '@/filters/bytes'; | import bytes from '@/filters/bytes'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let origin = $ref('local'); | let origin = $ref('local'); | ||||||
| let type = $ref(null); | let type = $ref(null); | ||||||
|  | @ -82,7 +90,7 @@ function clear() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function show(file) { | function show(file) { | ||||||
| 	os.pageWindow(`/admin-file/${file.id}`); | 	os.pageWindow(`/admin/file/${file.id}`); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function find() { | async function find() { | ||||||
|  | @ -104,22 +112,23 @@ async function find() { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
| 	[symbols.PAGE_INFO]: computed(() => ({ |  | ||||||
| 		title: i18n.ts.files, |  | ||||||
| 		icon: 'fas fa-cloud', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	text: i18n.ts.lookup, | 	text: i18n.ts.lookup, | ||||||
| 	icon: 'fas fa-search', | 	icon: 'fas fa-search', | ||||||
| 	handler: find, | 	handler: find, | ||||||
| 		}, { | }, { | ||||||
| 	text: i18n.ts.clearCachedFiles, | 	text: i18n.ts.clearCachedFiles, | ||||||
| 	icon: 'fas fa-trash-alt', | 	icon: 'fas fa-trash-alt', | ||||||
| 	handler: clear, | 	handler: clear, | ||||||
| 		}], | }]); | ||||||
| 	})), | 
 | ||||||
| }); | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => ({ | ||||||
|  | 	title: i18n.ts.files, | ||||||
|  | 	icon: 'fas fa-cloud', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
|  | }))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,8 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> | <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> | ||||||
| 	<div v-if="!narrow || initialPage == null" class="nav">	 | 	<div v-if="!narrow || initialPage == null" class="nav">	 | ||||||
| 		<MkHeader :info="header"></MkHeader> |  | ||||||
| 	 |  | ||||||
| 		<MkSpacer :content-max="700" :margin-min="16"> | 		<MkSpacer :content-max="700" :margin-min="16"> | ||||||
| 			<div class="lxpfedzu"> | 			<div class="lxpfedzu"> | ||||||
| 				<div class="banner"> | 				<div class="banner"> | ||||||
|  | @ -17,29 +15,26 @@ | ||||||
| 		</MkSpacer> | 		</MkSpacer> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div v-if="!(narrow && initialPage == null)" class="main"> | 	<div v-if="!(narrow && initialPage == null)" class="main"> | ||||||
| 		<MkStickyContainer> | 		<component :is="component" :key="initialPage" v-bind="pageProps"/> | ||||||
| 			<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template> |  | ||||||
| 			<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/> |  | ||||||
| 		</MkStickyContainer> |  | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <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 { i18n } from '@/i18n'; | ||||||
| import MkSuperMenu from '@/components/ui/super-menu.vue'; | import MkSuperMenu from '@/components/ui/super-menu.vue'; | ||||||
| import MkInfo from '@/components/ui/info.vue'; | import MkInfo from '@/components/ui/info.vue'; | ||||||
| import { scroll } from '@/scripts/scroll'; | import { scroll } from '@/scripts/scroll'; | ||||||
| import { instance } from '@/instance'; | import { instance } from '@/instance'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { lookupUser } from '@/scripts/lookup-user'; | 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 isEmpty = (x: string | null) => x == null || x === ''; | ||||||
| 
 | 
 | ||||||
| const nav = new MisskeyNavigator(); | const router = useRouter(); | ||||||
| 
 | 
 | ||||||
| const indexInfo = { | const indexInfo = { | ||||||
| 	title: i18n.ts.controlPanel, | 	title: i18n.ts.controlPanel, | ||||||
|  | @ -224,7 +219,7 @@ watch(component, () => { | ||||||
| 
 | 
 | ||||||
| watch(() => props.initialPage, () => { | watch(() => props.initialPage, () => { | ||||||
| 	if (props.initialPage == null && !narrow) { | 	if (props.initialPage == null && !narrow) { | ||||||
| 		nav.push('/admin/overview'); | 		router.push('/admin/overview'); | ||||||
| 	} else { | 	} else { | ||||||
| 		if (props.initialPage == null) { | 		if (props.initialPage == null) { | ||||||
| 			INFO = indexInfo; | 			INFO = indexInfo; | ||||||
|  | @ -234,7 +229,7 @@ watch(() => props.initialPage, () => { | ||||||
| 
 | 
 | ||||||
| watch(narrow, () => { | watch(narrow, () => { | ||||||
| 	if (props.initialPage == null && !narrow) { | 	if (props.initialPage == null && !narrow) { | ||||||
| 		nav.push('/admin/overview'); | 		router.push('/admin/overview'); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | @ -243,7 +238,7 @@ onMounted(() => { | ||||||
| 
 | 
 | ||||||
| 	narrow = el.offsetWidth < NARROW_THRESHOLD; | 	narrow = el.offsetWidth < NARROW_THRESHOLD; | ||||||
| 	if (props.initialPage == null && !narrow) { | 	if (props.initialPage == null && !narrow) { | ||||||
| 		nav.push('/admin/overview'); | 		router.push('/admin/overview'); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | @ -251,19 +246,19 @@ onUnmounted(() => { | ||||||
| 	ro.disconnect(); | 	ro.disconnect(); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const pageChanged = (page) => { | provideMetadataReceiver((info) => { | ||||||
| 	if (page == null) { | 	if (info == null) { | ||||||
| 		childInfo = null; | 		childInfo = null; | ||||||
| 	} else { | 	} else { | ||||||
| 		childInfo = page[symbols.PAGE_INFO]; | 		childInfo = info; | ||||||
| 	} | 	} | ||||||
| }; | }); | ||||||
| 
 | 
 | ||||||
| const invite = () => { | const invite = () => { | ||||||
| 	os.api('admin/invite').then(x => { | 	os.api('admin/invite').then(x => { | ||||||
| 		os.alert({ | 		os.alert({ | ||||||
| 			type: 'info', | 			type: 'info', | ||||||
| 			text: x.code | 			text: x.code, | ||||||
| 		}); | 		}); | ||||||
| 	}).catch(err => { | 	}).catch(err => { | ||||||
| 		os.alert({ | 		os.alert({ | ||||||
|  | @ -279,33 +274,38 @@ const lookup = (ev) => { | ||||||
| 		icon: 'fas fa-user', | 		icon: 'fas fa-user', | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			lookupUser(); | 			lookupUser(); | ||||||
| 		} | 		}, | ||||||
| 	}, { | 	}, { | ||||||
| 		text: i18n.ts.note, | 		text: i18n.ts.note, | ||||||
| 		icon: 'fas fa-pencil-alt', | 		icon: 'fas fa-pencil-alt', | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			alert('TODO'); | 			alert('TODO'); | ||||||
| 		} | 		}, | ||||||
| 	}, { | 	}, { | ||||||
| 		text: i18n.ts.file, | 		text: i18n.ts.file, | ||||||
| 		icon: 'fas fa-cloud', | 		icon: 'fas fa-cloud', | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			alert('TODO'); | 			alert('TODO'); | ||||||
| 		} | 		}, | ||||||
| 	}, { | 	}, { | ||||||
| 		text: i18n.ts.instance, | 		text: i18n.ts.instance, | ||||||
| 		icon: 'fas fa-globe', | 		icon: 'fas fa-globe', | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			alert('TODO'); | 			alert('TODO'); | ||||||
| 		} | 		}, | ||||||
| 	}], ev.currentTarget ?? ev.target); | 	}], ev.currentTarget ?? ev.target); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | const headerActions = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(INFO); | ||||||
|  | 
 | ||||||
| defineExpose({ | defineExpose({ | ||||||
| 	[symbols.PAGE_INFO]: INFO, |  | ||||||
| 	header: { | 	header: { | ||||||
| 		title: i18n.ts.controlPanel, | 		title: i18n.ts.controlPanel, | ||||||
| 	} | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | <MkStickyContainer> | ||||||
|  | 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||||
| 		<FormSuspense :p="init"> | 		<FormSuspense :p="init"> | ||||||
| 			<FormTextarea v-model="blockedHosts" class="_formBlock"> | 			<FormTextarea v-model="blockedHosts" class="_formBlock"> | ||||||
| 				<span>{{ i18n.ts.blockedInstances }}</span> | 				<span>{{ i18n.ts.blockedInstances }}</span> | ||||||
|  | @ -8,18 +10,20 @@ | ||||||
| 
 | 
 | ||||||
| 			<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> | 			<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> | ||||||
| 		</FormSuspense> | 		</FormSuspense> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import FormButton from '@/components/ui/button.vue'; | import FormButton from '@/components/ui/button.vue'; | ||||||
| import FormTextarea from '@/components/form/textarea.vue'; | import FormTextarea from '@/components/form/textarea.vue'; | ||||||
| import FormSuspense from '@/components/form/suspense.vue'; | import FormSuspense from '@/components/form/suspense.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { fetchInstance } from '@/instance'; | import { fetchInstance } from '@/instance'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let blockedHosts: string = $ref(''); | let blockedHosts: string = $ref(''); | ||||||
| 
 | 
 | ||||||
|  | @ -36,11 +40,13 @@ function save() { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.instanceBlocking, | 	title: i18n.ts.instanceBlocking, | ||||||
| 	icon: 'fas fa-ban', | 	icon: 'fas fa-ban', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||||
| 	<FormSuspense :p="init"> | 	<FormSuspense :p="init"> | ||||||
| 		<FormFolder class="_formBlock"> | 		<FormFolder class="_formBlock"> | ||||||
| 			<template #icon><i class="fab fa-twitter"></i></template> | 			<template #icon><i class="fab fa-twitter"></i></template> | ||||||
|  | @ -20,19 +21,19 @@ | ||||||
| 			<XDiscord/> | 			<XDiscord/> | ||||||
| 		</FormFolder> | 		</FormFolder> | ||||||
| 	</FormSuspense> | 	</FormSuspense> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
| import FormFolder from '@/components/form/folder.vue'; |  | ||||||
| import FormSuspense from '@/components/form/suspense.vue'; |  | ||||||
| import XTwitter from './integrations.twitter.vue'; | import XTwitter from './integrations.twitter.vue'; | ||||||
| import XGithub from './integrations.github.vue'; | import XGithub from './integrations.github.vue'; | ||||||
| import XDiscord from './integrations.discord.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 os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let enableTwitterIntegration: boolean = $ref(false); | let enableTwitterIntegration: boolean = $ref(false); | ||||||
| let enableGithubIntegration: boolean = $ref(false); | let enableGithubIntegration: boolean = $ref(false); | ||||||
|  | @ -45,11 +46,13 @@ async function init() { | ||||||
| 	enableDiscordIntegration = meta.enableDiscordIntegration; | 	enableDiscordIntegration = meta.enableDiscordIntegration; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.integration, | 	title: i18n.ts.integration, | ||||||
| 	icon: 'fas fa-share-alt', | 	icon: 'fas fa-share-alt', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | <MkStickyContainer> | ||||||
|  | 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||||
| 		<FormSuspense :p="init"> | 		<FormSuspense :p="init"> | ||||||
| 			<div class="_formRoot"> | 			<div class="_formRoot"> | ||||||
| 				<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch> | 				<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch> | ||||||
|  | @ -62,11 +64,13 @@ | ||||||
| 				</template> | 				</template> | ||||||
| 			</div> | 			</div> | ||||||
| 		</FormSuspense> | 		</FormSuspense> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import FormSwitch from '@/components/form/switch.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import FormInput from '@/components/form/input.vue'; | import FormInput from '@/components/form/input.vue'; | ||||||
| import FormGroup from '@/components/form/group.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 FormSplit from '@/components/form/split.vue'; | ||||||
| import FormSection from '@/components/form/section.vue'; | import FormSection from '@/components/form/section.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { fetchInstance } from '@/instance'; | import { fetchInstance } from '@/instance'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let useObjectStorage: boolean = $ref(false); | let useObjectStorage: boolean = $ref(false); | ||||||
| let objectStorageBaseUrl: string | null = $ref(null); | let objectStorageBaseUrl: string | null = $ref(null); | ||||||
|  | @ -129,17 +133,18 @@ function save() { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
|   [symbols.PAGE_INFO]: { |  | ||||||
| 		title: i18n.ts.objectStorage, |  | ||||||
| 		icon: 'fas fa-cloud', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	icon: 'fas fa-check', | 	icon: 'fas fa-check', | ||||||
| 	text: i18n.ts.save, | 	text: i18n.ts.save, | ||||||
| 	handler: save, | 	handler: save, | ||||||
| 		}], | }]); | ||||||
| 	} | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
|  | 	title: i18n.ts.objectStorage, | ||||||
|  | 	icon: 'fas fa-cloud', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,18 +1,22 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | <MkStickyContainer> | ||||||
|  | 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||||
| 		<FormSuspense :p="init"> | 		<FormSuspense :p="init"> | ||||||
| 			none | 			none | ||||||
| 		</FormSuspense> | 		</FormSuspense> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import FormSuspense from '@/components/form/suspense.vue'; | import FormSuspense from '@/components/form/suspense.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { fetchInstance } from '@/instance'; | import { fetchInstance } from '@/instance'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| async function init() { | async function init() { | ||||||
| 	await os.api('admin/meta'); | 	await os.api('admin/meta'); | ||||||
|  | @ -24,17 +28,18 @@ function save() { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
|   [symbols.PAGE_INFO]: { |  | ||||||
| 		title: i18n.ts.other, |  | ||||||
| 		icon: 'fas fa-cogs', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	icon: 'fas fa-check', | 	icon: 'fas fa-check', | ||||||
| 	text: i18n.ts.save, | 	text: i18n.ts.save, | ||||||
| 	handler: save, | 	handler: save, | ||||||
| 		}], | }]); | ||||||
| 	} | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
|  | 	title: i18n.ts.other, | ||||||
|  | 	icon: 'fas fa-cogs', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -67,6 +67,7 @@ | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; | import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; | ||||||
|  | import XMetrics from './metrics.vue'; | ||||||
| import MkInstanceStats from '@/components/instance-stats.vue'; | import MkInstanceStats from '@/components/instance-stats.vue'; | ||||||
| import MkNumberDiff from '@/components/number-diff.vue'; | import MkNumberDiff from '@/components/number-diff.vue'; | ||||||
| import MkContainer from '@/components/ui/container.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 MkQueueChart from '@/components/queue-chart.vue'; | ||||||
| import { version, url } from '@/config'; | import { version, url } from '@/config'; | ||||||
| import number from '@/filters/number'; | import number from '@/filters/number'; | ||||||
| import XMetrics from './metrics.vue'; |  | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { stream } from '@/stream'; | import { stream } from '@/stream'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let stats: any = $ref(null); | let stats: any = $ref(null); | ||||||
| let serverInfo: any = $ref(null); | let serverInfo: any = $ref(null); | ||||||
|  | @ -106,7 +106,7 @@ onMounted(async () => { | ||||||
| 	nextTick(() => { | 	nextTick(() => { | ||||||
| 		queueStatsConnection.send('requestLog', { | 		queueStatsConnection.send('requestLog', { | ||||||
| 			id: Math.random().toString().substr(2, 8), | 			id: Math.random().toString().substr(2, 8), | ||||||
| 			length: 200 | 			length: 200, | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
|  | @ -115,12 +115,14 @@ onBeforeUnmount(() => { | ||||||
| 	queueStatsConnection.dispose(); | 	queueStatsConnection.dispose(); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.dashboard, | 	title: i18n.ts.dashboard, | ||||||
| 	icon: 'fas fa-tachometer-alt', | 	icon: 'fas fa-tachometer-alt', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||||
| 	<FormSuspense :p="init"> | 	<FormSuspense :p="init"> | ||||||
| 		<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo> | 		<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo> | ||||||
| 		<MkKeyValue class="_formBlock"> | 		<MkKeyValue class="_formBlock"> | ||||||
|  | @ -9,7 +10,7 @@ | ||||||
| 
 | 
 | ||||||
| 		<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton> | 		<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton> | ||||||
| 	</FormSuspense> | 	</FormSuspense> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -19,9 +20,9 @@ import FormButton from '@/components/ui/button.vue'; | ||||||
| import MkInfo from '@/components/ui/info.vue'; | import MkInfo from '@/components/ui/info.vue'; | ||||||
| import FormSuspense from '@/components/form/suspense.vue'; | import FormSuspense from '@/components/form/suspense.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { fetchInstance } from '@/instance'; | import { fetchInstance } from '@/instance'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let proxyAccount: any = $ref(null); | let proxyAccount: any = $ref(null); | ||||||
| let proxyAccountId: any = $ref(null); | let proxyAccountId: any = $ref(null); | ||||||
|  | @ -50,11 +51,13 @@ function save() { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.proxyAccount, | 	title: i18n.ts.proxyAccount, | ||||||
| 	icon: 'fas fa-ghost', | 	icon: 'fas fa-ghost', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="800"> | <MkStickyContainer> | ||||||
|  | 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="800"> | ||||||
| 		<XQueue :connection="connection" domain="inbox"> | 		<XQueue :connection="connection" domain="inbox"> | ||||||
| 			<template #title>In</template> | 			<template #title>In</template> | ||||||
| 		</XQueue> | 		</XQueue> | ||||||
|  | @ -7,18 +9,20 @@ | ||||||
| 			<template #title>Out</template> | 			<template #title>Out</template> | ||||||
| 		</XQueue> | 		</XQueue> | ||||||
| 		<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton> | 		<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue'; | import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; |  | ||||||
| import XQueue from './queue.chart.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 * as os from '@/os'; | ||||||
| import { stream } from '@/stream'; | import { stream } from '@/stream'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import * as config from '@/config'; | import * as config from '@/config'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const connection = markRaw(stream.useChannel('queueStats')); | const connection = markRaw(stream.useChannel('queueStats')); | ||||||
| 
 | 
 | ||||||
|  | @ -38,7 +42,7 @@ onMounted(() => { | ||||||
| 	nextTick(() => { | 	nextTick(() => { | ||||||
| 		connection.send('requestLog', { | 		connection.send('requestLog', { | ||||||
| 			id: Math.random().toString().substr(2, 8), | 			id: Math.random().toString().substr(2, 8), | ||||||
| 			length: 200 | 			length: 200, | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
|  | @ -47,19 +51,20 @@ onBeforeUnmount(() => { | ||||||
| 	connection.dispose(); | 	connection.dispose(); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
| 	[symbols.PAGE_INFO]: { |  | ||||||
| 		title: i18n.ts.jobQueue, |  | ||||||
| 		icon: 'fas fa-clipboard-list', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	icon: 'fas fa-up-right-from-square', | 	icon: 'fas fa-up-right-from-square', | ||||||
| 	text: i18n.ts.dashboard, | 	text: i18n.ts.dashboard, | ||||||
| 	handler: () => { | 	handler: () => { | ||||||
| 		window.open(config.url + '/queue', '_blank'); | 		window.open(config.url + '/queue', '_blank'); | ||||||
| 	}, | 	}, | ||||||
| 		}], | }]); | ||||||
| 	} | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
|  | 	title: i18n.ts.jobQueue, | ||||||
|  | 	icon: 'fas fa-clipboard-list', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="800"> | <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 v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;"> | ||||||
| 			<div>{{ relay.inbox }}</div> | 			<div>{{ relay.inbox }}</div> | ||||||
| 			<div class="status"> | 			<div class="status"> | ||||||
|  | @ -10,15 +12,17 @@ | ||||||
| 			</div> | 			</div> | ||||||
| 			<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> | 			<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let relays: any[] = $ref([]); | let relays: any[] = $ref([]); | ||||||
| 
 | 
 | ||||||
|  | @ -26,30 +30,30 @@ async function addRelay() { | ||||||
| 	const { canceled, result: inbox } = await os.inputText({ | 	const { canceled, result: inbox } = await os.inputText({ | ||||||
| 		title: i18n.ts.addRelay, | 		title: i18n.ts.addRelay, | ||||||
| 		type: 'url', | 		type: 'url', | ||||||
| 		placeholder: i18n.ts.inboxUrl | 		placeholder: i18n.ts.inboxUrl, | ||||||
| 	}); | 	}); | ||||||
| 	if (canceled) return; | 	if (canceled) return; | ||||||
| 	os.api('admin/relays/add', { | 	os.api('admin/relays/add', { | ||||||
| 		inbox | 		inbox, | ||||||
| 	}).then((relay: any) => { | 	}).then((relay: any) => { | ||||||
| 		refresh(); | 		refresh(); | ||||||
| 	}).catch((err: any) => { | 	}).catch((err: any) => { | ||||||
| 		os.alert({ | 		os.alert({ | ||||||
| 			type: 'error', | 			type: 'error', | ||||||
| 			text: err.message || err | 			text: err.message || err, | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function remove(inbox: string) { | function remove(inbox: string) { | ||||||
| 	os.api('admin/relays/remove', { | 	os.api('admin/relays/remove', { | ||||||
| 		inbox | 		inbox, | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
| 		refresh(); | 		refresh(); | ||||||
| 	}).catch((err: any) => { | 	}).catch((err: any) => { | ||||||
| 		os.alert({ | 		os.alert({ | ||||||
| 			type: 'error', | 			type: 'error', | ||||||
| 			text: err.message || err | 			text: err.message || err, | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
|  | @ -62,18 +66,19 @@ function refresh() { | ||||||
| 
 | 
 | ||||||
| refresh(); | refresh(); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
| 	[symbols.PAGE_INFO]: { |  | ||||||
| 		title: i18n.ts.relays, |  | ||||||
| 		icon: 'fas fa-globe', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	icon: 'fas fa-plus', | 	icon: 'fas fa-plus', | ||||||
| 	text: i18n.ts.addRelay, | 	text: i18n.ts.addRelay, | ||||||
| 	handler: addRelay, | 	handler: addRelay, | ||||||
| 		}], | }]); | ||||||
| 	} | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
|  | 	title: i18n.ts.relays, | ||||||
|  | 	icon: 'fas fa-globe', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | <MkStickyContainer> | ||||||
|  | 	<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||||
| 		<FormSuspense :p="init"> | 		<FormSuspense :p="init"> | ||||||
| 			<div class="_formRoot"> | 			<div class="_formRoot"> | ||||||
| 				<FormFolder class="_formBlock"> | 				<FormFolder class="_formBlock"> | ||||||
|  | @ -26,11 +28,14 @@ | ||||||
| 				</FormFolder> | 				</FormFolder> | ||||||
| 			</div> | 			</div> | ||||||
| 		</FormSuspense> | 		</FormSuspense> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
|  | import XBotProtection from './bot-protection.vue'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import FormFolder from '@/components/form/folder.vue'; | import FormFolder from '@/components/form/folder.vue'; | ||||||
| import FormSwitch from '@/components/form/switch.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import FormInfo from '@/components/ui/info.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 FormSection from '@/components/form/section.vue'; | ||||||
| import FormInput from '@/components/form/input.vue'; | import FormInput from '@/components/form/input.vue'; | ||||||
| import FormButton from '@/components/ui/button.vue'; | import FormButton from '@/components/ui/button.vue'; | ||||||
| import XBotProtection from './bot-protection.vue'; |  | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { fetchInstance } from '@/instance'; | import { fetchInstance } from '@/instance'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let summalyProxy: string = $ref(''); | let summalyProxy: string = $ref(''); | ||||||
| let enableHcaptcha: boolean = $ref(false); | let enableHcaptcha: boolean = $ref(false); | ||||||
|  | @ -63,11 +67,13 @@ function save() { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.security, | 	title: i18n.ts.security, | ||||||
| 	icon: 'fas fa-lock', | 	icon: 'fas fa-lock', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | <div> | ||||||
|  | 	<MkStickyContainer> | ||||||
|  | 		<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> | ||||||
| 			<FormSuspense :p="init"> | 			<FormSuspense :p="init"> | ||||||
| 				<div class="_formRoot"> | 				<div class="_formRoot"> | ||||||
| 					<FormInput v-model="name" class="_formBlock"> | 					<FormInput v-model="name" class="_formBlock"> | ||||||
|  | @ -139,11 +142,14 @@ | ||||||
| 					</FormSection> | 					</FormSection> | ||||||
| 				</div> | 				</div> | ||||||
| 			</FormSuspense> | 			</FormSuspense> | ||||||
| </MkSpacer> | 		</MkSpacer> | ||||||
|  | 	</MkStickyContainer> | ||||||
|  | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import FormSwitch from '@/components/form/switch.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import FormInput from '@/components/form/input.vue'; | import FormInput from '@/components/form/input.vue'; | ||||||
| import FormTextarea from '@/components/form/textarea.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 FormSplit from '@/components/form/split.vue'; | ||||||
| import FormSuspense from '@/components/form/suspense.vue'; | import FormSuspense from '@/components/form/suspense.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { fetchInstance } from '@/instance'; | import { fetchInstance } from '@/instance'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let name: string | null = $ref(null); | let name: string | null = $ref(null); | ||||||
| let description: string | null = $ref(null); | let description: string | null = $ref(null); | ||||||
|  | @ -240,17 +246,18 @@ function save() { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
| 	[symbols.PAGE_INFO]: { |  | ||||||
| 		title: i18n.ts.general, |  | ||||||
| 		icon: 'fas fa-cog', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	icon: 'fas fa-check', | 	icon: 'fas fa-check', | ||||||
| 	text: i18n.ts.save, | 	text: i18n.ts.save, | ||||||
| 	handler: save, | 	handler: save, | ||||||
| 		}], | }]); | ||||||
| 	} | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
|  | 	title: i18n.ts.general, | ||||||
|  | 	icon: 'fas fa-cog', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,9 @@ | ||||||
| <template> | <template> | ||||||
| <div class="lknzcolw"> | <div> | ||||||
|  | 	<MkStickyContainer> | ||||||
|  | 		<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="900"> | ||||||
|  | 			<div class="lknzcolw"> | ||||||
| 				<div class="users"> | 				<div class="users"> | ||||||
| 					<div class="inputs"> | 					<div class="inputs"> | ||||||
| 						<MkSelect v-model="sort" style="flex: 1;"> | 						<MkSelect v-model="sort" style="flex: 1;"> | ||||||
|  | @ -58,19 +62,23 @@ | ||||||
| 						</button> | 						</button> | ||||||
| 					</MkPagination> | 					</MkPagination> | ||||||
| 				</div> | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</MkSpacer> | ||||||
|  | 	</MkStickyContainer> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
|  | import XHeader from './_header_.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkSelect from '@/components/form/select.vue'; | import MkSelect from '@/components/form/select.vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import { acct } from '@/filters/user'; | import { acct } from '@/filters/user'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { lookupUser } from '@/scripts/lookup-user'; | import { lookupUser } from '@/scripts/lookup-user'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let paginationComponent = $ref<InstanceType<typeof MkPagination>>(); | let paginationComponent = $ref<InstanceType<typeof MkPagination>>(); | ||||||
| 
 | 
 | ||||||
|  | @ -89,7 +97,7 @@ const pagination = { | ||||||
| 		username: searchUsername, | 		username: searchUsername, | ||||||
| 		hostname: searchHost, | 		hostname: searchHost, | ||||||
| 	})), | 	})), | ||||||
| 	offsetMode: true | 	offsetMode: true, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function searchUser() { | function searchUser() { | ||||||
|  | @ -106,7 +114,7 @@ async function addUser() { | ||||||
| 
 | 
 | ||||||
| 	const { canceled: canceled2, result: password } = await os.inputText({ | 	const { canceled: canceled2, result: password } = await os.inputText({ | ||||||
| 		title: i18n.ts.password, | 		title: i18n.ts.password, | ||||||
| 		type: 'password' | 		type: 'password', | ||||||
| 	}); | 	}); | ||||||
| 	if (canceled2) return; | 	if (canceled2) return; | ||||||
| 
 | 
 | ||||||
|  | @ -122,34 +130,34 @@ function show(user) { | ||||||
| 	os.pageWindow(`/user-info/${user.id}`); | 	os.pageWindow(`/user-info/${user.id}`); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
| 	[symbols.PAGE_INFO]: computed(() => ({ |  | ||||||
| 		title: i18n.ts.users, |  | ||||||
| 		icon: 'fas fa-users', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	icon: 'fas fa-search', | 	icon: 'fas fa-search', | ||||||
| 	text: i18n.ts.search, | 	text: i18n.ts.search, | ||||||
| 			handler: searchUser | 	handler: searchUser, | ||||||
| 		}, { | }, { | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	icon: 'fas fa-plus', | 	icon: 'fas fa-plus', | ||||||
| 	text: i18n.ts.addUser, | 	text: i18n.ts.addUser, | ||||||
| 			handler: addUser | 	handler: addUser, | ||||||
| 		}, { | }, { | ||||||
| 	asFullButton: true, | 	asFullButton: true, | ||||||
| 	icon: 'fas fa-search', | 	icon: 'fas fa-search', | ||||||
| 	text: i18n.ts.lookup, | 	text: i18n.ts.lookup, | ||||||
| 			handler: lookupUser | 	handler: lookupUser, | ||||||
| 		}], | }]); | ||||||
| 	})), | 
 | ||||||
| }); | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => ({ | ||||||
|  | 	title: i18n.ts.users, | ||||||
|  | 	icon: 'fas fa-users', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
|  | }))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .lknzcolw { | .lknzcolw { | ||||||
| 	> .users { | 	> .users { | ||||||
| 		margin: var(--margin); |  | ||||||
| 
 | 
 | ||||||
| 		> .inputs { | 		> .inputs { | ||||||
| 			display: flex; | 			display: flex; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="800"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="800"> | ||||||
| 		<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content"> | 		<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content"> | ||||||
| 			<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement"> | 			<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="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> | ||||||
|  | @ -12,46 +14,40 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 			</section> | 			</section> | ||||||
| 		</MkPagination> | 		</MkPagination> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent } from 'vue'; | import { } from 'vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const pagination = { | ||||||
| 	components: { |  | ||||||
| 		MkPagination, |  | ||||||
| 		MkButton |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			[symbols.PAGE_INFO]: { |  | ||||||
| 				title: this.$ts.announcements, |  | ||||||
| 				icon: 'fas fa-broadcast-tower', |  | ||||||
| 				bg: 'var(--bg)', |  | ||||||
| 			}, |  | ||||||
| 			pagination: { |  | ||||||
| 	endpoint: 'announcements' as const, | 	endpoint: 'announcements' as const, | ||||||
| 	limit: 10, | 	limit: 10, | ||||||
| 			}, | }; | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	methods: { | // TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい | ||||||
| 		// TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい | function read(items, announcement, i) { | ||||||
| 		read(items, announcement, i) { |  | ||||||
| 	items[i] = { | 	items[i] = { | ||||||
| 		...announcement, | 		...announcement, | ||||||
| 		isRead: true, | 		isRead: true, | ||||||
| 	}; | 	}; | ||||||
| 	os.api('i/read-announcement', { announcementId: announcement.id }); | 	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> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,9 @@ | ||||||
| <template> | <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 v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> | ||||||
| 	<div class="tl _block"> | 	<div class="tl _block"> | ||||||
| 		<XTimeline ref="tl" :key="antennaId" | 		<XTimeline | ||||||
|  | 			ref="tlEl" :key="antennaId" | ||||||
| 			class="tl" | 			class="tl" | ||||||
| 			src="antenna" | 			src="antenna" | ||||||
| 			:antenna="antennaId" | 			:antenna="antennaId" | ||||||
|  | @ -13,92 +14,78 @@ | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent, defineAsyncComponent, computed } from 'vue'; | import { computed, inject, watch } from 'vue'; | ||||||
| import XTimeline from '@/components/timeline.vue'; | import XTimeline from '@/components/timeline.vue'; | ||||||
| import { scroll } from '@/scripts/scroll'; | import { scroll } from '@/scripts/scroll'; | ||||||
| import * as os from '@/os'; | 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({ | const router = useRouter(); | ||||||
| 	components: { |  | ||||||
| 		XTimeline, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	props: { | const props = defineProps<{ | ||||||
| 		antennaId: { | 	antennaId: string; | ||||||
| 			type: String, | }>(); | ||||||
| 			required: true |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | let antenna = $ref(null); | ||||||
| 		return { | let queue = $ref(0); | ||||||
| 			antenna: null, | let rootEl = $ref<HTMLElement>(); | ||||||
| 			queue: 0, | let tlEl = $ref<InstanceType<typeof XTimeline>>(); | ||||||
| 			[symbols.PAGE_INFO]: computed(() => this.antenna ? { | const keymap = $computed(() => ({ | ||||||
| 				title: this.antenna.name, | 	't': focus, | ||||||
|  | })); | ||||||
|  | 
 | ||||||
|  | function queueUpdated(q) { | ||||||
|  | 	queue = q; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function top() { | ||||||
|  | 	scroll(rootEl, { top: 0 }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function timetravel() { | ||||||
|  | 	const { canceled, result: date } = await os.inputDate({ | ||||||
|  | 		title: i18n.ts.date, | ||||||
|  | 	}); | ||||||
|  | 	if (canceled) return; | ||||||
|  | 
 | ||||||
|  | 	tlEl.timetravel(date); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function settings() { | ||||||
|  | 	router.push(`/my/antennas/${props.antennaId}`); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function focus() { | ||||||
|  | 	tlEl.focus(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | watch(() => props.antennaId, async () => { | ||||||
|  | 	antenna = await os.api('antennas/show', { | ||||||
|  | 		antennaId: props.antennaId, | ||||||
|  | 	}); | ||||||
|  | }, { immediate: true }); | ||||||
|  | 
 | ||||||
|  | const headerActions = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => antenna ? { | ||||||
|  | 	title: antenna.name, | ||||||
| 	icon: 'fas fa-satellite', | 	icon: 'fas fa-satellite', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	actions: [{ | 	actions: [{ | ||||||
| 		icon: 'fas fa-calendar-alt', | 		icon: 'fas fa-calendar-alt', | ||||||
| 					text: this.$ts.jumpToSpecifiedDate, | 		text: i18n.ts.jumpToSpecifiedDate, | ||||||
| 					handler: this.timetravel | 		handler: timetravel, | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: 'fas fa-cog', | 		icon: 'fas fa-cog', | ||||||
| 					text: this.$ts.settings, | 		text: i18n.ts.settings, | ||||||
| 					handler: this.settings | 		handler: settings, | ||||||
| 	}], | 	}], | ||||||
| 			} : null), | } : null)); | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	computed: { |  | ||||||
| 		keymap(): any { |  | ||||||
| 			return { |  | ||||||
| 				't': this.focus |  | ||||||
| 			}; |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	watch: { |  | ||||||
| 		antennaId: { |  | ||||||
| 			async handler() { |  | ||||||
| 				this.antenna = await os.api('antennas/show', { |  | ||||||
| 					antennaId: this.antennaId |  | ||||||
| 				}); |  | ||||||
| 			}, |  | ||||||
| 			immediate: true |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		queueUpdated(q) { |  | ||||||
| 			this.queue = q; |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		top() { |  | ||||||
| 			scroll(this.$el, { top: 0 }); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		async timetravel() { |  | ||||||
| 			const { canceled, result: date } = await os.inputDate({ |  | ||||||
| 				title: this.$ts.date, |  | ||||||
| 			}); |  | ||||||
| 			if (canceled) return; |  | ||||||
| 
 |  | ||||||
| 			this.$refs.tl.timetravel(date); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		settings() { |  | ||||||
| 			this.$router.push(`/my/antennas/${this.antennaId}`); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		focus() { |  | ||||||
| 			(this.$refs.tl as any).focus(); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="700"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="700"> | ||||||
| 		<div class="_formRoot"> | 		<div class="_formRoot"> | ||||||
| 			<div class="_formBlock"> | 			<div class="_formBlock"> | ||||||
| 				<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()"> | 				<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()"> | ||||||
|  | @ -22,19 +24,20 @@ | ||||||
| 				</MkTextarea> | 				</MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| import JSON5 from 'json5'; | import JSON5 from 'json5'; | ||||||
|  | import { Endpoints } from 'misskey-js'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkTextarea from '@/components/form/textarea.vue'; | import MkTextarea from '@/components/form/textarea.vue'; | ||||||
| import MkSwitch from '@/components/form/switch.vue'; | import MkSwitch from '@/components/form/switch.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| import { Endpoints } from 'misskey-js'; |  | ||||||
| 
 | 
 | ||||||
| const body = ref('{}'); | const body = ref('{}'); | ||||||
| const endpoint = ref(''); | const endpoint = ref(''); | ||||||
|  | @ -75,10 +78,12 @@ function onEndpointChange() { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: 'API console', | 	title: 'API console', | ||||||
| 		icon: 'fas fa-terminal' | 	icon: 'fas fa-terminal', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ | ||||||
| 		<h1>{{ $ts._auth.denied }}</h1> | 		<h1>{{ $ts._auth.denied }}</h1> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div v-if="state == 'accepted'" class="accepted"> | 	<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.callback }}<MkEllipsis/></p> | ||||||
| 		<p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p> | 		<p v-if="!session.app.callbackUrl">{{ $ts._auth.pleaseGoBack }}</p> | ||||||
| 	</div> | 	</div> | ||||||
|  | @ -40,24 +40,20 @@ export default defineComponent({ | ||||||
| 		XForm, | 		XForm, | ||||||
| 		MkSignin, | 		MkSignin, | ||||||
| 	}, | 	}, | ||||||
|  | 	props: ['token'], | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			state: null, | 			state: null, | ||||||
| 			session: null, | 			session: null, | ||||||
| 			fetching: true | 			fetching: true, | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	computed: { |  | ||||||
| 		token(): string { |  | ||||||
| 			return this.$route.params.token; |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		if (!this.$i) return; | 		if (!this.$i) return; | ||||||
| 
 | 
 | ||||||
| 		// Fetch session | 		// Fetch session | ||||||
| 		os.api('auth/session/show', { | 		os.api('auth/session/show', { | ||||||
| 			token: this.token | 			token: this.token, | ||||||
| 		}).then(session => { | 		}).then(session => { | ||||||
| 			this.session = session; | 			this.session = session; | ||||||
| 			this.fetching = false; | 			this.fetching = false; | ||||||
|  | @ -65,7 +61,7 @@ export default defineComponent({ | ||||||
| 			// 既に連携していた場合 | 			// 既に連携していた場合 | ||||||
| 			if (this.session.app.isAuthorized) { | 			if (this.session.app.isAuthorized) { | ||||||
| 				os.api('auth/accept', { | 				os.api('auth/accept', { | ||||||
| 					token: this.session.token | 					token: this.session.token, | ||||||
| 				}).then(() => { | 				}).then(() => { | ||||||
| 					this.accepted(); | 					this.accepted(); | ||||||
| 				}); | 				}); | ||||||
|  | @ -85,8 +81,8 @@ export default defineComponent({ | ||||||
| 			} | 			} | ||||||
| 		}, onLogin(res) { | 		}, onLogin(res) { | ||||||
| 			login(res.i); | 			login(res.i); | ||||||
| 		} | 		}, | ||||||
| 	} | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="700"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="700"> | ||||||
| 		<div class="_formRoot"> | 		<div class="_formRoot"> | ||||||
| 			<MkInput v-model="name" class="_formBlock"> | 			<MkInput v-model="name" class="_formBlock"> | ||||||
| 				<template #label>{{ $ts.name }}</template> | 				<template #label>{{ $ts.name }}</template> | ||||||
|  | @ -20,108 +22,101 @@ | ||||||
| 				<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton> | 				<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, inject, watch } from 'vue'; | ||||||
| import MkTextarea from '@/components/form/textarea.vue'; | import MkTextarea from '@/components/form/textarea.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import { selectFile } from '@/scripts/select-file'; | import { selectFile } from '@/scripts/select-file'; | ||||||
| import * as os from '@/os'; | 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({ | const router = useRouter(); | ||||||
| 	components: { |  | ||||||
| 		MkTextarea, MkButton, MkInput, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	props: { | const props = defineProps<{ | ||||||
| 		channelId: { | 	channelId?: string; | ||||||
| 			type: String, | }>(); | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | let channel = $ref(null); | ||||||
| 		return { | let name = $ref(null); | ||||||
| 			[symbols.PAGE_INFO]: computed(() => this.channelId ? { | let description = $ref(null); | ||||||
| 				title: this.$ts._channel.edit, | let bannerUrl = $ref<string | null>(null); | ||||||
| 				icon: 'fas fa-satellite-dish', | let bannerId = $ref<string | null>(null); | ||||||
| 				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, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	watch: { | watch(() => bannerId, async () => { | ||||||
| 		async bannerId() { | 	if (bannerId == null) { | ||||||
| 			if (this.bannerId == null) { | 		bannerUrl = null; | ||||||
| 				this.bannerUrl = null; |  | ||||||
| 	} else { | 	} else { | ||||||
| 				this.bannerUrl = (await os.api('drive/files/show', { | 		bannerUrl = (await os.api('drive/files/show', { | ||||||
| 					fileId: this.bannerId, | 			fileId: bannerId, | ||||||
| 		})).url; | 		})).url; | ||||||
| 	} | 	} | ||||||
| 		}, | }); | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	async created() { | async function fetchChannel() { | ||||||
| 		if (this.channelId) { | 	if (props.channelId == null) return; | ||||||
| 			this.channel = await os.api('channels/show', { | 
 | ||||||
| 				channelId: this.channelId, | 	channel = await os.api('channels/show', { | ||||||
|  | 		channelId: props.channelId, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 			this.name = this.channel.name; | 	name = channel.name; | ||||||
| 			this.description = this.channel.description; | 	description = channel.description; | ||||||
| 			this.bannerId = this.channel.bannerId; | 	bannerId = channel.bannerId; | ||||||
| 			this.bannerUrl = this.channel.bannerUrl; | 	bannerUrl = channel.bannerUrl; | ||||||
| 		} | } | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	methods: { | fetchChannel(); | ||||||
| 		save() { | 
 | ||||||
|  | function save() { | ||||||
| 	const params = { | 	const params = { | ||||||
| 				name: this.name, | 		name: name, | ||||||
| 				description: this.description, | 		description: description, | ||||||
| 				bannerId: this.bannerId, | 		bannerId: bannerId, | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 			if (this.channelId) { | 	if (props.channelId) { | ||||||
| 				params.channelId = this.channelId; | 		params.channelId = props.channelId; | ||||||
| 				os.api('channels/update', params) | 		os.api('channels/update', params).then(() => { | ||||||
| 				.then(channel => { |  | ||||||
| 			os.success(); | 			os.success(); | ||||||
| 		}); | 		}); | ||||||
| 	} else { | 	} else { | ||||||
| 				os.api('channels/create', params) | 		os.api('channels/create', params).then(created => { | ||||||
| 				.then(channel => { |  | ||||||
| 			os.success(); | 			os.success(); | ||||||
| 					this.$router.push(`/channels/${channel.id}`); | 			router.push(`/channels/${created.id}`); | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		setBannerImage(evt) { | function setBannerImage(evt) { | ||||||
| 	selectFile(evt.currentTarget ?? evt.target, null).then(file => { | 	selectFile(evt.currentTarget ?? evt.target, null).then(file => { | ||||||
| 				this.bannerId = file.id; | 		bannerId = file.id; | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		removeBannerImage() { | function removeBannerImage() { | ||||||
| 			this.bannerId = null; | 	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> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="700"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="700"> | ||||||
| 		<div v-if="channel"> | 		<div v-if="channel"> | ||||||
| 			<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }"> | 			<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }"> | ||||||
| 				<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/> | 				<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/> | ||||||
|  | @ -25,74 +27,61 @@ | ||||||
| 
 | 
 | ||||||
| 			<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/> | 			<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, inject, watch } from 'vue'; | ||||||
| import MkContainer from '@/components/ui/container.vue'; | import MkContainer from '@/components/ui/container.vue'; | ||||||
| import XPostForm from '@/components/post-form.vue'; | import XPostForm from '@/components/post-form.vue'; | ||||||
| import XTimeline from '@/components/timeline.vue'; | import XTimeline from '@/components/timeline.vue'; | ||||||
| import XChannelFollowButton from '@/components/channel-follow-button.vue'; | import XChannelFollowButton from '@/components/channel-follow-button.vue'; | ||||||
| import * as os from '@/os'; | 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({ | const router = useRouter(); | ||||||
| 	components: { |  | ||||||
| 		MkContainer, |  | ||||||
| 		XPostForm, |  | ||||||
| 		XTimeline, |  | ||||||
| 		XChannelFollowButton |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	props: { | const props = defineProps<{ | ||||||
| 		channelId: { | 	channelId: string; | ||||||
| 			type: String, | }>(); | ||||||
| 			required: true |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | let channel = $ref(null); | ||||||
| 		return { | let showBanner = $ref(true); | ||||||
| 			[symbols.PAGE_INFO]: computed(() => this.channel ? { | const pagination = { | ||||||
| 				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, | 	endpoint: 'channels/timeline' as const, | ||||||
| 	limit: 10, | 	limit: 10, | ||||||
| 	params: computed(() => ({ | 	params: computed(() => ({ | ||||||
| 					channelId: this.channelId, | 		channelId: props.channelId, | ||||||
| 				})) | 	})), | ||||||
| 			}, | }; | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	watch: { | watch(() => props.channelId, async () => { | ||||||
| 		channelId: { | 	channel = await os.api('channels/show', { | ||||||
| 			async handler() { | 		channelId: props.channelId, | ||||||
| 				this.channel = await os.api('channels/show', { |  | ||||||
| 					channelId: this.channelId, |  | ||||||
| 	}); | 	}); | ||||||
| 			}, | }, { immediate: true }); | ||||||
| 			immediate: true |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	methods: { | function edit() { | ||||||
| 		edit() { | 	router.push(`/channels/${channel.id}/edit`); | ||||||
| 			this.$router.push(`/channels/${this.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> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="700"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="700"> | ||||||
| 		<div v-if="tab === 'featured'" class="_content grwlizim featured"> | 		<div v-if="tab === 'featured'" class="_content grwlizim featured"> | ||||||
| 			<MkPagination v-slot="{items}" :pagination="featuredPagination"> | 			<MkPagination v-slot="{items}" :pagination="featuredPagination"> | ||||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||||
|  | @ -16,67 +18,66 @@ | ||||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/> | ||||||
| 			</MkPagination> | 			</MkPagination> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, defineComponent, inject } from 'vue'; | ||||||
| import MkChannelPreview from '@/components/channel-preview.vue'; | import MkChannelPreview from '@/components/channel-preview.vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import MkButton from '@/components/ui/button.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({ | const router = useRouter(); | ||||||
| 	components: { | 
 | ||||||
| 		MkChannelPreview, MkPagination, MkButton, | let tab = $ref('featured'); | ||||||
| 	}, | 
 | ||||||
| 	data() { | const featuredPagination = { | ||||||
| 		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, | 	endpoint: 'channels/featured' as const, | ||||||
| 	noPaging: true, | 	noPaging: true, | ||||||
| 			}, | }; | ||||||
| 			followingPagination: { | const followingPagination = { | ||||||
| 	endpoint: 'channels/followed' as const, | 	endpoint: 'channels/followed' as const, | ||||||
| 	limit: 5, | 	limit: 5, | ||||||
| 			}, | }; | ||||||
| 			ownedPagination: { | const ownedPagination = { | ||||||
| 	endpoint: 'channels/owned' as const, | 	endpoint: 'channels/owned' as const, | ||||||
| 	limit: 5, | 	limit: 5, | ||||||
| 			}, | }; | ||||||
| 		}; | 
 | ||||||
| 	}, | function create() { | ||||||
| 	methods: { | 	router.push('/channels/new'); | ||||||
| 		create() { | } | ||||||
| 			this.$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> | </script> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="800"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions"/></template> | ||||||
|  | 		<MkSpacer :content-max="800"> | ||||||
| 		<div v-if="clip"> | 		<div v-if="clip"> | ||||||
| 			<div class="okzinsic _panel"> | 			<div class="okzinsic _panel"> | ||||||
| 				<div v-if="clip.description" class="description"> | 				<div v-if="clip.description" class="description"> | ||||||
|  | @ -12,7 +14,8 @@ | ||||||
| 
 | 
 | ||||||
| 			<XNotes :pagination="pagination" :detail="true"/> | 			<XNotes :pagination="pagination" :detail="true"/> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -22,7 +25,7 @@ import XNotes from '@/components/notes.vue'; | ||||||
| import { $i } from '@/account'; | import { $i } from '@/account'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	clipId: string, | 	clipId: string, | ||||||
|  | @ -49,12 +52,7 @@ watch(() => props.clipId, async () => { | ||||||
| 
 | 
 | ||||||
| provide('currentClipPage', $$(clip)); | provide('currentClipPage', $$(clip)); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => clip && isOwned ? [{ | ||||||
| 	[symbols.PAGE_INFO]: computed(() => clip ? { |  | ||||||
| 		title: clip.name, |  | ||||||
| 		icon: 'fas fa-paperclip', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: isOwned ? [{ |  | ||||||
| 	icon: 'fas fa-pencil-alt', | 	icon: 'fas fa-pencil-alt', | ||||||
| 	text: i18n.ts.edit, | 	text: i18n.ts.edit, | ||||||
| 	handler: async (): Promise<void> => { | 	handler: async (): Promise<void> => { | ||||||
|  | @ -84,7 +82,7 @@ defineExpose({ | ||||||
| 			...result, | 			...result, | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
| 		}, { | }, { | ||||||
| 	icon: 'fas fa-trash-alt', | 	icon: 'fas fa-trash-alt', | ||||||
| 	text: i18n.ts.delete, | 	text: i18n.ts.delete, | ||||||
| 	danger: true, | 	danger: true, | ||||||
|  | @ -99,9 +97,13 @@ defineExpose({ | ||||||
| 			clipId: clip.id, | 			clipId: clip.id, | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
| 		}] : [], | }] : null); | ||||||
| 	} : null), | 
 | ||||||
| }); | definePageMetadata(computed(() => clip ? { | ||||||
|  | 	title: clip.name, | ||||||
|  | 	icon: 'fas fa-paperclip', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
|  | } : null)); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -8,17 +8,19 @@ | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| import XDrive from '@/components/drive.vue'; | import XDrive from '@/components/drive.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let folder = $ref(null); | let folder = $ref(null); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => ({ | ||||||
| 	title: folder ? folder.name : i18n.ts.drive, | 	title: folder ? folder.name : i18n.ts.drive, | ||||||
| 	icon: 'fas fa-cloud', | 	icon: 'fas fa-cloud', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	hideHeader: true, | 	hideHeader: true, | ||||||
| 	})), | }))); | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -36,7 +36,6 @@ import MkSelect from '@/components/form/select.vue'; | ||||||
| import MkFolder from '@/components/ui/folder.vue'; | import MkFolder from '@/components/ui/folder.vue'; | ||||||
| import MkTab from '@/components/tab.vue'; | import MkTab from '@/components/tab.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { emojiCategories, emojiTags } from '@/instance'; | import { emojiCategories, emojiTags } from '@/instance'; | ||||||
| import XEmoji from './emojis.emoji.vue'; | import XEmoji from './emojis.emoji.vue'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,15 +1,18 @@ | ||||||
| <template> | <template> | ||||||
| <div :class="$style.root"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<div :class="$style.root"> | ||||||
| 		<XCategory v-if="tab === 'category'"/> | 		<XCategory v-if="tab === 'category'"/> | ||||||
| </div> | 	</div> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { ref, computed } from 'vue'; | import { ref, computed } from 'vue'; | ||||||
| import * as os from '@/os'; |  | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import XCategory from './emojis.category.vue'; | import XCategory from './emojis.category.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const tab = ref('category'); | const tab = ref('category'); | ||||||
| 
 | 
 | ||||||
|  | @ -31,20 +34,21 @@ function menu(ev) { | ||||||
| 					text: err.message, | 					text: err.message, | ||||||
| 				}); | 				}); | ||||||
| 			}); | 			}); | ||||||
| 		} | 		}, | ||||||
| 	}], ev.currentTarget ?? ev.target); | 	}], ev.currentTarget ?? ev.target); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
| 	[symbols.PAGE_INFO]: { | 	icon: 'fas fa-ellipsis-h', | ||||||
|  | 	handler: menu, | ||||||
|  | }]); | ||||||
|  | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.customEmojis, | 	title: i18n.ts.customEmojis, | ||||||
| 	icon: 'fas fa-laugh', | 	icon: 'fas fa-laugh', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 		actions: [{ |  | ||||||
| 			icon: 'fas fa-ellipsis-h', |  | ||||||
| 			handler: menu, |  | ||||||
| 		}], |  | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,11 +1,12 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
| 	<MkSpacer :content-max="1200"> | 	<MkSpacer :content-max="1200"> | ||||||
| 		<div class="lznhrdub"> | 		<div class="lznhrdub"> | ||||||
| 			<div v-if="tab === 'local'"> | 			<div v-if="tab === 'local'"> | ||||||
| 				<div v-if="meta && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> | 				<div v-if="instance && stats && tag == null" class="localfedi7 _block _isolated" :style="{ backgroundImage: instance.bannerUrl ? `url(${instance.bannerUrl})` : null }"> | ||||||
| 					<header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header> | 					<header><span>{{ $t('explore', { host: instance.name || 'Misskey' }) }}</span></header> | ||||||
| 					<div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div> | 					<div><span>{{ $t('exploreUsersCount', { count: number(stats.originalUsersCount) }) }}</span></div> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<template v-if="tag == null"> | 				<template v-if="tag == null"> | ||||||
|  | @ -32,7 +33,7 @@ | ||||||
| 					<header><span>{{ $ts.exploreFediverse }}</span></header> | 					<header><span>{{ $ts.exploreFediverse }}</span></header> | ||||||
| 				</div> | 				</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> | 					<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template> | ||||||
| 
 | 
 | ||||||
| 					<div class="vxjfqztj"> | 					<div class="vxjfqztj"> | ||||||
|  | @ -74,147 +75,127 @@ | ||||||
| 					</MkRadios> | 					</MkRadios> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<XUserList v-if="searchQuery" ref="search" class="_gap" :pagination="searchPagination"/> | 				<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</MkSpacer> | 	</MkSpacer> | ||||||
| </div> | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, defineComponent, watch } from 'vue'; | ||||||
| import XUserList from '@/components/user-list.vue'; | import XUserList from '@/components/user-list.vue'; | ||||||
| import MkFolder from '@/components/ui/folder.vue'; | import MkFolder from '@/components/ui/folder.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkRadios from '@/components/form/radios.vue'; | import MkRadios from '@/components/form/radios.vue'; | ||||||
| import number from '@/filters/number'; | import number from '@/filters/number'; | ||||||
| import * as os from '@/os'; | 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({ | const props = defineProps<{ | ||||||
| 	components: { | 	tag?: string; | ||||||
| 		XUserList, | }>(); | ||||||
| 		MkFolder, |  | ||||||
| 		MkInput, |  | ||||||
| 		MkRadios, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	props: { | let tab = $ref('local'); | ||||||
| 		tag: { | let tagsEl = $ref<InstanceType<typeof MkFolder>>(); | ||||||
| 			type: String, | let tagsLocal = $ref([]); | ||||||
| 			required: false | let tagsRemote = $ref([]); | ||||||
| 		} | let stats = $ref(null); | ||||||
| 	}, | let searchQuery = $ref(null); | ||||||
|  | let searchOrigin = $ref('combined'); | ||||||
| 
 | 
 | ||||||
| 	data() { | watch(() => props.tag, () => { | ||||||
| 		return { | 	if (tagsEl) tagsEl.toggleContent(props.tag == null); | ||||||
| 			[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: { | const tagUsers = $computed(() => ({ | ||||||
| 		meta() { |  | ||||||
| 			return this.$instance; |  | ||||||
| 		}, |  | ||||||
| 		tagUsers(): any { |  | ||||||
| 			return { |  | ||||||
| 	endpoint: 'hashtags/users' as const, | 	endpoint: 'hashtags/users' as const, | ||||||
| 	limit: 30, | 	limit: 30, | ||||||
| 	params: { | 	params: { | ||||||
| 					tag: this.tag, | 		tag: props.tag, | ||||||
| 		origin: 'combined', | 		origin: 'combined', | ||||||
| 		sort: '+follower', | 		sort: '+follower', | ||||||
| 				} |  | ||||||
| 			}; |  | ||||||
| 		}, |  | ||||||
| 	}, | 	}, | ||||||
|  | })); | ||||||
| 
 | 
 | ||||||
| 	watch: { | const pinnedUsers = { endpoint: 'pinned-users' }; | ||||||
| 		tag() { | const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { | ||||||
| 			if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); | 	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), | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| 	created() { | os.api('hashtags/list', { | ||||||
| 		os.api('hashtags/list', { |  | ||||||
| 	sort: '+attachedLocalUsers', | 	sort: '+attachedLocalUsers', | ||||||
| 	attachedToLocalUserOnly: true, | 	attachedToLocalUserOnly: true, | ||||||
| 			limit: 30 | 	limit: 30, | ||||||
| 		}).then(tags => { | }).then(tags => { | ||||||
| 			this.tagsLocal = tags; | 	tagsLocal = tags; | ||||||
| 		}); | }); | ||||||
| 		os.api('hashtags/list', { | os.api('hashtags/list', { | ||||||
| 	sort: '+attachedRemoteUsers', | 	sort: '+attachedRemoteUsers', | ||||||
| 	attachedToRemoteUserOnly: true, | 	attachedToRemoteUserOnly: true, | ||||||
| 			limit: 30 | 	limit: 30, | ||||||
| 		}).then(tags => { | }).then(tags => { | ||||||
| 			this.tagsRemote = tags; | 	tagsRemote = tags; | ||||||
| 		}); |  | ||||||
| 		os.api('stats').then(stats => { |  | ||||||
| 			this.stats = stats; |  | ||||||
| 		}); |  | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
|  | 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> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="800"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader/></template> | ||||||
|  | 	<MkSpacer :content-max="800"> | ||||||
| 		<MkPagination ref="pagingComponent" :pagination="pagination"> | 		<MkPagination ref="pagingComponent" :pagination="pagination"> | ||||||
| 			<template #empty> | 			<template #empty> | ||||||
| 				<div class="_fullinfo"> | 				<div class="_fullinfo"> | ||||||
|  | @ -14,7 +16,8 @@ | ||||||
| 				</XList> | 				</XList> | ||||||
| 			</template> | 			</template> | ||||||
| 		</MkPagination> | 		</MkPagination> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -22,8 +25,8 @@ import { ref } from 'vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import XNote from '@/components/note.vue'; | import XNote from '@/components/note.vue'; | ||||||
| import XList from '@/components/date-separated-list.vue'; | import XList from '@/components/date-separated-list.vue'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const pagination = { | const pagination = { | ||||||
| 	endpoint: 'i/favorites' as const, | 	endpoint: 'i/favorites' as const, | ||||||
|  | @ -32,12 +35,10 @@ const pagination = { | ||||||
| 
 | 
 | ||||||
| const pagingComponent = ref<InstanceType<typeof MkPagination>>(); | const pagingComponent = ref<InstanceType<typeof MkPagination>>(); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | definePageMetadata({ | ||||||
| 	[symbols.PAGE_INFO]: { |  | ||||||
| 	title: i18n.ts.favorites, | 	title: i18n.ts.favorites, | ||||||
| 	icon: 'fas fa-star', | 	icon: 'fas fa-star', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,13 +1,16 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="800"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader/></template> | ||||||
|  | 	<MkSpacer :content-max="800"> | ||||||
| 		<XNotes ref="notes" :pagination="pagination"/> | 		<XNotes ref="notes" :pagination="pagination"/> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import XNotes from '@/components/notes.vue'; | import XNotes from '@/components/notes.vue'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const pagination = { | const pagination = { | ||||||
| 	endpoint: 'notes/featured' as const, | 	endpoint: 'notes/featured' as const, | ||||||
|  | @ -15,11 +18,9 @@ const pagination = { | ||||||
| 	offsetMode: true, | 	offsetMode: true, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| defineExpose({ | definePageMetadata({ | ||||||
| 	[symbols.PAGE_INFO]: { |  | ||||||
| 	title: i18n.ts.featured, | 	title: i18n.ts.featured, | ||||||
| 	icon: 'fas fa-fire-alt', | 	icon: 'fas fa-fire-alt', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="1000"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="1000"> | ||||||
| 		<div class="taeiyria"> | 		<div class="taeiyria"> | ||||||
| 			<div class="query"> | 			<div class="query"> | ||||||
| 				<MkInput v-model="host" :debounce="true" class=""> | 				<MkInput v-model="host" :debounce="true" class=""> | ||||||
|  | @ -88,7 +90,8 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 			</MkPagination> | 			</MkPagination> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -99,8 +102,8 @@ import MkSelect from '@/components/form/select.vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import FormSplit from '@/components/form/split.vue'; | import FormSplit from '@/components/form/split.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let host = $ref(''); | let host = $ref(''); | ||||||
| let state = $ref('federating'); | let state = $ref('federating'); | ||||||
|  | @ -119,8 +122,8 @@ const pagination = { | ||||||
| 			state === 'suspended' ? { suspended: true } : | 			state === 'suspended' ? { suspended: true } : | ||||||
| 			state === 'blocked' ? { blocked: true } : | 			state === 'blocked' ? { blocked: true } : | ||||||
| 			state === 'notResponding' ? { notResponding: true } : | 			state === 'notResponding' ? { notResponding: true } : | ||||||
| 			{}) | 			{}), | ||||||
| 	})) | 	})), | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function getStatus(instance) { | function getStatus(instance) { | ||||||
|  | @ -129,12 +132,14 @@ function getStatus(instance) { | ||||||
| 	return 'alive'; | 	return 'alive'; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.federation, | 	title: i18n.ts.federation, | ||||||
| 	icon: 'fas fa-globe', | 	icon: 'fas fa-globe', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ | ||||||
| 				<div>{{ $ts.noFollowRequests }}</div> | 				<div>{{ $ts.noFollowRequests }}</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</template> | 		</template> | ||||||
| 		<template v-slot="{items}"> | 		<template #default="{items}"> | ||||||
| 			<div class="mk-follow-requests"> | 			<div class="mk-follow-requests"> | ||||||
| 				<div v-for="req in items" :key="req.id" class="user _panel"> | 				<div v-for="req in items" :key="req.id" class="user _panel"> | ||||||
| 					<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/> | 					<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 MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import { userPage, acct } from '@/filters/user'; | import { userPage, acct } from '@/filters/user'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const paginationComponent = ref<InstanceType<typeof MkPagination>>(); | const paginationComponent = ref<InstanceType<typeof MkPagination>>(); | ||||||
| 
 | 
 | ||||||
|  | @ -58,13 +58,15 @@ function reject(user) { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => ({ | ||||||
| 	title: i18n.ts.followRequests, | 	title: i18n.ts.followRequests, | ||||||
| 	icon: 'fas fa-user-clock', | 	icon: 'fas fa-user-clock', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	})), | }))); | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -5,8 +5,9 @@ | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import * as os from '@/os'; |  | ||||||
| import * as Acct from 'misskey-js/built/acct'; | import * as Acct from 'misskey-js/built/acct'; | ||||||
|  | import * as os from '@/os'; | ||||||
|  | import { mainRouter } from '@/router'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	created() { | 	created() { | ||||||
|  | @ -17,17 +18,17 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 		if (acct.startsWith('https://')) { | 		if (acct.startsWith('https://')) { | ||||||
| 			promise = os.api('ap/show', { | 			promise = os.api('ap/show', { | ||||||
| 				uri: acct | 				uri: acct, | ||||||
| 			}); | 			}); | ||||||
| 			promise.then(res => { | 			promise.then(res => { | ||||||
| 				if (res.type === 'User') { | 				if (res.type === 'User') { | ||||||
| 					this.follow(res.object); | 					this.follow(res.object); | ||||||
| 				} else if (res.type === 'Note') { | 				} else if (res.type === 'Note') { | ||||||
| 					this.$router.push(`/notes/${res.object.id}`); | 					mainRouter.push(`/notes/${res.object.id}`); | ||||||
| 				} else { | 				} else { | ||||||
| 					os.alert({ | 					os.alert({ | ||||||
| 						type: 'error', | 						type: 'error', | ||||||
| 						text: 'Not a user' | 						text: 'Not a user', | ||||||
| 					}).then(() => { | 					}).then(() => { | ||||||
| 						window.close(); | 						window.close(); | ||||||
| 					}); | 					}); | ||||||
|  | @ -56,9 +57,9 @@ export default defineComponent({ | ||||||
| 			} | 			} | ||||||
| 			 | 			 | ||||||
| 			os.apiWithDialog('following/create', { | 			os.apiWithDialog('following/create', { | ||||||
| 				userId: user.id | 				userId: user.id, | ||||||
| 			}); | 			}); | ||||||
| 		} | 		}, | ||||||
| 	} | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -27,8 +27,8 @@ | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, inject, watch } from 'vue'; | ||||||
| import FormButton from '@/components/ui/button.vue'; | import FormButton from '@/components/ui/button.vue'; | ||||||
| import FormInput from '@/components/form/input.vue'; | import FormInput from '@/components/form/input.vue'; | ||||||
| import FormTextarea from '@/components/form/textarea.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 FormSuspense from '@/components/form/suspense.vue'; | ||||||
| import { selectFiles } from '@/scripts/select-file'; | import { selectFiles } from '@/scripts/select-file'; | ||||||
| import * as os from '@/os'; | 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({ | const router = useRouter(); | ||||||
| 	components: { |  | ||||||
| 		FormButton, |  | ||||||
| 		FormInput, |  | ||||||
| 		FormTextarea, |  | ||||||
| 		FormSwitch, |  | ||||||
| 		FormGroup, |  | ||||||
| 		FormSuspense, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	props: { | const props = defineProps<{ | ||||||
| 		postId: { | 	postId?: string; | ||||||
| 			type: String, | }>(); | ||||||
| 			required: false, |  | ||||||
| 			default: null, |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | let init = $ref(null); | ||||||
| 		return { | let files = $ref([]); | ||||||
| 			[symbols.PAGE_INFO]: computed(() => this.postId ? { | let description = $ref(null); | ||||||
| 				title: this.$ts.edit, | let title = $ref(null); | ||||||
| 				icon: 'fas fa-pencil-alt' | let isSensitive = $ref(false); | ||||||
| 			} : { |  | ||||||
| 				title: this.$ts.postToGallery, |  | ||||||
| 				icon: 'fas fa-pencil-alt' |  | ||||||
| 			}), |  | ||||||
| 			init: null, |  | ||||||
| 			files: [], |  | ||||||
| 			description: null, |  | ||||||
| 			title: null, |  | ||||||
| 			isSensitive: false, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	watch: { | function selectFile(evt) { | ||||||
| 		postId: { | 	selectFiles(evt.currentTarget ?? evt.target, null).then(selected => { | ||||||
| 			handler() { | 		files = files.concat(selected); | ||||||
| 				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, |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		selectFile(evt) { |  | ||||||
| 			selectFiles(evt.currentTarget ?? evt.target, null).then(files => { |  | ||||||
| 				this.files = this.files.concat(files); |  | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		remove(file) { | function remove(file) { | ||||||
| 			this.files = this.files.filter(f => f.id !== file.id); | 	files = files.filter(f => f.id !== file.id); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		async save() { | async function save() { | ||||||
| 			if (this.postId) { | 	if (props.postId) { | ||||||
| 		await os.apiWithDialog('gallery/posts/update', { | 		await os.apiWithDialog('gallery/posts/update', { | ||||||
| 					postId: this.postId, | 			postId: props.postId, | ||||||
| 					title: this.title, | 			title: title, | ||||||
| 					description: this.description, | 			description: description, | ||||||
| 					fileIds: this.files.map(file => file.id), | 			fileIds: files.map(file => file.id), | ||||||
| 					isSensitive: this.isSensitive, | 			isSensitive: isSensitive, | ||||||
| 		}); | 		}); | ||||||
| 				this.$router.push(`/gallery/${this.postId}`); | 		mainRouter.push(`/gallery/${props.postId}`); | ||||||
| 	} else { | 	} else { | ||||||
| 				const post = await os.apiWithDialog('gallery/posts/create', { | 		const created = await os.apiWithDialog('gallery/posts/create', { | ||||||
| 					title: this.title, | 			title: title, | ||||||
| 					description: this.description, | 			description: description, | ||||||
| 					fileIds: this.files.map(file => file.id), | 			fileIds: files.map(file => file.id), | ||||||
| 					isSensitive: this.isSensitive, | 			isSensitive: isSensitive, | ||||||
| 		}); | 		}); | ||||||
| 				this.$router.push(`/gallery/${post.id}`); | 		router.push(`/gallery/${created.id}`); | ||||||
| 	} | 	} | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		async del() { | async function del() { | ||||||
| 	const { canceled } = await os.confirm({ | 	const { canceled } = await os.confirm({ | ||||||
| 		type: 'warning', | 		type: 'warning', | ||||||
| 				text: this.$ts.deleteConfirm, | 		text: i18n.ts.deleteConfirm, | ||||||
| 	}); | 	}); | ||||||
| 	if (canceled) return; | 	if (canceled) return; | ||||||
| 	await os.apiWithDialog('gallery/posts/delete', { | 	await os.apiWithDialog('gallery/posts/delete', { | ||||||
| 				postId: this.postId, | 		postId: props.postId, | ||||||
| 	}); | 	}); | ||||||
| 			this.$router.push(`/gallery`); | 	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> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <div class="xprsixdl _root"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="1400"> | ||||||
|  | 		<div class="_root"> | ||||||
| 			<MkTab v-if="$i" v-model="tab"> | 			<MkTab v-if="$i" v-model="tab"> | ||||||
| 				<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option> | 				<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="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option> | ||||||
|  | @ -39,11 +42,13 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				</MkPagination> | 				</MkPagination> | ||||||
| 			</div> | 			</div> | ||||||
| </div> | 		</div> | ||||||
|  | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, defineComponent, watch } from 'vue'; | ||||||
| import XUserList from '@/components/user-list.vue'; | import XUserList from '@/components/user-list.vue'; | ||||||
| import MkFolder from '@/components/ui/folder.vue'; | import MkFolder from '@/components/ui/folder.vue'; | ||||||
| import MkInput from '@/components/form/input.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 MkGalleryPostPreview from '@/components/gallery-post-preview.vue'; | ||||||
| import number from '@/filters/number'; | import number from '@/filters/number'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = defineProps<{ | ||||||
| 	components: { | 	tag?: string; | ||||||
| 		XUserList, | }>(); | ||||||
| 		MkFolder, |  | ||||||
| 		MkInput, |  | ||||||
| 		MkButton, |  | ||||||
| 		MkTab, |  | ||||||
| 		MkPagination, |  | ||||||
| 		MkGalleryPostPreview, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	props: { | let tab = $ref('explore'); | ||||||
| 		tag: { | let tags = $ref([]); | ||||||
| 			type: String, | let tagsRef = $ref(); | ||||||
| 			required: false |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | const recentPostsPagination = { | ||||||
| 		return { |  | ||||||
| 			[symbols.PAGE_INFO]: { |  | ||||||
| 				title: this.$ts.gallery, |  | ||||||
| 				icon: 'fas fa-icons' |  | ||||||
| 			}, |  | ||||||
| 			tab: 'explore', |  | ||||||
| 			recentPostsPagination: { |  | ||||||
| 	endpoint: 'gallery/posts' as const, | 	endpoint: 'gallery/posts' as const, | ||||||
| 	limit: 6, | 	limit: 6, | ||||||
| 			}, | }; | ||||||
| 			popularPostsPagination: { | const popularPostsPagination = { | ||||||
| 	endpoint: 'gallery/featured' as const, | 	endpoint: 'gallery/featured' as const, | ||||||
| 	limit: 5, | 	limit: 5, | ||||||
| 			}, | }; | ||||||
| 			myPostsPagination: { | const myPostsPagination = { | ||||||
| 	endpoint: 'i/gallery/posts' as const, | 	endpoint: 'i/gallery/posts' as const, | ||||||
| 	limit: 5, | 	limit: 5, | ||||||
| 			}, | }; | ||||||
| 			likedPostsPagination: { | const likedPostsPagination = { | ||||||
| 	endpoint: 'i/gallery/likes' as const, | 	endpoint: 'i/gallery/likes' as const, | ||||||
| 	limit: 5, | 	limit: 5, | ||||||
| 			}, | }; | ||||||
| 			tags: [], |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	computed: { | const tagUsersPagination = $computed(() => ({ | ||||||
| 		meta() { |  | ||||||
| 			return this.$instance; |  | ||||||
| 		}, |  | ||||||
| 		tagUsers(): any { |  | ||||||
| 			return { |  | ||||||
| 	endpoint: 'hashtags/users' as const, | 	endpoint: 'hashtags/users' as const, | ||||||
| 	limit: 30, | 	limit: 30, | ||||||
| 	params: { | 	params: { | ||||||
| 		tag: this.tag, | 		tag: this.tag, | ||||||
| 		origin: 'combined', | 		origin: 'combined', | ||||||
| 		sort: '+follower', | 		sort: '+follower', | ||||||
| 				} |  | ||||||
| 			}; |  | ||||||
| 		}, |  | ||||||
| 	}, | 	}, | ||||||
|  | })); | ||||||
| 
 | 
 | ||||||
| 	watch: { | watch(() => props.tag, () => { | ||||||
| 		tag() { | 	if (tagsRef) tagsRef.tags.toggleContent(props.tag == null); | ||||||
| 			if (this.$refs.tags) this.$refs.tags.toggleContent(this.tag == null); | }); | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	created() { | const headerActions = $computed(() => []); | ||||||
| 
 | 
 | ||||||
| 	}, | const headerTabs = $computed(() => []); | ||||||
| 
 | 
 | ||||||
| 	methods: { | definePageMetadata({ | ||||||
| 
 | 	title: i18n.ts.gallery, | ||||||
| 	} | 	icon: 'fas fa-icons', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .xprsixdl { |  | ||||||
| 	max-width: 1400px; |  | ||||||
| 	margin: 0 auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .vfpdbgtk { | .vfpdbgtk { | ||||||
| 	display: grid; | 	display: grid; | ||||||
| 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); | 	grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); | ||||||
|  |  | ||||||
|  | @ -49,123 +49,108 @@ | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, defineComponent, inject, watch } from 'vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import MkContainer from '@/components/ui/container.vue'; | import MkContainer from '@/components/ui/container.vue'; | ||||||
| import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; | import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import MkGalleryPostPreview from '@/components/gallery-post-preview.vue'; | import MkGalleryPostPreview from '@/components/gallery-post-preview.vue'; | ||||||
| import MkFollowButton from '@/components/follow-button.vue'; | import MkFollowButton from '@/components/follow-button.vue'; | ||||||
| import { url } from '@/config'; | import { url } from '@/config'; | ||||||
|  | import { useRouter } from '@/router'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const router = useRouter(); | ||||||
| 	components: { | 
 | ||||||
| 		MkContainer, | const props = defineProps<{ | ||||||
| 		ImgWithBlurhash, | 	postId: string; | ||||||
| 		MkPagination, | }>(); | ||||||
| 		MkGalleryPostPreview, | 
 | ||||||
| 		MkButton, | const post = $ref(null); | ||||||
| 		MkFollowButton, | const error = $ref(null); | ||||||
| 	}, | const otherPostsPagination = { | ||||||
| 	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, | 	endpoint: 'users/gallery/posts' as const, | ||||||
| 	limit: 6, | 	limit: 6, | ||||||
| 	params: computed(() => ({ | 	params: computed(() => ({ | ||||||
| 					userId: this.post.user.id | 		userId: post.user.id, | ||||||
| 	})), | 	})), | ||||||
| 			}, | }; | ||||||
| 			post: null, |  | ||||||
| 			error: null, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	watch: { | function fetchPost() { | ||||||
| 		postId: 'fetch' | 	post = null; | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	created() { |  | ||||||
| 		this.fetch(); |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		fetch() { |  | ||||||
| 			this.post = null; |  | ||||||
| 	os.api('gallery/posts/show', { | 	os.api('gallery/posts/show', { | ||||||
| 				postId: this.postId | 		postId: props.postId, | ||||||
| 			}).then(post => { | 	}).then(_post => { | ||||||
| 				this.post = post; | 		post = _post; | ||||||
| 			}).catch(err => { | 	}).catch(_error => { | ||||||
| 				this.error = err; | 		error = _error; | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		share() { | function share() { | ||||||
| 	navigator.share({ | 	navigator.share({ | ||||||
| 				title: this.post.title, | 		title: post.title, | ||||||
| 				text: this.post.description, | 		text: post.description, | ||||||
| 				url: `${url}/gallery/${this.post.id}` | 		url: `${url}/gallery/${post.id}`, | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		shareWithNote() { | function shareWithNote() { | ||||||
| 	os.post({ | 	os.post({ | ||||||
| 				initialText: `${this.post.title} ${url}/gallery/${this.post.id}` | 		initialText: `${post.title} ${url}/gallery/${post.id}`, | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		like() { | function like() { | ||||||
| 	os.apiWithDialog('gallery/posts/like', { | 	os.apiWithDialog('gallery/posts/like', { | ||||||
| 				postId: this.postId, | 		postId: props.postId, | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
| 				this.post.isLiked = true; | 		post.isLiked = true; | ||||||
| 				this.post.likedCount++; | 		post.likedCount++; | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		async unlike() { | async function unlike() { | ||||||
| 	const confirm = await os.confirm({ | 	const confirm = await os.confirm({ | ||||||
| 		type: 'warning', | 		type: 'warning', | ||||||
| 				text: this.$ts.unlikeConfirm, | 		text: i18n.ts.unlikeConfirm, | ||||||
| 	}); | 	}); | ||||||
| 	if (confirm.canceled) return; | 	if (confirm.canceled) return; | ||||||
| 	os.apiWithDialog('gallery/posts/unlike', { | 	os.apiWithDialog('gallery/posts/unlike', { | ||||||
| 				postId: this.postId, | 		postId: props.postId, | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
| 				this.post.isLiked = false; | 		post.isLiked = false; | ||||||
| 				this.post.likedCount--; | 		post.likedCount--; | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		edit() { | function edit() { | ||||||
| 			this.$router.push(`/gallery/${this.post.id}/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, | ||||||
|  | 	}, | ||||||
|  | 	actions: [{ | ||||||
|  | 		icon: 'fas fa-pencil-alt', | ||||||
|  | 		text: i18n.ts.edit, | ||||||
|  | 		handler: edit, | ||||||
|  | 	}], | ||||||
|  | } : null)); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="600" :margin-min="16" :margin-max="32"> | 	<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 v-if="instance" class="_formRoot"> | ||||||
| 		<div class="fnfelxur"> | 		<div class="fnfelxur"> | ||||||
| 			<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/> | 			<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> | 			<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink> | ||||||
| 		</FormSection> | 		</FormSection> | ||||||
| 	</div> | 	</div> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -120,8 +121,8 @@ import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import number from '@/filters/number'; | import number from '@/filters/number'; | ||||||
| import bytes from '@/filters/bytes'; | import bytes from '@/filters/bytes'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { iAmModerator } from '@/account'; | import { iAmModerator } from '@/account'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	host: string; | 	host: string; | ||||||
|  | @ -146,7 +147,7 @@ async function fetch() { | ||||||
| async function toggleBlock(ev) { | async function toggleBlock(ev) { | ||||||
| 	if (meta == null) return; | 	if (meta == null) return; | ||||||
| 	await os.api('admin/update-meta', { | 	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,8 +169,11 @@ function refreshMetadata() { | ||||||
| 
 | 
 | ||||||
| fetch(); | fetch(); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: props.host, | 	title: props.host, | ||||||
| 	icon: 'fas fa-info-circle', | 	icon: 'fas fa-info-circle', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
|  | @ -178,9 +182,8 @@ defineExpose({ | ||||||
| 		icon: 'fas fa-external-link-alt', | 		icon: 'fas fa-external-link-alt', | ||||||
| 		handler: () => { | 		handler: () => { | ||||||
| 			window.open(`https://${props.host}`, '_blank'); | 			window.open(`https://${props.host}`, '_blank'); | ||||||
| 			} |  | ||||||
| 		}], |  | ||||||
| 		}, | 		}, | ||||||
|  | 	}], | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,24 +1,27 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="800"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="800"> | ||||||
| 	<XNotes :pagination="pagination"/> | 	<XNotes :pagination="pagination"/> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import XNotes from '@/components/notes.vue'; | import XNotes from '@/components/notes.vue'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const pagination = { | const pagination = { | ||||||
| 	endpoint: 'notes/mentions' as const, | 	endpoint: 'notes/mentions' as const, | ||||||
| 	limit: 10, | 	limit: 10, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.mentions, | 	title: i18n.ts.mentions, | ||||||
| 	icon: 'fas fa-at', | 	icon: 'fas fa-at', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,27 +1,30 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="800"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="800"> | ||||||
| 	<XNotes :pagination="pagination"/> | 	<XNotes :pagination="pagination"/> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import XNotes from '@/components/notes.vue'; | import XNotes from '@/components/notes.vue'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const pagination = { | const pagination = { | ||||||
| 	endpoint: 'notes/mentions' as const, | 	endpoint: 'notes/mentions' as const, | ||||||
| 	limit: 10, | 	limit: 10, | ||||||
| 	params: { | 	params: { | ||||||
| 		visibility: 'specified' | 		visibility: 'specified', | ||||||
| 	}, | 	}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.directNotes, | 	title: i18n.ts.directNotes, | ||||||
| 	icon: 'fas fa-envelope', | 	icon: 'fas fa-envelope', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,10 +1,13 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="800"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="800"> | ||||||
| 		<div v-size="{ max: [400] }" class="yweeujhr"> | 		<div v-size="{ max: [400] }" class="yweeujhr"> | ||||||
| 			<MkButton primary class="start" @click="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton> | 			<MkButton primary class="start" @click="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton> | ||||||
| 
 | 
 | ||||||
| 			<div v-if="messages.length > 0" class="history"> | 			<div v-if="messages.length > 0" class="history"> | ||||||
| 			<MkA v-for="(message, i) in messages" | 				<MkA | ||||||
|  | 					v-for="(message, i) in messages" | ||||||
| 					:key="message.id" | 					:key="message.id" | ||||||
| 					v-anim="i" | 					v-anim="i" | ||||||
| 					class="message _block" | 					class="message _block" | ||||||
|  | @ -35,131 +38,128 @@ | ||||||
| 			</div> | 			</div> | ||||||
| 			<MkLoading v-if="fetching"/> | 			<MkLoading v-if="fetching"/> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; | import { defineAsyncComponent, defineComponent, inject, markRaw, onMounted, onUnmounted } from 'vue'; | ||||||
| import * as Acct from 'misskey-js/built/acct'; | import * as Acct from 'misskey-js/built/acct'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import { acct } from '@/filters/user'; | import { acct } from '@/filters/user'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { stream } from '@/stream'; | 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({ | const router = useRouter(); | ||||||
| 	components: { |  | ||||||
| 		MkButton |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | let fetching = $ref(true); | ||||||
| 		return { | let moreFetching = $ref(false); | ||||||
| 			[symbols.PAGE_INFO]: { | let messages = $ref([]); | ||||||
| 				title: this.$ts.messaging, | let connection = $ref(null); | ||||||
| 				icon: 'fas fa-comments', |  | ||||||
| 				bg: 'var(--bg)', |  | ||||||
| 			}, |  | ||||||
| 			fetching: true, |  | ||||||
| 			moreFetching: false, |  | ||||||
| 			messages: [], |  | ||||||
| 			connection: null, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	mounted() { | const getAcct = Acct.toString; | ||||||
| 		this.connection = markRaw(stream.useChannel('messagingIndex')); |  | ||||||
| 
 | 
 | ||||||
| 		this.connection.on('message', this.onMessage); | function isMe(message) { | ||||||
| 		this.connection.on('read', this.onRead); | 	return message.userId === $i.id; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 		os.api('messaging/history', { group: false }).then(userMessages => { | function onMessage(message) { | ||||||
| 			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; |  | ||||||
| 			}); |  | ||||||
| 		}); |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	beforeUnmount() { |  | ||||||
| 		this.connection.dispose(); |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		getAcct: Acct.toString, |  | ||||||
| 
 |  | ||||||
| 		isMe(message) { |  | ||||||
| 			return message.userId === this.$i.id; |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onMessage(message) { |  | ||||||
| 	if (message.recipientId) { | 	if (message.recipientId) { | ||||||
| 				this.messages = this.messages.filter(m => !( | 		messages = messages.filter(m => !( | ||||||
| 			(m.recipientId === message.recipientId && m.userId === message.userId) || | 			(m.recipientId === message.recipientId && m.userId === message.userId) || | ||||||
| 			(m.recipientId === message.userId && m.userId === message.recipientId))); | 			(m.recipientId === message.userId && m.userId === message.recipientId))); | ||||||
| 
 | 
 | ||||||
| 				this.messages.unshift(message); | 		messages.unshift(message); | ||||||
| 	} else if (message.groupId) { | 	} else if (message.groupId) { | ||||||
| 				this.messages = this.messages.filter(m => m.groupId !== message.groupId); | 		messages = messages.filter(m => m.groupId !== message.groupId); | ||||||
| 				this.messages.unshift(message); | 		messages.unshift(message); | ||||||
| 	} | 	} | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		onRead(ids) { | function onRead(ids) { | ||||||
| 	for (const id of ids) { | 	for (const id of ids) { | ||||||
| 				const found = this.messages.find(m => m.id === id); | 		const found = messages.find(m => m.id === id); | ||||||
| 		if (found) { | 		if (found) { | ||||||
| 			if (found.recipientId) { | 			if (found.recipientId) { | ||||||
| 				found.isRead = true; | 				found.isRead = true; | ||||||
| 			} else if (found.groupId) { | 			} else if (found.groupId) { | ||||||
| 						found.reads.push(this.$i.id); | 				found.reads.push($i.id); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		start(ev) { | function start(ev) { | ||||||
| 	os.popupMenu([{ | 	os.popupMenu([{ | ||||||
| 				text: this.$ts.messagingWithUser, | 		text: i18n.ts.messagingWithUser, | ||||||
| 		icon: 'fas fa-user', | 		icon: 'fas fa-user', | ||||||
| 				action: () => { this.startUser(); } | 		action: () => { startUser(); }, | ||||||
| 	}, { | 	}, { | ||||||
| 				text: this.$ts.messagingWithGroup, | 		text: i18n.ts.messagingWithGroup, | ||||||
| 		icon: 'fas fa-users', | 		icon: 'fas fa-users', | ||||||
| 				action: () => { this.startGroup(); } | 		action: () => { startGroup(); }, | ||||||
| 	}], ev.currentTarget ?? ev.target); | 	}], ev.currentTarget ?? ev.target); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		async startUser() { | async function startUser() { | ||||||
| 	os.selectUser().then(user => { | 	os.selectUser().then(user => { | ||||||
| 				this.$router.push(`/my/messaging/${Acct.toString(user)}`); | 		router.push(`/my/messaging/${Acct.toString(user)}`); | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		async startGroup() { | async function startGroup() { | ||||||
| 	const groups1 = await os.api('users/groups/owned'); | 	const groups1 = await os.api('users/groups/owned'); | ||||||
| 	const groups2 = await os.api('users/groups/joined'); | 	const groups2 = await os.api('users/groups/joined'); | ||||||
| 	if (groups1.length === 0 && groups2.length === 0) { | 	if (groups1.length === 0 && groups2.length === 0) { | ||||||
| 		os.alert({ | 		os.alert({ | ||||||
| 			type: 'warning', | 			type: 'warning', | ||||||
| 					title: this.$ts.youHaveNoGroups, | 			title: i18n.ts.youHaveNoGroups, | ||||||
| 					text: this.$ts.joinOrCreateGroup, | 			text: i18n.ts.joinOrCreateGroup, | ||||||
| 		}); | 		}); | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
| 	const { canceled, result: group } = await os.select({ | 	const { canceled, result: group } = await os.select({ | ||||||
| 				title: this.$ts.group, | 		title: i18n.ts.group, | ||||||
| 		items: groups1.concat(groups2).map(group => ({ | 		items: groups1.concat(groups2).map(group => ({ | ||||||
| 					value: group, text: group.name | 			value: group, text: group.name, | ||||||
| 				})) | 		})), | ||||||
| 	}); | 	}); | ||||||
| 	if (canceled) return; | 	if (canceled) return; | ||||||
| 			this.$router.push(`/my/messaging/group/${group.id}`); | 	router.push(`/my/messaging/group/${group.id}`); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		acct | 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> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -61,10 +61,10 @@ import { isBottomVisible, onScrollBottom, scrollToBottom } from '@/scripts/scrol | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { stream } from '@/stream'; | import { stream } from '@/stream'; | ||||||
| import * as sound from '@/scripts/sound'; | import * as sound from '@/scripts/sound'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { $i } from '@/account'; | import { $i } from '@/account'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	userAcct?: string; | 	userAcct?: string; | ||||||
|  | @ -280,15 +280,13 @@ onBeforeUnmount(() => { | ||||||
| 	if (scrollRemove) scrollRemove(); | 	if (scrollRemove) scrollRemove(); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | definePageMetadata(computed(() => !fetching ? user ? { | ||||||
| 	[symbols.PAGE_INFO]: computed(() => !fetching ? user ? { |  | ||||||
| 	userName: user, | 	userName: user, | ||||||
| 	avatar: user, | 	avatar: user, | ||||||
| 	} : { | } : { | ||||||
| 	title: group?.name, | 	title: group?.name, | ||||||
| 	icon: 'fas fa-users', | 	icon: 'fas fa-users', | ||||||
| 	} : null), | } : null)); | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mwysmxbg"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader/></template> | ||||||
|  | 	<div class="mwysmxbg"> | ||||||
| 		<div class="_isolated">{{ $ts._mfm.intro }}</div> | 		<div class="_isolated">{{ $ts._mfm.intro }}</div> | ||||||
| 		<div class="section _block"> | 		<div class="section _block"> | ||||||
| 			<div class="title">{{ $ts._mfm.mention }}</div> | 			<div class="title">{{ $ts._mfm.mention }}</div> | ||||||
|  | @ -293,56 +295,50 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| </div> | 	</div> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkTextarea from '@/components/form/textarea.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({ | const preview_mention = '@example'; | ||||||
| 	components: { | const preview_hashtag = '#test'; | ||||||
| 		MkTextarea | 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() { | definePageMetadata({ | ||||||
| 		return { | 	title: i18n.ts._mfm.cheatSheet, | ||||||
| 			[symbols.PAGE_INFO]: { |  | ||||||
| 				title: this.$ts._mfm.cheatSheet, |  | ||||||
| 	icon: 'fas fa-question-circle', | 	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 🍮]`, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -49,28 +49,12 @@ export default defineComponent({ | ||||||
| 		MkSignin, | 		MkSignin, | ||||||
| 		MkButton, | 		MkButton, | ||||||
| 	}, | 	}, | ||||||
|  | 	props: ['session', 'callback', 'name', 'icon', 'permission'], | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		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: { | 	methods: { | ||||||
| 		async accept() { | 		async accept() { | ||||||
| 			this.state = 'waiting'; | 			this.state = 'waiting'; | ||||||
|  | @ -84,7 +68,7 @@ export default defineComponent({ | ||||||
| 			this.state = 'accepted'; | 			this.state = 'accepted'; | ||||||
| 			if (this.callback) { | 			if (this.callback) { | ||||||
| 				location.href = appendQuery(this.callback, query({ | 				location.href = appendQuery(this.callback, query({ | ||||||
| 					session: this.session | 					session: this.session, | ||||||
| 				})); | 				})); | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
|  | @ -93,8 +77,8 @@ export default defineComponent({ | ||||||
| 		}, | 		}, | ||||||
| 		onLogin(res) { | 		onLogin(res) { | ||||||
| 			login(res.i); | 			login(res.i); | ||||||
| 		} | 		}, | ||||||
| 	} | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,11 +5,13 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { inject } from 'vue'; | ||||||
| import XAntenna from './editor.vue'; | import XAntenna from './editor.vue'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { router } from '@/router'; | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
|  | import { useRouter } from '@/router'; | ||||||
|  | 
 | ||||||
|  | const router = useRouter(); | ||||||
| 
 | 
 | ||||||
| let draft = $ref({ | let draft = $ref({ | ||||||
| 	name: '', | 	name: '', | ||||||
|  | @ -22,19 +24,21 @@ let draft = $ref({ | ||||||
| 	withReplies: false, | 	withReplies: false, | ||||||
| 	caseSensitive: false, | 	caseSensitive: false, | ||||||
| 	withFile: false, | 	withFile: false, | ||||||
| 	notify: false | 	notify: false, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| function onAntennaCreated() { | function onAntennaCreated() { | ||||||
| 	router.push('/my/antennas'); | 	router.push('/my/antennas'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.manageAntennas, | 	title: i18n.ts.manageAntennas, | ||||||
| 	icon: 'fas fa-satellite', | 	icon: 'fas fa-satellite', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,14 +5,14 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { watch } from 'vue'; | import { inject, watch } from 'vue'; | ||||||
| import XAntenna from './editor.vue'; | import XAntenna from './editor.vue'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { MisskeyNavigator } from '@/scripts/navigate'; |  | ||||||
| import { i18n } from '@/i18n'; | 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); | let antenna: any = $ref(null); | ||||||
| 
 | 
 | ||||||
|  | @ -21,18 +21,20 @@ const props = defineProps<{ | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| function onAntennaUpdated() { | function onAntennaUpdated() { | ||||||
| 	nav.push('/my/antennas'); | 	router.push('/my/antennas'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => { | os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => { | ||||||
| 	antenna = antennaResponse; | 	antenna = antennaResponse; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.manageAntennas, | 	title: i18n.ts.manageAntennas, | ||||||
| 	icon: 'fas fa-satellite', | 	icon: 'fas fa-satellite', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="700"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="700"> | ||||||
| 	<div class="ieepwinx"> | 	<div class="ieepwinx"> | ||||||
| 		<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton> | 		<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> | 			</MkPagination> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const pagination = { | const pagination = { | ||||||
| 	endpoint: 'antennas/list' as const, | 	endpoint: 'antennas/list' as const, | ||||||
| 	limit: 10, | 	limit: 10, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.manageAntennas, | 	title: i18n.ts.manageAntennas, | ||||||
| 	icon: 'fas fa-satellite', | 	icon: 'fas fa-satellite', | ||||||
| 		bg: 'var(--bg)' | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="700"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="700"> | ||||||
| 	<div class="qtcaoidl"> | 	<div class="qtcaoidl"> | ||||||
| 		<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> | 		<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> | ||||||
| 
 | 
 | ||||||
|  | @ -10,7 +11,7 @@ | ||||||
| 			</MkA> | 			</MkA> | ||||||
| 		</MkPagination> | 		</MkPagination> | ||||||
| 	</div> | 	</div> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -18,8 +19,8 @@ import { } from 'vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const pagination = { | const pagination = { | ||||||
| 	endpoint: 'clips/list' as const, | 	endpoint: 'clips/list' as const, | ||||||
|  | @ -61,15 +62,17 @@ function onClipDeleted() { | ||||||
| 	pagingComponent.reload(); | 	pagingComponent.reload(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.clip, | 	title: i18n.ts.clip, | ||||||
| 	icon: 'fas fa-paperclip', | 	icon: 'fas fa-paperclip', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	action: { | 	action: { | ||||||
| 		icon: 'fas fa-plus', | 		icon: 'fas fa-plus', | ||||||
| 			handler: create | 		handler: create, | ||||||
| 		}, |  | ||||||
| 	}, | 	}, | ||||||
| }); | }); | ||||||
| </script> | </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> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="700"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="700"> | ||||||
| 	<div class="qkcjvfiv"> | 	<div class="qkcjvfiv"> | ||||||
| 		<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton> | 		<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton> | ||||||
| 
 | 
 | ||||||
|  | @ -10,7 +11,7 @@ | ||||||
| 			</MkA> | 			</MkA> | ||||||
| 		</MkPagination> | 		</MkPagination> | ||||||
| 	</div> | 	</div> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -19,8 +20,8 @@ import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkAvatars from '@/components/avatars.vue'; | import MkAvatars from '@/components/avatars.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); | const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); | ||||||
| 
 | 
 | ||||||
|  | @ -38,8 +39,11 @@ async function create() { | ||||||
| 	pagingComponent.reload(); | 	pagingComponent.reload(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.manageLists, | 	title: i18n.ts.manageLists, | ||||||
| 	icon: 'fas fa-list-ul', | 	icon: 'fas fa-list-ul', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
|  | @ -47,7 +51,6 @@ defineExpose({ | ||||||
| 		icon: 'fas fa-plus', | 		icon: 'fas fa-plus', | ||||||
| 		handler: create, | 		handler: create, | ||||||
| 	}, | 	}, | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="700"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="700"> | ||||||
| 	<div class="mk-list-page"> | 	<div class="mk-list-page"> | ||||||
| 		<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> | 		<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> | ||||||
| 			<div v-if="list" class="_section"> | 			<div v-if="list" class="_section"> | ||||||
|  | @ -31,104 +32,96 @@ | ||||||
| 			</div> | 			</div> | ||||||
| 		</transition> | 		</transition> | ||||||
| 	</div> | 	</div> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, defineComponent, watch } from 'vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; | import { mainRouter } from '@/router'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = defineProps<{ | ||||||
| 	components: { | 	listId: string; | ||||||
| 		MkButton | }>(); | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | let list = $ref(null); | ||||||
| 		return { | let users = $ref([]); | ||||||
| 			[symbols.PAGE_INFO]: computed(() => this.list ? { |  | ||||||
| 				title: this.list.name, |  | ||||||
| 				icon: 'fas fa-list-ul', |  | ||||||
| 				bg: 'var(--bg)', |  | ||||||
| 			} : null), |  | ||||||
| 			list: null, |  | ||||||
| 			users: [], |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	watch: { | function fetchList() { | ||||||
| 		$route: 'fetch' |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	created() { |  | ||||||
| 		this.fetch(); |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		fetch() { |  | ||||||
| 	os.api('users/lists/show', { | 	os.api('users/lists/show', { | ||||||
| 				listId: this.$route.params.list | 		listId: props.listId, | ||||||
| 			}).then(list => { | 	}).then(_list => { | ||||||
| 				this.list = list; | 		list = _list; | ||||||
| 		os.api('users/show', { | 		os.api('users/show', { | ||||||
| 					userIds: this.list.userIds | 			userIds: list.userIds, | ||||||
| 				}).then(users => { | 		}).then(_users => { | ||||||
| 					this.users = users; | 			users = _users; | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		addUser() { | function addUser() { | ||||||
| 	os.selectUser().then(user => { | 	os.selectUser().then(user => { | ||||||
| 		os.apiWithDialog('users/lists/push', { | 		os.apiWithDialog('users/lists/push', { | ||||||
| 					listId: this.list.id, | 			listId: list.id, | ||||||
| 					userId: user.id | 			userId: user.id, | ||||||
| 		}).then(() => { | 		}).then(() => { | ||||||
| 					this.users.push(user); | 			users.push(user); | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		removeUser(user) { | function removeUser(user) { | ||||||
| 	os.api('users/lists/pull', { | 	os.api('users/lists/pull', { | ||||||
| 				listId: this.list.id, | 		listId: list.id, | ||||||
| 				userId: user.id | 		userId: user.id, | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
| 				this.users = this.users.filter(x => x.id !== user.id); | 		users = users.filter(x => x.id !== user.id); | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		async renameList() { | async function renameList() { | ||||||
| 	const { canceled, result: name } = await os.inputText({ | 	const { canceled, result: name } = await os.inputText({ | ||||||
| 				title: this.$ts.enterListName, | 		title: i18n.ts.enterListName, | ||||||
| 				default: this.list.name | 		default: list.name, | ||||||
| 	}); | 	}); | ||||||
| 	if (canceled) return; | 	if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 	await os.api('users/lists/update', { | 	await os.api('users/lists/update', { | ||||||
| 				listId: this.list.id, | 		listId: list.id, | ||||||
| 				name: name | 		name: name, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 			this.list.name = name; | 	list.name = name; | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		async deleteList() { | async function deleteList() { | ||||||
| 	const { canceled } = await os.confirm({ | 	const { canceled } = await os.confirm({ | ||||||
| 		type: 'warning', | 		type: 'warning', | ||||||
| 				text: this.$t('removeAreYouSure', { x: this.list.name }), | 		text: i18n.t('removeAreYouSure', { x: list.name }), | ||||||
| 	}); | 	}); | ||||||
| 	if (canceled) return; | 	if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 	await os.api('users/lists/delete', { | 	await os.api('users/lists/delete', { | ||||||
| 				listId: this.list.id | 		listId: list.id, | ||||||
| 	}); | 	}); | ||||||
| 	os.success(); | 	os.success(); | ||||||
| 			this.$router.push('/my/lists'); | 	mainRouter.push('/my/lists'); | ||||||
| 		} | } | ||||||
| 	} | 
 | ||||||
| }); | watch(() => props.listId, fetchList, { immediate: true }); | ||||||
|  | 
 | ||||||
|  | const headerActions = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => list ? { | ||||||
|  | 	title: list.name, | ||||||
|  | 	icon: 'fas fa-list-ul', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
|  | } : null)); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -8,14 +8,16 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.notFound, | 	title: i18n.ts.notFound, | ||||||
| 	icon: 'fas fa-exclamation-triangle', | 	icon: 'fas fa-exclamation-triangle', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="800"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="800"> | ||||||
| 	<div class="fcuexfpr"> | 	<div class="fcuexfpr"> | ||||||
| 		<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> | 		<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> | ||||||
| 			<div v-if="note" class="note"> | 			<div v-if="note" class="note"> | ||||||
| 				<div v-if="showNext" class="_gap"> | 				<div v-if="showNext" class="_gap"> | ||||||
| 					<XNotes class="_content" :pagination="next" :no-gap="true"/> | 					<XNotes class="_content" :pagination="nextPagination" :no-gap="true"/> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<div class="main _gap"> | 				<div class="main _gap"> | ||||||
|  | @ -27,96 +28,69 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<div v-if="showPrev" class="_gap"> | 				<div v-if="showPrev" class="_gap"> | ||||||
| 					<XNotes class="_content" :pagination="prev" :no-gap="true"/> | 					<XNotes class="_content" :pagination="prevPagination" :no-gap="true"/> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 			<MkError v-else-if="error" @retry="fetch()"/> | 			<MkError v-else-if="error" @retry="fetch()"/> | ||||||
| 			<MkLoading v-else/> | 			<MkLoading v-else/> | ||||||
| 		</transition> | 		</transition> | ||||||
| 	</div> | 	</div> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, defineComponent, watch } from 'vue'; | ||||||
|  | import * as misskey from 'misskey-js'; | ||||||
| import XNote from '@/components/note.vue'; | import XNote from '@/components/note.vue'; | ||||||
| import XNoteDetailed from '@/components/note-detailed.vue'; | import XNoteDetailed from '@/components/note-detailed.vue'; | ||||||
| import XNotes from '@/components/notes.vue'; | import XNotes from '@/components/notes.vue'; | ||||||
| import MkRemoteCaution from '@/components/remote-caution.vue'; | import MkRemoteCaution from '@/components/remote-caution.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = defineProps<{ | ||||||
| 	components: { | 	noteId: string; | ||||||
| 		XNote, | }>(); | ||||||
| 		XNoteDetailed, | 
 | ||||||
| 		XNotes, | let note = $ref<null | misskey.entities.Note>(); | ||||||
| 		MkRemoteCaution, | let clips = $ref(); | ||||||
| 		MkButton, | let hasPrev = $ref(false); | ||||||
| 	}, | let hasNext = $ref(false); | ||||||
| 	props: { | let showPrev = $ref(false); | ||||||
| 		noteId: { | let showNext = $ref(false); | ||||||
| 			type: String, | let error = $ref(); | ||||||
| 			required: true | 
 | ||||||
| 		} | const prevPagination = { | ||||||
| 	}, |  | ||||||
| 	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, | 	endpoint: 'users/notes' as const, | ||||||
| 	limit: 10, | 	limit: 10, | ||||||
| 				params: computed(() => ({ | 	params: computed(() => note ? ({ | ||||||
| 					userId: this.note.userId, | 		userId: note.userId, | ||||||
| 					untilId: this.note.id, | 		untilId: note.id, | ||||||
| 				})), | 	}) : null), | ||||||
| 			}, | }; | ||||||
| 			next: { | 
 | ||||||
|  | const nextPagination = { | ||||||
| 	reversed: true, | 	reversed: true, | ||||||
| 	endpoint: 'users/notes' as const, | 	endpoint: 'users/notes' as const, | ||||||
| 	limit: 10, | 	limit: 10, | ||||||
| 				params: computed(() => ({ | 	params: computed(() => note ? ({ | ||||||
| 					userId: this.note.userId, | 		userId: note.userId, | ||||||
| 					sinceId: this.note.id, | 		sinceId: note.id, | ||||||
| 				})), | 	}) : null), | ||||||
| 			}, | }; | ||||||
| 		}; | 
 | ||||||
| 	}, | function fetchNote() { | ||||||
| 	watch: { | 	hasPrev = false; | ||||||
| 		noteId: 'fetch' | 	hasNext = false; | ||||||
| 	}, | 	showPrev = false; | ||||||
| 	created() { | 	showNext = false; | ||||||
| 		this.fetch(); | 	note = null; | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		fetch() { |  | ||||||
| 			this.hasPrev = false; |  | ||||||
| 			this.hasNext = false; |  | ||||||
| 			this.showPrev = false; |  | ||||||
| 			this.showNext = false; |  | ||||||
| 			this.note = null; |  | ||||||
| 	os.api('notes/show', { | 	os.api('notes/show', { | ||||||
| 				noteId: this.noteId | 		noteId: props.noteId, | ||||||
| 			}).then(note => { | 	}).then(res => { | ||||||
| 				this.note = note; | 		note = res; | ||||||
| 		Promise.all([ | 		Promise.all([ | ||||||
| 			os.api('notes/clips', { | 			os.api('notes/clips', { | ||||||
| 				noteId: note.id, | 				noteId: note.id, | ||||||
|  | @ -131,17 +105,35 @@ export default defineComponent({ | ||||||
| 				sinceId: note.id, | 				sinceId: note.id, | ||||||
| 				limit: 1, | 				limit: 1, | ||||||
| 			}), | 			}), | ||||||
| 				]).then(([clips, prev, next]) => { | 		]).then(([_clips, prev, next]) => { | ||||||
| 					this.clips = clips; | 			clips = _clips; | ||||||
| 					this.hasPrev = prev.length !== 0; | 			hasPrev = prev.length !== 0; | ||||||
| 					this.hasNext = next.length !== 0; | 			hasNext = next.length !== 0; | ||||||
| 		}); | 		}); | ||||||
| 	}).catch(err => { | 	}).catch(err => { | ||||||
| 				this.error = 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> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,18 +1,21 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="800"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="800"> | ||||||
| 		<div class="clupoqwt"> | 		<div class="clupoqwt"> | ||||||
| 			<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/> | 			<XNotifications class="notifications" :include-types="includeTypes" :unread-only="tab === 'unread'"/> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
|  | import { notificationTypes } from 'misskey-js'; | ||||||
| import XNotifications from '@/components/notifications.vue'; | import XNotifications from '@/components/notifications.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { notificationTypes } from 'misskey-js'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| let tab = $ref('all'); | let tab = $ref('all'); | ||||||
| let includeTypes = $ref<string[] | null>(null); | let includeTypes = $ref<string[] | null>(null); | ||||||
|  | @ -23,46 +26,46 @@ function setFilter(ev) { | ||||||
| 		active: includeTypes && includeTypes.includes(t), | 		active: includeTypes && includeTypes.includes(t), | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			includeTypes = [t]; | 			includeTypes = [t]; | ||||||
| 		} | 		}, | ||||||
| 	})); | 	})); | ||||||
| 	const items = includeTypes != null ? [{ | 	const items = includeTypes != null ? [{ | ||||||
| 		icon: 'fas fa-times', | 		icon: 'fas fa-times', | ||||||
| 		text: i18n.ts.clear, | 		text: i18n.ts.clear, | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			includeTypes = null; | 			includeTypes = null; | ||||||
| 		} | 		}, | ||||||
| 	}, null, ...typeItems] : typeItems; | 	}, null, ...typeItems] : typeItems; | ||||||
| 	os.popupMenu(items, ev.currentTarget ?? ev.target); | 	os.popupMenu(items, ev.currentTarget ?? ev.target); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => [{ | ||||||
| 	[symbols.PAGE_INFO]: computed(() => ({ |  | ||||||
| 		title: i18n.ts.notifications, |  | ||||||
| 		icon: 'fas fa-bell', |  | ||||||
| 		bg: 'var(--bg)', |  | ||||||
| 		actions: [{ |  | ||||||
| 	text: i18n.ts.filter, | 	text: i18n.ts.filter, | ||||||
| 	icon: 'fas fa-filter', | 	icon: 'fas fa-filter', | ||||||
| 	highlighted: includeTypes != null, | 	highlighted: includeTypes != null, | ||||||
| 	handler: setFilter, | 	handler: setFilter, | ||||||
| 		}, { | }, { | ||||||
| 	text: i18n.ts.markAllAsRead, | 	text: i18n.ts.markAllAsRead, | ||||||
| 	icon: 'fas fa-check', | 	icon: 'fas fa-check', | ||||||
| 	handler: () => { | 	handler: () => { | ||||||
| 		os.apiWithDialog('notifications/mark-all-as-read'); | 		os.apiWithDialog('notifications/mark-all-as-read'); | ||||||
| 	}, | 	}, | ||||||
| 		}], | }]); | ||||||
| 		tabs: [{ | 
 | ||||||
|  | const headerTabs = $computed(() => [{ | ||||||
| 	active: tab === 'all', | 	active: tab === 'all', | ||||||
| 	title: i18n.ts.all, | 	title: i18n.ts.all, | ||||||
| 	onClick: () => { tab = 'all'; }, | 	onClick: () => { tab = 'all'; }, | ||||||
| 		}, { | }, { | ||||||
| 	active: tab === 'unread', | 	active: tab === 'unread', | ||||||
| 	title: i18n.ts.unread, | 	title: i18n.ts.unread, | ||||||
| 	onClick: () => { tab = 'unread'; }, | 	onClick: () => { tab = 'unread'; }, | ||||||
| 		},] | }]); | ||||||
| 	})), | 
 | ||||||
| }); | definePageMetadata(computed(() => ({ | ||||||
|  | 	title: i18n.ts.notifications, | ||||||
|  | 	icon: 'fas fa-bell', | ||||||
|  | 	bg: 'var(--bg)', | ||||||
|  | }))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="700"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="700"> | ||||||
| 	<div class="jqqmcavi"> | 	<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="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> | 		<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"> | 			<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}"> | 				<template #item="{element}"> | ||||||
| 					<XVariable | 					<XVariable | ||||||
| 						:modelValue="element" | 						:model-value="element" | ||||||
| 						:removable="true" | 						:removable="true" | ||||||
| 						:hpml="hpml" | 						:hpml="hpml" | ||||||
| 						:name="element.name" | 						:name="element.name" | ||||||
|  | @ -75,11 +76,11 @@ | ||||||
| 			<MkTextarea v-model="script" class="_code"/> | 			<MkTextarea v-model="script" class="_code"/> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent, defineAsyncComponent, computed } from 'vue'; | import { defineComponent, defineAsyncComponent, computed, provide, watch } from 'vue'; | ||||||
| import 'prismjs'; | import 'prismjs'; | ||||||
| import { highlight, languages } from 'prismjs/components/prism-core'; | import { highlight, languages } from 'prismjs/components/prism-core'; | ||||||
| import 'prismjs/components/prism-clike'; | import 'prismjs/components/prism-clike'; | ||||||
|  | @ -101,315 +102,210 @@ import { url } from '@/config'; | ||||||
| import { collectPageVars } from '@/scripts/collect-page-vars'; | import { collectPageVars } from '@/scripts/collect-page-vars'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { selectFile } from '@/scripts/select-file'; | 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({ | const props = defineProps<{ | ||||||
| 	components: { | 	initPageId?: string; | ||||||
| 		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), | 	initPageName?: string; | ||||||
| 		XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput, | 	initUser?: string; | ||||||
| 	}, | }>(); | ||||||
| 
 | 
 | ||||||
| 	provide() { | let tab = $ref('settings'); | ||||||
| 		return { | let author = $ref($i); | ||||||
| 			readonly: this.readonly, | let readonly = $ref(false); | ||||||
| 			getScriptBlockList: this.getScriptBlockList, | let page = $ref(null); | ||||||
| 			getPageBlockList: this.getPageBlockList | 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: { | provide('readonly', readonly); | ||||||
| 		initPageId: { | provide('getScriptBlockList', getScriptBlockList); | ||||||
| 			type: String, | provide('getPageBlockList', getPageBlockList); | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		initPageName: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		initUser: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | watch($$(eyeCatchingImageId), async () => { | ||||||
| 		return { | 	if (eyeCatchingImageId == null) { | ||||||
| 			[symbols.PAGE_INFO]: computed(() => { | 		eyeCatchingImage = null; | ||||||
| 				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 { | 	} else { | ||||||
| 				this.eyeCatchingImage = await os.api('drive/files/show', { | 		eyeCatchingImage = await os.api('drive/files/show', { | ||||||
| 					fileId: this.eyeCatchingImageId, | 			fileId: eyeCatchingImageId, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 		}, | }); | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	async created() { | function getSaveOptions() { | ||||||
| 		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 { | 	return { | ||||||
| 				title: this.title.trim(), | 		title: tatitle.trim(), | ||||||
| 				name: this.name.trim(), | 		name: taname.trim(), | ||||||
| 				summary: this.summary, | 		summary: tasummary, | ||||||
| 				font: this.font, | 		font: tafont, | ||||||
| 				script: this.script, | 		script: tascript, | ||||||
| 				hideTitleWhenPinned: this.hideTitleWhenPinned, | 		hideTitleWhenPinned: tahideTitleWhenPinned, | ||||||
| 				alignCenter: this.alignCenter, | 		alignCenter: taalignCenter, | ||||||
| 				content: this.content, | 		content: tacontent, | ||||||
| 				variables: this.variables, | 		variables: tavariables, | ||||||
| 				eyeCatchingImageId: this.eyeCatchingImageId, | 		eyeCatchingImageId: taeyeCatchingImageId, | ||||||
| 	}; | 	}; | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		save() { | function save() { | ||||||
| 			const options = this.getSaveOptions(); | 	const options = tagetSaveOptions(); | ||||||
| 
 | 
 | ||||||
| 	const onError = err => { | 	const onError = err => { | ||||||
| 		if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') { | 		if (err.id == '3d81ceae-475f-4600-b2a8-2bc116157532') { | ||||||
| 			if (err.info.param == 'name') { | 			if (err.info.param == 'name') { | ||||||
| 				os.alert({ | 				os.alert({ | ||||||
| 					type: 'error', | 					type: 'error', | ||||||
| 							title: this.$ts._pages.invalidNameTitle, | 					title: i18n.ts._pages.invalidNameTitle, | ||||||
| 							text: this.$ts._pages.invalidNameText | 					text: i18n.ts._pages.invalidNameText, | ||||||
| 				}); | 				}); | ||||||
| 			} | 			} | ||||||
| 		} else if (err.code == 'NAME_ALREADY_EXISTS') { | 		} else if (err.code == 'NAME_ALREADY_EXISTS') { | ||||||
| 			os.alert({ | 			os.alert({ | ||||||
| 				type: 'error', | 				type: 'error', | ||||||
| 						text: this.$ts._pages.nameAlreadyExists | 				text: i18n.ts._pages.nameAlreadyExists, | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 			if (this.pageId) { | 	if (tapageId) { | ||||||
| 				options.pageId = this.pageId; | 		options.pageId = tapageId; | ||||||
| 		os.api('pages/update', options) | 		os.api('pages/update', options) | ||||||
| 		.then(page => { | 		.then(page => { | ||||||
| 					this.currentName = this.name.trim(); | 			tacurrentName = taname.trim(); | ||||||
| 			os.alert({ | 			os.alert({ | ||||||
| 				type: 'success', | 				type: 'success', | ||||||
| 						text: this.$ts._pages.updated | 				text: i18n.ts._pages.updated, | ||||||
| 			}); | 			}); | ||||||
| 		}).catch(onError); | 		}).catch(onError); | ||||||
| 	} else { | 	} else { | ||||||
| 		os.api('pages/create', options) | 		os.api('pages/create', options) | ||||||
| 				.then(page => { | 		.then(created => { | ||||||
| 					this.pageId = page.id; | 			tapageId = created.id; | ||||||
| 					this.currentName = this.name.trim(); | 			tacurrentName = name.trim(); | ||||||
| 			os.alert({ | 			os.alert({ | ||||||
| 				type: 'success', | 				type: 'success', | ||||||
| 						text: this.$ts._pages.created | 				text: i18n.ts._pages.created, | ||||||
| 			}); | 			}); | ||||||
| 					this.$router.push(`/pages/edit/${this.pageId}`); | 			mainRouter.push(`/pages/edit/${pageId}`); | ||||||
| 		}).catch(onError); | 		}).catch(onError); | ||||||
| 	} | 	} | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		del() { | function del() { | ||||||
| 	os.confirm({ | 	os.confirm({ | ||||||
| 		type: 'warning', | 		type: 'warning', | ||||||
| 				text: this.$t('removeAreYouSure', { x: this.title.trim() }), | 		text: i18n.t('removeAreYouSure', { x: title.trim() }), | ||||||
| 	}).then(({ canceled }) => { | 	}).then(({ canceled }) => { | ||||||
| 		if (canceled) return; | 		if (canceled) return; | ||||||
| 		os.api('pages/delete', { | 		os.api('pages/delete', { | ||||||
| 					pageId: this.pageId, | 			pageId: pageId, | ||||||
| 		}).then(() => { | 		}).then(() => { | ||||||
| 			os.alert({ | 			os.alert({ | ||||||
| 				type: 'success', | 				type: 'success', | ||||||
| 						text: this.$ts._pages.deleted | 				text: i18n.ts._pages.deleted, | ||||||
| 			}); | 			}); | ||||||
| 					this.$router.push(`/pages`); | 			mainRouter.push('/pages'); | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		duplicate() { | function duplicate() { | ||||||
| 			this.title = this.title + ' - copy'; | 	tatitle = tatitle + ' - copy'; | ||||||
| 			this.name = this.name + '-copy'; | 	taname = taname + '-copy'; | ||||||
| 			os.api('pages/create', this.getSaveOptions()).then(page => { | 	os.api('pages/create', tagetSaveOptions()).then(created => { | ||||||
| 				this.pageId = page.id; | 		tapageId = created.id; | ||||||
| 				this.currentName = this.name.trim(); | 		tacurrentName = taname.trim(); | ||||||
| 		os.alert({ | 		os.alert({ | ||||||
| 			type: 'success', | 			type: 'success', | ||||||
| 					text: this.$ts._pages.created | 			text: i18n.ts._pages.created, | ||||||
| 		}); | 		}); | ||||||
| 				this.$router.push(`/pages/edit/${this.pageId}`); | 		mainRouter.push(`/pages/edit/${pageId}`); | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		async add() { | async function add() { | ||||||
| 	const { canceled, result: type } = await os.select({ | 	const { canceled, result: type } = await os.select({ | ||||||
| 		type: null, | 		type: null, | ||||||
| 				title: this.$ts._pages.chooseBlock, | 		title: i18n.ts._pages.chooseBlock, | ||||||
| 				groupedItems: this.getPageBlockList() | 		groupedItems: tagetPageBlockList(), | ||||||
| 	}); | 	}); | ||||||
| 	if (canceled) return; | 	if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 	const id = uuid(); | 	const id = uuid(); | ||||||
| 			this.content.push({ id, type }); | 	tacontent.push({ id, type }); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		async addVariable() { | async function addVariable() { | ||||||
| 	let { canceled, result: name } = await os.inputText({ | 	let { canceled, result: name } = await os.inputText({ | ||||||
| 				title: this.$ts._pages.enterVariableName, | 		title: i18n.ts._pages.enterVariableName, | ||||||
| 	}); | 	}); | ||||||
| 	if (canceled) return; | 	if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 	name = name.trim(); | 	name = name.trim(); | ||||||
| 
 | 
 | ||||||
| 			if (this.hpml.isUsedName(name)) { | 	if (tahpml.isUsedName(name)) { | ||||||
| 		os.alert({ | 		os.alert({ | ||||||
| 			type: 'error', | 			type: 'error', | ||||||
| 					text: this.$ts._pages.variableNameIsAlreadyUsed | 			text: i18n.ts._pages.variableNameIsAlreadyUsed, | ||||||
| 		}); | 		}); | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	const id = uuid(); | 	const id = uuid(); | ||||||
| 			this.variables.push({ id, name, type: null }); | 	tavariables.push({ id, name, type: null }); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		removeVariable(v) { | function removeVariable(v) { | ||||||
| 			this.variables = this.variables.filter(x => x.name !== v.name); | 	tavariables = tavariables.filter(x => x.name !== v.name); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		getPageBlockList() { | function getPageBlockList() { | ||||||
| 	return [{ | 	return [{ | ||||||
| 				label: this.$ts._pages.contentBlocks, | 		label: i18n.ts._pages.contentBlocks, | ||||||
| 		items: [ | 		items: [ | ||||||
| 					{ value: 'section', text: this.$ts._pages.blocks.section }, | 			{ value: 'section', text: i18n.ts._pages.blocks.section }, | ||||||
| 					{ value: 'text', text: this.$ts._pages.blocks.text }, | 			{ value: 'text', text: i18n.ts._pages.blocks.text }, | ||||||
| 					{ value: 'image', text: this.$ts._pages.blocks.image }, | 			{ value: 'image', text: i18n.ts._pages.blocks.image }, | ||||||
| 					{ value: 'textarea', text: this.$ts._pages.blocks.textarea }, | 			{ value: 'textarea', text: i18n.ts._pages.blocks.textarea }, | ||||||
| 					{ value: 'note', text: this.$ts._pages.blocks.note }, | 			{ value: 'note', text: i18n.ts._pages.blocks.note }, | ||||||
| 					{ value: 'canvas', text: this.$ts._pages.blocks.canvas }, | 			{ value: 'canvas', text: i18n.ts._pages.blocks.canvas }, | ||||||
| 				] | 		], | ||||||
| 	}, { | 	}, { | ||||||
| 				label: this.$ts._pages.inputBlocks, | 		label: i18n.ts._pages.inputBlocks, | ||||||
| 		items: [ | 		items: [ | ||||||
| 					{ value: 'button', text: this.$ts._pages.blocks.button }, | 			{ value: 'button', text: i18n.ts._pages.blocks.button }, | ||||||
| 					{ value: 'radioButton', text: this.$ts._pages.blocks.radioButton }, | 			{ value: 'radioButton', text: i18n.ts._pages.blocks.radioButton }, | ||||||
| 					{ value: 'textInput', text: this.$ts._pages.blocks.textInput }, | 			{ value: 'textInput', text: i18n.ts._pages.blocks.textInput }, | ||||||
| 					{ value: 'textareaInput', text: this.$ts._pages.blocks.textareaInput }, | 			{ value: 'textareaInput', text: i18n.ts._pages.blocks.textareaInput }, | ||||||
| 					{ value: 'numberInput', text: this.$ts._pages.blocks.numberInput }, | 			{ value: 'numberInput', text: i18n.ts._pages.blocks.numberInput }, | ||||||
| 					{ value: 'switch', text: this.$ts._pages.blocks.switch }, | 			{ value: 'switch', text: i18n.ts._pages.blocks.switch }, | ||||||
| 					{ value: 'counter', text: this.$ts._pages.blocks.counter } | 			{ value: 'counter', text: i18n.ts._pages.blocks.counter }, | ||||||
| 				] | 		], | ||||||
| 	}, { | 	}, { | ||||||
| 				label: this.$ts._pages.specialBlocks, | 		label: i18n.ts._pages.specialBlocks, | ||||||
| 		items: [ | 		items: [ | ||||||
| 					{ value: 'if', text: this.$ts._pages.blocks.if }, | 			{ value: 'if', text: i18n.ts._pages.blocks.if }, | ||||||
| 					{ value: 'post', text: this.$ts._pages.blocks.post } | 			{ value: 'post', text: i18n.ts._pages.blocks.post }, | ||||||
| 				] | 		], | ||||||
| 	}]; | 	}]; | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		getScriptBlockList(type: string = null) { | function getScriptBlockList(type: string = null) { | ||||||
| 	const list = []; | 	const list = []; | ||||||
| 
 | 
 | ||||||
| 	const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number'); | 	const blocks = blockDefs.filter(block => type === null || block.out === null || block.out === type || typeof block.out === 'number'); | ||||||
|  | @ -419,49 +315,136 @@ export default defineComponent({ | ||||||
| 		if (category) { | 		if (category) { | ||||||
| 			category.items.push({ | 			category.items.push({ | ||||||
| 				value: block.type, | 				value: block.type, | ||||||
| 						text: this.$t(`_pages.script.blocks.${block.type}`) | 				text: i18n.t(`_pages.script.blocks.${block.type}`), | ||||||
| 			}); | 			}); | ||||||
| 		} else { | 		} else { | ||||||
| 			list.push({ | 			list.push({ | ||||||
| 				category: block.category, | 				category: block.category, | ||||||
| 						label: this.$t(`_pages.script.categories.${block.category}`), | 				label: i18n.t(`_pages.script.categories.${block.category}`), | ||||||
| 				items: [{ | 				items: [{ | ||||||
| 					value: block.type, | 					value: block.type, | ||||||
| 							text: this.$t(`_pages.script.blocks.${block.type}`) | 					text: i18n.t(`_pages.script.blocks.${block.type}`), | ||||||
| 						}] | 				}], | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 			const userFns = this.variables.filter(x => x.type === 'fn'); | 	const userFns = variables.filter(x => x.type === 'fn'); | ||||||
| 	if (userFns.length > 0) { | 	if (userFns.length > 0) { | ||||||
| 		list.unshift({ | 		list.unshift({ | ||||||
| 					label: this.$t(`_pages.script.categories.fn`), | 			label: i18n.t('_pages.script.categories.fn'), | ||||||
| 			items: userFns.map(v => ({ | 			items: userFns.map(v => ({ | ||||||
| 				value: 'fn:' + v.name, | 				value: 'fn:' + v.name, | ||||||
| 						text: v.name | 				text: v.name, | ||||||
| 					})) | 			})), | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return list; | 	return list; | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		setEyeCatchingImage(e) { | function setEyeCatchingImage(e) { | ||||||
| 	selectFile(e.currentTarget ?? e.target, null).then(file => { | 	selectFile(e.currentTarget ?? e.target, null).then(file => { | ||||||
| 				this.eyeCatchingImageId = file.id; | 		eyeCatchingImageId = file.id; | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		removeEyeCatchingImage() { | function removeEyeCatchingImage() { | ||||||
| 			this.eyeCatchingImageId = null; | 	taeyeCatchingImageId = null; | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		highlighter(code) { | function highlighter(code) { | ||||||
| 	return highlight(code, languages.js, 'javascript'); | 	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> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| <template> | <template><MkStickyContainer> | ||||||
| <MkSpacer :content-max="700"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 		<MkSpacer :content-max="700"> | ||||||
| 	<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> | 	<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> | ||||||
| 		<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh"> | 		<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh"> | ||||||
| 			<div class="_block main"> | 			<div class="_block main"> | ||||||
|  | @ -56,138 +57,108 @@ | ||||||
| 		<MkError v-else-if="error" @retry="fetch()"/> | 		<MkError v-else-if="error" @retry="fetch()"/> | ||||||
| 		<MkLoading v-else/> | 		<MkLoading v-else/> | ||||||
| 	</transition> | 	</transition> | ||||||
| </MkSpacer> | </MkSpacer></MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, watch } from 'vue'; | ||||||
| import XPage from '@/components/page/page.vue'; | import XPage from '@/components/page/page.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { url } from '@/config'; | import { url } from '@/config'; | ||||||
| import MkFollowButton from '@/components/follow-button.vue'; | import MkFollowButton from '@/components/follow-button.vue'; | ||||||
| import MkContainer from '@/components/ui/container.vue'; | import MkContainer from '@/components/ui/container.vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import MkPagePreview from '@/components/page-preview.vue'; | import MkPagePreview from '@/components/page-preview.vue'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = defineProps<{ | ||||||
| 	components: { | 	pageName: string; | ||||||
| 		XPage, | 	username: string; | ||||||
| 		MkButton, | }>(); | ||||||
| 		MkFollowButton, |  | ||||||
| 		MkContainer, |  | ||||||
| 		MkPagination, |  | ||||||
| 		MkPagePreview, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	props: { | let page = $ref(null); | ||||||
| 		pageName: { | let error = $ref(null); | ||||||
| 			type: String, | const otherPostsPagination = { | ||||||
| 			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, | 	endpoint: 'users/pages' as const, | ||||||
| 	limit: 6, | 	limit: 6, | ||||||
| 	params: computed(() => ({ | 	params: computed(() => ({ | ||||||
| 					userId: this.page.user.id | 		userId: page.user.id, | ||||||
| 	})), | 	})), | ||||||
| 			}, | }; | ||||||
| 		}; | const path = $computed(() => props.username + '/' + props.pageName); | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	computed: { | function fetchPage() { | ||||||
| 		path(): string { | 	page = null; | ||||||
| 			return this.username + '/' + this.pageName; |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	watch: { |  | ||||||
| 		path() { |  | ||||||
| 			this.fetch(); |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	created() { |  | ||||||
| 		this.fetch(); |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		fetch() { |  | ||||||
| 			this.page = null; |  | ||||||
| 	os.api('pages/show', { | 	os.api('pages/show', { | ||||||
| 				name: this.pageName, | 		name: props.pageName, | ||||||
| 				username: this.username, | 		username: props.username, | ||||||
| 			}).then(page => { | 	}).then(_page => { | ||||||
| 				this.page = page; | 		page = _page; | ||||||
| 	}).catch(err => { | 	}).catch(err => { | ||||||
| 				this.error = err; | 		error = err; | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		share() { | function share() { | ||||||
| 	navigator.share({ | 	navigator.share({ | ||||||
| 				title: this.page.title || this.page.name, | 		title: page.title ?? page.name, | ||||||
| 				text: this.page.summary, | 		text: page.summary, | ||||||
| 				url: `${url}/@${this.page.user.username}/pages/${this.page.name}` | 		url: `${url}/@${page.user.username}/pages/${page.name}`, | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		shareWithNote() { | function shareWithNote() { | ||||||
| 	os.post({ | 	os.post({ | ||||||
| 				initialText: `${this.page.title || this.page.name} ${url}/@${this.page.user.username}/pages/${this.page.name}` | 		initialText: `${page.title || page.name} ${url}/@${page.user.username}/pages/${page.name}`, | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		like() { | function like() { | ||||||
| 	os.apiWithDialog('pages/like', { | 	os.apiWithDialog('pages/like', { | ||||||
| 				pageId: this.page.id, | 		pageId: page.id, | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
| 				this.page.isLiked = true; | 		page.isLiked = true; | ||||||
| 				this.page.likedCount++; | 		page.likedCount++; | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		async unlike() { | async function unlike() { | ||||||
| 	const confirm = await os.confirm({ | 	const confirm = await os.confirm({ | ||||||
| 		type: 'warning', | 		type: 'warning', | ||||||
| 				text: this.$ts.unlikeConfirm, | 		text: i18n.ts.unlikeConfirm, | ||||||
| 	}); | 	}); | ||||||
| 	if (confirm.canceled) return; | 	if (confirm.canceled) return; | ||||||
| 	os.apiWithDialog('pages/unlike', { | 	os.apiWithDialog('pages/unlike', { | ||||||
| 				pageId: this.page.id, | 		pageId: page.id, | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
| 				this.page.isLiked = false; | 		page.isLiked = false; | ||||||
| 				this.page.likedCount--; | 		page.likedCount--; | ||||||
| 	}); | 	}); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		pin(pin) { | function pin(pin) { | ||||||
| 	os.apiWithDialog('i/update', { | 	os.apiWithDialog('i/update', { | ||||||
| 				pinnedPageId: pin ? this.page.id : null, | 		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, | ||||||
|  | 	}, | ||||||
|  | } : null)); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="700"> | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="700"> | ||||||
| 		<div v-if="tab === 'featured'" class="rknalgpo"> | 		<div v-if="tab === 'featured'" class="rknalgpo"> | ||||||
| 			<MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> | 			<MkPagination v-slot="{items}" :pagination="featuredPagesPagination"> | ||||||
| 				<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/> | 				<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/> | ||||||
|  | @ -18,69 +20,68 @@ | ||||||
| 				<MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/> | 				<MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/> | ||||||
| 			</MkPagination> | 			</MkPagination> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { computed, defineComponent } from 'vue'; | import { computed, inject } from 'vue'; | ||||||
| import MkPagePreview from '@/components/page-preview.vue'; | import MkPagePreview from '@/components/page-preview.vue'; | ||||||
| import MkPagination from '@/components/ui/pagination.vue'; | import MkPagination from '@/components/ui/pagination.vue'; | ||||||
| import MkButton from '@/components/ui/button.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({ | const router = useRouter(); | ||||||
| 	components: { | 
 | ||||||
| 		MkPagePreview, MkPagination, MkButton | let tab = $ref('featured'); | ||||||
| 	}, | 
 | ||||||
| 	data() { | const featuredPagesPagination = { | ||||||
| 		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, | 	endpoint: 'pages/featured' as const, | ||||||
| 	noPaging: true, | 	noPaging: true, | ||||||
| 			}, | }; | ||||||
| 			myPagesPagination: { | const myPagesPagination = { | ||||||
| 	endpoint: 'i/pages' as const, | 	endpoint: 'i/pages' as const, | ||||||
| 	limit: 5, | 	limit: 5, | ||||||
| 			}, | }; | ||||||
| 			likedPagesPagination: { | const likedPagesPagination = { | ||||||
| 	endpoint: 'i/page-likes' as const, | 	endpoint: 'i/page-likes' as const, | ||||||
| 	limit: 5, | 	limit: 5, | ||||||
| 			}, | }; | ||||||
| 		}; | 
 | ||||||
| 	}, | function create() { | ||||||
| 	methods: { | 	router.push('/pages/new'); | ||||||
| 		create() { | } | ||||||
| 			this.$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> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -7,16 +7,18 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| import MkSample from '@/components/sample.vue'; | import MkSample from '@/components/sample.vue'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => ({ | ||||||
| 	title: i18n.ts.preview, | 	title: i18n.ts.preview, | ||||||
| 	icon: 'fas fa-eye', | 	icon: 'fas fa-eye', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	})), | }))); | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32"> | <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"> | 		<div class="_formRoot"> | ||||||
| 			<FormInput v-model="password" type="password" class="_formBlock"> | 			<FormInput v-model="password" type="password" class="_formBlock"> | ||||||
| 				<template #prefix><i class="fas fa-lock"></i></template> | 				<template #prefix><i class="fas fa-lock"></i></template> | ||||||
|  | @ -8,7 +10,8 @@ | ||||||
| 		 | 		 | ||||||
| 			<FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton> | 			<FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -16,9 +19,9 @@ import { defineAsyncComponent, onMounted } from 'vue'; | ||||||
| import FormInput from '@/components/form/input.vue'; | import FormInput from '@/components/form/input.vue'; | ||||||
| import FormButton from '@/components/ui/button.vue'; | import FormButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { router } from '@/router'; | import { mainRouter } from '@/router'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	token?: string; | 	token?: string; | ||||||
|  | @ -31,22 +34,24 @@ async function save() { | ||||||
| 		token: props.token, | 		token: props.token, | ||||||
| 		password: password, | 		password: password, | ||||||
| 	}); | 	}); | ||||||
| 	router.push('/'); | 	mainRouter.push('/'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
| 	if (props.token == null) { | 	if (props.token == null) { | ||||||
| 		os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {}, 'closed'); | 		os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {}, 'closed'); | ||||||
| 		router.push('/'); | 		mainRouter.push('/'); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.resetPassword, | 	title: i18n.ts.resetPassword, | ||||||
| 	icon: 'fas fa-lock', | 	icon: 'fas fa-lock', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { defineExpose, ref, watch } from 'vue'; | import { ref, watch } from 'vue'; | ||||||
| import 'prismjs'; | import 'prismjs'; | ||||||
| import { highlight, languages } from 'prismjs/components/prism-core'; | import { highlight, languages } from 'prismjs/components/prism-core'; | ||||||
| import 'prismjs/components/prism-clike'; | import 'prismjs/components/prism-clike'; | ||||||
|  | @ -32,9 +32,9 @@ import MkContainer from '@/components/ui/container.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import { createAiScriptEnv } from '@/scripts/aiscript/api'; | import { createAiScriptEnv } from '@/scripts/aiscript/api'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { $i } from '@/account'; | import { $i } from '@/account'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const code = ref(''); | const code = ref(''); | ||||||
| const logs = ref<any[]>([]); | const logs = ref<any[]>([]); | ||||||
|  | @ -67,7 +67,7 @@ async function run() { | ||||||
| 			logs.value.push({ | 			logs.value.push({ | ||||||
| 				id: Math.random(), | 				id: Math.random(), | ||||||
| 				text: value.type === 'str' ? value.value : utils.valToString(value), | 				text: value.type === 'str' ? value.value : utils.valToString(value), | ||||||
| 				print: true | 				print: true, | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 		log: (type, params) => { | 		log: (type, params) => { | ||||||
|  | @ -75,11 +75,11 @@ async function run() { | ||||||
| 				case 'end': logs.value.push({ | 				case 'end': logs.value.push({ | ||||||
| 					id: Math.random(), | 					id: Math.random(), | ||||||
| 					text: utils.valToString(params.val, true), | 					text: utils.valToString(params.val, true), | ||||||
| 					print: false | 					print: false, | ||||||
| 				}); break; | 				}); break; | ||||||
| 				default: break; | 				default: break; | ||||||
| 			} | 			} | ||||||
| 		} | 		}, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	let ast; | 	let ast; | ||||||
|  | @ -88,7 +88,7 @@ async function run() { | ||||||
| 	} catch (error) { | 	} catch (error) { | ||||||
| 		os.alert({ | 		os.alert({ | ||||||
| 			type: 'error', | 			type: 'error', | ||||||
| 			text: 'Syntax error :(' | 			text: 'Syntax error :(', | ||||||
| 		}); | 		}); | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
|  | @ -97,7 +97,7 @@ async function run() { | ||||||
| 	} catch (error: any) { | 	} catch (error: any) { | ||||||
| 		os.alert({ | 		os.alert({ | ||||||
| 			type: 'error', | 			type: 'error', | ||||||
| 			text: error.message | 			text: error.message, | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -106,11 +106,13 @@ function highlighter(code) { | ||||||
| 	return highlight(code, languages.js, 'javascript'); | 	return highlight(code, languages.js, 'javascript'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.scratchpad, | 	title: i18n.ts.scratchpad, | ||||||
| 	icon: 'fas fa-terminal', | 	icon: 'fas fa-terminal', | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,16 +1,17 @@ | ||||||
| <template> | <template> | ||||||
| <div class="_section"> | <MkStickyContainer> | ||||||
| 	<div class="_content"> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
|  | 	<MkSpacer :content-max="800"> | ||||||
| 		<XNotes ref="notes" :pagination="pagination"/> | 		<XNotes ref="notes" :pagination="pagination"/> | ||||||
| 	</div> | 	</MkSpacer> | ||||||
| </div> | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| import XNotes from '@/components/notes.vue'; | import XNotes from '@/components/notes.vue'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	query: string; | 	query: string; | ||||||
|  | @ -23,14 +24,16 @@ const pagination = { | ||||||
| 	params: computed(() => ({ | 	params: computed(() => ({ | ||||||
| 		query: props.query, | 		query: props.query, | ||||||
| 		channelId: props.channel, | 		channelId: props.channel, | ||||||
| 	})) | 	})), | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => ({ | ||||||
| 	title: i18n.t('searchWith', { q: props.query }), | 	title: i18n.t('searchWith', { q: props.query }), | ||||||
| 	icon: 'fas fa-search', | 	icon: 'fas fa-search', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	})), | }))); | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -127,30 +127,32 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { defineExpose, onMounted, ref } from 'vue'; | import { onMounted, ref } from 'vue'; | ||||||
| import FormSection from '@/components/form/section.vue'; | import FormSection from '@/components/form/section.vue'; | ||||||
| import MkKeyValue from '@/components/key-value.vue'; | import MkKeyValue from '@/components/key-value.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import number from '@/filters/number'; | import number from '@/filters/number'; | ||||||
| import bytes from '@/filters/bytes'; | import bytes from '@/filters/bytes'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { $i } from '@/account'; | import { $i } from '@/account'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const stats = ref<any>({}); | const stats = ref<any>({}); | ||||||
| 
 | 
 | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
| 	os.api('users/stats', { | 	os.api('users/stats', { | ||||||
| 		userId: $i!.id | 		userId: $i!.id, | ||||||
| 	}).then(response => { | 	}).then(response => { | ||||||
| 		stats.value = response; | 		stats.value = response; | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.accountInfo, | 	title: i18n.ts.accountInfo, | ||||||
| 		icon: 'fas fa-info-circle' | 	icon: 'fas fa-info-circle', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -21,13 +21,13 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { defineAsyncComponent, defineExpose, ref } from 'vue'; | import { defineAsyncComponent, ref } from 'vue'; | ||||||
| import FormSuspense from '@/components/form/suspense.vue'; | import FormSuspense from '@/components/form/suspense.vue'; | ||||||
| import FormButton from '@/components/ui/button.vue'; | import FormButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; | import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account'; | ||||||
| import { getAccounts, addAccount as addAccounts, login, $i } from '@/account'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const storedAccounts = ref<any>(null); | const storedAccounts = ref<any>(null); | ||||||
| const accounts = ref<any>(null); | const accounts = ref<any>(null); | ||||||
|  | @ -39,7 +39,7 @@ const init = async () => { | ||||||
| 		console.log(storedAccounts.value); | 		console.log(storedAccounts.value); | ||||||
| 
 | 
 | ||||||
| 		return os.api('users/show', { | 		return os.api('users/show', { | ||||||
| 			userIds: storedAccounts.value.map(x => x.id) | 			userIds: storedAccounts.value.map(x => x.id), | ||||||
| 		}); | 		}); | ||||||
| 	}).then(response => { | 	}).then(response => { | ||||||
| 		accounts.value = response; | 		accounts.value = response; | ||||||
|  | @ -70,6 +70,10 @@ function addAccount(ev) { | ||||||
| 	}], ev.currentTarget ?? ev.target); | 	}], ev.currentTarget ?? ev.target); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function removeAccount(account) { | ||||||
|  | 	_removeAccount(account.id); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function addExistingAccount() { | function addExistingAccount() { | ||||||
| 	os.popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, { | 	os.popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, { | ||||||
| 		done: res => { | 		done: res => { | ||||||
|  | @ -98,12 +102,14 @@ function switchAccountWithToken(token: string) { | ||||||
| 	login(token); | 	login(token); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.accounts, | 	title: i18n.ts.accounts, | ||||||
| 	icon: 'fas fa-users', | 	icon: 'fas fa-users', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,12 +7,12 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { defineAsyncComponent, defineExpose, ref } from 'vue'; | import { defineAsyncComponent, ref } from 'vue'; | ||||||
| import FormLink from '@/components/form/link.vue'; | import FormLink from '@/components/form/link.vue'; | ||||||
| import FormButton from '@/components/ui/button.vue'; | import FormButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const isDesktop = ref(window.innerWidth >= 1100); | const isDesktop = ref(window.innerWidth >= 1100); | ||||||
| 
 | 
 | ||||||
|  | @ -29,17 +29,19 @@ function generateToken() { | ||||||
| 			os.alert({ | 			os.alert({ | ||||||
| 				type: 'success', | 				type: 'success', | ||||||
| 				title: i18n.ts.token, | 				title: i18n.ts.token, | ||||||
| 				text: token | 				text: token, | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 	}, 'closed'); | 	}, 'closed'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: 'API', | 	title: 'API', | ||||||
| 	icon: 'fas fa-key', | 	icon: 'fas fa-key', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ | ||||||
| 				<div>{{ i18n.ts.nothing }}</div> | 				<div>{{ i18n.ts.nothing }}</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</template> | 		</template> | ||||||
| 		<template v-slot="{items}"> | 		<template #default="{items}"> | ||||||
| 			<div v-for="token in items" :key="token.id" class="_panel bfomjevm"> | 			<div v-for="token in items" :key="token.id" class="_panel bfomjevm"> | ||||||
| 				<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/> | 				<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/> | ||||||
| 				<div class="body"> | 				<div class="body"> | ||||||
|  | @ -38,11 +38,11 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { defineExpose, ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| import FormPagination from '@/components/ui/pagination.vue'; | import FormPagination from '@/components/ui/pagination.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const list = ref<any>(null); | const list = ref<any>(null); | ||||||
| 
 | 
 | ||||||
|  | @ -50,8 +50,8 @@ const pagination = { | ||||||
| 	endpoint: 'i/apps' as const, | 	endpoint: 'i/apps' as const, | ||||||
| 	limit: 100, | 	limit: 100, | ||||||
| 	params: { | 	params: { | ||||||
| 		sort: '+lastUsedAt' | 		sort: '+lastUsedAt', | ||||||
| 	} | 	}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function revoke(token) { | function revoke(token) { | ||||||
|  | @ -60,12 +60,14 @@ function revoke(token) { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.installedApps, | 	title: i18n.ts.installedApps, | ||||||
| 	icon: 'fas fa-plug', | 	icon: 'fas fa-plug', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,13 +9,13 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { defineExpose, ref, watch } from 'vue'; | import { ref, watch } from 'vue'; | ||||||
| import FormTextarea from '@/components/form/textarea.vue'; | import FormTextarea from '@/components/form/textarea.vue'; | ||||||
| import FormInfo from '@/components/ui/info.vue'; | import FormInfo from '@/components/ui/info.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { unisonReload } from '@/scripts/unison-reload'; | import { unisonReload } from '@/scripts/unison-reload'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const localCustomCss = ref(localStorage.getItem('customCss') ?? ''); | const localCustomCss = ref(localStorage.getItem('customCss') ?? ''); | ||||||
| 
 | 
 | ||||||
|  | @ -35,11 +35,13 @@ watch(localCustomCss, async () => { | ||||||
| 	await apply(); | 	await apply(); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.customCss, | 	title: i18n.ts.customCss, | ||||||
| 	icon: 'fas fa-code', | 	icon: 'fas fa-code', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed, defineExpose, watch } from 'vue'; | import { computed, watch } from 'vue'; | ||||||
| import FormSwitch from '@/components/form/switch.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import FormLink from '@/components/form/link.vue'; | import FormLink from '@/components/form/link.vue'; | ||||||
| import FormRadios from '@/components/form/radios.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 { deckStore } from '@/ui/deck/deck-store'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { unisonReload } from '@/scripts/unison-reload'; | import { unisonReload } from '@/scripts/unison-reload'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const navWindow = computed(deckStore.makeGetterSetter('navWindow')); | const navWindow = computed(deckStore.makeGetterSetter('navWindow')); | ||||||
| const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); | const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn')); | ||||||
|  | @ -62,7 +62,7 @@ watch(navWindow, async () => { | ||||||
| async function setProfile() { | async function setProfile() { | ||||||
| 	const { canceled, result: name } = await os.inputText({ | 	const { canceled, result: name } = await os.inputText({ | ||||||
| 		title: i18n.ts._deck.profile, | 		title: i18n.ts._deck.profile, | ||||||
| 		allowEmpty: false | 		allowEmpty: false, | ||||||
| 	}); | 	}); | ||||||
| 	if (canceled) return; | 	if (canceled) return; | ||||||
| 	 | 	 | ||||||
|  | @ -70,11 +70,13 @@ async function setProfile() { | ||||||
| 	unisonReload(); | 	unisonReload(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.deck, | 	title: i18n.ts.deck, | ||||||
| 	icon: 'fas fa-columns', | 	icon: 'fas fa-columns', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -8,13 +8,12 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { defineExpose } from 'vue'; |  | ||||||
| import FormInfo from '@/components/ui/info.vue'; | import FormInfo from '@/components/ui/info.vue'; | ||||||
| import FormButton from '@/components/ui/button.vue'; | import FormButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { signout } from '@/account'; | import { signout } from '@/account'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| async function deleteAccount() { | async function deleteAccount() { | ||||||
| 	{ | 	{ | ||||||
|  | @ -27,12 +26,12 @@ async function deleteAccount() { | ||||||
| 
 | 
 | ||||||
| 	const { canceled, result: password } = await os.inputText({ | 	const { canceled, result: password } = await os.inputText({ | ||||||
| 		title: i18n.ts.password, | 		title: i18n.ts.password, | ||||||
| 		type: 'password' | 		type: 'password', | ||||||
| 	}); | 	}); | ||||||
| 	if (canceled) return; | 	if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 	await os.apiWithDialog('i/delete-account', { | 	await os.apiWithDialog('i/delete-account', { | ||||||
| 		password: password | 		password: password, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	await os.alert({ | 	await os.alert({ | ||||||
|  | @ -42,11 +41,13 @@ async function deleteAccount() { | ||||||
| 	await signout(); | 	await signout(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts._accountDelete.accountDelete, | 	title: i18n.ts._accountDelete.accountDelete, | ||||||
| 	icon: 'fas fa-exclamation-triangle', | 	icon: 'fas fa-exclamation-triangle', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -34,7 +34,7 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed, defineExpose, ref } from 'vue'; | import { computed, ref } from 'vue'; | ||||||
| import tinycolor from 'tinycolor2'; | import tinycolor from 'tinycolor2'; | ||||||
| import FormLink from '@/components/form/link.vue'; | import FormLink from '@/components/form/link.vue'; | ||||||
| import FormSwitch from '@/components/form/switch.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 FormSplit from '@/components/form/split.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import bytes from '@/filters/bytes'; | import bytes from '@/filters/bytes'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
| import MkChart from '@/components/chart.vue'; | import MkChart from '@/components/chart.vue'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const fetching = ref(true); | const fetching = ref(true); | ||||||
| const usage = ref<any>(null); | const usage = ref<any>(null); | ||||||
|  | @ -59,8 +59,8 @@ const meterStyle = computed(() => { | ||||||
| 		background: tinycolor({ | 		background: tinycolor({ | ||||||
| 			h: 180 - (usage.value / capacity.value * 180), | 			h: 180 - (usage.value / capacity.value * 180), | ||||||
| 			s: 0.7, | 			s: 0.7, | ||||||
| 			l: 0.5 | 			l: 0.5, | ||||||
| 		}) | 		}), | ||||||
| 	}; | 	}; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | @ -74,7 +74,7 @@ os.api('drive').then(info => { | ||||||
| 
 | 
 | ||||||
| if (defaultStore.state.uploadFolder) { | if (defaultStore.state.uploadFolder) { | ||||||
| 	os.api('drive/folders/show', { | 	os.api('drive/folders/show', { | ||||||
| 		folderId: defaultStore.state.uploadFolder | 		folderId: defaultStore.state.uploadFolder, | ||||||
| 	}).then(response => { | 	}).then(response => { | ||||||
| 		uploadFolder.value = response; | 		uploadFolder.value = response; | ||||||
| 	}); | 	}); | ||||||
|  | @ -86,7 +86,7 @@ function chooseUploadFolder() { | ||||||
| 		os.success(); | 		os.success(); | ||||||
| 		if (defaultStore.state.uploadFolder) { | 		if (defaultStore.state.uploadFolder) { | ||||||
| 			uploadFolder.value = await os.api('drive/folders/show', { | 			uploadFolder.value = await os.api('drive/folders/show', { | ||||||
| 				folderId: defaultStore.state.uploadFolder | 				folderId: defaultStore.state.uploadFolder, | ||||||
| 			}); | 			}); | ||||||
| 		} else { | 		} else { | ||||||
| 			uploadFolder.value = null; | 			uploadFolder.value = null; | ||||||
|  | @ -94,12 +94,14 @@ function chooseUploadFolder() { | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.drive, | 	title: i18n.ts.drive, | ||||||
| 	icon: 'fas fa-cloud', | 	icon: 'fas fa-cloud', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -40,27 +40,27 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <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 FormSection from '@/components/form/section.vue'; | ||||||
| import FormInput from '@/components/form/input.vue'; | import FormInput from '@/components/form/input.vue'; | ||||||
| import FormSwitch from '@/components/form/switch.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { $i } from '@/account'; | import { $i } from '@/account'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const emailAddress = ref($i!.email); | const emailAddress = ref($i!.email); | ||||||
| 
 | 
 | ||||||
| const onChangeReceiveAnnouncementEmail = (v) => { | const onChangeReceiveAnnouncementEmail = (v) => { | ||||||
| 	os.api('i/update', { | 	os.api('i/update', { | ||||||
| 		receiveAnnouncementEmail: v | 		receiveAnnouncementEmail: v, | ||||||
| 	}); | 	}); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const saveEmailAddress = () => { | const saveEmailAddress = () => { | ||||||
| 	os.inputText({ | 	os.inputText({ | ||||||
| 		title: i18n.ts.password, | 		title: i18n.ts.password, | ||||||
| 		type: 'password' | 		type: 'password', | ||||||
| 	}).then(({ canceled, result: password }) => { | 	}).then(({ canceled, result: password }) => { | ||||||
| 		if (canceled) return; | 		if (canceled) return; | ||||||
| 		os.apiWithDialog('i/update-email', { | 		os.apiWithDialog('i/update-email', { | ||||||
|  | @ -86,7 +86,7 @@ const saveNotificationSettings = () => { | ||||||
| 			...[emailNotification_follow.value ? 'follow' : null], | 			...[emailNotification_follow.value ? 'follow' : null], | ||||||
| 			...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null], | 			...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null], | ||||||
| 			...[emailNotification_groupInvited.value ? 'groupInvited' : null], | 			...[emailNotification_groupInvited.value ? 'groupInvited' : null], | ||||||
| 		].filter(x => x != null) | 		].filter(x => x != null), | ||||||
| 	}); | 	}); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -100,11 +100,13 @@ onMounted(() => { | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.email, | 	title: i18n.ts.email, | ||||||
| 	icon: 'fas fa-envelope', | 	icon: 'fas fa-envelope', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -48,7 +48,8 @@ | ||||||
| 		<FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch> | 		<FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch> | ||||||
| 		<FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch> | 		<FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch> | ||||||
| 		<FormSwitch v-model="useSystemFont" class="_formBlock">{{ i18n.ts.useSystemFont }}</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> | 			<div><Mfm :key="useOsNativeEmojis" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div> | ||||||
| 		</FormSwitch> | 		</FormSwitch> | ||||||
| 		<FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch> | 		<FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch> | ||||||
|  | @ -92,7 +93,7 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <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 FormSwitch from '@/components/form/switch.vue'; | ||||||
| import FormSelect from '@/components/form/select.vue'; | import FormSelect from '@/components/form/select.vue'; | ||||||
| import FormRadios from '@/components/form/radios.vue'; | import FormRadios from '@/components/form/radios.vue'; | ||||||
|  | @ -104,8 +105,8 @@ import { langs } from '@/config'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { unisonReload } from '@/scripts/unison-reload'; | import { unisonReload } from '@/scripts/unison-reload'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const lang = ref(localStorage.getItem('lang')); | const lang = ref(localStorage.getItem('lang')); | ||||||
| const fontSize = ref(localStorage.getItem('fontSize')); | const fontSize = ref(localStorage.getItem('fontSize')); | ||||||
|  | @ -173,16 +174,18 @@ watch([ | ||||||
| 	aiChanMode, | 	aiChanMode, | ||||||
| 	showGapBetweenNotesInTimeline, | 	showGapBetweenNotesInTimeline, | ||||||
| 	instanceTicker, | 	instanceTicker, | ||||||
| 	overridedDeviceKind | 	overridedDeviceKind, | ||||||
| ], async () => { | ], async () => { | ||||||
| 	await reloadAsk(); | 	await reloadAsk(); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.general, | 	title: i18n.ts.general, | ||||||
| 	icon: 'fas fa-cogs', | 	icon: 'fas fa-cogs', | ||||||
| 		bg: 'var(--bg)' | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -38,15 +38,15 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { defineExpose, ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import FormSection from '@/components/form/section.vue'; | import FormSection from '@/components/form/section.vue'; | ||||||
| import FormGroup from '@/components/form/group.vue'; | import FormGroup from '@/components/form/group.vue'; | ||||||
| import FormSwitch from '@/components/form/switch.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { selectFile } from '@/scripts/select-file'; | import { selectFile } from '@/scripts/select-file'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
|  | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const excludeMutingUsers = ref(false); | const excludeMutingUsers = ref(false); | ||||||
| const excludeInactiveUsers = 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); | 	os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| defineExpose({ | const headerActions = $computed(() => []); | ||||||
| 	[symbols.PAGE_INFO]: { | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata({ | ||||||
| 	title: i18n.ts.importAndExport, | 	title: i18n.ts.importAndExport, | ||||||
| 	icon: 'fas fa-boxes', | 	icon: 'fas fa-boxes', | ||||||
| 	bg: 'var(--bg)', | 	bg: 'var(--bg)', | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,13 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="900" :margin-min="20" :margin-max="32"> | <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 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 class="body"> | ||||||
| 				<div v-if="!narrow || initialPage == null" class="nav"> | 				<div v-if="!narrow || initialPage == null" class="nav"> | ||||||
| 					<div class="baaadecd"> | 					<div class="baaadecd"> | ||||||
|  | @ -17,30 +12,31 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 				<div v-if="!(narrow && initialPage == null)" class="main"> | 				<div v-if="!(narrow && initialPage == null)" class="main"> | ||||||
| 					<div class="bkzroven"> | 					<div class="bkzroven"> | ||||||
| 					<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/> | 						<component :is="component" :key="initialPage" v-bind="pageProps"/> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| </MkSpacer> | 	</MkSpacer> | ||||||
|  | </mkstickycontainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> | <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 { i18n } from '@/i18n'; | ||||||
| import MkInfo from '@/components/ui/info.vue'; | import MkInfo from '@/components/ui/info.vue'; | ||||||
| import MkSuperMenu from '@/components/ui/super-menu.vue'; | import MkSuperMenu from '@/components/ui/super-menu.vue'; | ||||||
| import { scroll } from '@/scripts/scroll'; | import { scroll } from '@/scripts/scroll'; | ||||||
| import { signout } from '@/account'; | import { signout , $i } from '@/account'; | ||||||
| import { unisonReload } from '@/scripts/unison-reload'; | import { unisonReload } from '@/scripts/unison-reload'; | ||||||
| import * as symbols from '@/symbols'; |  | ||||||
| import { instance } from '@/instance'; | import { instance } from '@/instance'; | ||||||
| import { $i } from '@/account'; | import { useRouter } from '@/router'; | ||||||
| import { MisskeyNavigator } from '@/scripts/navigate'; | import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
|   initialPage?: string |   initialPage?: string; | ||||||
| }>(); | }>(), { | ||||||
|  | }); | ||||||
| 
 | 
 | ||||||
| const indexInfo = { | const indexInfo = { | ||||||
| 	title: i18n.ts.settings, | 	title: i18n.ts.settings, | ||||||
|  | @ -52,7 +48,7 @@ const INFO = ref(indexInfo); | ||||||
| const el = ref<HTMLElement | null>(null); | const el = ref<HTMLElement | null>(null); | ||||||
| const childInfo = ref(null); | const childInfo = ref(null); | ||||||
| 
 | 
 | ||||||
| const nav = new MisskeyNavigator(); | const router = useRouter(); | ||||||
| 
 | 
 | ||||||
| const narrow = ref(false); | const narrow = ref(false); | ||||||
| const NARROW_THRESHOLD = 600; | const NARROW_THRESHOLD = 600; | ||||||
|  | @ -189,7 +185,7 @@ const menuDef = computed(() => [{ | ||||||
| 			signout(); | 			signout(); | ||||||
| 		}, | 		}, | ||||||
| 		danger: true, | 		danger: true, | ||||||
| 	},], | 	}], | ||||||
| }]); | }]); | ||||||
| 
 | 
 | ||||||
| const pageProps = ref({}); | const pageProps = ref({}); | ||||||
|  | @ -242,7 +238,7 @@ watch(component, () => { | ||||||
| 
 | 
 | ||||||
| watch(() => props.initialPage, () => { | watch(() => props.initialPage, () => { | ||||||
| 	if (props.initialPage == null && !narrow.value) { | 	if (props.initialPage == null && !narrow.value) { | ||||||
| 		nav.push('/settings/profile'); | 		router.push('/settings/profile'); | ||||||
| 	} else { | 	} else { | ||||||
| 		if (props.initialPage == null) { | 		if (props.initialPage == null) { | ||||||
| 			INFO.value = indexInfo; | 			INFO.value = indexInfo; | ||||||
|  | @ -252,7 +248,7 @@ watch(() => props.initialPage, () => { | ||||||
| 
 | 
 | ||||||
| watch(narrow, () => { | watch(narrow, () => { | ||||||
| 	if (props.initialPage == null && !narrow.value) { | 	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; | 	narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; | ||||||
| 	if (props.initialPage == null && !narrow.value) { | 	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 emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified)); | ||||||
| 
 | 
 | ||||||
| const pageChanged = (page) => { | provideMetadataReceiver((info) => { | ||||||
| 	if (page == null) { | 	if (info == null) { | ||||||
| 		childInfo.value = null; | 		childInfo.value = null; | ||||||
| 	} else { | 	} else { | ||||||
| 		childInfo.value = page[symbols.PAGE_INFO]; | 		childInfo.value = info; | ||||||
| 	} | 	} | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| defineExpose({ |  | ||||||
| 	[symbols.PAGE_INFO]: INFO, |  | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | const headerActions = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(INFO); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .vvcocwet { | .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 { | 	> .body { | ||||||
| 		> .nav { | 		> .nav { | ||||||
| 			.baaadecd { | 			.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