Compare commits

...

164 Commits

Author SHA1 Message Date
d22148b418 12.21.0 2020-02-23 02:53:44 +09:00
5dc75c9cea Fix #6029 2020-02-23 02:34:54 +09:00
200e82decb Fix #6063 2020-02-23 02:28:07 +09:00
50359dbaf4 Resolve #6053 2020-02-22 06:57:54 +09:00
7165f21a62 Fix style 2020-02-22 06:54:35 +09:00
8aab828c65 Better featured injection 2020-02-22 06:49:12 +09:00
c9f8c12f5b 🍕 2020-02-22 06:43:46 +09:00
a347f8fa49 🎨 2020-02-22 06:40:48 +09:00
2d76bdd0f8 Fix bug 2020-02-22 06:36:15 +09:00
c5cdd56edb 🎨 2020-02-22 03:51:31 +09:00
6901ab39ed Merge pull request #6058 from syuilo/patch/autogen/v11
[AUTOMATED] Update README.md
2020-02-22 03:02:58 +09:00
b851b7f431 12.20.0 2020-02-22 02:38:37 +09:00
ccaa99115c New Crowdin translations (#6047)
* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Kannada)

* New translations ja-JP.yml (Kannada)

* New translations ja-JP.yml (Kannada)
2020-02-22 02:38:11 +09:00
813de15e85 🎨 2020-02-22 02:30:41 +09:00
fa33181fa9 Update particle.vue 2020-02-22 02:26:01 +09:00
d4324dc0cb Reaction particle 2020-02-22 01:20:58 +09:00
ccc27bcc14 Fix #6057 (#6061) 2020-02-22 01:03:50 +09:00
014c1673c6 Update README.md [AUTOGEN] 2020-02-21 22:39:06 +09:00
3a3319ff52 Merge pull request #6056 from syuilo/patch/autogen/v11
[AUTOMATED] Update README.md
2020-02-21 20:11:43 +09:00
5b54ec8fb5 Update README.md [AUTOGEN] 2020-02-21 18:37:07 +09:00
e690556286 patch #6039 (#6052) 2020-02-21 17:16:51 +09:00
660956917f Merge pull request #6034 from syuilo/patch/autogen/v11
[AUTOMATED] Update README.md
2020-02-21 11:18:52 +09:00
3a5201747b 🎨 2020-02-21 09:17:33 +09:00
b338e8a83f 🎨 2020-02-21 09:11:35 +09:00
5584d56b6a Clean up 2020-02-21 08:36:18 +09:00
c925498120 Improve usability 2020-02-21 07:21:27 +09:00
75615cf503 Update style.scss 2020-02-21 07:11:25 +09:00
39f708b0fc 複数タブで開いてるときに動作がおかしい問題を修正 2020-02-21 03:51:41 +09:00
ac32077221 12.19.0 2020-02-21 00:36:17 +09:00
a5902acacd New Crowdin translations (#6037)
* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)
2020-02-21 00:36:03 +09:00
c7c08b7511 Resolve #6043 2020-02-21 00:28:45 +09:00
7de915d47b Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-02-20 23:27:48 +09:00
9107547501 Update CHANGELOG.md 2020-02-20 23:27:32 +09:00
95dc76ca19 Fix comments 2020-02-20 23:26:35 +09:00
49c2a9b372 ボリュームが0のときサウンドを鳴らさないように 2020-02-20 23:17:17 +09:00
b378cabfc7 Fix bug 2020-02-20 23:11:09 +09:00
4263dbef31 Fix #6026 2020-02-20 23:07:20 +09:00
32fc6ae2eb Fix bug 2020-02-20 23:07:03 +09:00
238cb0077f Fix bug 2020-02-20 23:02:55 +09:00
f5a06b6494 Fix #6036 2020-02-20 22:54:26 +09:00
2c01329085 Update README.md [AUTOGEN] 2020-02-20 14:10:06 +09:00
502de89ab1 12.18.1 2020-02-20 13:41:28 +09:00
128de6750c New translations ja-JP.yml (Spanish) (#6027) 2020-02-20 13:41:16 +09:00
e59e2d9f0b Resolve #6028 2020-02-20 13:38:40 +09:00
2504b8391b Better validation 2020-02-20 13:33:41 +09:00
330ea7d210 12.18.0 2020-02-20 07:30:43 +09:00
1edd173a29 Add sounds 2020-02-20 07:29:34 +09:00
98d873a7f9 Update search-by-tag.ts 2020-02-20 07:19:27 +09:00
09175b84df Fix #6016 2020-02-20 07:18:40 +09:00
177e19632a Fix #6016 2020-02-20 07:18:16 +09:00
8e6207f3e9 Remove header transition 2020-02-20 06:42:20 +09:00
ff3a97f6cf Fix #5943 2020-02-20 06:38:19 +09:00
b8e155ab40 🎨 2020-02-20 06:08:54 +09:00
b8e7df198d Improve sound 2020-02-20 06:08:49 +09:00
34311e3181 12.17.0 2020-02-20 03:55:38 +09:00
46115d3f04 New Crowdin translations (#5997)
* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Kannada)
2020-02-20 03:52:23 +09:00
c1d25d2394 切断時ダイアログのタイミングの変更など (#6014)
* 再接続時インジケーター

* Update ja-JP.yml

* Update stream-indicator.vue

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2020-02-20 03:42:35 +09:00
880cea5a56 Better sfx 2020-02-20 03:14:17 +09:00
e7205d9cc2 サウンド設定など 2020-02-20 02:40:53 +09:00
f456feb3ff media-listのgridの高さがsub-note-detailsのdetailsの中だと287pxになってしまっていたのを修正 (#5951)
* fix files grid height

* missing colon

* ✌️

* ✌️

* fix

* remove unused event listener
2020-02-20 01:24:45 +09:00
3f83beedb7 Fix #5943 (#6023) 2020-02-20 00:38:26 +09:00
e6c9b1d9bd LegacyReaction変換にstarを追加 (#6013) 2020-02-19 22:06:54 +09:00
b46114f4fa Update index.home.vue 2020-02-19 17:59:28 +09:00
8d77e2ba22 Fix bug 2020-02-19 17:55:55 +09:00
cb3900921f remove unused event listener 2020-02-19 07:49:53 +09:00
ae2021583d 🎨 2020-02-19 07:00:44 +09:00
36cd88e6b7 12.16.0 2020-02-19 06:42:46 +09:00
517b0908da 🎨 2020-02-19 06:41:30 +09:00
b23b3e4d21 Fix #5984 2020-02-19 06:36:50 +09:00
883fc5dde0 Improve notification 2020-02-19 06:26:29 +09:00
9d044329f6 🎨 2020-02-19 06:17:41 +09:00
d1e9e74cb8 Resolve #5978 2020-02-19 06:16:49 +09:00
98a87ee75f 12.15.0 2020-02-19 04:16:00 +09:00
331491077d New translations ja-JP.yml (French) (#5972) 2020-02-19 04:15:39 +09:00
913c3a6636 Fix page like button 2020-02-19 04:15:14 +09:00
fbaf5fe355 Clean up 2020-02-19 04:12:49 +09:00
804c932f60 Resolve #5995 2020-02-19 04:08:35 +09:00
cef6d1d1b6 モデレーターになってしまっている場合は解除できるように (#5983) 2020-02-19 03:24:37 +09:00
e4e7ab1135 ページ遷移のトランジションをなくした 2020-02-19 03:22:10 +09:00
6ca30df8c4 Some tweaks 2020-02-19 03:16:10 +09:00
a340d4ed8e 固定投稿フォームを実装 (#5994)
* 固定投稿フォームを実装

* fix
2020-02-19 03:11:09 +09:00
ca7cb94358 Fix bug 2020-02-18 23:42:08 +09:00
54779b25f5 Clean up 2020-02-18 23:16:55 +09:00
44d7652171 12.14.0 2020-02-18 21:35:16 +09:00
c9ed15b682 add missing image (#5967)
fix for explore banner
2020-02-18 21:33:51 +09:00
8faad646ae New Crowdin translations (#5971)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Kannada)
2020-02-18 21:33:38 +09:00
1d50bc3382 Fix bug 2020-02-18 21:27:47 +09:00
da4af041af ログビューア実装 2020-02-18 21:27:43 +09:00
e2ff408f2f Implement object storage settings 2020-02-18 21:12:05 +09:00
50d1500dfc 12.13.0 2020-02-18 19:50:04 +09:00
94441f93a5 New Crowdin translations (#5969)
* New translations ja-JP.yml (Kannada)

* New translations ja-JP.yml (Kannada)

* New translations ja-JP.yml (Kannada)

* New translations ja-JP.yml (Kannada)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)
2020-02-18 19:48:14 +09:00
5f712fbf3c Implement photo widget 2020-02-18 19:47:30 +09:00
1c757f10e0 Update CHANGELOG.md 2020-02-18 19:36:20 +09:00
0508d5f643 Add activity widget 2020-02-18 19:31:11 +09:00
d9986b7a2f Implement featured note injection 2020-02-18 19:05:11 +09:00
3d79e7a136 Improve paging 2020-02-18 18:19:11 +09:00
52fb1237ec Imprement promo read 2020-02-18 18:14:38 +09:00
8a7197726e Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-02-18 17:54:13 +09:00
b7f5458684 Fix bug 2020-02-18 17:53:56 +09:00
52710f3810 管理者はモデレーターに変更できないように (#5970)
* 管理者をモデレーターに変更できないように

* Change error message
2020-02-18 17:53:52 +09:00
a54de07260 Resolve #5963 2020-02-18 08:41:32 +09:00
aa2c8d101e Fix type 2020-02-18 08:13:47 +09:00
1441fd93b9 Clean up 2020-02-18 08:05:27 +09:00
4a585e8920 Improve chart logging 2020-02-18 03:03:34 +09:00
8c4245a09d Update core.ts 2020-02-18 02:27:18 +09:00
e4af16989a Fix bug 2020-02-18 01:25:02 +09:00
5dc0944fe8 Resolve #5949 2020-02-18 01:12:35 +09:00
b4d24f4377 12.12.0 2020-02-17 07:24:16 +09:00
67be47b8db New Crowdin translations (#5961)
* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)
2020-02-17 07:23:58 +09:00
e382982d32 Fix bug 2020-02-17 07:23:39 +09:00
b09b74b5da 🎨 2020-02-17 07:14:03 +09:00
c628bdb7a6 Fix glitch 2020-02-17 06:48:04 +09:00
2fcf6fb0fd UI tweak 2020-02-17 06:43:52 +09:00
4f3fc9ffd0 🎨 2020-02-17 06:39:41 +09:00
15839a7399 🎨 2020-02-17 06:37:39 +09:00
26b3a14a63 Clean up 2020-02-17 06:23:18 +09:00
f2f0799df1 Update app.vue 2020-02-17 05:38:00 +09:00
6c99c32100 i18n 2020-02-17 03:19:27 +09:00
93d25a2a34 ユーザー設定とクライアント設定を分離 2020-02-17 03:10:51 +09:00
88f5ec59d7 🎨 2020-02-17 02:41:03 +09:00
586d3c4db7 Better instance page 2020-02-17 02:27:14 +09:00
f45fb56e15 Improve instance info page 2020-02-17 02:21:27 +09:00
8fe153c7c1 12.11.0 2020-02-16 22:53:35 +09:00
36a8720fbb New Crowdin translations (#5948)
* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)
2020-02-16 22:53:16 +09:00
9cbfdc94d9 Clean up 2020-02-16 22:46:51 +09:00
091923764d Implement image dialog 2020-02-16 22:46:18 +09:00
dc39caed1e Resolve #5942 2020-02-16 22:15:49 +09:00
bcd7d1f007 Update CHANGELOG.md 2020-02-16 21:11:44 +09:00
40d4dc0474 Refactor 2020-02-16 21:11:27 +09:00
02ac30c0d0 Resolve #5958 2020-02-16 21:10:52 +09:00
518bc92673 Clean up 2020-02-16 21:05:17 +09:00
a5b92e316c Refactor: Extract scroll utility functions 2020-02-16 20:58:41 +09:00
828c7b66a0 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-02-16 20:54:33 +09:00
93474eaa06 2回目以降の読み込みは30個までフェッチするように 2020-02-16 20:54:25 +09:00
237f366aa2 Update notification (#5956)
* Add icon for messaging

This will add icon within messaging

* Update messaging-room.message.vue

Link to missing icon

* Update notification.vue

fix renote icon in notification

https://github.com/syuilo/misskey/issues/5955
2020-02-16 20:21:05 +09:00
714bcf28d5 テーマの切り替え時に時計の色が変わるように (#5959)
* テーマの切り替え時に時計の色が変わるように

* ディレイを追加
2020-02-16 20:20:02 +09:00
420eeb4d68 Clean up 2020-02-15 23:42:52 +09:00
bc6daf4a2e Update CHANGELOG.md 2020-02-15 23:16:50 +09:00
6f7832c09b API doc 2020-02-15 23:13:59 +09:00
bef67fa275 Update trend.ts 2020-02-15 23:01:41 +09:00
05d7198667 🎨 2020-02-15 22:51:46 +09:00
df0bfc14e5 🎨 2020-02-15 22:21:35 +09:00
3f28f7451f Prefetch aicons 2020-02-15 21:56:21 +09:00
dbb9199d6f Fix widget bg (#5952) 2020-02-15 21:42:45 +09:00
72cb3b03af Update CHANGELOG.md 2020-02-15 21:39:59 +09:00
d0085f00ed Fix #5950 2020-02-15 21:39:38 +09:00
43734f027b Refactoring 2020-02-15 21:33:32 +09:00
f799375635 fix 2020-02-15 21:31:56 +09:00
65704bbf01 ✌️ 2020-02-15 21:20:01 +09:00
9cb3882efa ✌️ 2020-02-15 20:18:37 +09:00
a0833ca691 missing colon 2020-02-15 19:34:30 +09:00
a4f197f608 fix files grid height 2020-02-15 19:33:12 +09:00
bb903cab40 🎨 2020-02-15 18:47:50 +09:00
92f765bc47 Update sequential-entrance.vue 2020-02-15 18:42:43 +09:00
742889a035 Update sequential-entrance.vue 2020-02-15 18:39:45 +09:00
24453ebcc3 Improve banner animation performance 2020-02-15 17:44:26 +09:00
8b8ab1bf5c Update CHANGELOG.md 2020-02-15 17:33:51 +09:00
e9bc9b8675 Fix bug 2020-02-15 17:31:45 +09:00
eeaa27c7ca Improve usability 2020-02-15 09:22:16 +09:00
ccea1755fc なんか 2020-02-15 09:10:49 +09:00
c32a5d602b 🎨 2020-02-15 08:52:21 +09:00
2a04f2ca4d Improve follow-requests page 2020-02-15 08:42:21 +09:00
37c80e8ef5 Improve wallpaper feature 2020-02-15 08:29:59 +09:00
1dce62e42a 🎨 2020-02-15 07:54:20 +09:00
384 changed files with 4468 additions and 1933 deletions

View File

@ -1,6 +1,135 @@
ChangeLog ChangeLog
========= =========
12.21.0 (2020/02/23)
-------------------
### ✨Improvements
* ノートのメニューに詳細ページへのリンクを追加
* UIの調整
### 🐛Fixes
* チャットで自分の送信したURLが視認しにくい問題を修正
* ノートの内のインラインコードが横に突き抜ける問題を修正
* (新しいノートがあります)表示中にタイムラインを切り替えると、表示が消えなくなってしまう問題を修正
* 引用RNフォームを開いた時だけ、textareaにフォーカスが当たっていない問題を修正
12.20.0 (2020/02/22)
-------------------
### ✨Improvements
* UIの調整
### 🐛Fixes
* 複数タブで開いてるときに動作がおかしい問題を修正
* メディア付きートの詳細表示をした後TLに戻るとートがバグる問題を修正
12.19.0 (2020/02/21)
-------------------
### ✨Improvements
* アンテナで除外キーワードを設定できるように
### 🐛Fixes
* ハッシュタグをもっと見るできないのを修正
* 無効になっているタイムラインでも使用できるかのように表示される問題を修正
* バックグラウンドで受信したノートの画像が表示されない問題を修正
* サインインフォームが表示されない場所がある問題を修正
* ボリュームが0のときサウンドを鳴らさないように
12.18.1 (2020/02/20)
-------------------
### 🐛Fixes
* タイムラインのハイライトに自分のノートは含めないように
* ハッシュタグの集計に関する問題を修正
12.18.0 (2020/02/20)
-------------------
### ✨Improvements
* 効果音設定を強化
* UIの調整
### 🐛Fixes
* ユーザープレビューが稀に画面上から消えなくなってしまう問題を修正
* ハッシュタグ検索が遅い問題を修正
12.17.0 (2020/02/20)
-------------------
### ✨Improvements
* 効果音を実装
* 切断時ダイアログを控えめに
### 🐛Fixes
* 新しいノートの通知が崩れる問題を修正
* LegacyReaction変換にstarを追加
* ユーザープレビューが稀に画面上から消えなくなってしまう問題を修正
* media-listのgridの高さがsub-note-detailsのdetailsの中だと287pxになってしまっていたのを修正
12.16.0 (2020/02/19)
-------------------
### ✨Improvements
* 通知一覧をポップアップではなくページで表示できるように
* 返信、引用、メンションの通知を直接ノートとして表示するように
### 🐛Fixes
* v12以前のリアクションが表示されない問題を修正
12.15.0 (2020/02/19)
-------------------
### ✨Improvements
* 固定投稿フォームを実装
* ページ遷移のトランジションを無しに
* スクロールしてるときに新しいノートが来たときにわかるように表示するように
### 🐛Fixes
* ページのいいねボタンを修正
12.14.0 (2020/02/18)
-------------------
### ✨Improvements
* オブジェクトストレージの設定を実装
* サーバーログビューア実装
12.13.0 (2020/02/18)
-------------------
### ✨Improvements
* プロモーションノート機能を実装
* インスタンス管理者が、重要なお知らせやユーザーにやってもらいたいアンケートなどをタイムラインの途中に挿入する機能
* プロモーションされる期限を設定できる
* 複数のプロモーションがある場合はランダムに選択されて表示される
* ユーザーがプロモーションを個別に非表示にすることもできる
* ハイライトインジェクション機能を実装
* タイムラインの途中におすすめのノートを表示できる機能
* 設定で有効/無効を切り替えられる
* アクティビティウィジェットを実装
* フォトウィジェットを実装
* タイムラインの一番上までスクロールできるように
* 管理者はモデレーターに変更できないように
### 🐛Fixes
* admin/show-users APIがadminかつmoderator設定されているとき使えない問題を修正
12.12.0 (2020/02/17)
-------------------
### ✨Improvements
* インスタンス情報ページを強化
* インスタンス設定ページを強化
* 設定ページをアカウント設定ページとクライアント設定ページに分離
* UIの調整
12.11.0 (2020/02/16)
-------------------
### ✨Improvements
* 投稿詳細ページで前後の投稿を見れるように
* 自分のfollowersートはRenoteできるように
* 画像ダイアログを実装
* フォロー申請ページの調整
* 壁紙設定の強化
* 画面が狭い状態でMisskeyを起動した場合でも、画面幅が広がったときにウィジェットを表示するように
* 「もっと読み込む」したときの読み込み量を増量
### 🐛Fixes
* 認証なしでグローバルTLにアクセスすると妙なエラーが返る問題を修正
* API docが見れない問題を修正
* 画面右上に当たり判定があるのを修正
12.10.0 (2020/02/15) 12.10.0 (2020/02/15)
------------------- -------------------
### ✨Improvements ### ✨Improvements

View File

@ -110,7 +110,6 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19045173/cb91c0f345c24d4ebfd05f19906d5e26/1.png?token-time=2145916800&token-hash=o_zKBytJs_AxHwSYw_5R8eD0eSJe3RoTR3kR3Q0syN0%3D" alt="kiritan" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19045173/cb91c0f345c24d4ebfd05f19906d5e26/1.png?token-time=2145916800&token-hash=o_zKBytJs_AxHwSYw_5R8eD0eSJe3RoTR3kR3Q0syN0%3D" alt="kiritan" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24430516/b1964ac5b9f746d2a12ff53dbc9aa40a/1.jpg?token-time=2145916800&token-hash=bmEiMGYpp3bS7hCCbymjGGsHBZM3AXuBOFO3Kro37PU%3D" alt="Eduardo Quiros" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24430516/b1964ac5b9f746d2a12ff53dbc9aa40a/1.jpg?token-time=2145916800&token-hash=bmEiMGYpp3bS7hCCbymjGGsHBZM3AXuBOFO3Kro37PU%3D" alt="Eduardo Quiros" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/14215107/1cbe1912c26143919fa0faca16f12ce1/3.png?token-time=2145916800&token-hash=Zq1TCK2tdY7xudEm_aV70bc_wxmol6pNj3ZWbpFUNbI%3D" alt="Nesakko" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/14215107/1cbe1912c26143919fa0faca16f12ce1/3.png?token-time=2145916800&token-hash=Zq1TCK2tdY7xudEm_aV70bc_wxmol6pNj3ZWbpFUNbI%3D" alt="Nesakko" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/776209" alt="Denshi" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/user?u=20832595">Roujo</a></td> <td><a href="https://www.patreon.com/user?u=20832595">Roujo</a></td>
<td><a href="https://www.patreon.com/user?u=27956229">Oliver Maximilian Seidel</a></td> <td><a href="https://www.patreon.com/user?u=27956229">Oliver Maximilian Seidel</a></td>
@ -119,9 +118,9 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=19045173">kiritan</a></td> <td><a href="https://www.patreon.com/user?u=19045173">kiritan</a></td>
<td><a href="https://www.patreon.com/user?u=24430516">Eduardo Quiros</a></td> <td><a href="https://www.patreon.com/user?u=24430516">Eduardo Quiros</a></td>
<td><a href="https://www.patreon.com/Nesakko">Nesakko</a></td> <td><a href="https://www.patreon.com/Nesakko">Nesakko</a></td>
<td><a href="https://www.patreon.com/user?u=776209">Denshi</a></td>
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c8.patreon.com/2/200/776209" alt="Denshi" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/3075183/c2ae575c604e420297f000ccc396e395/1.jpeg?token-time=2145916800&token-hash=O9qmPtpo6wWb0OuvnkEekhk_1WO2MTdytLr7ZgsAr80%3D" alt="Liaizon Wakest" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/3075183/c2ae575c604e420297f000ccc396e395/1.jpeg?token-time=2145916800&token-hash=O9qmPtpo6wWb0OuvnkEekhk_1WO2MTdytLr7ZgsAr80%3D" alt="Liaizon Wakest" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/557245" alt="mkatze" width="100"></td> <td><img src="https://c8.patreon.com/2/200/557245" alt="mkatze" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/23915207/25428766ecd745478e600b3d7f871eb2/1.png?token-time=2145916800&token-hash=urCLLA4KjJZX92Y1CxcBP4d8bVTHGkiaPnQZp-Tqz68%3D" alt="kabo2468y" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/23915207/25428766ecd745478e600b3d7f871eb2/1.png?token-time=2145916800&token-hash=urCLLA4KjJZX92Y1CxcBP4d8bVTHGkiaPnQZp-Tqz68%3D" alt="kabo2468y" width="100"></td>
@ -131,8 +130,8 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5788159/af42076ab3354bb49803cfba65f94bee/1.jpg?token-time=2145916800&token-hash=iSaxp_Yr2-ZiU2YVi9rcpZZj9mj3UvNSMrZr4CU4qtA%3D" alt="mewl hayabusa" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5788159/af42076ab3354bb49803cfba65f94bee/1.jpg?token-time=2145916800&token-hash=iSaxp_Yr2-ZiU2YVi9rcpZZj9mj3UvNSMrZr4CU4qtA%3D" alt="mewl hayabusa" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/11357794/923ce94cd8c44ba788ee931907881839/1.png?token-time=2145916800&token-hash=9nEQje_eMvUjq9a7L3uBqW-MQbS-rRMaMgd7UYVoFNM%3D" alt="mydarkstar" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/11357794/923ce94cd8c44ba788ee931907881839/1.png?token-time=2145916800&token-hash=9nEQje_eMvUjq9a7L3uBqW-MQbS-rRMaMgd7UYVoFNM%3D" alt="mydarkstar" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28779508/3cd4cb7f017f4ee0864341e3464d42f9/1.png?token-time=2145916800&token-hash=eGQtR15be44kgvh8fw2Jx8Db4Bv15YBp2ldxh0EKRxA%3D" alt="S Y" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/user?u=776209">Denshi</a></td>
<td><a href="https://www.patreon.com/wakest">Liaizon Wakest</a></td> <td><a href="https://www.patreon.com/wakest">Liaizon Wakest</a></td>
<td><a href="https://www.patreon.com/user?u=557245">mkatze</a></td> <td><a href="https://www.patreon.com/user?u=557245">mkatze</a></td>
<td><a href="https://www.patreon.com/user?u=23915207">kabo2468y</a></td> <td><a href="https://www.patreon.com/user?u=23915207">kabo2468y</a></td>
@ -142,55 +141,60 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61</a></td> <td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61</a></td>
<td><a href="https://www.patreon.com/hs_sh_net">mewl hayabusa</a></td> <td><a href="https://www.patreon.com/hs_sh_net">mewl hayabusa</a></td>
<td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td> <td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td>
<td><a href="https://www.patreon.com/user?u=28779508">S Y</a></td>
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28779508/3cd4cb7f017f4ee0864341e3464d42f9/1.png?token-time=2145916800&token-hash=eGQtR15be44kgvh8fw2Jx8Db4Bv15YBp2ldxh0EKRxA%3D" alt="S Y" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/16542964" alt="Takumi Sugita" width="100"></td> <td><img src="https://c8.patreon.com/2/200/16542964" alt="Takumi Sugita" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/17866454" alt="sikyosyounin" width="100"></td> <td><img src="https://c8.patreon.com/2/200/17866454" alt="sikyosyounin" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3.png?token-time=2145916800&token-hash=KjfQL8nf3AIf6WqzLshBYAyX44piAqOAZiYXgZS_H6A%3D" alt="YUKIMOCHI" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3.png?token-time=2145916800&token-hash=KjfQL8nf3AIf6WqzLshBYAyX44piAqOAZiYXgZS_H6A%3D" alt="YUKIMOCHI" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/26340354/08834cf767b3449e93098ef73a434e2f/2.png?token-time=2145916800&token-hash=nyM8DnKRL8hR47HQ619mUzsqVRpkWZjgtgBU9RY15Uc%3D" alt="totokoro" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/26340354/08834cf767b3449e93098ef73a434e2f/2.png?token-time=2145916800&token-hash=nyM8DnKRL8hR47HQ619mUzsqVRpkWZjgtgBU9RY15Uc%3D" alt="totokoro" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/2.jpeg?token-time=2145916800&token-hash=_sTj3dUBOhn9qwiJ7F19Qd-yWWfUqJC_0jG1h0agEqQ%3D" alt="sheeta.s" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/2.jpeg?token-time=2145916800&token-hash=_sTj3dUBOhn9qwiJ7F19Qd-yWWfUqJC_0jG1h0agEqQ%3D" alt="sheeta.s" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5827393/59893c191dda408f9cabd0f20a3a5627/1.jpeg?token-time=2145916800&token-hash=i9N05vOph-eP1LTLb9_npATjYOpntL0ZsHNaZFSsPmE%3D" alt="motcha" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5827393/59893c191dda408f9cabd0f20a3a5627/1.jpeg?token-time=2145916800&token-hash=i9N05vOph-eP1LTLb9_npATjYOpntL0ZsHNaZFSsPmE%3D" alt="motcha" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/20494440/540beaf2445f408ea6597bc61e077bb3/1.png?token-time=2145916800&token-hash=UJ0JQge64Bx9XmN_qYA1inMQhrWf4U91fqz7VAKJeSg%3D" alt="axtuki1" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1.jpg?token-time=2145916800&token-hash=nVAntpybQrznE0rg05keLrSE6ogPKJXB13rmrJng42c%3D" alt="takimura" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1.jpg?token-time=2145916800&token-hash=nVAntpybQrznE0rg05keLrSE6ogPKJXB13rmrJng42c%3D" alt="takimura" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13100201/fc5be4fa90444f09a9c8a06f72385272/1.png?token-time=2145916800&token-hash=i8PjlgfOB2LPEdbtWyx8ZPsBKhGcNZqcw_FQmH71UGU%3D" alt="aqz tamaina" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/user?u=28779508">S Y</a></td>
<td><a href="https://www.patreon.com/user?u=16542964">Takumi Sugita</a></td> <td><a href="https://www.patreon.com/user?u=16542964">Takumi Sugita</a></td>
<td><a href="https://www.patreon.com/user?u=17866454">sikyosyounin</a></td> <td><a href="https://www.patreon.com/user?u=17866454">sikyosyounin</a></td>
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td> <td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
<td><a href="https://www.patreon.com/user?u=26340354">totokoro</a></td> <td><a href="https://www.patreon.com/user?u=26340354">totokoro</a></td>
<td><a href="https://www.patreon.com/user?u=19356899">sheeta.s</a></td> <td><a href="https://www.patreon.com/user?u=19356899">sheeta.s</a></td>
<td><a href="https://www.patreon.com/user?u=5827393">motcha</a></td> <td><a href="https://www.patreon.com/user?u=5827393">motcha</a></td>
<td><a href="https://www.patreon.com/user?u=20494440">axtuki1</a></td>
<td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td> <td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td>
<td><a href="https://www.patreon.com/takimura">takimura</a></td> <td><a href="https://www.patreon.com/takimura">takimura</a></td>
<td><a href="https://www.patreon.com/aqz">aqz tamaina</a></td>
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13100201/fc5be4fa90444f09a9c8a06f72385272/1.png?token-time=2145916800&token-hash=i8PjlgfOB2LPEdbtWyx8ZPsBKhGcNZqcw_FQmH71UGU%3D" alt="aqz tamaina" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/619ab87cc08448439222631ebb26802f/1.gif?token-time=2145916800&token-hash=o27K7M02s1z-LkDUEO5Oa7cu-GviRXeOXxryi4o_6VU%3D" alt="Atsuko Tominaga" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/619ab87cc08448439222631ebb26802f/1.gif?token-time=2145916800&token-hash=o27K7M02s1z-LkDUEO5Oa7cu-GviRXeOXxryi4o_6VU%3D" alt="Atsuko Tominaga" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpg?token-time=2145916800&token-hash=7bkMqTwHPRsJPGAq42PYdDXDZBVGLqdgr1ZmBxX8GFQ%3D" alt="Hekovic" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpg?token-time=2145916800&token-hash=7bkMqTwHPRsJPGAq42PYdDXDZBVGLqdgr1ZmBxX8GFQ%3D" alt="Hekovic" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24641572/b4fd175424814f15b0ca9178d2d2d2e4/1.png?token-time=2145916800&token-hash=e2fyqdbuJbpCckHcwux7rbuW6OPkKdERcus0u2wIEWU%3D" alt="uroco @99" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24641572/b4fd175424814f15b0ca9178d2d2d2e4/1.png?token-time=2145916800&token-hash=e2fyqdbuJbpCckHcwux7rbuW6OPkKdERcus0u2wIEWU%3D" alt="uroco @99" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1.jpeg?token-time=2145916800&token-hash=L55UhJ0rcuNAH3w_ryeeGN4hC6taoOixyAhraEi0bzw%3D" alt="dansup" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/aqz">aqz tamaina</a></td>
<td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td> <td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td>
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td> <td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/noellabo">noellabo</a></td> <td><a href="https://www.patreon.com/noellabo">noellabo</a></td>
<td><a href="https://www.patreon.com/Corset">CG</a></td> <td><a href="https://www.patreon.com/Corset">CG</a></td>
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td> <td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
<td><a href="https://www.patreon.com/user?u=24641572">uroco @99</a></td> <td><a href="https://www.patreon.com/user?u=24641572">uroco @99</a></td>
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1.jpeg?token-time=2145916800&token-hash=L55UhJ0rcuNAH3w_ryeeGN4hC6taoOixyAhraEi0bzw%3D" alt="dansup" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/23932002" alt="nenohi" width="100"></td> <td><img src="https://c8.patreon.com/2/200/23932002" alt="nenohi" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td> <td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
<td><a href="https://www.patreon.com/user?u=23932002">nenohi</a></td> <td><a href="https://www.patreon.com/user?u=23932002">nenohi</a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table> </tr></table>
**Last updated:** Wed, 05 Feb 2020 00:42:12 UTC **Last updated:** Fri, 21 Feb 2020 13:39:06 UTC
<!-- PATREON_END --> <!-- PATREON_END -->
[backer-url]: #backers [backer-url]: #backers

View File

@ -1,2 +1,111 @@
--- ---
_lang_: "Deutsch" _lang_: "Deutsch"
monthAndDay: "{day}/{month}"
search: "Suchen"
notifications: "Benachrichtigungen"
username: "Benutzername"
password: "Passwort"
fetchingAsApObject: "Aus Fediverse holen"
ok: "OK"
gotIt: "Verstanden!"
cancel: "Abbrechen"
enterUsername: "Benutzername eingeben"
renotedBy: "Renote von {user}"
noNotes: "Keine Notizen"
noNotifications: "Keine Benachrichtigungen"
instance: "Instanz"
settings: "Einstellungen"
profile: "Profil"
timeline: "Zeitleiste"
noAccountDescription: "Keine Selbsteinführung"
login: "Einloggen"
loggingIn: "Einloggen in bearbeitung"
logout: "Ausloggen"
signup: "Registrieren"
uploading: "Upload läuft"
save: "Speichern"
users: "Benutzer"
addUser: "Benutzer hinzufügen"
favorite: "Favoriten"
favorites: "Favoriten"
unfavorite: "Aus Favoriten entfernen"
pin: "Anheften"
unpin: "Lösen"
copyContent: "Inhalt kopieren"
copyLink: "Link kopieren"
delete: "Löschen"
addToList: "Zur Liste hinzufügen"
sendMessage: "Nachricht senden"
copyUsername: "Benutzernamen kopieren"
reply: "Antworten"
loadMore: "Zeige mehr"
youGotNewFollower: "Sie haben einen neuen Follower"
receiveFollowRequest: "Follow Request erhalten."
followRequestAccepted: "FollowRequestAkzeptiert"
mentions: "Erwähnungen"
directNotes: "Direktnachrichten"
importAndExport: "Importieren und Exportieren"
import: "Importieren"
export: "Exportieren"
files: "Dateien"
download: "Download"
lists: "Listen"
noLists: "Keine Liste!"
note: "Noten"
following: "Folgen"
followers: "Folgende"
manageLists: "Liste verwalten"
error: "Ein Problem ist aufgetreten"
retry: "Wiederholen"
privacy: "Privatsphäre"
defaultNoteVisibility: "Die Standardsichtbarkeit"
follow: "Folgen"
followRequest: "Follower-Anfragen"
followRequests: "Follower-Anfragen"
unfollow: "Nicht mehr folgen"
followRequestPending: "Ausstehend"
clickToShow: "Klicke zum den Inhalt anzusehen"
sensitive: "Dieser Inhalt ist NSFW"
add: "Hinzufügen"
reaction: "Reaktionen"
selectUser: "Benutzer wählen"
instances: "Instanz"
mutedUsers: "Stummgestellte Benutzer"
blockedUsers: "Blockierte Benutzer"
noUsers: "Keine Benutzer"
remove: "Löschen"
nsfw: "Dieser Inhalt ist NSFW"
userList: "Listen"
_sfx:
notification: "Benachrichtigungen"
_widgets:
notifications: "Benachrichtigungen"
timeline: "Zeitleiste"
_cw:
show: "Zeige mehr"
_visibility:
followers: "Folgende"
_profile:
username: "Benutzername"
_exportOrImport:
followingList: "Folgen"
userLists: "Listen"
_pages:
script:
categories:
list: "Listen"
blocks:
_join:
arg1: "Listen"
_randomPick:
arg1: "Listen"
_dailyRandomPick:
arg1: "Listen"
_seedRandomPick:
arg2: "Listen"
_pick:
arg1: "Listen"
_listLen:
arg1: "Listen"
types:
array: "Listen"

View File

@ -239,6 +239,8 @@ avatar: "Avatar"
banner: "Banner" banner: "Banner"
nsfw: "NSFW" nsfw: "NSFW"
disconnectedFromServer: "Connection to the server was inturrupted" disconnectedFromServer: "Connection to the server was inturrupted"
reload: "Refresh"
doNothing: "Ignore"
reloadConfirm: "Would you like to retry?" reloadConfirm: "Would you like to retry?"
watch: "Watch" watch: "Watch"
unwatch: "Undo Watch" unwatch: "Undo Watch"
@ -252,9 +254,9 @@ tosUrl: "Terms of Service URL"
thisYear: "Year" thisYear: "Year"
thisMonth: "Month" thisMonth: "Month"
today: "Today" today: "Today"
dayX: "{day} days" dayX: "{day}"
monthX: "{month} months" monthX: "{month}"
yearX: "{year} years" yearX: "{year} /"
pages: "Pages" pages: "Pages"
integration: "Integration" integration: "Integration"
connectSerice: "Connect" connectSerice: "Connect"
@ -283,7 +285,8 @@ antennas: "Antennas"
manageAntennas: "Manage Antennas" manageAntennas: "Manage Antennas"
name: "Name" name: "Name"
antennaSource: "Antenna source" antennaSource: "Antenna source"
antennaKeywords: "Antenna keywords" antennaKeywords: "Keywords to receive"
antennaExcludeKeywords: "Keywords to exclude"
antennaKeywordsDescription: "Separate with spaces for AND condition. Separate with line breaks for OR." antennaKeywordsDescription: "Separate with spaces for AND condition. Separate with line breaks for OR."
notifyAntenna: "Notify newer notes" notifyAntenna: "Notify newer notes"
withFileAntenna: "Filter only notes with file attached" withFileAntenna: "Filter only notes with file attached"
@ -400,6 +403,41 @@ docSource: "Source of this document"
createAccount: "Create account" createAccount: "Create account"
existingAcount: "Existing accounts" existingAcount: "Existing accounts"
regenerate: "Regenerate" regenerate: "Regenerate"
fontSize: "Font size"
noFollowRequests: "You don't have any pending follow requests"
openImageInNewTab: "Open image in new tab"
dashboard: "Dashboard"
local: "Local"
remote: "Remote"
total: "Total"
weekOverWeekChanges: "Weekly"
dayOverDayChanges: "Daily"
accessibility: "Accessibility"
clinetSettings: "Client Settings"
accountSettings: "Account Settings"
promotion: "Promoted"
promote: "Promote"
numberOfDays: "Amount of days"
hideThisNote: "Hide this note"
showFeaturedNotesInTimeline: "Show Featured notes in Timeline"
objectStorage: "Object Storage"
useObjectStorage: "Use object storage"
serverLogs: "Server logs"
deleteAll: "Delete all"
showFixedPostForm: "Display the posting form at the top of the timeline"
newNoteRecived: "You've got a new note"
useNotificationsPopup: "Display notification list in popup"
sounds: "Sounds"
listen: "Listen"
none: "None"
volume: "Volume"
_sfx:
note: "New note"
noteMy: "My note"
notification: "Notifications"
chat: "Messaging"
chatBg: "Messaging (Background)"
antenna: "Antenna Reception"
_ago: _ago:
unknown: "Unknown" unknown: "Unknown"
future: "Future" future: "Future"
@ -426,7 +464,7 @@ _tutorial:
step3_1: "Finished setting up your profile?" step3_1: "Finished setting up your profile?"
step3_2: "The next step is to post a note. You can do this by pressing a pencil icon on the screen." step3_2: "The next step is to post a note. You can do this by pressing a pencil icon on the screen."
step3_3: "Fill in the modal and press the button on the right top to post." step3_3: "Fill in the modal and press the button on the right top to post."
step3_4: "Have nothing to say? Try \"I just started Misskey!\"" step3_4: "Have nothing to say? Try \"just setting up my msky\"!"
step4_1: "Finished posting your first note?" step4_1: "Finished posting your first note?"
step4_2: "Hurray! Now your first note is displayed on your timeline." step4_2: "Hurray! Now your first note is displayed on your timeline."
step5_1: "Now, let's try making your timeline more lively by following other people." step5_1: "Now, let's try making your timeline more lively by following other people."
@ -500,6 +538,8 @@ _widgets:
trends: "Trending" trends: "Trending"
clock: "Clock" clock: "Clock"
rss: "RSS reader" rss: "RSS reader"
activity: "Activity"
photos: "Photos"
_cw: _cw:
hide: "Hide" hide: "Hide"
show: "Load more" show: "Load more"

View File

@ -239,6 +239,8 @@ avatar: "Avatar"
banner: "Banner" banner: "Banner"
nsfw: "Marcado como sensible" nsfw: "Marcado como sensible"
disconnectedFromServer: "Desconectado del servidor" disconnectedFromServer: "Desconectado del servidor"
reload: "Recargar"
doNothing: "No hacer nada"
reloadConfirm: "¿Desea recargar?" reloadConfirm: "¿Desea recargar?"
watch: "Ver" watch: "Ver"
unwatch: "Dejar de ver" unwatch: "Dejar de ver"
@ -283,7 +285,8 @@ antennas: "Antenas"
manageAntennas: "Administrar antenas" manageAntennas: "Administrar antenas"
name: "Nombre" name: "Nombre"
antennaSource: "Origen de la antena" antennaSource: "Origen de la antena"
antennaKeywords: "Palabras clave de la antena" antennaKeywords: "Palabras clave para recibir"
antennaExcludeKeywords: "Palabras clave para excluir"
antennaKeywordsDescription: "Separar con espacios es una declaración AND, separar con una linea nueva es una declaración OR" antennaKeywordsDescription: "Separar con espacios es una declaración AND, separar con una linea nueva es una declaración OR"
notifyAntenna: "Notificar nueva nota" notifyAntenna: "Notificar nueva nota"
withFileAntenna: "Sólo notas con archivos adjuntados" withFileAntenna: "Sólo notas con archivos adjuntados"
@ -400,6 +403,41 @@ docSource: "Fuente de este documento"
createAccount: "Crear cuenta" createAccount: "Crear cuenta"
existingAcount: "Cuentas existentes" existingAcount: "Cuentas existentes"
regenerate: "Regenerar" regenerate: "Regenerar"
fontSize: "Tamaño de la letra"
noFollowRequests: "No hay solicitudes de seguimiento"
openImageInNewTab: "Abrir imagen en nueva pestaña"
dashboard: "Panel de control"
local: "Local"
remote: "Remoto"
total: "Total"
weekOverWeekChanges: "Dif semanal"
dayOverDayChanges: "Dif diaria"
accessibility: "Accesibilidad"
clinetSettings: "Ajustes del cliente"
accountSettings: "Ajustes de cuenta"
promotion: "Promovido"
promote: "Promover"
numberOfDays: "Cantidad de dias"
hideThisNote: "Ocultar esta nota"
showFeaturedNotesInTimeline: "Mostrar notas destacadas en la línea de tiempo"
objectStorage: "Almacenamiento de objetos"
useObjectStorage: "Usar almacenamiento de objetos"
serverLogs: "Registros del servidor"
deleteAll: "Eliminar todos"
showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo"
newNoteRecived: "Tienes una nota nuevo"
useNotificationsPopup: "Mostrar lista de notificaciones en ventana emergente"
sounds: "Sonidos"
listen: "Escuchar"
none: "Ninguna"
volume: "Volumen"
_sfx:
note: "Notas"
noteMy: "Nota (a mí mismo)"
notification: "Notificaciones"
chat: "Chat"
chatBg: "Chat (Fondo)"
antenna: "Antena receptora"
_ago: _ago:
unknown: "Desconocido" unknown: "Desconocido"
future: "Futuro" future: "Futuro"
@ -500,6 +538,8 @@ _widgets:
trends: "Tendencias" trends: "Tendencias"
clock: "Reloj" clock: "Reloj"
rss: "Lector RSS" rss: "Lector RSS"
activity: "Actividad"
photos: "Fotos"
_cw: _cw:
hide: "Ocultar" hide: "Ocultar"
show: "Ver más" show: "Ver más"

View File

@ -239,6 +239,8 @@ avatar: "Avatar"
banner: "Bannière" banner: "Bannière"
nsfw: "Contenu sensible" nsfw: "Contenu sensible"
disconnectedFromServer: "Déconnecté du serveur" disconnectedFromServer: "Déconnecté du serveur"
reload: "Rafraîchir"
doNothing: "Ignorer"
reloadConfirm: "Voulez-vous recharger?" reloadConfirm: "Voulez-vous recharger?"
watch: "Surveiller" watch: "Surveiller"
unwatch: "Ne plus surveiller" unwatch: "Ne plus surveiller"
@ -283,7 +285,8 @@ antennas: "Antenne"
manageAntennas: "Gestion d'antenne" manageAntennas: "Gestion d'antenne"
name: "Nom" name: "Nom"
antennaSource: "Recevoir la source" antennaSource: "Recevoir la source"
antennaKeywords: "Mots clés entrants" antennaKeywords: "Mots clés à recevoir"
antennaExcludeKeywords: "Mots clés à exclure"
antennaKeywordsDescription: "Lorsqu'il est séparé par un espace, il devient une spécification ET, et lorsqu'il est séparé par un saut de ligne, il devient une spécification OU." antennaKeywordsDescription: "Lorsqu'il est séparé par un espace, il devient une spécification ET, et lorsqu'il est séparé par un saut de ligne, il devient une spécification OU."
notifyAntenna: "Notifier les nouvelles notes" notifyAntenna: "Notifier les nouvelles notes"
withFileAntenna: "Notes uniquement avec fichiers joints" withFileAntenna: "Notes uniquement avec fichiers joints"
@ -400,6 +403,41 @@ docSource: "Source de ce document"
createAccount: "Créer compte" createAccount: "Créer compte"
existingAcount: "Comptes existants" existingAcount: "Comptes existants"
regenerate: "Régénérer" regenerate: "Régénérer"
fontSize: "Taille de la police"
noFollowRequests: "Vous n'avez aucune demandes d'abonnement en attente"
openImageInNewTab: "Ouvrir l'image dans un nouvel onglet"
dashboard: "Tableau de bord"
local: "Local"
remote: "Distant"
total: "Total"
weekOverWeekChanges: "Diff hebdo"
dayOverDayChanges: "Diff quotidien"
accessibility: "Accessibilité"
clinetSettings: "Paramètres du client"
accountSettings: "Paramètres du compte"
promotion: "Promu"
promote: "Promouvoir"
numberOfDays: "Nombre de jours"
hideThisNote: "Masquer cette note"
showFeaturedNotesInTimeline: "Afficher les notes en vedette dans Fil d'actualité"
objectStorage: "Stockage d'objets"
useObjectStorage: "Utiliser le stockage d'objets"
serverLogs: "Journaux serveur"
deleteAll: "Supprimer tout"
showFixedPostForm: "Afficher le formulaire en haut du fil d'actualité"
newNoteRecived: "Vous avez un nouveau note"
useNotificationsPopup: "Afficher la liste des notifications dans une fenêtre contextuelle"
sounds: "Sons"
listen: "Écouter"
none: "Rien"
volume: "Volume"
_sfx:
note: "Nouvelle note"
noteMy: "Ma note"
notification: "Notifications"
chat: "Discuter"
chatBg: "Discuter (De fond)"
antenna: "Réception d'antenne"
_ago: _ago:
unknown: "Inconnu" unknown: "Inconnu"
future: "Futur" future: "Futur"
@ -419,6 +457,16 @@ _time:
_tutorial: _tutorial:
title: "Comment utiliser Misskey" title: "Comment utiliser Misskey"
step1_1: "Bienvenue," step1_1: "Bienvenue,"
step1_2: "Cette page est appelée \"timeline\". Elle montre les \"notes\" des personnes que vous \"suivez\" dans l'ordre chronologique."
step1_3: "Vous n'avez pas encore posté de notes ou ne suivez personne, vous ne devriez donc rien voir dans la chronologie."
step2_1: "Finissons de créer votre profil avant d'écrire une note ou de suivre quelqu'un."
step2_2: "En fournissant quelques informations sur vous, il sera plus facile pour les autres de vous suivre."
step3_1: "Vous avez fini de créer votre profil ?"
step3_2: "Létape suivante consiste à créer une note. Vous pouvez commencer en cliquant sur licône crayon sur lécran."
step3_3: "Remplissez le cadran et cliquez sur le bouton en haut à droite pour envoyer."
step3_4: "Vous n'avez rien à dire ? Essayez de dire \"J'ai commencé à utiliser Misskey\"."
step4_1: "Avez-vous posté votre première notes ?"
step4_2: "Votre première note est maintenant affichée sur votre timeline."
_2fa: _2fa:
alreadyRegistered: "Cette étape à déjà été complétée" alreadyRegistered: "Cette étape à déjà été complétée"
registerDevice: "Sinscrire l'appareil" registerDevice: "Sinscrire l'appareil"
@ -480,6 +528,8 @@ _widgets:
trends: "Tendances" trends: "Tendances"
clock: "Horloge" clock: "Horloge"
rss: "Lecteur de flux RSS" rss: "Lecteur de flux RSS"
activity: "Activités"
photos: "Photos"
_cw: _cw:
hide: "Masquer" hide: "Masquer"
show: "Voir plus" show: "Voir plus"

View File

@ -239,6 +239,8 @@ avatar: "アイコン"
banner: "バナー" banner: "バナー"
nsfw: "閲覧注意" nsfw: "閲覧注意"
disconnectedFromServer: "サーバーから切断されました" disconnectedFromServer: "サーバーから切断されました"
reload: "リロード"
doNothing: "なにもしない"
reloadConfirm: "リロードしますか?" reloadConfirm: "リロードしますか?"
watch: "ウォッチ" watch: "ウォッチ"
unwatch: "ウォッチ解除" unwatch: "ウォッチ解除"
@ -284,6 +286,7 @@ manageAntennas: "アンテナの管理"
name: "名前" name: "名前"
antennaSource: "受信ソース" antennaSource: "受信ソース"
antennaKeywords: "受信キーワード" antennaKeywords: "受信キーワード"
antennaExcludeKeywords: "除外キーワード"
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
notifyAntenna: "新しいノートを通知する" notifyAntenna: "新しいノートを通知する"
withFileAntenna: "ファイルが添付されたノートのみ" withFileAntenna: "ファイルが添付されたノートのみ"
@ -401,6 +404,42 @@ createAccount: "アカウントを作成"
existingAcount: "既存のアカウント" existingAcount: "既存のアカウント"
regenerate: "再生成" regenerate: "再生成"
fontSize: "フォントサイズ" fontSize: "フォントサイズ"
noFollowRequests: "フォロー申請はありません"
openImageInNewTab: "画像を新しいタブで開く"
dashboard: "ダッシュボード"
local: "ローカル"
remote: "リモート"
total: "合計"
weekOverWeekChanges: "前週比"
dayOverDayChanges: "前日比"
accessibility: "アクセシビリティ"
clinetSettings: "クライアント設定"
accountSettings: "アカウント設定"
promotion: "プロモーション"
promote: "プロモート"
numberOfDays: "日数"
hideThisNote: "このノートを非表示"
showFeaturedNotesInTimeline: "タイムラインにおすすめのノートを表示する"
objectStorage: "オブジェクトストレージ"
useObjectStorage: "オブジェクトストレージを使用"
serverLogs: "サーバーログ"
deleteAll: "全て削除"
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
newNoteRecived: "新しいノートがあります"
useNotificationsPopup: "通知一覧をポップアップで表示"
sounds: "サウンド"
listen: "聴く"
none: "なし"
volume: "音量"
details: "詳細"
_sfx:
note: "ノート"
noteMy: "ノート(自分)"
notification: "通知"
chat: "チャット"
chatBg: "チャット(バックグラウンド)"
antenna: "アンテナ受信"
_ago: _ago:
unknown: "謎" unknown: "謎"
@ -510,6 +549,8 @@ _widgets:
trends: "トレンド" trends: "トレンド"
clock: "時計" clock: "時計"
rss: "RSSリーダー" rss: "RSSリーダー"
activity: "アクティビティ"
photos: "フォト"
_cw: _cw:
hide: "隠す" hide: "隠す"

View File

@ -109,6 +109,8 @@ aboutMisskey: "Misskeyってなんや"
notFoundDescription: "指定されたURLに該当するページはあらへんやった。" notFoundDescription: "指定されたURLに該当するページはあらへんやった。"
close: "さいなら" close: "さいなら"
joinedGroups: "参加しとるグループ" joinedGroups: "参加しとるグループ"
_sfx:
notification: "通知"
_ago: _ago:
unknown: "謎" unknown: "謎"
future: "未来" future: "未来"

View File

@ -1,2 +1,65 @@
--- ---
_lang_: "ಕನ್ನಡ" _lang_: "ಕನ್ನಡ"
introMisskey: "ಸ್ವಾಗತ! Misskey ಓಪನ್ ಸೋರ್ಸ್ ಒಕ್ಕೂಟ ಮೈಕ್ರೋಬ್ಲಾಗಿಂಗ್ ಸೇವೆಯಾಗಿದೆ.\n ಏನಾಗುತ್ತಿದೆ ಎಂಬುದನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ಅಥವಾ ನಿಮ್ಮ ಬಗ್ಗೆ ಎಲ್ಲರಿಗೂ ಹೇಳಲು \"ಟಿಪ್ಪಣಿ\"ಗಳನ್ನು ರಚಿಸಿ📡\n \"ಸ್ಪಂದನೆ\" ಕ್ರಿಯೆಯೊಂದಿಗೆ, ನೀವು ಎಲ್ಲರ ಟಿಪ್ಪಣಿಗಳಿಗೆ ತ್ವರಿತವಾಗಿ ಸ್ಪಂದನೆಗಳನ್ನು ಕೂಡ ಸೇರಿಸಬಹುದು.👍\n ಹೊಸ ಜಗತ್ತನ್ನು ಅನ್ವೇಷಿಸಿ🚀"
monthAndDay: "{month}ನೇ ತಿಂಗಳ {day}ನೇ ದಿನ"
search: "ಹುಡುಕು"
notifications: "ಅಧಿಸೂಚನೆಗಳು"
username: "ಬಳಕೆಹೆಸರು"
password: "ಗುಪ್ತಪದ"
fetchingAsApObject: "ಒಕ್ಕೂಟದಿಂದ ಪಡೆಯಲಾಗುತ್ತಿದೆ..."
ok: "ಸರಿ"
gotIt: "ಅರ್ಥವಾಯಿತು!"
cancel: "ರದ್ದು"
enterUsername: "ಬಳಕೆಹೆಸರನ್ನು ಭರ್ತಿ ಮಾಡಿ"
renotedBy: "{user} ಪುನರಾವರ್ತಿಸಿದರು"
noNotes: "ಟಿಪ್ಪಣಿಗಳಿಲ್ಲ"
noNotifications: "ಅಧಿಸೂಚನೆಗಳಿಲ್ಲ"
instance: "ನಿದರ್ಶನ"
settings: "ಸಿದ್ಧತೆಗಳು"
profile: "ಪ್ರೊಫೈಲು"
timeline: "ಸಮಯಸಾಲು"
noAccountDescription: "ಇವರು ಸ್ವಯಂ ಪರಿಚಯ ರಚಿಸಿಲ್ಲ"
login: "ಪ್ರವೇಶ"
loggingIn: "ಪ್ರವೇಶಿಸುತ್ತಾ..."
logout: "ಆಚೆಗೆ"
signup: "ನೋಂದಣಿ"
uploading: "ಅಪ್‌ಲೋಡಾಗುತ್ತಿದೆ"
save: "ಉಳಿಸಿ"
users: "ಬಳಕೆದಾರ"
addUser: "ಬಳಕೆದಾರರನ್ನು ಸೇರಿಸಿ"
favorite: "ಮೆಚ್ಚಿನ"
favorites: "ಮೆಚ್ಚಿನವುಗಳು"
unfavorite: "ಮೆಚ್ಚುಗೆ ಅಳಿಸು"
pin: "ಪ್ರೊಫ಼ೈಲಿಗೆ ಅಂಟಿಸು"
unpin: "ಪ್ರೊಫ಼ೈಲಿಂದ ಅಂಟುತೆಗೆ"
copyContent: "ವಿಷಯವನ್ನು ನಕಲಿಸು"
copyLink: "ಲಿಂಕನ್ನು ನಕಲಿಸು"
delete: "ಅಳಿಸು"
addToList: "ಪಟ್ಟಿಗೆ ಸೇರಿಸು"
sendMessage: "ಸಂದೇಶ ಕಳುಹಿಸು"
copyUsername: "ಬಳಕೆಹೆಸರು ನಕಲಿಸು"
reply: "ಉತ್ತರಿಸು"
loadMore: "ಇನ್ನಷ್ಟು ನೋಡು"
youGotNewFollower: "ಹಿಂಬಾಲಿಸಿದರು"
receiveFollowRequest: "ಹಿಂಬಾಲನೆ ವಿನಂತಿ ಬಂದಿದೆ"
followRequestAccepted: "ಹಿಂಬಾಲನೆ ವಿನಂತಿ ಸ್ವೀಕರಿಸಲಾಯಿತು"
mentions: "ಹೆಸರಿಸಿದ"
directNotes: "ನೇರ ಟಿಪ್ಪಣಿಗಳು"
importAndExport: "ಆಮದು/ರಫ್ತು"
import: "ಆಮದು"
export: "ರಫ್ತು"
files: "ಕಡತಗಳು"
download: "ಜಾಲದಿಂದಿಳಿಸು"
driveFileDeleteConfirm: "\"{name}\" ಕಡತವನ್ನು ಅಳಿಸಲು ನೀವು ಬಯಸುವಿರಾ? ಈ ನೋಡಿರಿ ಲಗತ್ತಿಸಲಾದ ಟಿಪ್ಪಣಿ ಸಹ ಕಣ್ಮರೆಯಾಗುತ್ತದೆ."
unfollowConfirm: "{name}ಅನ್ನು ಹಿಂಬಾಲಿಸದಿರುವುದೇ?"
instances: "ನಿದರ್ಶನ"
remove: "ಅಳಿಸು"
_sfx:
notification: "ಅಧಿಸೂಚನೆಗಳು"
_widgets:
notifications: "ಅಧಿಸೂಚನೆಗಳು"
timeline: "ಸಮಯಸಾಲು"
_cw:
show: "ಇನ್ನಷ್ಟು ನೋಡು"
_profile:
username: "ಬಳಕೆಹೆಸರು"

View File

@ -106,8 +106,8 @@ customEmojis: "커스텀 이모지"
emojiName: "이모지 이름" emojiName: "이모지 이름"
emojiUrl: "이모지 URL" emojiUrl: "이모지 URL"
addEmoji: "이모지 추가" addEmoji: "이모지 추가"
cacheRemoteFiles: "원격 파일을 캐시" cacheRemoteFiles: "리모트 파일을 캐시"
cacheRemoteFilesDescription: "이 설정을 해지하면 원격 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다." cacheRemoteFilesDescription: "이 설정을 해지하면 리모트 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다."
flagAsBot: "나는 봇입니다" flagAsBot: "나는 봇입니다"
flagAsCat: "나는 고양이다냥" flagAsCat: "나는 고양이다냥"
autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락" autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락"
@ -154,7 +154,7 @@ clearQueue: "대기열 비우기"
clearQueueConfirmTitle: "대기열을 비우시겠습니까?" clearQueueConfirmTitle: "대기열을 비우시겠습니까?"
clearQueueConfirmText: "대기열에 남아 있는 노트는 더이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다." clearQueueConfirmText: "대기열에 남아 있는 노트는 더이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다."
clearCachedFiles: "캐시 비우기" clearCachedFiles: "캐시 비우기"
clearCachedFilesConfirm: "캐시된 원격 파일을 모두 삭제하시겠습니까?" clearCachedFilesConfirm: "캐시된 리모트 파일을 모두 삭제하시겠습니까?"
blockedInstances: "차단된 인스턴스" blockedInstances: "차단된 인스턴스"
blockedInstancesDescription: "차단하려는 인스턴스의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다." blockedInstancesDescription: "차단하려는 인스턴스의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다."
muteAndBlock: "뮤트 및 차단" muteAndBlock: "뮤트 및 차단"
@ -239,6 +239,8 @@ avatar: "아바타"
banner: "배너" banner: "배너"
nsfw: "열람주의" nsfw: "열람주의"
disconnectedFromServer: "서버와의 연결이 끊어졌습니다" disconnectedFromServer: "서버와의 연결이 끊어졌습니다"
reload: "새로고침"
doNothing: "무시하기"
reloadConfirm: "새로고침 하시겠습니까?" reloadConfirm: "새로고침 하시겠습니까?"
watch: "지켜보기" watch: "지켜보기"
unwatch: "지켜보기 해제" unwatch: "지켜보기 해제"
@ -253,7 +255,7 @@ thisYear: "올해"
thisMonth: "이번 달" thisMonth: "이번 달"
today: "오늘" today: "오늘"
dayX: "{day}일" dayX: "{day}일"
monthX: "{month}월" monthX: "{month}월"
yearX: "{year}년" yearX: "{year}년"
pages: "페이지" pages: "페이지"
integration: "연동" integration: "연동"
@ -265,8 +267,8 @@ disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리
registration: "등록" registration: "등록"
enableRegistration: "신규 회원가입을 활성화" enableRegistration: "신규 회원가입을 활성화"
invite: "초대" invite: "초대"
proxyRemoteFiles: "원격 파일 프록시" proxyRemoteFiles: "리모트 파일 프록시"
proxyRemoteFilesDescription: "이 설정을 활성화할 경우, 저장되지 않았거나 저장용량 초과로 삭제된 원격 파일을 로컬에서 프록시하여 썸네일을 생성하게 됩니다. 서버의 스토리지에는 영향을 주지 않습니다." proxyRemoteFilesDescription: "이 설정을 활성화할 경우, 저장되지 않았거나 저장용량 초과로 삭제된 리모트 파일을 로컬에서 프록시하여 썸네일을 생성하게 됩니다. 서버의 스토리지에는 영향을 주지 않습니다."
driveCapacityPerLocalAccount: "로컬 유저 한 명당 드라이브 용량" driveCapacityPerLocalAccount: "로컬 유저 한 명당 드라이브 용량"
driveCapacityPerRemoteAccount: "리모트 유저 한 명당 드라이브 용량" driveCapacityPerRemoteAccount: "리모트 유저 한 명당 드라이브 용량"
inMb: "메가바이트 단위" inMb: "메가바이트 단위"
@ -284,6 +286,7 @@ manageAntennas: "안테나 관리"
name: "이름" name: "이름"
antennaSource: "받을 소스" antennaSource: "받을 소스"
antennaKeywords: "받을 키워드" antennaKeywords: "받을 키워드"
antennaExcludeKeywords: "제외할 키워드"
antennaKeywordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다" antennaKeywordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다"
notifyAntenna: "새로운 노트를 알림" notifyAntenna: "새로운 노트를 알림"
withFileAntenna: "파일이 첨부된 노트만" withFileAntenna: "파일이 첨부된 노트만"
@ -400,6 +403,41 @@ docSource: "이 문서의 소스"
createAccount: "계정 만들기" createAccount: "계정 만들기"
existingAcount: "기존 계정" existingAcount: "기존 계정"
regenerate: "다시 생성" regenerate: "다시 생성"
fontSize: "글자 크기"
noFollowRequests: "처리되지 않은 팔로우 요청이 없습니다"
openImageInNewTab: "새 탭에서 이미지 열기"
dashboard: "대시보드"
local: "로컬"
remote: "리모트"
total: "합계"
weekOverWeekChanges: "지난주보다"
dayOverDayChanges: "어제보다"
accessibility: "접근성"
clinetSettings: "클라이언트 설정"
accountSettings: "계정 설정"
promotion: "프로모션"
promote: "프로모션하기"
numberOfDays: "며칠동안"
hideThisNote: "이 노트를 숨기기"
showFeaturedNotesInTimeline: "타임라인에 추천 노트를 표시"
objectStorage: "오브젝트 스토리지"
useObjectStorage: "오브젝트 스토리지를 사용"
serverLogs: "서버 로그"
deleteAll: "모두 삭제"
showFixedPostForm: "타임라인 상단에 글 작성란을 표시"
newNoteRecived: "새 노트가 있습니다"
useNotificationsPopup: "알림 목록을 팝업으로 표시"
sounds: "소리"
listen: "듣기"
none: "없음"
volume: "음량"
_sfx:
note: "새 노트"
noteMy: "내 노트"
notification: "알림"
chat: "대화"
chatBg: "대화 (백그라운드)"
antenna: "안테나 수신"
_ago: _ago:
unknown: "알 수 없음" unknown: "알 수 없음"
future: "미래" future: "미래"
@ -500,6 +538,8 @@ _widgets:
trends: "트렌드" trends: "트렌드"
clock: "시계" clock: "시계"
rss: "RSS 리더" rss: "RSS 리더"
activity: "활동"
photos: "사진"
_cw: _cw:
hide: "숨기기" hide: "숨기기"
show: "더 보기" show: "더 보기"

View File

@ -1,2 +1,36 @@
--- ---
_lang_: "Русский язык" _lang_: "Русский язык"
search: "Поиск"
notifications: "Уведомления"
password: "Пароль"
ok: "Окей"
cancel: "Отмена"
instance: "Экземпляр"
settings: "Настройки"
profile: "Профиль"
timeline: "Лента"
login: "Войти"
logout: "Выйти"
signup: "Регистрация"
save: "Сохранить"
favorite: "Избранное"
favorites: "Избранное"
unfavorite: "Удалить из избранных"
pin: "Закрепить"
unpin: "Открепить"
copyLink: "Скопировать ссылку"
delete: "Удалить"
addToList: "Добавить в список"
reply: "Ответить"
loadMore: "Показать еще"
importAndExport: "Импорт / Экспорт"
files: "Файл"
instances: "Экземпляр"
remove: "Удалить"
_sfx:
notification: "Уведомления"
_widgets:
notifications: "Уведомления"
timeline: "Лента"
_cw:
show: "Показать еще"

View File

@ -121,18 +121,23 @@ searchWith: "搜索:{q}"
youHaveNoLists: "列表为空" youHaveNoLists: "列表为空"
followConfirm: "你确定要关注{name}吗?" followConfirm: "你确定要关注{name}吗?"
proxyAccount: "代理账户" proxyAccount: "代理账户"
proxyAccountDescription: "代理帐户是在某些情况下充当用户的远程关注者的帐户。 例如,当一个用户列出一个远程用户时,如果没有人跟随该列出的用户,则该活动将不会传递到该实例,因此将代之以代理帐户。"
host: "主机名" host: "主机名"
selectUser: "选择用户" selectUser: "选择用户"
recipient: "收件人" recipient: "收件人"
annotation: "注解" annotation: "注解"
federation: "联合" federation: "联合"
instances: "实例" instances: "实例"
registeredAt: "初次观察"
latestRequestSentAt: "上次发送的请求" latestRequestSentAt: "上次发送的请求"
latestRequestReceivedAt: "上次收到的请求" latestRequestReceivedAt: "上次收到的请求"
latestStatus: "最后状态"
storageUsage: "已用存储" storageUsage: "已用存储"
charts: "图表" charts: "图表"
perHour: "每小时" perHour: "每小时"
perDay: "每天" perDay: "每天"
stopActivityDelivery: "停止发送活动"
blockThisInstance: "阻止此实例"
operations: "操作" operations: "操作"
software: "软件" software: "软件"
version: "版本" version: "版本"
@ -147,6 +152,7 @@ instanceInfo: "实例情报"
statistics: "统计" statistics: "统计"
clearQueue: "清除队列" clearQueue: "清除队列"
clearQueueConfirmTitle: "确定清除队列?" clearQueueConfirmTitle: "确定清除队列?"
clearQueueConfirmText: "未送达的帖子将不会送达。 通常,您不需要这样做。"
clearCachedFiles: "清除缓存" clearCachedFiles: "清除缓存"
clearCachedFilesConfirm: "确定要清除缓存文件?" clearCachedFilesConfirm: "确定要清除缓存文件?"
blockedInstances: "被阻拦的实例" blockedInstances: "被阻拦的实例"
@ -273,6 +279,7 @@ recaptcha: "reCAPTCHA"
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)" enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptchaSiteKey: "网站密钥" recaptchaSiteKey: "网站密钥"
recaptchaSecretKey: "reCAPTCHA 密钥" recaptchaSecretKey: "reCAPTCHA 密钥"
antennas: "天线"
name: "名称" name: "名称"
antennaKeywordsDescription: "使用空格分隔会产生AND规范并且使用换行符分隔会产生OR规范" antennaKeywordsDescription: "使用空格分隔会产生AND规范并且使用换行符分隔会产生OR规范"
serviceworker: "ServiceWorker" serviceworker: "ServiceWorker"
@ -297,6 +304,7 @@ aboutMisskey: "关于 Misskey"
aboutMisskeyText: "Misskey是由syuilo于2014年开发的开放源代码软件。" aboutMisskeyText: "Misskey是由syuilo于2014年开发的开放源代码软件。"
misskeyMembers: "现在由以下成员进行开发和维护:" misskeyMembers: "现在由以下成员进行开发和维护:"
misskeySource: "源代码在这里公开:" misskeySource: "源代码在这里公开:"
misskeyTranslation: "与我们一同进行Misskey的翻译工作"
misskeyDonate: "可以向 Misskey 进行捐款以支持开发:" misskeyDonate: "可以向 Misskey 进行捐款以支持开发:"
morePatrons: "还有很多其他的人也在支持我们,非常感谢🥰" morePatrons: "还有很多其他的人也在支持我们,非常感谢🥰"
patrons: "支持者" patrons: "支持者"
@ -307,8 +315,10 @@ moderator: "版主"
nUsersMentioned: "{n} 被提到" nUsersMentioned: "{n} 被提到"
securityKey: "安全密钥" securityKey: "安全密钥"
securityKeyName: "密钥名称" securityKeyName: "密钥名称"
registerSecurityKey: "注册安全密钥"
lastUsed: "最后使用:" lastUsed: "最后使用:"
unregister: "删除账户" unregister: "删除账户"
passwordLessLogin: "无密码登录"
resetPassword: "重置密码" resetPassword: "重置密码"
newPasswordIs: "新的密码是「{password}」" newPasswordIs: "新的密码是「{password}」"
post: "投稿" post: "投稿"
@ -319,10 +329,12 @@ autoNoteWatchDescription: "让您能够收到关于「反应」和回复其他
reduceUiAnimation: "减少UI动画" reduceUiAnimation: "减少UI动画"
share: "分享" share: "分享"
notFound: "未找到" notFound: "未找到"
notFoundDescription: "没有与指定URL对应的页面。"
uploadFolder: "默认上传文件夹" uploadFolder: "默认上传文件夹"
cacheClear: "清空缓存" cacheClear: "清空缓存"
markAsReadAllNotifications: "将所有通知标为已读" markAsReadAllNotifications: "将所有通知标为已读"
markAsReadAllUnreadNotes: "将所有帖子标记为已读" markAsReadAllUnreadNotes: "将所有帖子标记为已读"
markAsReadAllTalkMessages: "将所有聊天标记为已读"
help: "帮助" help: "帮助"
inputMessageHere: "在此键入信息" inputMessageHere: "在此键入信息"
close: "关闭" close: "关闭"
@ -335,6 +347,8 @@ invites: "邀请"
groupName: "群组名" groupName: "群组名"
members: "成员" members: "成员"
transfer: "转让" transfer: "转让"
messagingWithUser: "与用户聊天"
messagingWithGroup: "与群组聊天"
title: "标题" title: "标题"
text: "文本" text: "文本"
enable: "启用" enable: "启用"
@ -343,6 +357,69 @@ retype: "重新输入"
noteOf: "{user}的帖子" noteOf: "{user}的帖子"
inviteToGroup: "群组邀请" inviteToGroup: "群组邀请"
maxNoteTextLength: "帖子的字数限制" maxNoteTextLength: "帖子的字数限制"
quoteAttached: "已引用"
quoteQuestion: "是否将其作为引用附上?"
noMessagesYet: "现在没有新的聊天"
newMessageExists: "新信息"
onlyOneFileCanBeAttached: "只能添加一个附件"
signinRequired: "请先登录"
invitationCode: "邀请码"
checking: "正在确认"
available: "可用"
unavailable: "不可用"
usernameInvalidFormat: "可使用大小写英文字母、数字和下划线。"
tooShort: "过短"
tooLong: "过长"
weakPassword: "密码强度:弱"
normalPassword: "密码强度:中等"
strongPassword: "密码强度:强"
passwordMatched: "密码一致"
passwordNotMatched: "密码不一致"
signinWith: "以{x}登录"
tapSecurityKey: "点击安全密钥"
or: "或者"
uiLanguage: "显示语言"
groupInvited: "群组招待"
aboutX: "关于 {x}"
useOsNativeEmojis: "使用OS原生Emoji"
noGroups: "没有组"
joinOrCreateGroup: "加入或者创建群组"
noHistory: "没有历史记录"
disableAnimatedMfm: "禁用MFM动画"
doing: "正在进行"
category: "类别"
tags: "标签"
docSource: "文件来源"
createAccount: "注册账户"
existingAcount: "现有的帐户"
regenerate: "重新生成"
fontSize: "字体大小"
noFollowRequests: "没有关注申请"
openImageInNewTab: "在新标签页中打开图片"
dashboard: "Dashboard"
local: "本地"
remote: "远程"
total: "总计"
weekOverWeekChanges: "与前一周相比"
dayOverDayChanges: "与前一日相比"
accessibility: "辅助功能"
clinetSettings: "客户端设置"
accountSettings: "账户设置"
numberOfDays: "天数"
hideThisNote: "隐藏这条帖子"
showFeaturedNotesInTimeline: "在时间轴上显示热门推荐"
objectStorage: "对象存储"
useObjectStorage: "使用对象存储"
serverLogs: "服务器日志"
deleteAll: "删除全部"
showFixedPostForm: "在时间线顶部显示帖子表单"
newNoteRecived: "有新的帖子"
useNotificationsPopup: "在弹出窗口中显示通知列表"
none: "空"
_sfx:
note: "帖子"
notification: "通知"
chat: "聊天"
_ago: _ago:
unknown: "未知" unknown: "未知"
future: "未来" future: "未来"
@ -362,6 +439,17 @@ _time:
_tutorial: _tutorial:
title: "Misskey的使用方法" title: "Misskey的使用方法"
step1_1: "欢迎!" step1_1: "欢迎!"
step1_2: "这个页面叫做「时间线」,它会按照时间顺序显示所有你「关注」的人所发的「帖子」。"
step1_3: "如果你并没有发布任何帖子,也没有关注其他的人,你的时间线页面应当什么都没有显示。"
step2_1: "在你想发布一些帖子之前,让我们先进行一下个人资料设置。"
step2_2: "如果别人能够更加的了解你,关注你的概率也会得到提升。"
step3_1: "已经设置完个人资料了吗?"
step3_2: "那么接下来,试着写一些什么东西来发布吧。你可以通过点击屏幕上的铅笔图标来打开投稿页面。"
step3_3: "写完内容后,点击窗口右上方的按钮就可以投稿。"
step3_4: "不知道说些什么好吗那就写下「Misskey我来啦」这样的话吧。"
step4_1: "将你的话语发布出去了吗?"
step4_2: "太棒了!现在你可以在你的时间线中看到你刚刚发布的帖子了。"
step7_3: "接下来享受Misskey带来的乐趣吧🚀"
_2fa: _2fa:
alreadyRegistered: "此设备已被注册" alreadyRegistered: "此设备已被注册"
registerDevice: "注册设备" registerDevice: "注册设备"
@ -392,6 +480,9 @@ _permissions:
"write:user-groups": "操作用户组" "write:user-groups": "操作用户组"
_auth: _auth:
permissionAsk: "这个应用程序需要以下权限" permissionAsk: "这个应用程序需要以下权限"
_antennaSources:
all: "所有帖子"
homeTimeline: "已关注用户的帖子"
_weekday: _weekday:
sunday: "星期日" sunday: "星期日"
monday: "星期一" monday: "星期一"
@ -408,6 +499,8 @@ _widgets:
trends: "趋势" trends: "趋势"
clock: "时钟" clock: "时钟"
rss: "RSS阅读器" rss: "RSS阅读器"
activity: "活动"
photos: "照片"
_cw: _cw:
hide: "隐藏" hide: "隐藏"
show: "查看更多" show: "查看更多"
@ -439,13 +532,27 @@ _poll:
_visibility: _visibility:
public: "公开" public: "公开"
home: "首页" home: "首页"
homeDescription: "仅发送至首页的时间线"
followers: "关注者" followers: "关注者"
followersDescription: "仅发送至关注者"
specified: "指定用户" specified: "指定用户"
specifiedDescription: "仅发送至指定用户"
localOnly: "仅限本地" localOnly: "仅限本地"
_postForm:
replyPlaceholder: "回复这个帖子..."
quotePlaceholder: "引用这个帖子..."
_placeholders:
a: "现在如何?"
b: "发生了什么?"
c: "你有什么想法?"
d: "你想要发布些什么吗?"
e: "请写下来吧"
f: "等待您的发布..."
_profile: _profile:
name: "名称" name: "名称"
username: "用户名" username: "用户名"
description: "个人简介" description: "个人简介"
youCanIncludeHashtags: "您可以包含一个哈希标签。"
metadata: "额外信息" metadata: "额外信息"
metadataLabel: "标签" metadataLabel: "标签"
metadataContent: "内容" metadataContent: "内容"
@ -464,9 +571,11 @@ _charts:
notesIncDec: "帖子:增加/减少" notesIncDec: "帖子:增加/减少"
notesTotal: "帖子总数" notesTotal: "帖子总数"
_instanceCharts: _instanceCharts:
requests: "请求"
users: "用户数量:增加/减少" users: "用户数量:增加/减少"
usersTotal: "用户总数" usersTotal: "用户总数"
notes: "帖子:增加/减少" notes: "帖子:增加/减少"
notesTotal: "帖子:总数"
ff: "关注/被关注:数量变化" ff: "关注/被关注:数量变化"
ffTotal: "关注/被关注:总数" ffTotal: "关注/被关注:总数"
cacheSize: "缓存大小:增加/减少" cacheSize: "缓存大小:增加/减少"

View File

@ -0,0 +1,28 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class promo1581979837262 implements MigrationInterface {
name = 'promo1581979837262'
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`CREATE TABLE "promo_note" ("noteId" character varying(32) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "REL_e263909ca4fe5d57f8d4230dd5" UNIQUE ("noteId"), CONSTRAINT "PK_e263909ca4fe5d57f8d4230dd5c" PRIMARY KEY ("noteId"))`, undefined);
await queryRunner.query(`CREATE INDEX "IDX_83f0862e9bae44af52ced7099e" ON "promo_note" ("userId") `, undefined);
await queryRunner.query(`CREATE TABLE "promo_read" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_61917c1541002422b703318b7c9" PRIMARY KEY ("id"))`, undefined);
await queryRunner.query(`CREATE INDEX "IDX_9657d55550c3d37bfafaf7d4b0" ON "promo_read" ("userId") `, undefined);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2882b8a1a07c7d281a98b6db16" ON "promo_read" ("userId", "noteId") `, undefined);
await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4"`, undefined);
await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05"`, undefined);
await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`, undefined);
await queryRunner.query(`DROP INDEX "IDX_2882b8a1a07c7d281a98b6db16"`, undefined);
await queryRunner.query(`DROP INDEX "IDX_9657d55550c3d37bfafaf7d4b0"`, undefined);
await queryRunner.query(`DROP TABLE "promo_read"`, undefined);
await queryRunner.query(`DROP INDEX "IDX_83f0862e9bae44af52ced7099e"`, undefined);
await queryRunner.query(`DROP TABLE "promo_note"`, undefined);
}
}

View File

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class featuredInjecttion1582019042083 implements MigrationInterface {
name = 'featuredInjecttion1582019042083'
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "injectFeaturedNote" boolean NOT NULL DEFAULT true`, undefined);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "injectFeaturedNote"`, undefined);
}
}

View File

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class antennaExclude1582210532752 implements MigrationInterface {
name = 'antennaExclude1582210532752'
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "antenna" ADD "excludeKeywords" jsonb NOT NULL DEFAULT '[]'`, undefined);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "excludeKeywords"`, undefined);
}
}

View File

@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>", "author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.10.0", "version": "12.21.0",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",
@ -250,7 +250,6 @@
"vue-meta": "2.3.2", "vue-meta": "2.3.2",
"vue-prism-component": "1.1.1", "vue-prism-component": "1.1.1",
"vue-router": "3.1.5", "vue-router": "3.1.5",
"vue-sequential-entrance": "1.1.3",
"vue-style-loader": "4.1.2", "vue-style-loader": "4.1.2",
"vue-svg-inline-loader": "1.4.5", "vue-svg-inline-loader": "1.4.5",
"vue-template-compiler": "2.6.11", "vue-template-compiler": "2.6.11",

View File

@ -44,27 +44,33 @@
<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/> <mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
</button> </button>
<div class="divider"></div> <div class="divider"></div>
<router-link class="item index" active-class="active" to="/" exact v-if="$store.getters.isSignedIn"> <button class="item _button index active" @click="top()" v-if="$route.name === 'index'">
<fa :icon="faHome" fixed-width/><span class="text">{{ $t('timeline') }}</span> <fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
</router-link>
<router-link class="item index" active-class="active" to="/" exact v-else>
<fa :icon="faHome" fixed-width/><span class="text">{{ $t('home') }}</span>
</router-link>
<button class="item _button notifications" @click="notificationsOpen = !notificationsOpen" ref="notificationButton" v-if="$store.getters.isSignedIn">
<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
<i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i>
</button> </button>
<router-link class="item" active-class="active" to="/my/messaging" v-if="$store.getters.isSignedIn"> <router-link class="item index" active-class="active" to="/" exact v-else>
<fa :icon="faComments" fixed-width/><span class="text">{{ $t('messaging') }}</span> <fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
<i v-if="$store.state.i.hasUnreadMessagingMessage"><fa :icon="faCircle"/></i>
</router-link>
<router-link class="item" active-class="active" to="/my/follow-requests" v-if="$store.getters.isSignedIn && $store.state.i.isLocked">
<fa :icon="faUserClock" fixed-width/><span class="text">{{ $t('followRequests') }}</span>
<i v-if="$store.state.i.pendingReceivedFollowRequestsCount"><fa :icon="faCircle"/></i>
</router-link>
<router-link class="item" active-class="active" to="/my/drive" v-if="$store.getters.isSignedIn">
<fa :icon="faCloud" fixed-width/><span class="text">{{ $t('drive') }}</span>
</router-link> </router-link>
<template v-if="$store.getters.isSignedIn">
<button class="item _button notifications" @click="notificationsOpen = !notificationsOpen" ref="notificationButton" v-if="$store.state.device.useNotificationsPopup">
<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
<i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i>
</button>
<router-link class="item notifications" active-class="active" to="/my/notifications" ref="notificationButton" v-else>
<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
<i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i>
</router-link>
<router-link class="item" active-class="active" to="/my/messaging">
<fa :icon="faComments" fixed-width/><span class="text">{{ $t('messaging') }}</span>
<i v-if="$store.state.i.hasUnreadMessagingMessage"><fa :icon="faCircle"/></i>
</router-link>
<router-link class="item" active-class="active" to="/my/drive">
<fa :icon="faCloud" fixed-width/><span class="text">{{ $t('drive') }}</span>
</router-link>
<router-link class="item" active-class="active" to="/my/follow-requests" v-if="$store.state.i.isLocked">
<fa :icon="faUserClock" fixed-width/><span class="text">{{ $t('followRequests') }}</span>
<i v-if="$store.state.i.hasPendingReceivedFollowRequest"><fa :icon="faCircle"/></i>
</router-link>
</template>
<div class="divider"></div> <div class="divider"></div>
<router-link class="item" active-class="active" to="/featured"> <router-link class="item" active-class="active" to="/featured">
<fa :icon="faFireAlt" fixed-width/><span class="text">{{ $t('featured') }}</span> <fa :icon="faFireAlt" fixed-width/><span class="text">{{ $t('featured') }}</span>
@ -87,11 +93,14 @@
<fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span> <fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span>
<i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes)"><fa :icon="faCircle"/></i> <i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes)"><fa :icon="faCircle"/></i>
</button> </button>
<router-link class="item" active-class="active" to="/preferences">
<fa :icon="faCog" fixed-width/><span class="text">{{ $t('settings') }}</span>
</router-link>
</div> </div>
</nav> </nav>
</transition> </transition>
<div class="contents" ref="contents"> <div class="contents" ref="contents" :class="{ wallpaper }">
<main ref="main"> <main ref="main">
<div class="content"> <div class="content">
<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition"> <transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
@ -137,9 +146,11 @@
</div> </div>
<div class="buttons"> <div class="buttons">
<button v-if="$store.getters.isSignedIn" class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="$store.state.i.hasUnreadSpecifiedNotes || $store.state.i.pendingReceivedFollowRequestsCount || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement"><fa :icon="faCircle"/></i></button> <button class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadSpecifiedNotes || $store.state.i.hasPendingReceivedFollowRequest || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement)"><fa :icon="faCircle"/></i></button>
<button v-if="$store.getters.isSignedIn" class="button home _button" :disabled="$route.path === '/'" @click="$router.push('/')"><fa :icon="faHome"/></button> <button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button>
<button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton2"><fa :icon="notificationsOpen ? faTimes : faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button> <button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button>
<button v-if="$store.getters.isSignedIn && $store.state.device.useNotificationsPopup" class="button notifications _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton2"><fa :icon="notificationsOpen ? faTimes : faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
<button v-if="$store.getters.isSignedIn && !$store.state.device.useNotificationsPopup" class="button notifications _button" @click="$router.push('/my/notifications')" ref="notificationButton2"><fa :icon="faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button> <button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
</div> </div>
@ -148,6 +159,8 @@
<transition name="zoom-in-top"> <transition name="zoom-in-top">
<x-notifications v-if="notificationsOpen" class="notifications" ref="notifications"/> <x-notifications v-if="notificationsOpen" class="notifications" ref="notifications"/>
</transition> </transition>
<stream-indicator v-if="$store.getters.isSignedIn"/>
</div> </div>
</template> </template>
@ -163,6 +176,8 @@ import { search } from './scripts/search';
import contains from './scripts/contains'; import contains from './scripts/contains';
import MkToast from './components/toast.vue'; import MkToast from './components/toast.vue';
const DESKTOP_THRESHOLD = 1100;
export default Vue.extend({ export default Vue.extend({
i18n, i18n,
@ -186,9 +201,9 @@ export default Vue.extend({
searchQuery: '', searchQuery: '',
searchWait: false, searchWait: false,
widgetsEditMode: false, widgetsEditMode: false,
isDesktop: window.innerWidth >= 1100, isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
canBack: false, canBack: false,
disconnectedDialog: null as Promise<void> | null, wallpaper: localStorage.getItem('wallpaper') != null,
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer
}; };
}, },
@ -226,6 +241,10 @@ export default Vue.extend({
el.removeEventListener('mousedown', this.onMousedown); el.removeEventListener('mousedown', this.onMousedown);
} }
} }
},
isDesktop() {
if (this.isDesktop) this.adjustWidgetsWidth();
} }
}, },
@ -247,44 +266,10 @@ export default Vue.extend({
}]); }]);
} }
} }
this.$root.stream.on('_disconnected_', () => {
if (this.disconnectedDialog) return;
if (this.$store.state.device.autoReload) {
location.reload();
return;
}
setTimeout(() => {
if (this.$root.stream.state !== 'reconnecting') return;
this.disconnectedDialog = this.$root.dialog({
type: 'warning',
showCancelButton: true,
title: this.$t('disconnectedFromServer'),
text: this.$t('reloadConfirm'),
}).then(({ canceled }) => {
if (!canceled) {
location.reload();
}
this.disconnectedDialog = null;
});
}, 150)
});
}, },
mounted() { mounted() {
// https://stackoverflow.com/questions/33891709/when-flexbox-items-wrap-in-column-mode-container-does-not-grow-its-width if (this.isDesktop) this.adjustWidgetsWidth();
if (this.isDesktop) {
const adjustWidgetsWidth = () => {
const lastChild = this.$refs.widgets.children[this.$refs.widgets.children.length - 1];
if (lastChild == null) return;
const width = lastChild.offsetLeft + 300 + 16;
this.$refs.widgets.style.width = width + 'px';
};
setInterval(adjustWidgetsWidth, 1000);
}
const adjustTitlePosition = () => { const adjustTitlePosition = () => {
this.$refs.title.style.left = (this.$refs.main.getBoundingClientRect().left - this.$refs.nav.offsetWidth) + 'px'; this.$refs.title.style.left = (this.$refs.main.getBoundingClientRect().left - this.$refs.nav.offsetWidth) + 'px';
@ -298,10 +283,33 @@ export default Vue.extend({
ro.observe(this.$refs.contents); ro.observe(this.$refs.contents);
window.addEventListener('resize', adjustTitlePosition); window.addEventListener('resize', adjustTitlePosition, { passive: true });
if (!this.isDesktop) {
window.addEventListener('resize', () => {
if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
}, { passive: true });
}
}, },
methods: { methods: {
adjustWidgetsWidth() {
// https://stackoverflow.com/questions/33891709/when-flexbox-items-wrap-in-column-mode-container-does-not-grow-its-width
const adjust = () => {
const lastChild = this.$refs.widgets.children[this.$refs.widgets.children.length - 1];
if (lastChild == null) return;
const width = lastChild.offsetLeft + 300 + 16;
this.$refs.widgets.style.width = width + 'px';
};
setInterval(adjust, 1000);
setTimeout(adjust, 100);
},
top() {
window.scroll({ top: 0, behavior: 'smooth' });
},
help() { help() {
this.$router.push('/docs/keyboard-shortcut'); this.$router.push('/docs/keyboard-shortcut');
}, },
@ -361,21 +369,18 @@ export default Vue.extend({
avatar: this.$store.state.i, avatar: this.$store.state.i,
}, { }, {
type: 'link', type: 'link',
text: this.$t('settings'), text: this.$t('accountSettings'),
to: '/my/settings', to: '/my/settings',
icon: faCog, icon: faCog,
}, null, ...accountItems, { }, null, ...accountItems, {
type: 'item',
icon: faPlus, icon: faPlus,
text: this.$t('addAcount'), text: this.$t('addAcount'),
action: () => { action: () => {
this.$root.menu({ this.$root.menu({
items: [{ items: [{
type: 'item',
text: this.$t('existingAcount'), text: this.$t('existingAcount'),
action: () => { this.addAcount(); }, action: () => { this.addAcount(); },
}, { }, {
type: 'item',
text: this.$t('createAccount'), text: this.$t('createAccount'),
action: () => { this.createAccount(); }, action: () => { this.createAccount(); },
}], }],
@ -397,9 +402,14 @@ export default Vue.extend({
this.$root.menu({ this.$root.menu({
items: [{ items: [{
type: 'link', type: 'link',
text: this.$t('statistics'), text: this.$t('dashboard'),
to: '/instance/stats', to: '/instance',
icon: faChartBar, icon: faTachometerAlt,
}, null, {
type: 'link',
text: this.$t('settings'),
to: '/instance/settings',
icon: faCog,
}, { }, {
type: 'link', type: 'link',
text: this.$t('customEmojis'), text: this.$t('customEmojis'),
@ -415,11 +425,6 @@ export default Vue.extend({
text: this.$t('files'), text: this.$t('files'),
to: '/instance/files', to: '/instance/files',
icon: faCloud, icon: faCloud,
}, {
type: 'link',
text: this.$t('monitor'),
to: '/instance/monitor',
icon: faTachometerAlt,
}, { }, {
type: 'link', type: 'link',
text: this.$t('jobQueue'), text: this.$t('jobQueue'),
@ -435,11 +440,6 @@ export default Vue.extend({
text: this.$t('announcements'), text: this.$t('announcements'),
to: '/instance/announcements', to: '/instance/announcements',
icon: faBroadcastTower, icon: faBroadcastTower,
}, null, {
type: 'link',
text: this.$t('general'),
to: '/instance',
icon: faCog,
}], }],
align: 'left', align: 'left',
fixed: true, fixed: true,
@ -555,13 +555,17 @@ export default Vue.extend({
onNotification(notification) { onNotification(notification) {
// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない // TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
this.$root.stream.send('readNotification', { if (true) {
id: notification.id this.$root.stream.send('readNotification', {
}); id: notification.id
});
this.$root.new(MkToast, { this.$root.new(MkToast, {
notification notification
}); });
}
this.$root.sound('notification');
}, },
onMousedown(e) { onMousedown(e) {
@ -590,7 +594,9 @@ export default Vue.extend({
'calendar', 'calendar',
'rss', 'rss',
'trends', 'trends',
'clock' 'clock',
'activity',
'photos',
]; ];
this.$root.menu({ this.$root.menu({
@ -620,30 +626,6 @@ export default Vue.extend({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.header-enter-active, .header-leave-active {
transition: opacity 0.5s, transform 0.5s !important;
}
.header-enter {
opacity: 0;
transform: scale(0.9);
}
.header-leave-to {
opacity: 0;
transform: scale(0.9);
}
.page-enter-active, .page-leave-active {
transition: opacity 0.5s, transform 0.5s !important;
}
.page-enter {
opacity: 0;
transform: translateY(-32px);
}
.page-leave-to {
opacity: 0;
transform: translateY(32px);
}
.nav-enter-active, .nav-enter-active,
.nav-leave-active { .nav-leave-active {
opacity: 1; opacity: 1;
@ -870,6 +852,7 @@ export default Vue.extend({
width: $nav-width; width: $nav-width;
height: 100vh; height: 100vh;
padding: 16px 0; padding: 16px 0;
padding-bottom: calc(3.7rem + 24px);
box-sizing: border-box; box-sizing: border-box;
overflow: auto; overflow: auto;
background: var(--navBg); background: var(--navBg);
@ -883,6 +866,7 @@ export default Vue.extend({
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) { @media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
width: $nav-icon-only-width; width: $nav-icon-only-width;
padding: 8px 0; padding: 8px 0;
padding-bottom: calc(3.7rem + 24px);
> .divider { > .divider {
margin: 8px auto; margin: 8px auto;
@ -930,12 +914,24 @@ export default Vue.extend({
&:hover { &:hover {
text-decoration: none; text-decoration: none;
color: var(--navHoverFg);
} }
&.active { &.active {
color: var(--navActive); color: var(--navActive);
} }
&:last-child {
position: fixed;
bottom: 0;
width: inherit;
padding-top: 8px;
padding-bottom: 8px;
background: var(--navBg);
border-top: solid 1px var(--divider);
border-right: solid 1px var(--divider);
}
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) { @media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
padding-left: 0; padding-left: 0;
width: 100%; width: 100%;
@ -972,6 +968,10 @@ export default Vue.extend({
margin: 0 auto; margin: 0 auto;
min-width: 0; min-width: 0;
&.wallpaper {
background: var(--wallpaperOverlay);
}
> main { > main {
width: $main-width; width: $main-width;
min-width: $main-width; min-width: $main-width;
@ -1168,7 +1168,7 @@ export default Vue.extend({
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
color: var(--accent); color: var(--indicator);
font-size: 16px; font-size: 16px;
animation: blink 1s infinite; animation: blink 1s infinite;
} }
@ -1182,15 +1182,17 @@ export default Vue.extend({
left: 0; left: 0;
right: 0; right: 0;
margin: 0 auto; margin: 0 auto;
padding: 8px 8px 0 8px;
z-index: 10001; z-index: 10001;
width: 350px; width: 350px;
height: 400px; height: 400px;
box-sizing: border-box;
background: var(--vocsgcxy); background: var(--vocsgcxy);
-webkit-backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
border-radius: 6px; border-radius: 6px;
box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15); box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15);
overflow: hidden; overflow: auto;
@media (max-width: 800px) { @media (max-width: 800px) {
width: 320px; width: 320px;

BIN
src/client/assets/fedi.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>Misskey API</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='/api.json'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -55,13 +55,15 @@ export default Vue.extend({
handsTailLength: 0.7, handsTailLength: 0.7,
hHandLengthRatio: 0.75, hHandLengthRatio: 0.75,
mHandLengthRatio: 1, mHandLengthRatio: 1,
sHandLengthRatio: 1 sHandLengthRatio: 1,
computedStyle: getComputedStyle(document.documentElement)
}; };
}, },
computed: { computed: {
dark(): boolean { dark(): boolean {
return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--bg')).isDark(); return tinycolor(this.computedStyle.getPropertyValue('--bg')).isDark();
}, },
majorGraduationColor(): string { majorGraduationColor(): string {
@ -75,10 +77,10 @@ export default Vue.extend({
return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'; return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
}, },
mHandColor(): string { mHandColor(): string {
return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--fg')).toHexString(); return tinycolor(this.computedStyle.getPropertyValue('--fg')).toHexString();
}, },
hHandColor(): string { hHandColor(): string {
return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent')).toHexString(); return tinycolor(this.computedStyle.getPropertyValue('--accent')).toHexString();
}, },
ms(): number { ms(): number {
@ -123,6 +125,16 @@ export default Vue.extend({
} }
}; };
update(); update();
this.$store.subscribe((mutation, state) => {
if (mutation.type !== 'device/set') return;
if (mutation?.payload?.key !== 'theme') return;
setTimeout(() => {
this.computedStyle = getComputedStyle(document.documentElement);
}, 250);
});
}, },
beforeDestroy() { beforeDestroy() {

View File

@ -2,7 +2,7 @@
<div class="swhvrteh" @contextmenu.prevent="() => {}"> <div class="swhvrteh" @contextmenu.prevent="() => {}">
<ol class="users" ref="suggests" v-if="type === 'user'"> <ol class="users" ref="suggests" v-if="type === 'user'">
<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1" class="user"> <li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1" class="user">
<img class="avatar" :src="user.avatarUrl" alt=""/> <img class="avatar" :src="user.avatarUrl"/>
<span class="name"> <span class="name">
<mk-user-name :user="user" :key="user.id"/> <mk-user-name :user="user" :key="user.id"/>
</span> </span>

View File

@ -1,15 +1,15 @@
<template> <template>
<sequential-entrance class="sqadhkmv" ref="list" :direction="direction" :reversed="reversed"> <component :is="$store.state.device.animation ? 'transition-group' : 'div'" class="sqadhkmv" name="list" tag="div" appear :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'">
<template v-for="(item, i) in items"> <template v-for="(item, i) in items">
<slot :item="item" :i="i"></slot> <slot :item="item" :i="i"></slot>
<div class="separator" :key="item.id + '_date'" v-if="i != items.length - 1 && new Date(item.createdAt).getDate() != new Date(items[i + 1].createdAt).getDate()"> <div class="separator" :key="item.id + '_date'" v-if="showDate(i, item)">
<p class="date"> <p class="date">
<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span> <span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span> <span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span>
</p> </p>
</div> </div>
</template> </template>
</sequential-entrance> </component>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -27,7 +27,8 @@ export default Vue.extend({
}, },
direction: { direction: {
type: String, type: String,
required: false required: false,
default: 'down'
}, },
reversed: { reversed: {
type: Boolean, type: Boolean,
@ -52,13 +53,49 @@ export default Vue.extend({
}); });
}, },
showDate(i, item) {
return (
i != this.items.length - 1 &&
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
!item._prId_ &&
!this.items[i + 1]._prId_ &&
!item._featuredId_ &&
!this.items[i + 1]._featuredId_);
},
focus() { focus() {
this.$refs.list.focus(); this.$slots.default[0].elm.focus();
} }
} }
}); });
</script> </script>
<style lang="scss">
.sqadhkmv {
> .list-move {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
}
> .list-enter-active {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
}
&[data-direction="up"] {
> .list-enter {
opacity: 0;
transform: translateY(64px);
}
}
&[data-direction="down"] {
> .list-enter {
opacity: 0;
transform: translateY(-64px);
}
}
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
.sqadhkmv { .sqadhkmv {
> .separator { > .separator {

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="mk-dialog" :class="{ iconOnly }"> <div class="mk-dialog" :class="{ iconOnly }">
<transition name="bg-fade" appear> <transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
<div class="bg" ref="bg" @click="onBgClick" v-if="show"></div> <div class="bg" ref="bg" @click="onBgClick" v-if="show"></div>
</transition> </transition>
<transition name="dialog" appear @after-leave="() => { destroyDom(); }"> <transition :name="$store.state.device.animation ? 'dialog' : ''" appear @after-leave="() => { destroyDom(); }">
<div class="main" ref="main" v-if="show"> <div class="main" ref="main" v-if="show">
<template v-if="type == 'signin'"> <template v-if="type == 'signin'">
<mk-signin/> <mk-signin/>
@ -55,6 +55,7 @@ import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-i
import MkButton from './ui/button.vue'; import MkButton from './ui/button.vue';
import MkInput from './ui/input.vue'; import MkInput from './ui/input.vue';
import MkSelect from './ui/select.vue'; import MkSelect from './ui/select.vue';
import MkSignin from './signin.vue';
import parseAcct from '../../misc/acct/parse'; import parseAcct from '../../misc/acct/parse';
import i18n from '../i18n'; import i18n from '../i18n';
@ -65,6 +66,7 @@ export default Vue.extend({
MkButton, MkButton,
MkInput, MkInput,
MkSelect, MkSelect,
MkSignin,
}, },
props: { props: {

View File

@ -12,7 +12,7 @@
preload="metadata" preload="metadata"
controls controls
v-else-if="detail && is === 'video'"/> v-else-if="detail && is === 'video'"/>
<img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/> <img :src="file.thumbnailUrl" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/>
<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/> <fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
<fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/> <fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>

View File

@ -83,17 +83,14 @@ export default Vue.extend({
} else { } else {
this.$root.menu({ this.$root.menu({
items: [{ items: [{
type: 'item',
text: this.$t('rename'), text: this.$t('rename'),
icon: faICursor, icon: faICursor,
action: this.rename action: this.rename
}, { }, {
type: 'item',
text: this.file.isSensitive ? this.$t('unmarkAsSensitive') : this.$t('markAsSensitive'), text: this.file.isSensitive ? this.$t('unmarkAsSensitive') : this.$t('markAsSensitive'),
icon: this.file.isSensitive ? faEye : faEyeSlash, icon: this.file.isSensitive ? faEye : faEyeSlash,
action: this.toggleSensitive action: this.toggleSensitive
}, null, { }, null, {
type: 'item',
text: this.$t('copyUrl'), text: this.$t('copyUrl'),
icon: faLink, icon: faLink,
action: this.copyUrl action: this.copyUrl
@ -105,22 +102,9 @@ export default Vue.extend({
icon: faDownload, icon: faDownload,
download: this.file.name download: this.file.name
}, null, { }, null, {
type: 'item',
text: this.$t('delete'), text: this.$t('delete'),
icon: faTrashAlt, icon: faTrashAlt,
action: this.deleteFile action: this.deleteFile
}, null, {
type: 'nest',
text: this.$t('contextmenu.else-files'),
menu: [{
type: 'item',
text: this.$t('contextmenu.set-as-avatar'),
action: this.setAsAvatar
}, {
type: 'item',
text: this.$t('contextmenu.set-as-banner'),
action: this.setAsBanner
}]
}], }],
source: ev.currentTarget || ev.target, source: ev.currentTarget || ev.target,
}); });

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="mjndxjcg _panel"> <div class="mjndxjcg _panel">
<img src="https://xn--931a.moe/assets/error.png" alt="" class="_ghost"/> <img src="https://xn--931a.moe/assets/error.png" class="_ghost"/>
<p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p> <p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p>
<mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button> <mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button>
</div> </div>

View File

@ -7,7 +7,7 @@
<span class="hhnn">{{ hh }}<span :style="{ visibility: now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{{ nn }}</span> <span class="hhnn">{{ hh }}<span :style="{ visibility: now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{{ nn }}</span>
</time> </time>
</div> </div>
<div class="content _panel"> <div class="content _panel _ghost">
<mk-clock/> <mk-clock/>
</div> </div>
</div> </div>
@ -66,8 +66,10 @@ export default Vue.extend({
> .header { > .header {
padding: 0 12px; padding: 0 12px;
padding-top: 4px;
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
font-family: Lucida Console, Courier, monospace;
&:hover + .content { &:hover + .content {
opacity: 1; opacity: 1;
@ -90,7 +92,6 @@ export default Vue.extend({
position: absolute; position: absolute;
top: auto; top: auto;
right: 0; right: 0;
z-index: 3;
margin: 16px 0 0 0; margin: 16px 0 0 0;
padding: 16px; padding: 16px;
width: 230px; width: 230px;

View File

@ -0,0 +1,54 @@
<template>
<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }">
<img class="xubzgfga" ref="img" :src="image.url" :alt="image.name" :title="image.name" @click="close" tabindex="-1"/>
</x-modal>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../i18n';
import XModal from './modal.vue';
export default Vue.extend({
i18n,
components: {
XModal,
},
props: {
image: {
type: Object,
required: true
},
},
mounted() {
this.$nextTick(() => {
this.$refs.img.focus();
});
},
methods: {
close() {
this.$refs.modal.close();
},
}
});
</script>
<style lang="scss" scoped>
.xubzgfga {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
max-width: 100%;
max-height: 100%;
margin: auto;
cursor: zoom-out;
image-orientation: from-image;
}
</style>

View File

@ -11,6 +11,7 @@ import url from './url.vue';
import loading from './loading.vue'; import loading from './loading.vue';
import SequentialEntrance from './sequential-entrance.vue'; import SequentialEntrance from './sequential-entrance.vue';
import error from './error.vue'; import error from './error.vue';
import streamIndicator from './stream-indicator.vue';
Vue.component('mfm', mfm); Vue.component('mfm', mfm);
Vue.component('mk-acct', acct); Vue.component('mk-acct', acct);
@ -23,3 +24,4 @@ Vue.component('mk-url', url);
Vue.component('mk-loading', loading); Vue.component('mk-loading', loading);
Vue.component('mk-error', error); Vue.component('mk-error', error);
Vue.component('sequential-entrance', SequentialEntrance); Vue.component('sequential-entrance', SequentialEntrance);
Vue.component('stream-indicator', streamIndicator);

View File

@ -1,8 +1,91 @@
<template> <template>
<div class="mk-instance-stats"> <div class="zbcjwnqg">
<div class="stats" v-if="info">
<div class="_panel">
<div>
<b><fa :icon="faUser"/>{{ $t('users') }}</b>
<small>{{ $t('local') }}</small>
</div>
<div>
<dl class="total">
<dt>{{ $t('total') }}</dt>
<dd>{{ info.originalUsersCount | number }}</dd>
</dl>
<dl class="diff" :class="{ inc: usersLocalDoD > 0 }">
<dt>{{ $t('dayOverDayChanges') }}</dt>
<dd>{{ usersLocalDoD | number }}</dd>
</dl>
<dl class="diff" :class="{ inc: usersLocalWoW > 0 }">
<dt>{{ $t('weekOverWeekChanges') }}</dt>
<dd>{{ usersLocalWoW | number }}</dd>
</dl>
</div>
</div>
<div class="_panel">
<div>
<b><fa :icon="faUser"/>{{ $t('users') }}</b>
<small>{{ $t('remote') }}</small>
</div>
<div>
<dl class="total">
<dt>{{ $t('total') }}</dt>
<dd>{{ (info.usersCount - info.originalUsersCount) | number }}</dd>
</dl>
<dl class="diff" :class="{ inc: usersRemoteDoD > 0 }">
<dt>{{ $t('dayOverDayChanges') }}</dt>
<dd>{{ usersRemoteDoD | number }}</dd>
</dl>
<dl class="diff" :class="{ inc: usersRemoteWoW > 0 }">
<dt>{{ $t('weekOverWeekChanges') }}</dt>
<dd>{{ usersRemoteWoW | number }}</dd>
</dl>
</div>
</div>
<div class="_panel">
<div>
<b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
<small>{{ $t('local') }}</small>
</div>
<div>
<dl class="total">
<dt>{{ $t('total') }}</dt>
<dd>{{ info.originalNotesCount | number }}</dd>
</dl>
<dl class="diff" :class="{ inc: notesLocalDoD > 0 }">
<dt>{{ $t('dayOverDayChanges') }}</dt>
<dd>{{ notesLocalDoD | number }}</dd>
</dl>
<dl class="diff" :class="{ inc: notesLocalWoW > 0 }">
<dt>{{ $t('weekOverWeekChanges') }}</dt>
<dd>{{ notesLocalWoW | number }}</dd>
</dl>
</div>
</div>
<div class="_panel">
<div>
<b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
<small>{{ $t('remote') }}</small>
</div>
<div>
<dl class="total">
<dt>{{ $t('total') }}</dt>
<dd>{{ (info.notesCount - info.originalNotesCount) | number }}</dd>
</dl>
<dl class="diff" :class="{ inc: notesRemoteDoD > 0 }">
<dt>{{ $t('dayOverDayChanges') }}</dt>
<dd>{{ notesRemoteDoD | number }}</dd>
</dl>
<dl class="diff" :class="{ inc: notesRemoteWoW > 0 }">
<dt>{{ $t('weekOverWeekChanges') }}</dt>
<dd>{{ notesRemoteWoW | number }}</dd>
</dl>
</div>
</div>
</div>
<section class="_card"> <section class="_card">
<div class="_title"><fa :icon="faChartBar"/> {{ $t('statistics') }}</div> <div class="_title"><fa :icon="faChartBar"/> {{ $t('statistics') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;"> <div class="_content" style="margin-top: -8px;">
<div class="selects" style="display: flex;"> <div class="selects" style="display: flex;">
<mk-select v-model="chartSrc" style="margin: 0; flex: 1;"> <mk-select v-model="chartSrc" style="margin: 0; flex: 1;">
<optgroup :label="$t('federation')"> <optgroup :label="$t('federation')">
@ -40,10 +123,10 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faChartBar } from '@fortawesome/free-solid-svg-icons'; import { faChartBar, faUser, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import Chart from 'chart.js'; import Chart from 'chart.js';
import i18n from '../../i18n'; import i18n from '../i18n';
import MkSelect from '../../components/ui/select.vue'; import MkSelect from './ui/select.vue';
const chartLimit = 90; const chartLimit = 90;
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
@ -59,24 +142,27 @@ const alpha = (hex, a) => {
export default Vue.extend({ export default Vue.extend({
i18n, i18n,
metaInfo() {
return {
title: `${this.$t('statistics')} | ${this.$t('instance')}`
};
},
components: { components: {
MkSelect MkSelect
}, },
data() { data() {
return { return {
info: null,
notesLocalWoW: 0,
notesLocalDoD: 0,
notesRemoteWoW: 0,
notesRemoteDoD: 0,
usersLocalWoW: 0,
usersLocalDoD: 0,
usersRemoteWoW: 0,
usersRemoteDoD: 0,
now: null, now: null,
chart: null, chart: null,
chartInstance: null, chartInstance: null,
chartSrc: 'notes', chartSrc: 'notes',
chartSpan: 'hour', chartSpan: 'hour',
faChartBar faChartBar, faUser, faPencilAlt
} }
}, },
@ -121,6 +207,8 @@ export default Vue.extend({
}, },
async created() { async created() {
this.info = await this.$root.api('stats');
this.now = new Date(); this.now = new Date();
const [perHour, perDay] = await Promise.all([Promise.all([ const [perHour, perDay] = await Promise.all([Promise.all([
@ -154,6 +242,15 @@ export default Vue.extend({
} }
}; };
this.notesLocalWoW = this.info.originalNotesCount - chart.perDay.notes.local.total[7];
this.notesLocalDoD = this.info.originalNotesCount - chart.perDay.notes.local.total[1];
this.notesRemoteWoW = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[7];
this.notesRemoteDoD = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[1];
this.usersLocalWoW = this.info.originalUsersCount - chart.perDay.users.local.total[7];
this.usersLocalDoD = this.info.originalUsersCount - chart.perDay.users.local.total[1];
this.usersRemoteWoW = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[7];
this.usersRemoteDoD = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[1];
this.chart = chart; this.chart = chart;
this.renderChart(); this.renderChart();
@ -489,3 +586,80 @@ export default Vue.extend({
} }
}); });
</script> </script>
<style lang="scss" scoped>
.zbcjwnqg {
> .stats {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin: calc(0px - var(--margin) / 2);
margin-bottom: calc(var(--margin) / 2);
> div {
display: flex;
flex: 1 0 213px;
margin: calc(var(--margin) / 2);
box-sizing: border-box;
padding: 16px 20px;
> div {
width: 50%;
&:first-child {
> b {
display: block;
> [data-icon] {
width: 16px;
margin-right: 8px;
}
}
> small {
margin-left: 16px + 8px;
opacity: 0.7;
}
}
&:last-child {
> dl {
display: flex;
margin: 0;
line-height: 1.5em;
> dt,
> dd {
width: 50%;
margin: 0;
}
> dt {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
&.total {
> dt,
> dd {
font-weight: bold;
}
}
&.diff.inc {
> dd {
color: #82c11c;
&:before {
content: "+";
}
}
}
}
}
}
}
}
}
</style>

View File

@ -20,6 +20,7 @@ import Vue from 'vue';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n'; import i18n from '../i18n';
import { getStaticImageUrl } from '../scripts/get-static-image-url'; import { getStaticImageUrl } from '../scripts/get-static-image-url';
import ImageViewer from './image-viewer.vue';
export default Vue.extend({ export default Vue.extend({
i18n, i18n,
@ -60,7 +61,16 @@ export default Vue.extend({
}, },
methods: { methods: {
onClick() { onClick() {
window.open(this.image.url, '_blank'); if (this.$store.state.device.imageNewTab) {
window.open(this.image.url, '_blank');
} else {
const viewer = this.$root.new(ImageViewer, {
image: this.image
});
this.$once('hook:beforeDestroy', () => {
viewer.close();
});
}
} }
} }
}); });

View File

@ -3,8 +3,8 @@
<template v-for="media in mediaList.filter(media => !previewable(media))"> <template v-for="media in mediaList.filter(media => !previewable(media))">
<x-banner :media="media" :key="media.id"/> <x-banner :media="media" :key="media.id"/>
</template> </template>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container"> <div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" ref="gridOuter">
<div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid"> <div :data-count="mediaList.filter(media => previewable(media)).length" :style="gridInnerStyle">
<template v-for="media in mediaList"> <template v-for="media in mediaList">
<x-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/> <x-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
<x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/> <x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
@ -32,19 +32,56 @@ export default Vue.extend({
}, },
raw: { raw: {
default: false default: false
},
// specify the parent element
parentElement: {
type: Object
}
},
data() {
return {
gridInnerStyle: {},
sizeWaiting: false
} }
}, },
mounted() { mounted() {
//#region for Safari bug this.size();
if (this.$refs.grid) { window.addEventListener('resize', this.size);
this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` },
: '287px'; beforeDestroy() {
} window.removeEventListener('resize', this.size);
//#endregion },
activated() {
this.size();
}, },
methods: { methods: {
previewable(file) { previewable(file) {
return file.type.startsWith('video') || file.type.startsWith('image'); return file.type.startsWith('video') || file.type.startsWith('image');
},
size() {
// for Safari bug
if (this.sizeWaiting) return;
this.sizeWaiting = true;
window.requestAnimationFrame(() => {
this.sizeWaiting = false;
if (this.$refs.gridOuter) {
let height = 287;
const parent = this.$props.parentElement || this.$parent.$el;
if (this.$refs.gridOuter.clientHeight) {
height = this.$refs.gridOuter.clientHeight;
} else if (parent) {
height = parent.getBoundingClientRect().width * 9 / 16;
}
this.gridInnerStyle = { height: `${height}px` };
} else {
this.gridInnerStyle = {};
}
});
} }
} }
}); });

View File

@ -176,7 +176,7 @@ export default Vue.extend({
position: absolute; position: absolute;
top: 5px; top: 5px;
left: 13px; left: 13px;
color: var(--accent); color: var(--indicator);
font-size: 12px; font-size: 12px;
animation: blink 1s infinite; animation: blink 1s infinite;
} }

View File

@ -37,6 +37,10 @@ export default Vue.extend({
font-size: 0.8em; font-size: 0.8em;
} }
::v-deep > code {
word-break: break-all;
}
::v-deep .title { ::v-deep .title {
text-align: center; text-align: center;
border-bottom: solid 1px var(--divider); border-bottom: solid 1px var(--divider);

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mk-modal"> <div class="mk-modal" v-hotkey.global="keymap">
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear> <transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
<div class="bg" ref="bg" v-if="show" @click="close()"></div> <div class="bg" ref="bg" v-if="show" @click="close()"></div>
</transition> </transition>
@ -20,6 +20,13 @@ export default Vue.extend({
show: true, show: true,
}; };
}, },
computed: {
keymap(): any {
return {
'esc': this.close,
};
},
},
methods: { methods: {
close() { close() {
this.show = false; this.show = false;

View File

@ -77,23 +77,19 @@ export default Vue.extend({
> .admin, > .admin,
> .moderator { > .moderator {
margin-right: 0.5em; margin-right: 0.5em;
color: var(--badge);
} }
> .username { > .username {
margin: 0 .5em 0 0; margin: 0 .5em 0 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
color: var(--noteHeaderAcct);
} }
> .info { > .info {
margin-left: auto; margin-left: auto;
font-size: 0.9em; font-size: 0.9em;
> * {
color: var(--noteHeaderInfo);
}
> .mobile { > .mobile {
margin-right: 8px; margin-right: 8px;
} }

View File

@ -9,7 +9,9 @@
> >
<x-sub v-for="note in conversation" :key="note.id" :note="note"/> <x-sub v-for="note in conversation" :key="note.id" :note="note"/>
<x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/> <x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
<div class="pinned" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div> <div class="info" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div>
<div class="info" v-if="appearNote._prId_"><fa :icon="faBullhorn"/> {{ $t('promotion') }}<button class="_textButton hide" @click="readPromo()">{{ $t('hideThisNote') }} <fa :icon="faTimes"/></button></div>
<div class="info" v-if="appearNote._featuredId_"><fa :icon="faBolt"/> {{ $t('featured') }}</div>
<div class="renote" v-if="isRenote"> <div class="renote" v-if="isRenote">
<mk-avatar class="avatar" :user="note.user"/> <mk-avatar class="avatar" :user="note.user"/>
<fa :icon="faRetweet"/> <fa :icon="faRetweet"/>
@ -58,7 +60,7 @@
<template v-else><fa :icon="faReply"/></template> <template v-else><fa :icon="faReply"/></template>
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
</button> </button>
<button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" class="button _button" ref="renoteButton"> <button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton">
<fa :icon="faRetweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> <fa :icon="faRetweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
</button> </button>
<button v-else class="button _button"> <button v-else class="button _button">
@ -83,7 +85,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight } from '@fortawesome/free-solid-svg-icons'; import { faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
import { parse } from '../../mfm/parse'; import { parse } from '../../mfm/parse';
import { sum, unique } from '../../prelude/array'; import { sum, unique } from '../../prelude/array';
@ -140,7 +142,7 @@ export default Vue.extend({
replies: [], replies: [],
showContent: false, showContent: false,
hideThisNote: false, hideThisNote: false,
faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan
}; };
}, },
@ -190,16 +192,16 @@ export default Vue.extend({
return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId); return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId);
}, },
canRenote(): boolean {
return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
},
reactionsCount(): number { reactionsCount(): number {
return this.appearNote.reactions return this.appearNote.reactions
? sum(Object.values(this.appearNote.reactions)) ? sum(Object.values(this.appearNote.reactions))
: 0; : 0;
}, },
title(): string {
return '';
},
urls(): string[] { urls(): string[] {
if (this.appearNote.text) { if (this.appearNote.text) {
const ast = parse(this.appearNote.text); const ast = parse(this.appearNote.text);
@ -263,6 +265,13 @@ export default Vue.extend({
}, },
methods: { methods: {
readPromo() {
(this as any).$root.api('promo/read', {
noteId: this.appearNote.id
});
this.hideThisNote = true;
},
capture(withHandler = false) { capture(withHandler = false) {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id }); this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
@ -383,7 +392,7 @@ export default Vue.extend({
}] }]
source: this.$refs.renoteButton, source: this.$refs.renoteButton,
viaKeyboard viaKeyboard
}).then(this.focus); });
}, },
renoteDirectly() { renoteDirectly() {
@ -480,6 +489,11 @@ export default Vue.extend({
noteId: this.appearNote.id noteId: this.appearNote.id
}); });
menu = [{ menu = [{
type: 'link',
icon: faInfoCircle,
text: this.$t('details'),
to: '/notes/' + this.appearNote.id
}, null, {
icon: faCopy, icon: faCopy,
text: this.$t('copyContent'), text: this.$t('copyContent'),
action: this.copyContent action: this.copyContent
@ -522,6 +536,15 @@ export default Vue.extend({
text: this.$t('pin'), text: this.$t('pin'),
action: () => this.togglePin(true) action: () => this.togglePin(true)
} : undefined, } : undefined,
...(this.$store.state.i.isModerator || this.$store.state.i.isAdmin ? [
null,
{
icon: faBullhorn,
text: this.$t('promote'),
action: this.promote
}]
: []
),
...(this.appearNote.userId == this.$store.state.i.id ? [ ...(this.appearNote.userId == this.$store.state.i.id ? [
null, null,
{ {
@ -614,6 +637,30 @@ export default Vue.extend({
}); });
}, },
async promote() {
const { canceled, result: days } = await this.$root.dialog({
title: this.$t('numberOfDays'),
input: { type: 'number' }
});
if (canceled) return;
this.$root.api('admin/promo/create', {
noteId: this.appearNote.id,
expiresAt: Date.now() + (86400000 * days)
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
},
focus() { focus() {
this.$el.focus(); this.$el.focus();
}, },
@ -710,7 +757,9 @@ export default Vue.extend({
border-radius: 0 0 var(--radius) var(--radius); border-radius: 0 0 var(--radius) var(--radius);
} }
> .pinned { > .info {
display: flex;
align-items: center;
padding: 16px 32px 8px 32px; padding: 16px 32px 8px 32px;
line-height: 24px; line-height: 24px;
font-size: 90%; font-size: 90%;
@ -724,9 +773,14 @@ export default Vue.extend({
> [data-icon] { > [data-icon] {
margin-right: 4px; margin-right: 4px;
} }
> .hide {
margin-left: auto;
color: inherit;
}
} }
> .pinned + .article { > .info + .article {
padding-top: 8px; padding-top: 8px;
} }

View File

@ -1,22 +1,29 @@
<template> <template>
<div class="mk-notes" v-size="[{ max: 500 }]"> <div class="mk-notes" v-size="[{ max: 500 }]">
<div class="empty" v-if="empty"> <div class="empty" v-if="empty">
<img src="https://xn--931a.moe/assets/info.png" alt="" class="_ghost"/> <img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
<div>{{ $t('noNotes') }}</div> <div>{{ $t('noNotes') }}</div>
</div> </div>
<mk-error v-if="error" @retry="init()"/> <mk-error v-if="error" @retry="init()"/>
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }"> <div class="more" v-if="more && reversed" style="margin-bottom: var(--margin);">
<x-note :note="note" :detail="detail" :key="note.id"/>
</x-list>
<footer class="more" v-if="more">
<mk-button class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()" primary> <mk-button class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()" primary>
<template v-if="!moreFetching">{{ $t('loadMore') }}</template> <template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><mk-loading inline/></template> <template v-if="moreFetching"><mk-loading inline/></template>
</mk-button> </mk-button>
</footer> </div>
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
<x-note :note="note" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
</x-list>
<div class="more" v-if="more && !reversed" style="margin-top: var(--margin);">
<mk-button class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()" primary>
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><mk-loading inline/></template>
</mk-button>
</div>
</div> </div>
</template> </template>
@ -67,6 +74,10 @@ export default Vue.extend({
notes(): any[] { notes(): any[] {
return this.extract ? this.extract(this.items) : this.items; return this.extract ? this.extract(this.items) : this.items;
}, },
reversed(): boolean {
return this.pagination.reversed;
}
}, },
methods: { methods: {
@ -92,14 +103,14 @@ export default Vue.extend({
} }
> .notes { > .notes {
> ::v-deep * { > ::v-deep *:not(:last-child) {
margin-bottom: var(--marginFull); margin-bottom: var(--marginFull);
} }
} }
&.max-width_500px { &.max-width_500px {
> .notes { > .notes {
> ::v-deep * { > ::v-deep *:not(:last-child) {
margin-bottom: var(--marginHalf); margin-bottom: var(--marginHalf);
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mk-notification" :class="notification.type"> <div class="mk-notification" :class="notification.type" v-size="[{ max: 500 }, { max: 600 }]">
<div class="head"> <div class="head">
<mk-avatar class="avatar" :user="notification.user"/> <mk-avatar class="avatar" :user="notification.user"/>
<div class="icon" :class="notification.type"> <div class="icon" :class="notification.type">
@ -113,12 +113,17 @@ export default Vue.extend({
.mk-notification { .mk-notification {
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
padding: 16px; padding: 24px 32px;
font-size: 0.9em; font-size: 0.9em;
overflow-wrap: break-word; overflow-wrap: break-word;
display: flex; display: flex;
@media (max-width: 500px) { &.max-width_600px {
padding: 16px;
font-size: 0.9em;
}
&.max-width_500px {
padding: 12px; padding: 12px;
font-size: 0.8em; font-size: 0.8em;
} }
@ -169,7 +174,7 @@ export default Vue.extend({
background: #36aed2; background: #36aed2;
} }
&.retweet { &.renote {
padding: 3px; padding: 3px;
background: #36d298; background: #36d298;
} }

View File

@ -1,19 +1,18 @@
<template> <template>
<div class="mk-notifications"> <div class="mk-notifications" :class="{ page }">
<div class="contents"> <x-list class="notifications" :items="items" v-slot="{ item: notification }">
<x-list class="notifications" :items="items" v-slot="{ item: notification, i }"> <x-note v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" :key="notification.id"/>
<x-notification :notification="notification" :with-time="true" :full="true" class="notification" :key="notification.id"/> <x-notification v-else :notification="notification" :with-time="true" :full="true" class="notification" :class="{ _panel: page }" :key="notification.id"/>
</x-list> </x-list>
<button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching"> <button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template> <template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template> <template v-if="moreFetching"><fa :icon="faSpinner" pulse fixed-width/></template>
</button> </button>
<p class="empty" v-if="empty">{{ $t('noNotifications') }}</p> <p class="empty" v-if="empty">{{ $t('noNotifications') }}</p>
<mk-error v-if="error" @retry="init()"/> <mk-error v-if="error" @retry="init()"/>
</div>
</div> </div>
</template> </template>
@ -24,6 +23,7 @@ import i18n from '../i18n';
import paging from '../scripts/paging'; import paging from '../scripts/paging';
import XNotification from './notification.vue'; import XNotification from './notification.vue';
import XList from './date-separated-list.vue'; import XList from './date-separated-list.vue';
import XNote from './note.vue';
export default Vue.extend({ export default Vue.extend({
i18n, i18n,
@ -31,6 +31,7 @@ export default Vue.extend({
components: { components: {
XNotification, XNotification,
XList, XList,
XNote,
}, },
mixins: [ mixins: [
@ -42,7 +43,7 @@ export default Vue.extend({
type: String, type: String,
required: false required: false
}, },
wide: { page: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
@ -93,11 +94,15 @@ export default Vue.extend({
<style lang="scss" scoped> <style lang="scss" scoped>
.mk-notifications { .mk-notifications {
> .contents { &.page {
overflow: auto; > .notifications {
height: 100%; > ::v-deep * {
padding: 8px 8px 0 8px; margin-bottom: var(--margin);
}
}
}
&:not(.page) {
> .notifications { > .notifications {
> ::v-deep * { > ::v-deep * {
margin-bottom: 8px; margin-bottom: 8px;
@ -109,28 +114,28 @@ export default Vue.extend({
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
} }
}
> .more { > .more {
display: block; display: block;
width: 100%; width: 100%;
padding: 16px; padding: 16px;
> [data-icon] { > [data-icon] {
margin-right: 4px; margin-right: 4px;
}
} }
}
> .empty { > .empty {
margin: 0; margin: 0;
padding: 16px; padding: 16px;
text-align: center; text-align: center;
color: var(--fg); color: var(--fg);
} }
> .placeholder { > .placeholder {
padding: 32px; padding: 32px;
opacity: 0.3; opacity: 0.3;
}
} }
} }
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="lzyxtsnt"> <div class="lzyxtsnt">
<img v-if="image" :src="image.url" alt=""/> <img v-if="image" :src="image.url"/>
</div> </div>
</template> </template>

View File

@ -0,0 +1,108 @@
<template>
<div class="vswabwbm" :style="{ top: `${y - 64}px`, left: `${x - 64}px` }" :class="{ active }">
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<circle fill="none" cx="64" cy="64">
<animate attributeName="r"
begin="0s" dur="0.5s"
values="4; 32"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="1" />
<animate attributeName="stroke-width"
begin="0s" dur="0.5s"
values="16; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="1" />
</circle>
<g fill="none" fill-rule="evenodd">
<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color">
<animate attributeName="r"
begin="0s" dur="0.8s"
:values="`${particle.size}; 0`"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="1" />
<animate attributeName="cx"
begin="0s" dur="0.8s"
:values="`${particle.xA}; ${particle.xB}`"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="1" />
<animate attributeName="cy"
begin="0s" dur="0.8s"
:values="`${particle.yA}; ${particle.yB}`"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="1" />
</circle>
</g>
</svg>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
x: {
type: Number,
required: true
},
y: {
type: Number,
required: true
}
},
data() {
const particles = [];
const origin = 64;
const colors = ['#FF1493', '#00FFFF', '#FFE202'];
for (let i = 0; i < 12; i++) {
const angle = Math.random() * (Math.PI * 2);
const pos = Math.random() * 16;
const velocity = 16 + (Math.random() * 48);
particles.push({
size: 4 + (Math.random() * 8),
xA: origin + (Math.sin(angle) * pos),
yA: origin + (Math.cos(angle) * pos),
xB: origin + (Math.sin(angle) * (pos + velocity)),
yB: origin + (Math.cos(angle) * (pos + velocity)),
color: colors[Math.floor(Math.random() * colors.length)]
});
}
return {
particles
};
},
mounted() {
setTimeout(() => {
this.destroyDom();
}, 1100);
}
});
</script>
<style lang="scss" scoped>
.vswabwbm {
pointer-events: none;
position: fixed;
z-index: 1000000;
width: 128px;
height: 128px;
> svg {
> circle {
stroke: var(--accent);
}
}
}
</style>

View File

@ -112,8 +112,7 @@ export default Vue.extend({
margin: 4px 0; margin: 4px 0;
padding: 4px 8px; padding: 4px 8px;
width: 100%; width: 100%;
color: var(--pollChoiceText); border: solid 1px var(--divider);
border: solid 1px var(--pollChoiceBorder);
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="ulveipglmagnxfgvitaxyszerjwiqmwl"> <div class="ulveipglmagnxfgvitaxyszerjwiqmwl">
<transition :name="$store.state.device.animation ? 'form-fade' : ''" appear> <transition :name="$store.state.device.animation ? 'form-fade' : ''" appear @after-leave="$emit('closed');">
<div class="bg" ref="bg" v-if="show" @click="close()"></div> <div class="bg" ref="bg" v-if="show" @click="close()"></div>
</transition> </transition>
<div class="main" ref="main" @click.self="close()" @keydown="onKeydown"> <div class="main" ref="main" @click.self="close()" @keydown="onKeydown">

View File

@ -6,7 +6,7 @@
@drop.stop="onDrop" @drop.stop="onDrop"
> >
<header> <header>
<button class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button> <button v-if="!fixed" class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button>
<div> <div>
<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span> <span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span>
<button class="_button visibility" @click="setVisibility" ref="visibilityButton"> <button class="_button visibility" @click="setVisibility" ref="visibilityButton">
@ -18,7 +18,7 @@
<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<fa :icon="reply ? faReply : renote ? faQuoteRight : faPaperPlane"/></button> <button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<fa :icon="reply ? faReply : renote ? faQuoteRight : faPaperPlane"/></button>
</div> </div>
</header> </header>
<div class="form"> <div class="form" :class="{ fixed }">
<x-note-preview class="preview" v-if="reply" :note="reply"/> <x-note-preview class="preview" v-if="reply" :note="reply"/>
<x-note-preview class="preview" v-if="renote" :note="renote"/> <x-note-preview class="preview" v-if="renote" :note="renote"/>
<div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('quoteAttached') }}<button @click="quoteId = null"><fa icon="times"/></button></div> <div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('quoteAttached') }}<button @click="quoteId = null"><fa icon="times"/></button></div>
@ -108,6 +108,11 @@ export default Vue.extend({
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
},
fixed: {
type: Boolean,
required: false,
default: false
} }
}, },
@ -582,7 +587,6 @@ export default Vue.extend({
.gafaadew { .gafaadew {
background: var(--panel); background: var(--panel);
border-radius: var(--radius); border-radius: var(--radius);
box-shadow: 0 0 2px rgba(#000, 0.1);
> header { > header {
z-index: 1000; z-index: 1000;
@ -651,6 +655,10 @@ export default Vue.extend({
max-width: 500px; max-width: 500px;
margin: 0 auto; margin: 0 auto;
&.fixed {
max-width: unset;
}
> .preview { > .preview {
padding: 16px; padding: 16px;
} }

View File

@ -1,20 +1,9 @@
<template> <template>
<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap"> <x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap">
<div class="rdfaahpb"> <div class="rdfaahpb">
<transition-group <div class="buttons" ref="buttons" :class="{ showFocus }">
name="reaction-fade" <button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :tabindex="i + 1" :title="reaction" v-particle><x-reaction-icon :reaction="reaction"/></button>
tag="div" </div>
class="buttons"
ref="buttons"
:class="{ showFocus }"
:css="false"
@before-enter="beforeEnter"
@enter="enter"
mode="out-in"
appear
>
<button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :tabindex="i + 1" :title="reaction"><x-reaction-icon :reaction="reaction"/></button>
</transition-group>
<input class="text" v-model="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }"> <input class="text" v-model="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }">
</div> </div>
</x-popup> </x-popup>
@ -84,7 +73,7 @@ export default Vue.extend({
watch: { watch: {
focus(i) { focus(i) {
this.$refs.buttons.children[i].elm.focus(); this.$refs.buttons.children[i].focus();
} }
}, },
@ -129,21 +118,7 @@ export default Vue.extend({
}, },
choose() { choose() {
this.$refs.buttons.children[this.focus].elm.click(); this.$refs.buttons.children[this.focus].click();
},
beforeEnter(el) {
el.style.opacity = 0;
el.style.transform = 'scale(0.7)';
},
enter(el, done) {
el.style.transition = [getComputedStyle(el).transition, 'transform 1s cubic-bezier(0.23, 1, 0.32, 1)', 'opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1)'].filter(x => x != '').join(',');
setTimeout(() => {
el.style.opacity = 1;
el.style.transform = 'scale(1)';
setTimeout(done, 1000);
}, 0 * el.dataset.index)
}, },
} }
}); });

View File

@ -1,16 +1,17 @@
<template> <template>
<span <button
class="reaction _button" class="hkzvhatu _button"
:class="{ reacted: note.myReaction == reaction }" :class="{ reacted: note.myReaction == reaction }"
@click="toggleReaction(reaction)" @click="toggleReaction(reaction)"
v-if="count > 0" v-if="count > 0"
@mouseover="onMouseover" @mouseover="onMouseover"
@mouseleave="onMouseleave" @mouseleave="onMouseleave"
ref="reaction" ref="reaction"
v-particle
> >
<x-reaction-icon :reaction="reaction" ref="icon"/> <x-reaction-icon :reaction="reaction" ref="icon"/>
<span>{{ count }}</span> <span>{{ count }}</span>
</span> </button>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -136,7 +137,7 @@ export default Vue.extend({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.reaction { .hkzvhatu {
display: inline-block; display: inline-block;
height: 32px; height: 32px;
margin: 2px; margin: 2px;

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mk-reactions-viewer" :class="{ isMe }"> <div class="tdflqwzn" :class="{ isMe }">
<x-reaction v-for="(count, reaction) in note.reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :key="reaction"/> <x-reaction v-for="(count, reaction) in note.reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :key="reaction"/>
</div> </div>
</template> </template>
@ -32,7 +32,7 @@ export default Vue.extend({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.mk-reactions-viewer { .tdflqwzn {
margin: 4px -2px 0 -2px; margin: 4px -2px 0 -2px;
&:empty { &:empty {

View File

@ -1,12 +1,8 @@
<template> <template>
<transition-group v-if="$store.state.device.animation" <transition-group v-if="$store.state.device.animation"
name="staggered-fade" class="uupnnhew"
name="staggered"
tag="div" tag="div"
:css="false"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
mode="out-in"
appear appear
> >
<slot></slot> <slot></slot>
@ -20,65 +16,25 @@
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
props: {
delay: {
type: Number,
required: false,
default: 40
},
direction: {
type: String,
required: false,
default: 'down'
},
reversed: {
type: Boolean,
required: false,
default: false
}
},
i: 0,
methods: { methods: {
beforeEnter(el) {
if (document.hidden) return;
el.style.opacity = 0;
el.style.transform = this.direction === 'down' ? 'translateY(-64px)' : 'translateY(64px)';
const delay = this.delay * this.$options.i;
el.style.transition = [getComputedStyle(el).transition, `transform 0.7s cubic-bezier(0.23, 1, 0.32, 1) ${delay}ms`, `opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1) ${delay}ms`].filter(x => x != '').join(',');
this.$options.i++;
setTimeout(() => {
el.style.transition = null;
el.style.transform = null;
el.style.opacity = null;
this.$options.i--;
}, delay + 710);
},
enter(el) {
if (document.hidden) {
el.style.opacity = 1;
el.style.transform = 'translateY(0px)';
} else {
setTimeout(() => { // 必要
el.style.opacity = 1;
el.style.transform = 'translateY(0px)';
});
}
},
leave(el) {
el.style.opacity = 0;
el.style.transform = this.direction === 'down' ? 'translateY(64px)' : 'translateY(-64px)';
},
focus() { focus() {
this.$slots.default[0].elm.focus(); this.$slots.default[0].elm.focus();
} }
} },
}); });
</script> </script>
<style lang="scss"> <style lang="scss">
.staggered-fade-move { .uupnnhew {
transition: transform 0.7s !important; > .staggered-enter {
opacity: 0;
transform: translateY(-64px);
}
@for $i from 1 through 30 {
> .staggered-enter-active:nth-child(#{$i}) {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1) (15ms * ($i - 1)), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1) (15ms * ($i - 1));
}
}
} }
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<x-window ref="window" @closed="() => { $emit('closed'); destroyDom(); }"> <x-window ref="window" @closed="() => { $emit('closed'); destroyDom(); }">
<template #header>{{ $t('login') }}</template> <template #header>{{ $t('login') }}</template>
<x-signin :auto-set="autoSet" @login="onLogin"/> <mk-signin :auto-set="autoSet" @login="onLogin"/>
</x-window> </x-window>
</template> </template>
@ -9,13 +9,13 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../i18n'; import i18n from '../i18n';
import XWindow from './window.vue'; import XWindow from './window.vue';
import XSignin from './signin.vue'; import MkSignin from './signin.vue';
export default Vue.extend({ export default Vue.extend({
i18n, i18n,
components: { components: {
XSignin, MkSignin,
XWindow, XWindow,
}, },

View File

@ -0,0 +1,80 @@
<template>
<div class="nsbbhtug" v-if="hasDisconnected" @click="resetDisconnected">
<div>{{ $t('disconnectedFromServer') }}</div>
<div class="command">
<button class="_textButton" @click="reload">{{ $t('reload') }}</button>
<button class="_textButton">{{ $t('doNothing') }}</button>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../i18n';
export default Vue.extend({
i18n,
data() {
return {
hasDisconnected: false,
}
},
computed: {
stream() {
return this.$root.stream;
},
},
created() {
this.$root.stream.on('_connected_', this.onConnected);
this.$root.stream.on('_disconnected_', this.onDisconnected);
},
beforeDestroy() {
this.$root.stream.off('_connected_', this.onConnected);
this.$root.stream.off('_disconnected_', this.onDisconnected);
},
methods: {
onConnected() {
if (this.hasDisconnected) {
if (this.$store.state.device.autoReload) {
this.reload();
}
}
},
onDisconnected() {
this.hasDisconnected = true;
},
resetDisconnected() {
this.hasDisconnected = false;
},
reload() {
location.reload();
},
}
});
</script>
<style lang="scss" scoped>
.nsbbhtug {
position: fixed;
z-index: 16385;
bottom: 8px;
right: 8px;
margin: 0;
padding: 6px 12px;
font-size: 0.9em;
color: #fff;
background: #000;
opacity: 0.8;
border-radius: 4px;
max-width: 320px;
> .command {
display: flex;
justify-content: space-around;
> button {
padding: 0.7em;
}
}
}
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<x-notes ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/> <x-notes ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -21,6 +21,11 @@ export default Vue.extend({
}, },
antenna: { antenna: {
required: false required: false
},
sound: {
type: Boolean,
required: false,
default: false,
} }
}, },
@ -46,6 +51,10 @@ export default Vue.extend({
const prepend = note => { const prepend = note => {
(this.$refs.tl as any).prepend(note); (this.$refs.tl as any).prepend(note);
if (this.sound) {
this.$root.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
}
}; };
const onUserAdded = () => { const onUserAdded = () => {

View File

@ -243,6 +243,10 @@ export default Vue.extend({
margin-top: 8px; margin-top: 8px;
} }
&:not(.inline):last-child {
margin-bottom: 8px;
}
> .icon { > .icon {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -56,7 +56,7 @@ export default Vue.extend({
} }
}, },
filled(): boolean { filled(): boolean {
return this.v != '' && this.v != null; return true;
} }
}, },
mounted() { mounted() {
@ -81,6 +81,10 @@ export default Vue.extend({
margin-top: 8px; margin-top: 8px;
} }
&:not(.inline):last-child {
margin-bottom: 8px;
}
> .icon { > .icon {
position: absolute; position: absolute;
top: 0; top: 0;
@ -96,6 +100,7 @@ export default Vue.extend({
> .input { > .input {
display: flex; display: flex;
position: relative;
&:before { &:before {
content: ''; content: '';
@ -147,7 +152,7 @@ export default Vue.extend({
font-weight: normal; font-weight: normal;
font-size: 16px; font-size: 16px;
height: 32px; height: 32px;
background: var(--panel); background: none;
border: none; border: none;
border-radius: 0; border-radius: 0;
outline: none; outline: none;

View File

@ -129,7 +129,6 @@ export default Vue.extend({
> .label { > .label {
margin-left: 8px; margin-left: 8px;
display: block; display: block;
font-size: 16px;
cursor: pointer; cursor: pointer;
transition: inherit; transition: inherit;
color: var(--fg); color: var(--fg);

View File

@ -51,6 +51,7 @@ export default Vue.extend({
target: self ? null : '_blank', target: self ? null : '_blank',
showTimer: null, showTimer: null,
hideTimer: null, hideTimer: null,
checkTimer: null,
preview: null, preview: null,
faExternalLinkSquareAlt faExternalLinkSquareAlt
}; };
@ -78,9 +79,14 @@ export default Vue.extend({
}).$mount(); }).$mount();
document.body.appendChild(this.preview.$el); document.body.appendChild(this.preview.$el);
this.checkTimer = setInterval(() => {
if (!document.body.contains(this.$el)) this.closePreview();
}, 1000);
}, },
closePreview() { closePreview() {
if (this.preview) { if (this.preview) {
clearInterval(this.checkTimer);
this.preview.destroyDom(); this.preview.destroyDom();
this.preview = null; this.preview = null;
} }

View File

@ -3,7 +3,7 @@
<template #header><mk-user-name :user="user"/></template> <template #header><mk-user-name :user="user"/></template>
<div class="vrcsvlkm"> <div class="vrcsvlkm">
<mk-button @click="resetPassword()" primary>{{ $t('resetPassword') }}</mk-button> <mk-button @click="resetPassword()" primary>{{ $t('resetPassword') }}</mk-button>
<mk-switch v-if="$store.state.i.isAdmin" @change="toggleModerator()" v-model="moderator">{{ $t('moderator') }}</mk-switch> <mk-switch v-if="$store.state.i.isAdmin && (this.moderator || !user.isAdmin)" @change="toggleModerator()" v-model="moderator">{{ $t('moderator') }}</mk-switch>
<mk-switch @change="toggleSilence()" v-model="silenced">{{ $t('silence') }}</mk-switch> <mk-switch @change="toggleSilence()" v-model="silenced">{{ $t('silence') }}</mk-switch>
<mk-switch @change="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch> <mk-switch @change="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch>
</div> </div>

View File

@ -53,6 +53,7 @@ export default Vue.extend({
return { return {
u: null, u: null,
show: false, show: false,
closed: false,
top: 0, top: 0,
left: 0, left: 0,
}; };
@ -68,6 +69,7 @@ export default Vue.extend({
{ userId: this.user }; { userId: this.user };
this.$root.api('users/show', query).then(user => { this.$root.api('users/show', query).then(user => {
if (this.closed) return;
this.u = user; this.u = user;
this.show = true; this.show = true;
}); });
@ -83,6 +85,7 @@ export default Vue.extend({
methods: { methods: {
close() { close() {
this.closed = true;
this.show = false; this.show = false;
if (this.$refs.content) (this.$refs.content as any).style.pointerEvents = 'none'; if (this.$refs.content) (this.$refs.content as any).style.pointerEvents = 'none';
} }

View File

@ -3,8 +3,10 @@ import Vue from 'vue';
import userPreview from './user-preview'; import userPreview from './user-preview';
import autocomplete from './autocomplete'; import autocomplete from './autocomplete';
import size from './size'; import size from './size';
import particle from './particle';
Vue.directive('autocomplete', autocomplete); Vue.directive('autocomplete', autocomplete);
Vue.directive('userPreview', userPreview); Vue.directive('userPreview', userPreview);
Vue.directive('user-preview', userPreview); Vue.directive('user-preview', userPreview);
Vue.directive('size', size); Vue.directive('size', size);
Vue.directive('particle', particle);

View File

@ -0,0 +1,22 @@
import Particle from '../components/particle.vue';
export default {
bind(el, binding, vn) {
el.addEventListener('click', () => {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.clientWidth / 2);
const y = rect.top + (el.clientHeight / 2);
const particle = new Particle({
parent: vn.context,
propsData: {
x,
y
}
}).$mount();
document.body.appendChild(particle.$el);
});
}
};

View File

@ -54,6 +54,8 @@ export default {
calc(); calc();
vn.context.$on('hook:activated', calc);
const ro = new ResizeObserver((entries, observer) => { const ro = new ResizeObserver((entries, observer) => {
calc(); calc();
}); });

View File

@ -8,9 +8,11 @@ export default {
self.tag = null; self.tag = null;
self.showTimer = null; self.showTimer = null;
self.hideTimer = null; self.hideTimer = null;
self.checkTimer = null;
self.close = () => { self.close = () => {
if (self.tag) { if (self.tag) {
clearInterval(self.checkTimer);
self.tag.close(); self.tag.close();
self.tag = null; self.tag = null;
} }
@ -38,6 +40,14 @@ export default {
}); });
document.body.appendChild(self.tag.$el); document.body.appendChild(self.tag.$el);
self.checkTimer = setInterval(() => {
if (!document.body.contains(el)) {
clearTimeout(self.showTimer);
clearTimeout(self.hideTimer);
self.close();
}
}, 1000);
}; };
el.addEventListener('mouseover', () => { el.addEventListener('mouseover', () => {
@ -60,8 +70,6 @@ export default {
unbind(el, binding, vn) { unbind(el, binding, vn) {
const self = el._userPreviewDirective_; const self = el._userPreviewDirective_;
clearTimeout(self.showTimer); clearInterval(self.checkTimer);
clearTimeout(self.hideTimer);
self.close();
} }
}; };

View File

@ -136,7 +136,13 @@ document.body.innerHTML = '<div id="app"></div>';
const os = new MiOS(); const os = new MiOS();
os.init(async () => { os.init(async () => {
if (os.store.state.settings.wallpaper) document.documentElement.style.backgroundImage = `url(${os.store.state.settings.wallpaper})`; window.addEventListener('storage', e => {
if (e.key === 'vuex') {
os.store.replaceState(JSON.parse(localStorage['vuex']));
} else if (e.key === 'i') {
location.reload();
}
}, false)
if ('Notification' in window && os.store.getters.isSignedIn) { if ('Notification' in window && os.store.getters.isSignedIn) {
// 許可を得ていなかったらリクエスト // 許可を得ていなかったらリクエスト
@ -191,6 +197,14 @@ os.init(async () => {
if (cb) vm.$once('closed', cb); if (cb) vm.$once('closed', cb);
(vm as any).focus(); (vm as any).focus();
}, },
sound(type: string) {
if (this.$store.state.device.sfxVolume === 0) return;
const sound = this.$store.state.device['sfx' + type.substr(0, 1).toUpperCase() + type.substr(1)];
if (sound == null) return;
const audio = new Audio(`/assets/sounds/${sound}.mp3`);
audio.volume = this.$store.state.device.sfxVolume;
audio.play();
}
}, },
router: router, router: router,
render: createEl => createEl(App) render: createEl => createEl(App)
@ -200,4 +214,96 @@ os.init(async () => {
// マウント // マウント
app.$mount('#app'); app.$mount('#app');
if (app.$store.getters.isSignedIn) {
const main = os.stream.useSharedConnection('main');
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
app.$store.dispatch('mergeMe', i);
});
main.on('readAllNotifications', () => {
app.$store.dispatch('mergeMe', {
hasUnreadNotification: false
});
});
main.on('unreadNotification', () => {
app.$store.dispatch('mergeMe', {
hasUnreadNotification: true
});
});
main.on('unreadMention', () => {
app.$store.dispatch('mergeMe', {
hasUnreadMentions: true
});
});
main.on('readAllUnreadMentions', () => {
app.$store.dispatch('mergeMe', {
hasUnreadMentions: false
});
});
main.on('unreadSpecifiedNote', () => {
app.$store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: true
});
});
main.on('readAllUnreadSpecifiedNotes', () => {
app.$store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: false
});
});
main.on('readAllMessagingMessages', () => {
app.$store.dispatch('mergeMe', {
hasUnreadMessagingMessage: false
});
});
main.on('unreadMessagingMessage', () => {
app.$store.dispatch('mergeMe', {
hasUnreadMessagingMessage: true
});
app.sound('chatBg');
});
main.on('readAllAntennas', () => {
app.$store.dispatch('mergeMe', {
hasUnreadAntenna: false
});
});
main.on('unreadAntenna', () => {
app.$store.dispatch('mergeMe', {
hasUnreadAntenna: true
});
app.sound('antenna');
});
main.on('readAllAnnouncements', () => {
app.$store.dispatch('mergeMe', {
hasUnreadAnnouncement: false
});
});
main.on('clientSettingUpdated', x => {
app.$store.commit('settings/set', {
key: x.key,
value: x.value
});
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {
os.signout();
});
}
}); });

View File

@ -3,7 +3,7 @@ import Vue from 'vue';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import initStore from './store'; import initStore from './store';
import { apiUrl, version, locale } from './config'; import { apiUrl, version } from './config';
import Progress from './scripts/loading'; import Progress from './scripts/loading';
import Stream from './scripts/stream'; import Stream from './scripts/stream';
@ -142,95 +142,6 @@ export default class MiOS extends EventEmitter {
@autobind @autobind
private initStream() { private initStream() {
this.stream = new Stream(this); this.stream = new Stream(this);
if (this.store.getters.isSignedIn) {
const main = this.stream.useSharedConnection('main');
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
this.store.dispatch('mergeMe', i);
});
main.on('readAllNotifications', () => {
this.store.dispatch('mergeMe', {
hasUnreadNotification: false
});
});
main.on('unreadNotification', () => {
this.store.dispatch('mergeMe', {
hasUnreadNotification: true
});
});
main.on('unreadMention', () => {
this.store.dispatch('mergeMe', {
hasUnreadMentions: true
});
});
main.on('readAllUnreadMentions', () => {
this.store.dispatch('mergeMe', {
hasUnreadMentions: false
});
});
main.on('unreadSpecifiedNote', () => {
this.store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: true
});
});
main.on('readAllUnreadSpecifiedNotes', () => {
this.store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: false
});
});
main.on('readAllMessagingMessages', () => {
this.store.dispatch('mergeMe', {
hasUnreadMessagingMessage: false
});
});
main.on('unreadMessagingMessage', () => {
this.store.dispatch('mergeMe', {
hasUnreadMessagingMessage: true
});
});
main.on('readAllAntennas', () => {
this.store.dispatch('mergeMe', {
hasUnreadAntenna: false
});
});
main.on('unreadAntenna', () => {
this.store.dispatch('mergeMe', {
hasUnreadAntenna: true
});
});
main.on('readAllAnnouncements', () => {
this.store.dispatch('mergeMe', {
hasUnreadAnnouncement: false
});
});
main.on('clientSettingUpdated', x => {
this.store.commit('settings/set', {
key: x.key,
value: x.value
});
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {
alert(locale['common']['my-token-regenerated']);
this.signout();
});
}
} }
/** /**

View File

@ -12,14 +12,12 @@
<div><b>{{ $t('administrator') }}</b><span>{{ meta.maintainerName }}</span></div> <div><b>{{ $t('administrator') }}</b><span>{{ meta.maintainerName }}</span></div>
<div><b></b><span>{{ meta.maintainerEmail }}</span></div> <div><b></b><span>{{ meta.maintainerEmail }}</span></div>
</div> </div>
<div class="_content table" v-if="stats">
<div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
<div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
</div>
<div class="_content table"> <div class="_content table">
<div><b>Misskey</b><span>v{{ version }}</span></div> <div><b>Misskey</b><span>v{{ version }}</span></div>
</div> </div>
</section> </section>
<mk-instance-stats style="margin-top: var(--margin);"/>
</div> </div>
</template> </template>
@ -28,6 +26,7 @@ import Vue from 'vue';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { version } from '../config'; import { version } from '../config';
import i18n from '../i18n'; import i18n from '../i18n';
import MkInstanceStats from '../components/instance-stats.vue';
export default Vue.extend({ export default Vue.extend({
i18n, i18n,
@ -38,10 +37,13 @@ export default Vue.extend({
}; };
}, },
components: {
MkInstanceStats
},
data() { data() {
return { return {
version, version,
stats: null,
serverInfo: null, serverInfo: null,
faInfoCircle faInfoCircle
} }
@ -52,12 +54,6 @@ export default Vue.extend({
return this.$store.state.instance.meta; return this.$store.state.instance.meta;
}, },
}, },
created() {
this.$root.api('stats').then(res => {
this.stats = res;
});
},
}); });
</script> </script>

View File

@ -8,7 +8,7 @@
<div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> <div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
<div class="_content"> <div class="_content">
<mfm :text="announcement.text"/> <mfm :text="announcement.text"/>
<img v-if="announcement.imageUrl" :src="announcement.imageUrl" alt=""/> <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
</div> </div>
<div class="_footer" v-if="$store.getters.isSignedIn && !announcement.isRead"> <div class="_footer" v-if="$store.getters.isSignedIn && !announcement.isRead">
<mk-button @click="read(announcement)" primary><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button> <mk-button @click="read(announcement)" primary><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button>

View File

@ -34,11 +34,13 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../i18n'; import i18n from '../i18n';
import XForm from './auth.form.vue'; import XForm from './auth.form.vue';
import MkSignin from '../components/signin.vue';
export default Vue.extend({ export default Vue.extend({
i18n, i18n,
components: { components: {
XForm XForm,
MkSignin,
}, },
data() { data() {
return { return {

View File

@ -1,27 +1,40 @@
<template> <template>
<mk-pagination :pagination="pagination" #default="{items}" class="mk-follow-requests" ref="list"> <div>
<div class="user _panel" v-for="(req, i) in items" :key="req.id"> <portal to="icon"><fa :icon="faUserClock"/></portal>
<mk-avatar class="avatar" :user="req.follower"/> <portal to="title">{{ $t('followRequests') }}</portal>
<div class="body">
<div class="name"> <mk-pagination :pagination="pagination" class="mk-follow-requests" ref="list">
<router-link class="name" :to="req.follower | userPage" v-user-preview="req.follower.id"><mk-user-name :user="req.follower"/></router-link> <template #empty>
<p class="acct">@{{ req.follower | acct }}</p> <div class="tkdrhpxr">
<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
<div>{{ $t('noFollowRequests') }}</div>
</div> </div>
<div class="description" v-if="req.follower.description" :title="req.follower.description"> </template>
<mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$store.state.i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/> <template #default="{items}">
<div class="user _panel" v-for="req in items" :key="req.id">
<mk-avatar class="avatar" :user="req.follower"/>
<div class="body">
<div class="name">
<router-link class="name" :to="req.follower | userPage" v-user-preview="req.follower.id"><mk-user-name :user="req.follower"/></router-link>
<p class="acct">@{{ req.follower | acct }}</p>
</div>
<div class="description" v-if="req.follower.description" :title="req.follower.description">
<mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$store.state.i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
</div>
<div class="actions">
<button class="_button" @click="accept(req.follower)"><fa :icon="faCheck"/></button>
<button class="_button" @click="reject(req.follower)"><fa :icon="faTimes"/></button>
</div>
</div>
</div> </div>
<div class="actions"> </template>
<button class="_button" @click="accept(req.follower)"><fa :icon="faCheck"/></button> </mk-pagination>
<button class="_button" @click="reject(req.follower)"><fa :icon="faTimes"/></button> </div>
</div>
</div>
</div>
</mk-pagination>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faCheck, faTimes } from '@fortawesome/free-solid-svg-icons'; import { faUserClock, faCheck, faTimes } from '@fortawesome/free-solid-svg-icons';
import MkPagination from '../components/ui/pagination.vue'; import MkPagination from '../components/ui/pagination.vue';
export default Vue.extend({ export default Vue.extend({
@ -41,7 +54,7 @@ export default Vue.extend({
endpoint: 'following/requests/list', endpoint: 'following/requests/list',
limit: 10, limit: 10,
}, },
faCheck, faTimes faCheck, faTimes, faUserClock
}; };
}, },
@ -62,6 +75,18 @@ export default Vue.extend({
<style lang="scss" scoped> <style lang="scss" scoped>
.mk-follow-requests { .mk-follow-requests {
.tkdrhpxr {
padding: 32px;
text-align: center;
> img {
vertical-align: bottom;
height: 128px;
margin-bottom: 16px;
border-radius: 16px;
}
}
> .user { > .user {
display: flex; display: flex;
padding: 16px; padding: 16px;

View File

@ -14,9 +14,12 @@
</button> </button>
</portal> </portal>
<div class="new" v-if="queue > 0" :style="{ width: width + 'px' }"><button class="_buttonPrimary" @click="top()">{{ $t('newNoteRecived') }}</button></div>
<x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/> <x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/>
<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list" :antenna="antenna" @before="before()" @after="after()"/> <x-post-form class="post-form _panel" fixed v-if="$store.state.device.showFixedPostForm"/>
<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list" :antenna="antenna" :sound="true" @before="before()" @after="after()" @queue="queueUpdated"/>
</div> </div>
</template> </template>
@ -27,6 +30,7 @@ import { faComments } from '@fortawesome/free-regular-svg-icons';
import Progress from '../scripts/loading'; import Progress from '../scripts/loading';
import XTimeline from '../components/timeline.vue'; import XTimeline from '../components/timeline.vue';
import XTutorial from './index.home.tutorial.vue'; import XTutorial from './index.home.tutorial.vue';
import XPostForm from '../components/post-form.vue';
export default Vue.extend({ export default Vue.extend({
metaInfo() { metaInfo() {
@ -38,6 +42,7 @@ export default Vue.extend({
components: { components: {
XTimeline, XTimeline,
XTutorial, XTutorial,
XPostForm,
}, },
props: { props: {
@ -53,6 +58,8 @@ export default Vue.extend({
list: null, list: null,
antenna: null, antenna: null,
menuOpened: false, menuOpened: false,
queue: 0,
width: 0,
faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faCircle faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faCircle
}; };
}, },
@ -63,6 +70,10 @@ export default Vue.extend({
't': this.focus 't': this.focus
}; };
}, },
meta() {
return this.$store.state.instance.meta;
},
}, },
watch: { watch: {
@ -91,6 +102,10 @@ export default Vue.extend({
} }
}, },
mounted() {
this.width = this.$el.offsetWidth;
},
methods: { methods: {
before() { before() {
Progress.start(); Progress.start();
@ -100,7 +115,17 @@ export default Vue.extend({
Progress.done(); Progress.done();
}, },
queueUpdated(q) {
if (this.$el.offsetWidth !== 0) this.width = this.$el.offsetWidth;
this.queue = q;
},
top() {
window.scroll({ top: 0, behavior: 'instant' });
},
async choose(ev) { async choose(ev) {
if (this.meta == null) return;
this.menuOpened = true; this.menuOpened = true;
const [antennas, lists] = await Promise.all([ const [antennas, lists] = await Promise.all([
this.$root.api('antennas/list'), this.$root.api('antennas/list'),
@ -128,15 +153,15 @@ export default Vue.extend({
text: this.$t('_timelines.home'), text: this.$t('_timelines.home'),
icon: faHome, icon: faHome,
action: () => { this.setSrc('home') } action: () => { this.setSrc('home') }
}, { }, this.meta.disableLocalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
text: this.$t('_timelines.local'), text: this.$t('_timelines.local'),
icon: faComments, icon: faComments,
action: () => { this.setSrc('local') } action: () => { this.setSrc('local') }
}, { }, this.meta.disableLocalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
text: this.$t('_timelines.social'), text: this.$t('_timelines.social'),
icon: faShareAlt, icon: faShareAlt,
action: () => { this.setSrc('social') } action: () => { this.setSrc('social') }
}, { }, this.meta.disableGlobalTimeline && !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin ? undefined : {
text: this.$t('_timelines.global'), text: this.$t('_timelines.global'),
icon: faGlobe, icon: faGlobe,
action: () => { this.setSrc('global') } action: () => { this.setSrc('global') }
@ -169,9 +194,26 @@ export default Vue.extend({
<style lang="scss" scoped> <style lang="scss" scoped>
.mk-home { .mk-home {
> .new {
position: fixed;
z-index: 1000;
> button {
display: block;
margin: 0 auto;
padding: 8px 16px;
border-radius: 32px;
}
}
> .tutorial { > .tutorial {
margin-bottom: var(--margin); margin-bottom: var(--margin);
} }
> .post-form {
position: relative;
margin-bottom: var(--margin);
}
} }
._kjvfvyph_ { ._kjvfvyph_ {
@ -184,7 +226,7 @@ export default Vue.extend({
position: absolute; position: absolute;
top: 16px; top: 16px;
right: 8px; right: 8px;
color: var(--accent); color: var(--indicator);
font-size: 12px; font-size: 12px;
animation: blink 1s infinite; animation: blink 1s infinite;
} }

View File

@ -1,165 +1,90 @@
<template> <template>
<div v-if="meta" class="mk-instance-page"> <div v-if="meta" class="xhexznfu">
<portal to="icon"><fa :icon="faServer"/></portal> <portal to="icon"><fa :icon="faServer"/></portal>
<portal to="title">{{ $t('instance') }}</portal> <portal to="title">{{ $t('instance') }}</portal>
<section class="_card info"> <mk-instance-stats style="margin-bottom: var(--margin);"/>
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
<section class="_card logs">
<div class="_title"><fa :icon="faStream"/> {{ $t('serverLogs') }}</div>
<div class="_content"> <div class="_content">
<mk-input v-model="name">{{ $t('instanceName') }}</mk-input> <div class="_inputs">
<mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea> <mk-input v-model="logDomain" :debounce="true">
<mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input> <span>{{ $t('domain') }}</span>
<mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input> </mk-input>
<mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input> <mk-select v-model="logLevel">
<mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input> <template #label>{{ $t('level') }}</template>
<mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input> <option value="all">{{ $t('levels.all') }}</option>
<option value="info">{{ $t('levels.info') }}</option>
<option value="success">{{ $t('levels.success') }}</option>
<option value="warning">{{ $t('levels.warning') }}</option>
<option value="error">{{ $t('levels.error') }}</option>
<option value="debug">{{ $t('levels.debug') }}</option>
</mk-select>
</div>
<div class="logs">
<code v-for="log in logs" :key="log.id" :class="log.level">
<details>
<summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>
</details>
</code>
</div>
</div> </div>
<div class="_footer"> <div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> <mk-button @click="deleteAllLogs()" primary><fa :icon="faTrashAlt"/> {{ $t('deleteAll') }}</mk-button>
</div>
</section>
<section class="_card chart">
<div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="cpumem"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div>
</div>
<div class="row">
<div class="cell"><div class="label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div>
<div class="cell"><div class="label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
<div class="cell"><div class="label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</section>
<section class="_card chart">
<div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="disk"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div>
<div class="cell"><div class="label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
<div class="cell"><div class="label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</section>
<section class="_card chart">
<div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="net"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div>
</div>
</div>
</div> </div>
</section> </section>
<section class="_card info"> <section class="_card info">
<div class="_content">
<mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
</div>
<div class="_content">
<mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
</div>
</section>
<section class="_card info">
<div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
<div class="_content">
<mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
<mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
<div class="_content">
<mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
<template v-if="enableRecaptcha">
<mk-info>{{ $t('recaptchaInfo') }}</mk-info>
<mk-info warn>{{ $t('recaptchaInfo2') }}</mk-info>
<mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input>
<mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input>
</template>
</div>
<div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
<header>{{ $t('preview') }}</header>
<div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
<div class="_content">
<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
<template v-if="enableServiceWorker">
<mk-info>{{ $t('vapidInfo') }}<br><code>npx web-push generate-vapid-keys</code></mk-info>
<mk-horizon-group inputs class="fit-bottom">
<mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
<mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
</mk-horizon-group>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
<div class="_content">
<mk-textarea v-model="pinnedUsers">
<template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template>
</mk-textarea>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
<div class="_content">
<mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
<mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch>
<mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
<mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
<div class="_content">
<mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
<mk-button primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
<div class="_content">
<mk-textarea v-model="blockedHosts">
<template #desc>{{ $t('blockedInstancesDescription') }}</template>
</mk-textarea>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
<div class="_content">
<header><fa :icon="faTwitter"/> Twitter</header>
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableTwitterIntegration">
<mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faGithub"/> GitHub</header>
<mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableGithubIntegration">
<mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faDiscord"/> Discord</header>
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableDiscordIntegration">
<mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card info">
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div>
<div class="_content table" v-if="stats">
<div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
<div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
</div>
<div class="_content table"> <div class="_content table">
<div><b>Misskey</b><span>v{{ version }}</span></div> <div><b>Misskey</b><span>v{{ version }}</span></div>
</div> </div>
@ -174,18 +99,23 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons'; import { faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons'; import Chart from 'chart.js';
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'; import VueJsonPretty from 'vue-json-pretty';
import MkInstanceStats from '../../components/instance-stats.vue';
import MkButton from '../../components/ui/button.vue'; import MkButton from '../../components/ui/button.vue';
import MkSelect from '../../components/ui/select.vue';
import MkInput from '../../components/ui/input.vue'; import MkInput from '../../components/ui/input.vue';
import MkTextarea from '../../components/ui/textarea.vue';
import MkSwitch from '../../components/ui/switch.vue';
import MkInfo from '../../components/ui/info.vue';
import MkUserSelect from '../../components/user-select.vue';
import { version, url } from '../../config'; import { version, url } from '../../config';
import i18n from '../../i18n'; import i18n from '../../i18n';
import getAcct from '../../../misc/acct/render';
const alpha = (hex, a) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
const r = parseInt(result[1], 16);
const g = parseInt(result[2], 16);
const b = parseInt(result[3], 16);
return `rgba(${r}, ${g}, ${b}, ${a})`;
};
export default Vue.extend({ export default Vue.extend({
i18n, i18n,
@ -197,11 +127,11 @@ export default Vue.extend({
}, },
components: { components: {
MkInstanceStats,
MkButton, MkButton,
MkSelect,
MkInput, MkInput,
MkTextarea, VueJsonPretty
MkSwitch,
MkInfo,
}, },
data() { data() {
@ -210,41 +140,14 @@ export default Vue.extend({
url, url,
stats: null, stats: null,
serverInfo: null, serverInfo: null,
proxyAccount: null, connection: null,
proxyAccountId: null, memUsage: 0,
cacheRemoteFiles: false, chartCpuMem: null,
proxyRemoteFiles: false, chartNet: null,
localDriveCapacityMb: 0, logs: [],
remoteDriveCapacityMb: 0, logLevel: 'all',
blockedHosts: '', logDomain: '',
pinnedUsers: '', faServer, faExchangeAlt, faMicrochip, faHdd, faStream, faTrashAlt
maintainerName: null,
maintainerEmail: null,
name: null,
description: null,
tosUrl: null,
bannerUrl: null,
iconUrl: null,
maxNoteTextLength: 0,
enableRegistration: false,
enableLocalTimeline: false,
enableGlobalTimeline: false,
enableRecaptcha: false,
recaptchaSiteKey: null,
recaptchaSecretKey: null,
enableServiceWorker: false,
swPublicKey: null,
swPrivateKey: null,
enableTwitterIntegration: false,
twitterConsumerKey: null,
twitterConsumerSecret: null,
enableGithubIntegration: false,
githubClientId: null,
githubClientSecret: null,
enableDiscordIntegration: false,
discordClientId: null,
discordClientSecret: null,
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
} }
}, },
@ -254,153 +157,376 @@ export default Vue.extend({
}, },
}, },
created() { watch: {
this.name = this.meta.name; logLevel() {
this.description = this.meta.description; this.logs = [];
this.tosUrl = this.meta.tosUrl; this.fetchLogs();
this.bannerUrl = this.meta.bannerUrl; },
this.iconUrl = this.meta.iconUrl; logDomain() {
this.maintainerName = this.meta.maintainerName; this.logs = [];
this.maintainerEmail = this.meta.maintainerEmail; this.fetchLogs();
this.maxNoteTextLength = this.meta.maxNoteTextLength;
this.enableRegistration = !this.meta.disableRegistration;
this.enableLocalTimeline = !this.meta.disableLocalTimeline;
this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
this.enableRecaptcha = this.meta.enableRecaptcha;
this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
this.proxyAccountId = this.meta.proxyAccountId;
this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
this.blockedHosts = this.meta.blockedHosts.join('\n');
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
this.enableServiceWorker = this.meta.enableServiceWorker;
this.swPublicKey = this.meta.swPublickey;
this.swPrivateKey = this.meta.swPrivateKey;
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
this.twitterConsumerKey = this.meta.twitterConsumerKey;
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
this.enableGithubIntegration = this.meta.enableGithubIntegration;
this.githubClientId = this.meta.githubClientId;
this.githubClientSecret = this.meta.githubClientSecret;
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
this.discordClientId = this.meta.discordClientId;
this.discordClientSecret = this.meta.discordClientSecret;
if (this.proxyAccountId) {
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
this.proxyAccount = proxyAccount;
});
} }
this.$root.api('admin/server-info').then(res => {
this.serverInfo = res;
});
this.$root.api('stats').then(res => {
this.stats = res;
});
}, },
mounted() { mounted() {
const renderRecaptchaPreview = () => { this.fetchLogs();
if (!(window as any).grecaptcha) return;
if (!this.$refs.recaptcha) return; Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
if (!this.recaptchaSiteKey) return;
(window as any).grecaptcha.render(this.$refs.recaptcha, { this.chartCpuMem = new Chart(this.$refs.cpumem, {
sitekey: this.recaptchaSiteKey type: 'line',
}); data: {
}; labels: [],
window.onRecaotchaLoad = () => { datasets: [{
renderRecaptchaPreview(); label: 'CPU',
}; pointRadius: 0,
const head = document.getElementsByTagName('head')[0]; lineTension: 0,
const script = document.createElement('script'); borderWidth: 2,
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad'); borderColor: '#86b300',
head.appendChild(script); backgroundColor: alpha('#86b300', 0.1),
this.$watch('enableRecaptcha', () => { data: []
renderRecaptchaPreview(); }, {
label: 'MEM (active)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
backgroundColor: alpha('#935dbf', 0.02),
data: []
}, {
label: 'MEM (used)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
borderDash: [5, 5],
fill: false,
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false
},
ticks: {
display: false
}
}],
yAxes: [{
position: 'right',
ticks: {
display: false,
max: 100
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
}); });
this.$watch('recaptchaSiteKey', () => {
renderRecaptchaPreview(); this.chartNet = new Chart(this.$refs.net, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'In',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Out',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false
},
ticks: {
display: false
}
}],
yAxes: [{
position: 'right',
ticks: {
display: false,
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
});
this.chartDisk = new Chart(this.$refs.disk, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Read',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Write',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false
},
ticks: {
display: false
}
}],
yAxes: [{
position: 'right',
ticks: {
display: false,
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
});
this.$root.api('admin/server-info', {}).then(res => {
this.serverInfo = res;
this.connection = this.$root.stream.useSharedConnection('serverStats');
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 150
});
}); });
}, },
beforeDestroy() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
this.connection.dispose();
},
methods: { methods: {
addPinUser() { fetchLogs() {
this.$root.new(MkUserSelect, {}).$once('selected', user => { this.$root.api('admin/logs', {
this.pinnedUsers = this.pinnedUsers.trim(); level: this.logLevel === 'all' ? null : this.logLevel,
this.pinnedUsers += '\n@' + getAcct(user); domain: this.logDomain === '' ? null : this.logDomain,
this.pinnedUsers = this.pinnedUsers.trim(); limit: 30
}).then(logs => {
this.logs = logs.reverse();
}); });
}, },
chooseProxyAccount() { deleteAllLogs() {
this.$root.new(MkUserSelect, {}).$once('selected', user => { this.$root.api('admin/delete-logs').then(() => {
this.proxyAccount = user;
this.proxyAccountId = user.id;
this.save(true);
});
},
save(withDialog = false) {
this.$root.api('admin/update-meta', {
name: this.name,
description: this.description,
tosUrl: this.tosUrl,
bannerUrl: this.bannerUrl,
iconUrl: this.iconUrl,
maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail,
maxNoteTextLength: this.maxNoteTextLength,
disableRegistration: !this.enableRegistration,
disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline,
enableRecaptcha: this.enableRecaptcha,
recaptchaSiteKey: this.recaptchaSiteKey,
recaptchaSecretKey: this.recaptchaSecretKey,
proxyAccountId: this.proxyAccountId,
cacheRemoteFiles: this.cacheRemoteFiles,
proxyRemoteFiles: this.proxyRemoteFiles,
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
blockedHosts: this.blockedHosts.split('\n') || [],
pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey,
enableTwitterIntegration: this.enableTwitterIntegration,
twitterConsumerKey: this.twitterConsumerKey,
twitterConsumerSecret: this.twitterConsumerSecret,
enableGithubIntegration: this.enableGithubIntegration,
githubClientId: this.githubClientId,
githubClientSecret: this.githubClientSecret,
enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret,
}).then(() => {
this.$store.dispatch('instance/fetch');
if (withDialog) {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}
}).catch(e => {
this.$root.dialog({ this.$root.dialog({
type: 'error', type: 'success',
text: e iconOnly: true, autoClose: true
}); });
}); });
},
onStats(stats) {
const cpu = (stats.cpu * 100).toFixed(0);
const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
this.memUsage = stats.mem.active;
this.chartCpuMem.data.labels.push('');
this.chartCpuMem.data.datasets[0].data.push(cpu);
this.chartCpuMem.data.datasets[1].data.push(memActive);
this.chartCpuMem.data.datasets[2].data.push(memUsed);
this.chartNet.data.labels.push('');
this.chartNet.data.datasets[0].data.push(stats.net.rx);
this.chartNet.data.datasets[1].data.push(stats.net.tx);
this.chartDisk.data.labels.push('');
this.chartDisk.data.datasets[0].data.push(stats.fs.r);
this.chartDisk.data.datasets[1].data.push(stats.fs.w);
if (this.chartCpuMem.data.datasets[0].data.length > 150) {
this.chartCpuMem.data.labels.shift();
this.chartCpuMem.data.datasets[0].data.shift();
this.chartCpuMem.data.datasets[1].data.shift();
this.chartCpuMem.data.datasets[2].data.shift();
this.chartNet.data.labels.shift();
this.chartNet.data.datasets[0].data.shift();
this.chartNet.data.datasets[1].data.shift();
this.chartDisk.data.labels.shift();
this.chartDisk.data.datasets[0].data.shift();
this.chartDisk.data.datasets[1].data.shift();
}
this.chartCpuMem.update();
this.chartNet.update();
this.chartDisk.update();
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
this.onStats(stats);
}
} }
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.mk-instance-page { .xhexznfu {
> .stats {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin: calc(0px - var(--margin) / 2);
margin-bottom: calc(var(--margin) / 2);
> div {
flex: 1 0 213px;
margin: calc(var(--margin) / 2);
box-sizing: border-box;
padding: 16px;
}
}
> .logs {
> ._content {
> .logs {
padding: 8px;
background: #000;
color: #fff;
font-size: 0.9em;
> code {
display: block;
&.error {
color: #f00;
}
&.warning {
color: #ff0;
}
&.success {
color: #0f0;
}
&.debug {
opacity: 0.7;
}
}
}
}
}
> .chart {
> ._content {
> .table {
> .row {
display: flex;
&:not(:last-child) {
margin-bottom: 16px;
@media (max-width: 500px) {
margin-bottom: 8px;
}
}
> .cell {
flex: 1;
> .label {
font-size: 80%;
opacity: 0.7;
> .icon {
margin-right: 4px;
display: none;
}
}
}
}
}
}
}
> .info { > .info {
> .table { > .table {
> div { > div {

View File

@ -1,381 +0,0 @@
<template>
<div class="mk-instance-monitor">
<section class="_card">
<div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="cpumem"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div>
</div>
<div class="row">
<div class="cell"><div class="label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div>
<div class="cell"><div class="label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
<div class="cell"><div class="label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="disk"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div>
<div class="cell"><div class="label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
<div class="cell"><div class="label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
</div>
</div>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div>
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
<canvas ref="net"></canvas>
</div>
<div class="_content" v-if="serverInfo">
<div class="table">
<div class="row">
<div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faTachometerAlt, faExchangeAlt, faMicrochip, faHdd } from '@fortawesome/free-solid-svg-icons';
import Chart from 'chart.js';
import i18n from '../../i18n';
const alpha = (hex, a) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
const r = parseInt(result[1], 16);
const g = parseInt(result[2], 16);
const b = parseInt(result[3], 16);
return `rgba(${r}, ${g}, ${b}, ${a})`;
};
export default Vue.extend({
i18n,
metaInfo() {
return {
title: `${this.$t('monitor')} | ${this.$t('instance')}`
};
},
components: {
},
data() {
return {
connection: null,
serverInfo: null,
memUsage: 0,
chartCpuMem: null,
chartNet: null,
faTachometerAlt, faExchangeAlt, faMicrochip, faHdd
}
},
mounted() {
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
this.chartCpuMem = new Chart(this.$refs.cpumem, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'CPU',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#86b300',
backgroundColor: alpha('#86b300', 0.1),
data: []
}, {
label: 'MEM (active)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
backgroundColor: alpha('#935dbf', 0.02),
data: []
}, {
label: 'MEM (used)',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#935dbf',
borderDash: [5, 5],
fill: false,
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false
},
ticks: {
display: false
}
}],
yAxes: [{
position: 'right',
ticks: {
display: false,
max: 100
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
});
this.chartNet = new Chart(this.$refs.net, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'In',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Out',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false
},
ticks: {
display: false
}
}],
yAxes: [{
position: 'right',
ticks: {
display: false,
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
});
this.chartDisk = new Chart(this.$refs.disk, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Read',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#94a029',
backgroundColor: alpha('#94a029', 0.1),
data: []
}, {
label: 'Write',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#ff9156',
backgroundColor: alpha('#ff9156', 0.1),
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 0,
right: 0,
top: 8,
bottom: 0
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false
},
ticks: {
display: false
}
}],
yAxes: [{
position: 'right',
ticks: {
display: false,
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
});
this.$root.api('admin/server-info', {}).then(res => {
this.serverInfo = res;
this.connection = this.$root.stream.useSharedConnection('serverStats');
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 150
});
});
},
beforeDestroy() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
this.connection.dispose();
},
methods: {
onStats(stats) {
const cpu = (stats.cpu * 100).toFixed(0);
const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
this.memUsage = stats.mem.active;
this.chartCpuMem.data.labels.push('');
this.chartCpuMem.data.datasets[0].data.push(cpu);
this.chartCpuMem.data.datasets[1].data.push(memActive);
this.chartCpuMem.data.datasets[2].data.push(memUsed);
this.chartNet.data.labels.push('');
this.chartNet.data.datasets[0].data.push(stats.net.rx);
this.chartNet.data.datasets[1].data.push(stats.net.tx);
this.chartDisk.data.labels.push('');
this.chartDisk.data.datasets[0].data.push(stats.fs.r);
this.chartDisk.data.datasets[1].data.push(stats.fs.w);
if (this.chartCpuMem.data.datasets[0].data.length > 150) {
this.chartCpuMem.data.labels.shift();
this.chartCpuMem.data.datasets[0].data.shift();
this.chartCpuMem.data.datasets[1].data.shift();
this.chartCpuMem.data.datasets[2].data.shift();
this.chartNet.data.labels.shift();
this.chartNet.data.datasets[0].data.shift();
this.chartNet.data.datasets[1].data.shift();
this.chartDisk.data.labels.shift();
this.chartDisk.data.datasets[0].data.shift();
this.chartDisk.data.datasets[1].data.shift();
}
this.chartCpuMem.update();
this.chartNet.update();
this.chartDisk.update();
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
this.onStats(stats);
}
}
}
});
</script>
<style lang="scss" scoped>
.mk-instance-monitor {
> section {
> ._content {
> .table {
> .row {
display: flex;
&:not(:last-child) {
margin-bottom: 16px;
@media (max-width: 500px) {
margin-bottom: 8px;
}
}
> .cell {
flex: 1;
> .label {
font-size: 80%;
opacity: 0.7;
> .icon {
margin-right: 4px;
display: none;
}
}
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,427 @@
<template>
<div v-if="meta">
<portal to="icon"><fa :icon="faCog"/></portal>
<portal to="title">{{ $t('settings') }}</portal>
<section class="_card info">
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
<div class="_content">
<mk-input v-model="name">{{ $t('instanceName') }}</mk-input>
<mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea>
<mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input>
<mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input>
<mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input>
<mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input>
<mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card info">
<div class="_content">
<mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
</div>
<div class="_content">
<mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
</div>
</section>
<section class="_card info">
<div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
<div class="_content">
<mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
<mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
<div class="_content">
<mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
<template v-if="enableRecaptcha">
<mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input>
<mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input>
</template>
</div>
<div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
<header>{{ $t('preview') }}</header>
<div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
<div class="_content">
<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
<template v-if="enableServiceWorker">
<div class="_inputs">
<mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
<mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
</div>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
<div class="_content">
<mk-textarea v-model="pinnedUsers">
<template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template>
</mk-textarea>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
<div class="_content">
<mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
<mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch>
<mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
<mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faCloud"/> {{ $t('objectStorage') }}</div>
<div class="_content">
<mk-switch v-model="useObjectStorage">{{ $t('useObjectStorage') }}</mk-switch>
<template v-if="useObjectStorage">
<mk-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">URL</mk-input>
<div class="_inputs">
<mk-input v-model="objectStorageBucket" :disabled="!useObjectStorage">Bucket</mk-input>
<mk-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">Prefix</mk-input>
</div>
<mk-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">Endpoint</mk-input>
<div class="_inputs">
<mk-input v-model="objectStorageRegion" :disabled="!useObjectStorage">Region</mk-input>
<mk-input v-model="objectStoragePort" type="number" :disabled="!useObjectStorage">Port</mk-input>
</div>
<div class="_inputs">
<mk-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Access key</mk-input>
<mk-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Secret key</mk-input>
</div>
<mk-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">SSL</mk-switch>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
<div class="_content">
<mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
<mk-button primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
<div class="_content">
<mk-textarea v-model="blockedHosts">
<template #desc>{{ $t('blockedInstancesDescription') }}</template>
</mk-textarea>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
<div class="_content">
<header><fa :icon="faTwitter"/> Twitter</header>
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableTwitterIntegration">
<mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faGithub"/> GitHub</header>
<mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableGithubIntegration">
<mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faDiscord"/> Discord</header>
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableDiscordIntegration">
<mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_footer">
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
import MkButton from '../../components/ui/button.vue';
import MkInput from '../../components/ui/input.vue';
import MkTextarea from '../../components/ui/textarea.vue';
import MkSwitch from '../../components/ui/switch.vue';
import MkInfo from '../../components/ui/info.vue';
import MkUserSelect from '../../components/user-select.vue';
import { url } from '../../config';
import i18n from '../../i18n';
import getAcct from '../../../misc/acct/render';
export default Vue.extend({
i18n,
metaInfo() {
return {
title: this.$t('instance') as string
};
},
components: {
MkButton,
MkInput,
MkTextarea,
MkSwitch,
MkInfo,
},
data() {
return {
url,
proxyAccount: null,
proxyAccountId: null,
cacheRemoteFiles: false,
proxyRemoteFiles: false,
localDriveCapacityMb: 0,
remoteDriveCapacityMb: 0,
blockedHosts: '',
pinnedUsers: '',
maintainerName: null,
maintainerEmail: null,
name: null,
description: null,
tosUrl: null,
bannerUrl: null,
iconUrl: null,
maxNoteTextLength: 0,
enableRegistration: false,
enableLocalTimeline: false,
enableGlobalTimeline: false,
enableRecaptcha: false,
recaptchaSiteKey: null,
recaptchaSecretKey: null,
enableServiceWorker: false,
swPublicKey: null,
swPrivateKey: null,
useObjectStorage: false,
objectStorageBaseUrl: null,
objectStorageBucket: null,
objectStoragePrefix: null,
objectStorageEndpoint: null,
objectStorageRegion: null,
objectStoragePort: null,
objectStorageAccessKey: null,
objectStorageSecretKey: null,
objectStorageUseSSL: false,
enableTwitterIntegration: false,
twitterConsumerKey: null,
twitterConsumerSecret: null,
enableGithubIntegration: false,
githubClientId: null,
githubClientSecret: null,
enableDiscordIntegration: false,
discordClientId: null,
discordClientSecret: null,
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
}
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
created() {
this.name = this.meta.name;
this.description = this.meta.description;
this.tosUrl = this.meta.tosUrl;
this.bannerUrl = this.meta.bannerUrl;
this.iconUrl = this.meta.iconUrl;
this.maintainerName = this.meta.maintainerName;
this.maintainerEmail = this.meta.maintainerEmail;
this.maxNoteTextLength = this.meta.maxNoteTextLength;
this.enableRegistration = !this.meta.disableRegistration;
this.enableLocalTimeline = !this.meta.disableLocalTimeline;
this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
this.enableRecaptcha = this.meta.enableRecaptcha;
this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
this.proxyAccountId = this.meta.proxyAccountId;
this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
this.blockedHosts = this.meta.blockedHosts.join('\n');
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
this.enableServiceWorker = this.meta.enableServiceWorker;
this.swPublicKey = this.meta.swPublickey;
this.swPrivateKey = this.meta.swPrivateKey;
this.useObjectStorage = this.meta.useObjectStorage;
this.objectStorageBaseUrl = this.meta.objectStorageBaseUrl;
this.objectStorageBucket = this.meta.objectStorageBucket;
this.objectStoragePrefix = this.meta.objectStoragePrefix;
this.objectStorageEndpoint = this.meta.objectStorageEndpoint;
this.objectStorageRegion = this.meta.objectStorageRegion;
this.objectStoragePort = this.meta.objectStoragePort;
this.objectStorageAccessKey = this.meta.objectStorageAccessKey;
this.objectStorageSecretKey = this.meta.objectStorageSecretKey;
this.objectStorageUseSSL = this.meta.objectStorageUseSSL;
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
this.twitterConsumerKey = this.meta.twitterConsumerKey;
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
this.enableGithubIntegration = this.meta.enableGithubIntegration;
this.githubClientId = this.meta.githubClientId;
this.githubClientSecret = this.meta.githubClientSecret;
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
this.discordClientId = this.meta.discordClientId;
this.discordClientSecret = this.meta.discordClientSecret;
if (this.proxyAccountId) {
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
this.proxyAccount = proxyAccount;
});
}
},
mounted() {
const renderRecaptchaPreview = () => {
if (!(window as any).grecaptcha) return;
if (!this.$refs.recaptcha) return;
if (!this.recaptchaSiteKey) return;
(window as any).grecaptcha.render(this.$refs.recaptcha, {
sitekey: this.recaptchaSiteKey
});
};
window.onRecaotchaLoad = () => {
renderRecaptchaPreview();
};
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad');
head.appendChild(script);
this.$watch('enableRecaptcha', () => {
renderRecaptchaPreview();
});
this.$watch('recaptchaSiteKey', () => {
renderRecaptchaPreview();
});
},
methods: {
addPinUser() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
this.pinnedUsers = this.pinnedUsers.trim();
this.pinnedUsers += '\n@' + getAcct(user);
this.pinnedUsers = this.pinnedUsers.trim();
});
},
chooseProxyAccount() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
this.proxyAccount = user;
this.proxyAccountId = user.id;
this.save(true);
});
},
save(withDialog = false) {
this.$root.api('admin/update-meta', {
name: this.name,
description: this.description,
tosUrl: this.tosUrl,
bannerUrl: this.bannerUrl,
iconUrl: this.iconUrl,
maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail,
maxNoteTextLength: this.maxNoteTextLength,
disableRegistration: !this.enableRegistration,
disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline,
enableRecaptcha: this.enableRecaptcha,
recaptchaSiteKey: this.recaptchaSiteKey,
recaptchaSecretKey: this.recaptchaSecretKey,
proxyAccountId: this.proxyAccountId,
cacheRemoteFiles: this.cacheRemoteFiles,
proxyRemoteFiles: this.proxyRemoteFiles,
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
blockedHosts: this.blockedHosts.split('\n') || [],
pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey,
useObjectStorage: this.useObjectStorage,
objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
objectStorageUseSSL: this.objectStorageUseSSL,
enableTwitterIntegration: this.enableTwitterIntegration,
twitterConsumerKey: this.twitterConsumerKey,
twitterConsumerSecret: this.twitterConsumerSecret,
enableGithubIntegration: this.enableGithubIntegration,
githubClientId: this.githubClientId,
githubClientSecret: this.githubClientSecret,
enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret,
}).then(() => {
this.$store.dispatch('instance/fetch');
if (withDialog) {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
}
}
});
</script>

View File

@ -310,7 +310,7 @@ export default Vue.extend({
} }
> .text { > .text {
&, * { &, ::v-deep * {
color: #fff !important; color: #fff !important;
} }
} }

View File

@ -184,12 +184,7 @@ export default Vue.extend({
}, },
onMessage(message) { onMessage(message) {
// サウンドを再生する this.$root.sound('chat');
if (this.$store.state.device.enableSounds) {
const sound = new Audio(`${url}/assets/message.mp3`);
sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
const isBottom = this.isBottom(); const isBottom = this.isBottom();

View File

@ -32,7 +32,7 @@
</router-link> </router-link>
</sequential-entrance> </sequential-entrance>
<div class="no-history" v-if="!fetching && messages.length == 0"> <div class="no-history" v-if="!fetching && messages.length == 0">
<img src="https://xn--931a.moe/assets/info.png" alt="" class="_ghost"/> <img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
<div>{{ $t('noHistory') }}</div> <div>{{ $t('noHistory') }}</div>
</div> </div>
<mk-loading v-if="fetching"/> <mk-loading v-if="fetching"/>

View File

@ -30,6 +30,10 @@
<span>{{ $t('antennaKeywords') }}</span> <span>{{ $t('antennaKeywords') }}</span>
<template #desc>{{ $t('antennaKeywordsDescription') }}</template> <template #desc>{{ $t('antennaKeywordsDescription') }}</template>
</mk-textarea> </mk-textarea>
<mk-textarea v-model="excludeKeywords">
<span>{{ $t('antennaExcludeKeywords') }}</span>
<template #desc>{{ $t('antennaKeywordsDescription') }}</template>
</mk-textarea>
<mk-switch v-model="caseSensitive">{{ $t('caseSensitive') }}</mk-switch> <mk-switch v-model="caseSensitive">{{ $t('caseSensitive') }}</mk-switch>
<mk-switch v-model="withFile">{{ $t('withFileAntenna') }}</mk-switch> <mk-switch v-model="withFile">{{ $t('withFileAntenna') }}</mk-switch>
<mk-switch v-model="notify">{{ $t('notifyAntenna') }}</mk-switch> <mk-switch v-model="notify">{{ $t('notifyAntenna') }}</mk-switch>
@ -75,6 +79,7 @@ export default Vue.extend({
userGroupId: null, userGroupId: null,
users: '', users: '',
keywords: '', keywords: '',
excludeKeywords: '',
caseSensitive: false, caseSensitive: false,
withReplies: false, withReplies: false,
withFile: false, withFile: false,
@ -107,6 +112,7 @@ export default Vue.extend({
this.userGroupId = this.antenna.userGroupId; this.userGroupId = this.antenna.userGroupId;
this.users = this.antenna.users.join('\n'); this.users = this.antenna.users.join('\n');
this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n'); this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n');
this.excludeKeywords = this.antenna.excludeKeywords.map(x => x.join(' ')).join('\n');
this.caseSensitive = this.antenna.caseSensitive; this.caseSensitive = this.antenna.caseSensitive;
this.withReplies = this.antenna.withReplies; this.withReplies = this.antenna.withReplies;
this.withFile = this.antenna.withFile; this.withFile = this.antenna.withFile;
@ -126,7 +132,8 @@ export default Vue.extend({
notify: this.notify, notify: this.notify,
caseSensitive: this.caseSensitive, caseSensitive: this.caseSensitive,
users: this.users.trim().split('\n').map(x => x.trim()), users: this.users.trim().split('\n').map(x => x.trim()),
keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')) keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')),
excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
}); });
this.$emit('created'); this.$emit('created');
} else { } else {
@ -141,7 +148,8 @@ export default Vue.extend({
notify: this.notify, notify: this.notify,
caseSensitive: this.caseSensitive, caseSensitive: this.caseSensitive,
users: this.users.trim().split('\n').map(x => x.trim()), users: this.users.trim().split('\n').map(x => x.trim()),
keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')) keywords: this.keywords.trim().split('\n').map(x => x.trim().split(' ')),
excludeKeywords: this.excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
}); });
} }

View File

@ -53,6 +53,7 @@ export default Vue.extend({
userGroupId: null, userGroupId: null,
users: [], users: [],
keywords: [], keywords: [],
excludeKeywords: [],
withReplies: false, withReplies: false,
caseSensitive: false, caseSensitive: false,
withFile: false, withFile: false,

View File

@ -0,0 +1,109 @@
<template>
<div>
<portal to="icon"><fa :icon="faCog"/></portal>
<portal to="title">{{ $t('accountSettings') }}</portal>
<x-profile-setting/>
<x-privacy-setting/>
<x-reaction-setting/>
<section class="_card">
<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
<div class="_content">
<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
</mk-switch>
<mk-switch v-model="$store.state.i.injectFeaturedNote" @change="onChangeInjectFeaturedNote">
{{ $t('showFeaturedNotesInTimeline') }}
</mk-switch>
</div>
<div class="_content">
<mk-button @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</mk-button>
<mk-button @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</mk-button>
<mk-button @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</mk-button>
</div>
</section>
<x-import-export/>
<x-drive/>
<x-mute-block/>
<x-security/>
<x-2fa/>
<x-integration/>
<x-api/>
<mk-button @click="$root.signout()" primary style="margin: var(--margin) auto;">{{ $t('logout') }}</mk-button>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faCog } from '@fortawesome/free-solid-svg-icons';
import XProfileSetting from './profile.vue';
import XPrivacySetting from './privacy.vue';
import XImportExport from './import-export.vue';
import XDrive from './drive.vue';
import XReactionSetting from './reaction.vue';
import XMuteBlock from './mute-block.vue';
import XSecurity from './security.vue';
import X2fa from './2fa.vue';
import XIntegration from './integration.vue';
import XApi from './api.vue';
import MkButton from '../../components/ui/button.vue';
import MkSwitch from '../../components/ui/switch.vue';
export default Vue.extend({
metaInfo() {
return {
title: this.$t('settings') as string
};
},
components: {
XProfileSetting,
XPrivacySetting,
XImportExport,
XDrive,
XReactionSetting,
XMuteBlock,
XSecurity,
X2fa,
XIntegration,
XApi,
MkButton,
MkSwitch,
},
data() {
return {
faCog
}
},
methods: {
onChangeAutoWatch(v) {
this.$root.api('i/update', {
autoWatch: v
});
},
onChangeInjectFeaturedNote(v) {
this.$root.api('i/update', {
injectFeaturedNote: v
});
},
readAllUnreadNotes() {
this.$root.api('i/read_all_unread_notes');
},
readAllMessagingMessages() {
this.$root.api('i/read_all_messaging_messages');
},
readAllNotifications() {
this.$root.api('notifications/mark_all_as_read');
},
}
});
</script>

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