Compare commits
	
		
			62 Commits
		
	
	
		
			2025.3.1-b
			...
			2025.3.2-a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ![github-actions[bot]](/assets/img/avatar_default.png)  | c9fa95429a | ||
|   | e5d117dc98 | ||
|   | 4a73feb041 | ||
|   | a06b9eefaa | ||
|   | 3129fcf164 | ||
|   | 35a4544477 | ||
|   | aa1cc2f817 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 15685be4cc | ||
|   | 8508c4dadc | ||
|   | e594fb0037 | ||
|   | a369721791 | ||
|   | f8e244f48d | ||
|   | 8410611512 | ||
|   | caab1ec7c3 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | ffade9740e | ||
|   | b03bcf26cd | ||
|   | ddbc83b2e4 | ||
|   | d185785f20 | ||
|   | 02d7fbefc4 | ||
|   | f7ea92c68c | ||
|   | e891d5c5d3 | ||
|   | 57a6b630b7 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | eda768a08c | ||
|   | 1f345eb839 | ||
|   | 1f2801af02 | ||
|   | a4ba096e2a | ||
|   | 6841cdfa76 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 794f360bc2 | ||
|   | f797765b1d | ||
|   | 9dce512fbb | ||
|   | 9e91f85370 | ||
|   | 9998cb84e8 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 5ed1101bbd | ||
|   | 6c9153300d | ||
|   | 7957ee5191 | ||
|   | b200743845 | ||
|   | 08f7e7d9b3 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 16ad6b3f6c | ||
|   | 4df9083bf0 | ||
|   | 6419af2179 | ||
|   | d9858b03c9 | ||
|   | 88efc0a3be | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | ac21fa7194 | ||
|   | c76afce9a7 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 8e3304344f | ||
|   | db5c127cdd | ||
|   | 0402866b43 | ||
|   | 6cefabc6b6 | ||
|   | c9c04d8391 | ||
|   | 27e8805dcb | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 933abedc90 | ||
|   | 69eee9f050 | ||
|   | 2918fb2609 | ||
|   | fcd7fa62ba | ||
|   | be7e3b9a0c | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 06e7272ca1 | ||
|   | f35eb0f6d9 | ||
|   | bdb74539d4 | ||
|   | abc1e9168d | ||
|   | d30ddd4c2e | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 05cdc095c0 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 7c1dc3d632 | 
							
								
								
									
										19
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -1,3 +1,22 @@ | |||||||
|  | ## 2025.3.2 | ||||||
|  |  | ||||||
|  | ### General | ||||||
|  | - | ||||||
|  |  | ||||||
|  | ### Client | ||||||
|  | - Feat: 設定の管理が強化されました | ||||||
|  |   - 自動でバックアップされるように | ||||||
|  | 	- 任意の設定項目をデバイス間で同期できるように(実験的) | ||||||
|  | - Enhance: プラグインの管理が強化されました | ||||||
|  | - Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに | ||||||
|  | - Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように | ||||||
|  | - Enhance: テーマ設定画面のデザインを改善 | ||||||
|  | - Fix: テーマ切り替え時に一部の色が変わらない問題を修正 | ||||||
|  |  | ||||||
|  | ### Server | ||||||
|  | - Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正 | ||||||
|  | - Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正 | ||||||
|  |  | ||||||
| ## 2025.3.1 | ## 2025.3.1 | ||||||
|  |  | ||||||
| ### General | ### General | ||||||
|   | |||||||
| Before Width: | Height: | Size: 94 KiB | 
| Before Width: | Height: | Size: 317 KiB | 
| Before Width: | Height: | Size: 24 KiB | 
| Before Width: | Height: | Size: 95 KiB | 
| Before Width: | Height: | Size: 238 KiB | 
| Before Width: | Height: | Size: 148 KiB | 
							
								
								
									
										182
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -5278,6 +5278,176 @@ export interface Locale extends ILocale { | |||||||
|      * アクセシビリティ |      * アクセシビリティ | ||||||
|      */ |      */ | ||||||
|     "accessibility": string; |     "accessibility": string; | ||||||
|  |     /** | ||||||
|  |      * 設定のプロファイル | ||||||
|  |      */ | ||||||
|  |     "preferencesProfile": string; | ||||||
|  |     /** | ||||||
|  |      * 設定IDをコピー | ||||||
|  |      */ | ||||||
|  |     "copyPreferenceId": string; | ||||||
|  |     /** | ||||||
|  |      * 初期値に戻す | ||||||
|  |      */ | ||||||
|  |     "resetToDefaultValue": string; | ||||||
|  |     /** | ||||||
|  |      * アカウントで上書き | ||||||
|  |      */ | ||||||
|  |     "overrideByAccount": string; | ||||||
|  |     /** | ||||||
|  |      * 無題 | ||||||
|  |      */ | ||||||
|  |     "untitled": string; | ||||||
|  |     /** | ||||||
|  |      * 名前はありません | ||||||
|  |      */ | ||||||
|  |     "noName": string; | ||||||
|  |     /** | ||||||
|  |      * スキップ | ||||||
|  |      */ | ||||||
|  |     "skip": string; | ||||||
|  |     /** | ||||||
|  |      * 復元 | ||||||
|  |      */ | ||||||
|  |     "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": { | ||||||
|  |         /** | ||||||
|  |          * プロファイル名 | ||||||
|  |          */ | ||||||
|  |         "profileName": string; | ||||||
|  |         /** | ||||||
|  |          * このデバイスを識別する名前を設定してください。 | ||||||
|  |          */ | ||||||
|  |         "profileNameDescription": string; | ||||||
|  |         /** | ||||||
|  |          * 例: 「メインPC」、「スマホ」など | ||||||
|  |          */ | ||||||
|  |         "profileNameDescription2": string; | ||||||
|  |     }; | ||||||
|  |     "_preferencesBackup": { | ||||||
|  |         /** | ||||||
|  |          * 自動バックアップ | ||||||
|  |          */ | ||||||
|  |         "autoBackup": string; | ||||||
|  |         /** | ||||||
|  |          * バックアップから復元 | ||||||
|  |          */ | ||||||
|  |         "restoreFromBackup": string; | ||||||
|  |         /** | ||||||
|  |          * バックアップが見つかりませんでした | ||||||
|  |          */ | ||||||
|  |         "noBackupsFoundTitle": string; | ||||||
|  |         /** | ||||||
|  |          * 自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。 | ||||||
|  |          */ | ||||||
|  |         "noBackupsFoundDescription": string; | ||||||
|  |         /** | ||||||
|  |          * 復元するバックアップを選択してください | ||||||
|  |          */ | ||||||
|  |         "selectBackupToRestore": string; | ||||||
|  |         /** | ||||||
|  |          * 自動バックアップを有効にするにはプロファイル名の設定が必要です。 | ||||||
|  |          */ | ||||||
|  |         "youNeedToNameYourProfileToEnableAutoBackup": string; | ||||||
|  |         /** | ||||||
|  |          * このデバイスで設定の自動バックアップは有効になっていません。 | ||||||
|  |          */ | ||||||
|  |         "autoPreferencesBackupIsNotEnabledForThisDevice": string; | ||||||
|  |         /** | ||||||
|  |          * 設定のバックアップが見つかりました | ||||||
|  |          */ | ||||||
|  |         "backupFound": string; | ||||||
|  |     }; | ||||||
|     "_accountSettings": { |     "_accountSettings": { | ||||||
|         /** |         /** | ||||||
|          * コンテンツの表示にログインを必須にする |          * コンテンツの表示にログインを必須にする | ||||||
| @@ -5315,6 +5485,10 @@ export interface Locale extends ILocale { | |||||||
|          * リモートサーバーに連合されたノートには効果が及ばない場合があります。 |          * リモートサーバーに連合されたノートには効果が及ばない場合があります。 | ||||||
|          */ |          */ | ||||||
|         "mayNotEffectForFederatedNotes": string; |         "mayNotEffectForFederatedNotes": string; | ||||||
|  |         /** | ||||||
|  |          * これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。 | ||||||
|  |          */ | ||||||
|  |         "mayNotEffectSomeSituations": string; | ||||||
|         /** |         /** | ||||||
|          * 指定した時間を経過しているノート |          * 指定した時間を経過しているノート | ||||||
|          */ |          */ | ||||||
| @@ -7662,6 +7836,10 @@ export interface Locale extends ILocale { | |||||||
|          * 標準のテーマ |          * 標準のテーマ | ||||||
|          */ |          */ | ||||||
|         "builtinThemes": string; |         "builtinThemes": string; | ||||||
|  |         /** | ||||||
|  |          * サーバーのテーマ | ||||||
|  |          */ | ||||||
|  |         "instanceTheme": string; | ||||||
|         /** |         /** | ||||||
|          * そのテーマは既にインストールされています |          * そのテーマは既にインストールされています | ||||||
|          */ |          */ | ||||||
| @@ -9670,6 +9848,10 @@ export interface Locale extends ILocale { | |||||||
|          * 幅を自動調整 |          * 幅を自動調整 | ||||||
|          */ |          */ | ||||||
|         "flexible": string; |         "flexible": string; | ||||||
|  |         /** | ||||||
|  |          * プロファイル情報のデバイス間同期を有効にする | ||||||
|  |          */ | ||||||
|  |         "enableSyncBetweenDevicesForProfiles": string; | ||||||
|         "_columns": { |         "_columns": { | ||||||
|             /** |             /** | ||||||
|              * メイン |              * メイン | ||||||
|   | |||||||
| @@ -1315,6 +1315,53 @@ markAsSensitiveConfirm: "このメディアをセンシティブとして設定 | |||||||
| unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか?" | unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか?" | ||||||
| preferences: "環境設定" | preferences: "環境設定" | ||||||
| accessibility: "アクセシビリティ" | accessibility: "アクセシビリティ" | ||||||
|  | preferencesProfile: "設定のプロファイル" | ||||||
|  | copyPreferenceId: "設定IDをコピー" | ||||||
|  | resetToDefaultValue: "初期値に戻す" | ||||||
|  | overrideByAccount: "アカウントで上書き" | ||||||
|  | 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: "プロファイル名" | ||||||
|  |   profileNameDescription: "このデバイスを識別する名前を設定してください。" | ||||||
|  |   profileNameDescription2: "例: 「メインPC」、「スマホ」など" | ||||||
|  |  | ||||||
|  | _preferencesBackup: | ||||||
|  |   autoBackup: "自動バックアップ" | ||||||
|  |   restoreFromBackup: "バックアップから復元" | ||||||
|  |   noBackupsFoundTitle: "バックアップが見つかりませんでした" | ||||||
|  |   noBackupsFoundDescription: "自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。" | ||||||
|  |   selectBackupToRestore: "復元するバックアップを選択してください" | ||||||
|  |   youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効にするにはプロファイル名の設定が必要です。" | ||||||
|  |   autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になっていません。" | ||||||
|  |   backupFound: "設定のバックアップが見つかりました" | ||||||
|  |  | ||||||
| _accountSettings: | _accountSettings: | ||||||
|   requireSigninToViewContents: "コンテンツの表示にログインを必須にする" |   requireSigninToViewContents: "コンテンツの表示にログインを必須にする" | ||||||
| @@ -1326,6 +1373,7 @@ _accountSettings: | |||||||
|   makeNotesHiddenBefore: "過去のノートを非公開化する" |   makeNotesHiddenBefore: "過去のノートを非公開化する" | ||||||
|   makeNotesHiddenBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。" |   makeNotesHiddenBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。" | ||||||
|   mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばない場合があります。" |   mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばない場合があります。" | ||||||
|  |   mayNotEffectSomeSituations: "これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。" | ||||||
|   notesHavePassedSpecifiedPeriod: "指定した時間を経過しているノート" |   notesHavePassedSpecifiedPeriod: "指定した時間を経過しているノート" | ||||||
|   notesOlderThanSpecifiedDateAndTime: "指定した日時より前のノート" |   notesOlderThanSpecifiedDateAndTime: "指定した日時より前のノート" | ||||||
|  |  | ||||||
| @@ -2007,6 +2055,7 @@ _theme: | |||||||
|   installed: "{name}をインストールしました" |   installed: "{name}をインストールしました" | ||||||
|   installedThemes: "インストールされたテーマ" |   installedThemes: "インストールされたテーマ" | ||||||
|   builtinThemes: "標準のテーマ" |   builtinThemes: "標準のテーマ" | ||||||
|  |   instanceTheme: "サーバーのテーマ" | ||||||
|   alreadyInstalled: "そのテーマは既にインストールされています" |   alreadyInstalled: "そのテーマは既にインストールされています" | ||||||
|   invalid: "テーマの形式が間違っています" |   invalid: "テーマの形式が間違っています" | ||||||
|   make: "テーマを作る" |   make: "テーマを作る" | ||||||
| @@ -2554,6 +2603,7 @@ _deck: | |||||||
|   useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示" |   useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示" | ||||||
|   usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります" |   usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります" | ||||||
|   flexible: "幅を自動調整" |   flexible: "幅を自動調整" | ||||||
|  |   enableSyncBetweenDevicesForProfiles: "プロファイル情報のデバイス間同期を有効にする" | ||||||
|  |  | ||||||
|   _columns: |   _columns: | ||||||
|     main: "メイン" |     main: "メイン" | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
| 	"name": "misskey", | 	"name": "misskey", | ||||||
| 	"version": "2025.3.1-beta.3", | 	"version": "2025.3.2-alpha.9", | ||||||
| 	"codename": "nasubi", | 	"codename": "nasubi", | ||||||
| 	"repository": { | 	"repository": { | ||||||
| 		"type": "git", | 		"type": "git", | ||||||
| @@ -24,6 +24,7 @@ | |||||||
| 		"build": "pnpm build-pre && pnpm -r build && pnpm build-assets", | 		"build": "pnpm build-pre && pnpm -r build && pnpm build-assets", | ||||||
| 		"build-storybook": "pnpm --filter frontend build-storybook", | 		"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-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": "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", | 		"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", | 		"init": "pnpm migrate", | ||||||
| @@ -65,12 +66,12 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"devDependencies": { | 	"devDependencies": { | ||||||
| 		"@misskey-dev/eslint-plugin": "2.1.0", | 		"@misskey-dev/eslint-plugin": "2.1.0", | ||||||
| 		"@types/node": "22.13.9", | 		"@types/node": "22.13.10", | ||||||
| 		"@typescript-eslint/eslint-plugin": "8.26.0", | 		"@typescript-eslint/eslint-plugin": "8.26.0", | ||||||
| 		"@typescript-eslint/parser": "8.26.0", | 		"@typescript-eslint/parser": "8.26.0", | ||||||
| 		"cross-env": "7.0.3", | 		"cross-env": "7.0.3", | ||||||
| 		"cypress": "14.1.0", | 		"cypress": "14.1.0", | ||||||
| 		"eslint": "9.21.0", | 		"eslint": "9.22.0", | ||||||
| 		"globals": "16.0.0", | 		"globals": "16.0.0", | ||||||
| 		"ncp": "2.0.0", | 		"ncp": "2.0.0", | ||||||
| 		"pnpm": "10.6.1", | 		"pnpm": "10.6.1", | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ import type { Config } from '@/config.js'; | |||||||
| import { StatusError } from '@/misc/status-error.js'; | import { StatusError } from '@/misc/status-error.js'; | ||||||
| import { bindThis } from '@/decorators.js'; | import { bindThis } from '@/decorators.js'; | ||||||
| import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; | import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; | ||||||
| import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; | import { assertActivityMatchesUrl, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; | ||||||
| import type { IObject } from '@/core/activitypub/type.js'; | import type { IObject } from '@/core/activitypub/type.js'; | ||||||
| import type { Response } from 'node-fetch'; | import type { Response } from 'node-fetch'; | ||||||
| import type { URL } from 'node:url'; | import type { URL } from 'node:url'; | ||||||
| @@ -265,7 +265,7 @@ export class HttpRequestService { | |||||||
| 		const finalUrl = res.url; // redirects may have been involved | 		const finalUrl = res.url; // redirects may have been involved | ||||||
| 		const activity = await res.json() as IObject; | 		const activity = await res.json() as IObject; | ||||||
|  |  | ||||||
| 		assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail); | 		assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail); | ||||||
|  |  | ||||||
| 		return activity; | 		return activity; | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -499,11 +499,28 @@ export class ApRendererService { | |||||||
| 			this.userProfilesRepository.findOneByOrFail({ userId: user.id }), | 			this.userProfilesRepository.findOneByOrFail({ userId: user.id }), | ||||||
| 		]); | 		]); | ||||||
|  |  | ||||||
|  | 		const tryRewriteUrl = (maybeUrl: string) => { | ||||||
|  | 			const urlSafeRegex = /^(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/; | ||||||
|  | 			try { | ||||||
|  | 				const match = maybeUrl.match(urlSafeRegex); | ||||||
|  | 				if (!match) { | ||||||
|  | 					return maybeUrl; | ||||||
|  | 				} | ||||||
|  | 				const urlPart = match[0]; | ||||||
|  | 				const urlPartParsed = new URL(urlPart); | ||||||
|  | 				const restPart = maybeUrl.slice(match[0].length); | ||||||
|  | 				 | ||||||
|  | 				return `<a href="${urlPartParsed.href}" rel="me nofollow noopener" target="_blank">${urlPart}</a>${restPart}`; | ||||||
|  | 			} catch (e) { | ||||||
|  | 				return maybeUrl; | ||||||
|  | 			} | ||||||
|  | 		}; | ||||||
|  |  | ||||||
| 		const attachment = profile.fields.map(field => ({ | 		const attachment = profile.fields.map(field => ({ | ||||||
| 			type: 'PropertyValue', | 			type: 'PropertyValue', | ||||||
| 			name: field.name, | 			name: field.name, | ||||||
| 			value: (field.value.startsWith('http://') || field.value.startsWith('https://')) | 			value: (field.value.startsWith('http://') || field.value.startsWith('https://')) | ||||||
| 				? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>` | 				? tryRewriteUrl(field.value) | ||||||
| 				: field.value, | 				: field.value, | ||||||
| 		})); | 		})); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ import { LoggerService } from '@/core/LoggerService.js'; | |||||||
| import { bindThis } from '@/decorators.js'; | import { bindThis } from '@/decorators.js'; | ||||||
| import type Logger from '@/logger.js'; | import type Logger from '@/logger.js'; | ||||||
| import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; | import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; | ||||||
| import { assertActivityMatchesUrls, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; | import { assertActivityMatchesUrl, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; | ||||||
| import type { IObject } from './type.js'; | import type { IObject } from './type.js'; | ||||||
|  |  | ||||||
| type Request = { | type Request = { | ||||||
| @@ -258,7 +258,7 @@ export class ApRequestService { | |||||||
| 		const finalUrl = res.url; // redirects may have been involved | 		const finalUrl = res.url; // redirects may have been involved | ||||||
| 		const activity = await res.json() as IObject; | 		const activity = await res.json() as IObject; | ||||||
|  |  | ||||||
| 		assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail); | 		assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail); | ||||||
|  |  | ||||||
| 		return activity; | 		return activity; | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -75,7 +75,7 @@ function normalizeSynonymousSubdomain(url: URL | string): URL { | |||||||
| 	return new URL(urlParsed.toString().replace(host, normalizedHost)); | 	return new URL(urlParsed.toString().replace(host, normalizedHost)); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function assertActivityMatchesUrls(requestUrl: string | URL, activity: IObject, candidateUrls: (string | URL)[], allowSoftfail: FetchAllowSoftFailMask): FetchAllowSoftFailMask { | export function assertActivityMatchesUrl(requestUrl: string | URL, activity: IObject, finalUrl: string | URL, allowSoftfail: FetchAllowSoftFailMask): FetchAllowSoftFailMask { | ||||||
| 	// must have a unique identifier to verify authority | 	// must have a unique identifier to verify authority | ||||||
| 	if (!activity.id) { | 	if (!activity.id) { | ||||||
| 		throw new Error('bad Activity: missing id field'); | 		throw new Error('bad Activity: missing id field'); | ||||||
| @@ -95,26 +95,32 @@ export function assertActivityMatchesUrls(requestUrl: string | URL, activity: IO | |||||||
| 	const requestUrlParsed = normalizeSynonymousSubdomain(requestUrl); | 	const requestUrlParsed = normalizeSynonymousSubdomain(requestUrl); | ||||||
| 	const idParsed = normalizeSynonymousSubdomain(activity.id); | 	const idParsed = normalizeSynonymousSubdomain(activity.id); | ||||||
|  |  | ||||||
| 	const candidateUrlsParsed = candidateUrls.map(it => normalizeSynonymousSubdomain(it)); | 	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 requestUrlSecure = requestUrlParsed.protocol === 'https:'; | 	const requestUrlSecure = requestUrlParsed.protocol === 'https:'; | ||||||
| 	const finalUrlSecure = candidateUrlsParsed.every(it => it.protocol === 'https:'); | 	const finalUrlSecure = finalUrlParsed.protocol === 'https:'; | ||||||
| 	if (requestUrlSecure && !finalUrlSecure) { | 	if (requestUrlSecure && !finalUrlSecure) { | ||||||
| 		throw new Error(`bad Activity: id(${activity.id}) is not allowed to have http:// in the url`); | 		throw new Error(`bad Activity: id(${activity.id}) is not allowed to have http:// in the url`); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Compare final URL to the ID | 	// Compare final URL to the ID | ||||||
| 	if (!candidateUrlsParsed.some(it => it.href === idParsed.href)) { | 	if (finalUrlParsed.href !== idParsed.href) { | ||||||
| 		requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${candidateUrlsParsed.map(it => it.toString())})`); | 		requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${finalUrlParsed.toString()})`); | ||||||
|  |  | ||||||
| 		// at lease host need to match exactly (ActivityPub requirement) | 		// at lease host need to match exactly (ActivityPub requirement) | ||||||
| 		if (!candidateUrlsParsed.some(it => idParsed.host === it.host)) { | 		if (idParsed.host !== finalUrlParsed.host) { | ||||||
| 			throw new Error(`bad Activity: id(${activity.id}) does not match response host(${candidateUrlsParsed.map(it => it.host)})`); | 			throw new Error(`bad Activity: id(${activity.id}) does not match response host(${finalUrlParsed.host})`); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Compare request URL to the ID | 	// Compare request URL to the ID | ||||||
| 	if (!requestUrlParsed.href.includes(idParsed.href)) { | 	if (requestUrlParsed.href !== idParsed.href) { | ||||||
| 		requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match request url(${requestUrlParsed.toString()})`); | 		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) | 		// 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 { genRsaKeyPair } from '@/misc/gen-key-pair.js'; | ||||||
| import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; | import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; | ||||||
| import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; | import { assertActivityMatchesUrl, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; | ||||||
| import { IObject } from '@/core/activitypub/type.js'; | import { IObject } from '@/core/activitypub/type.js'; | ||||||
|  |  | ||||||
| export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { | export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { | ||||||
| @@ -66,23 +66,26 @@ describe('ap-request', () => { | |||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	test('rejects non matching domain', () => { | 	test('rejects non matching domain', () => { | ||||||
| 		assert.doesNotThrow(() => assertActivityMatchesUrls( | 		assert.doesNotThrow(() => assertActivityMatchesUrl( | ||||||
| 			'https://alice.example.com/abc', | 			'https://alice.example.com/abc', | ||||||
| 			{ id: 'https://alice.example.com/abc' } as IObject, | 			{ id: 'https://alice.example.com/abc' } as IObject, | ||||||
| 			[ |  | ||||||
| 			'https://alice.example.com/abc', | 			'https://alice.example.com/abc', | ||||||
| 			], |  | ||||||
| 			FetchAllowSoftFailMask.Strict, | 			FetchAllowSoftFailMask.Strict, | ||||||
| 		), 'validation should pass base case'); | 		), 'validation should pass base case'); | ||||||
| 		assert.throws(() => assertActivityMatchesUrls( | 		assert.throws(() => assertActivityMatchesUrl( | ||||||
| 			'https://alice.example.com/abc', | 			'https://alice.example.com/abc', | ||||||
| 			{ id: 'https://bob.example.com/abc' } as IObject, | 			{ id: 'https://bob.example.com/abc' } as IObject, | ||||||
| 			[ |  | ||||||
| 			'https://alice.example.com/abc', | 			'https://alice.example.com/abc', | ||||||
| 			], |  | ||||||
| 			FetchAllowSoftFailMask.Any, | 			FetchAllowSoftFailMask.Any, | ||||||
| 		), 'validation should fail no matter what if the response URL is inconsistent with the object ID'); | 		), '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 | 		// fix issues like threads | ||||||
| 		// https://github.com/misskey-dev/misskey/issues/15039 | 		// https://github.com/misskey-dev/misskey/issues/15039 | ||||||
| 		const withOrWithoutWWW = [ | 		const withOrWithoutWWW = [ | ||||||
| @@ -97,89 +100,71 @@ describe('ap-request', () => { | |||||||
| 			), | 			), | ||||||
| 			withOrWithoutWWW, | 			withOrWithoutWWW, | ||||||
| 		).forEach(([[a, b], c]) => { | 		).forEach(([[a, b], c]) => { | ||||||
| 			assert.doesNotThrow(() => assertActivityMatchesUrls( | 			assert.doesNotThrow(() => assertActivityMatchesUrl( | ||||||
| 				a, | 				a, | ||||||
| 				{ id: b } as IObject, | 				{ id: b } as IObject, | ||||||
| 				[ |  | ||||||
| 				c, | 				c, | ||||||
| 				], |  | ||||||
| 				FetchAllowSoftFailMask.Strict, | 				FetchAllowSoftFailMask.Strict, | ||||||
| 			), 'validation should pass with or without www. subdomain'); | 			), 'validation should pass with or without www. subdomain'); | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	test('cross origin lookup', () => { | 	test('cross origin lookup', () => { | ||||||
| 		assert.doesNotThrow(() => assertActivityMatchesUrls( | 		assert.doesNotThrow(() => assertActivityMatchesUrl( | ||||||
| 			'https://alice.example.com/abc', | 			'https://alice.example.com/abc', | ||||||
| 			{ id: 'https://bob.example.com/abc' } as IObject, | 			{ id: 'https://bob.example.com/abc' } as IObject, | ||||||
| 			[ |  | ||||||
| 			'https://bob.example.com/abc', | 			'https://bob.example.com/abc', | ||||||
| 			], |  | ||||||
| 			FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId, | 			FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId, | ||||||
| 		), 'validation should pass if the response is otherwise consistent and cross-origin is allowed'); | 		), 'validation should pass if the response is otherwise consistent and cross-origin is allowed'); | ||||||
| 		assert.throws(() => assertActivityMatchesUrls( | 		assert.throws(() => assertActivityMatchesUrl( | ||||||
| 			'https://alice.example.com/abc', | 			'https://alice.example.com/abc', | ||||||
| 			{ id: 'https://bob.example.com/abc' } as IObject, | 			{ id: 'https://bob.example.com/abc' } as IObject, | ||||||
| 			[ |  | ||||||
| 			'https://bob.example.com/abc', | 			'https://bob.example.com/abc', | ||||||
| 			], |  | ||||||
| 			FetchAllowSoftFailMask.Strict, | 			FetchAllowSoftFailMask.Strict, | ||||||
| 		), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed'); | 		), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed'); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	test('rejects non-canonical ID', () => { | 	test('rejects non-canonical ID', () => { | ||||||
| 		assert.throws(() => assertActivityMatchesUrls( | 		assert.throws(() => assertActivityMatchesUrl( | ||||||
| 			'https://alice.example.com/@alice', | 			'https://alice.example.com/@alice', | ||||||
| 			{ id: 'https://alice.example.com/users/alice' } as IObject, | 			{ id: 'https://alice.example.com/users/alice' } as IObject, | ||||||
| 			[ | 			'https://alice.example.com/users/alice', | ||||||
| 				'https://alice.example.com/users/alice' |  | ||||||
| 			], |  | ||||||
| 			FetchAllowSoftFailMask.Strict, | 			FetchAllowSoftFailMask.Strict, | ||||||
| 		), 'throws if the response ID did not exactly match the expected ID'); | 		), 'throws if the response ID did not exactly match the expected ID'); | ||||||
| 		assert.doesNotThrow(() => assertActivityMatchesUrls( | 		assert.doesNotThrow(() => assertActivityMatchesUrl( | ||||||
| 			'https://alice.example.com/@alice', | 			'https://alice.example.com/@alice', | ||||||
| 			{ id: 'https://alice.example.com/users/alice' } as IObject, | 			{ id: 'https://alice.example.com/users/alice' } as IObject, | ||||||
| 			[ |  | ||||||
| 			'https://alice.example.com/users/alice', | 			'https://alice.example.com/users/alice', | ||||||
| 			], |  | ||||||
| 			FetchAllowSoftFailMask.NonCanonicalId, | 			FetchAllowSoftFailMask.NonCanonicalId, | ||||||
| 		), 'does not throw if non-canonical ID is allowed'); | 		), 'does not throw if non-canonical ID is allowed'); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	test('origin relaxed alignment', () => { | 	test('origin relaxed alignment', () => { | ||||||
| 		assert.doesNotThrow(() => assertActivityMatchesUrls( | 		assert.doesNotThrow(() => assertActivityMatchesUrl( | ||||||
| 			'https://alice.example.com/abc', | 			'https://alice.example.com/abc', | ||||||
| 			{ id: 'https://ap.alice.example.com/abc' } as IObject, | 			{ 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, | 			FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId, | ||||||
| 		), 'validation should pass if response is a subdomain of the expected origin'); | 		), 'validation should pass if response is a subdomain of the expected origin'); | ||||||
| 		assert.throws(() => assertActivityMatchesUrls( | 		assert.throws(() => assertActivityMatchesUrl( | ||||||
| 			'https://alice.multi-tenant.example.com/abc', | 			'https://alice.multi-tenant.example.com/abc', | ||||||
| 			{ id: 'https://alice.multi-tenant.example.com/abc' } as IObject, | 			{ 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, | 			FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId, | ||||||
| 		), 'validation should fail if response is a disjoint domain of the expected origin'); | 		), 'validation should fail if response is a disjoint domain of the expected origin'); | ||||||
| 		assert.throws(() => assertActivityMatchesUrls( | 		assert.throws(() => assertActivityMatchesUrl( | ||||||
| 			'https://alice.example.com/abc', | 			'https://alice.example.com/abc', | ||||||
| 			{ id: 'https://ap.alice.example.com/abc' } as IObject, | 			{ id: 'https://ap.alice.example.com/abc' } as IObject, | ||||||
| 			[ |  | ||||||
| 			'https://ap.alice.example.com/abc', | 			'https://ap.alice.example.com/abc', | ||||||
| 			], |  | ||||||
| 			FetchAllowSoftFailMask.Strict, | 			FetchAllowSoftFailMask.Strict, | ||||||
| 		), 'throws if relaxed origin is forbidden'); | 		), 'throws if relaxed origin is forbidden'); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	test('resist HTTP downgrade', () => { | 	test('resist HTTP downgrade', () => { | ||||||
| 		assert.throws(() => assertActivityMatchesUrls( | 		assert.throws(() => assertActivityMatchesUrl( | ||||||
| 			'https://alice.example.com/abc', | 			'https://alice.example.com/abc', | ||||||
| 			{ id: 'https://alice.example.com/abc' } as IObject, | 			{ id: 'https://alice.example.com/abc' } as IObject, | ||||||
| 			[ |  | ||||||
| 			'http://alice.example.com/abc', | 			'http://alice.example.com/abc', | ||||||
| 			], |  | ||||||
| 			FetchAllowSoftFailMask.Strict, | 			FetchAllowSoftFailMask.Strict, | ||||||
| 		), 'throws if HTTP downgrade is detected'); | 		), 'throws if HTTP downgrade is detected'); | ||||||
| 	}); | 	}); | ||||||
|   | |||||||
| @@ -17,8 +17,52 @@ interface SatisfiesExpression extends estree.BaseExpression { | |||||||
| 	reference: estree.Identifier; | 	reference: estree.Identifier; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | interface ImportDeclaration extends estree.ImportDeclaration { | ||||||
|  | 	kind?: 'type'; | ||||||
|  | } | ||||||
|  |  | ||||||
| const generator = { | const generator = { | ||||||
| 	...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) { | 	SatisfiesExpression(node: SatisfiesExpression, state: State) { | ||||||
| 		switch (node.expression.type) { | 		switch (node.expression.type) { | ||||||
| 			case 'ArrowFunctionExpression': { | 			case 'ArrowFunctionExpression': { | ||||||
| @@ -155,7 +199,8 @@ function toStories(component: string): Promise<string> { | |||||||
| 									/> as estree.ImportSpecifier, | 									/> as estree.ImportSpecifier, | ||||||
| 								]), | 								]), | ||||||
| 					]} | 					]} | ||||||
| 				/> as estree.ImportDeclaration, | 					kind={'type'} | ||||||
|  | 				/> as ImportDeclaration, | ||||||
| 				...(hasMsw | 				...(hasMsw | ||||||
| 					? [ | 					? [ | ||||||
| 							<import-declaration | 							<import-declaration | ||||||
| @@ -165,7 +210,7 @@ function toStories(component: string): Promise<string> { | |||||||
| 										local={<identifier name='msw' /> as estree.Identifier} | 										local={<identifier name='msw' /> as estree.Identifier} | ||||||
| 									/> as estree.ImportNamespaceSpecifier, | 									/> as estree.ImportNamespaceSpecifier, | ||||||
| 								]} | 								]} | ||||||
| 							/> as estree.ImportDeclaration, | 							/> as ImportDeclaration, | ||||||
| 						] | 						] | ||||||
| 					: []), | 					: []), | ||||||
| 				...(hasImplStories | 				...(hasImplStories | ||||||
| @@ -176,7 +221,7 @@ function toStories(component: string): Promise<string> { | |||||||
| 								specifiers={[ | 								specifiers={[ | ||||||
| 									<import-default-specifier local={identifier} /> as estree.ImportDefaultSpecifier, | 									<import-default-specifier local={identifier} /> as estree.ImportDefaultSpecifier, | ||||||
| 								]} | 								]} | ||||||
| 							/> as estree.ImportDeclaration, | 							/> as ImportDeclaration, | ||||||
| 						]), | 						]), | ||||||
| 				...(hasMetaStories | 				...(hasMetaStories | ||||||
| 					? [ | 					? [ | ||||||
| @@ -187,7 +232,7 @@ function toStories(component: string): Promise<string> { | |||||||
| 										local={<identifier name='storiesMeta' /> as estree.Identifier} | 										local={<identifier name='storiesMeta' /> as estree.Identifier} | ||||||
| 									/> as estree.ImportNamespaceSpecifier, | 									/> as estree.ImportNamespaceSpecifier, | ||||||
| 								]} | 								]} | ||||||
| 							/> as estree.ImportDeclaration, | 							/> as ImportDeclaration, | ||||||
| 						] | 						] | ||||||
| 					: []), | 					: []), | ||||||
| 				<variable-declaration | 				<variable-declaration | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ let moduleInitialized = false; | |||||||
| let unobserve = () => {}; | let unobserve = () => {}; | ||||||
| let misskeyOS = null; | let misskeyOS = null; | ||||||
|  |  | ||||||
| function loadTheme(applyTheme: typeof import('../src/scripts/theme')['applyTheme']) { | function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) { | ||||||
| 	unobserve(); | 	unobserve(); | ||||||
| 	const theme = themes[document.documentElement.dataset.misskeyTheme]; | 	const theme = themes[document.documentElement.dataset.misskeyTheme]; | ||||||
| 	if (theme) { | 	if (theme) { | ||||||
| @@ -67,10 +67,10 @@ queueMicrotask(() => { | |||||||
| 		import('../src/components'), | 		import('../src/components'), | ||||||
| 		import('../src/directives'), | 		import('../src/directives'), | ||||||
| 		import('../src/widgets'), | 		import('../src/widgets'), | ||||||
| 		import('../src/scripts/theme'), | 		import('../src/theme'), | ||||||
| 		import('../src/store'), | 		import('../src/preferences'), | ||||||
| 		import('../src/os'), | 		import('../src/os'), | ||||||
| 	]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { defaultStore }, os]) => { | 	]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { prefer }, os]) => { | ||||||
| 		setup((app) => { | 		setup((app) => { | ||||||
| 			moduleInitialized = true; | 			moduleInitialized = true; | ||||||
| 			if (app[appInitialized]) { | 			if (app[appInitialized]) { | ||||||
| @@ -83,7 +83,7 @@ queueMicrotask(() => { | |||||||
| 			widgets(app); | 			widgets(app); | ||||||
| 			misskeyOS = os; | 			misskeyOS = os; | ||||||
| 			if (isChromatic()) { | 			if (isChromatic()) { | ||||||
| 				defaultStore.set('animation', false); | 				prefer.set('animation', false); | ||||||
| 			} | 			} | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| @@ -104,9 +104,9 @@ const preview = { | |||||||
| 							} | 							} | ||||||
| 						}).catch(() => {}) | 						}).catch(() => {}) | ||||||
| 					: Promise.resolve(); | 					: Promise.resolve(); | ||||||
| 				const resetDefaultStorePromise = import('../src/store').then(({ defaultStore }) => { | 				const resetDefaultStorePromise = import('../src/store').then(({ store }) => { | ||||||
| 					// @ts-expect-error | 					// @ts-expect-error | ||||||
| 					defaultStore.init(); | 					store.init(); | ||||||
| 				}).catch(() => {}); | 				}).catch(() => {}); | ||||||
| 				Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => { | 				Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => { | ||||||
| 					initLocalStorage(); | 					initLocalStorage(); | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								packages/frontend/@types/theme.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -4,7 +4,7 @@ | |||||||
|  */ |  */ | ||||||
|  |  | ||||||
| declare module '@@/themes/*.json5' { | declare module '@@/themes/*.json5' { | ||||||
| 	import { Theme } from '@/scripts/theme.js'; | 	import { Theme } from '@/theme.js'; | ||||||
|  |  | ||||||
| 	const theme: Theme; | 	const theme: Theme; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/bell_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 25 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/cloud_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 18 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/desktop_computer_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 34 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/electric_plug_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/gear_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 39 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/link_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 30 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/locked_with_key_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 43 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/mens_room_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 31 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/musical_note_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 31 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/package_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 30 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/prohibited_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 40 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/speaker_high_volume_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 27 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/unlocked_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 30 KiB | 
| @@ -58,7 +58,7 @@ describe(normalizeClass.name, () => { | |||||||
|  |  | ||||||
| it('Composition API (standard)', () => { | it('Composition API (standard)', () => { | ||||||
| 	const ast = parse(` | 	const ast = parse(` | ||||||
| import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js'; | import { c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js'; | ||||||
| import { M as MkContainer } from './MkContainer-!~{03M}~.js'; | import { M as MkContainer } from './MkContainer-!~{03M}~.js'; | ||||||
| import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js'; | import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js'; | ||||||
| import './photoswipe-!~{003}~.js'; | import './photoswipe-!~{003}~.js'; | ||||||
| @@ -74,7 +74,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({ | |||||||
|     let fetching = ref(true); |     let fetching = ref(true); | ||||||
|     let images = ref([]); |     let images = ref([]); | ||||||
|     function thumbnail(image) { |     function thumbnail(image) { | ||||||
|       return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; |       return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; | ||||||
|     } |     } | ||||||
|     onMounted(() => { |     onMounted(() => { | ||||||
|       const image = [ |       const image = [ | ||||||
| @@ -173,7 +173,7 @@ export { index_photos as default }; | |||||||
| `.slice(1), { ecmaVersion: 'latest', sourceType: 'module' }); | `.slice(1), { ecmaVersion: 'latest', sourceType: 'module' }); | ||||||
| 	unwindCssModuleClassName(ast); | 	unwindCssModuleClassName(ast); | ||||||
| 	expect(generate(ast)).toBe(` | 	expect(generate(ast)).toBe(` | ||||||
| import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js'; | import {c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js'; | ||||||
| import {M as MkContainer} from './MkContainer-!~{03M}~.js'; | import {M as MkContainer} from './MkContainer-!~{03M}~.js'; | ||||||
| import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js'; | import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js'; | ||||||
| import './photoswipe-!~{003}~.js'; | import './photoswipe-!~{003}~.js'; | ||||||
| @@ -190,7 +190,7 @@ const index_photos = defineComponent({ | |||||||
|     let fetching = ref(true); |     let fetching = ref(true); | ||||||
|     let images = ref([]); |     let images = ref([]); | ||||||
|     function thumbnail(image) { |     function thumbnail(image) { | ||||||
|       return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; |       return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl; | ||||||
|     } |     } | ||||||
|     onMounted(() => { |     onMounted(() => { | ||||||
|       const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"]; |       const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"]; | ||||||
| @@ -268,7 +268,7 @@ export {index_photos as default}; | |||||||
| it('Composition API (with `useCssModule()`)', () => { | it('Composition API (with `useCssModule()`)', () => { | ||||||
| 	const ast = parse(` | 	const ast = parse(` | ||||||
| import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js'; | import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js'; | ||||||
| import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js'; | import { d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js'; | ||||||
|  |  | ||||||
| function isDebuggerEnabled(id) { | function isDebuggerEnabled(id) { | ||||||
|   try { |   try { | ||||||
| @@ -393,7 +393,7 @@ const _sfc_main = defineComponent({ | |||||||
|       el.style.left = ""; |       el.style.left = ""; | ||||||
|     } |     } | ||||||
|     return () => h( |     return () => h( | ||||||
|       defaultStore.state.animation ? TransitionGroup : "div", |       prefer.s.animation ? TransitionGroup : "div", | ||||||
|       { |       { | ||||||
|         class: { |         class: { | ||||||
|           [$style["date-separated-list"]]: true, |           [$style["date-separated-list"]]: true, | ||||||
| @@ -402,7 +402,7 @@ const _sfc_main = defineComponent({ | |||||||
|           [$style["direction-down"]]: props.direction === "down", |           [$style["direction-down"]]: props.direction === "down", | ||||||
|           [$style["direction-up"]]: props.direction === "up" |           [$style["direction-up"]]: props.direction === "up" | ||||||
|         }, |         }, | ||||||
|         ...defaultStore.state.animation ? { |         ...prefer.s.animation ? { | ||||||
|           name: "list", |           name: "list", | ||||||
|           tag: "div", |           tag: "div", | ||||||
|           onBeforeLeave, |           onBeforeLeave, | ||||||
| @@ -441,7 +441,7 @@ export { MkDateSeparatedList as M }; | |||||||
| 	unwindCssModuleClassName(ast); | 	unwindCssModuleClassName(ast); | ||||||
| 	expect(generate(ast)).toBe(` | 	expect(generate(ast)).toBe(` | ||||||
| import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js'; | import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js'; | ||||||
| import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js'; | import {d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js'; | ||||||
| function isDebuggerEnabled(id) { | function isDebuggerEnabled(id) { | ||||||
|   try { |   try { | ||||||
|     return localStorage.getItem(\`DEBUG_\${id}\`) !== null; |     return localStorage.getItem(\`DEBUG_\${id}\`) !== null; | ||||||
| @@ -555,7 +555,7 @@ const _sfc_main = defineComponent({ | |||||||
|       el.style.top = ""; |       el.style.top = ""; | ||||||
|       el.style.left = ""; |       el.style.left = ""; | ||||||
|     } |     } | ||||||
|     return () => h(defaultStore.state.animation ? TransitionGroup : "div", { |     return () => h(prefer.s.animation ? TransitionGroup : "div", { | ||||||
|       class: { |       class: { | ||||||
|         [$style["date-separated-list"]]: true, |         [$style["date-separated-list"]]: true, | ||||||
|         [$style["date-separated-list-nogap"]]: props.noGap, |         [$style["date-separated-list-nogap"]]: props.noGap, | ||||||
| @@ -563,7 +563,7 @@ const _sfc_main = defineComponent({ | |||||||
|         [$style["direction-down"]]: props.direction === "down", |         [$style["direction-down"]]: props.direction === "down", | ||||||
|         [$style["direction-up"]]: props.direction === "up" |         [$style["direction-up"]]: props.direction === "up" | ||||||
|       }, |       }, | ||||||
|       ...defaultStore.state.animation ? { |       ...prefer.s.animation ? { | ||||||
|         name: "list", |         name: "list", | ||||||
|         tag: "div", |         tag: "div", | ||||||
|         onBeforeLeave, |         onBeforeLeave, | ||||||
|   | |||||||
| @@ -1213,22 +1213,37 @@ async function processVueFile( | |||||||
| 	transformedCodeCache: Record<string, string> | 	transformedCodeCache: Record<string, string> | ||||||
| }> { | }> { | ||||||
| 	const normalizedId = id.replace(/\\/g, '/'); // ファイルパスを正規化 | 	const normalizedId = id.replace(/\\/g, '/'); // ファイルパスを正規化 | ||||||
| 	// すでにキャッシュに存在する場合は、そのまま返す |  | ||||||
| 	if (transformedCodeCache[normalizedId] && transformedCodeCache[normalizedId].includes('markerId=')) { | 	// 開発モード時はコード内容に変更があれば常に再処理する | ||||||
|  | 	// コード内容が同じ場合のみキャッシュを使用 | ||||||
|  | 	const isDevMode = process.env.NODE_ENV === 'development'; | ||||||
|  |  | ||||||
|  | 	const s = new MagicString(code); // magic-string のインスタンスを作成 | ||||||
|  |  | ||||||
|  | 	if (!isDevMode && transformedCodeCache[normalizedId] && transformedCodeCache[normalizedId].includes('markerId=')) { | ||||||
| 		logger.info(`Using cached version for ${id}`); | 		logger.info(`Using cached version for ${id}`); | ||||||
| 		return { | 		return { | ||||||
| 			code: transformedCodeCache[normalizedId], | 			code: transformedCodeCache[normalizedId], | ||||||
| 			map: null, | 			map: s.generateMap({ source: id, includeContent: true }), | ||||||
|  | 			transformedCodeCache | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// すでに処理済みのファイルでコードに変更がない場合はキャッシュを返す | ||||||
|  | 	if (transformedCodeCache[normalizedId] === code) { | ||||||
|  | 		logger.info(`Code unchanged for ${id}, using cached version`); | ||||||
|  | 		return { | ||||||
|  | 			code: transformedCodeCache[normalizedId], | ||||||
|  | 			map: s.generateMap({ source: id, includeContent: true }), | ||||||
| 			transformedCodeCache | 			transformedCodeCache | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const s = new MagicString(code); // magic-string のインスタンスを作成 |  | ||||||
| 	const parsed = vueSfcParse(code, { filename: id }); | 	const parsed = vueSfcParse(code, { filename: id }); | ||||||
| 	if (!parsed.descriptor.template) { | 	if (!parsed.descriptor.template) { | ||||||
| 		return { | 		return { | ||||||
| 			code, | 			code, | ||||||
| 			map: null, | 			map: s.generateMap({ source: id, includeContent: true }), | ||||||
| 			transformedCodeCache | 			transformedCodeCache | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| @@ -1413,6 +1428,23 @@ 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 | // Rollup プラグインとして export | ||||||
| export default function pluginCreateSearchIndex(options: Options): Plugin { | export default function pluginCreateSearchIndex(options: Options): Plugin { | ||||||
| @@ -1430,19 +1462,7 @@ export default function pluginCreateSearchIndex(options: Options): Plugin { | |||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			const filePaths = options.targetFilePaths.reduce<string[]>((acc, filePathPattern) => { | 			transformedCodeCache = await generateSearchIndex(options, transformedCodeCache); | ||||||
| 				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) { | 		async transform(code, id) { | ||||||
| @@ -1466,16 +1486,21 @@ export default function pluginCreateSearchIndex(options: Options): Plugin { | |||||||
| 				if (isMatch) break; // いずれかのパターンでマッチしたら、outer loop も抜ける | 				if (isMatch) break; // いずれかのパターンでマッチしたら、outer loop も抜ける | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  |  | ||||||
| 			if (!isMatch) { | 			if (!isMatch) { | ||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			// ファイルの内容が変更された場合は再処理を行う | ||||||
|  | 			const normalizedId = id.replace(/\\/g, '/'); | ||||||
|  | 			const hasContentChanged = !transformedCodeCache[normalizedId] || transformedCodeCache[normalizedId] !== code; | ||||||
|  |  | ||||||
| 			const transformed = await processVueFile(code, id, options, transformedCodeCache); | 			const transformed = await processVueFile(code, id, options, transformedCodeCache); | ||||||
| 			transformedCodeCache = transformed.transformedCodeCache; // キャッシュを更新 | 			transformedCodeCache = transformed.transformedCodeCache; // キャッシュを更新 | ||||||
| 			if (isDevServer) { |  | ||||||
| 				await analyzeVueProps({ ...options, transformedCodeCache }); // analyzeVueProps を呼び出す | 			if (isDevServer && hasContentChanged) { | ||||||
|  | 				await analyzeVueProps({ ...options, transformedCodeCache }); // ファイルが変更されたときのみ分析を実行 | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			return transformed; | 			return transformed; | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
| 	"scripts": { | 	"scripts": { | ||||||
| 		"watch": "vite", | 		"watch": "vite", | ||||||
| 		"build": "vite build", | 		"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\"", | 		"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-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", | 		"build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static", | ||||||
| @@ -75,7 +76,8 @@ | |||||||
| 		"v-code-diff": "1.13.1", | 		"v-code-diff": "1.13.1", | ||||||
| 		"vite": "6.2.1", | 		"vite": "6.2.1", | ||||||
| 		"vue": "3.5.13", | 		"vue": "3.5.13", | ||||||
| 		"vuedraggable": "next" | 		"vuedraggable": "next", | ||||||
|  | 		"wanakana": "5.3.1" | ||||||
| 	}, | 	}, | ||||||
| 	"devDependencies": { | 	"devDependencies": { | ||||||
| 		"@misskey-dev/summaly": "5.2.0", | 		"@misskey-dev/summaly": "5.2.0", | ||||||
| @@ -132,6 +134,7 @@ | |||||||
| 		"start-server-and-test": "2.0.10", | 		"start-server-and-test": "2.0.10", | ||||||
| 		"storybook": "8.6.4", | 		"storybook": "8.6.4", | ||||||
| 		"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", | 		"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", | ||||||
|  | 		"vite-node": "3.0.8", | ||||||
| 		"vite-plugin-turbosnap": "1.0.3", | 		"vite-plugin-turbosnap": "1.0.3", | ||||||
| 		"vitest": "3.0.8", | 		"vitest": "3.0.8", | ||||||
| 		"vitest-fetch-mock": "0.4.5", | 		"vitest-fetch-mock": "0.4.5", | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								packages/frontend/scripts/generate-search-index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | |||||||
|  | /* | ||||||
|  |  * 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(); | ||||||
| @@ -8,15 +8,16 @@ import * as Misskey from 'misskey-js'; | |||||||
| import { apiUrl } from '@@/js/config.js'; | import { apiUrl } from '@@/js/config.js'; | ||||||
| import type { MenuItem, MenuButton } from '@/types/menu.js'; | import type { MenuItem, MenuButton } from '@/types/menu.js'; | ||||||
| import { defaultMemoryStorage } from '@/memory-storage'; | import { defaultMemoryStorage } from '@/memory-storage'; | ||||||
| import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; | import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { miLocalStorage } from '@/local-storage.js'; | import { miLocalStorage } from '@/local-storage.js'; | ||||||
| import { del, get, set } from '@/scripts/idb-proxy.js'; | import { del, get, set } from '@/utility/idb-proxy.js'; | ||||||
| import { waiting, popup, popupMenu, success, alert } from '@/os.js'; | import { waiting, popup, popupMenu, success, alert } from '@/os.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; | import { unisonReload, reloadChannel } from '@/utility/unison-reload.js'; | ||||||
|  |  | ||||||
| // TODO: 他のタブと永続化されたstateを同期 | // TODO: 他のタブと永続化されたstateを同期 | ||||||
|  | // TODO: accountsはpreferences管理にする(tokenは別管理) | ||||||
|  |  | ||||||
| type Account = Misskey.entities.MeDetailed & { token: string }; | type Account = Misskey.entities.MeDetailed & { token: string }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ import * as Misskey from 'misskey-js'; | |||||||
| import { url, lang } from '@@/js/config.js'; | import { url, lang } from '@@/js/config.js'; | ||||||
| import { assertStringAndIsIn } from './common.js'; | import { assertStringAndIsIn } from './common.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { $i } from '@/account.js'; | import { $i } from '@/account.js'; | ||||||
| import { miLocalStorage } from '@/local-storage.js'; | import { miLocalStorage } from '@/local-storage.js'; | ||||||
| import { customEmojis } from '@/custom-emojis.js'; | import { customEmojis } from '@/custom-emojis.js'; | ||||||
| @@ -6,26 +6,29 @@ | |||||||
| import { computed, watch, version as vueVersion } from 'vue'; | import { computed, watch, version as vueVersion } from 'vue'; | ||||||
| import { compareVersions } from 'compare-versions'; | import { compareVersions } from 'compare-versions'; | ||||||
| import { version, lang, updateLocale, locale } from '@@/js/config.js'; | import { version, lang, updateLocale, locale } from '@@/js/config.js'; | ||||||
|  | import defaultLightTheme from '@@/themes/l-light.json5'; | ||||||
|  | import defaultDarkTheme from '@@/themes/d-green-lime.json5'; | ||||||
| import type { App } from 'vue'; | import type { App } from 'vue'; | ||||||
| import widgets from '@/widgets/index.js'; | import widgets from '@/widgets/index.js'; | ||||||
| import directives from '@/directives/index.js'; | import directives from '@/directives/index.js'; | ||||||
| import components from '@/components/index.js'; | import components from '@/components/index.js'; | ||||||
| import { applyTheme } from '@/scripts/theme.js'; | import { applyTheme } from '@/theme.js'; | ||||||
| import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; | import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js'; | ||||||
| import { updateI18n, i18n } from '@/i18n.js'; | import { updateI18n, i18n } from '@/i18n.js'; | ||||||
| import { $i, refreshAccount, login } from '@/account.js'; | import { $i, refreshAccount, login } from '@/account.js'; | ||||||
| import { defaultStore, ColdDeviceStorage } from '@/store.js'; | import { store } from '@/store.js'; | ||||||
| import { fetchInstance, instance } from '@/instance.js'; | import { fetchInstance, instance } from '@/instance.js'; | ||||||
| import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js'; | import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js'; | ||||||
| import { reloadChannel } from '@/scripts/unison-reload.js'; | import { reloadChannel } from '@/utility/unison-reload.js'; | ||||||
| import { getUrlWithoutLoginId } from '@/scripts/login-id.js'; | import { getUrlWithoutLoginId } from '@/utility/login-id.js'; | ||||||
| import { getAccountFromId } from '@/scripts/get-account-from-id.js'; | import { getAccountFromId } from '@/utility/get-account-from-id.js'; | ||||||
| import { deckStore } from '@/ui/deck/deck-store.js'; | import { deckStore } from '@/ui/deck/deck-store.js'; | ||||||
| import { analytics, initAnalytics } from '@/analytics.js'; | import { analytics, initAnalytics } from '@/analytics.js'; | ||||||
| import { miLocalStorage } from '@/local-storage.js'; | import { miLocalStorage } from '@/local-storage.js'; | ||||||
| import { fetchCustomEmojis } from '@/custom-emojis.js'; | import { fetchCustomEmojis } from '@/custom-emojis.js'; | ||||||
| import { setupRouter } from '@/router/main.js'; | import { setupRouter } from '@/router/main.js'; | ||||||
| import { createMainRouter } from '@/router/definition.js'; | import { createMainRouter } from '@/router/definition.js'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| export async function common(createVue: () => App<Element>) { | export async function common(createVue: () => App<Element>) { | ||||||
| 	console.info(`Misskey v${version}`); | 	console.info(`Misskey v${version}`); | ||||||
| @@ -38,7 +41,7 @@ export async function common(createVue: () => App<Element>) { | |||||||
| 		// eslint-disable-next-line @typescript-eslint/no-explicit-any | 		// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
| 		(window as any).$i = $i; | 		(window as any).$i = $i; | ||||||
| 		// eslint-disable-next-line @typescript-eslint/no-explicit-any | 		// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
| 		(window as any).$store = defaultStore; | 		(window as any).$store = store; | ||||||
|  |  | ||||||
| 		window.addEventListener('error', event => { | 		window.addEventListener('error', event => { | ||||||
| 			console.error(event); | 			console.error(event); | ||||||
| @@ -123,7 +126,7 @@ export async function common(createVue: () => App<Element>) { | |||||||
| 	html.setAttribute('lang', lang); | 	html.setAttribute('lang', lang); | ||||||
| 	//#endregion | 	//#endregion | ||||||
|  |  | ||||||
| 	await defaultStore.ready; | 	await store.ready; | ||||||
| 	await deckStore.ready; | 	await deckStore.ready; | ||||||
|  |  | ||||||
| 	const fetchInstanceMetaPromise = fetchInstance(); | 	const fetchInstanceMetaPromise = fetchInstance(); | ||||||
| @@ -151,56 +154,63 @@ export async function common(createVue: () => App<Element>) { | |||||||
| 	//#endregion | 	//#endregion | ||||||
|  |  | ||||||
| 	// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため) | 	// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため) | ||||||
| 	watch(defaultStore.reactiveState.darkMode, (darkMode) => { | 	watch(store.r.darkMode, (darkMode) => { | ||||||
| 		applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); | 		applyTheme(darkMode | ||||||
|  | 			? (prefer.s.darkTheme ?? defaultDarkTheme) | ||||||
|  | 			: (prefer.s.lightTheme ?? defaultLightTheme), | ||||||
|  | 		); | ||||||
| 	}, { immediate: miLocalStorage.getItem('theme') == null }); | 	}, { immediate: miLocalStorage.getItem('theme') == null }); | ||||||
|  |  | ||||||
| 	document.documentElement.dataset.colorScheme = defaultStore.state.darkMode ? 'dark' : 'light'; | 	document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light'; | ||||||
|  |  | ||||||
| 	const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); | 	const darkTheme = prefer.model('darkTheme'); | ||||||
| 	const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); | 	const lightTheme = prefer.model('lightTheme'); | ||||||
|  |  | ||||||
| 	watch(darkTheme, (theme) => { | 	watch(darkTheme, (theme) => { | ||||||
| 		if (defaultStore.state.darkMode) { | 		if (store.s.darkMode) { | ||||||
| 			applyTheme(theme); | 			applyTheme(theme ?? defaultDarkTheme); | ||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	watch(lightTheme, (theme) => { | 	watch(lightTheme, (theme) => { | ||||||
| 		if (!defaultStore.state.darkMode) { | 		if (!store.s.darkMode) { | ||||||
| 			applyTheme(theme); | 			applyTheme(theme ?? defaultLightTheme); | ||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	//#region Sync dark mode | 	//#region Sync dark mode | ||||||
| 	if (ColdDeviceStorage.get('syncDeviceDarkMode')) { | 	if (prefer.s.syncDeviceDarkMode) { | ||||||
| 		defaultStore.set('darkMode', isDeviceDarkmode()); | 		store.set('darkMode', isDeviceDarkmode()); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => { | 	window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => { | ||||||
| 		if (ColdDeviceStorage.get('syncDeviceDarkMode')) { | 		if (prefer.s.syncDeviceDarkMode) { | ||||||
| 			defaultStore.set('darkMode', mql.matches); | 			store.set('darkMode', mql.matches); | ||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
| 	//#endregion | 	//#endregion | ||||||
|  |  | ||||||
| 	fetchInstanceMetaPromise.then(() => { | 	if (prefer.s.darkTheme && store.s.darkMode) { | ||||||
| 		if (defaultStore.state.themeInitial) { | 		if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme); | ||||||
| 			if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme)); | 	} else if (prefer.s.lightTheme && !store.s.darkMode) { | ||||||
| 			if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme)); | 		if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme); | ||||||
| 			defaultStore.set('themeInitial', false); |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	fetchInstanceMetaPromise.then(() => { | ||||||
|  | 		// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア | ||||||
|  | 		if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme)); | ||||||
|  | 		if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme)); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => { | 	watch(prefer.r.overridedDeviceKind, (kind) => { | ||||||
| 		updateDeviceKind(kind); | 		updateDeviceKind(kind); | ||||||
| 	}, { immediate: true }); | 	}, { immediate: true }); | ||||||
|  |  | ||||||
| 	watch(defaultStore.reactiveState.useBlurEffectForModal, v => { | 	watch(prefer.r.useBlurEffectForModal, v => { | ||||||
| 		document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none'); | 		document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none'); | ||||||
| 	}, { immediate: true }); | 	}, { immediate: true }); | ||||||
|  |  | ||||||
| 	watch(defaultStore.reactiveState.useBlurEffect, v => { | 	watch(prefer.r.useBlurEffect, v => { | ||||||
| 		if (v) { | 		if (v) { | ||||||
| 			document.documentElement.style.removeProperty('--MI-blur'); | 			document.documentElement.style.removeProperty('--MI-blur'); | ||||||
| 		} else { | 		} else { | ||||||
| @@ -214,7 +224,7 @@ export async function common(createVue: () => App<Element>) { | |||||||
| 			navigator.wakeLock.request('screen'); | 			navigator.wakeLock.request('screen'); | ||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
| 	if (defaultStore.state.keepScreenOn && 'wakeLock' in navigator) { | 	if (prefer.s.keepScreenOn && 'wakeLock' in navigator) { | ||||||
| 		navigator.wakeLock.request('screen') | 		navigator.wakeLock.request('screen') | ||||||
| 			.then(onVisibilityChange) | 			.then(onVisibilityChange) | ||||||
| 			.catch(() => { | 			.catch(() => { | ||||||
|   | |||||||
| @@ -5,26 +5,31 @@ | |||||||
|  |  | ||||||
| import { createApp, defineAsyncComponent, markRaw } from 'vue'; | import { createApp, defineAsyncComponent, markRaw } from 'vue'; | ||||||
| import { ui } from '@@/js/config.js'; | import { ui } from '@@/js/config.js'; | ||||||
| import { common } from './common.js'; |  | ||||||
| import * as Misskey from 'misskey-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 { Component } from 'vue'; | ||||||
|  | import type { Keymap } from '@/utility/hotkey.js'; | ||||||
|  | import type { DeckProfile } from '@/deck.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { alert, confirm, popup, post, toast } from '@/os.js'; | import { alert, confirm, popup, post, toast } from '@/os.js'; | ||||||
| import { useStream } from '@/stream.js'; | import { useStream } from '@/stream.js'; | ||||||
| import * as sound from '@/scripts/sound.js'; | import * as sound from '@/utility/sound.js'; | ||||||
| import { $i, signout, updateAccountPartial } from '@/account.js'; | import { $i, signout, updateAccountPartial } from '@/account.js'; | ||||||
| import { instance } from '@/instance.js'; | import { instance } from '@/instance.js'; | ||||||
| import { ColdDeviceStorage, defaultStore } from '@/store.js'; | import { ColdDeviceStorage, store } from '@/store.js'; | ||||||
| import { reactionPicker } from '@/scripts/reaction-picker.js'; | import { reactionPicker } from '@/utility/reaction-picker.js'; | ||||||
| import { miLocalStorage } from '@/local-storage.js'; | import { miLocalStorage } from '@/local-storage.js'; | ||||||
| import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; | import { claimAchievement, claimedAchievements } from '@/utility/achievements.js'; | ||||||
| import { initializeSw } from '@/scripts/initialize-sw.js'; | import { initializeSw } from '@/utility/initialize-sw.js'; | ||||||
| import { deckStore } from '@/ui/deck/deck-store.js'; | import { emojiPicker } from '@/utility/emoji-picker.js'; | ||||||
| import { emojiPicker } from '@/scripts/emoji-picker.js'; |  | ||||||
| import { mainRouter } from '@/router/main.js'; | import { mainRouter } from '@/router/main.js'; | ||||||
| import { makeHotkey } from '@/scripts/hotkey.js'; | import { makeHotkey } from '@/utility/hotkey.js'; | ||||||
| import type { Keymap } from '@/scripts/hotkey.js'; |  | ||||||
| import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js'; | import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
|  | import { deckStore } from '@/ui/deck/deck-store.js'; | ||||||
|  | import { launchPlugins } from '@/plugin.js'; | ||||||
|  |  | ||||||
| export async function mainBoot() { | export async function mainBoot() { | ||||||
| 	const { isClientUpdated } = await common(() => { | 	const { isClientUpdated } = await common(() => { | ||||||
| @@ -34,7 +39,7 @@ export async function mainBoot() { | |||||||
| 		if (!$i) uiStyle = 'visitor'; | 		if (!$i) uiStyle = 'visitor'; | ||||||
|  |  | ||||||
| 		if (searchParams.has('zen')) uiStyle = 'zen'; | 		if (searchParams.has('zen')) uiStyle = 'zen'; | ||||||
| 		if (uiStyle === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') uiStyle = 'zen'; | 		if (uiStyle === 'deck' && prefer.s['deck.useSimpleUiForNonRootPages'] && location.pathname !== '/') uiStyle = 'zen'; | ||||||
|  |  | ||||||
| 		if (searchParams.has('ui')) uiStyle = searchParams.get('ui'); | 		if (searchParams.has('ui')) uiStyle = searchParams.get('ui'); | ||||||
|  |  | ||||||
| @@ -73,9 +78,9 @@ export async function mainBoot() { | |||||||
|  |  | ||||||
| 	let reloadDialogShowing = false; | 	let reloadDialogShowing = false; | ||||||
| 	stream.on('_disconnected_', async () => { | 	stream.on('_disconnected_', async () => { | ||||||
| 		if (defaultStore.state.serverDisconnectedBehavior === 'reload') { | 		if (prefer.s.serverDisconnectedBehavior === 'reload') { | ||||||
| 			location.reload(); | 			location.reload(); | ||||||
| 		} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { | 		} else if (prefer.s.serverDisconnectedBehavior === 'dialog') { | ||||||
| 			if (reloadDialogShowing) return; | 			if (reloadDialogShowing) return; | ||||||
| 			reloadDialogShowing = true; | 			reloadDialogShowing = true; | ||||||
| 			const { canceled } = await confirm({ | 			const { canceled } = await confirm({ | ||||||
| @@ -102,30 +107,24 @@ export async function mainBoot() { | |||||||
| 		removeCustomEmojis(emojiData.emojis); | 		removeCustomEmojis(emojiData.emojis); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { | 	launchPlugins(); | ||||||
| 		import('@/plugin.js').then(async ({ install }) => { |  | ||||||
| 			// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 |  | ||||||
| 			await new Promise(r => setTimeout(r, 0)); |  | ||||||
| 			install(plugin); |  | ||||||
| 		}); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	try { | 	try { | ||||||
| 		if (defaultStore.state.enableSeasonalScreenEffect) { | 		if (prefer.s.enableSeasonalScreenEffect) { | ||||||
| 			const month = new Date().getMonth() + 1; | 			const month = new Date().getMonth() + 1; | ||||||
| 			if (defaultStore.state.hemisphere === 'S') { | 			if (prefer.s.hemisphere === 'S') { | ||||||
| 				// ▼南半球 | 				// ▼南半球 | ||||||
| 				if (month === 7 || month === 8) { | 				if (month === 7 || month === 8) { | ||||||
| 					const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect; | 					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect; | ||||||
| 					new SnowfallEffect({}).render(); | 					new SnowfallEffect({}).render(); | ||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
| 				// ▼北半球 | 				// ▼北半球 | ||||||
| 				if (month === 12 || month === 1) { | 				if (month === 12 || month === 1) { | ||||||
| 					const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect; | 					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect; | ||||||
| 					new SnowfallEffect({}).render(); | 					new SnowfallEffect({}).render(); | ||||||
| 				} else if (month === 3 || month === 4) { | 				} else if (month === 3 || month === 4) { | ||||||
| 					const SakuraEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect; | 					const SakuraEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect; | ||||||
| 					new SakuraEffect({ | 					new SakuraEffect({ | ||||||
| 						sakura: true, | 						sakura: true, | ||||||
| 					}).render(); | 					}).render(); | ||||||
| @@ -138,8 +137,120 @@ export async function mainBoot() { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if ($i) { | 	if ($i) { | ||||||
| 		defaultStore.loaded.then(() => { | 		store.loaded.then(async () => { | ||||||
| 			if (defaultStore.state.accountSetupWizard !== -1) { | 			// prefereces migration | ||||||
|  | 			// TODO: そのうち消す | ||||||
|  | 			if (store.s.menu.length > 0) { | ||||||
|  | 				const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []); | ||||||
|  | 				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')); | ||||||
|  | 				prefer.commit('overridedDeviceKind', store.s.overridedDeviceKind); | ||||||
|  | 				prefer.commit('widgets', store.s.widgets); | ||||||
|  | 				prefer.commit('keepCw', store.s.keepCw); | ||||||
|  | 				prefer.commit('collapseRenotes', store.s.collapseRenotes); | ||||||
|  | 				prefer.commit('rememberNoteVisibility', store.s.rememberNoteVisibility); | ||||||
|  | 				prefer.commit('uploadFolder', store.s.uploadFolder); | ||||||
|  | 				prefer.commit('keepOriginalUploading', store.s.keepOriginalUploading); | ||||||
|  | 				prefer.commit('menu', store.s.menu); | ||||||
|  | 				prefer.commit('statusbars', store.s.statusbars); | ||||||
|  | 				prefer.commit('pinnedUserLists', store.s.pinnedUserLists); | ||||||
|  | 				prefer.commit('serverDisconnectedBehavior', store.s.serverDisconnectedBehavior); | ||||||
|  | 				prefer.commit('nsfw', store.s.nsfw); | ||||||
|  | 				prefer.commit('highlightSensitiveMedia', store.s.highlightSensitiveMedia); | ||||||
|  | 				prefer.commit('animation', store.s.animation); | ||||||
|  | 				prefer.commit('animatedMfm', store.s.animatedMfm); | ||||||
|  | 				prefer.commit('advancedMfm', store.s.advancedMfm); | ||||||
|  | 				prefer.commit('showReactionsCount', store.s.showReactionsCount); | ||||||
|  | 				prefer.commit('enableQuickAddMfmFunction', store.s.enableQuickAddMfmFunction); | ||||||
|  | 				prefer.commit('loadRawImages', store.s.loadRawImages); | ||||||
|  | 				prefer.commit('imageNewTab', store.s.imageNewTab); | ||||||
|  | 				prefer.commit('disableShowingAnimatedImages', store.s.disableShowingAnimatedImages); | ||||||
|  | 				prefer.commit('emojiStyle', store.s.emojiStyle); | ||||||
|  | 				prefer.commit('menuStyle', store.s.menuStyle); | ||||||
|  | 				prefer.commit('useBlurEffectForModal', store.s.useBlurEffectForModal); | ||||||
|  | 				prefer.commit('useBlurEffect', store.s.useBlurEffect); | ||||||
|  | 				prefer.commit('showFixedPostForm', store.s.showFixedPostForm); | ||||||
|  | 				prefer.commit('showFixedPostFormInChannel', store.s.showFixedPostFormInChannel); | ||||||
|  | 				prefer.commit('enableInfiniteScroll', store.s.enableInfiniteScroll); | ||||||
|  | 				prefer.commit('useReactionPickerForContextMenu', store.s.useReactionPickerForContextMenu); | ||||||
|  | 				prefer.commit('showGapBetweenNotesInTimeline', store.s.showGapBetweenNotesInTimeline); | ||||||
|  | 				prefer.commit('instanceTicker', store.s.instanceTicker); | ||||||
|  | 				prefer.commit('emojiPickerScale', store.s.emojiPickerScale); | ||||||
|  | 				prefer.commit('emojiPickerWidth', store.s.emojiPickerWidth); | ||||||
|  | 				prefer.commit('emojiPickerHeight', store.s.emojiPickerHeight); | ||||||
|  | 				prefer.commit('emojiPickerStyle', store.s.emojiPickerStyle); | ||||||
|  | 				prefer.commit('reportError', store.s.reportError); | ||||||
|  | 				prefer.commit('squareAvatars', store.s.squareAvatars); | ||||||
|  | 				prefer.commit('showAvatarDecorations', store.s.showAvatarDecorations); | ||||||
|  | 				prefer.commit('numberOfPageCache', store.s.numberOfPageCache); | ||||||
|  | 				prefer.commit('showNoteActionsOnlyHover', store.s.showNoteActionsOnlyHover); | ||||||
|  | 				prefer.commit('showClipButtonInNoteFooter', store.s.showClipButtonInNoteFooter); | ||||||
|  | 				prefer.commit('reactionsDisplaySize', store.s.reactionsDisplaySize); | ||||||
|  | 				prefer.commit('limitWidthOfReaction', store.s.limitWidthOfReaction); | ||||||
|  | 				prefer.commit('forceShowAds', store.s.forceShowAds); | ||||||
|  | 				prefer.commit('aiChanMode', store.s.aiChanMode); | ||||||
|  | 				prefer.commit('devMode', store.s.devMode); | ||||||
|  | 				prefer.commit('mediaListWithOneImageAppearance', store.s.mediaListWithOneImageAppearance); | ||||||
|  | 				prefer.commit('notificationPosition', store.s.notificationPosition); | ||||||
|  | 				prefer.commit('notificationStackAxis', store.s.notificationStackAxis); | ||||||
|  | 				prefer.commit('enableCondensedLine', store.s.enableCondensedLine); | ||||||
|  | 				prefer.commit('keepScreenOn', store.s.keepScreenOn); | ||||||
|  | 				prefer.commit('disableStreamingTimeline', store.s.disableStreamingTimeline); | ||||||
|  | 				prefer.commit('useGroupedNotifications', store.s.useGroupedNotifications); | ||||||
|  | 				prefer.commit('dataSaver', store.s.dataSaver); | ||||||
|  | 				prefer.commit('enableSeasonalScreenEffect', store.s.enableSeasonalScreenEffect); | ||||||
|  | 				prefer.commit('enableHorizontalSwipe', store.s.enableHorizontalSwipe); | ||||||
|  | 				prefer.commit('useNativeUiForVideoAudioPlayer', store.s.useNativeUIForVideoAudioPlayer); | ||||||
|  | 				prefer.commit('keepOriginalFilename', store.s.keepOriginalFilename); | ||||||
|  | 				prefer.commit('alwaysConfirmFollow', store.s.alwaysConfirmFollow); | ||||||
|  | 				prefer.commit('confirmWhenRevealingSensitiveMedia', store.s.confirmWhenRevealingSensitiveMedia); | ||||||
|  | 				prefer.commit('contextMenu', store.s.contextMenu); | ||||||
|  | 				prefer.commit('skipNoteRender', store.s.skipNoteRender); | ||||||
|  | 				prefer.commit('showSoftWordMutedWord', store.s.showSoftWordMutedWord); | ||||||
|  | 				prefer.commit('confirmOnReact', store.s.confirmOnReact); | ||||||
|  | 				prefer.commit('sound.masterVolume', store.s.sound_masterVolume); | ||||||
|  | 				prefer.commit('sound.notUseSound', store.s.sound_notUseSound); | ||||||
|  | 				prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive); | ||||||
|  | 				prefer.commit('sound.on.note', store.s.sound_note as any); | ||||||
|  | 				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', []); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if (store.s.accountSetupWizard !== -1) { | ||||||
| 				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, { | 				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, { | ||||||
| 					closed: () => dispose(), | 					closed: () => dispose(), | ||||||
| 				}); | 				}); | ||||||
| @@ -154,7 +265,7 @@ export async function mainBoot() { | |||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		function onAnnouncementCreated (ev: { announcement: Misskey.entities.Announcement }) { | 		function onAnnouncementCreated(ev: { announcement: Misskey.entities.Announcement }) { | ||||||
| 			const announcement = ev.announcement; | 			const announcement = ev.announcement; | ||||||
| 			if (announcement.display === 'dialog') { | 			if (announcement.display === 'dialog') { | ||||||
| 				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { | 				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { | ||||||
| @@ -412,7 +523,7 @@ export async function mainBoot() { | |||||||
| 			post(); | 			post(); | ||||||
| 		}, | 		}, | ||||||
| 		'd': () => { | 		'd': () => { | ||||||
| 			defaultStore.set('darkMode', !defaultStore.state.darkMode); | 			store.set('darkMode', !store.s.darkMode); | ||||||
| 		}, | 		}, | ||||||
| 		's': () => { | 		's': () => { | ||||||
| 			mainRouter.push('/search'); | 			mainRouter.push('/search'); | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
|  |  | ||||||
| import { createApp, defineAsyncComponent } from 'vue'; | import { createApp, defineAsyncComponent } from 'vue'; | ||||||
| import { common } from './common.js'; | import { common } from './common.js'; | ||||||
| import { emojiPicker } from '@/scripts/emoji-picker.js'; | import { emojiPicker } from '@/utility/emoji-picker.js'; | ||||||
|  |  | ||||||
| export async function subBoot() { | export async function subBoot() { | ||||||
| 	const { isClientUpdated } = await common(() => createApp( | 	const { isClientUpdated } = await common(() => createApp( | ||||||
|   | |||||||
| @@ -4,8 +4,8 @@ | |||||||
|  */ |  */ | ||||||
|  |  | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import { Cache } from '@/scripts/cache.js'; | import { Cache } from '@/utility/cache.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
|  |  | ||||||
| export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list')); | export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list')); | ||||||
| export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list')); | export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list')); | ||||||
|   | |||||||
| @@ -90,7 +90,7 @@ import MkFolder from '@/components/MkFolder.vue'; | |||||||
| import RouterView from '@/components/global/RouterView.vue'; | import RouterView from '@/components/global/RouterView.vue'; | ||||||
| import { useRouterFactory } from '@/router/supplier'; | import { useRouterFactory } from '@/router/supplier'; | ||||||
| import MkTextarea from '@/components/MkTextarea.vue'; | import MkTextarea from '@/components/MkTextarea.vue'; | ||||||
| import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; | import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	report: Misskey.entities.AdminAbuseUserReportsResponse[number]; | 	report: Misskey.entities.AdminAbuseUserReportsResponse[number]; | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ import * as Misskey from 'misskey-js'; | |||||||
| import MkMention from './MkMention.vue'; | import MkMention from './MkMention.vue'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { host as localHost } from '@@/js/config.js'; | import { host as localHost } from '@@/js/config.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
|  |  | ||||||
| const user = ref<Misskey.entities.UserLite>(); | const user = ref<Misskey.entities.UserLite>(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ import { HttpResponse, http } from 'msw'; | |||||||
| import { userDetailed } from '../../.storybook/fakes.js'; | import { userDetailed } from '../../.storybook/fakes.js'; | ||||||
| import { commonHandlers } from '../../.storybook/mocks.js'; | import { commonHandlers } from '../../.storybook/mocks.js'; | ||||||
| import MkAchievements from './MkAchievements.vue'; | import MkAchievements from './MkAchievements.vue'; | ||||||
| import { ACHIEVEMENT_TYPES } from '@/scripts/achievements.js'; | import { ACHIEVEMENT_TYPES } from '@/utility/achievements.js'; | ||||||
| export const Empty = { | export const Empty = { | ||||||
| 	render(args) { | 	render(args) { | ||||||
| 		return { | 		return { | ||||||
|   | |||||||
| @@ -55,9 +55,9 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import { onMounted, ref, computed } from 'vue'; | import { onMounted, ref, computed } from 'vue'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js'; | import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/utility/achievements.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	user: Misskey.entities.User; | 	user: Misskey.entities.User; | ||||||
|   | |||||||
| @@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { computed, onMounted, onBeforeUnmount, ref } from 'vue'; | import { computed, onMounted, onBeforeUnmount, ref } from 'vue'; | ||||||
| import tinycolor from 'tinycolor2'; | import tinycolor from 'tinycolor2'; | ||||||
| import { globalEvents } from '@/events.js'; | import { globalEvents } from '@/events.js'; | ||||||
| import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js'; | import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js'; | ||||||
|  |  | ||||||
| // https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles | // https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles | ||||||
| const angleDiff = (a: number, b: number) => { | const angleDiff = (a: number, b: number) => { | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { onMounted, shallowRef } from 'vue'; | import { onMounted, shallowRef } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import MkModal from '@/components/MkModal.vue'; | import MkModal from '@/components/MkModal.vue'; | ||||||
| import MkButton from '@/components/MkButton.vue'; | import MkButton from '@/components/MkButton.vue'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
|   | |||||||
| @@ -59,10 +59,10 @@ import MkTextarea from '@/components/MkTextarea.vue'; | |||||||
| import MkSelect from '@/components/MkSelect.vue'; | import MkSelect from '@/components/MkSelect.vue'; | ||||||
| import MkSwitch from '@/components/MkSwitch.vue'; | import MkSwitch from '@/components/MkSwitch.vue'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { deepMerge } from '@/scripts/merge.js'; | import { deepMerge } from '@/utility/merge.js'; | ||||||
| import type { DeepPartial } from '@/scripts/merge.js'; | import type { DeepPartial } from '@/utility/merge.js'; | ||||||
|  |  | ||||||
| type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & { | type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & { | ||||||
| 	id?: string; | 	id?: string; | ||||||
|   | |||||||
| @@ -71,7 +71,7 @@ import MkInput from '@/components/MkInput.vue'; | |||||||
| import MkSwitch from '@/components/MkSwitch.vue'; | import MkSwitch from '@/components/MkSwitch.vue'; | ||||||
| import MkTextarea from '@/components/MkTextarea.vue'; | import MkTextarea from '@/components/MkTextarea.vue'; | ||||||
| import MkSelect from '@/components/MkSelect.vue'; | import MkSelect from '@/components/MkSelect.vue'; | ||||||
| import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/scripts/aiscript/ui.js'; | import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js'; | ||||||
| import MkFolder from '@/components/MkFolder.vue'; | import MkFolder from '@/components/MkFolder.vue'; | ||||||
| import MkPostForm from '@/components/MkPostForm.vue'; | import MkPostForm from '@/components/MkPostForm.vue'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -123,8 +123,8 @@ import MkButton from '@/components/MkButton.vue'; | |||||||
| import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js'; | import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; | import { getProxiedImageUrl } from '@/utility/media-proxy.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	name?: string; | 	name?: string; | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ import { userDetailed } from '../../.storybook/fakes.js'; | |||||||
| import { commonHandlers } from '../../.storybook/mocks.js'; | import { commonHandlers } from '../../.storybook/mocks.js'; | ||||||
| import MkAutocomplete from './MkAutocomplete.vue'; | import MkAutocomplete from './MkAutocomplete.vue'; | ||||||
| import MkInput from './MkInput.vue'; | import MkInput from './MkInput.vue'; | ||||||
| import { tick } from '@/scripts/test-utils.js'; | import { tick } from '@/utility/test-utils.js'; | ||||||
| const common = { | const common = { | ||||||
| 	render(args) { | 	render(args) { | ||||||
| 		return { | 		return { | ||||||
|   | |||||||
| @@ -49,22 +49,23 @@ import sanitizeHtml from 'sanitize-html'; | |||||||
| import { emojilist, getEmojiName } from '@@/js/emojilist.js'; | import { emojilist, getEmojiName } from '@@/js/emojilist.js'; | ||||||
| import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js'; | import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js'; | ||||||
| import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js'; | import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js'; | ||||||
| import type { EmojiDef } from '@/scripts/search-emoji.js'; | import type { EmojiDef } from '@/utility/search-emoji.js'; | ||||||
| import contains from '@/scripts/contains.js'; | import contains from '@/utility/contains.js'; | ||||||
| import { acct } from '@/filters/user.js'; | import { acct } from '@/filters/user.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { store } from '@/store.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { miLocalStorage } from '@/local-storage.js'; | import { miLocalStorage } from '@/local-storage.js'; | ||||||
| import { customEmojis } from '@/custom-emojis.js'; | import { customEmojis } from '@/custom-emojis.js'; | ||||||
| import { searchEmoji } from '@/scripts/search-emoji.js'; | import { searchEmoji } from '@/utility/search-emoji.js'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const lib = emojilist.filter(x => x.category !== 'flags'); | const lib = emojilist.filter(x => x.category !== 'flags'); | ||||||
|  |  | ||||||
| const emojiDb = computed(() => { | const emojiDb = computed(() => { | ||||||
| 	//#region Unicode Emoji | 	//#region Unicode Emoji | ||||||
| 	const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; | 	const char2path = prefer.r.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath; | ||||||
|  |  | ||||||
| 	const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({ | 	const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({ | ||||||
| 		emoji: x.char, | 		emoji: x.char, | ||||||
| @@ -72,7 +73,7 @@ const emojiDb = computed(() => { | |||||||
| 		url: char2path(x.char), | 		url: char2path(x.char), | ||||||
| 	})); | 	})); | ||||||
|  |  | ||||||
| 	for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { | 	for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) { | ||||||
| 		for (const [emoji, keywords] of Object.entries(index)) { | 		for (const [emoji, keywords] of Object.entries(index)) { | ||||||
| 			for (const k of keywords) { | 			for (const k of keywords) { | ||||||
| 				unicodeEmojiDB.push({ | 				unicodeEmojiDB.push({ | ||||||
| @@ -154,10 +155,10 @@ function complete(type: string, value: any) { | |||||||
| 	emit('done', { type, value }); | 	emit('done', { type, value }); | ||||||
| 	emit('closed'); | 	emit('closed'); | ||||||
| 	if (type === 'emoji') { | 	if (type === 'emoji') { | ||||||
| 		let recents = defaultStore.state.recentlyUsedEmojis; | 		let recents = store.s.recentlyUsedEmojis; | ||||||
| 		recents = recents.filter((emoji: any) => emoji !== value); | 		recents = recents.filter((emoji: any) => emoji !== value); | ||||||
| 		recents.unshift(value); | 		recents.unshift(value); | ||||||
| 		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); | 		store.set('recentlyUsedEmojis', recents.splice(0, 32)); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -237,7 +238,7 @@ function exec() { | |||||||
| 	} else if (props.type === 'emoji') { | 	} else if (props.type === 'emoji') { | ||||||
| 		if (!props.q || props.q === '') { | 		if (!props.q || props.q === '') { | ||||||
| 			// 最近使った絵文字をサジェスト | 			// 最近使った絵文字をサジェスト | ||||||
| 			emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[]; | 			emojis.value = store.s.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[]; | ||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { onMounted, ref } from 'vue'; | import { onMounted, ref } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	userIds: string[]; | 	userIds: string[]; | ||||||
|   | |||||||
| @@ -220,28 +220,28 @@ function onMousedown(evt: MouseEvent): void { | |||||||
| 		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); | 		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB)); | ||||||
|  |  | ||||||
| 		&:not(:disabled):hover { | 		&:not(:disabled):hover { | ||||||
| 			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))); | 			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))); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		&:not(:disabled):active { | 		&:not(:disabled):active { | ||||||
| 			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))); | 			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))); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	&.danger { | 	&.danger { | ||||||
| 		font-weight: bold; | 		font-weight: bold; | ||||||
| 		color: #ff2a2a; | 		color: var(--MI_THEME-error); | ||||||
|  |  | ||||||
| 		&.primary { | 		&.primary { | ||||||
| 			color: #fff; | 			color: #fff; | ||||||
| 			background: #ff2a2a; | 			background: var(--MI_THEME-error); | ||||||
|  |  | ||||||
| 			&:not(:disabled):hover { | 			&:not(:disabled):hover { | ||||||
| 				background: #ff4242; | 				background: hsl(from var(--MI_THEME-error) h s calc(l + 10)); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			&:not(:disabled):active { | 			&:not(:disabled):active { | ||||||
| 				background: #d42e2e; | 				background: hsl(from var(--MI_THEME-error) h s calc(l - 10)); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue'; | import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue'; | ||||||
| import { defaultStore } from '@/store.js'; | import { store } from '@/store.js'; | ||||||
|  |  | ||||||
| // APIs provided by Captcha services | // APIs provided by Captcha services | ||||||
| // see: https://docs.hcaptcha.com/configuration/#javascript-api | // see: https://docs.hcaptcha.com/configuration/#javascript-api | ||||||
| @@ -154,7 +154,7 @@ async function requestRender() { | |||||||
|  |  | ||||||
| 		captchaWidgetId.value = captcha.value.render(elem, { | 		captchaWidgetId.value = captcha.value.render(elem, { | ||||||
| 			sitekey: props.sitekey, | 			sitekey: props.sitekey, | ||||||
| 			theme: defaultStore.state.darkMode ? 'dark' : 'light', | 			theme: store.s.darkMode ? 'dark' : 'light', | ||||||
| 			callback: callback, | 			callback: callback, | ||||||
| 			'expired-callback': () => callback(undefined), | 			'expired-callback': () => callback(undefined), | ||||||
| 			'error-callback': () => callback(undefined), | 			'error-callback': () => callback(undefined), | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
|   | |||||||
| @@ -53,15 +53,15 @@ export type ChartSrc = | |||||||
| import { onMounted, ref, shallowRef, watch } from 'vue'; | import { onMounted, ref, shallowRef, watch } from 'vue'; | ||||||
| import { Chart } from 'chart.js'; | import { Chart } from 'chart.js'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import { misskeyApiGet } from '@/scripts/misskey-api.js'; | import { misskeyApiGet } from '@/utility/misskey-api.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { store } from '@/store.js'; | ||||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; | import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; | ||||||
| import { chartVLine } from '@/scripts/chart-vline.js'; | import { chartVLine } from '@/utility/chart-vline.js'; | ||||||
| import { alpha } from '@/scripts/color.js'; | import { alpha } from '@/utility/color.js'; | ||||||
| import date from '@/filters/date.js'; | import date from '@/filters/date.js'; | ||||||
| import bytes from '@/filters/bytes.js'; | import bytes from '@/filters/bytes.js'; | ||||||
| import { initChart } from '@/scripts/init-chart.js'; | import { initChart } from '@/utility/init-chart.js'; | ||||||
| import { chartLegend } from '@/scripts/chart-legend.js'; | import { chartLegend } from '@/utility/chart-legend.js'; | ||||||
| import MkChartLegend from '@/components/MkChartLegend.vue'; | import MkChartLegend from '@/components/MkChartLegend.vue'; | ||||||
|  |  | ||||||
| initChart(); | initChart(); | ||||||
| @@ -161,7 +161,7 @@ const render = () => { | |||||||
| 		chartInstance.destroy(); | 		chartInstance.destroy(); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; | 	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; | ||||||
|  |  | ||||||
| 	const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y))); | 	const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y))); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,9 +23,9 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'; | |||||||
| import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; | import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { useInterval } from '@@/js/use-interval.js'; | import { useInterval } from '@@/js/use-interval.js'; | ||||||
| import * as game from '@/scripts/clicker-game.js'; | import * as game from '@/utility/clicker-game.js'; | ||||||
| import number from '@/filters/number.js'; | import number from '@/filters/number.js'; | ||||||
| import { claimAchievement } from '@/scripts/achievements.js'; | import { claimAchievement } from '@/utility/achievements.js'; | ||||||
|  |  | ||||||
| const saveData = game.saveData; | const saveData = game.saveData; | ||||||
| const cookies = computed(() => saveData.value?.cookies); | const cookies = computed(() => saveData.value?.cookies); | ||||||
|   | |||||||
| @@ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { computed, ref, watch } from 'vue'; | import { computed, ref, watch } from 'vue'; | ||||||
| import { bundledLanguagesInfo } from 'shiki/langs'; | import { bundledLanguagesInfo } from 'shiki/langs'; | ||||||
| import type { BundledLanguage } from 'shiki/langs'; | import type { BundledLanguage } from 'shiki/langs'; | ||||||
| import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js'; | import { getHighlighter, getTheme } from '@/utility/code-highlighter.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { store } from '@/store.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	code: string; | 	code: string; | ||||||
| @@ -22,7 +22,7 @@ const props = defineProps<{ | |||||||
| }>(); | }>(); | ||||||
|  |  | ||||||
| const highlighter = await getHighlighter(); | const highlighter = await getHighlighter(); | ||||||
| const darkMode = defaultStore.reactiveState.darkMode; | const darkMode = store.r.darkMode; | ||||||
| const codeLang = ref<BundledLanguage | 'aiscript'>('js'); | const codeLang = ref<BundledLanguage | 'aiscript'>('js'); | ||||||
|  |  | ||||||
| const [lightThemeName, darkThemeName] = await Promise.all([ | const [lightThemeName, darkThemeName] = await Promise.all([ | ||||||
| @@ -74,10 +74,8 @@ watch(() => props.lang, (to) => { | |||||||
| <style module lang="scss"> | <style module lang="scss"> | ||||||
| .codeBlockRoot :global(.shiki) { | .codeBlockRoot :global(.shiki) { | ||||||
| 	padding: 1em; | 	padding: 1em; | ||||||
| 	margin: .5em 0; | 	margin: 0; | ||||||
| 	overflow: auto; | 	overflow: auto; | ||||||
| 	border-radius: 8px; |  | ||||||
| 	border: 1px solid var(--MI_THEME-divider); |  | ||||||
| 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; | 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; | ||||||
|  |  | ||||||
| 	color: var(--shiki-fallback); | 	color: var(--shiki-fallback); | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| 	</button> | 	</button> | ||||||
| 	<Suspense> | 	<Suspense> | ||||||
| 		<template #fallback> | 		<template #fallback> | ||||||
| 			<MkLoading /> | 			<MkLoading/> | ||||||
| 		</template> | 		</template> | ||||||
| 		<XCode v-if="show && lang" :code="code" :lang="lang"/> | 		<XCode v-if="show && lang" :code="code" :lang="lang"/> | ||||||
| 		<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre> | 		<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre> | ||||||
| @@ -28,9 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { defineAsyncComponent, ref } from 'vue'; | import { defineAsyncComponent, ref } from 'vue'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import MkLoading from '@/components/global/MkLoading.vue'; | import MkLoading from '@/components/global/MkLoading.vue'; | ||||||
| import { defaultStore } from '@/store.js'; |  | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; | import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	code: string; | 	code: string; | ||||||
| @@ -42,7 +42,7 @@ const props = withDefaults(defineProps<{ | |||||||
| 	forceShow: false, | 	forceShow: false, | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code); | const show = ref(props.forceShow === true ? true : !prefer.s.dataSaver.code); | ||||||
|  |  | ||||||
| const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue')); | const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue')); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| 		</div> | 		</div> | ||||||
| 	</header> | 	</header> | ||||||
| 	<Transition | 	<Transition | ||||||
| 		:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" | 		:enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''" | ||||||
| 		:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" | 		:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''" | ||||||
| 		:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''" | 		:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''" | ||||||
| 		:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''" | 		:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''" | ||||||
| 		@enter="enter" | 		@enter="enter" | ||||||
| 		@afterEnter="afterEnter" | 		@afterEnter="afterEnter" | ||||||
| 		@leave="leave" | 		@leave="leave" | ||||||
| @@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'; | import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'; | ||||||
| import { defaultStore } from '@/store.js'; | import { prefer } from '@/preferences.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
|   | |||||||
| @@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| <template> | <template> | ||||||
| <Transition | <Transition | ||||||
| 	appear | 	appear | ||||||
| 	:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''" | 	:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''" | ||||||
| 	:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''" | 	:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''" | ||||||
| 	:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''" | 	:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''" | ||||||
| 	:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" | 	:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''" | ||||||
| > | > | ||||||
| 	<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> | 	<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> | ||||||
| 		<MkMenu :items="items" :align="'left'" @close="emit('closed')"/> | 		<MkMenu :items="items" :align="'left'" @close="emit('closed')"/> | ||||||
| @@ -21,8 +21,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue'; | import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue'; | ||||||
| import MkMenu from './MkMenu.vue'; | import MkMenu from './MkMenu.vue'; | ||||||
| import type { MenuItem } from '@/types/menu.js'; | import type { MenuItem } from '@/types/menu.js'; | ||||||
| import contains from '@/scripts/contains.js'; | import contains from '@/utility/contains.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { prefer } from '@/preferences.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
|   | |||||||
| @@ -35,13 +35,13 @@ import { onMounted, shallowRef, ref } from 'vue'; | |||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import Cropper from 'cropperjs'; | import Cropper from 'cropperjs'; | ||||||
| import tinycolor from 'tinycolor2'; | import tinycolor from 'tinycolor2'; | ||||||
|  | import { apiUrl } from '@@/js/config.js'; | ||||||
| import MkModalWindow from '@/components/MkModalWindow.vue'; | import MkModalWindow from '@/components/MkModalWindow.vue'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { $i } from '@/account.js'; | import { $i } from '@/account.js'; | ||||||
| import { defaultStore } from '@/store.js'; |  | ||||||
| import { apiUrl } from '@@/js/config.js'; |  | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; | import { getProxiedImageUrl } from '@/utility/media-proxy.js'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const emit = defineEmits<{ | const emit = defineEmits<{ | ||||||
| 	(ev: 'ok', cropped: Misskey.entities.DriveFile): void; | 	(ev: 'ok', cropped: Misskey.entities.DriveFile): void; | ||||||
| @@ -81,8 +81,8 @@ const ok = async () => { | |||||||
| 			formData.append('i', $i!.token); | 			formData.append('i', $i!.token); | ||||||
| 			if (props.uploadFolder) { | 			if (props.uploadFolder) { | ||||||
| 				formData.append('folderId', props.uploadFolder); | 				formData.append('folderId', props.uploadFolder); | ||||||
| 			} else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) { | 			} else if (props.uploadFolder !== null && prefer.s.uploadFolder) { | ||||||
| 				formData.append('folderId', defaultStore.state.uploadFolder); | 				formData.append('folderId', prefer.s.uploadFolder); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			window.fetch(apiUrl + '/drive/files/create', { | 			window.fetch(apiUrl + '/drive/files/create', { | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; | import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; | ||||||
| import { concat } from '@/scripts/array.js'; | import { concat } from '@/utility/array.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import MkButton from '@/components/MkButton.vue'; | import MkButton from '@/components/MkButton.vue'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, h, TransitionGroup, useCssModule } from 'vue'; | import { defineComponent, h, TransitionGroup, useCssModule } from 'vue'; | ||||||
| import type { PropType } from 'vue'; | import type { PropType } from 'vue'; | ||||||
|  | import type { MisskeyEntity } from '@/types/date-separated-list.js'; | ||||||
| import MkAd from '@/components/global/MkAd.vue'; | import MkAd from '@/components/global/MkAd.vue'; | ||||||
| import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js'; | import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { instance } from '@/instance.js'; | import { instance } from '@/instance.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { prefer } from '@/preferences.js'; | ||||||
| import type { MisskeyEntity } from '@/types/date-separated-list.js'; |  | ||||||
|  |  | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| @@ -150,7 +150,7 @@ export default defineComponent({ | |||||||
| 			[$style['direction-up']]: props.direction === 'up', | 			[$style['direction-up']]: props.direction === 'up', | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
| 		return () => defaultStore.state.animation ? h(TransitionGroup, { | 		return () => prefer.s.animation ? h(TransitionGroup, { | ||||||
| 			class: classes, | 			class: classes, | ||||||
| 			name: 'list', | 			name: 'list', | ||||||
| 			tag: 'div', | 			tag: 'div', | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { onMounted, onUnmounted, ref, watch } from 'vue'; | import { onMounted, onUnmounted, ref, watch } from 'vue'; | ||||||
| import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js'; | import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	showS?: boolean; | 	showS?: boolean; | ||||||
|   | |||||||
| @@ -45,8 +45,8 @@ import bytes from '@/filters/bytes.js'; | |||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { $i } from '@/account.js'; | import { $i } from '@/account.js'; | ||||||
| import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js'; | import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js'; | ||||||
| import { deviceKind } from '@/scripts/device-kind.js'; | import { deviceKind } from '@/utility/device-kind.js'; | ||||||
| import { useRouter } from '@/router/supplier.js'; | import { useRouter } from '@/router/supplier.js'; | ||||||
|  |  | ||||||
| const router = useRouter(); | const router = useRouter(); | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| 		<template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template> | 		<template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template> | ||||||
| 		{{ folder.name }} | 		{{ folder.name }} | ||||||
| 	</p> | 	</p> | ||||||
| 	<p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload"> | 	<p v-if="prefer.s.uploadFolder == folder.id" :class="$style.upload"> | ||||||
| 		{{ i18n.ts.uploadFolder }} | 		{{ i18n.ts.uploadFolder }} | ||||||
| 	</p> | 	</p> | ||||||
| 	<button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked"> | 	<button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked"> | ||||||
| @@ -38,11 +38,11 @@ import { computed, defineAsyncComponent, ref } from 'vue'; | |||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import type { MenuItem } from '@/types/menu.js'; | import type { MenuItem } from '@/types/menu.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { claimAchievement } from '@/utility/achievements.js'; | ||||||
| import { claimAchievement } from '@/scripts/achievements.js'; | import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; | ||||||
| import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	folder: Misskey.entities.DriveFolder; | 	folder: Misskey.entities.DriveFolder; | ||||||
| @@ -244,8 +244,8 @@ function deleteFolder() { | |||||||
| 	misskeyApi('drive/folders/delete', { | 	misskeyApi('drive/folders/delete', { | ||||||
| 		folderId: props.folder.id, | 		folderId: props.folder.id, | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
| 		if (defaultStore.state.uploadFolder === props.folder.id) { | 		if (prefer.s.uploadFolder === props.folder.id) { | ||||||
| 			defaultStore.set('uploadFolder', null); | 			prefer.commit('uploadFolder', null); | ||||||
| 		} | 		} | ||||||
| 	}).catch(err => { | 	}).catch(err => { | ||||||
| 		switch (err.id) { | 		switch (err.id) { | ||||||
| @@ -266,7 +266,7 @@ function deleteFolder() { | |||||||
| } | } | ||||||
|  |  | ||||||
| function setAsUploadFolder() { | function setAsUploadFolder() { | ||||||
| 	defaultStore.set('uploadFolder', props.folder.id); | 	prefer.commit('uploadFolder', props.folder.id); | ||||||
| } | } | ||||||
|  |  | ||||||
| function onContextmenu(ev: MouseEvent) { | function onContextmenu(ev: MouseEvent) { | ||||||
| @@ -295,7 +295,7 @@ function onContextmenu(ev: MouseEvent) { | |||||||
| 		danger: true, | 		danger: true, | ||||||
| 		action: deleteFolder, | 		action: deleteFolder, | ||||||
| 	}]; | 	}]; | ||||||
| 	if (defaultStore.state.devMode) { | 	if (prefer.s.devMode) { | ||||||
| 		menu = menu.concat([{ type: 'divider' }, { | 		menu = menu.concat([{ type: 'divider' }, { | ||||||
| 			icon: 'ti ti-id', | 			icon: 'ti ti-id', | ||||||
| 			text: i18n.ts.copyFolderId, | 			text: i18n.ts.copyFolderId, | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
|   | |||||||
| @@ -104,12 +104,12 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue'; | |||||||
| import XFolder from '@/components/MkDrive.folder.vue'; | import XFolder from '@/components/MkDrive.folder.vue'; | ||||||
| import XFile from '@/components/MkDrive.file.vue'; | import XFile from '@/components/MkDrive.file.vue'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { useStream } from '@/stream.js'; | import { useStream } from '@/stream.js'; | ||||||
| import { defaultStore } from '@/store.js'; |  | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { uploadFile, uploads } from '@/scripts/upload.js'; | import { uploadFile, uploads } from '@/utility/upload.js'; | ||||||
| import { claimAchievement } from '@/scripts/achievements.js'; | import { claimAchievement } from '@/utility/achievements.js'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	initialFolder?: Misskey.entities.DriveFolder; | 	initialFolder?: Misskey.entities.DriveFolder; | ||||||
| @@ -142,7 +142,7 @@ const selectedFiles = ref<Misskey.entities.DriveFile[]>([]); | |||||||
| const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]); | const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]); | ||||||
| const uploadings = uploads; | const uploadings = uploads; | ||||||
| const connection = useStream().useChannel('drive'); | const connection = useStream().useChannel('drive'); | ||||||
| const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい | const keepOriginal = ref<boolean>(prefer.s.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい | ||||||
|  |  | ||||||
| // ドロップされようとしているか | // ドロップされようとしているか | ||||||
| const draghover = ref(false); | const draghover = ref(false); | ||||||
| @@ -716,7 +716,7 @@ function onContextmenu(ev: MouseEvent) { | |||||||
| } | } | ||||||
|  |  | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
| 	if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) { | 	if (prefer.s.enableInfiniteScroll && loadMoreFiles.value) { | ||||||
| 		nextTick(() => { | 		nextTick(() => { | ||||||
| 			ilFilesObserver.observe(loadMoreFiles.value?.$el); | 			ilFilesObserver.observe(loadMoreFiles.value?.$el); | ||||||
| 		}); | 		}); | ||||||
| @@ -737,7 +737,7 @@ onMounted(() => { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| onActivated(() => { | onActivated(() => { | ||||||
| 	if (defaultStore.state.enableInfiniteScroll) { | 	if (prefer.s.enableInfiniteScroll) { | ||||||
| 		nextTick(() => { | 		nextTick(() => { | ||||||
| 			ilFilesObserver.observe(loadMoreFiles.value?.$el); | 			ilFilesObserver.observe(loadMoreFiles.value?.$el); | ||||||
| 		}); | 		}); | ||||||
|   | |||||||
| @@ -105,8 +105,8 @@ import MkInfo from '@/components/MkInfo.vue'; | |||||||
|  |  | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; | import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; | ||||||
| import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js'; | import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js'; | ||||||
|  |  | ||||||
| const emit = defineEmits<{ | const emit = defineEmits<{ | ||||||
| 	(ev: 'ok'): void; | 	(ev: 'ok'): void; | ||||||
|   | |||||||
| @@ -131,13 +131,14 @@ import type { | |||||||
| import XSection from '@/components/MkEmojiPicker.section.vue'; | import XSection from '@/components/MkEmojiPicker.section.vue'; | ||||||
| import MkRippleEffect from '@/components/MkRippleEffect.vue'; | import MkRippleEffect from '@/components/MkRippleEffect.vue'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { isTouchUsing } from '@/scripts/touch.js'; | import { isTouchUsing } from '@/utility/touch.js'; | ||||||
| import { deviceKind } from '@/scripts/device-kind.js'; | import { deviceKind } from '@/utility/device-kind.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { store } from '@/store.js'; | ||||||
| import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js'; | import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js'; | ||||||
| import { $i } from '@/account.js'; | import { $i } from '@/account.js'; | ||||||
| import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; | import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	showPinned?: boolean; | 	showPinned?: boolean; | ||||||
| @@ -163,8 +164,9 @@ const { | |||||||
| 	emojiPickerScale, | 	emojiPickerScale, | ||||||
| 	emojiPickerWidth, | 	emojiPickerWidth, | ||||||
| 	emojiPickerHeight, | 	emojiPickerHeight, | ||||||
| 	recentlyUsedEmojis, | } = prefer.r; | ||||||
| } = defaultStore.reactiveState; |  | ||||||
|  | const recentlyUsedEmojis = store.r.recentlyUsedEmojis; | ||||||
|  |  | ||||||
| const recentlyUsedEmojisDef = computed(() => { | const recentlyUsedEmojisDef = computed(() => { | ||||||
| 	return recentlyUsedEmojis.value.map(getDef); | 	return recentlyUsedEmojis.value.map(getDef); | ||||||
| @@ -317,7 +319,7 @@ watch(q, () => { | |||||||
| 			} | 			} | ||||||
| 			if (matches.size >= max) return matches; | 			if (matches.size >= max) return matches; | ||||||
|  |  | ||||||
| 			for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { | 			for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) { | ||||||
| 				for (const emoji of emojis) { | 				for (const emoji of emojis) { | ||||||
| 					if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) { | 					if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) { | ||||||
| 						matches.add(emoji); | 						matches.add(emoji); | ||||||
| @@ -334,7 +336,7 @@ watch(q, () => { | |||||||
| 			} | 			} | ||||||
| 			if (matches.size >= max) return matches; | 			if (matches.size >= max) return matches; | ||||||
|  |  | ||||||
| 			for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { | 			for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) { | ||||||
| 				for (const emoji of emojis) { | 				for (const emoji of emojis) { | ||||||
| 					if (index[emoji.char].some(k => k.startsWith(newQ))) { | 					if (index[emoji.char].some(k => k.startsWith(newQ))) { | ||||||
| 						matches.add(emoji); | 						matches.add(emoji); | ||||||
| @@ -351,7 +353,7 @@ watch(q, () => { | |||||||
| 			} | 			} | ||||||
| 			if (matches.size >= max) return matches; | 			if (matches.size >= max) return matches; | ||||||
|  |  | ||||||
| 			for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) { | 			for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) { | ||||||
| 				for (const emoji of emojis) { | 				for (const emoji of emojis) { | ||||||
| 					if (index[emoji.char].some(k => k.includes(newQ))) { | 					if (index[emoji.char].some(k => k.includes(newQ))) { | ||||||
| 						matches.add(emoji); | 						matches.add(emoji); | ||||||
| @@ -413,7 +415,7 @@ function computeButtonTitle(ev: MouseEvent): void { | |||||||
|  |  | ||||||
| function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) { | function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) { | ||||||
| 	const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; | 	const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; | ||||||
| 	if (el && defaultStore.state.animation) { | 	if (el && prefer.s.animation) { | ||||||
| 		const rect = el.getBoundingClientRect(); | 		const rect = el.getBoundingClientRect(); | ||||||
| 		const x = rect.left + (el.offsetWidth / 2); | 		const x = rect.left + (el.offsetWidth / 2); | ||||||
| 		const y = rect.top + (el.offsetHeight / 2); | 		const y = rect.top + (el.offsetHeight / 2); | ||||||
| @@ -427,10 +429,10 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, | |||||||
|  |  | ||||||
| 	// 最近使った絵文字更新 | 	// 最近使った絵文字更新 | ||||||
| 	if (!pinned.value?.includes(key)) { | 	if (!pinned.value?.includes(key)) { | ||||||
| 		let recents = defaultStore.state.recentlyUsedEmojis; | 		let recents = store.s.recentlyUsedEmojis; | ||||||
| 		recents = recents.filter((emoji) => emoji !== key); | 		recents = recents.filter((emoji) => emoji !== key); | ||||||
| 		recents.unshift(key); | 		recents.unshift(key); | ||||||
| 		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); | 		store.set('recentlyUsedEmojis', recents.splice(0, 32)); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| 	ref="modal" | 	ref="modal" | ||||||
| 	v-slot="{ type, maxHeight }" | 	v-slot="{ type, maxHeight }" | ||||||
| 	:zPriority="'middle'" | 	:zPriority="'middle'" | ||||||
| 	:preferType="defaultStore.state.emojiPickerStyle" | 	:preferType="prefer.s.emojiPickerStyle" | ||||||
| 	:hasInteractionWithOtherFocusTrappedEls="true" | 	:hasInteractionWithOtherFocusTrappedEls="true" | ||||||
| 	:transparentBg="true" | 	:transparentBg="true" | ||||||
| 	:manualShowing="manualShowing" | 	:manualShowing="manualShowing" | ||||||
| @@ -40,7 +40,7 @@ import * as Misskey from 'misskey-js'; | |||||||
| import { shallowRef } from 'vue'; | import { shallowRef } from 'vue'; | ||||||
| import MkModal from '@/components/MkModal.vue'; | import MkModal from '@/components/MkModal.vue'; | ||||||
| import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; | import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; | ||||||
| import { defaultStore } from '@/store.js'; | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	manualShowing?: boolean | null; | 	manualShowing?: boolean | null; | ||||||
|   | |||||||
							
								
								
									
										43
									
								
								packages/frontend/src/components/MkFeatureBanner.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,43 @@ | |||||||
|  | <!-- | ||||||
|  | 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> | ||||||
| @@ -14,10 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| 		</button> | 		</button> | ||||||
| 	</header> | 	</header> | ||||||
| 	<Transition | 	<Transition | ||||||
| 		:enterActiveClass="defaultStore.state.animation ? $style.folderToggleEnterActive : ''" | 		:enterActiveClass="prefer.s.animation ? $style.folderToggleEnterActive : ''" | ||||||
| 		:leaveActiveClass="defaultStore.state.animation ? $style.folderToggleLeaveActive : ''" | 		:leaveActiveClass="prefer.s.animation ? $style.folderToggleLeaveActive : ''" | ||||||
| 		:enterFromClass="defaultStore.state.animation ? $style.folderToggleEnterFrom : ''" | 		:enterFromClass="prefer.s.animation ? $style.folderToggleEnterFrom : ''" | ||||||
| 		:leaveToClass="defaultStore.state.animation ? $style.folderToggleLeaveTo : ''" | 		:leaveToClass="prefer.s.animation ? $style.folderToggleLeaveTo : ''" | ||||||
| 		@enter="enter" | 		@enter="enter" | ||||||
| 		@afterEnter="afterEnter" | 		@afterEnter="afterEnter" | ||||||
| 		@leave="leave" | 		@leave="leave" | ||||||
| @@ -33,8 +33,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { onMounted, ref, shallowRef, watch } from 'vue'; | import { onMounted, ref, shallowRef, watch } from 'vue'; | ||||||
| import { miLocalStorage } from '@/local-storage.js'; | import { miLocalStorage } from '@/local-storage.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { prefer } from '@/preferences.js'; | ||||||
| import { getBgColor } from '@/scripts/get-bg-color.js'; | import { getBgColor } from '@/utility/get-bg-color.js'; | ||||||
|  |  | ||||||
| const miLocalStoragePrefix = 'ui:folder:' as const; | const miLocalStoragePrefix = 'ui:folder:' as const; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,10 +27,10 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
|  |  | ||||||
| 		<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened"> | 		<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened"> | ||||||
| 			<Transition | 			<Transition | ||||||
| 				:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" | 				:enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''" | ||||||
| 				:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" | 				:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''" | ||||||
| 				:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''" | 				:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''" | ||||||
| 				:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''" | 				:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''" | ||||||
| 				@enter="enter" | 				@enter="enter" | ||||||
| 				@afterEnter="afterEnter" | 				@afterEnter="afterEnter" | ||||||
| 				@leave="leave" | 				@leave="leave" | ||||||
| @@ -57,8 +57,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { nextTick, onMounted, ref, shallowRef } from 'vue'; | import { nextTick, onMounted, ref, shallowRef } from 'vue'; | ||||||
| import { defaultStore } from '@/store.js'; | import { prefer } from '@/preferences.js'; | ||||||
| import { getBgColor } from '@/scripts/get-bg-color.js'; | import { getBgColor } from '@/utility/get-bg-color.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	defaultOpen?: boolean; | 	defaultOpen?: boolean; | ||||||
|   | |||||||
| @@ -39,13 +39,14 @@ import { onBeforeUnmount, onMounted, ref } from 'vue'; | |||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import { host } from '@@/js/config.js'; | import { host } from '@@/js/config.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { useStream } from '@/stream.js'; | import { useStream } from '@/stream.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { claimAchievement } from '@/scripts/achievements.js'; | import { claimAchievement } from '@/utility/achievements.js'; | ||||||
| import { pleaseLogin } from '@/scripts/please-login.js'; | import { pleaseLogin } from '@/utility/please-login.js'; | ||||||
| import { $i } from '@/account.js'; | import { $i } from '@/account.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { store } from '@/store.js'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	user: Misskey.entities.UserDetailed, | 	user: Misskey.entities.UserDetailed, | ||||||
| @@ -100,7 +101,7 @@ async function onClick() { | |||||||
| 				userId: props.user.id, | 				userId: props.user.id, | ||||||
| 			}); | 			}); | ||||||
| 		} else { | 		} else { | ||||||
| 			if (defaultStore.state.alwaysConfirmFollow) { | 			if (prefer.s.alwaysConfirmFollow) { | ||||||
| 				const { canceled } = await os.confirm({ | 				const { canceled } = await os.confirm({ | ||||||
| 					type: 'question', | 					type: 'question', | ||||||
| 					text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }), | 					text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }), | ||||||
| @@ -120,11 +121,11 @@ async function onClick() { | |||||||
| 			} else { | 			} else { | ||||||
| 				await misskeyApi('following/create', { | 				await misskeyApi('following/create', { | ||||||
| 					userId: props.user.id, | 					userId: props.user.id, | ||||||
| 					withReplies: defaultStore.state.defaultWithReplies, | 					withReplies: store.s.defaultWithReplies, | ||||||
| 				}); | 				}); | ||||||
| 				emit('update:user', { | 				emit('update:user', { | ||||||
| 					...props.user, | 					...props.user, | ||||||
| 					withReplies: defaultStore.state.defaultWithReplies, | 					withReplies: store.s.defaultWithReplies, | ||||||
| 				}); | 				}); | ||||||
| 				hasPendingFollowRequestFromYou.value = true; | 				hasPendingFollowRequestFromYou.value = true; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,8 +15,8 @@ import * as Misskey from 'misskey-js'; | |||||||
| import { computed, ref } from 'vue'; | import { computed, ref } from 'vue'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import MkButton from '@/components/MkButton.vue'; | import MkButton from '@/components/MkButton.vue'; | ||||||
| import { selectFile } from '@/scripts/select-file.js'; | import { selectFile } from '@/utility/select-file.js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	fileId?: string | null; | 	fileId?: string | null; | ||||||
|   | |||||||
| @@ -80,7 +80,7 @@ import MkRange from './MkRange.vue'; | |||||||
| import MkButton from './MkButton.vue'; | import MkButton from './MkButton.vue'; | ||||||
| import MkRadios from './MkRadios.vue'; | import MkRadios from './MkRadios.vue'; | ||||||
| import XFile from './MkFormDialog.file.vue'; | import XFile from './MkFormDialog.file.vue'; | ||||||
| import type { Form } from '@/scripts/form.js'; | import type { Form } from '@/utility/form.js'; | ||||||
| import MkModalWindow from '@/components/MkModalWindow.vue'; | import MkModalWindow from '@/components/MkModalWindow.vue'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { infoImageUrl } from '@/instance.js'; | import { infoImageUrl } from '@/instance.js'; | ||||||
|   | |||||||
| @@ -35,14 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import { computed, ref } from 'vue'; | import { computed, ref } from 'vue'; | ||||||
| import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; | import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; | ||||||
| import { defaultStore } from '@/store.js'; | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	post: Misskey.entities.GalleryPost; | 	post: Misskey.entities.GalleryPost; | ||||||
| }>(); | }>(); | ||||||
|  |  | ||||||
| const hover = ref(false); | const hover = ref(false); | ||||||
| const safe = computed(() => defaultStore.state.nsfw === 'ignore' || defaultStore.state.nsfw === 'respect' && !props.post.isSensitive); | const safe = computed(() => prefer.s.nsfw === 'ignore' || prefer.s.nsfw === 'respect' && !props.post.isSensitive); | ||||||
| const show = computed(() => safe.value || hover.value); | const show = computed(() => safe.value || hover.value); | ||||||
|  |  | ||||||
| function enterHover(): void { | function enterHover(): void { | ||||||
|   | |||||||
| @@ -16,11 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { onMounted, nextTick, watch, shallowRef, ref } from 'vue'; | import { onMounted, nextTick, watch, shallowRef, ref } from 'vue'; | ||||||
| import { Chart } from 'chart.js'; | import { Chart } from 'chart.js'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | import { misskeyApi } from '@/utility/misskey-api.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { store } from '@/store.js'; | ||||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; | import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; | ||||||
| import { alpha } from '@/scripts/color.js'; | import { alpha } from '@/utility/color.js'; | ||||||
| import { initChart } from '@/scripts/init-chart.js'; | import { initChart } from '@/utility/init-chart.js'; | ||||||
|  |  | ||||||
| initChart(); | initChart(); | ||||||
|  |  | ||||||
| @@ -106,7 +106,7 @@ async function renderChart() { | |||||||
|  |  | ||||||
| 	await nextTick(); | 	await nextTick(); | ||||||
|  |  | ||||||
| 	const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300'; | 	const color = store.s.darkMode ? '#b4e900' : '#86b300'; | ||||||
|  |  | ||||||
| 	// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする | 	// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする | ||||||
| 	const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3; | 	const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3; | ||||||
|   | |||||||
| @@ -28,12 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { ref, shallowRef, computed, nextTick, watch } from 'vue'; | import { ref, shallowRef, computed, nextTick, watch } from 'vue'; | ||||||
| import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; | import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; | ||||||
| import { defaultStore } from '@/store.js'; | import { isHorizontalSwipeSwiping as isSwiping } from '@/utility/touch.js'; | ||||||
| import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js'; | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const rootEl = shallowRef<HTMLDivElement>(); | const rootEl = shallowRef<HTMLDivElement>(); | ||||||
|  |  | ||||||
| // eslint-disable-next-line no-undef |  | ||||||
| const tabModel = defineModel<string>('tab'); | const tabModel = defineModel<string>('tab'); | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| @@ -44,7 +43,7 @@ const emit = defineEmits<{ | |||||||
| 	(ev: 'swiped', newKey: string, direction: 'left' | 'right'): void; | 	(ev: 'swiped', newKey: string, direction: 'left' | 'right'): void; | ||||||
| }>(); | }>(); | ||||||
|  |  | ||||||
| const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontalSwipe.value || defaultStore.reactiveState.animation.value); | const shouldAnimate = computed(() => prefer.r.enableHorizontalSwipe.value || prefer.r.animation.value); | ||||||
|  |  | ||||||
| // ▼ しきい値 ▼ // | // ▼ しきい値 ▼ // | ||||||
|  |  | ||||||
| @@ -72,7 +71,7 @@ const isSwipingForClass = ref(false); | |||||||
| let swipeAborted = false; | let swipeAborted = false; | ||||||
|  |  | ||||||
| function touchStart(event: TouchEvent) { | function touchStart(event: TouchEvent) { | ||||||
| 	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; | 	if (!prefer.r.enableHorizontalSwipe.value) return; | ||||||
|  |  | ||||||
| 	if (event.touches.length !== 1) return; | 	if (event.touches.length !== 1) return; | ||||||
|  |  | ||||||
| @@ -83,7 +82,7 @@ function touchStart(event: TouchEvent) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function touchMove(event: TouchEvent) { | function touchMove(event: TouchEvent) { | ||||||
| 	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; | 	if (!prefer.r.enableHorizontalSwipe.value) return; | ||||||
|  |  | ||||||
| 	if (event.touches.length !== 1) return; | 	if (event.touches.length !== 1) return; | ||||||
|  |  | ||||||
| @@ -134,7 +133,7 @@ function touchEnd(event: TouchEvent) { | |||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return; | 	if (!prefer.r.enableHorizontalSwipe.value) return; | ||||||
|  |  | ||||||
| 	if (event.touches.length !== 0) return; | 	if (event.touches.length !== 0) return; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| <template> | <template> | ||||||
| <div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''"> | <div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''"> | ||||||
| 	<TransitionGroup | 	<TransitionGroup | ||||||
| 		:duration="defaultStore.state.animation && props.transition?.duration || undefined" | 		:duration="prefer.s.animation && props.transition?.duration || undefined" | ||||||
| 		:enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined" | 		:enterActiveClass="prefer.s.animation && props.transition?.enterActiveClass || undefined" | ||||||
| 		:leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined" | 		:leaveActiveClass="prefer.s.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined" | ||||||
| 		:enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined" | 		:enterFromClass="prefer.s.animation && props.transition?.enterFromClass || undefined" | ||||||
| 		:leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined" | 		:leaveToClass="prefer.s.animation && props.transition?.leaveToClass || undefined" | ||||||
| 		:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined" | 		:enterToClass="prefer.s.animation && props.transition?.enterToClass || undefined" | ||||||
| 		:leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined" | 		:leaveFromClass="prefer.s.animation && props.transition?.leaveFromClass || undefined" | ||||||
| 	> | 	> | ||||||
| 		<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/> | 		<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/> | ||||||
| 		<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/> | 		<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/> | ||||||
| @@ -60,7 +60,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol | |||||||
| import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue'; | import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| import { render } from 'buraha'; | import { render } from 'buraha'; | ||||||
| import { defaultStore } from '@/store.js'; | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	transition?: { | 	transition?: { | ||||||
|   | |||||||
| @@ -50,8 +50,8 @@ import { debounce } from 'throttle-debounce'; | |||||||
| import MkButton from '@/components/MkButton.vue'; | import MkButton from '@/components/MkButton.vue'; | ||||||
| import { useInterval } from '@@/js/use-interval.js'; | import { useInterval } from '@@/js/use-interval.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { Autocomplete } from '@/scripts/autocomplete.js'; | import { Autocomplete } from '@/utility/autocomplete.js'; | ||||||
| import type { SuggestionType } from '@/scripts/autocomplete.js'; | import type { SuggestionType } from '@/utility/autocomplete.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	modelValue: string | number | null; | 	modelValue: string | number | null; | ||||||
|   | |||||||
| @@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import MkMiniChart from '@/components/MkMiniChart.vue'; | import MkMiniChart from '@/components/MkMiniChart.vue'; | ||||||
| import { misskeyApiGet } from '@/scripts/misskey-api.js'; | import { misskeyApiGet } from '@/utility/misskey-api.js'; | ||||||
| import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; | import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	instance: Misskey.entities.FederationInstance; | 	instance: Misskey.entities.FederationInstance; | ||||||
|   | |||||||
| @@ -88,10 +88,10 @@ import { onMounted, ref, computed, shallowRef } from 'vue'; | |||||||
| import { Chart } from 'chart.js'; | import { Chart } from 'chart.js'; | ||||||
| import MkSelect from '@/components/MkSelect.vue'; | import MkSelect from '@/components/MkSelect.vue'; | ||||||
| import MkChart from '@/components/MkChart.vue'; | import MkChart from '@/components/MkChart.vue'; | ||||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; | import { useChartTooltip } from '@/utility/use-chart-tooltip.js'; | ||||||
| import { $i } from '@/account.js'; | import { $i } from '@/account.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { misskeyApiGet } from '@/scripts/misskey-api.js'; | import { misskeyApiGet } from '@/utility/misskey-api.js'; | ||||||
| import { instance } from '@/instance.js'; | import { instance } from '@/instance.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import MkHeatmap from '@/components/MkHeatmap.vue'; | import MkHeatmap from '@/components/MkHeatmap.vue'; | ||||||
| @@ -99,7 +99,7 @@ import type { HeatmapSource } from '@/components/MkHeatmap.vue'; | |||||||
| import MkFoldableSection from '@/components/MkFoldableSection.vue'; | import MkFoldableSection from '@/components/MkFoldableSection.vue'; | ||||||
| import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; | import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; | ||||||
| import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue'; | import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue'; | ||||||
| import { initChart } from '@/scripts/init-chart.js'; | import { initChart } from '@/utility/init-chart.js'; | ||||||
|  |  | ||||||
| initChart(); | initChart(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ import { computed } from 'vue'; | |||||||
| import type { CSSProperties } from 'vue'; | import type { CSSProperties } from 'vue'; | ||||||
| import { instanceName as localInstanceName } from '@@/js/config.js'; | import { instanceName as localInstanceName } from '@@/js/config.js'; | ||||||
| import { instance as localInstance } from '@/instance.js'; | import { instance as localInstance } from '@/instance.js'; | ||||||
| import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; | import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	host: string | null; | 	host: string | null; | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ import { computed } from 'vue'; | |||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import MkFolder from '@/components/MkFolder.vue'; | import MkFolder from '@/components/MkFolder.vue'; | ||||||
| import MkButton from '@/components/MkButton.vue'; | import MkButton from '@/components/MkButton.vue'; | ||||||
| import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; | import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
|  |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { } from 'vue'; | import { } from 'vue'; | ||||||
| import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; | import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { shallowRef } from 'vue'; | import { shallowRef } from 'vue'; | ||||||
| import MkModal from '@/components/MkModal.vue'; | import MkModal from '@/components/MkModal.vue'; | ||||||
| import { navbarItemDef } from '@/navbar.js'; | import { navbarItemDef } from '@/navbar.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { deviceKind } from '@/utility/device-kind.js'; | ||||||
| import { deviceKind } from '@/scripts/device-kind.js'; | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	src?: HTMLElement; | 	src?: HTMLElement; | ||||||
| @@ -50,7 +50,7 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop | |||||||
|  |  | ||||||
| const modal = shallowRef<InstanceType<typeof MkModal>>(); | const modal = shallowRef<InstanceType<typeof MkModal>>(); | ||||||
|  |  | ||||||
| const menu = defaultStore.state.menu; | const menu = prefer.s.menu; | ||||||
|  |  | ||||||
| const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({ | const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({ | ||||||
| 	type: def.to ? 'link' : 'button', | 	type: def.to ? 'link' : 'button', | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { defineAsyncComponent, ref } from 'vue'; | import { defineAsyncComponent, ref } from 'vue'; | ||||||
| import { url as local } from '@@/js/config.js'; | import { url as local } from '@@/js/config.js'; | ||||||
| import { useTooltip } from '@/scripts/use-tooltip.js'; | import { useTooltip } from '@/utility/use-tooltip.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { isEnabledUrlPreview } from '@/instance.js'; | import { isEnabledUrlPreview } from '@/instance.js'; | ||||||
| import type { MkABehavior } from '@/components/global/MkA.vue'; | import type { MkABehavior } from '@/components/global/MkA.vue'; | ||||||
|   | |||||||
| @@ -10,20 +10,20 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| 	tabindex="0" | 	tabindex="0" | ||||||
| 	:class="[ | 	:class="[ | ||||||
| 		$style.audioContainer, | 		$style.audioContainer, | ||||||
| 		(audio.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive, | 		(audio.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive, | ||||||
| 	]" | 	]" | ||||||
| 	@contextmenu.stop | 	@contextmenu.stop | ||||||
| 	@keydown.stop | 	@keydown.stop | ||||||
| > | > | ||||||
| 	<button v-if="hide" :class="$style.hidden" @click="show"> | 	<button v-if="hide" :class="$style.hidden" @click="show"> | ||||||
| 		<div :class="$style.hiddenTextWrapper"> | 		<div :class="$style.hiddenTextWrapper"> | ||||||
| 			<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b> | 			<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b> | ||||||
| 			<b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b> | 			<b v-else style="display: block;"><i class="ti ti-music"></i> {{ prefer.s.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b> | ||||||
| 			<span style="display: block;">{{ i18n.ts.clickToShow }}</span> | 			<span style="display: block;">{{ i18n.ts.clickToShow }}</span> | ||||||
| 		</div> | 		</div> | ||||||
| 	</button> | 	</button> | ||||||
|  |  | ||||||
| 	<div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.nativeAudioContainer"> | 	<div v-else-if="prefer.s.useNativeUiForVideoAudioPlayer" :class="$style.nativeAudioContainer"> | ||||||
| 		<audio | 		<audio | ||||||
| 			ref="audioEl" | 			ref="audioEl" | ||||||
| 			preload="metadata" | 			preload="metadata" | ||||||
| @@ -91,15 +91,15 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue'; | import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import type { MenuItem } from '@/types/menu.js'; | import type { MenuItem } from '@/types/menu.js'; | ||||||
| import type { Keymap } from '@/scripts/hotkey.js'; | import type { Keymap } from '@/utility/hotkey.js'; | ||||||
| import { copyToClipboard } from '@/scripts/copy-to-clipboard'; | import { copyToClipboard } from '@/utility/copy-to-clipboard'; | ||||||
| import { defaultStore } from '@/store.js'; |  | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import bytes from '@/filters/bytes.js'; | import bytes from '@/filters/bytes.js'; | ||||||
| import { hms } from '@/filters/hms.js'; | import { hms } from '@/filters/hms.js'; | ||||||
| import MkMediaRange from '@/components/MkMediaRange.vue'; | import MkMediaRange from '@/components/MkMediaRange.vue'; | ||||||
| import { $i, iAmModerator } from '@/account.js'; | import { $i, iAmModerator } from '@/account.js'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	audio: Misskey.entities.DriveFile; | 	audio: Misskey.entities.DriveFile; | ||||||
| @@ -155,10 +155,10 @@ const playerEl = shallowRef<HTMLDivElement>(); | |||||||
| const audioEl = shallowRef<HTMLAudioElement>(); | const audioEl = shallowRef<HTMLAudioElement>(); | ||||||
|  |  | ||||||
| // eslint-disable-next-line vue/no-setup-props-reactivity-loss | // eslint-disable-next-line vue/no-setup-props-reactivity-loss | ||||||
| const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore')); | const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore')); | ||||||
|  |  | ||||||
| async function show() { | async function show() { | ||||||
| 	if (props.audio.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { | 	if (props.audio.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { | ||||||
| 		const { canceled } = await os.confirm({ | 		const { canceled } = await os.confirm({ | ||||||
| 			type: 'question', | 			type: 'question', | ||||||
| 			text: i18n.ts.sensitiveMediaRevealConfirm, | 			text: i18n.ts.sensitiveMediaRevealConfirm, | ||||||
| @@ -240,7 +240,7 @@ function showMenu(ev: MouseEvent) { | |||||||
| 		menu.push({ type: 'divider' }, ...details); | 		menu.push({ type: 'divider' }, ...details); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (defaultStore.state.devMode) { | 	if (prefer.s.devMode) { | ||||||
| 		menu.push({ type: 'divider' }, { | 		menu.push({ type: 'divider' }, { | ||||||
| 			icon: 'ti ti-id', | 			icon: 'ti ti-id', | ||||||
| 			text: i18n.ts.copyFileId, | 			text: i18n.ts.copyFileId, | ||||||
| @@ -407,7 +407,7 @@ onDeactivated(() => { | |||||||
| 	elapsedTimeMs.value = 0; | 	elapsedTimeMs.value = 0; | ||||||
| 	durationMs.value = 0; | 	durationMs.value = 0; | ||||||
| 	bufferedEnd.value = 0; | 	bufferedEnd.value = 0; | ||||||
| 	hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'); | 	hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore'); | ||||||
| 	stopAudioElWatch(); | 	stopAudioElWatch(); | ||||||
| 	onceInit = false; | 	onceInit = false; | ||||||
| 	if (mediaTickFrameId) { | 	if (mediaTickFrameId) { | ||||||
|   | |||||||
| @@ -27,9 +27,9 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { defaultStore } from '@/store.js'; |  | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import MkMediaAudio from '@/components/MkMediaAudio.vue'; | import MkMediaAudio from '@/components/MkMediaAudio.vue'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	media: Misskey.entities.DriveFile; | 	media: Misskey.entities.DriveFile; | ||||||
| @@ -38,7 +38,7 @@ const props = defineProps<{ | |||||||
| const hide = ref(true); | const hide = ref(true); | ||||||
|  |  | ||||||
| async function show() { | async function show() { | ||||||
| 	if (props.media.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { | 	if (props.media.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { | ||||||
| 		const { canceled } = await os.confirm({ | 		const { canceled } = await os.confirm({ | ||||||
| 			type: 'question', | 			type: 'question', | ||||||
| 			text: i18n.ts.sensitiveMediaRevealConfirm, | 			text: i18n.ts.sensitiveMediaRevealConfirm, | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| --> | --> | ||||||
|  |  | ||||||
| <template> | <template> | ||||||
| <div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" @click="onclick"> | <div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="onclick"> | ||||||
| 	<component | 	<component | ||||||
| 		:is="disableImageLink ? 'div' : 'a'" | 		:is="disableImageLink ? 'div' : 'a'" | ||||||
| 		v-bind="disableImageLink ? { | 		v-bind="disableImageLink ? { | ||||||
| @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| 	> | 	> | ||||||
| 		<ImgWithBlurhash | 		<ImgWithBlurhash | ||||||
| 			:hash="image.blurhash" | 			:hash="image.blurhash" | ||||||
| 			:src="(defaultStore.state.dataSaver.media && hide) ? null : url" | 			:src="(prefer.s.dataSaver.media && hide) ? null : url" | ||||||
| 			:forceBlurhash="hide" | 			:forceBlurhash="hide" | ||||||
| 			:cover="hide || cover" | 			:cover="hide || cover" | ||||||
| 			:alt="image.comment || image.name" | 			:alt="image.comment || image.name" | ||||||
| @@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| 	<template v-if="hide"> | 	<template v-if="hide"> | ||||||
| 		<div :class="$style.hiddenText"> | 		<div :class="$style.hiddenText"> | ||||||
| 			<div :class="$style.hiddenTextWrapper"> | 			<div :class="$style.hiddenTextWrapper"> | ||||||
| 				<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> | 				<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> | ||||||
| 				<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b> | 				<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ prefer.s.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b> | ||||||
| 				<span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span> | 				<span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| @@ -54,14 +54,14 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { watch, ref, computed } from 'vue'; | import { watch, ref, computed } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import type { MenuItem } from '@/types/menu.js'; | import type { MenuItem } from '@/types/menu.js'; | ||||||
| import { copyToClipboard } from '@/scripts/copy-to-clipboard'; | import { copyToClipboard } from '@/utility/copy-to-clipboard'; | ||||||
| import { getStaticImageUrl } from '@/scripts/media-proxy.js'; | import { getStaticImageUrl } from '@/utility/media-proxy.js'; | ||||||
| import bytes from '@/filters/bytes.js'; | import bytes from '@/filters/bytes.js'; | ||||||
| import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; | import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; | ||||||
| import { defaultStore } from '@/store.js'; |  | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { $i, iAmModerator } from '@/account.js'; | import { $i, iAmModerator } from '@/account.js'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	image: Misskey.entities.DriveFile; | 	image: Misskey.entities.DriveFile; | ||||||
| @@ -77,9 +77,9 @@ const props = withDefaults(defineProps<{ | |||||||
|  |  | ||||||
| const hide = ref(true); | const hide = ref(true); | ||||||
|  |  | ||||||
| const url = computed(() => (props.raw || defaultStore.state.loadRawImages) | const url = computed(() => (props.raw || prefer.s.loadRawImages) | ||||||
| 	? props.image.url | 	? props.image.url | ||||||
| 	: defaultStore.state.disableShowingAnimatedImages | 	: prefer.s.disableShowingAnimatedImages | ||||||
| 		? getStaticImageUrl(props.image.url) | 		? getStaticImageUrl(props.image.url) | ||||||
| 		: props.image.thumbnailUrl, | 		: props.image.thumbnailUrl, | ||||||
| ); | ); | ||||||
| @@ -91,7 +91,7 @@ async function onclick(ev: MouseEvent) { | |||||||
|  |  | ||||||
| 	if (hide.value) { | 	if (hide.value) { | ||||||
| 		ev.stopPropagation(); | 		ev.stopPropagation(); | ||||||
| 		if (props.image.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { | 		if (props.image.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { | ||||||
| 			const { canceled } = await os.confirm({ | 			const { canceled } = await os.confirm({ | ||||||
| 				type: 'question', | 				type: 'question', | ||||||
| 				text: i18n.ts.sensitiveMediaRevealConfirm, | 				text: i18n.ts.sensitiveMediaRevealConfirm, | ||||||
| @@ -105,7 +105,7 @@ async function onclick(ev: MouseEvent) { | |||||||
|  |  | ||||||
| // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする | // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする | ||||||
| watch(() => props.image, () => { | watch(() => props.image, () => { | ||||||
| 	hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore'); | 	hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.image.isSensitive && prefer.s.nsfw !== 'ignore'); | ||||||
| }, { | }, { | ||||||
| 	deep: true, | 	deep: true, | ||||||
| 	immediate: true, | 	immediate: true, | ||||||
| @@ -166,7 +166,7 @@ function showMenu(ev: MouseEvent) { | |||||||
| 		menuItems.push({ type: 'divider' }, ...details); | 		menuItems.push({ type: 'divider' }, ...details); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (defaultStore.state.devMode) { | 	if (prefer.s.devMode) { | ||||||
| 		menuItems.push({ type: 'divider' }, { | 		menuItems.push({ type: 'divider' }, { | ||||||
| 			icon: 'ti ti-id', | 			icon: 'ti ti-id', | ||||||
| 			text: i18n.ts.copyFileId, | 			text: i18n.ts.copyFileId, | ||||||
|   | |||||||
| @@ -12,9 +12,9 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| 			:class="[ | 			:class="[ | ||||||
| 				$style.medias, | 				$style.medias, | ||||||
| 				count === 1 ? [$style.n1, { | 				count === 1 ? [$style.n1, { | ||||||
| 					[$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9', | 					[$style.n116_9]: prefer.s.mediaListWithOneImageAppearance === '16_9', | ||||||
| 					[$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1', | 					[$style.n11_1]: prefer.s.mediaListWithOneImageAppearance === '1_1', | ||||||
| 					[$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3', | 					[$style.n12_3]: prefer.s.mediaListWithOneImageAppearance === '2_3', | ||||||
| 				}] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany, | 				}] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany, | ||||||
| 			]" | 			]" | ||||||
| 		> | 		> | ||||||
| @@ -33,13 +33,13 @@ import * as Misskey from 'misskey-js'; | |||||||
| import PhotoSwipeLightbox from 'photoswipe/lightbox'; | import PhotoSwipeLightbox from 'photoswipe/lightbox'; | ||||||
| import PhotoSwipe from 'photoswipe'; | import PhotoSwipe from 'photoswipe'; | ||||||
| import 'photoswipe/style.css'; | import 'photoswipe/style.css'; | ||||||
|  | import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js'; | ||||||
| import XBanner from '@/components/MkMediaBanner.vue'; | import XBanner from '@/components/MkMediaBanner.vue'; | ||||||
| import XImage from '@/components/MkMediaImage.vue'; | import XImage from '@/components/MkMediaImage.vue'; | ||||||
| import XVideo from '@/components/MkMediaVideo.vue'; | import XVideo from '@/components/MkMediaVideo.vue'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js'; | import { focusParent } from '@/utility/focus.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { prefer } from '@/preferences.js'; | ||||||
| import { focusParent } from '@/scripts/focus.js'; |  | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	mediaList: Misskey.entities.DriveFile[]; | 	mediaList: Misskey.entities.DriveFile[]; | ||||||
| @@ -75,7 +75,7 @@ async function calcAspectRatio() { | |||||||
| 		return `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`; | 		return `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`; | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	switch (defaultStore.state.mediaListWithOneImageAppearance) { | 	switch (prefer.s.mediaListWithOneImageAppearance) { | ||||||
| 		case '16_9': | 		case '16_9': | ||||||
| 			gallery.value.style.aspectRatio = ratioMax(16 / 9); | 			gallery.value.style.aspectRatio = ratioMax(16 / 9); | ||||||
| 			break; | 			break; | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| 	:class="[ | 	:class="[ | ||||||
| 		$style.videoContainer, | 		$style.videoContainer, | ||||||
| 		controlsShowing && $style.active, | 		controlsShowing && $style.active, | ||||||
| 		(video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive, | 		(video.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive, | ||||||
| 	]" | 	]" | ||||||
| 	@mouseover="onMouseOver" | 	@mouseover="onMouseOver" | ||||||
| 	@mouseleave="onMouseLeave" | 	@mouseleave="onMouseLeave" | ||||||
| @@ -20,13 +20,13 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| > | > | ||||||
| 	<button v-if="hide" :class="$style.hidden" @click="show"> | 	<button v-if="hide" :class="$style.hidden" @click="show"> | ||||||
| 		<div :class="$style.hiddenTextWrapper"> | 		<div :class="$style.hiddenTextWrapper"> | ||||||
| 			<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> | 			<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> | ||||||
| 			<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b> | 			<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ prefer.s.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b> | ||||||
| 			<span style="display: block;">{{ i18n.ts.clickToShow }}</span> | 			<span style="display: block;">{{ i18n.ts.clickToShow }}</span> | ||||||
| 		</div> | 		</div> | ||||||
| 	</button> | 	</button> | ||||||
|  |  | ||||||
| 	<div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.videoRoot"> | 	<div v-else-if="prefer.s.useNativeUiForVideoAudioPlayer" :class="$style.videoRoot"> | ||||||
| 		<video | 		<video | ||||||
| 			ref="videoEl" | 			ref="videoEl" | ||||||
| 			:class="$style.video" | 			:class="$style.video" | ||||||
| @@ -112,17 +112,17 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue'; | import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import type { MenuItem } from '@/types/menu.js'; | import type { MenuItem } from '@/types/menu.js'; | ||||||
| import type { Keymap } from '@/scripts/hotkey.js'; | import type { Keymap } from '@/utility/hotkey.js'; | ||||||
| import { copyToClipboard } from '@/scripts/copy-to-clipboard'; | import { copyToClipboard } from '@/utility/copy-to-clipboard'; | ||||||
| import bytes from '@/filters/bytes.js'; | import bytes from '@/filters/bytes.js'; | ||||||
| import { hms } from '@/filters/hms.js'; | import { hms } from '@/filters/hms.js'; | ||||||
| import { defaultStore } from '@/store.js'; |  | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js'; | import { exitFullscreen, requestFullscreen } from '@/utility/fullscreen.js'; | ||||||
| import hasAudio from '@/scripts/media-has-audio.js'; | import hasAudio from '@/utility/media-has-audio.js'; | ||||||
| import MkMediaRange from '@/components/MkMediaRange.vue'; | import MkMediaRange from '@/components/MkMediaRange.vue'; | ||||||
| import { $i, iAmModerator } from '@/account.js'; | import { $i, iAmModerator } from '@/account.js'; | ||||||
|  | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	video: Misskey.entities.DriveFile; | 	video: Misskey.entities.DriveFile; | ||||||
| @@ -175,10 +175,10 @@ function hasFocus() { | |||||||
| } | } | ||||||
|  |  | ||||||
| // eslint-disable-next-line vue/no-setup-props-reactivity-loss | // eslint-disable-next-line vue/no-setup-props-reactivity-loss | ||||||
| const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore')); | const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore')); | ||||||
|  |  | ||||||
| async function show() { | async function show() { | ||||||
| 	if (props.video.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { | 	if (props.video.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { | ||||||
| 		const { canceled } = await os.confirm({ | 		const { canceled } = await os.confirm({ | ||||||
| 			type: 'question', | 			type: 'question', | ||||||
| 			text: i18n.ts.sensitiveMediaRevealConfirm, | 			text: i18n.ts.sensitiveMediaRevealConfirm, | ||||||
| @@ -265,7 +265,7 @@ function showMenu(ev: MouseEvent) { | |||||||
| 		menu.push({ type: 'divider' }, ...details); | 		menu.push({ type: 'divider' }, ...details); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (defaultStore.state.devMode) { | 	if (prefer.s.devMode) { | ||||||
| 		menu.push({ type: 'divider' }, { | 		menu.push({ type: 'divider' }, { | ||||||
| 			icon: 'ti ti-id', | 			icon: 'ti ti-id', | ||||||
| 			text: i18n.ts.copyFileId, | 			text: i18n.ts.copyFileId, | ||||||
| @@ -502,7 +502,7 @@ onDeactivated(() => { | |||||||
| 	elapsedTimeMs.value = 0; | 	elapsedTimeMs.value = 0; | ||||||
| 	durationMs.value = 0; | 	durationMs.value = 0; | ||||||
| 	bufferedEnd.value = 0; | 	bufferedEnd.value = 0; | ||||||
| 	hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'); | 	hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore'); | ||||||
| 	stopVideoElWatch(); | 	stopVideoElWatch(); | ||||||
| 	onceInit = false; | 	onceInit = false; | ||||||
| 	if (mediaTickFrameId) { | 	if (mediaTickFrameId) { | ||||||
|   | |||||||
| @@ -19,8 +19,8 @@ import { computed } from 'vue'; | |||||||
| import { host as localHost } from '@@/js/config.js'; | import { host as localHost } from '@@/js/config.js'; | ||||||
| import type { MkABehavior } from '@/components/global/MkA.vue'; | import type { MkABehavior } from '@/components/global/MkA.vue'; | ||||||
| import { $i } from '@/account.js'; | import { $i } from '@/account.js'; | ||||||
| import { defaultStore } from '@/store.js'; | import { getStaticImageUrl } from '@/utility/media-proxy.js'; | ||||||
| import { getStaticImageUrl } from '@/scripts/media-proxy.js'; | import { prefer } from '@/preferences.js'; | ||||||
|  |  | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	username: string; | 	username: string; | ||||||
| @@ -36,7 +36,7 @@ const isMe = $i && ( | |||||||
| 	`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase() | 	`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase() | ||||||
| ); | ); | ||||||
|  |  | ||||||
| const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar | const avatarUrl = computed(() => prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar | ||||||
| 	? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`) | 	? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`) | ||||||
| 	: `/avatar/@${props.username}@${props.host}`, | 	: `/avatar/@${props.username}@${props.host}`, | ||||||
| ); | ); | ||||||
|   | |||||||
| @@ -177,14 +177,14 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue'; | import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue'; | ||||||
| import MkSwitchButton from '@/components/MkSwitch.button.vue'; |  | ||||||
| import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js'; | 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 * as os from '@/os.js'; | import * as os from '@/os.js'; | ||||||
| import { i18n } from '@/i18n.js'; | import { i18n } from '@/i18n.js'; | ||||||
| import { isTouchUsing } from '@/scripts/touch.js'; | import { isTouchUsing } from '@/utility/touch.js'; | ||||||
| import type { Keymap } from '@/scripts/hotkey.js'; | import { isFocusable } from '@/utility/focus.js'; | ||||||
| import { isFocusable } from '@/scripts/focus.js'; | import { getNodeOrNull } from '@/utility/get-dom-node-or-null.js'; | ||||||
| import { getNodeOrNull } from '@/scripts/get-dom-node-or-null.js'; |  | ||||||
|  |  | ||||||
| const childrenCache = new WeakMap<MenuParent, MenuItem[]>(); | const childrenCache = new WeakMap<MenuParent, MenuItem[]>(); | ||||||
| </script> | </script> | ||||||
| @@ -558,11 +558,11 @@ onBeforeUnmount(() => { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	&.danger { | 	&.danger { | ||||||
| 		--menuFg: #ff2a2a; | 		--menuFg: var(--MI_THEME-error); | ||||||
| 		--menuHoverFg: #fff; | 		--menuHoverFg: #fff; | ||||||
| 		--menuHoverBg: #ff4242; | 		--menuHoverBg: var(--MI_THEME-error); | ||||||
| 		--menuActiveFg: #fff; | 		--menuActiveFg: #fff; | ||||||
| 		--menuActiveBg: #d42e2e; | 		--menuActiveBg: hsl(from var(--MI_THEME-error) h s calc(l - 10)); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	&.radio { | 	&.radio { | ||||||
|   | |||||||