Compare commits
	
		
			11 Commits
		
	
	
		
			2025.3.2-a
			...
			refine-piz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2402754dcc | ||
| 
						 | 
					2493592bd0 | ||
| 
						 | 
					eec4ab841a | ||
| 
						 | 
					d0b8ffe629 | ||
| 
						 | 
					cef7575b76 | ||
| 
						 | 
					9842eb2eeb | ||
| 
						 | 
					05078e9c14 | ||
| 
						 | 
					db5c6fa3c2 | ||
| 
						 | 
					8a4e2659ed | ||
| 
						 | 
					d19c094a9b | ||
| 
						 | 
					a7f7ff33e7 | 
@@ -6,16 +6,12 @@
 | 
			
		||||
### Client
 | 
			
		||||
- Feat: 設定の管理が強化されました
 | 
			
		||||
  - 自動でバックアップされるように
 | 
			
		||||
	- 任意の設定項目をデバイス間で同期できるように(実験的)
 | 
			
		||||
- Enhance: プラグインの管理が強化されました
 | 
			
		||||
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
 | 
			
		||||
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
 | 
			
		||||
- Enhance: テーマ設定画面のデザインを改善
 | 
			
		||||
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
 | 
			
		||||
 | 
			
		||||
### Server
 | 
			
		||||
- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正
 | 
			
		||||
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## 2025.3.1
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								assets/about/drive.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 94 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/about/post.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 317 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/about/reaction.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 24 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/about/ui.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 95 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/ss/explore.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 238 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								assets/ss/user.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 148 KiB  | 
							
								
								
									
										102
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -5310,96 +5310,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
     * 復元
 | 
			
		||||
     */
 | 
			
		||||
    "restore": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * デバイス間で同期
 | 
			
		||||
     */
 | 
			
		||||
    "syncBetweenDevices": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * サーバーに設定値が存在します
 | 
			
		||||
     */
 | 
			
		||||
    "preferenceSyncConflictTitle": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?
 | 
			
		||||
     */
 | 
			
		||||
    "preferenceSyncConflictText": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * サーバーの設定値
 | 
			
		||||
     */
 | 
			
		||||
    "preferenceSyncConflictChoiceServer": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * デバイスの設定値
 | 
			
		||||
     */
 | 
			
		||||
    "preferenceSyncConflictChoiceDevice": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 同期の有効化をキャンセル
 | 
			
		||||
     */
 | 
			
		||||
    "preferenceSyncConflictChoiceCancel": string;
 | 
			
		||||
    "_settings": {
 | 
			
		||||
        /**
 | 
			
		||||
         * ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "driveBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * プラグインを利用するとクライアントの機能を拡張することができます。プラグインのインストール、個別の設定と管理が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "pluginBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * サーバーからの受信する通知の種類と範囲や、プッシュ通知の設定が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "notificationsBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * API
 | 
			
		||||
         */
 | 
			
		||||
        "api": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * Webhook
 | 
			
		||||
         */
 | 
			
		||||
        "webhook": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * サービス連携
 | 
			
		||||
         */
 | 
			
		||||
        "serviceConnection": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "serviceConnectionBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * アカウントのデータ
 | 
			
		||||
         */
 | 
			
		||||
        "accountData": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * アカウントのデータをエクスポート/インポートして管理できます。
 | 
			
		||||
         */
 | 
			
		||||
        "accountDataBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "muteAndBlockBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。
 | 
			
		||||
         */
 | 
			
		||||
        "accessibilityBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "privacyBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * パスワード、ログイン方法、認証アプリ、パスキーなどアカウントのセキュリティに関する設定を行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "securityBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 好みに応じた、クライアントの全体的な動作の設定が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "preferencesBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "appearanceBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * クライアントで再生するサウンドの設定が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "soundsBanner": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_preferencesProfile": {
 | 
			
		||||
        /**
 | 
			
		||||
         * プロファイル名
 | 
			
		||||
@@ -5485,10 +5395,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
         * リモートサーバーに連合されたノートには効果が及ばない場合があります。
 | 
			
		||||
         */
 | 
			
		||||
        "mayNotEffectForFederatedNotes": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。
 | 
			
		||||
         */
 | 
			
		||||
        "mayNotEffectSomeSituations": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 指定した時間を経過しているノート
 | 
			
		||||
         */
 | 
			
		||||
@@ -7836,10 +7742,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
         * 標準のテーマ
 | 
			
		||||
         */
 | 
			
		||||
        "builtinThemes": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * サーバーのテーマ
 | 
			
		||||
         */
 | 
			
		||||
        "instanceTheme": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * そのテーマは既にインストールされています
 | 
			
		||||
         */
 | 
			
		||||
@@ -9848,10 +9750,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
         * 幅を自動調整
 | 
			
		||||
         */
 | 
			
		||||
        "flexible": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * プロファイル情報のデバイス間同期を有効にする
 | 
			
		||||
         */
 | 
			
		||||
        "enableSyncBetweenDevicesForProfiles": string;
 | 
			
		||||
        "_columns": {
 | 
			
		||||
            /**
 | 
			
		||||
             * メイン
 | 
			
		||||
 
 | 
			
		||||
@@ -1323,30 +1323,6 @@ untitled: "無題"
 | 
			
		||||
noName: "名前はありません"
 | 
			
		||||
skip: "スキップ"
 | 
			
		||||
restore: "復元"
 | 
			
		||||
syncBetweenDevices: "デバイス間で同期"
 | 
			
		||||
preferenceSyncConflictTitle: "サーバーに設定値が存在します"
 | 
			
		||||
preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?"
 | 
			
		||||
preferenceSyncConflictChoiceServer: "サーバーの設定値"
 | 
			
		||||
preferenceSyncConflictChoiceDevice: "デバイスの設定値"
 | 
			
		||||
preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
 | 
			
		||||
 | 
			
		||||
_settings:
 | 
			
		||||
  driveBanner: "ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。"
 | 
			
		||||
  pluginBanner: "プラグインを利用するとクライアントの機能を拡張することができます。プラグインのインストール、個別の設定と管理が行えます。"
 | 
			
		||||
  notificationsBanner: "サーバーからの受信する通知の種類と範囲や、プッシュ通知の設定が行えます。"
 | 
			
		||||
  api: "API"
 | 
			
		||||
  webhook: "Webhook"
 | 
			
		||||
  serviceConnection: "サービス連携"
 | 
			
		||||
  serviceConnectionBanner: "外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。"
 | 
			
		||||
  accountData: "アカウントのデータ"
 | 
			
		||||
  accountDataBanner: "アカウントのデータをエクスポート/インポートして管理できます。"
 | 
			
		||||
  muteAndBlockBanner: "非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。"
 | 
			
		||||
  accessibilityBanner: "クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。"
 | 
			
		||||
  privacyBanner: "コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。"
 | 
			
		||||
  securityBanner: "パスワード、ログイン方法、認証アプリ、パスキーなどアカウントのセキュリティに関する設定を行えます。"
 | 
			
		||||
  preferencesBanner: "好みに応じた、クライアントの全体的な動作の設定が行えます。"
 | 
			
		||||
  appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。"
 | 
			
		||||
  soundsBanner: "クライアントで再生するサウンドの設定が行えます。"
 | 
			
		||||
 | 
			
		||||
_preferencesProfile:
 | 
			
		||||
  profileName: "プロファイル名"
 | 
			
		||||
@@ -1373,7 +1349,6 @@ _accountSettings:
 | 
			
		||||
  makeNotesHiddenBefore: "過去のノートを非公開化する"
 | 
			
		||||
  makeNotesHiddenBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。"
 | 
			
		||||
  mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばない場合があります。"
 | 
			
		||||
  mayNotEffectSomeSituations: "これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。"
 | 
			
		||||
  notesHavePassedSpecifiedPeriod: "指定した時間を経過しているノート"
 | 
			
		||||
  notesOlderThanSpecifiedDateAndTime: "指定した日時より前のノート"
 | 
			
		||||
 | 
			
		||||
@@ -2055,7 +2030,6 @@ _theme:
 | 
			
		||||
  installed: "{name}をインストールしました"
 | 
			
		||||
  installedThemes: "インストールされたテーマ"
 | 
			
		||||
  builtinThemes: "標準のテーマ"
 | 
			
		||||
  instanceTheme: "サーバーのテーマ"
 | 
			
		||||
  alreadyInstalled: "そのテーマは既にインストールされています"
 | 
			
		||||
  invalid: "テーマの形式が間違っています"
 | 
			
		||||
  make: "テーマを作る"
 | 
			
		||||
@@ -2603,7 +2577,6 @@ _deck:
 | 
			
		||||
  useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
 | 
			
		||||
  usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"
 | 
			
		||||
  flexible: "幅を自動調整"
 | 
			
		||||
  enableSyncBetweenDevicesForProfiles: "プロファイル情報のデバイス間同期を有効にする"
 | 
			
		||||
 | 
			
		||||
  _columns:
 | 
			
		||||
    main: "メイン"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"version": "2025.3.2-alpha.9",
 | 
			
		||||
	"version": "2025.3.2-alpha.4",
 | 
			
		||||
	"codename": "nasubi",
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"type": "git",
 | 
			
		||||
@@ -24,7 +24,6 @@
 | 
			
		||||
		"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
 | 
			
		||||
		"build-storybook": "pnpm --filter frontend build-storybook",
 | 
			
		||||
		"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
 | 
			
		||||
		"build-frontend-search-index": "pnpm --filter frontend build-search-index",
 | 
			
		||||
		"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
 | 
			
		||||
		"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
 | 
			
		||||
		"init": "pnpm migrate",
 | 
			
		||||
@@ -66,12 +65,12 @@
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@misskey-dev/eslint-plugin": "2.1.0",
 | 
			
		||||
		"@types/node": "22.13.10",
 | 
			
		||||
		"@types/node": "22.13.9",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "8.26.0",
 | 
			
		||||
		"@typescript-eslint/parser": "8.26.0",
 | 
			
		||||
		"cross-env": "7.0.3",
 | 
			
		||||
		"cypress": "14.1.0",
 | 
			
		||||
		"eslint": "9.22.0",
 | 
			
		||||
		"eslint": "9.21.0",
 | 
			
		||||
		"globals": "16.0.0",
 | 
			
		||||
		"ncp": "2.0.0",
 | 
			
		||||
		"pnpm": "10.6.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import type { Config } from '@/config.js';
 | 
			
		||||
import { StatusError } from '@/misc/status-error.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
 | 
			
		||||
import { assertActivityMatchesUrl, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import type { IObject } from '@/core/activitypub/type.js';
 | 
			
		||||
import type { Response } from 'node-fetch';
 | 
			
		||||
import type { URL } from 'node:url';
 | 
			
		||||
@@ -265,7 +265,7 @@ export class HttpRequestService {
 | 
			
		||||
		const finalUrl = res.url; // redirects may have been involved
 | 
			
		||||
		const activity = await res.json() as IObject;
 | 
			
		||||
 | 
			
		||||
		assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail);
 | 
			
		||||
		assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail);
 | 
			
		||||
 | 
			
		||||
		return activity;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import { LoggerService } from '@/core/LoggerService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
 | 
			
		||||
import { assertActivityMatchesUrl, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import { assertActivityMatchesUrls, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import type { IObject } from './type.js';
 | 
			
		||||
 | 
			
		||||
type Request = {
 | 
			
		||||
@@ -258,7 +258,7 @@ export class ApRequestService {
 | 
			
		||||
		const finalUrl = res.url; // redirects may have been involved
 | 
			
		||||
		const activity = await res.json() as IObject;
 | 
			
		||||
 | 
			
		||||
		assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail);
 | 
			
		||||
		assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail);
 | 
			
		||||
 | 
			
		||||
		return activity;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ function normalizeSynonymousSubdomain(url: URL | string): URL {
 | 
			
		||||
	return new URL(urlParsed.toString().replace(host, normalizedHost));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function assertActivityMatchesUrl(requestUrl: string | URL, activity: IObject, finalUrl: string | URL, allowSoftfail: FetchAllowSoftFailMask): FetchAllowSoftFailMask {
 | 
			
		||||
export function assertActivityMatchesUrls(requestUrl: string | URL, activity: IObject, candidateUrls: (string | URL)[], allowSoftfail: FetchAllowSoftFailMask): FetchAllowSoftFailMask {
 | 
			
		||||
	// must have a unique identifier to verify authority
 | 
			
		||||
	if (!activity.id) {
 | 
			
		||||
		throw new Error('bad Activity: missing id field');
 | 
			
		||||
@@ -95,32 +95,26 @@ export function assertActivityMatchesUrl(requestUrl: string | URL, activity: IOb
 | 
			
		||||
	const requestUrlParsed = normalizeSynonymousSubdomain(requestUrl);
 | 
			
		||||
	const idParsed = normalizeSynonymousSubdomain(activity.id);
 | 
			
		||||
 | 
			
		||||
	const finalUrlParsed = normalizeSynonymousSubdomain(finalUrl);
 | 
			
		||||
 | 
			
		||||
	// mastodon sends activities with hash in the URL
 | 
			
		||||
	// currently it only happens with likes, deletes etc.
 | 
			
		||||
	// but object ID never has hash
 | 
			
		||||
	requestUrlParsed.hash = '';
 | 
			
		||||
	finalUrlParsed.hash = '';
 | 
			
		||||
	const candidateUrlsParsed = candidateUrls.map(it => normalizeSynonymousSubdomain(it));
 | 
			
		||||
 | 
			
		||||
	const requestUrlSecure = requestUrlParsed.protocol === 'https:';
 | 
			
		||||
	const finalUrlSecure = finalUrlParsed.protocol === 'https:';
 | 
			
		||||
	const finalUrlSecure = candidateUrlsParsed.every(it => it.protocol === 'https:');
 | 
			
		||||
	if (requestUrlSecure && !finalUrlSecure) {
 | 
			
		||||
		throw new Error(`bad Activity: id(${activity.id}) is not allowed to have http:// in the url`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Compare final URL to the ID
 | 
			
		||||
	if (finalUrlParsed.href !== idParsed.href) {
 | 
			
		||||
		requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${finalUrlParsed.toString()})`);
 | 
			
		||||
	if (!candidateUrlsParsed.some(it => it.href === idParsed.href)) {
 | 
			
		||||
		requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${candidateUrlsParsed.map(it => it.toString())})`);
 | 
			
		||||
 | 
			
		||||
		// at lease host need to match exactly (ActivityPub requirement)
 | 
			
		||||
		if (idParsed.host !== finalUrlParsed.host) {
 | 
			
		||||
			throw new Error(`bad Activity: id(${activity.id}) does not match response host(${finalUrlParsed.host})`);
 | 
			
		||||
		if (!candidateUrlsParsed.some(it => idParsed.host === it.host)) {
 | 
			
		||||
			throw new Error(`bad Activity: id(${activity.id}) does not match response host(${candidateUrlsParsed.map(it => it.host)})`);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Compare request URL to the ID
 | 
			
		||||
	if (requestUrlParsed.href !== idParsed.href) {
 | 
			
		||||
	if (!requestUrlParsed.href.includes(idParsed.href)) {
 | 
			
		||||
		requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match request url(${requestUrlParsed.toString()})`);
 | 
			
		||||
 | 
			
		||||
		// if cross-origin lookup is allowed, we can accept some variation between the original request URL to the final object ID (but not between the final URL and the object ID)
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import httpSignature from '@peertube/http-signature';
 | 
			
		||||
 | 
			
		||||
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
 | 
			
		||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
 | 
			
		||||
import { assertActivityMatchesUrl, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import { IObject } from '@/core/activitypub/type.js';
 | 
			
		||||
 | 
			
		||||
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
 | 
			
		||||
@@ -66,26 +66,23 @@ describe('ap-request', () => {
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('rejects non matching domain', () => {
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrls(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://alice.example.com/abc' } as IObject,
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			[
 | 
			
		||||
				'https://alice.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'validation should pass base case');
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://bob.example.com/abc' } as IObject,
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			[
 | 
			
		||||
				'https://alice.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.Any,
 | 
			
		||||
		), 'validation should fail no matter what if the response URL is inconsistent with the object ID');
 | 
			
		||||
		
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.example.com/abc#test',
 | 
			
		||||
			{ id: 'https://alice.example.com/abc' } as IObject,
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'validation should pass with hash in request URL');
 | 
			
		||||
 | 
			
		||||
		// fix issues like threads
 | 
			
		||||
		// https://github.com/misskey-dev/misskey/issues/15039
 | 
			
		||||
		const withOrWithoutWWW = [
 | 
			
		||||
@@ -100,71 +97,89 @@ describe('ap-request', () => {
 | 
			
		||||
			),
 | 
			
		||||
			withOrWithoutWWW,
 | 
			
		||||
		).forEach(([[a, b], c]) => {
 | 
			
		||||
			assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
			assert.doesNotThrow(() => assertActivityMatchesUrls(
 | 
			
		||||
				a,
 | 
			
		||||
				{ id: b } as IObject,
 | 
			
		||||
				c,
 | 
			
		||||
				[
 | 
			
		||||
					c,
 | 
			
		||||
				],
 | 
			
		||||
				FetchAllowSoftFailMask.Strict,
 | 
			
		||||
			), 'validation should pass with or without www. subdomain');
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('cross origin lookup', () => {
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrls(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://bob.example.com/abc' } as IObject,
 | 
			
		||||
			'https://bob.example.com/abc',
 | 
			
		||||
			[
 | 
			
		||||
				'https://bob.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId,
 | 
			
		||||
		), 'validation should pass if the response is otherwise consistent and cross-origin is allowed');
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://bob.example.com/abc' } as IObject,
 | 
			
		||||
			'https://bob.example.com/abc',
 | 
			
		||||
			[
 | 
			
		||||
				'https://bob.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('rejects non-canonical ID', () => {
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
			'https://alice.example.com/@alice',
 | 
			
		||||
			{ id: 'https://alice.example.com/users/alice' } as IObject,
 | 
			
		||||
			'https://alice.example.com/users/alice',
 | 
			
		||||
			[
 | 
			
		||||
				'https://alice.example.com/users/alice'
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'throws if the response ID did not exactly match the expected ID');
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrls(
 | 
			
		||||
			'https://alice.example.com/@alice',
 | 
			
		||||
			{ id: 'https://alice.example.com/users/alice' } as IObject,
 | 
			
		||||
			'https://alice.example.com/users/alice',
 | 
			
		||||
			[
 | 
			
		||||
				'https://alice.example.com/users/alice',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.NonCanonicalId,
 | 
			
		||||
		), 'does not throw if non-canonical ID is allowed');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('origin relaxed alignment', () => {
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrls(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://ap.alice.example.com/abc' } as IObject,
 | 
			
		||||
			'https://ap.alice.example.com/abc',
 | 
			
		||||
			[
 | 
			
		||||
				'https://ap.alice.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
 | 
			
		||||
		), 'validation should pass if response is a subdomain of the expected origin');
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
			'https://alice.multi-tenant.example.com/abc',
 | 
			
		||||
			{ id: 'https://alice.multi-tenant.example.com/abc' } as IObject,
 | 
			
		||||
			'https://bob.multi-tenant.example.com/abc',
 | 
			
		||||
			[
 | 
			
		||||
				'https://bob.multi-tenant.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
 | 
			
		||||
		), 'validation should fail if response is a disjoint domain of the expected origin');
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://ap.alice.example.com/abc' } as IObject,
 | 
			
		||||
			'https://ap.alice.example.com/abc',
 | 
			
		||||
			[
 | 
			
		||||
				'https://ap.alice.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'throws if relaxed origin is forbidden');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('resist HTTP downgrade', () => {
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://alice.example.com/abc' } as IObject,
 | 
			
		||||
			'http://alice.example.com/abc',
 | 
			
		||||
			[
 | 
			
		||||
				'http://alice.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'throws if HTTP downgrade is detected');
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
@@ -17,52 +17,8 @@ interface SatisfiesExpression extends estree.BaseExpression {
 | 
			
		||||
	reference: estree.Identifier;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ImportDeclaration extends estree.ImportDeclaration {
 | 
			
		||||
	kind?: 'type';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const generator = {
 | 
			
		||||
	...GENERATOR,
 | 
			
		||||
	ImportDeclaration(node: ImportDeclaration, state: State) {
 | 
			
		||||
		state.write('import ');
 | 
			
		||||
		if (node.kind === 'type') state.write('type ');
 | 
			
		||||
		const { specifiers } = node;
 | 
			
		||||
		if (specifiers.length > 0) {
 | 
			
		||||
			let i = 0;
 | 
			
		||||
			for (; i < specifiers.length; i++) {
 | 
			
		||||
				if (i > 0) {
 | 
			
		||||
					state.write(', ');
 | 
			
		||||
				}
 | 
			
		||||
				const specifier = specifiers[i]!;
 | 
			
		||||
				if (specifier.type === 'ImportDefaultSpecifier') {
 | 
			
		||||
					state.write(specifier.local.name, specifier);
 | 
			
		||||
				} else if (specifier.type === 'ImportNamespaceSpecifier') {
 | 
			
		||||
					state.write(`* as ${specifier.local.name}`, specifier);
 | 
			
		||||
				} else {
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (i < specifiers.length) {
 | 
			
		||||
				state.write('{');
 | 
			
		||||
				for (; i < specifiers.length; i++) {
 | 
			
		||||
					const specifier = specifiers[i]! as estree.ImportSpecifier;
 | 
			
		||||
					const { name } = specifier.imported as estree.Identifier;
 | 
			
		||||
					state.write(name, specifier);
 | 
			
		||||
					if (name !== specifier.local.name) {
 | 
			
		||||
						state.write(` as ${specifier.local.name}`);
 | 
			
		||||
					}
 | 
			
		||||
					if (i < specifiers.length - 1) {
 | 
			
		||||
						state.write(', ');
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				state.write('}');
 | 
			
		||||
			}
 | 
			
		||||
			state.write(' from ');
 | 
			
		||||
		}
 | 
			
		||||
		this.Literal(node.source, state);
 | 
			
		||||
 | 
			
		||||
		state.write(';');
 | 
			
		||||
	},
 | 
			
		||||
	SatisfiesExpression(node: SatisfiesExpression, state: State) {
 | 
			
		||||
		switch (node.expression.type) {
 | 
			
		||||
			case 'ArrowFunctionExpression': {
 | 
			
		||||
@@ -106,7 +62,7 @@ type ToKebab<T extends readonly string[]> = T extends readonly [
 | 
			
		||||
	: T extends readonly [
 | 
			
		||||
			infer XH extends string,
 | 
			
		||||
			...infer XR extends readonly string[]
 | 
			
		||||
		]
 | 
			
		||||
	  ]
 | 
			
		||||
	? `${XH}${XR extends readonly string[] ? `-${ToKebab<XR>}` : ''}`
 | 
			
		||||
	: '';
 | 
			
		||||
 | 
			
		||||
@@ -176,7 +132,7 @@ function toStories(component: string): Promise<string> {
 | 
			
		||||
								kind={'init' as const}
 | 
			
		||||
								shorthand
 | 
			
		||||
							/> as estree.Property,
 | 
			
		||||
						]
 | 
			
		||||
					  ]
 | 
			
		||||
					: []),
 | 
			
		||||
			]}
 | 
			
		||||
		/> as estree.ObjectExpression;
 | 
			
		||||
@@ -199,8 +155,7 @@ function toStories(component: string): Promise<string> {
 | 
			
		||||
									/> as estree.ImportSpecifier,
 | 
			
		||||
								]),
 | 
			
		||||
					]}
 | 
			
		||||
					kind={'type'}
 | 
			
		||||
				/> as ImportDeclaration,
 | 
			
		||||
				/> as estree.ImportDeclaration,
 | 
			
		||||
				...(hasMsw
 | 
			
		||||
					? [
 | 
			
		||||
							<import-declaration
 | 
			
		||||
@@ -210,8 +165,8 @@ function toStories(component: string): Promise<string> {
 | 
			
		||||
										local={<identifier name='msw' /> as estree.Identifier}
 | 
			
		||||
									/> as estree.ImportNamespaceSpecifier,
 | 
			
		||||
								]}
 | 
			
		||||
							/> as ImportDeclaration,
 | 
			
		||||
						]
 | 
			
		||||
							/> as estree.ImportDeclaration,
 | 
			
		||||
					  ]
 | 
			
		||||
					: []),
 | 
			
		||||
				...(hasImplStories
 | 
			
		||||
					? []
 | 
			
		||||
@@ -221,8 +176,8 @@ function toStories(component: string): Promise<string> {
 | 
			
		||||
								specifiers={[
 | 
			
		||||
									<import-default-specifier local={identifier} /> as estree.ImportDefaultSpecifier,
 | 
			
		||||
								]}
 | 
			
		||||
							/> as ImportDeclaration,
 | 
			
		||||
						]),
 | 
			
		||||
							/> as estree.ImportDeclaration,
 | 
			
		||||
					  ]),
 | 
			
		||||
				...(hasMetaStories
 | 
			
		||||
					? [
 | 
			
		||||
							<import-declaration
 | 
			
		||||
@@ -232,7 +187,7 @@ function toStories(component: string): Promise<string> {
 | 
			
		||||
										local={<identifier name='storiesMeta' /> as estree.Identifier}
 | 
			
		||||
									/> as estree.ImportNamespaceSpecifier,
 | 
			
		||||
								]}
 | 
			
		||||
							/> as ImportDeclaration,
 | 
			
		||||
							/> as estree.ImportDeclaration,
 | 
			
		||||
						]
 | 
			
		||||
					: []),
 | 
			
		||||
				<variable-declaration
 | 
			
		||||
 
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 25 KiB  | 
| 
		 Before Width: | Height: | Size: 18 KiB  | 
| 
		 Before Width: | Height: | Size: 34 KiB  | 
| 
		 Before Width: | Height: | Size: 16 KiB  | 
| 
		 Before Width: | Height: | Size: 39 KiB  | 
| 
		 Before Width: | Height: | Size: 30 KiB  | 
| 
		 Before Width: | Height: | Size: 43 KiB  | 
| 
		 Before Width: | Height: | Size: 31 KiB  | 
| 
		 Before Width: | Height: | Size: 31 KiB  | 
| 
		 Before Width: | Height: | Size: 30 KiB  | 
| 
		 Before Width: | Height: | Size: 40 KiB  | 
| 
		 Before Width: | Height: | Size: 27 KiB  | 
| 
		 Before Width: | Height: | Size: 30 KiB  | 
@@ -1428,23 +1428,6 @@ async function processVueFile(
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function generateSearchIndex(options: Options, transformedCodeCache: Record<string, string> = {}) {
 | 
			
		||||
	const filePaths = options.targetFilePaths.reduce<string[]>((acc, filePathPattern) => {
 | 
			
		||||
		const matchedFiles = glob.sync(filePathPattern);
 | 
			
		||||
		return [...acc, ...matchedFiles];
 | 
			
		||||
	}, []);
 | 
			
		||||
 | 
			
		||||
	for (const filePath of filePaths) {
 | 
			
		||||
		const id = path.resolve(filePath); // 絶対パスに変換
 | 
			
		||||
		const code = fs.readFileSync(filePath, 'utf-8'); // ファイル内容を読み込む
 | 
			
		||||
		const { transformedCodeCache: newCache } = await processVueFile(code, id, options, transformedCodeCache); // processVueFile 関数を呼び出す
 | 
			
		||||
		transformedCodeCache = newCache; // キャッシュを更新
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	await analyzeVueProps({ ...options, transformedCodeCache }); // 開発サーバー起動時にも analyzeVueProps を実行
 | 
			
		||||
 | 
			
		||||
	return transformedCodeCache; // キャッシュを返す
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Rollup プラグインとして export
 | 
			
		||||
export default function pluginCreateSearchIndex(options: Options): Plugin {
 | 
			
		||||
@@ -1462,7 +1445,19 @@ export default function pluginCreateSearchIndex(options: Options): Plugin {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			transformedCodeCache = await generateSearchIndex(options, transformedCodeCache);
 | 
			
		||||
			const filePaths = options.targetFilePaths.reduce<string[]>((acc, filePathPattern) => {
 | 
			
		||||
				const matchedFiles = glob.sync(filePathPattern);
 | 
			
		||||
				return [...acc, ...matchedFiles];
 | 
			
		||||
			}, []);
 | 
			
		||||
 | 
			
		||||
			for (const filePath of filePaths) {
 | 
			
		||||
				const id = path.resolve(filePath); // 絶対パスに変換
 | 
			
		||||
				const code = fs.readFileSync(filePath, 'utf-8'); // ファイル内容を読み込む
 | 
			
		||||
				const { transformedCodeCache: newCache } = await processVueFile(code, id, options, transformedCodeCache); // processVueFile 関数を呼び出す
 | 
			
		||||
				transformedCodeCache = newCache; // キャッシュを更新
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			await analyzeVueProps({ ...options, transformedCodeCache }); // 開発サーバー起動時にも analyzeVueProps を実行
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async transform(code, id) {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"watch": "vite",
 | 
			
		||||
		"build": "vite build",
 | 
			
		||||
		"build-search-index": "vite-node --config \"./vite-node.config.ts\" \"./scripts/generate-search-index.ts\"",
 | 
			
		||||
		"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
 | 
			
		||||
		"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
 | 
			
		||||
		"build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static",
 | 
			
		||||
@@ -134,7 +133,6 @@
 | 
			
		||||
		"start-server-and-test": "2.0.10",
 | 
			
		||||
		"storybook": "8.6.4",
 | 
			
		||||
		"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
 | 
			
		||||
		"vite-node": "3.0.8",
 | 
			
		||||
		"vite-plugin-turbosnap": "1.0.3",
 | 
			
		||||
		"vitest": "3.0.8",
 | 
			
		||||
		"vitest-fetch-mock": "0.4.5",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { searchIndexes } from '../vite.config.js';
 | 
			
		||||
import { generateSearchIndex } from '../lib/vite-plugin-create-search-index.js';
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	for (const searchIndex of searchIndexes) {
 | 
			
		||||
		await generateSearchIndex(searchIndex);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
@@ -17,7 +17,6 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
 | 
			
		||||
 | 
			
		||||
// TODO: 他のタブと永続化されたstateを同期
 | 
			
		||||
// TODO: accountsはpreferences管理にする(tokenは別管理)
 | 
			
		||||
 | 
			
		||||
type Account = Misskey.entities.MeDetailed & { token: string };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -180,12 +180,12 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
 | 
			
		||||
	//#region Sync dark mode
 | 
			
		||||
	if (prefer.s.syncDeviceDarkMode) {
 | 
			
		||||
		store.set('darkMode', isDeviceDarkmode());
 | 
			
		||||
		store.commit('darkMode', isDeviceDarkmode());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
 | 
			
		||||
		if (prefer.s.syncDeviceDarkMode) {
 | 
			
		||||
			store.set('darkMode', mql.matches);
 | 
			
		||||
			store.commit('darkMode', mql.matches);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	//#endregion
 | 
			
		||||
 
 | 
			
		||||
@@ -6,11 +6,9 @@
 | 
			
		||||
import { createApp, defineAsyncComponent, markRaw } from 'vue';
 | 
			
		||||
import { ui } from '@@/js/config.js';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import { common } from './common.js';
 | 
			
		||||
import type { Component } from 'vue';
 | 
			
		||||
import type { Keymap } from '@/utility/hotkey.js';
 | 
			
		||||
import type { DeckProfile } from '@/deck.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { alert, confirm, popup, post, toast } from '@/os.js';
 | 
			
		||||
import { useStream } from '@/stream.js';
 | 
			
		||||
@@ -145,34 +143,12 @@ export async function mainBoot() {
 | 
			
		||||
				if (themes.length > 0) {
 | 
			
		||||
					prefer.commit('themes', themes);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const plugins = ColdDeviceStorage.get('plugins');
 | 
			
		||||
				prefer.commit('plugins', plugins.map(p => ({
 | 
			
		||||
					...p,
 | 
			
		||||
					installId: (p as any).id,
 | 
			
		||||
					id: undefined,
 | 
			
		||||
				})));
 | 
			
		||||
 | 
			
		||||
				prefer.commit('deck.profile', deckStore.s.profile);
 | 
			
		||||
				misskeyApi('i/registry/keys', {
 | 
			
		||||
					scope: ['client', 'deck', 'profiles'],
 | 
			
		||||
				}).then(async keys => {
 | 
			
		||||
					const profiles: DeckProfile[] = [];
 | 
			
		||||
					for (const key of keys) {
 | 
			
		||||
						const deck = await misskeyApi('i/registry/get', {
 | 
			
		||||
							scope: ['client', 'deck', 'profiles'],
 | 
			
		||||
							key: key,
 | 
			
		||||
						});
 | 
			
		||||
						profiles.push({
 | 
			
		||||
							id: uuid(),
 | 
			
		||||
							name: key,
 | 
			
		||||
							columns: deck.columns,
 | 
			
		||||
							layout: deck.layout,
 | 
			
		||||
						});
 | 
			
		||||
					}
 | 
			
		||||
					prefer.commit('deck.profiles', profiles);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				prefer.commit('lightTheme', ColdDeviceStorage.get('lightTheme'));
 | 
			
		||||
				prefer.commit('darkTheme', ColdDeviceStorage.get('darkTheme'));
 | 
			
		||||
				prefer.commit('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
 | 
			
		||||
@@ -247,7 +223,10 @@ export async function mainBoot() {
 | 
			
		||||
				prefer.commit('sound.on.noteMy', store.s.sound_noteMy as any);
 | 
			
		||||
				prefer.commit('sound.on.notification', store.s.sound_notification as any);
 | 
			
		||||
				prefer.commit('sound.on.reaction', store.s.sound_reaction as any);
 | 
			
		||||
				store.set('menu', []);
 | 
			
		||||
				store.commit('deck.profile', deckStore.s.profile);
 | 
			
		||||
				store.commit('deck.columns', deckStore.s.columns);
 | 
			
		||||
				store.commit('deck.layout', deckStore.s.layout);
 | 
			
		||||
				store.commit('menu', []);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (store.s.accountSetupWizard !== -1) {
 | 
			
		||||
@@ -523,7 +502,7 @@ export async function mainBoot() {
 | 
			
		||||
			post();
 | 
			
		||||
		},
 | 
			
		||||
		'd': () => {
 | 
			
		||||
			store.set('darkMode', !store.s.darkMode);
 | 
			
		||||
			store.commit('darkMode', !store.s.darkMode);
 | 
			
		||||
		},
 | 
			
		||||
		's': () => {
 | 
			
		||||
			mainRouter.push('/search');
 | 
			
		||||
 
 | 
			
		||||
@@ -158,7 +158,7 @@ function complete(type: string, value: any) {
 | 
			
		||||
		let recents = store.s.recentlyUsedEmojis;
 | 
			
		||||
		recents = recents.filter((emoji: any) => emoji !== value);
 | 
			
		||||
		recents.unshift(value);
 | 
			
		||||
		store.set('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
		store.commit('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -220,28 +220,28 @@ function onMousedown(evt: MouseEvent): void {
 | 
			
		||||
		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 | 
			
		||||
 | 
			
		||||
		&:not(:disabled):hover {
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&:not(:disabled):active {
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.danger {
 | 
			
		||||
		font-weight: bold;
 | 
			
		||||
		color: var(--MI_THEME-error);
 | 
			
		||||
		color: #ff2a2a;
 | 
			
		||||
 | 
			
		||||
		&.primary {
 | 
			
		||||
			color: #fff;
 | 
			
		||||
			background: var(--MI_THEME-error);
 | 
			
		||||
			background: #ff2a2a;
 | 
			
		||||
 | 
			
		||||
			&:not(:disabled):hover {
 | 
			
		||||
				background: hsl(from var(--MI_THEME-error) h s calc(l + 10));
 | 
			
		||||
				background: #ff4242;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:not(:disabled):active {
 | 
			
		||||
				background: hsl(from var(--MI_THEME-error) h s calc(l - 10));
 | 
			
		||||
				background: #d42e2e;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -432,7 +432,7 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef,
 | 
			
		||||
		let recents = store.s.recentlyUsedEmojis;
 | 
			
		||||
		recents = recents.filter((emoji) => emoji !== key);
 | 
			
		||||
		recents.unshift(key);
 | 
			
		||||
		store.set('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
		store.commit('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,43 +0,0 @@
 | 
			
		||||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div v-panel :class="$style.root">
 | 
			
		||||
	<img :class="$style.img" :src="icon"/>
 | 
			
		||||
	<div :class="$style.text">
 | 
			
		||||
		<slot></slot>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
withDefaults(defineProps<{
 | 
			
		||||
	icon: string;
 | 
			
		||||
	color: string;
 | 
			
		||||
}>(), {
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style module lang="scss">
 | 
			
		||||
.root {
 | 
			
		||||
	padding: 20px 24px;
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	border-radius: var(--MI-radius);
 | 
			
		||||
	background: linear-gradient(180deg, color(from v-bind(color) srgb r g b / 0.1), color(from v-bind(color) srgb r g b / 0));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.img {
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: 0 auto;
 | 
			
		||||
	width: 40px;
 | 
			
		||||
	aspect-ratio: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text {
 | 
			
		||||
	margin-top: 12px;
 | 
			
		||||
	font-size: 85%;
 | 
			
		||||
	mix-blend-mode: luminosity;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -177,12 +177,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue';
 | 
			
		||||
import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
 | 
			
		||||
import type { Keymap } from '@/utility/hotkey.js';
 | 
			
		||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
 | 
			
		||||
import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { isTouchUsing } from '@/utility/touch.js';
 | 
			
		||||
import type { Keymap } from '@/utility/hotkey.js';
 | 
			
		||||
import { isFocusable } from '@/utility/focus.js';
 | 
			
		||||
import { getNodeOrNull } from '@/utility/get-dom-node-or-null.js';
 | 
			
		||||
 | 
			
		||||
@@ -558,11 +558,11 @@ onBeforeUnmount(() => {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.danger {
 | 
			
		||||
		--menuFg: var(--MI_THEME-error);
 | 
			
		||||
		--menuFg: #ff2a2a;
 | 
			
		||||
		--menuHoverFg: #fff;
 | 
			
		||||
		--menuHoverBg: var(--MI_THEME-error);
 | 
			
		||||
		--menuHoverBg: #ff4242;
 | 
			
		||||
		--menuActiveFg: #fff;
 | 
			
		||||
		--menuActiveBg: hsl(from var(--MI_THEME-error) h s calc(l - 10));
 | 
			
		||||
		--menuActiveBg: #d42e2e;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.radio {
 | 
			
		||||
 
 | 
			
		||||
@@ -32,20 +32,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
 | 
			
		||||
import { url } from '@@/js/config.js';
 | 
			
		||||
import { getScrollContainer } from '@@/js/scroll.js';
 | 
			
		||||
import type { PageMetadata } from '@/page.js';
 | 
			
		||||
import type { PageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import RouterView from '@/components/global/RouterView.vue';
 | 
			
		||||
import MkWindow from '@/components/MkWindow.vue';
 | 
			
		||||
import { popout as _popout } from '@/utility/popout.js';
 | 
			
		||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 | 
			
		||||
import { useScrollPositionManager } from '@/nirax.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
 | 
			
		||||
import { provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { openingWindowsCount } from '@/os.js';
 | 
			
		||||
import { claimAchievement } from '@/utility/achievements.js';
 | 
			
		||||
import { useRouterFactory } from '@/router/supplier.js';
 | 
			
		||||
import { mainRouter } from '@/router/main.js';
 | 
			
		||||
import { analytics } from '@/analytics.js';
 | 
			
		||||
import { DI } from '@/di.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	initialPath: string;
 | 
			
		||||
@@ -120,7 +119,7 @@ windowRouter.addListener('change', ctx => {
 | 
			
		||||
 | 
			
		||||
windowRouter.init();
 | 
			
		||||
 | 
			
		||||
provide(DI.router, windowRouter);
 | 
			
		||||
provide('router', windowRouter);
 | 
			
		||||
provide('inAppSearchMarkerId', searchMarkerId);
 | 
			
		||||
provideMetadataReceiver((metadataGetter) => {
 | 
			
		||||
	const info = metadataGetter();
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
	<MkSpacer :marginMin="20" :marginMax="28">
 | 
			
		||||
		<div style="padding: 0 0 16px 0; text-align: center;">
 | 
			
		||||
			<img src="/client-assets/locked_with_key_3d.png" alt="🔐" style="display: block; margin: 0 auto; width: 48px;">
 | 
			
		||||
			<img src="/fluent-emoji/1f510.png" alt="🔐" style="display: block; margin: 0 auto; width: 48px;">
 | 
			
		||||
			<div style="margin-top: 16px;">{{ i18n.ts.authenticationRequiredToContinue }}</div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -177,7 +177,7 @@ const files = ref(props.initialFiles ?? []);
 | 
			
		||||
const poll = ref<PollEditorModelValue | null>(null);
 | 
			
		||||
const useCw = ref<boolean>(!!props.initialCw);
 | 
			
		||||
const showPreview = ref(store.s.showPreview);
 | 
			
		||||
watch(showPreview, () => store.set('showPreview', showPreview.value));
 | 
			
		||||
watch(showPreview, () => store.commit('showPreview', showPreview.value));
 | 
			
		||||
const showAddMfmFunction = ref(prefer.s.enableQuickAddMfmFunction);
 | 
			
		||||
watch(showAddMfmFunction, () => prefer.commit('enableQuickAddMfmFunction', showAddMfmFunction.value));
 | 
			
		||||
const cw = ref<string | null>(props.initialCw ?? null);
 | 
			
		||||
@@ -265,19 +265,13 @@ const canPost = computed((): boolean => {
 | 
			
		||||
			quoteId.value != null
 | 
			
		||||
		) &&
 | 
			
		||||
		(textLength.value <= maxTextLength.value) &&
 | 
			
		||||
		(
 | 
			
		||||
			useCw.value ?
 | 
			
		||||
				(
 | 
			
		||||
					cw.value != null && cw.value.trim() !== '' &&
 | 
			
		||||
					cwTextLength.value <= maxCwTextLength
 | 
			
		||||
				) : true
 | 
			
		||||
		) &&
 | 
			
		||||
		(cwTextLength.value <= maxCwTextLength) &&
 | 
			
		||||
		(files.value.length <= 16) &&
 | 
			
		||||
		(!poll.value || poll.value.choices.length >= 2);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const withHashtags = computed(store.makeGetterSetter('postFormWithHashtags'));
 | 
			
		||||
const hashtags = computed(store.makeGetterSetter('postFormHashtags'));
 | 
			
		||||
const withHashtags = store.model('postFormWithHashtags');
 | 
			
		||||
const hashtags = store.model('postFormHashtags');
 | 
			
		||||
 | 
			
		||||
watch(text, () => {
 | 
			
		||||
	checkMissingMention();
 | 
			
		||||
@@ -486,7 +480,7 @@ function setVisibility() {
 | 
			
		||||
		changeVisibility: v => {
 | 
			
		||||
			visibility.value = v;
 | 
			
		||||
			if (prefer.s.rememberNoteVisibility) {
 | 
			
		||||
				store.set('visibility', visibility.value);
 | 
			
		||||
				store.commit('visibility', visibility.value);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		closed: () => dispose(),
 | 
			
		||||
@@ -534,7 +528,7 @@ async function toggleLocalOnly() {
 | 
			
		||||
 | 
			
		||||
	localOnly.value = !localOnly.value;
 | 
			
		||||
	if (prefer.s.rememberNoteVisibility) {
 | 
			
		||||
		store.set('localOnly', localOnly.value);
 | 
			
		||||
		store.commit('localOnly', localOnly.value);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -750,6 +744,14 @@ function isAnnoying(text: string): boolean {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function post(ev?: MouseEvent) {
 | 
			
		||||
	if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
 | 
			
		||||
		os.alert({
 | 
			
		||||
			type: 'error',
 | 
			
		||||
			text: i18n.ts.cwNotationRequired,
 | 
			
		||||
		});
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (ev) {
 | 
			
		||||
		const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,15 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div :class="$style.root" @contextmenu.prevent.stop="showMenu($event, true)">
 | 
			
		||||
<div :class="$style.root">
 | 
			
		||||
	<div :class="$style.body">
 | 
			
		||||
		<slot></slot>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div :class="$style.menu">
 | 
			
		||||
		<i v-if="isSyncEnabled" class="ti ti-cloud-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i>
 | 
			
		||||
		<i v-if="isAccountOverrided" class="ti ti-user-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i>
 | 
			
		||||
		<div :class="$style.buttons">
 | 
			
		||||
			<button class="_button" style="color: var(--MI_THEME-fg)" @click="showMenu($event)"><i class="ti ti-dots"></i></button>
 | 
			
		||||
			<button class="_button" style="color: var(--MI_THEME-fg)" @click="showMenu"><i class="ti ti-dots"></i></button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -22,32 +21,24 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import type { PREF_DEF } from '@/preferences/def.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
import { profileManager } from '@/preferences.js';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	k: keyof typeof PREF_DEF;
 | 
			
		||||
}>(), {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const isAccountOverrided = ref(prefer.isAccountOverrided(props.k));
 | 
			
		||||
const isSyncEnabled = ref(prefer.isSyncEnabled(props.k));
 | 
			
		||||
const isAccountOverrided = ref(profileManager.isAccountOverrided(props.k));
 | 
			
		||||
 | 
			
		||||
function showMenu(ev: MouseEvent, contextmenu?: boolean) {
 | 
			
		||||
function showMenu(ev: MouseEvent) {
 | 
			
		||||
	const i = window.setInterval(() => {
 | 
			
		||||
		isAccountOverrided.value = prefer.isAccountOverrided(props.k);
 | 
			
		||||
		isSyncEnabled.value = prefer.isSyncEnabled(props.k);
 | 
			
		||||
		isAccountOverrided.value = profileManager.isAccountOverrided(props.k);
 | 
			
		||||
	}, 100);
 | 
			
		||||
	if (contextmenu) {
 | 
			
		||||
		os.contextMenu(prefer.getPerPrefMenu(props.k), ev).then(() => {
 | 
			
		||||
	os.popupMenu(profileManager.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, {
 | 
			
		||||
		onClosing: () => {
 | 
			
		||||
			window.clearInterval(i);
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		os.popupMenu(prefer.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, {
 | 
			
		||||
			onClosing: () => {
 | 
			
		||||
				window.clearInterval(i);
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,96 +0,0 @@
 | 
			
		||||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<svg
 | 
			
		||||
	version="1.1"
 | 
			
		||||
	viewBox="0 0 203.2 152.4"
 | 
			
		||||
	xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
	xmlns:xlink="http://www.w3.org/1999/xlink"
 | 
			
		||||
>
 | 
			
		||||
	<g fill-rule="evenodd">
 | 
			
		||||
		<rect width="203.2" height="152.4" :fill="themeVariables.bg" stroke-width=".26458" />
 | 
			
		||||
		<rect width="65.498" height="152.4" :fill="themeVariables.panel" stroke-width=".26458" />
 | 
			
		||||
		<rect x="65.498" width="137.7" height="40.892" :fill="themeVariables.acrylicBg" stroke-width=".265" />
 | 
			
		||||
		<path transform="scale(.26458)" d="m439.77 247.19c-43.673 0-78.832 35.157-78.832 78.83v249.98h407.06v-328.81z" :fill="themeVariables.panel" />
 | 
			
		||||
	</g>
 | 
			
		||||
	<circle cx="32.749" cy="83.054" r="21.132" :fill="themeVariables.accentedBg" stroke-dasharray="0.319256, 0.319256" stroke-width=".15963" style="paint-order:stroke fill markers" />
 | 
			
		||||
	<circle cx="136.67" cy="106.76" r="23.876" :fill="themeVariables.fg" fill-opacity="0.5" stroke-dasharray="0.352425, 0.352425" stroke-width=".17621" style="paint-order:stroke fill markers" />
 | 
			
		||||
	<g :fill="themeVariables.fg" fill-rule="evenodd" stroke-width=".26458">
 | 
			
		||||
		<rect x="171.27" y="87.815" width="48.576" height="6.8747" ry="3.4373"/>
 | 
			
		||||
		<rect x="171.27" y="105.09" width="48.576" height="6.875" ry="3.4375"/>
 | 
			
		||||
		<rect x="171.27" y="121.28" width="48.576" height="6.875" ry="3.4375"/>
 | 
			
		||||
		<rect x="171.27" y="137.47" width="48.576" height="6.875" ry="3.4375"/>
 | 
			
		||||
	</g>
 | 
			
		||||
	<path d="m65.498 40.892h137.7" :stroke="themeVariables.divider" stroke-width="0.75" />
 | 
			
		||||
	<g transform="matrix(.60823 0 0 .60823 25.45 75.755)" fill="none" :stroke="themeVariables.accent" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
 | 
			
		||||
		<path d="m0 0h24v24h-24z" fill="none" stroke="none" />
 | 
			
		||||
		<path d="m5 12h-2l9-9 9 9h-2" />
 | 
			
		||||
		<path d="m5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7" />
 | 
			
		||||
		<path d="m9 21v-6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v6" />
 | 
			
		||||
	</g>
 | 
			
		||||
	<g transform="matrix(.61621 0 0 .61621 25.354 117.92)" fill="none" :stroke="themeVariables.fg" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
 | 
			
		||||
		<path d="m0 0h24v24h-24z" fill="none" stroke="none" />
 | 
			
		||||
		<path d="m10 5a2 2 0 1 1 4 0 7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6" />
 | 
			
		||||
		<path d="m9 17v1a3 3 0 0 0 6 0v-1" />
 | 
			
		||||
	</g>
 | 
			
		||||
	<image x="20.948" y="18.388" width="23.602" height="23.602" image-rendering="optimizeSpeed" preserveAspectRatio="xMidYMid meet" v-bind="{ 'xlink:href': instance.iconUrl || '/favicon.ico' }" />
 | 
			
		||||
</svg>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, watch } from 'vue';
 | 
			
		||||
import { instance } from '@/instance.js';
 | 
			
		||||
import { compile } from '@/theme.js';
 | 
			
		||||
import type { Theme } from '@/theme.js';
 | 
			
		||||
import { deepClone } from '@/utility/clone.js';
 | 
			
		||||
import lightTheme from '@@/themes/_light.json5';
 | 
			
		||||
import darkTheme from '@@/themes/_dark.json5';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	theme: Theme;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const themeVariables = ref<{
 | 
			
		||||
	bg: string;
 | 
			
		||||
	acrylicBg: string;
 | 
			
		||||
	panel: string;
 | 
			
		||||
	fg: string;
 | 
			
		||||
	divider: string;
 | 
			
		||||
	accent: string;
 | 
			
		||||
	accentedBg: string;
 | 
			
		||||
}>({
 | 
			
		||||
	bg: 'var(--MI_THEME-bg)',
 | 
			
		||||
	acrylicBg: 'var(--MI_THEME-acrylicBg)',
 | 
			
		||||
	panel: 'var(--MI_THEME-panel)',
 | 
			
		||||
	fg: 'var(--MI_THEME-fg)',
 | 
			
		||||
	divider: 'var(--MI_THEME-divider)',
 | 
			
		||||
	accent: 'var(--MI_THEME-accent)',
 | 
			
		||||
	accentedBg: 'var(--MI_THEME-accentedBg)',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(() => props.theme, (theme) => {
 | 
			
		||||
	if (theme == null) return;
 | 
			
		||||
 | 
			
		||||
	const _theme = deepClone(theme);
 | 
			
		||||
 | 
			
		||||
	if (_theme?.base != null) {
 | 
			
		||||
		const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
 | 
			
		||||
		if (base) _theme.props = Object.assign({}, base.props, _theme.props);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const compiled = compile(_theme);
 | 
			
		||||
 | 
			
		||||
	themeVariables.value = {
 | 
			
		||||
		bg: compiled.bg ?? 'var(--MI_THEME-bg)',
 | 
			
		||||
		acrylicBg: compiled.acrylicBg ?? 'var(--MI_THEME-acrylicBg)',
 | 
			
		||||
		panel: compiled.panel ?? 'var(--MI_THEME-panel)',
 | 
			
		||||
		fg: compiled.fg ?? 'var(--MI_THEME-fg)',
 | 
			
		||||
		divider: compiled.divider ?? 'var(--MI_THEME-divider)',
 | 
			
		||||
		accent: compiled.accent ?? 'var(--MI_THEME-accent)',
 | 
			
		||||
		accentedBg: compiled.accentedBg ?? 'var(--MI_THEME-accentedBg)',
 | 
			
		||||
	};
 | 
			
		||||
}, { immediate: true });
 | 
			
		||||
</script>
 | 
			
		||||
@@ -131,7 +131,7 @@ async function ok() {
 | 
			
		||||
	let recents = store.s.recentlyUsedUsers;
 | 
			
		||||
	recents = recents.filter(x => x !== selected.value?.id);
 | 
			
		||||
	recents.unshift(selected.value.id);
 | 
			
		||||
	store.set('recentlyUsedUsers', recents.splice(0, 16));
 | 
			
		||||
	store.commit('recentlyUsedUsers', recents.splice(0, 16));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cancel() {
 | 
			
		||||
 
 | 
			
		||||
@@ -152,7 +152,7 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 | 
			
		||||
const page = ref(store.s.accountSetupWizard);
 | 
			
		||||
 | 
			
		||||
watch(page, () => {
 | 
			
		||||
	store.set('accountSetupWizard', page.value);
 | 
			
		||||
	store.commit('accountSetupWizard', page.value);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function close(skip: boolean) {
 | 
			
		||||
@@ -165,11 +165,11 @@ async function close(skip: boolean) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dialog.value?.close();
 | 
			
		||||
	store.set('accountSetupWizard', -1);
 | 
			
		||||
	store.commit('accountSetupWizard', -1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupComplete() {
 | 
			
		||||
	store.set('accountSetupWizard', -1);
 | 
			
		||||
	store.commit('accountSetupWizard', -1);
 | 
			
		||||
	dialog.value?.close();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -194,7 +194,7 @@ async function later(later: boolean) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dialog.value?.close();
 | 
			
		||||
	store.set('accountSetupWizard', 0);
 | 
			
		||||
	store.commit('accountSetupWizard', 0);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -113,7 +113,7 @@ const shouldHide = ref(!prefer.s.forceShowAds && $i && $i.policies.canHideAds &&
 | 
			
		||||
function reduceFrequency(): void {
 | 
			
		||||
	if (chosen.value == null) return;
 | 
			
		||||
	if (store.s.mutedAds.includes(chosen.value.id)) return;
 | 
			
		||||
	store.push('mutedAds', chosen.value.id);
 | 
			
		||||
	store.commit('mutedAds', [...store.s.mutedAds, chosen.value.id]);
 | 
			
		||||
	os.success();
 | 
			
		||||
	chosen.value = choseAd();
 | 
			
		||||
	showMenu.value = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -47,9 +47,9 @@ import { scrollToTop } from '@@/js/scroll.js';
 | 
			
		||||
import XTabs from './MkPageHeader.tabs.vue';
 | 
			
		||||
import type { Tab } from './MkPageHeader.tabs.vue';
 | 
			
		||||
import type { PageHeaderItem } from '@/types/page-header.js';
 | 
			
		||||
import type { PageMetadata } from '@/page.js';
 | 
			
		||||
import type { PageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { globalEvents } from '@/events.js';
 | 
			
		||||
import { injectReactiveMetadata } from '@/page.js';
 | 
			
		||||
import { injectReactiveMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
 
 | 
			
		||||
@@ -24,21 +24,20 @@ import type { IRouter, Resolved, RouteDef } from '@/nirax.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
import { globalEvents } from '@/events.js';
 | 
			
		||||
import MkLoadingPage from '@/pages/_loading_.vue';
 | 
			
		||||
import { DI } from '@/di.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	router?: IRouter;
 | 
			
		||||
	nested?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const router = props.router ?? inject(DI.router);
 | 
			
		||||
const router = props.router ?? inject('router');
 | 
			
		||||
 | 
			
		||||
if (router == null) {
 | 
			
		||||
	throw new Error('no router provided');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const currentDepth = inject(DI.routerCurrentDepth, 0);
 | 
			
		||||
provide(DI.routerCurrentDepth, currentDepth + 1);
 | 
			
		||||
const currentDepth = inject('routerCurrentDepth', 0);
 | 
			
		||||
provide('routerCurrentDepth', currentDepth + 1);
 | 
			
		||||
 | 
			
		||||
function resolveNested(current: Resolved, d = 0): Resolved | null {
 | 
			
		||||
	if (!props.nested) return current;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,23 +3,13 @@
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { throttle } from 'throttle-debounce';
 | 
			
		||||
import { notificationTypes } from 'misskey-js';
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import { i18n } from './i18n.js';
 | 
			
		||||
import type { BasicTimelineType } from '@/timelines.js';
 | 
			
		||||
import type { SoundStore } from '@/preferences/def.js';
 | 
			
		||||
import type { MenuItem } from '@/types/menu.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { deepClone } from '@/utility/clone.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
 | 
			
		||||
export type DeckProfile = {
 | 
			
		||||
	name: string;
 | 
			
		||||
	id: string;
 | 
			
		||||
	columns: Column[];
 | 
			
		||||
	layout: Column['id'][][];
 | 
			
		||||
};
 | 
			
		||||
import { store } from '@/store.js';
 | 
			
		||||
 | 
			
		||||
type ColumnWidget = {
 | 
			
		||||
	name: string;
 | 
			
		||||
@@ -63,132 +53,127 @@ export type Column = {
 | 
			
		||||
	soundSetting?: SoundStore;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const _currentProfile = prefer.s['deck.profiles'].find(p => p.name === prefer.s['deck.profile']);
 | 
			
		||||
const __currentProfile = _currentProfile ? deepClone(_currentProfile) : null;
 | 
			
		||||
export const columns = ref(__currentProfile ? __currentProfile.columns : []);
 | 
			
		||||
export const layout = ref(__currentProfile ? __currentProfile.layout : []);
 | 
			
		||||
export const loadDeck = async () => {
 | 
			
		||||
	let deck;
 | 
			
		||||
 | 
			
		||||
if (prefer.s['deck.profile'] == null) {
 | 
			
		||||
	addProfile('Main');
 | 
			
		||||
}
 | 
			
		||||
	try {
 | 
			
		||||
		deck = await misskeyApi('i/registry/get', {
 | 
			
		||||
			scope: ['client', 'deck', 'profiles'],
 | 
			
		||||
			key: store.s['deck.profile'],
 | 
			
		||||
		});
 | 
			
		||||
	} catch (err) {
 | 
			
		||||
		if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') {
 | 
			
		||||
			// 後方互換性のため
 | 
			
		||||
			if (store.s['deck.profile'] === 'default') {
 | 
			
		||||
				saveDeck();
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
export function forceSaveCurrentDeckProfile() {
 | 
			
		||||
	const currentProfile = prefer.s['deck.profiles'].find(p => p.name === prefer.s['deck.profile']);
 | 
			
		||||
	if (currentProfile == null) return;
 | 
			
		||||
			store.commit('deck.columns', []);
 | 
			
		||||
			store.commit('deck.layout', []);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		throw err;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const newProfile = deepClone(currentProfile);
 | 
			
		||||
	newProfile.columns = columns.value;
 | 
			
		||||
	newProfile.layout = layout.value;
 | 
			
		||||
 | 
			
		||||
	const newProfiles = prefer.s['deck.profiles'].filter(p => p.name !== prefer.s['deck.profile']);
 | 
			
		||||
	newProfiles.push(newProfile);
 | 
			
		||||
	prefer.commit('deck.profiles', newProfiles);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const saveCurrentDeckProfile = () => {
 | 
			
		||||
	forceSaveCurrentDeckProfile();
 | 
			
		||||
	store.commit('deck.columns', deck.columns);
 | 
			
		||||
	store.commit('deck.layout', deck.layout);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function switchProfile(profile: DeckProfile) {
 | 
			
		||||
	prefer.commit('deck.profile', profile.name);
 | 
			
		||||
	const currentProfile = deepClone(profile);
 | 
			
		||||
	columns.value = currentProfile.columns;
 | 
			
		||||
	layout.value = currentProfile.layout;
 | 
			
		||||
	forceSaveCurrentDeckProfile();
 | 
			
		||||
export async function forceSaveDeck() {
 | 
			
		||||
	await misskeyApi('i/registry/set', {
 | 
			
		||||
		scope: ['client', 'deck', 'profiles'],
 | 
			
		||||
		key: store.s['deck.profile'],
 | 
			
		||||
		value: {
 | 
			
		||||
			columns: store.r['deck.columns'].value,
 | 
			
		||||
			layout: store.r['deck.layout'].value,
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addProfile(name: string) {
 | 
			
		||||
	if (name.trim() === '') return;
 | 
			
		||||
	if (prefer.s['deck.profiles'].find(p => p.name === name)) return;
 | 
			
		||||
// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
 | 
			
		||||
export const saveDeck = throttle(1000, () => {
 | 
			
		||||
	forceSaveDeck();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
	const newProfile: DeckProfile = {
 | 
			
		||||
		id: uuid(),
 | 
			
		||||
		name,
 | 
			
		||||
		columns: [],
 | 
			
		||||
		layout: [],
 | 
			
		||||
	};
 | 
			
		||||
	prefer.commit('deck.profiles', [...prefer.s['deck.profiles'], newProfile]);
 | 
			
		||||
	switchProfile(newProfile);
 | 
			
		||||
export async function getProfiles(): Promise<string[]> {
 | 
			
		||||
	return await misskeyApi('i/registry/keys', {
 | 
			
		||||
		scope: ['client', 'deck', 'profiles'],
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createFirstProfile() {
 | 
			
		||||
	addProfile('Main');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function deleteProfile(name: string): void {
 | 
			
		||||
	const newProfiles = prefer.s['deck.profiles'].filter(p => p.name !== name);
 | 
			
		||||
	prefer.commit('deck.profiles', newProfiles);
 | 
			
		||||
 | 
			
		||||
	if (prefer.s['deck.profiles'].length === 0) {
 | 
			
		||||
		createFirstProfile();
 | 
			
		||||
	} else {
 | 
			
		||||
		switchProfile(prefer.s['deck.profiles'][0]);
 | 
			
		||||
	}
 | 
			
		||||
export async function deleteProfile(key: string): Promise<void> {
 | 
			
		||||
	return await misskeyApi('i/registry/remove', {
 | 
			
		||||
		scope: ['client', 'deck', 'profiles'],
 | 
			
		||||
		key: key,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function addColumn(column: Column) {
 | 
			
		||||
	if (column.name === undefined) column.name = null;
 | 
			
		||||
	columns.value.push(column);
 | 
			
		||||
	layout.value.push([column.id]);
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	store.commit('deck.columns', [...store.s['deck.columns'], column]);
 | 
			
		||||
	store.commit('deck.layout', [...store.s['deck.layout'], [column.id]]);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removeColumn(id: Column['id']) {
 | 
			
		||||
	columns.value = columns.value.filter(c => c.id !== id);
 | 
			
		||||
	layout.value = layout.value.map(ids => ids.filter(_id => _id !== id)).filter(ids => ids.length > 0);
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	store.commit('deck.columns', store.s['deck.columns'].filter(c => c.id !== id));
 | 
			
		||||
	store.commit('deck.layout', store.s['deck.layout']
 | 
			
		||||
		.map(ids => ids.filter(_id => _id !== id))
 | 
			
		||||
		.filter(ids => ids.length > 0));
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function swapColumn(a: Column['id'], b: Column['id']) {
 | 
			
		||||
	const aX = layout.value.findIndex(ids => ids.indexOf(a) !== -1);
 | 
			
		||||
	const aY = layout.value[aX].findIndex(id => id === a);
 | 
			
		||||
	const bX = layout.value.findIndex(ids => ids.indexOf(b) !== -1);
 | 
			
		||||
	const bY = layout.value[bX].findIndex(id => id === b);
 | 
			
		||||
	const newLayout = deepClone(layout.value);
 | 
			
		||||
	newLayout[aX][aY] = b;
 | 
			
		||||
	newLayout[bX][bY] = a;
 | 
			
		||||
	layout.value = newLayout;
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	const aX = store.s['deck.layout'].findIndex(ids => ids.indexOf(a) !== -1);
 | 
			
		||||
	const aY = store.s['deck.layout'][aX].findIndex(id => id === a);
 | 
			
		||||
	const bX = store.s['deck.layout'].findIndex(ids => ids.indexOf(b) !== -1);
 | 
			
		||||
	const bY = store.s['deck.layout'][bX].findIndex(id => id === b);
 | 
			
		||||
	const layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	layout[aX][aY] = b;
 | 
			
		||||
	layout[bX][bY] = a;
 | 
			
		||||
	store.commit('deck.layout', layout);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function swapLeftColumn(id: Column['id']) {
 | 
			
		||||
	const newLayout = deepClone(layout.value);
 | 
			
		||||
	layout.value.some((ids, i) => {
 | 
			
		||||
	const layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	store.s['deck.layout'].some((ids, i) => {
 | 
			
		||||
		if (ids.includes(id)) {
 | 
			
		||||
			const left = layout.value[i - 1];
 | 
			
		||||
			const left = store.s['deck.layout'][i - 1];
 | 
			
		||||
			if (left) {
 | 
			
		||||
				newLayout[i - 1] = layout.value[i];
 | 
			
		||||
				newLayout[i] = left;
 | 
			
		||||
				layout.value = newLayout;
 | 
			
		||||
				layout[i - 1] = store.s['deck.layout'][i];
 | 
			
		||||
				layout[i] = left;
 | 
			
		||||
				store.commit('deck.layout', layout);
 | 
			
		||||
			}
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
	});
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function swapRightColumn(id: Column['id']) {
 | 
			
		||||
	const newLayout = deepClone(layout.value);
 | 
			
		||||
	layout.value.some((ids, i) => {
 | 
			
		||||
	const layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	store.s['deck.layout'].some((ids, i) => {
 | 
			
		||||
		if (ids.includes(id)) {
 | 
			
		||||
			const right = layout.value[i + 1];
 | 
			
		||||
			const right = store.s['deck.layout'][i + 1];
 | 
			
		||||
			if (right) {
 | 
			
		||||
				newLayout[i + 1] = layout.value[i];
 | 
			
		||||
				newLayout[i] = right;
 | 
			
		||||
				layout.value = newLayout;
 | 
			
		||||
				layout[i + 1] = store.s['deck.layout'][i];
 | 
			
		||||
				layout[i] = right;
 | 
			
		||||
				store.commit('deck.layout', layout);
 | 
			
		||||
			}
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
	});
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function swapUpColumn(id: Column['id']) {
 | 
			
		||||
	const newLayout = deepClone(layout.value);
 | 
			
		||||
	const idsIndex = layout.value.findIndex(ids => ids.includes(id));
 | 
			
		||||
	const ids = deepClone(layout.value[idsIndex]);
 | 
			
		||||
	const layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	const ids = deepClone(store.s['deck.layout'][idsIndex]);
 | 
			
		||||
	ids.some((x, i) => {
 | 
			
		||||
		if (x === id) {
 | 
			
		||||
			const up = ids[i - 1];
 | 
			
		||||
@@ -196,20 +181,20 @@ export function swapUpColumn(id: Column['id']) {
 | 
			
		||||
				ids[i - 1] = id;
 | 
			
		||||
				ids[i] = up;
 | 
			
		||||
 | 
			
		||||
				newLayout[idsIndex] = ids;
 | 
			
		||||
				layout.value = newLayout;
 | 
			
		||||
				layout[idsIndex] = ids;
 | 
			
		||||
				store.commit('deck.layout', layout);
 | 
			
		||||
			}
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
	});
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function swapDownColumn(id: Column['id']) {
 | 
			
		||||
	const newLayout = deepClone(layout.value);
 | 
			
		||||
	const idsIndex = layout.value.findIndex(ids => ids.includes(id));
 | 
			
		||||
	const ids = deepClone(layout.value[idsIndex]);
 | 
			
		||||
	const layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	const ids = deepClone(store.s['deck.layout'][idsIndex]);
 | 
			
		||||
	ids.some((x, i) => {
 | 
			
		||||
		if (x === id) {
 | 
			
		||||
			const down = ids[i + 1];
 | 
			
		||||
@@ -217,137 +202,105 @@ export function swapDownColumn(id: Column['id']) {
 | 
			
		||||
				ids[i + 1] = id;
 | 
			
		||||
				ids[i] = down;
 | 
			
		||||
 | 
			
		||||
				newLayout[idsIndex] = ids;
 | 
			
		||||
				layout.value = newLayout;
 | 
			
		||||
				layout[idsIndex] = ids;
 | 
			
		||||
				store.commit('deck.layout', layout);
 | 
			
		||||
			}
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
	});
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function stackLeftColumn(id: Column['id']) {
 | 
			
		||||
	let newLayout = deepClone(layout.value);
 | 
			
		||||
	const i = layout.value.findIndex(ids => ids.includes(id));
 | 
			
		||||
	newLayout = newLayout.map(ids => ids.filter(_id => _id !== id));
 | 
			
		||||
	newLayout[i - 1].push(id);
 | 
			
		||||
	newLayout = newLayout.filter(ids => ids.length > 0);
 | 
			
		||||
	layout.value = newLayout;
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	let layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	const i = store.s['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	layout = layout.map(ids => ids.filter(_id => _id !== id));
 | 
			
		||||
	layout[i - 1].push(id);
 | 
			
		||||
	layout = layout.filter(ids => ids.length > 0);
 | 
			
		||||
	store.commit('deck.layout', layout);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function popRightColumn(id: Column['id']) {
 | 
			
		||||
	let newLayout = deepClone(layout.value);
 | 
			
		||||
	const i = layout.value.findIndex(ids => ids.includes(id));
 | 
			
		||||
	const affected = newLayout[i];
 | 
			
		||||
	newLayout = newLayout.map(ids => ids.filter(_id => _id !== id));
 | 
			
		||||
	newLayout.splice(i + 1, 0, [id]);
 | 
			
		||||
	newLayout = newLayout.filter(ids => ids.length > 0);
 | 
			
		||||
	layout.value = newLayout;
 | 
			
		||||
	let layout = deepClone(store.s['deck.layout']);
 | 
			
		||||
	const i = store.s['deck.layout'].findIndex(ids => ids.includes(id));
 | 
			
		||||
	const affected = layout[i];
 | 
			
		||||
	layout = layout.map(ids => ids.filter(_id => _id !== id));
 | 
			
		||||
	layout.splice(i + 1, 0, [id]);
 | 
			
		||||
	layout = layout.filter(ids => ids.length > 0);
 | 
			
		||||
	store.commit('deck.layout', layout);
 | 
			
		||||
 | 
			
		||||
	const newColumns = deepClone(columns.value);
 | 
			
		||||
	for (const column of newColumns) {
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	for (const column of columns) {
 | 
			
		||||
		if (affected.includes(column.id)) {
 | 
			
		||||
			column.active = true;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	columns.value = newColumns;
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
 | 
			
		||||
	const newColumns = deepClone(columns.value);
 | 
			
		||||
	const columnIndex = columns.value.findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(columns.value[columnIndex]);
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.s['deck.columns'][columnIndex]);
 | 
			
		||||
	if (column == null) return;
 | 
			
		||||
	if (column.widgets == null) column.widgets = [];
 | 
			
		||||
	column.widgets.unshift(widget);
 | 
			
		||||
	newColumns[columnIndex] = column;
 | 
			
		||||
	columns.value = newColumns;
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	columns[columnIndex] = column;
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
 | 
			
		||||
	const newColumns = deepClone(columns.value);
 | 
			
		||||
	const columnIndex = columns.value.findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(columns.value[columnIndex]);
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.s['deck.columns'][columnIndex]);
 | 
			
		||||
	if (column == null) return;
 | 
			
		||||
	if (column.widgets == null) column.widgets = [];
 | 
			
		||||
	column.widgets = column.widgets.filter(w => w.id !== widget.id);
 | 
			
		||||
	newColumns[columnIndex] = column;
 | 
			
		||||
	columns.value = newColumns;
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	columns[columnIndex] = column;
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
 | 
			
		||||
	const newColumns = deepClone(columns.value);
 | 
			
		||||
	const columnIndex = columns.value.findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(columns.value[columnIndex]);
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.s['deck.columns'][columnIndex]);
 | 
			
		||||
	if (column == null) return;
 | 
			
		||||
	column.widgets = widgets;
 | 
			
		||||
	newColumns[columnIndex] = column;
 | 
			
		||||
	columns.value = newColumns;
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	columns[columnIndex] = column;
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
 | 
			
		||||
	const newColumns = deepClone(columns.value);
 | 
			
		||||
	const columnIndex = columns.value.findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(columns.value[columnIndex]);
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const column = deepClone(store.s['deck.columns'][columnIndex]);
 | 
			
		||||
	if (column == null) return;
 | 
			
		||||
	if (column.widgets == null) column.widgets = [];
 | 
			
		||||
	column.widgets = column.widgets.map(w => w.id === widgetId ? {
 | 
			
		||||
		...w,
 | 
			
		||||
		data: widgetData,
 | 
			
		||||
	} : w);
 | 
			
		||||
	newColumns[columnIndex] = column;
 | 
			
		||||
	columns.value = newColumns;
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
	columns[columnIndex] = column;
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateColumn(id: Column['id'], column: Partial<Column>) {
 | 
			
		||||
	const newColumns = deepClone(columns.value);
 | 
			
		||||
	const columnIndex = columns.value.findIndex(c => c.id === id);
 | 
			
		||||
	const currentColumn = deepClone(columns.value[columnIndex]);
 | 
			
		||||
	const columns = deepClone(store.s['deck.columns']);
 | 
			
		||||
	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
 | 
			
		||||
	const currentColumn = deepClone(store.s['deck.columns'][columnIndex]);
 | 
			
		||||
	if (currentColumn == null) return;
 | 
			
		||||
	for (const [k, v] of Object.entries(column)) {
 | 
			
		||||
		currentColumn[k] = v;
 | 
			
		||||
	}
 | 
			
		||||
	newColumns[columnIndex] = currentColumn;
 | 
			
		||||
	columns.value = newColumns;
 | 
			
		||||
	saveCurrentDeckProfile();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function switchProfileMenu(ev: MouseEvent) {
 | 
			
		||||
	const items: MenuItem[] = prefer.s['deck.profile'] ? [{
 | 
			
		||||
		text: prefer.s['deck.profile'],
 | 
			
		||||
		active: true,
 | 
			
		||||
		action: () => {},
 | 
			
		||||
	}] : [];
 | 
			
		||||
 | 
			
		||||
	const profiles = prefer.s['deck.profiles'];
 | 
			
		||||
 | 
			
		||||
	items.push(...(profiles.filter(p => p.name !== prefer.s['deck.profile']).map(p => ({
 | 
			
		||||
		text: p.name,
 | 
			
		||||
		action: () => {
 | 
			
		||||
			switchProfile(p);
 | 
			
		||||
		},
 | 
			
		||||
	}))), { type: 'divider' as const }, {
 | 
			
		||||
		text: i18n.ts._deck.newProfile,
 | 
			
		||||
		icon: 'ti ti-plus',
 | 
			
		||||
		action: async () => {
 | 
			
		||||
			const { canceled, result: name } = await os.inputText({
 | 
			
		||||
				title: i18n.ts._deck.profile,
 | 
			
		||||
				minLength: 1,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (canceled || name == null || name.trim() === '') return;
 | 
			
		||||
 | 
			
		||||
			addProfile(name);
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	os.popupMenu(items, ev.currentTarget ?? ev.target);
 | 
			
		||||
	columns[columnIndex] = currentColumn;
 | 
			
		||||
	store.commit('deck.columns', columns);
 | 
			
		||||
	saveDeck();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import type { InjectionKey, Ref } from 'vue';
 | 
			
		||||
import type { IRouter } from '@/nirax.js';
 | 
			
		||||
 | 
			
		||||
export const DI = {
 | 
			
		||||
	routerCurrentDepth: Symbol() as InjectionKey<number>,
 | 
			
		||||
	router: Symbol() as InjectionKey<IRouter>,
 | 
			
		||||
};
 | 
			
		||||
@@ -33,7 +33,7 @@ import MkLink from '@/components/MkLink.vue';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { unisonReload } from '@/utility/unison-reload.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
import { serverErrorImageUrl } from '@/instance.js';
 | 
			
		||||
@@ -67,7 +67,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.error,
 | 
			
		||||
	icon: 'ti ti-alert-triangle',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -145,7 +145,7 @@ import { i18n } from '@/i18n.js';
 | 
			
		||||
import { instance } from '@/instance.js';
 | 
			
		||||
import { store } from '@/store.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
 | 
			
		||||
@@ -450,7 +450,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.aboutMisskey,
 | 
			
		||||
	icon: null,
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ import { computed, defineAsyncComponent, ref, watch } from 'vue';
 | 
			
		||||
import { instance } from '@/instance.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { claimAchievement } from '@/utility/achievements.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 | 
			
		||||
 | 
			
		||||
const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
 | 
			
		||||
@@ -81,7 +81,7 @@ const headerTabs = computed(() => {
 | 
			
		||||
	return items;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.instanceInfo,
 | 
			
		||||
	icon: 'ti ti-info-circle',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
 | 
			
		||||
import MkAchievements from '@/components/MkAchievements.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import { claimAchievement } from '@/utility/achievements.js';
 | 
			
		||||
 | 
			
		||||
@@ -48,7 +48,7 @@ onDeactivated(() => {
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.achievements,
 | 
			
		||||
	icon: 'ti ti-medal',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -85,7 +85,7 @@ import bytes from '@/filters/bytes.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { iAmAdmin, iAmModerator } from '@/account.js';
 | 
			
		||||
 | 
			
		||||
const tab = ref('overview');
 | 
			
		||||
@@ -161,7 +161,7 @@ const headerTabs = computed(() => [{
 | 
			
		||||
	icon: 'ti ti-code',
 | 
			
		||||
}]);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: file.value ? `${i18n.ts.file}: ${file.value.name}` : i18n.ts.file,
 | 
			
		||||
	icon: 'ti ti-file',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -231,7 +231,7 @@ import MkInfo from '@/components/MkInfo.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { acct } from '@/filters/user.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { iAmAdmin, $i, iAmModerator } from '@/account.js';
 | 
			
		||||
import MkRolePreview from '@/components/MkRolePreview.vue';
 | 
			
		||||
@@ -545,7 +545,7 @@ const headerTabs = computed(() => isSystem.value ? [{
 | 
			
		||||
	icon: 'ti ti-code',
 | 
			
		||||
}]);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: user.value ? acct(user.value) : i18n.ts.userInfo,
 | 
			
		||||
	icon: 'ti ti-user-exclamation',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ import { scrollToTop } from '@@/js/scroll.js';
 | 
			
		||||
import { popupMenu } from '@/os.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import { globalEvents } from '@/events.js';
 | 
			
		||||
import { injectReactiveMetadata } from '@/page.js';
 | 
			
		||||
import { injectReactiveMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
 | 
			
		||||
type Tab = {
 | 
			
		||||
	key?: string | null;
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ import MkSelect from '@/components/MkSelect.vue';
 | 
			
		||||
import MkPagination from '@/components/MkPagination.vue';
 | 
			
		||||
import XAbuseReport from '@/components/MkAbuseReport.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkInfo from '@/components/MkInfo.vue';
 | 
			
		||||
import { store } from '@/store.js';
 | 
			
		||||
@@ -93,14 +93,14 @@ function resolved(reportId) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function closeTutorial() {
 | 
			
		||||
	store.set('abusesTutorial', false);
 | 
			
		||||
	store.commit('abusesTutorial', false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.abuseReports,
 | 
			
		||||
	icon: 'ti ti-exclamation-circle',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -98,7 +98,7 @@ import FormSplit from '@/components/form/split.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
 | 
			
		||||
const ads = ref<Misskey.entities.Ad[]>([]);
 | 
			
		||||
 | 
			
		||||
@@ -255,7 +255,7 @@ const headerActions = computed(() => [{
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.ads,
 | 
			
		||||
	icon: 'ti ti-ad',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -96,7 +96,7 @@ import MkInfo from '@/components/MkInfo.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import MkTextarea from '@/components/MkTextarea.vue';
 | 
			
		||||
 | 
			
		||||
@@ -199,7 +199,7 @@ const headerActions = computed(() => [{
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.announcements,
 | 
			
		||||
	icon: 'ti ti-speakerphone',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -114,7 +114,7 @@ import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { instance, fetchInstance } from '@/instance.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkColorInput from '@/components/MkColorInput.vue';
 | 
			
		||||
import { host } from '@@/js/config.js';
 | 
			
		||||
@@ -175,7 +175,7 @@ function save() {
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.branding,
 | 
			
		||||
	icon: 'ti ti-paint',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import XGridLocalComponent from '@/pages/admin/custom-emojis-manager.local.vue';
 | 
			
		||||
import XGridRemoteComponent from '@/pages/admin/custom-emojis-manager.remote.vue';
 | 
			
		||||
import MkPageHeader from '@/components/global/MkPageHeader.vue';
 | 
			
		||||
@@ -36,7 +36,7 @@ const headerTabs = computed(() => [{
 | 
			
		||||
	title: i18n.ts.remote,
 | 
			
		||||
}]);
 | 
			
		||||
 | 
			
		||||
definePage(computed(() => ({
 | 
			
		||||
definePageMetadata(computed(() => ({
 | 
			
		||||
	title: i18n.ts.customEmojis,
 | 
			
		||||
	icon: 'ti ti-icons',
 | 
			
		||||
	needWideArea: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import bytes from '@/filters/bytes.js';
 | 
			
		||||
import number from '@/filters/number.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
 | 
			
		||||
const databasePromiseFactory = () => misskeyApi('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +33,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.database,
 | 
			
		||||
	icon: 'ti ti-database',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -76,7 +76,7 @@ import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { fetchInstance, instance } from '@/instance.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
 | 
			
		||||
const enableEmail = ref<boolean>(false);
 | 
			
		||||
@@ -130,7 +130,7 @@ function save() {
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.emailServer,
 | 
			
		||||
	icon: 'ti ti-mail',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { fetchInstance } from '@/instance.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
 | 
			
		||||
const deeplAuthKey = ref<string>('');
 | 
			
		||||
@@ -88,7 +88,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.externalServices,
 | 
			
		||||
	icon: 'ti ti-link',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,7 @@ import MkPagination from '@/components/MkPagination.vue';
 | 
			
		||||
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
 | 
			
		||||
import FormSplit from '@/components/form/split.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
 | 
			
		||||
const host = ref('');
 | 
			
		||||
const state = ref('federating');
 | 
			
		||||
@@ -112,7 +112,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.federation,
 | 
			
		||||
	icon: 'ti ti-whirl',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { lookupFile } from '@/utility/admin-lookup.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
 | 
			
		||||
const origin = ref('local');
 | 
			
		||||
const type = ref<string | null>(null);
 | 
			
		||||
@@ -85,7 +85,7 @@ const headerActions = computed(() => [{
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.files,
 | 
			
		||||
	icon: 'ti ti-cloud',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -41,8 +41,8 @@ import { lookup } from '@/utility/lookup.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { lookupUser, lookupUserByEmail, lookupFile } from '@/utility/admin-lookup.js';
 | 
			
		||||
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
 | 
			
		||||
import type { PageMetadata } from '@/page.js';
 | 
			
		||||
import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import type { PageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { useRouter } from '@/router/supplier.js';
 | 
			
		||||
 | 
			
		||||
const isEmpty = (x: string | null) => x == null || x === '';
 | 
			
		||||
@@ -318,7 +318,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => INFO.value);
 | 
			
		||||
definePageMetadata(() => INFO.value);
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	header: {
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
import MkPagination from '@/components/MkPagination.vue';
 | 
			
		||||
import type { Paging } from '@/components/MkPagination.vue';
 | 
			
		||||
import MkInviteCode from '@/components/MkInviteCode.vue';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
 | 
			
		||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
 | 
			
		||||
 | 
			
		||||
@@ -114,7 +114,7 @@ function deleted(id: string) {
 | 
			
		||||
const headerActions = computed(() => []);
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.invite,
 | 
			
		||||
	icon: 'ti ti-user-plus',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -137,7 +137,7 @@ import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { fetchInstance } from '@/instance.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import FormLink from '@/components/form/link.vue';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
@@ -259,7 +259,7 @@ function save_mediaSilencedHosts() {
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.moderation,
 | 
			
		||||
	icon: 'ti ti-shield',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ import MkSelect from '@/components/MkSelect.vue';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import MkPagination from '@/components/MkPagination.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 | 
			
		||||
 | 
			
		||||
const logs = shallowRef<InstanceType<typeof MkPagination>>();
 | 
			
		||||
@@ -59,7 +59,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.moderationLogs,
 | 
			
		||||
	icon: 'ti ti-list-search',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -93,7 +93,7 @@ import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { fetchInstance } from '@/instance.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
 | 
			
		||||
const useObjectStorage = ref<boolean>(false);
 | 
			
		||||
@@ -149,7 +149,7 @@ function save() {
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.objectStorage,
 | 
			
		||||
	icon: 'ti ti-cloud',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -82,7 +82,7 @@ import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 | 
			
		||||
import { useStream } from '@/stream.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
 | 
			
		||||
 | 
			
		||||
const rootEl = shallowRef<HTMLElement>();
 | 
			
		||||
@@ -184,7 +184,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.dashboard,
 | 
			
		||||
	icon: 'ti ti-dashboard',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -114,7 +114,7 @@ import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { fetchInstance } from '@/instance.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
@@ -202,7 +202,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.other,
 | 
			
		||||
	icon: 'ti ti-adjustments',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ import XHeader from './_header_.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import * as config from '@@/js/config.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
 | 
			
		||||
export type ApQueueDomain = 'deliver' | 'inbox';
 | 
			
		||||
@@ -71,7 +71,7 @@ const headerTabs = computed(() => [{
 | 
			
		||||
	title: 'Inbox',
 | 
			
		||||
}]);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.jobQueue,
 | 
			
		||||
	icon: 'ti ti-clock-play',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@ import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
 | 
			
		||||
const relays = ref<Misskey.entities.AdminRelaysListResponse>([]);
 | 
			
		||||
 | 
			
		||||
@@ -84,7 +84,7 @@ const headerActions = computed(() => [{
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.relays,
 | 
			
		||||
	icon: 'ti ti-planet',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ import XEditor from './roles.editor.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import { rolesCache } from '@/cache.js';
 | 
			
		||||
import { useRouter } from '@/router/supplier.js';
 | 
			
		||||
@@ -87,7 +87,7 @@ async function save() {
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: role.value ? `${i18n.ts._role.edit}: ${role.value.name}` : i18n.ts._role.new,
 | 
			
		||||
	icon: 'ti ti-badge',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -69,7 +69,7 @@ import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
 | 
			
		||||
import MkInfo from '@/components/MkInfo.vue';
 | 
			
		||||
@@ -170,7 +170,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: `${i18n.ts.role}: ${role.name}`,
 | 
			
		||||
	icon: 'ti ti-badge',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -292,7 +292,7 @@ import MkRolePreview from '@/components/MkRolePreview.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { instance, fetchInstance } from '@/instance.js';
 | 
			
		||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
 | 
			
		||||
import { useRouter } from '@/router/supplier.js';
 | 
			
		||||
@@ -338,7 +338,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.roles,
 | 
			
		||||
	icon: 'ti ti-badges',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -134,7 +134,7 @@ import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { fetchInstance } from '@/instance.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { useForm } from '@/utility/use-form.js';
 | 
			
		||||
import MkFormFooter from '@/components/MkFormFooter.vue';
 | 
			
		||||
 | 
			
		||||
@@ -206,7 +206,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.security,
 | 
			
		||||
	icon: 'ti ti-lock',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ import XHeader from './_header_.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { fetchInstance, instance } from '@/instance.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +67,7 @@ const remove = (index: number): void => {
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.serverRules,
 | 
			
		||||
	icon: 'ti ti-checkbox',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -269,7 +269,7 @@ import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { fetchInstance, instance } from '@/instance.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import MkKeyValue from '@/components/MkKeyValue.vue';
 | 
			
		||||
@@ -391,7 +391,7 @@ const proxyAccountForm = useForm({
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.general,
 | 
			
		||||
	icon: 'ti ti-settings',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ import { computed, onMounted, ref } from 'vue';
 | 
			
		||||
import { entities } from 'misskey-js';
 | 
			
		||||
import XItem from './system-webhook.item.vue';
 | 
			
		||||
import FormSection from '@/components/form/section.vue';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import XHeader from '@/pages/admin/_header_.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
@@ -82,7 +82,7 @@ onMounted(async () => {
 | 
			
		||||
	await fetchWebhooks();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: 'SystemWebhook',
 | 
			
		||||
	icon: 'ti ti-webhook',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,7 @@ import MkPagination from '@/components/MkPagination.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { lookupUser } from '@/utility/admin-lookup.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
 | 
			
		||||
import { dateString } from '@/filters/date.js';
 | 
			
		||||
 | 
			
		||||
@@ -169,7 +169,7 @@ watchEffect(() => {
 | 
			
		||||
	}));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.users,
 | 
			
		||||
	icon: 'ti ti-users',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -16,11 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { instance } from '@/instance.js';
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.ads,
 | 
			
		||||
	icon: 'ti ti-ad',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { $i, updateAccountPartial } from '@/account.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
 | 
			
		||||
@@ -102,7 +102,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: announcement.value ? announcement.value.title : i18n.ts.announcements,
 | 
			
		||||
	icon: 'ti ti-speakerphone',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { $i, updateAccountPartial } from '@/account.js';
 | 
			
		||||
 | 
			
		||||
const paginationCurrent = {
 | 
			
		||||
@@ -111,7 +111,7 @@ const headerTabs = computed(() => [{
 | 
			
		||||
	icon: 'ti ti-point',
 | 
			
		||||
}]);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.announcements,
 | 
			
		||||
	icon: 'ti ti-speakerphone',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
 | 
			
		||||
import { scroll } from '@@/js/scroll.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { useRouter } from '@/router/supplier.js';
 | 
			
		||||
 | 
			
		||||
@@ -88,7 +88,7 @@ const headerActions = computed(() => antenna.value ? [{
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: antenna.value ? antenna.value.name : i18n.ts.antennas,
 | 
			
		||||
	icon: 'ti ti-antenna',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import MkTextarea from '@/components/MkTextarea.vue';
 | 
			
		||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
 | 
			
		||||
const body = ref('{}');
 | 
			
		||||
const endpoint = ref('');
 | 
			
		||||
@@ -87,7 +87,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: 'API console',
 | 
			
		||||
	icon: 'ti ti-terminal-2',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ import XForm from './auth.form.vue';
 | 
			
		||||
import MkSignin from '@/components/MkSignin.vue';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { $i, login } from '@/account.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
@@ -97,7 +97,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts._auth.shareAccessTitle,
 | 
			
		||||
	icon: 'ti ti-apps',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ import { signinRequired } from '@/account.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
 | 
			
		||||
const $i = signinRequired();
 | 
			
		||||
 | 
			
		||||
@@ -86,7 +86,7 @@ const headerActions = computed(() => [{
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.avatarDecorations,
 | 
			
		||||
	icon: 'ti ti-sparkles',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,7 @@ import MkColorInput from '@/components/MkColorInput.vue';
 | 
			
		||||
import { selectFile } from '@/utility/select-file.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
@@ -202,7 +202,7 @@ const headerActions = computed(() => []);
 | 
			
		||||
 | 
			
		||||
const headerTabs = computed(() => []);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: props.channelId ? i18n.ts._channel.edit : i18n.ts._channel.create,
 | 
			
		||||
	icon: 'ti ti-device-tv',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -84,7 +84,7 @@ import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { $i, iAmModerator } from '@/account.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { deviceKind } from '@/utility/device-kind.js';
 | 
			
		||||
import MkNotes from '@/components/MkNotes.vue';
 | 
			
		||||
import { favoritedChannelsCache } from '@/cache.js';
 | 
			
		||||
@@ -265,7 +265,7 @@ const headerTabs = computed(() => [{
 | 
			
		||||
	icon: 'ti ti-search',
 | 
			
		||||
}]);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: channel.value ? channel.value.name : i18n.ts.channel,
 | 
			
		||||
	icon: 'ti ti-device-tv',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -69,7 +69,7 @@ import MkRadios from '@/components/MkRadios.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
 | 
			
		||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { useRouter } from '@/router/supplier.js';
 | 
			
		||||
 | 
			
		||||
@@ -161,7 +161,7 @@ const headerTabs = computed(() => [{
 | 
			
		||||
	icon: 'ti ti-edit',
 | 
			
		||||
}]);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.channel,
 | 
			
		||||
	icon: 'ti ti-device-tv',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import MkClickerGame from '@/components/MkClickerGame.vue';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: '🍪👈',
 | 
			
		||||
	icon: 'ti ti-cookie',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,7 @@ import { $i } from '@/account.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import { clipsCache } from '@/cache.js';
 | 
			
		||||
import { isSupportShare } from '@/utility/navigator.js';
 | 
			
		||||
@@ -193,7 +193,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
 | 
			
		||||
	},
 | 
			
		||||
}] : null);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: clip.value ? clip.value.name : i18n.ts.clip,
 | 
			
		||||
	icon: 'ti ti-paperclip',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -37,11 +37,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { instance } from '@/instance.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkKeyValue from '@/components/MkKeyValue.vue';
 | 
			
		||||
import MkLink from '@/components/MkLink.vue';
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.inquiry,
 | 
			
		||||
	icon: 'ti ti-help-circle',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -86,7 +86,7 @@ import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
 | 
			
		||||
const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
 | 
			
		||||
 | 
			
		||||
@@ -326,7 +326,7 @@ const headerTabs = computed(() => [{
 | 
			
		||||
	title: i18n.ts.remote,
 | 
			
		||||
}]);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts.customEmojis,
 | 
			
		||||
	icon: 'ti ti-icons',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, ref, defineAsyncComponent } from 'vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { definePage } from '@/page.js';
 | 
			
		||||
import { definePageMetadata } from '@/utility/page-metadata.js';
 | 
			
		||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
@@ -48,7 +48,7 @@ const headerTabs = computed(() => [{
 | 
			
		||||
	icon: 'ti ti-pencil',
 | 
			
		||||
}]);
 | 
			
		||||
 | 
			
		||||
definePage(() => ({
 | 
			
		||||
definePageMetadata(() => ({
 | 
			
		||||
	title: i18n.ts._fileViewer.title,
 | 
			
		||||
	icon: 'ti ti-file',
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||