Compare commits
41 Commits
a935335ca9
...
569fbbcbd7
Author | SHA1 | Date |
---|---|---|
|
569fbbcbd7 | |
|
0bf49818c4 | |
|
63c97b9038 | |
|
3f5d933efd | |
|
d0a98f6e6c | |
|
102578712b | |
|
2e0c80bc21 | |
|
d44fd87b69 | |
|
55d835ad51 | |
|
3ec5bf114b | |
|
a5f9eba974 | |
|
bd8162c2bd | |
|
4f206eea9f | |
|
f4f9832f8b | |
|
70133a88f2 | |
|
20bc80728c | |
|
1d9810627e | |
|
eb5061db29 | |
|
b0c3ec3267 | |
|
3a6de462bb | |
|
b0cdf2feff | |
|
4e1e8ff238 | |
|
d04acf04a6 | |
|
165c7993e6 | |
|
500a5615f0 | |
|
5aca91251a | |
|
1c683c3fcc | |
|
08072e294b | |
|
f637d0dff9 | |
|
15a5bb17e3 | |
|
888e04ce82 | |
|
3a028abea5 | |
|
ff59089ad9 | |
|
93214862b1 | |
|
ea722b8360 | |
|
49f1f7194d | |
|
8baf54e629 | |
|
393f893a2c | |
|
87a7238976 | |
|
e0d8702839 | |
|
6e929ece6f |
|
@ -165,6 +165,11 @@ id: 'aidx'
|
|||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
#sentryForFrontend:
|
||||
# vueIntegration:
|
||||
# tracingOptions:
|
||||
# trackComponents: true
|
||||
# browserTracingIntegration:
|
||||
# replayIntegration:
|
||||
# options:
|
||||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
|
|
|
@ -177,6 +177,11 @@ id: 'aidx'
|
|||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
#sentryForFrontend:
|
||||
# vueIntegration:
|
||||
# tracingOptions:
|
||||
# trackComponents: true
|
||||
# browserTracingIntegration:
|
||||
# replayIntegration:
|
||||
# options:
|
||||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
|
|
|
@ -259,6 +259,11 @@ id: 'aidx'
|
|||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
#sentryForFrontend:
|
||||
# vueIntegration:
|
||||
# tracingOptions:
|
||||
# trackComponents: true
|
||||
# browserTracingIntegration:
|
||||
# replayIntegration:
|
||||
# options:
|
||||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
|
|
|
@ -152,6 +152,11 @@ id: 'aidx'
|
|||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
#sentryForFrontend:
|
||||
# vueIntegration:
|
||||
# tracingOptions:
|
||||
# trackComponents: true
|
||||
# browserTracingIntegration:
|
||||
# replayIntegration:
|
||||
# options:
|
||||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
uses: pnpm/action-setup@v4.1.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
- name: Checkout head
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
|
||||
- name: setup node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: pnpm
|
||||
|
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -38,7 +38,7 @@ jobs:
|
|||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
- uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
@ -69,13 +69,13 @@ jobs:
|
|||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
- uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Restore eslint cache
|
||||
uses: actions/cache@v4.2.2
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: ${{ env.eslint-cache-path }}
|
||||
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
@ -99,7 +99,7 @@ jobs:
|
|||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
- uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- uses: actions/setup-node@v4.2.0
|
||||
- uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -45,7 +45,7 @@ jobs:
|
|||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -62,7 +62,7 @@ jobs:
|
|||
fi
|
||||
done
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
@ -109,7 +109,7 @@ jobs:
|
|||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
fi
|
||||
done
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -38,7 +38,7 @@ jobs:
|
|||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
@ -93,7 +93,7 @@ jobs:
|
|||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
uses: pnpm/action-setup@v4.1.0
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
|
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,4 +1,4 @@
|
|||
## 2025.3.2
|
||||
## 2025.4.0
|
||||
|
||||
### General
|
||||
- Feat: チャットがリニューアルして復活しました(beta)
|
||||
|
@ -13,7 +13,11 @@
|
|||
- メッセージにはリアクションも可能です
|
||||
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
|
||||
- Misskeyネイティブでダッシュボードを実装予定です
|
||||
- Enhance: フロントエンドのエラートラッキングができるように
|
||||
- `.config/default.yml`中の項目`sentryForFrontend`を適宜設定してください。
|
||||
- 外部サービスであるSentryへエラー情報が送信されます。ご利用の地域の法令に従い、適切なプライバシーポリシーを策定の上で運用してください。
|
||||
- Enhance: ミュートしているユーザーをユーザー検索の結果から除外するように
|
||||
- Fix: 通知のページネーションで2つ以上読み込めなくなることがある問題を修正
|
||||
- Enhance: アンテナでセンシティブなチャンネルのノートを除外できるように `#14177`
|
||||
|
||||
### Client
|
||||
|
@ -40,6 +44,9 @@
|
|||
- Enhance: プラグインの管理が強化されました
|
||||
- インストール/アンインストール/設定の変更時にリロード不要になりました
|
||||
- Enhance: ログアウト時、ブラウザに保存されたWebクライアントのデータを全て消去するように
|
||||
- Enhance: デッキUIでカラム間のマージンを設定できるように
|
||||
- Enhance: デッキUIでデッキメニューの位置を設定できるように
|
||||
- Enhance: デッキUIでナビゲーションバーの位置を設定できるように
|
||||
- Enhance: アイコンのスクロール追従を無効化してパフォーマンス向上できるように
|
||||
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
|
||||
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
|
||||
|
@ -53,8 +60,8 @@
|
|||
- Fix: 読み込み直後にスクロールしようとすると途中で止まる場合があるのを修正
|
||||
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
|
||||
- NOTE: 構造上クラシックUIを新しいデザインシステムに移行することが困難なため、クラシックUIが削除されました
|
||||
- デッキUIでカラムを中央寄せにし、メインカラムの左右にウィジェットカラムを配置することである程度クラシックUIを再現できます
|
||||
- また、デッキでナビゲーションバーを上部に表示するオプションを実装予定です
|
||||
- デッキUIでカラムを中央寄せにし、メインカラムの左右にウィジェットカラムを配置し、ナビゲーションバーを上部に表示することである程度クラシックUIを再現できます
|
||||
- Fix: iPadOSでdeck uiをマウスカーソルによってスクロールできない問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance 全体的なパフォーマンス向上
|
||||
|
|
|
@ -173,6 +173,11 @@ id: "aidx"
|
|||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
#sentryForFrontend:
|
||||
# vueIntegration:
|
||||
# tracingOptions:
|
||||
# trackComponents: true
|
||||
# browserTracingIntegration:
|
||||
# replayIntegration:
|
||||
# options:
|
||||
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
|
||||
|
||||
|
|
|
@ -1128,7 +1128,7 @@ pleaseAgreeAllToContinue: "Has d'acceptar tots els camps de dalt per poder conti
|
|||
continue: "Continuar"
|
||||
preservedUsernames: "Noms d'usuaris reservats"
|
||||
preservedUsernamesDescription: "Llistat de noms d'usuaris que no es poden fer servir separats per salts de linia. Aquests noms d'usuaris no estaran disponibles quan es creï un compte d'usuari normal, però els administradors els poden fer servir per crear comptes manualment. Per altre banda els comptes ja creats amb aquests noms d'usuari no es veure'n afectats."
|
||||
createNoteFromTheFile: "Compon una nota des d'aquest fitxer"
|
||||
createNoteFromTheFile: "Escriu una nota incloent aquest fitxer"
|
||||
archive: "Arxiu"
|
||||
archived: "Arxivat"
|
||||
unarchive: "Desarxivar"
|
||||
|
@ -1336,6 +1336,9 @@ chat: "Xat"
|
|||
migrateOldSettings: "Migració de la configuració antiga "
|
||||
migrateOldSettings_description: "Normalment això es fa automàticament, però si la transició no es fa, el procés es pot iniciar manualment. S'esborrarà la configuració actual."
|
||||
compress: "Comprimir "
|
||||
right: "Dreta"
|
||||
bottom: "A baix "
|
||||
top: "A dalt "
|
||||
_chat:
|
||||
noMessagesYet: "Encara no tens missatges "
|
||||
newMessage: "Missatge nou"
|
||||
|
@ -2593,6 +2596,9 @@ _notification:
|
|||
_deck:
|
||||
alwaysShowMainColumn: "Mostrar sempre la columna principal"
|
||||
columnAlign: "Alinea les columnes"
|
||||
columnGap: "Espai entre columnes"
|
||||
deckMenuPosition: "Posició del menú del tauler"
|
||||
navbarPosition: "Posició de la barra de navegació "
|
||||
addColumn: "Afig una columna"
|
||||
newNoteNotificationSettings: "Configuració de notificacions per a notes noves"
|
||||
configureColumn: "Configuració de columnes"
|
||||
|
|
|
@ -962,8 +962,8 @@ cropImageAsk: "Möchtest du das Bild zuschneiden?"
|
|||
cropYes: "Zuschneiden"
|
||||
cropNo: "Unbearbeitet verwenden"
|
||||
file: "Datei"
|
||||
recentNHours: "Letzten {n} Stunden"
|
||||
recentNDays: "Letzten {n} Tage"
|
||||
recentNHours: "Letzte {n} Stunden"
|
||||
recentNDays: "Letzte {n} Tage"
|
||||
noEmailServerWarning: "Es ist kein Email-Server konfiguriert."
|
||||
thereIsUnresolvedAbuseReportWarning: "Es liegen ungelöste Meldungen vor."
|
||||
recommended: "Empfehlung"
|
||||
|
@ -971,7 +971,7 @@ check: "Check"
|
|||
driveCapOverrideLabel: "Die Drive-Kapazität dieses Nutzers verändern"
|
||||
driveCapOverrideCaption: "Gib einen Wert von 0 oder weniger ein, um die Kapazität auf den Standard zurückzusetzen."
|
||||
requireAdminForView: "Melde dich mit einem Administratorkonto an, um dies einzusehen."
|
||||
isSystemAccount: "Ein Benutzerkonto, dass durch das System erstellt und automatisch kontrolliert wird."
|
||||
isSystemAccount: "Ein Benutzerkonto, das durch das System erstellt und automatisch verwaltet wird."
|
||||
typeToConfirm: "Bitte gib zur Bestätigung {x} ein"
|
||||
deleteAccount: "Benutzerkonto löschen"
|
||||
document: "Dokumentation"
|
||||
|
|
|
@ -345,7 +345,7 @@ emptyDrive: "Your Drive is empty"
|
|||
emptyFolder: "This folder is empty"
|
||||
unableToDelete: "Unable to delete"
|
||||
inputNewFileName: "Enter a new filename"
|
||||
inputNewDescription: "Enter new caption"
|
||||
inputNewDescription: "Enter new alt text"
|
||||
inputNewFolderName: "Enter a new folder name"
|
||||
circularReferenceFolder: "The destination folder is a subfolder of the folder you wish to move."
|
||||
hasChildFilesOrFolders: "Since this folder is not empty, it can not be deleted."
|
||||
|
@ -643,8 +643,8 @@ disablePlayer: "Close video player"
|
|||
expandTweet: "Expand post"
|
||||
themeEditor: "Theme editor"
|
||||
description: "Description"
|
||||
describeFile: "Add caption"
|
||||
enterFileDescription: "Enter caption"
|
||||
describeFile: "Add alt text"
|
||||
enterFileDescription: "Enter alt text"
|
||||
author: "Author"
|
||||
leaveConfirm: "There are unsaved changes. Do you want to discard them?"
|
||||
manage: "Management"
|
||||
|
@ -1014,7 +1014,7 @@ sendPushNotificationReadMessageCaption: "This may increase the power consumption
|
|||
windowMaximize: "Maximize"
|
||||
windowMinimize: "Minimize"
|
||||
windowRestore: "Restore"
|
||||
caption: "Caption"
|
||||
caption: "Alt text"
|
||||
loggedInAsBot: "Currently logged in as bot"
|
||||
tools: "Tools"
|
||||
cannotLoad: "Unable to load"
|
||||
|
@ -1336,6 +1336,9 @@ chat: "Chat"
|
|||
migrateOldSettings: "Migrate old client settings"
|
||||
migrateOldSettings_description: "This should be done automatically but if for some reason the migration was not successful, you can trigger the migration process yourself manually. The current configuration information will be overwritten."
|
||||
compress: "Compress"
|
||||
right: "Right"
|
||||
bottom: "Bottom"
|
||||
top: "Top"
|
||||
_chat:
|
||||
noMessagesYet: "No messages yet"
|
||||
newMessage: "New message"
|
||||
|
@ -2593,6 +2596,9 @@ _notification:
|
|||
_deck:
|
||||
alwaysShowMainColumn: "Always show main column"
|
||||
columnAlign: "Align columns"
|
||||
columnGap: "Margin between columns"
|
||||
deckMenuPosition: "Deck menu position"
|
||||
navbarPosition: "Navigation bar position"
|
||||
addColumn: "Add column"
|
||||
newNoteNotificationSettings: "Notification setting for new notes"
|
||||
configureColumn: "Column settings"
|
||||
|
@ -2606,7 +2612,7 @@ _deck:
|
|||
newProfile: "New profile"
|
||||
deleteProfile: "Delete profile"
|
||||
introduction: "Create the perfect interface for you by arranging columns freely!"
|
||||
introduction2: "Click on the + on the right of the screen to add new colums whenever you want."
|
||||
introduction2: "Click on the + on the right of the screen to add new columns whenever you want."
|
||||
widgetsIntroduction: "Please select \"Edit widgets\" in the column menu and add a widget."
|
||||
useSimpleUiForNonRootPages: "Use simple UI for navigated pages"
|
||||
usedAsMinWidthWhenFlexible: "Minimum width will be used for this when the \"Auto-adjust width\" option is enabled"
|
||||
|
|
|
@ -1295,6 +1295,7 @@ messageToFollower: "Mensaje a seguidores"
|
|||
target: "Para"
|
||||
federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador."
|
||||
federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores"
|
||||
preferences: "Preferencias"
|
||||
postForm: "Formulario"
|
||||
information: "Información"
|
||||
_chat:
|
||||
|
|
|
@ -5366,6 +5366,18 @@ export interface Locale extends ILocale {
|
|||
* 圧縮
|
||||
*/
|
||||
"compress": string;
|
||||
/**
|
||||
* 右
|
||||
*/
|
||||
"right": string;
|
||||
/**
|
||||
* 下
|
||||
*/
|
||||
"bottom": string;
|
||||
/**
|
||||
* 上
|
||||
*/
|
||||
"top": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* まだメッセージはありません
|
||||
|
@ -5501,7 +5513,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"thisUserAllowsChatOnlyFromFollowers": string;
|
||||
/**
|
||||
* このユーザーはフォローしているユーザーからのみチャットを受け付けています。
|
||||
* このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。
|
||||
*/
|
||||
"thisUserAllowsChatOnlyFromFollowing": string;
|
||||
/**
|
||||
|
@ -5654,6 +5666,10 @@ export interface Locale extends ILocale {
|
|||
* オフのとき
|
||||
*/
|
||||
"ifOff": string;
|
||||
/**
|
||||
* デバイス間でインストールしたテーマを同期
|
||||
*/
|
||||
"enableSyncThemesBetweenDevices": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* 送信者の名前を表示
|
||||
|
@ -8227,23 +8243,19 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"header": string;
|
||||
/**
|
||||
* サイドバーの背景
|
||||
* ナビゲーションバーの背景
|
||||
*/
|
||||
"navBg": string;
|
||||
/**
|
||||
* サイドバーの文字
|
||||
* ナビゲーションバーの文字
|
||||
*/
|
||||
"navFg": string;
|
||||
/**
|
||||
* サイドバー文字(ホバー)
|
||||
*/
|
||||
"navHoverFg": string;
|
||||
/**
|
||||
* サイドバー文字(アクティブ)
|
||||
* ナビゲーションバー文字(アクティブ)
|
||||
*/
|
||||
"navActive": string;
|
||||
/**
|
||||
* サイドバーのインジケーター
|
||||
* ナビゲーションバーのインジケーター
|
||||
*/
|
||||
"navIndicator": string;
|
||||
/**
|
||||
|
@ -8263,7 +8275,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"mentionMe": string;
|
||||
/**
|
||||
* Renote
|
||||
* リノート
|
||||
*/
|
||||
"renote": string;
|
||||
/**
|
||||
|
@ -8326,10 +8338,6 @@ export interface Locale extends ILocale {
|
|||
* ドライブフォルダーの背景
|
||||
*/
|
||||
"driveFolderBg": string;
|
||||
/**
|
||||
* 壁紙のオーバーレイ
|
||||
*/
|
||||
"wallpaperOverlay": string;
|
||||
/**
|
||||
* バッジ
|
||||
*/
|
||||
|
@ -8338,14 +8346,6 @@ export interface Locale extends ILocale {
|
|||
* チャットの背景
|
||||
*/
|
||||
"messageBg": string;
|
||||
/**
|
||||
* アクセント (暗め)
|
||||
*/
|
||||
"accentDarken": string;
|
||||
/**
|
||||
* アクセント (明るめ)
|
||||
*/
|
||||
"accentLighten": string;
|
||||
/**
|
||||
* 強調された文字
|
||||
*/
|
||||
|
@ -10069,6 +10069,18 @@ export interface Locale extends ILocale {
|
|||
* カラムの寄せ
|
||||
*/
|
||||
"columnAlign": string;
|
||||
/**
|
||||
* カラム間のマージン
|
||||
*/
|
||||
"columnGap": string;
|
||||
/**
|
||||
* デッキメニューの位置
|
||||
*/
|
||||
"deckMenuPosition": string;
|
||||
/**
|
||||
* ナビゲーションバーの位置
|
||||
*/
|
||||
"navbarPosition": string;
|
||||
/**
|
||||
* カラムを追加
|
||||
*/
|
||||
|
@ -10122,7 +10134,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"introduction": string;
|
||||
/**
|
||||
* 画面の右にある + を押して、いつでもカラムを追加できます。
|
||||
* カラムを追加するには、画面の + をクリックします。
|
||||
*/
|
||||
"introduction2": string;
|
||||
/**
|
||||
|
|
|
@ -1337,6 +1337,9 @@ chat: "チャット"
|
|||
migrateOldSettings: "旧設定情報を移行"
|
||||
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
|
||||
compress: "圧縮"
|
||||
right: "右"
|
||||
bottom: "下"
|
||||
top: "上"
|
||||
|
||||
_chat:
|
||||
noMessagesYet: "まだメッセージはありません"
|
||||
|
@ -1372,7 +1375,7 @@ _chat:
|
|||
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
|
||||
chatWithThisUser: "チャットする"
|
||||
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromFollowing: "このユーザーはフォローしているユーザーからのみチャットを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。"
|
||||
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
|
||||
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
|
||||
chatAllowedUsers: "チャットを許可する相手"
|
||||
|
@ -1414,6 +1417,7 @@ _settings:
|
|||
showNavbarSubButtons: "ナビゲーションバーに副ボタンを表示"
|
||||
ifOn: "オンのとき"
|
||||
ifOff: "オフのとき"
|
||||
enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期"
|
||||
|
||||
_chat:
|
||||
showSenderName: "送信者の名前を表示"
|
||||
|
@ -2159,16 +2163,15 @@ _theme:
|
|||
panel: "パネル"
|
||||
shadow: "影"
|
||||
header: "ヘッダー"
|
||||
navBg: "サイドバーの背景"
|
||||
navFg: "サイドバーの文字"
|
||||
navHoverFg: "サイドバー文字(ホバー)"
|
||||
navActive: "サイドバー文字(アクティブ)"
|
||||
navIndicator: "サイドバーのインジケーター"
|
||||
navBg: "ナビゲーションバーの背景"
|
||||
navFg: "ナビゲーションバーの文字"
|
||||
navActive: "ナビゲーションバー文字(アクティブ)"
|
||||
navIndicator: "ナビゲーションバーのインジケーター"
|
||||
link: "リンク"
|
||||
hashtag: "ハッシュタグ"
|
||||
mention: "メンション"
|
||||
mentionMe: "あなた宛てメンション"
|
||||
renote: "Renote"
|
||||
renote: "リノート"
|
||||
modalBg: "モーダルの背景"
|
||||
divider: "分割線"
|
||||
scrollbarHandle: "スクロールバーの取っ手"
|
||||
|
@ -2184,11 +2187,8 @@ _theme:
|
|||
buttonHoverBg: "ボタンの背景 (ホバー)"
|
||||
inputBorder: "入力ボックスの縁取り"
|
||||
driveFolderBg: "ドライブフォルダーの背景"
|
||||
wallpaperOverlay: "壁紙のオーバーレイ"
|
||||
badge: "バッジ"
|
||||
messageBg: "チャットの背景"
|
||||
accentDarken: "アクセント (暗め)"
|
||||
accentLighten: "アクセント (明るめ)"
|
||||
fgHighlighted: "強調された文字"
|
||||
|
||||
_sfx:
|
||||
|
@ -2663,6 +2663,9 @@ _notification:
|
|||
_deck:
|
||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||
columnAlign: "カラムの寄せ"
|
||||
columnGap: "カラム間のマージン"
|
||||
deckMenuPosition: "デッキメニューの位置"
|
||||
navbarPosition: "ナビゲーションバーの位置"
|
||||
addColumn: "カラムを追加"
|
||||
newNoteNotificationSettings: "新着ノート通知の設定"
|
||||
configureColumn: "カラムの設定"
|
||||
|
@ -2676,7 +2679,7 @@ _deck:
|
|||
newProfile: "新規プロファイル"
|
||||
deleteProfile: "プロファイルを削除"
|
||||
introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
|
||||
introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
|
||||
introduction2: "カラムを追加するには、画面の + をクリックします。"
|
||||
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"
|
||||
useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
|
||||
usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"
|
||||
|
|
|
@ -1336,6 +1336,9 @@ chat: "聊天"
|
|||
migrateOldSettings: "迁移旧设置信息"
|
||||
migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。"
|
||||
compress: "压缩"
|
||||
right: "右"
|
||||
bottom: "下"
|
||||
top: "上"
|
||||
_chat:
|
||||
noMessagesYet: "还没有消息"
|
||||
newMessage: "新消息"
|
||||
|
@ -2593,6 +2596,9 @@ _notification:
|
|||
_deck:
|
||||
alwaysShowMainColumn: "总是显示主列"
|
||||
columnAlign: "列对齐"
|
||||
columnGap: "列间距"
|
||||
deckMenuPosition: "Deck 菜单位置"
|
||||
navbarPosition: "导航栏位置"
|
||||
addColumn: "添加列"
|
||||
newNoteNotificationSettings: "新帖子通知设定"
|
||||
configureColumn: "列设置"
|
||||
|
@ -2606,7 +2612,7 @@ _deck:
|
|||
newProfile: "新建配置文件"
|
||||
deleteProfile: "删除配置文件"
|
||||
introduction: "将各列进行组合以创建您自己的界面!"
|
||||
introduction2: "您可以随时通过屏幕右侧的 + 来添加列"
|
||||
introduction2: "可以随时通过屏幕右侧的 + 来添加列"
|
||||
widgetsIntroduction: "从列菜单中,选择“小工具编辑”来添加小工具"
|
||||
useSimpleUiForNonRootPages: "用简易UI表示非根页面"
|
||||
usedAsMinWidthWhenFlexible: "「自适应宽度」被启用的时候,这就是最小的宽度"
|
||||
|
|
|
@ -1336,6 +1336,9 @@ chat: "聊天"
|
|||
migrateOldSettings: "遷移舊設定資訊"
|
||||
migrateOldSettings_description: "通常情況下,這會自動進行,但若因某些原因未能順利遷移,您可以手動觸發遷移處理。請注意,當前的設定資訊將會被覆寫。"
|
||||
compress: "壓縮"
|
||||
right: "右"
|
||||
bottom: "下"
|
||||
top: "上"
|
||||
_chat:
|
||||
noMessagesYet: "尚無訊息"
|
||||
newMessage: "新訊息"
|
||||
|
@ -2593,6 +2596,9 @@ _notification:
|
|||
_deck:
|
||||
alwaysShowMainColumn: "總是顯示主欄"
|
||||
columnAlign: "對齊欄位"
|
||||
columnGap: "欄與欄之間的邊距"
|
||||
deckMenuPosition: "多欄模式的選單位置"
|
||||
navbarPosition: "導覽列位置"
|
||||
addColumn: "新增欄位"
|
||||
newNoteNotificationSettings: "新貼文通知的設定"
|
||||
configureColumn: "欄位的設定"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.3.2-beta.18",
|
||||
"version": "2025.4.0-beta.1",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -186,6 +186,7 @@
|
|||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@nestjs/platform-express": "10.4.15",
|
||||
"@sentry/vue": "9.8.0",
|
||||
"@simplewebauthn/types": "12.0.0",
|
||||
"@swc/jest": "0.2.37",
|
||||
"@types/accepts": "1.3.7",
|
||||
|
|
|
@ -7,7 +7,8 @@ import * as fs from 'node:fs';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import type * as Sentry from '@sentry/node';
|
||||
import type * as SentryVue from '@sentry/vue';
|
||||
import type { RedisOptions } from 'ioredis';
|
||||
|
||||
type RedisOptionsSource = Partial<RedisOptions> & {
|
||||
|
@ -62,7 +63,12 @@ type Source = {
|
|||
scope?: 'local' | 'global' | string[];
|
||||
};
|
||||
sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
|
||||
sentryForFrontend?: { options: Partial<Sentry.NodeOptions> };
|
||||
sentryForFrontend?: {
|
||||
options: Partial<SentryVue.BrowserOptions> & { dsn: string };
|
||||
vueIntegration?: SentryVue.VueIntegrationOptions | null;
|
||||
browserTracingIntegration?: Parameters<typeof SentryVue.browserTracingIntegration>[0] | null;
|
||||
replayIntegration?: Parameters<typeof SentryVue.replayIntegration>[0] | null;
|
||||
};
|
||||
|
||||
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
|
||||
|
||||
|
@ -198,7 +204,12 @@ export type Config = {
|
|||
redisForTimelines: RedisOptions & RedisOptionsSource;
|
||||
redisForReactions: RedisOptions & RedisOptionsSource;
|
||||
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
|
||||
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
|
||||
sentryForFrontend: {
|
||||
options: Partial<SentryVue.BrowserOptions> & { dsn: string };
|
||||
vueIntegration?: SentryVue.VueIntegrationOptions | null;
|
||||
browserTracingIntegration?: Parameters<typeof SentryVue.browserTracingIntegration>[0] | null;
|
||||
replayIntegration?: Parameters<typeof SentryVue.replayIntegration>[0] | null;
|
||||
} | undefined;
|
||||
perChannelMaxNoteCacheCount: number;
|
||||
perUserNotificationsMaxCount: number;
|
||||
deactivateAntennaThreshold: number;
|
||||
|
|
|
@ -7,13 +7,13 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import { ulid } from 'ulid';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { genAid, isSafeAidT, parseAid } from '@/misc/id/aid.js';
|
||||
import { genAidx, isSafeAidxT, parseAidx } from '@/misc/id/aidx.js';
|
||||
import { genMeid, isSafeMeidT, parseMeid } from '@/misc/id/meid.js';
|
||||
import { genMeidg, isSafeMeidgT, parseMeidg } from '@/misc/id/meidg.js';
|
||||
import { genObjectId, isSafeObjectIdT, parseObjectId } from '@/misc/id/object-id.js';
|
||||
import { genAid, isSafeAidT, parseAid, parseAidFull } from '@/misc/id/aid.js';
|
||||
import { genAidx, isSafeAidxT, parseAidx, parseAidxFull } from '@/misc/id/aidx.js';
|
||||
import { genMeid, isSafeMeidT, parseMeid, parseMeidFull } from '@/misc/id/meid.js';
|
||||
import { genMeidg, isSafeMeidgT, parseMeidg, parseMeidgFull } from '@/misc/id/meidg.js';
|
||||
import { genObjectId, isSafeObjectIdT, parseObjectId, parseObjectIdFull } from '@/misc/id/object-id.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { parseUlid } from '@/misc/id/ulid.js';
|
||||
import { parseUlid, parseUlidFull } from '@/misc/id/ulid.js';
|
||||
|
||||
@Injectable()
|
||||
export class IdService {
|
||||
|
@ -70,4 +70,18 @@ export class IdService {
|
|||
default: throw new Error('unrecognized id generation method');
|
||||
}
|
||||
}
|
||||
|
||||
// Note: additional is at most 64 bits
|
||||
@bindThis
|
||||
public parseFull(id: string): { date: number; additional: bigint; } {
|
||||
switch (this.method) {
|
||||
case 'aid': return parseAidFull(id);
|
||||
case 'aidx': return parseAidxFull(id);
|
||||
case 'objectid': return parseObjectIdFull(id);
|
||||
case 'meid': return parseMeidFull(id);
|
||||
case 'meidg': return parseMeidgFull(id);
|
||||
case 'ulid': return parseUlidFull(id);
|
||||
default: throw new Error('unrecognized id generation method');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { setTimeout } from 'node:timers/promises';
|
|||
import * as Redis from 'ioredis';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { ReplyError } from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
|
@ -19,7 +20,7 @@ import { IdService } from '@/core/IdService.js';
|
|||
import { CacheService } from '@/core/CacheService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
import type { FilterUnionByProperty } from '@/types.js';
|
||||
import { FilterUnionByProperty, groupedNotificationTypes, obsoleteNotificationTypes } from '@/types.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -145,21 +146,36 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
const notification = {
|
||||
const createdAt = new Date();
|
||||
let notification: FilterUnionByProperty<MiNotification, 'type', T>;
|
||||
let redisId: string;
|
||||
|
||||
do {
|
||||
notification = {
|
||||
id: this.idService.gen(),
|
||||
createdAt: new Date(),
|
||||
createdAt,
|
||||
type: type,
|
||||
...(notifierId ? {
|
||||
notifierId,
|
||||
} : {}),
|
||||
...data,
|
||||
} as any as FilterUnionByProperty<MiNotification, 'type', T>;
|
||||
} as unknown as FilterUnionByProperty<MiNotification, 'type', T>;
|
||||
|
||||
const redisIdPromise = this.redisClient.xadd(
|
||||
try {
|
||||
redisId = (await this.redisClient.xadd(
|
||||
`notificationTimeline:${notifieeId}`,
|
||||
'MAXLEN', '~', this.config.perUserNotificationsMaxCount.toString(),
|
||||
'*',
|
||||
'data', JSON.stringify(notification));
|
||||
this.toXListId(notification.id),
|
||||
'data', JSON.stringify(notification)))!;
|
||||
} catch (e) {
|
||||
// The ID specified in XADD is equal or smaller than the target stream top item で失敗することがあるのでリトライ
|
||||
if (e instanceof ReplyError) continue;
|
||||
throw e;
|
||||
}
|
||||
|
||||
break;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
} while (true);
|
||||
|
||||
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
|
||||
|
||||
|
@ -173,7 +189,7 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
const interval = notification.type === 'test' ? 0 : 2000;
|
||||
setTimeout(interval, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
|
||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
|
||||
if (latestReadNotificationId && (latestReadNotificationId >= (await redisIdPromise)!)) return;
|
||||
if (latestReadNotificationId && (latestReadNotificationId >= redisId)) return;
|
||||
|
||||
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||
|
@ -228,6 +244,79 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
this.#shutdownController.abort();
|
||||
}
|
||||
|
||||
private toXListId(id: string): string {
|
||||
const { date, additional } = this.idService.parseFull(id);
|
||||
return date.toString() + '-' + additional.toString();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getNotifications(
|
||||
userId: MiUser['id'],
|
||||
{
|
||||
sinceId,
|
||||
untilId,
|
||||
limit = 20,
|
||||
includeTypes,
|
||||
excludeTypes,
|
||||
}: {
|
||||
sinceId?: string,
|
||||
untilId?: string,
|
||||
limit?: number,
|
||||
// any extra types are allowed, those are no-op
|
||||
includeTypes?: (MiNotification['type'] | string)[],
|
||||
excludeTypes?: (MiNotification['type'] | string)[],
|
||||
},
|
||||
): Promise<MiNotification[]> {
|
||||
let sinceTime = sinceId ? this.toXListId(sinceId) : null;
|
||||
let untilTime = untilId ? this.toXListId(untilId) : null;
|
||||
|
||||
let notifications: MiNotification[];
|
||||
for (;;) {
|
||||
let notificationsRes: [id: string, fields: string[]][];
|
||||
|
||||
// sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
|
||||
if (sinceTime && !untilTime) {
|
||||
notificationsRes = await this.redisClient.xrange(
|
||||
`notificationTimeline:${userId}`,
|
||||
'(' + sinceTime,
|
||||
'+',
|
||||
'COUNT', limit);
|
||||
} else {
|
||||
notificationsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${userId}`,
|
||||
untilTime ? '(' + untilTime : '+',
|
||||
sinceTime ? '(' + sinceTime : '-',
|
||||
'COUNT', limit);
|
||||
}
|
||||
|
||||
if (notificationsRes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[];
|
||||
|
||||
if (includeTypes && includeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
|
||||
} else if (excludeTypes && excludeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
|
||||
}
|
||||
|
||||
if (notifications.length !== 0) {
|
||||
// 通知が1件以上ある場合は返す
|
||||
break;
|
||||
}
|
||||
|
||||
// フィルタしたことで通知が0件になった場合、次のページを取得する
|
||||
if (sinceId && !untilId) {
|
||||
sinceTime = notificationsRes[notificationsRes.length - 1][0];
|
||||
} else {
|
||||
untilTime = notificationsRes[notificationsRes.length - 1][0];
|
||||
}
|
||||
}
|
||||
|
||||
return notifications;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
|
|
|
@ -127,6 +127,7 @@ export class MetaEntityService {
|
|||
|
||||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||
|
||||
sentryForFrontend: this.config.sentryForFrontend ?? null,
|
||||
mediaProxy: this.config.mediaProxy,
|
||||
enableUrlPreview: instance.urlPreviewEnabled,
|
||||
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
function parseBigIntChunked(str: string, base: number, chunkSize: number, powerOfChunkSize: bigint): bigint {
|
||||
const chunks = [];
|
||||
while (str.length > 0) {
|
||||
chunks.unshift(str.slice(-chunkSize));
|
||||
str = str.slice(0, -chunkSize);
|
||||
}
|
||||
let result = 0n;
|
||||
for (const chunk of chunks) {
|
||||
result *= powerOfChunkSize;
|
||||
const int = parseInt(chunk, base);
|
||||
if (Number.isNaN(int)) {
|
||||
throw new Error('Invalid base36 string');
|
||||
}
|
||||
result += BigInt(int);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseBigInt36(str: string): bigint {
|
||||
// log_36(Number.MAX_SAFE_INTEGER) => 10.251599391715352
|
||||
// so we process 10 chars at once
|
||||
return parseBigIntChunked(str, 36, 10, 36n ** 10n);
|
||||
}
|
||||
|
||||
export function parseBigInt16(str: string): bigint {
|
||||
// log_16(Number.MAX_SAFE_INTEGER) => 13.25
|
||||
// so we process 13 chars at once
|
||||
return parseBigIntChunked(str, 16, 13, 16n ** 13n);
|
||||
}
|
||||
|
||||
export function parseBigInt32(str: string): bigint {
|
||||
// log_32(Number.MAX_SAFE_INTEGER) => 10.6
|
||||
// so we process 10 chars at once
|
||||
return parseBigIntChunked(str, 32, 10, 32n ** 10n);
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列]
|
||||
|
||||
import * as crypto from 'node:crypto';
|
||||
import { parseBigInt36 } from '@/misc/bigint.js';
|
||||
|
||||
export const aidRegExp = /^[0-9a-z]{10}$/;
|
||||
|
||||
|
@ -35,6 +36,12 @@ export function parseAid(id: string): { date: Date; } {
|
|||
return { date: new Date(time) };
|
||||
}
|
||||
|
||||
export function parseAidFull(id: string): { date: number; additional: bigint; } {
|
||||
const date = parseInt(id.slice(0, 8), 36) + TIME2000;
|
||||
const additional = parseBigInt36(id.slice(8, 10));
|
||||
return { date, additional };
|
||||
}
|
||||
|
||||
export function isSafeAidT(t: number): boolean {
|
||||
return t > TIME2000;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
// https://misskey.m544.net/notes/71899acdcc9859ec5708ac24
|
||||
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { parseBigInt36 } from '@/misc/bigint.js';
|
||||
|
||||
export const aidxRegExp = /^[0-9a-z]{16}$/;
|
||||
|
||||
|
@ -16,6 +17,7 @@ const TIME2000 = 946684800000;
|
|||
const TIME_LENGTH = 8;
|
||||
const NODE_LENGTH = 4;
|
||||
const NOISE_LENGTH = 4;
|
||||
const AIDX_LENGTH = TIME_LENGTH + NODE_LENGTH + NOISE_LENGTH;
|
||||
|
||||
const nodeId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', NODE_LENGTH)();
|
||||
let counter = 0;
|
||||
|
@ -42,6 +44,12 @@ export function parseAidx(id: string): { date: Date; } {
|
|||
return { date: new Date(time) };
|
||||
}
|
||||
|
||||
export function parseAidxFull(id: string): { date: number; additional: bigint; } {
|
||||
const date = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000;
|
||||
const additional = parseBigInt36(id.slice(TIME_LENGTH, AIDX_LENGTH));
|
||||
return { date, additional };
|
||||
}
|
||||
|
||||
export function isSafeAidxT(t: number): boolean {
|
||||
return t > TIME2000;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { parseBigInt16 } from '@/misc/bigint.js';
|
||||
|
||||
const CHARS = '0123456789abcdef';
|
||||
|
||||
// same as object-id
|
||||
|
@ -39,6 +41,13 @@ export function parseMeid(id: string): { date: Date; } {
|
|||
};
|
||||
}
|
||||
|
||||
export function parseMeidFull(id: string): { date: number; additional: bigint; } {
|
||||
return {
|
||||
date: parseInt(id.slice(0, 12), 16) - 0x800000000000,
|
||||
additional: parseBigInt16(id.slice(12, 24)),
|
||||
};
|
||||
}
|
||||
|
||||
export function isSafeMeidT(t: number): boolean {
|
||||
return t > 0;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { parseBigInt16 } from '@/misc/bigint.js';
|
||||
|
||||
const CHARS = '0123456789abcdef';
|
||||
|
||||
// 4bit Fixed hex value 'g'
|
||||
|
@ -39,6 +41,13 @@ export function parseMeidg(id: string): { date: Date; } {
|
|||
};
|
||||
}
|
||||
|
||||
export function parseMeidgFull(id: string): { date: number; additional: bigint; } {
|
||||
return {
|
||||
date: parseInt(id.slice(1, 12), 16),
|
||||
additional: parseBigInt16(id.slice(12, 24)),
|
||||
};
|
||||
}
|
||||
|
||||
export function isSafeMeidgT(t: number): boolean {
|
||||
return t > 0;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { parseBigInt16 } from '@/misc/bigint.js';
|
||||
|
||||
const CHARS = '0123456789abcdef';
|
||||
|
||||
// same as meid
|
||||
|
@ -39,6 +41,13 @@ export function parseObjectId(id: string): { date: Date; } {
|
|||
};
|
||||
}
|
||||
|
||||
export function parseObjectIdFull(id: string): { date: number; additional: bigint; } {
|
||||
return {
|
||||
date: parseInt(id.slice(0, 8), 16) * 1000,
|
||||
additional: parseBigInt16(id.slice(8, 24)),
|
||||
};
|
||||
}
|
||||
|
||||
export function isSafeObjectIdT(t: number): boolean {
|
||||
return t > 0;
|
||||
}
|
||||
|
|
|
@ -5,15 +5,27 @@
|
|||
|
||||
// Crockford's Base32
|
||||
// https://github.com/ulid/spec#encoding
|
||||
import { parseBigInt32 } from '@/misc/bigint.js';
|
||||
|
||||
const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
||||
|
||||
export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/;
|
||||
|
||||
export function parseUlid(id: string): { date: Date; } {
|
||||
const timestamp = id.slice(0, 10);
|
||||
function parseBase32(timestamp: string) {
|
||||
let time = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let i = 0; i < timestamp.length; i++) {
|
||||
time = time * 32 + CHARS.indexOf(timestamp[i]);
|
||||
}
|
||||
return { date: new Date(time) };
|
||||
return time;
|
||||
}
|
||||
|
||||
export function parseUlid(id: string): { date: Date; } {
|
||||
return { date: new Date(parseBase32(id.slice(0, 10))) };
|
||||
}
|
||||
|
||||
export function parseUlidFull(id: string): { date: number; additional: bigint; } {
|
||||
return {
|
||||
date: parseBase32(id.slice(0, 10)),
|
||||
additional: parseBigInt32(id.slice(10, 26)),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -211,6 +211,38 @@ export const packedMetaLiteSchema = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
sentryForFrontend: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
properties: {
|
||||
options: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
dsn: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
additionalProperties: true,
|
||||
},
|
||||
vueIntegration: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
additionalProperties: true,
|
||||
},
|
||||
browserTracingIntegration: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
additionalProperties: true,
|
||||
},
|
||||
replayIntegration: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
additionalProperties: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
mediaProxy: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
@ -7,7 +7,12 @@ import { In } from 'typeorm';
|
|||
import * as Redis from 'ioredis';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import { obsoleteNotificationTypes, groupedNotificationTypes, FilterUnionByProperty } from '@/types.js';
|
||||
import {
|
||||
obsoleteNotificationTypes,
|
||||
groupedNotificationTypes,
|
||||
FilterUnionByProperty,
|
||||
notificationTypes,
|
||||
} from '@/types.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
|
@ -47,10 +52,10 @@ export const paramDef = {
|
|||
markAsRead: { type: 'boolean', default: true },
|
||||
// 後方互換のため、廃止された通知タイプも受け付ける
|
||||
includeTypes: { type: 'array', items: {
|
||||
type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes],
|
||||
type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
|
||||
} },
|
||||
excludeTypes: { type: 'array', items: {
|
||||
type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes],
|
||||
type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
|
||||
} },
|
||||
},
|
||||
required: [],
|
||||
|
@ -74,31 +79,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
return [];
|
||||
}
|
||||
// excludeTypes に全指定されている場合はクエリしない
|
||||
if (groupedNotificationTypes.every(type => ps.excludeTypes?.includes(type))) {
|
||||
if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][];
|
||||
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][];
|
||||
|
||||
const limit = (ps.limit + EXTRA_LIMIT) + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||
const notificationsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${me.id}`,
|
||||
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
||||
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
|
||||
'COUNT', limit);
|
||||
|
||||
if (notificationsRes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[];
|
||||
|
||||
if (includeTypes && includeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
|
||||
} else if (excludeTypes && excludeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
|
||||
}
|
||||
const notifications = await this.notificationService.getNotifications(me.id, {
|
||||
sinceId: ps.sinceId,
|
||||
untilId: ps.untilId,
|
||||
limit: ps.limit,
|
||||
includeTypes,
|
||||
excludeTypes,
|
||||
});
|
||||
|
||||
if (notifications.length === 0) {
|
||||
return [];
|
||||
|
|
|
@ -82,52 +82,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||
|
||||
let sinceTime = ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime().toString() : null;
|
||||
let untilTime = ps.untilId ? this.idService.parse(ps.untilId).date.getTime().toString() : null;
|
||||
|
||||
let notifications: MiNotification[];
|
||||
for (;;) {
|
||||
let notificationsRes: [id: string, fields: string[]][];
|
||||
|
||||
// sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
|
||||
if (sinceTime && !untilTime) {
|
||||
notificationsRes = await this.redisClient.xrange(
|
||||
`notificationTimeline:${me.id}`,
|
||||
'(' + sinceTime,
|
||||
'+',
|
||||
'COUNT', ps.limit);
|
||||
} else {
|
||||
notificationsRes = await this.redisClient.xrevrange(
|
||||
`notificationTimeline:${me.id}`,
|
||||
untilTime ? '(' + untilTime : '+',
|
||||
sinceTime ? '(' + sinceTime : '-',
|
||||
'COUNT', ps.limit);
|
||||
}
|
||||
|
||||
if (notificationsRes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[];
|
||||
|
||||
if (includeTypes && includeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
|
||||
} else if (excludeTypes && excludeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
|
||||
}
|
||||
|
||||
if (notifications.length !== 0) {
|
||||
// 通知が1件以上ある場合は返す
|
||||
break;
|
||||
}
|
||||
|
||||
// フィルタしたことで通知が0件になった場合、次のページを取得する
|
||||
if (ps.sinceId && !ps.untilId) {
|
||||
sinceTime = notificationsRes[notificationsRes.length - 1][0];
|
||||
} else {
|
||||
untilTime = notificationsRes[notificationsRes.length - 1][0];
|
||||
}
|
||||
}
|
||||
const notifications = await this.notificationService.getNotifications(me.id, {
|
||||
sinceId: ps.sinceId,
|
||||
untilId: ps.untilId,
|
||||
limit: ps.limit,
|
||||
includeTypes,
|
||||
excludeTypes,
|
||||
});
|
||||
|
||||
// Mark all as read
|
||||
if (ps.markAsRead) {
|
||||
|
|
|
@ -127,11 +127,6 @@
|
|||
document.documentElement.classList.add('useSystemFont');
|
||||
}
|
||||
|
||||
const wallpaper = localStorage.getItem('wallpaper');
|
||||
if (wallpaper) {
|
||||
document.documentElement.style.backgroundImage = `url(${wallpaper})`;
|
||||
}
|
||||
|
||||
const customCss = localStorage.getItem('customCss');
|
||||
if (customCss && customCss.length > 0) {
|
||||
const style = document.createElement('style');
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"typescript": "5.8.2",
|
||||
"uuid": "11.1.0",
|
||||
"json5": "2.2.3",
|
||||
"vite": "6.2.3",
|
||||
"vite": "6.2.4",
|
||||
"vue": "3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -95,7 +95,7 @@ async function onclick(ev: MouseEvent) {
|
|||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background-color: var(--MI_THEME-fg);
|
||||
color: var(--MI_THEME-accentLighten);
|
||||
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
font-size: 12px;
|
||||
opacity: .5;
|
||||
padding: 5px 8px;
|
||||
|
@ -153,7 +153,7 @@ html[data-color-scheme=light] .visible {
|
|||
/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
|
||||
background-color: black;
|
||||
border-radius: 6px;
|
||||
color: var(--MI_THEME-accentLighten);
|
||||
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 0.8em;
|
||||
|
|
|
@ -278,7 +278,7 @@ rt {
|
|||
}
|
||||
|
||||
._acrylic {
|
||||
background: var(--MI_THEME-acrylicPanel);
|
||||
background: color(from var(--MI_THEME-panel) srgb r g b / 0.5);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
}
|
||||
|
|
|
@ -10,13 +10,10 @@
|
|||
|
||||
props: {
|
||||
accent: '#86b300',
|
||||
accentDarken: ':darken<10<@accent',
|
||||
accentLighten: ':lighten<10<@accent',
|
||||
accentedBg: ':alpha<0.15<@accent',
|
||||
love: '#dd2e44',
|
||||
focus: ':alpha<0.3<@accent',
|
||||
bg: '#000',
|
||||
acrylicBg: ':alpha<0.5<@bg',
|
||||
fg: '#dadada',
|
||||
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||
fgTransparent: ':alpha<0.5<@fg',
|
||||
|
@ -31,14 +28,12 @@
|
|||
panelHeaderFg: '@fg',
|
||||
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||
panelBorder: '" solid 1px var(--MI_THEME-divider)',
|
||||
acrylicPanel: ':alpha<0.5<@panel',
|
||||
windowHeader: ':alpha<0.85<@panel',
|
||||
popup: ':lighten<3<@panel',
|
||||
shadow: 'rgba(0, 0, 0, 0.3)',
|
||||
header: ':alpha<0.7<@panel',
|
||||
navBg: '@panel',
|
||||
navFg: '@fg',
|
||||
navHoverFg: ':lighten<17<@fg',
|
||||
navActive: '@accent',
|
||||
navIndicator: '@indicator',
|
||||
link: '#44a4c1',
|
||||
|
@ -68,7 +63,6 @@
|
|||
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||
badge: '#31b1ce',
|
||||
messageBg: '@bg',
|
||||
success: '#86b300',
|
||||
|
|
|
@ -10,13 +10,10 @@
|
|||
|
||||
props: {
|
||||
accent: '#86b300',
|
||||
accentDarken: ':darken<10<@accent',
|
||||
accentLighten: ':lighten<10<@accent',
|
||||
accentedBg: ':alpha<0.15<@accent',
|
||||
love: '#dd2e44',
|
||||
focus: ':alpha<0.3<@accent',
|
||||
bg: '#fff',
|
||||
acrylicBg: ':alpha<0.5<@bg',
|
||||
fg: '#5f5f5f',
|
||||
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||
fgTransparent: ':alpha<0.5<@fg',
|
||||
|
@ -31,14 +28,12 @@
|
|||
panelHeaderFg: '@fg',
|
||||
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||
panelBorder: '" solid 1px var(--MI_THEME-divider)',
|
||||
acrylicPanel: ':alpha<0.5<@panel',
|
||||
windowHeader: ':alpha<0.85<@panel',
|
||||
popup: ':lighten<3<@panel',
|
||||
shadow: 'rgba(0, 0, 0, 0.1)',
|
||||
header: ':alpha<0.7<@panel',
|
||||
navBg: '@panel',
|
||||
navFg: '@fg',
|
||||
navHoverFg: ':darken<17<@fg',
|
||||
navActive: '@accent',
|
||||
navIndicator: '@indicator',
|
||||
link: '#44a4c1',
|
||||
|
@ -68,7 +63,6 @@
|
|||
inputBorder: 'rgba(0, 0, 0, 0.1)',
|
||||
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
|
||||
badge: '#31b1ce',
|
||||
messageBg: '@bg',
|
||||
success: '#86b300',
|
||||
|
|
|
@ -25,22 +25,17 @@
|
|||
mention: '#ffd152',
|
||||
modalBg: 'rgba(0, 0, 0, 0.5)',
|
||||
success: '#86b300',
|
||||
acrylicBg: ':alpha<0.5<@bg',
|
||||
indicator: '@accent',
|
||||
mentionMe: '#fb5d38',
|
||||
messageBg: '@bg',
|
||||
navActive: '@accent',
|
||||
infoWarnBg: '#42321c',
|
||||
infoWarnFg: '#ffbd3e',
|
||||
navHoverFg: ':lighten<17<@fg',
|
||||
dateLabelFg: '@fg',
|
||||
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||
panelBorder: '" solid 1px var(--MI_THEME-divider)',
|
||||
accentDarken: ':darken<10<@accent',
|
||||
acrylicPanel: ':alpha<0.5<@panel',
|
||||
navIndicator: '@accent',
|
||||
accentLighten: ':lighten<10<@accent',
|
||||
buttonGradateA: '@accent',
|
||||
buttonGradateB: ':hue<-20<@accent',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
|
@ -51,7 +46,6 @@
|
|||
fgOnWhite: '@accent',
|
||||
panelHighlight: ':lighten<3<@panel',
|
||||
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
|
||||
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
|
||||
},
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
modalBg: 'rgba(0, 0, 0, 0.5)',
|
||||
success: '#86b300',
|
||||
switchBg: 'rgba(255, 255, 255, 0.15)',
|
||||
acrylicBg: ':alpha<0.5<@bg',
|
||||
indicator: '@accent',
|
||||
mentionMe: '@mention',
|
||||
messageBg: '@bg',
|
||||
|
@ -43,15 +42,11 @@
|
|||
fgOnWhite: '@accent',
|
||||
infoWarnBg: '#42321c',
|
||||
infoWarnFg: '#ffbd3e',
|
||||
navHoverFg: ':lighten<17<@fg',
|
||||
codeBoolean: '#c59eff',
|
||||
dateLabelFg: '@fg',
|
||||
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||
panelBorder: '" solid 1px var(--MI_THEME-divider)',
|
||||
accentDarken: ':darken<10<@accent',
|
||||
acrylicPanel: ':alpha<0.5<@panel',
|
||||
navIndicator: '@indicator',
|
||||
accentLighten: ':lighten<10<@accent',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
fgHighlighted: ':lighten<3<@fg',
|
||||
fgTransparent: ':alpha<0.5<@fg',
|
||||
|
@ -63,7 +58,6 @@
|
|||
panelHighlight: ':lighten<3<@panel',
|
||||
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
|
||||
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
success: '#86b300',
|
||||
buttonBg: '#0000000d',
|
||||
switchBg: 'rgba(255, 255, 255, 0.15)',
|
||||
acrylicBg: ':alpha<0.5<@bg',
|
||||
indicator: '@accent',
|
||||
mentionMe: '@mention',
|
||||
messageBg: '@bg',
|
||||
|
@ -44,15 +43,11 @@
|
|||
fgOnWhite: '@accent',
|
||||
infoWarnBg: '#42321c',
|
||||
infoWarnFg: '#ffbd3e',
|
||||
navHoverFg: ':lighten<17<@fg',
|
||||
codeBoolean: '#c59eff',
|
||||
dateLabelFg: '@fg',
|
||||
inputBorder: 'rgba(255, 255, 255, 0.1)',
|
||||
panelBorder: '" solid 1px var(--MI_THEME-divider)',
|
||||
accentDarken: ':darken<10<@accent',
|
||||
acrylicPanel: ':alpha<0.5<@panel',
|
||||
navIndicator: '@indicator',
|
||||
accentLighten: ':lighten<10<@accent',
|
||||
buttonHoverBg: '#0000001a',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
fgHighlighted: ':lighten<3<@fg',
|
||||
|
@ -65,7 +60,6 @@
|
|||
panelHighlight: ':lighten<3<@panel',
|
||||
scrollbarHandle: '#74747433',
|
||||
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
|
||||
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
|
||||
scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)',
|
||||
|
|
|
@ -28,22 +28,17 @@
|
|||
mention: '@accent',
|
||||
modalBg: 'rgba(0, 0, 0, 0.3)',
|
||||
success: '#86b300',
|
||||
acrylicBg: ':alpha<0.5<@bg',
|
||||
indicator: '@accent',
|
||||
mentionMe: '@mention',
|
||||
messageBg: '@bg',
|
||||
navActive: '@accent',
|
||||
infoWarnBg: '#fff0db',
|
||||
infoWarnFg: '#8f6e31',
|
||||
navHoverFg: ':darken<17<@fg',
|
||||
dateLabelFg: '@fg',
|
||||
inputBorder: 'rgba(0, 0, 0, 0.1)',
|
||||
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
|
||||
panelBorder: '" solid 1px var(--MI_THEME-divider)',
|
||||
accentDarken: ':darken<10<@accent',
|
||||
acrylicPanel: ':alpha<0.5<@panel',
|
||||
navIndicator: '@accent',
|
||||
accentLighten: ':lighten<10<@accent',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
fgHighlighted: ':darken<3<@fg',
|
||||
fgTransparent: ':alpha<0.5<@fg',
|
||||
|
@ -53,7 +48,6 @@
|
|||
htmlThemeColor: '@bg',
|
||||
panelHighlight: ':darken<3<@panel',
|
||||
scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
|
||||
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
|
||||
fgTransparentWeak: ':alpha<0.75<@fg',
|
||||
panelHeaderDivider: '@divider',
|
||||
scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)',
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "6.0.2",
|
||||
"@rollup/pluginutils": "5.1.4",
|
||||
"@sentry/vue": "9.8.0",
|
||||
"@syuilo/aiscript": "0.19.0",
|
||||
"@tabler/icons-webfont": "3.31.0",
|
||||
"@twemoji/parser": "15.1.1",
|
||||
|
@ -74,7 +75,7 @@
|
|||
"typescript": "5.8.2",
|
||||
"uuid": "11.1.0",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "6.2.3",
|
||||
"vite": "6.2.4",
|
||||
"vue": "3.5.13",
|
||||
"vuedraggable": "next",
|
||||
"wanakana": "5.3.1"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { computed, watch, version as vueVersion } from 'vue';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { version, lang, updateLocale, locale } from '@@/js/config.js';
|
||||
import { version, lang, updateLocale, locale, apiUrl } from '@@/js/config.js';
|
||||
import defaultLightTheme from '@@/themes/l-light.json5';
|
||||
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
|
||||
import type { App } from 'vue';
|
||||
|
@ -291,6 +291,41 @@ export async function common(createVue: () => Promise<App<Element>>) {
|
|||
return root;
|
||||
})();
|
||||
|
||||
if (instance.sentryForFrontend) {
|
||||
const Sentry = await import('@sentry/vue');
|
||||
Sentry.init({
|
||||
app,
|
||||
integrations: [
|
||||
...(instance.sentryForFrontend.vueIntegration !== undefined ? [
|
||||
Sentry.vueIntegration(instance.sentryForFrontend.vueIntegration ?? undefined),
|
||||
] : []),
|
||||
...(instance.sentryForFrontend.browserTracingIntegration !== undefined ? [
|
||||
Sentry.browserTracingIntegration(instance.sentryForFrontend.browserTracingIntegration ?? undefined),
|
||||
] : []),
|
||||
...(instance.sentryForFrontend.replayIntegration !== undefined ? [
|
||||
Sentry.replayIntegration(instance.sentryForFrontend.replayIntegration ?? undefined),
|
||||
] : []),
|
||||
],
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled
|
||||
...(instance.sentryForFrontend.browserTracingIntegration !== undefined ? {
|
||||
tracePropagationTargets: [apiUrl],
|
||||
} : {}),
|
||||
|
||||
// Capture Replay for 10% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
...(instance.sentryForFrontend.replayIntegration !== undefined ? {
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
} : {}),
|
||||
|
||||
...instance.sentryForFrontend.options,
|
||||
});
|
||||
}
|
||||
|
||||
app.mount(rootEl);
|
||||
|
||||
// boot.jsのやつを解除
|
||||
|
|
|
@ -420,7 +420,7 @@ onBeforeUnmount(() => {
|
|||
}
|
||||
|
||||
&:active {
|
||||
background: var(--MI_THEME-accentDarken);
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,13 +103,13 @@ async function onClick() {
|
|||
background: var(--MI_THEME-accent);
|
||||
|
||||
&:hover {
|
||||
background: var(--MI_THEME-accentLighten);
|
||||
border-color: var(--MI_THEME-accentLighten);
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
border-color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--MI_THEME-accentDarken);
|
||||
border-color: var(--MI_THEME-accentDarken);
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
|
||||
border-color: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -151,11 +151,11 @@ function onDragend() {
|
|||
background: var(--MI_THEME-accent);
|
||||
|
||||
&:hover {
|
||||
background: var(--MI_THEME-accentLighten);
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--MI_THEME-accentDarken);
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
|
||||
}
|
||||
|
||||
> .label {
|
||||
|
|
|
@ -239,7 +239,7 @@ onMounted(() => {
|
|||
bottom: var(--MI-stickyBottom, 0px);
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
background-size: auto auto;
|
||||
|
|
|
@ -211,13 +211,13 @@ onBeforeUnmount(() => {
|
|||
background: var(--MI_THEME-accent);
|
||||
|
||||
&:hover {
|
||||
background: var(--MI_THEME-accentLighten);
|
||||
border-color: var(--MI_THEME-accentLighten);
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
border-color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--MI_THEME-accentDarken);
|
||||
border-color: var(--MI_THEME-accentDarken);
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
|
||||
border-color: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ withDefaults(defineProps<{
|
|||
padding-top: calc(var(--fukidashi-radius) * .13);
|
||||
|
||||
&.accented {
|
||||
--fukidashi-bg: var(--MI_THEME-accent);
|
||||
--fukidashi-bg: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-panel) 85%);
|
||||
}
|
||||
|
||||
&.shadow {
|
||||
|
@ -87,6 +87,12 @@ withDefaults(defineProps<{
|
|||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
@container (max-width: 450px) {
|
||||
.content {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
|
@ -220,7 +220,7 @@ function showMenu(ev: MouseEvent) {
|
|||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background-color: var(--MI_THEME-fg);
|
||||
color: var(--MI_THEME-accentLighten);
|
||||
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
font-size: 12px;
|
||||
opacity: .5;
|
||||
padding: 5px 8px;
|
||||
|
@ -294,7 +294,7 @@ html[data-color-scheme=light] .visible {
|
|||
/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
|
||||
background-color: black;
|
||||
border-radius: 6px;
|
||||
color: var(--MI_THEME-accentLighten);
|
||||
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 0.8em;
|
||||
|
|
|
@ -553,7 +553,7 @@ onDeactivated(() => {
|
|||
/* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
|
||||
background-color: black;
|
||||
border-radius: 6px;
|
||||
color: var(--MI_THEME-accentLighten);
|
||||
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 0.8em;
|
||||
|
@ -565,7 +565,7 @@ onDeactivated(() => {
|
|||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background-color: var(--MI_THEME-fg);
|
||||
color: var(--MI_THEME-accentLighten);
|
||||
color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
font-size: 12px;
|
||||
opacity: .5;
|
||||
padding: 5px 8px;
|
||||
|
|
|
@ -23,10 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
tag="div"
|
||||
>
|
||||
<template v-for="(note, i) in notes" :key="note.id">
|
||||
<div v-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]">
|
||||
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
<div v-if="note._shouldInsertAd_" :class="$style.ad">
|
||||
<div :class="$style.ad">
|
||||
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkNote v-else :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
|
|
|
@ -159,12 +159,13 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
|||
|
||||
const onDrag = (ev: MouseEvent | TouchEvent) => {
|
||||
ev.preventDefault();
|
||||
let beforeValue = finalValue.value;
|
||||
const containerRect = containerEl.value!.getBoundingClientRect();
|
||||
const pointerX = 'touches' in ev && ev.touches.length > 0 ? ev.touches[0].clientX : 'clientX' in ev ? ev.clientX : 0;
|
||||
const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth / 2));
|
||||
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth)));
|
||||
|
||||
if (props.continuousUpdate) {
|
||||
if (props.continuousUpdate && beforeValue !== finalValue.value) {
|
||||
emit('update:modelValue', finalValue.value);
|
||||
}
|
||||
};
|
||||
|
@ -286,7 +287,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
|||
border-radius: 999px;
|
||||
|
||||
&:hover {
|
||||
background: var(--MI_THEME-accentLighten);
|
||||
background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ async function done() {
|
|||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
}
|
||||
|
|
|
@ -144,7 +144,7 @@ fetchRoles();
|
|||
}
|
||||
|
||||
.roleItemArea {
|
||||
background-color: var(--MI_THEME-acrylicBg);
|
||||
background-color: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
border-radius: var(--MI-radius);
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
|
|
|
@ -84,7 +84,7 @@ function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true })
|
|||
align-items: center;
|
||||
font-weight: bold;
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
|
|
@ -280,7 +280,7 @@ onMounted(async () => {
|
|||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
}
|
||||
|
|
|
@ -4,25 +4,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<svg
|
||||
version="1.1"
|
||||
viewBox="0 0 203.2 152.4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<svg viewBox="0 0 200 150">
|
||||
<g fill-rule="evenodd">
|
||||
<rect width="203.2" height="152.4" :fill="themeVariables.bg" stroke-width=".26458" />
|
||||
<rect width="65.498" height="152.4" :fill="themeVariables.panel" stroke-width=".26458" />
|
||||
<rect x="65.498" width="137.7" height="40.892" :fill="themeVariables.acrylicBg" stroke-width=".265" />
|
||||
<rect width="200" height="150" :fill="themeVariables.bg"/>
|
||||
<rect width="64" height="150" :fill="themeVariables.navBg"/>
|
||||
<rect x="64" width="136" height="41" :fill="themeVariables.bg"/>
|
||||
<path transform="scale(.26458)" d="m439.77 247.19c-43.673 0-78.832 35.157-78.832 78.83v249.98h407.06v-328.81z" :fill="themeVariables.panel"/>
|
||||
</g>
|
||||
<circle cx="32.749" cy="83.054" r="21.132" :fill="themeVariables.accentedBg" stroke-dasharray="0.319256, 0.319256" stroke-width=".15963" style="paint-order:stroke fill markers" />
|
||||
<circle cx="136.67" cy="106.76" r="23.876" :fill="themeVariables.fg" fill-opacity="0.5" stroke-dasharray="0.352425, 0.352425" stroke-width=".17621" style="paint-order:stroke fill markers" />
|
||||
<g :fill="themeVariables.fg" fill-rule="evenodd" stroke-width=".26458">
|
||||
<rect x="171.27" y="87.815" width="48.576" height="6.8747" ry="3.4373"/>
|
||||
<rect x="171.27" y="105.09" width="48.576" height="6.875" ry="3.4375"/>
|
||||
<rect x="171.27" y="121.28" width="48.576" height="6.875" ry="3.4375"/>
|
||||
<rect x="171.27" y="137.47" width="48.576" height="6.875" ry="3.4375"/>
|
||||
<circle cx="32" cy="83" r="21" :fill="themeVariables.accentedBg"/>
|
||||
<g>
|
||||
<rect x="120" y="88" width="40" height="6" ry="3" :fill="themeVariables.fg"/>
|
||||
<rect x="170" y="88" width="20" height="6" ry="3" :fill="themeVariables.mention"/>
|
||||
<rect x="120" y="108" width="20" height="6" ry="3" :fill="themeVariables.hashtag"/>
|
||||
<rect x="150" y="108" width="40" height="6" ry="3" :fill="themeVariables.fg"/>
|
||||
<rect x="120" y="128" width="40" height="6" ry="3" :fill="themeVariables.fg"/>
|
||||
<rect x="170" y="128" width="20" height="6" ry="3" :fill="themeVariables.link"/>
|
||||
</g>
|
||||
<path d="m65.498 40.892h137.7" :stroke="themeVariables.divider" stroke-width="0.75"/>
|
||||
<g transform="matrix(.60823 0 0 .60823 25.45 75.755)" fill="none" :stroke="themeVariables.accent" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||
|
@ -36,18 +32,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<path d="m10 5a2 2 0 1 1 4 0 7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6"/>
|
||||
<path d="m9 17v1a3 3 0 0 0 6 0v-1"/>
|
||||
</g>
|
||||
<image x="20.948" y="18.388" width="23.602" height="23.602" image-rendering="optimizeSpeed" preserveAspectRatio="xMidYMid meet" v-bind="{ 'xlink:href': instance.iconUrl || '/favicon.ico' }" />
|
||||
<circle cx="32" cy="32" r="16" :fill="themeVariables.accent"/>
|
||||
<circle cx="140" cy="20" r="6" :fill="themeVariables.success"/>
|
||||
<circle cx="160" cy="20" r="6" :fill="themeVariables.warn"/>
|
||||
<circle cx="180" cy="20" r="6" :fill="themeVariables.error"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { compile } from '@/theme.js';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import { compile } from '@/theme.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
|
||||
const props = defineProps<{
|
||||
theme: Theme;
|
||||
|
@ -55,20 +53,32 @@ const props = defineProps<{
|
|||
|
||||
const themeVariables = ref<{
|
||||
bg: string;
|
||||
acrylicBg: string;
|
||||
panel: string;
|
||||
fg: string;
|
||||
mention: string;
|
||||
hashtag: string;
|
||||
link: string;
|
||||
divider: string;
|
||||
accent: string;
|
||||
accentedBg: string;
|
||||
navBg: string;
|
||||
success: string;
|
||||
warn: string;
|
||||
error: string;
|
||||
}>({
|
||||
bg: 'var(--MI_THEME-bg)',
|
||||
acrylicBg: 'var(--MI_THEME-acrylicBg)',
|
||||
panel: 'var(--MI_THEME-panel)',
|
||||
fg: 'var(--MI_THEME-fg)',
|
||||
mention: 'var(--MI_THEME-mention)',
|
||||
hashtag: 'var(--MI_THEME-hashtag)',
|
||||
link: 'var(--MI_THEME-link)',
|
||||
divider: 'var(--MI_THEME-divider)',
|
||||
accent: 'var(--MI_THEME-accent)',
|
||||
accentedBg: 'var(--MI_THEME-accentedBg)',
|
||||
navBg: 'var(--MI_THEME-navBg)',
|
||||
success: 'var(--MI_THEME-success)',
|
||||
warn: 'var(--MI_THEME-warn)',
|
||||
error: 'var(--MI_THEME-error)',
|
||||
});
|
||||
|
||||
watch(() => props.theme, (theme) => {
|
||||
|
@ -76,7 +86,7 @@ watch(() => props.theme, (theme) => {
|
|||
|
||||
const _theme = deepClone(theme);
|
||||
|
||||
if (_theme?.base != null) {
|
||||
if (_theme.base != null) {
|
||||
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
|
||||
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
|
||||
}
|
||||
|
@ -85,12 +95,18 @@ watch(() => props.theme, (theme) => {
|
|||
|
||||
themeVariables.value = {
|
||||
bg: compiled.bg ?? 'var(--MI_THEME-bg)',
|
||||
acrylicBg: compiled.acrylicBg ?? 'var(--MI_THEME-acrylicBg)',
|
||||
panel: compiled.panel ?? 'var(--MI_THEME-panel)',
|
||||
fg: compiled.fg ?? 'var(--MI_THEME-fg)',
|
||||
mention: compiled.mention ?? 'var(--MI_THEME-mention)',
|
||||
hashtag: compiled.hashtag ?? 'var(--MI_THEME-hashtag)',
|
||||
link: compiled.link ?? 'var(--MI_THEME-link)',
|
||||
divider: compiled.divider ?? 'var(--MI_THEME-divider)',
|
||||
accent: compiled.accent ?? 'var(--MI_THEME-accent)',
|
||||
accentedBg: compiled.accentedBg ?? 'var(--MI_THEME-accentedBg)',
|
||||
navBg: compiled.navBg ?? 'var(--MI_THEME-navBg)',
|
||||
success: compiled.success ?? 'var(--MI_THEME-success)',
|
||||
warn: compiled.warn ?? 'var(--MI_THEME-warn)',
|
||||
error: compiled.error ?? 'var(--MI_THEME-error)',
|
||||
};
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
|
|
@ -249,6 +249,7 @@ async function close(skip: boolean) {
|
|||
|
||||
.pageFooter {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
flex-shrink: 0;
|
||||
|
|
|
@ -68,7 +68,7 @@ const emit = defineEmits<{
|
|||
(ev: 'update:tab', key: string);
|
||||
}>();
|
||||
|
||||
const viewId = inject(DI.viewId);
|
||||
//const viewId = inject(DI.viewId);
|
||||
const injectedPageMetadata = inject(DI.pageMetadata);
|
||||
const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div>
|
||||
<MkAnimBg style="position: absolute;"/>
|
||||
<div class="_pageScrollable" style="position: absolute; top: 0; width: 100%; height: 100%;">
|
||||
<div class="_pageScrollable" :class="$style.body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,5 +17,13 @@ import MkAnimBg from '@/components/MkAnimBg.vue';
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.body {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// _pageScrollable はパフォーマンス上の理由で背景色が設定されているため
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -345,7 +345,7 @@ $cellHeight: 28px;
|
|||
border: solid 0.5px transparent;
|
||||
|
||||
&.selected {
|
||||
border: solid 0.5px var(--MI_THEME-accentLighten);
|
||||
border: solid 0.5px hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||
}
|
||||
|
||||
&.ranged {
|
||||
|
|
|
@ -17,7 +17,6 @@ export type Keys = (
|
|||
'lang' |
|
||||
'drafts' |
|
||||
'hashtags' |
|
||||
'wallpaper' |
|
||||
'colorScheme' |
|
||||
'useSystemFont' |
|
||||
'fontSize' |
|
||||
|
|
|
@ -296,7 +296,7 @@ onMounted(async () => {
|
|||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
}
|
||||
|
|
|
@ -213,7 +213,7 @@ async function del() {
|
|||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
}
|
||||
|
|
|
@ -273,7 +273,7 @@ definePage(() => ({
|
|||
.footer {
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="[$style.root, { [$style.isMe]: isMe }]">
|
||||
<MkAvatar :class="$style.avatar" :user="message.fromUser" :link="!isMe" :preview="false"/>
|
||||
<div :class="$style.body" @contextmenu.stop="onContextmenu">
|
||||
<div v-if="!isMe && prefer.s['chat.showSenderName']" :class="$style.header"><MkUserName :user="message.fromUser"/></div>
|
||||
<div :class="$style.header"><MkUserName v-if="!isMe && prefer.s['chat.showSenderName']" :user="message.fromUser"/></div>
|
||||
<MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe">
|
||||
<div v-if="!message.isDeleted" :class="$style.content">
|
||||
<Mfm
|
||||
|
@ -216,10 +216,6 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
|
|||
flex-direction: row-reverse;
|
||||
text-align: right;
|
||||
|
||||
.content {
|
||||
color: var(--MI_THEME-fgOnAccent);
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
@ -230,8 +226,27 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
|
|||
position: sticky;
|
||||
top: calc(16px + var(--MI-stickyTop, 0px));
|
||||
display: block;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
@container (max-width: 450px) {
|
||||
.root {
|
||||
&.isMe {
|
||||
.avatar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.fukidashi {
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
|
@ -239,6 +254,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
|
|||
}
|
||||
|
||||
.header {
|
||||
min-height: 4px; // fukidashiの位置調整も兼ねるため
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
|
@ -252,9 +268,6 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.file {
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -119,7 +119,8 @@ function start(ev: MouseEvent) {
|
|||
}
|
||||
|
||||
async function startUser() {
|
||||
os.selectUser().then(user => {
|
||||
// TODO: localOnly は連合に対応したら消す
|
||||
os.selectUser({ localOnly: true }).then(user => {
|
||||
router.push(`/chat/user/${user.id}`);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -63,11 +63,11 @@ function save() {
|
|||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.areYouSure,
|
||||
text: i18n.tsx.deleteAreYouSure({ x: name_.value }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
misskeyApi('chat/rooms/delete', {
|
||||
await os.apiWithDialog('chat/rooms/delete', {
|
||||
roomId: props.room.id,
|
||||
});
|
||||
router.push('/chat');
|
||||
|
@ -81,10 +81,6 @@ watch(isMuted, async () => {
|
|||
mute: isMuted.value,
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkUserCardMini :user="room.owner"/>
|
||||
</MkA>
|
||||
|
||||
<hr>
|
||||
<hr v-if="memberships.length > 0">
|
||||
|
||||
<div v-for="membership in memberships" :key="membership.id" :class="$style.membership">
|
||||
<MkA :class="$style.membershipBody" :to="`${userPage(membership.user)}`">
|
||||
|
|
|
@ -9,20 +9,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-model="searchQuery"
|
||||
:placeholder="i18n.ts._chat.searchMessages"
|
||||
type="search"
|
||||
@enter="search()"
|
||||
>
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<MkButton v-if="searchQuery.length > 0" primary rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
<MkButton primary rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
|
||||
<MkFoldableSection v-if="searched">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<div v-if="searchResults.length > 0" class="_gaps_s">
|
||||
<div v-for="message in searchResults" :key="message.id" :class="$style.searchResultItem">
|
||||
<XMessage :message="message" :user="message.fromUser" :isSearchResult="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="_fullinfo">
|
||||
<img :src="infoImageUrl" draggable="false"/>
|
||||
<div>{{ i18n.ts.notFound }}</div>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -33,6 +38,7 @@ import * as Misskey from 'misskey-js';
|
|||
import XMessage from './XMessage.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
|
|
@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<Transition name="fade">
|
||||
<div v-show="showIndicator" :class="$style.new">
|
||||
<button class="_buttonPrimary" :class="$style.newButton" @click="onIndicatorClick">
|
||||
<i class="fas ti-fw fa-arrow-circle-down" :class="$style.newIcon"></i>{{ i18n.ts.newMessageExists }}
|
||||
<i class="fas ti-fw fa-arrow-circle-down" :class="$style.newIcon"></i>{{ i18n.ts._chat.newMessage }}
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
|
@ -391,6 +391,7 @@ const headerActions = computed(() => [{
|
|||
|
||||
definePage(computed(() => !initializing.value ? user.value ? {
|
||||
userName: user,
|
||||
title: user.value.name ?? user.value.username,
|
||||
avatar: user,
|
||||
} : {
|
||||
title: room.value?.name,
|
||||
|
|
|
@ -245,7 +245,7 @@ async function del() {
|
|||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
}
|
||||
|
|
|
@ -467,7 +467,7 @@ definePage(() => ({
|
|||
<style lang="scss" module>
|
||||
.footer {
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
border-top: solid .5px var(--MI_THEME-divider);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -292,7 +292,7 @@ onUnmounted(() => {
|
|||
.footer {
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,10 +8,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_m">
|
||||
<SearchMarker :keywords="['sync', 'profiles', 'devices']">
|
||||
<MkSwitch :modelValue="profilesSyncEnabled" @update:modelValue="changeProfilesSyncEnabled">
|
||||
<template #label><SearchLabel>{{ i18n.ts._deck.enableSyncBetweenDevicesForProfiles }}</SearchLabel></template>
|
||||
<template #label><i class="ti ti-cloud-cog"></i> <SearchLabel>{{ i18n.ts._deck.enableSyncBetweenDevicesForProfiles }}</SearchLabel></template>
|
||||
</MkSwitch>
|
||||
</SearchMarker>
|
||||
|
||||
<hr>
|
||||
|
||||
<SearchMarker :keywords="['ui', 'root', 'page']">
|
||||
<MkPreferenceContainer k="deck.useSimpleUiForNonRootPages">
|
||||
<MkSwitch v-model="useSimpleUiForNonRootPages">
|
||||
|
@ -45,23 +47,77 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkRadios>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['menu', 'position']">
|
||||
<MkPreferenceContainer k="deck.menuPosition">
|
||||
<MkRadios v-model="menuPosition">
|
||||
<template #label><SearchLabel>{{ i18n.ts._deck.deckMenuPosition }}</SearchLabel></template>
|
||||
<option value="right">{{ i18n.ts.right }}</option>
|
||||
<option value="bottom">{{ i18n.ts.bottom }}</option>
|
||||
</MkRadios>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['navbar', 'position']">
|
||||
<MkPreferenceContainer k="deck.navbarPosition">
|
||||
<MkRadios v-model="navbarPosition">
|
||||
<template #label><SearchLabel>{{ i18n.ts._deck.navbarPosition }}</SearchLabel></template>
|
||||
<option value="left">{{ i18n.ts.left }}</option>
|
||||
<option value="top">{{ i18n.ts.top }}</option>
|
||||
<option value="bottom">{{ i18n.ts.bottom }}</option>
|
||||
</MkRadios>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['column', 'gap', 'margin']">
|
||||
<MkPreferenceContainer k="deck.columnGap">
|
||||
<MkRange v-model="columnGap" :min="3" :max="100" :step="1" :continuousUpdate="true">
|
||||
<template #label><SearchLabel>{{ i18n.ts._deck.columnGap }}</SearchLabel></template>
|
||||
</MkRange>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['wallpaper']">
|
||||
<MkPreferenceContainer k="deck.wallpaper">
|
||||
<MkButton v-if="wallpaper == null" @click="setWallpaper"><SearchLabel>{{ i18n.ts.setWallpaper }}</SearchLabel></MkButton>
|
||||
<MkButton v-else @click="wallpaper = null">{{ i18n.ts.removeWallpaper }}</MkButton>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
</div>
|
||||
</SearchMarker>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkRange from '@/components/MkRange.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
|
||||
import { reloadAsk } from '@/utility/reload-ask.js';
|
||||
import { selectFile } from '@/utility/select-file.js';
|
||||
|
||||
const navWindow = prefer.model('deck.navWindow');
|
||||
const useSimpleUiForNonRootPages = prefer.model('deck.useSimpleUiForNonRootPages');
|
||||
const alwaysShowMainColumn = prefer.model('deck.alwaysShowMainColumn');
|
||||
const columnAlign = prefer.model('deck.columnAlign');
|
||||
const columnGap = prefer.model('deck.columnGap');
|
||||
const menuPosition = prefer.model('deck.menuPosition');
|
||||
const navbarPosition = prefer.model('deck.navbarPosition');
|
||||
const wallpaper = prefer.model('deck.wallpaper');
|
||||
|
||||
watch(wallpaper, async () => {
|
||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
||||
});
|
||||
|
||||
function setWallpaper(ev: MouseEvent) {
|
||||
selectFile(ev.currentTarget ?? ev.target, null).then(file => {
|
||||
wallpaper.value = file.url;
|
||||
});
|
||||
}
|
||||
|
||||
const profilesSyncEnabled = ref(prefer.isSyncEnabled('deck.profiles'));
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_m">
|
||||
<SearchMarker :keywords="['sync', 'palettes', 'devices']">
|
||||
<MkSwitch :modelValue="palettesSyncEnabled" @update:modelValue="changePalettesSyncEnabled">
|
||||
<template #label><SearchLabel>{{ i18n.ts._emojiPalette.enableSyncBetweenDevicesForPalettes }}</SearchLabel></template>
|
||||
<template #label><i class="ti ti-cloud-cog"></i> <SearchLabel>{{ i18n.ts._emojiPalette.enableSyncBetweenDevicesForPalettes }}</SearchLabel></template>
|
||||
</MkSwitch>
|
||||
</SearchMarker>
|
||||
</div>
|
||||
|
|
|
@ -38,14 +38,13 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { getBuiltinThemesRef } from '@/theme.js';
|
||||
import { getBuiltinThemesRef, getThemesRef, removeTheme } from '@/theme.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import * as os from '@/os.js';
|
||||
import { getThemes, removeTheme } from '@/theme-store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
const installedThemes = ref(getThemes());
|
||||
const installedThemes = getThemesRef();
|
||||
const builtinThemes = getBuiltinThemesRef();
|
||||
const selectedThemeId = ref<string | null>(null);
|
||||
|
||||
|
|
|
@ -181,6 +181,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</div>
|
||||
|
||||
<SearchMarker :keywords="['sync', 'themes', 'devices']">
|
||||
<MkSwitch :modelValue="themesSyncEnabled" @update:modelValue="changeThemesSyncEnabled">
|
||||
<template #label><i class="ti ti-cloud-cog"></i> <SearchLabel>{{ i18n.ts._settings.enableSyncThemesBetweenDevices }}</SearchLabel></template>
|
||||
</MkSwitch>
|
||||
</SearchMarker>
|
||||
|
||||
<FormSection>
|
||||
<div class="_formLinksGrid">
|
||||
<FormLink to="/settings/theme/manage"><template #icon><i class="ti ti-tool"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
|
||||
|
@ -189,17 +195,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<FormLink to="/theme-editor"><template #icon><i class="ti ti-paint"></i></template>{{ i18n.ts._theme.make }}</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<SearchMarker :keywords="['wallpaper']">
|
||||
<MkButton v-if="wallpaper == null" @click="setWallpaper"><SearchLabel>{{ i18n.ts.setWallpaper }}</SearchLabel></MkButton>
|
||||
<MkButton v-else @click="wallpaper = null">{{ i18n.ts.removeWallpaper }}</MkButton>
|
||||
</SearchMarker>
|
||||
</div>
|
||||
</SearchMarker>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onActivated, ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import defaultLightTheme from '@@/themes/l-light.json5';
|
||||
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
|
||||
|
@ -207,23 +208,21 @@ import type { Theme } from '@/theme.js';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkThemePreview from '@/components/MkThemePreview.vue';
|
||||
import { getBuiltinThemesRef } from '@/theme.js';
|
||||
import { getBuiltinThemesRef, getThemesRef } from '@/theme.js';
|
||||
import { selectFile } from '@/utility/select-file.js';
|
||||
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
|
||||
import { store } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { uniqueBy } from '@/utility/array.js';
|
||||
import { getThemes } from '@/theme-store.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { reloadAsk } from '@/utility/reload-ask.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
const installedThemes = ref(getThemes());
|
||||
const installedThemes = getThemesRef();
|
||||
const builtinThemes = getBuiltinThemesRef();
|
||||
|
||||
const instanceDarkTheme = computed<Theme | null>(() => instance.defaultDarkTheme ? JSON5.parse(instance.defaultDarkTheme) : null);
|
||||
|
@ -263,7 +262,6 @@ const lightThemeId = computed({
|
|||
|
||||
const darkMode = computed(store.makeGetterSetter('darkMode'));
|
||||
const syncDeviceDarkMode = prefer.model('syncDeviceDarkMode');
|
||||
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
|
||||
const themesCount = installedThemes.value.length;
|
||||
|
||||
watch(syncDeviceDarkMode, () => {
|
||||
|
@ -272,23 +270,18 @@ watch(syncDeviceDarkMode, () => {
|
|||
}
|
||||
});
|
||||
|
||||
watch(wallpaper, async () => {
|
||||
if (wallpaper.value == null) {
|
||||
miLocalStorage.removeItem('wallpaper');
|
||||
const themesSyncEnabled = ref(prefer.isSyncEnabled('themes'));
|
||||
|
||||
function changeThemesSyncEnabled(value: boolean) {
|
||||
if (value) {
|
||||
prefer.enableSync('themes').then((res) => {
|
||||
if (res == null) return;
|
||||
if (res.enabled) themesSyncEnabled.value = true;
|
||||
});
|
||||
} else {
|
||||
miLocalStorage.setItem('wallpaper', wallpaper.value);
|
||||
prefer.disableSync('themes');
|
||||
themesSyncEnabled.value = false;
|
||||
}
|
||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
installedThemes.value = getThemes();
|
||||
});
|
||||
|
||||
function setWallpaper(event) {
|
||||
selectFile(event.currentTarget ?? event.target, null).then(file => {
|
||||
wallpaper.value = file.url;
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
@ -77,7 +77,7 @@ definePage(() => ({
|
|||
.footer {
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
@ -86,10 +86,9 @@ import MkCodeEditor from '@/components/MkCodeEditor.vue';
|
|||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import { $i } from '@/i.js';
|
||||
import { applyTheme } from '@/theme.js';
|
||||
import { addTheme, applyTheme } from '@/theme.js';
|
||||
import * as os from '@/os.js';
|
||||
import { store } from '@/store.js';
|
||||
import { addTheme } from '@/theme-store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useLeaveGuard } from '@/use/use-leave-guard.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
|
|
@ -151,7 +151,7 @@ misskeyApiGet('federation/instances', {
|
|||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
background: var(--MI_THEME-acrylicPanel);
|
||||
background: color(from var(--MI_THEME-panel) srgb r g b / 0.5);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
border-radius: 999px;
|
||||
|
|
|
@ -371,7 +371,19 @@ export const PREF_DEF = {
|
|||
default: true,
|
||||
},
|
||||
'deck.columnAlign': {
|
||||
default: 'left' as 'left' | 'right' | 'center',
|
||||
default: 'center' as 'left' | 'right' | 'center',
|
||||
},
|
||||
'deck.columnGap': {
|
||||
default: 6,
|
||||
},
|
||||
'deck.menuPosition': {
|
||||
default: 'bottom' as 'right' | 'bottom',
|
||||
},
|
||||
'deck.navbarPosition': {
|
||||
default: 'left' as 'left' | 'top' | 'bottom',
|
||||
},
|
||||
'deck.wallpaper': {
|
||||
default: null as string | null,
|
||||
},
|
||||
|
||||
'chat.showSenderName': {
|
||||
|
|
|
@ -42,5 +42,5 @@ mainRouter.addListener('change', ctx => {
|
|||
mainRouter.init();
|
||||
|
||||
export function useRouter(): Router {
|
||||
return inject(DI.router) ?? mainRouter;
|
||||
return inject(DI.router, null) ?? mainRouter;
|
||||
}
|
||||
|
|
|
@ -398,7 +398,7 @@ rt {
|
|||
}
|
||||
|
||||
._acrylic {
|
||||
background: var(--MI_THEME-acrylicPanel);
|
||||
background: color(from var(--MI_THEME-panel) srgb r g b / 0.5);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Theme } from '@/theme.js';
|
||||
import { getBuiltinThemes } from '@/theme.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
export function getThemes(): Theme[] {
|
||||
if ($i == null) return [];
|
||||
return prefer.s.themes;
|
||||
}
|
||||
|
||||
export async function addTheme(theme: Theme): Promise<void> {
|
||||
if ($i == null) return;
|
||||
const builtinThemes = await getBuiltinThemes();
|
||||
if (builtinThemes.some(t => t.id === theme.id)) {
|
||||
throw new Error('builtin theme');
|
||||
}
|
||||
const themes = getThemes();
|
||||
if (themes.some(t => t.id === theme.id)) {
|
||||
throw new Error('already exists');
|
||||
}
|
||||
prefer.commit('themes', [...themes, theme]);
|
||||
}
|
||||
|
||||
export async function removeTheme(theme: Theme): Promise<void> {
|
||||
if ($i == null) return;
|
||||
const themes = getThemes().filter(t => t.id !== theme.id);
|
||||
prefer.commit('themes', themes);
|
||||
}
|
|
@ -8,11 +8,13 @@ import tinycolor from 'tinycolor2';
|
|||
import lightTheme from '@@/themes/_light.json5';
|
||||
import darkTheme from '@@/themes/_dark.json5';
|
||||
import JSON5 from 'json5';
|
||||
import type { Ref } from 'vue';
|
||||
import type { BundledTheme } from 'shiki/themes';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { addTheme, getThemes } from '@/theme-store.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
|
||||
export type Theme = {
|
||||
id: string;
|
||||
|
@ -57,11 +59,34 @@ export const getBuiltinThemes = () => Promise.all(
|
|||
].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
|
||||
);
|
||||
|
||||
export const getBuiltinThemesRef = () => {
|
||||
export function getBuiltinThemesRef() {
|
||||
const builtinThemes = ref<Theme[]>([]);
|
||||
getBuiltinThemes().then(themes => builtinThemes.value = themes);
|
||||
return builtinThemes;
|
||||
};
|
||||
}
|
||||
|
||||
export function getThemesRef(): Ref<Theme[]> {
|
||||
return prefer.r.themes;
|
||||
}
|
||||
|
||||
export async function addTheme(theme: Theme): Promise<void> {
|
||||
if ($i == null) return;
|
||||
const builtinThemes = await getBuiltinThemes();
|
||||
if (builtinThemes.some(t => t.id === theme.id)) {
|
||||
throw new Error('builtin theme');
|
||||
}
|
||||
const themes = prefer.s.themes;
|
||||
if (themes.some(t => t.id === theme.id)) {
|
||||
throw new Error('already exists');
|
||||
}
|
||||
prefer.commit('themes', [...themes, theme]);
|
||||
}
|
||||
|
||||
export async function removeTheme(theme: Theme): Promise<void> {
|
||||
if ($i == null) return;
|
||||
const themes = prefer.s.themes.filter(t => t.id !== theme.id);
|
||||
prefer.commit('themes', themes);
|
||||
}
|
||||
|
||||
let timeout: number | null = null;
|
||||
|
||||
|
@ -173,7 +198,7 @@ export function parseThemeCode(code: string): Theme {
|
|||
if (!validateTheme(theme)) {
|
||||
throw new Error('This theme is invaild');
|
||||
}
|
||||
if (getThemes().some(t => t.id === theme.id)) {
|
||||
if (prefer.s.themes.some(t => t.id === theme.id)) {
|
||||
throw new Error('This theme is already installed');
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue