Compare commits

...

62 Commits

Author SHA1 Message Date
github-actions[bot]
c9fa95429a Bump version to 2025.3.2-alpha.9 2025-03-12 12:45:35 +00:00
饺子w (Yumechi)
e5d117dc98 fix(backend): tighten an overly relaxed criteria and remove capability of matching multiple final URLs in URL authority checking (#15655)
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-03-12 12:39:24 +00:00
syuilo
4a73feb041 enhance(frontend): make deck profiles syncable 2025-03-12 21:12:08 +09:00
syuilo
a06b9eefaa enhance(frontend): suppress needless confirmation when turn on pref sync 2025-03-12 21:05:39 +09:00
syuilo
3129fcf164 fix(frontend): fix type errors 2025-03-12 20:17:54 +09:00
syuilo
35a4544477 add todo 2025-03-12 18:54:36 +09:00
zyoshoka
aa1cc2f817 fix(storybook): use type-only imports in generated stories (#15654) 2025-03-12 16:51:10 +09:00
github-actions[bot]
15685be4cc Bump version to 2025.3.2-alpha.8 2025-03-12 06:10:35 +00:00
syuilo
8508c4dadc refactor 2025-03-12 15:07:45 +09:00
かっこかり
e594fb0037 enhance(dev): frontendの検索インデックス作成を単独のコマンドで行えるように (#15653) 2025-03-12 14:37:57 +09:00
syuilo
a369721791 remove todo 2025-03-12 14:35:22 +09:00
syuilo
f8e244f48d enhance(frontend): アカウントオーバーライド設定とデバイス間同期の併用に対応 2025-03-12 14:34:10 +09:00
syuilo
8410611512 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-12 13:04:44 +09:00
syuilo
caab1ec7c3 🎨 2025-03-12 13:04:41 +09:00
github-actions[bot]
ffade9740e Bump version to 2025.3.2-alpha.7 2025-03-12 03:03:37 +00:00
syuilo
b03bcf26cd enhance(frontend): 設定値の同期を実装(実験的) 2025-03-12 11:39:05 +09:00
syuilo
ddbc83b2e4 chore(frontend): tweak settings page 2025-03-11 20:42:06 +09:00
syuilo
d185785f20 enhance(frontend): improve settings page 2025-03-11 14:52:04 +09:00
syuilo
02d7fbefc4 🎨 2025-03-11 12:08:15 +09:00
syuilo
f7ea92c68c chore: remove unused files 2025-03-11 12:02:41 +09:00
syuilo
e891d5c5d3 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-11 11:44:34 +09:00
syuilo
57a6b630b7 chore: add note 2025-03-11 11:44:25 +09:00
github-actions[bot]
eda768a08c Bump version to 2025.3.2-alpha.6 2025-03-11 02:43:27 +00:00
syuilo
1f345eb839 enhance(frontend): deckをpreferences管理に 2025-03-11 11:14:55 +09:00
syuilo
1f2801af02 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-10 21:42:30 +09:00
syuilo
a4ba096e2a chore(frontend): improve preference store stability 2025-03-10 21:42:17 +09:00
ろむねこ
6841cdfa76 enhance(frontend): CWの注釈テキストが入力されていない場合はPostボタンを非アクティブに (#15639)
* add condition to disable post button when CW text is empty

* standardize condition by using 1<= inserted of 0<

* unify CW text length condition to improve readability

* add missing CW state check

* fix state check, add empty/null check, improve max length validation

* simplify CW validation by removing minimum length check

* Update CHANGELOG

* remove CW text validation in post()

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-03-10 10:35:37 +00:00
github-actions[bot]
794f360bc2 Bump version to 2025.3.2-alpha.5 2025-03-10 09:40:41 +00:00
かっこかり
f797765b1d enhance(frontend): テーマ設定で簡易プレビューを表示するように (#15643)
* enhance(frontend): テーマ設定で簡易プレビューを表示するように

* Update Changelog

* fix lint

* 🎨

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-03-10 09:35:51 +00:00
syuilo
9dce512fbb enhance(frontend): add navbar transition animation 2025-03-10 15:47:00 +09:00
syuilo
9e91f85370 refactor(frontend): use Symbol for vue provide/inject 2025-03-10 15:08:40 +09:00
syuilo
9998cb84e8 refactor(frontend): page-metadata -> page 2025-03-10 13:47:38 +09:00
renovate[bot]
5ed1101bbd chore(deps): update [root] update dependencies (#15624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 12:30:37 +09:00
syuilo
6c9153300d chore(frontend): tweak lockdown setting 2025-03-10 12:03:54 +09:00
syuilo
7957ee5191 fix(frontend): rename pizzax fields 2025-03-10 11:28:54 +09:00
syuilo
b200743845 refactor(frontend): rename store.set -> store.commit 2025-03-10 11:27:07 +09:00
syuilo
08f7e7d9b3 refactor(frontend): rename pizzax fields 2025-03-10 10:51:54 +09:00
github-actions[bot]
16ad6b3f6c Bump version to 2025.3.2-alpha.4 2025-03-10 01:09:42 +00:00
syuilo
4df9083bf0 fix(frontend): テーマ切り替え時に一部の色が変わらない問題を修正 2025-03-10 10:05:50 +09:00
taichan
6419af2179 fix(frontend, dev): storybookのビルドエラー修正のため、as構文にリファクタ (#15640) 2025-03-10 09:34:45 +09:00
syuilo
d9858b03c9 enhance(frontend): improve plugin management 2025-03-10 09:28:07 +09:00
taichan
88efc0a3be fix(dev): 検索インデックス対象ファイルでHMRが効かない問題を修正 (#15638) 2025-03-09 22:45:17 +00:00
github-actions[bot]
ac21fa7194 Bump version to 2025.3.2-alpha.3 2025-03-09 13:01:46 +00:00
syuilo
c76afce9a7 enhance(frontend): improve plugin management 2025-03-09 21:57:56 +09:00
github-actions[bot]
8e3304344f Bump version to 2025.3.2-alpha.2 2025-03-09 12:32:54 +00:00
饺子w (Yumechi)
db5c127cdd fix(backend): fix handling of invalid urls in user profile (#15635)
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-03-09 12:28:47 +00:00
syuilo
0402866b43 enhance(frontend): improve plugin management 2025-03-09 21:23:36 +09:00
syuilo
6cefabc6b6 chore(frontend): remove unused binding 2025-03-09 17:19:21 +09:00
syuilo
c9c04d8391 enhance(frontend): migrate overridedDeviceKind to preference 2025-03-09 17:14:48 +09:00
syuilo
27e8805dcb refactor(frontend): relocate plugin consts 2025-03-09 17:02:46 +09:00
github-actions[bot]
933abedc90 Bump version to 2025.3.2-alpha.1 2025-03-09 06:16:49 +00:00
syuilo
69eee9f050 enhance(frontend): ウィジェットもpreference管理に 2025-03-09 15:13:49 +09:00
syuilo
2918fb2609 refactor(frontend): relocate theme script 2025-03-09 14:32:29 +09:00
syuilo
fcd7fa62ba Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-03-09 14:28:06 +09:00
syuilo
be7e3b9a0c refactor(frontend): scripts -> utility 2025-03-09 14:28:01 +09:00
github-actions[bot]
06e7272ca1 Bump version to 2025.3.2-alpha.0 2025-03-09 05:22:26 +00:00
かっこかり
f35eb0f6d9 enhnace(frontend): 文字列比較のためのローマナイズを強化(設定の検索) (#15632)
* enhnace(frontend): 文字列比較のためのローマナイズを強化

* docs

* fix

* fix

* fix

* comment

* wanakanaの初回ロードをコンポーネント内に移動

* comment

* fix

* add tests

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-03-09 14:21:23 +09:00
syuilo
bdb74539d4 enhance(frontend): tweak settings page 2025-03-09 14:18:50 +09:00
syuilo
abc1e9168d refactor 2025-03-09 12:39:43 +09:00
syuilo
d30ddd4c2e Refine preferences (#15597)
* wip

* wip

* wip

* test

* wip rollup pluginでsearchIndexの情報生成

* wip

* SPDX

* wip: markerIdを自動付与

* rollupでビルド時・devモード時に毎回uuidを生成するように

* 開発サーバーでだけ必要な挙動は開発サーバーのみで

* 条件が逆

* wip: childrenの生成

* update comment

* update comment

* rename auto generated file

* hashをパスと行数から決定

* Update privacy.vue

* Update privacy.vue

* wip

* Update general.vue

* Update general.vue

* wip

* wip

* Update SearchMarker.vue

* wip

* Update profile.vue

* Update mute-block.vue

* Update mute-block.vue

* Update general.vue

* Update general.vue

* childrenがduplicate key errorを吐く問題をいったん解決

* マーカーの形を成形

* loggerを置きかえ

* とりあえず省略記法に対応

* Refactor and Format codes

* wip

* Update settings-search-index.ts

* wip

* wip

* とりあえず不確定要因の仮置きidを削除

* hashの生成を正規化(絶対パスになっていたのを緩和)

* pathの入力を省略可能に

* adminでもパス生成できるように

* Update settings-search-index.ts

* Update privacy.vue

* wip

* build searchIndex

* wip

* build

* Update general.vue

* build

* Update sounds.vue

* build

* build

* Update sounds.vue

* 🎨

* 🎨

* Update privacy.vue

* Update privacy.vue

* Update security.vue

* create-search-indexを多少改善

* build

* Update 2fa.vue

* wip

* 必ずtransformCodeCacheを利用するように, キャッシュの明確な受け渡しを定義

* キャッシュはdevServerでなくても更新

* Revert "wip"

This reverts commit 41bffd3a13.

* inlining

* wip

* Update theme.vue

* 🎨

* wip normalize

* Update theme.vue

* キャッシュのパス変換

* build

* wip

* wip

* Update SearchMarker.vue

* i18n.ts['key'] の形式が取り出せない問題のFix

* build

* 仮でpath入れ

* 必ず絶対パスが使われるように

* wip

* 🎨

* storybookビルド時はcreateSearchIndexをしない

* inliningの構造化

* format code

* Update index.vue

* wip

* wip

* 🎨

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* clean up

* wip

* wip

* wip

* Update rollup-plugin-unwind-css-module-class-name.test.ts

* Update navbar.vue

* clean up

* wip

* wip

* wip

* wip

* wip

* Update preferences-backups.vue

* Update common.ts

* Update preferences.ts

* wip

* wip

* wip

* wip

* Update MkPreferenceContainer.vue

* Update MkPreferenceContainer.vue

* Update MkPreferenceContainer.vue

* enhance: 検索で上下矢印を使用することで検索結果を移動できるように

* Update main-boot.ts

* refactor

* wip

* Update sounds.vue

* fix(frontend): PageWindowでSearchMarkerが動作するように

* enhance(frontend): SearchMarkerの点滅を一定時間で止める

* wip

* lint fix

* fix: 子要素監視が抜けていたのを修正

* アニメーションの回数はCSSで制御するように

* refactor

* enhance(frontend): 検索インデックス作成時のログを削減

* revert

* fix

* fix

* Update preferences.ts

* Update preferences.ts

* wip

* Update preferences.ts

* wip

* 🎨

* wip

* Update MkPreferenceContainer.vue

* wip

* Update preferences.ts

* wip

* Update preferences.ts

* Update preferences.ts

* wip

* wip

* Update preferences.ts

* wip

* wip

* Update preferences.ts

* Update CHANGELOG.md

* Update preferences.ts

* Update deck-store.ts

* deckStoreをdefaultStoreに統合

* wip

* defaultStore -> store

* Update profile.ts

* wip

* refactor

* wip: plugin

* plugin

* plugin

* plugin

* Update plugin.ts

* wip

* Update plugin.vue

* Update preferences.ts

* Update main-boot.ts

* wip

* fix test

* Update plugin.vue

* Update plugin.vue

* Update utility.ts

* wip

* wip

* Update utility.ts

* wip

* wip

* clean up

* Update utility.ts

---------

Co-authored-by: tai-cha <dev@taichan.site>
Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
2025-03-09 12:34:08 +09:00
github-actions[bot]
05cdc095c0 [skip ci] Update CHANGELOG.md (prepend template) 2025-03-09 03:30:00 +00:00
github-actions[bot]
7c1dc3d632 Release: 2025.3.1 2025-03-09 03:29:54 +00:00
555 changed files with 6968 additions and 4695 deletions

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

182
locales/index.d.ts vendored
View File

@@ -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": {
/** /**
* メイン * メイン

View File

@@ -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: "メイン"

View File

@@ -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",

View File

@@ -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;
} }

View File

@@ -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,
})); }));

View File

@@ -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;
} }

View File

@@ -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)

View File

@@ -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');
}); });

View File

@@ -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': {
@@ -62,7 +106,7 @@ type ToKebab<T extends readonly string[]> = T extends readonly [
: T extends readonly [ : T extends readonly [
infer XH extends string, infer XH extends string,
...infer XR extends readonly string[] ...infer XR extends readonly string[]
] ]
? `${XH}${XR extends readonly string[] ? `-${ToKebab<XR>}` : ''}` ? `${XH}${XR extends readonly string[] ? `-${ToKebab<XR>}` : ''}`
: ''; : '';
@@ -132,7 +176,7 @@ function toStories(component: string): Promise<string> {
kind={'init' as const} kind={'init' as const}
shorthand shorthand
/> as estree.Property, /> as estree.Property,
] ]
: []), : []),
]} ]}
/> as estree.ObjectExpression; /> as estree.ObjectExpression;
@@ -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,8 +210,8 @@ 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,8 +221,8 @@ 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
? [ ? [
<import-declaration <import-declaration
@@ -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

View File

@@ -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();

View File

@@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -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,

View File

@@ -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;
}, },

View File

@@ -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",

View 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();

View File

@@ -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 };

View File

@@ -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';

View File

@@ -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
if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
} else if (prefer.s.lightTheme && !store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
}
fetchInstanceMetaPromise.then(() => { fetchInstanceMetaPromise.then(() => {
if (defaultStore.state.themeInitial) { // TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme)); if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme)); if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
}
}); });
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(() => {

View File

@@ -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');

View File

@@ -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(

View File

@@ -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'));

View File

@@ -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];

View File

@@ -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>();

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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';

View File

@@ -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;

View File

@@ -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';

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;
} }

View File

@@ -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[];

View File

@@ -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));
} }
} }
} }

View File

@@ -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),

View File

@@ -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<{

View File

@@ -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)));

View File

@@ -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);

View File

@@ -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);

View File

@@ -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'));

View File

@@ -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<{

View File

@@ -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<{

View File

@@ -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', {

View File

@@ -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';

View File

@@ -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',

View File

@@ -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;

View File

@@ -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();

View File

@@ -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,

View File

@@ -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<{

View File

@@ -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);
}); });

View File

@@ -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;

View File

@@ -131,17 +131,18 @@ 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;
pinnedEmojis?: string[]; pinnedEmojis?: string[];
maxHeight?: number; maxHeight?: number;
asDrawer?: boolean; asDrawer?: boolean;
asWindow?: boolean; asWindow?: 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));
} }
} }

View File

@@ -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,16 +40,16 @@ 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;
src?: HTMLElement; src?: HTMLElement;
showPinned?: boolean; showPinned?: boolean;
pinnedEmojis?: string[], pinnedEmojis?: string[],
asReactionPicker?: boolean; asReactionPicker?: boolean;
targetNote?: Misskey.entities.Note; targetNote?: Misskey.entities.Note;
choseAndClose?: boolean; choseAndClose?: boolean;
}>(), { }>(), {
manualShowing: null, manualShowing: null,
showPinned: true, showPinned: true,

View 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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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?: {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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',

View File

@@ -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';

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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}`,
); );

View File

@@ -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 {

Some files were not shown because too many files have changed in this diff Show More