Compare commits
	
		
			75 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 84a1ec01bc | ||
|   | 36e59c5b5f | ||
|   | 5389b16c59 | ||
|   | da3008af1c | ||
|   | 6637766554 | ||
|   | 2bc63631a4 | ||
|   | 5215721942 | ||
|   | d02e14cb94 | ||
|   | fa75b40dfd | ||
|   | f32d8b7069 | ||
|   | 90e8527556 | ||
|   | 66377d3f27 | ||
|   | c6ae93df80 | ||
|   | 55e9099091 | ||
|   | c6ace29446 | ||
|   | b0b885aacd | ||
|   | 3615b5d353 | ||
|   | 74c71e6283 | ||
|   | 9b07c5af05 | ||
|   | cda1803e59 | ||
|   | 96eab7e12b | ||
|   | 916512fd47 | ||
|   | 58d3a37908 | ||
|   | a19e252c9e | ||
|   | 63225ed0fd | ||
|   | 11cc9cbc7c | ||
|   | 36b9a0d42f | ||
|   | e7da10ae58 | ||
|   | f07047d1e8 | ||
|   | c62aff76af | ||
|   | 1c20de4e9c | ||
|   | 4903eb4a4a | ||
|   | b5981ab544 | ||
|   | 00e1dbfdfb | ||
|   | df69ca4d56 | ||
|   | cb631d4abb | ||
|   | 3c351d8300 | ||
|   | ca66acac2b | ||
|   | 9fcf94b197 | ||
|   | aa34000f0b | ||
|   | d3c0f3c251 | ||
|   | bb7edfee04 | ||
|   | caa14c70ef | ||
|   | 3a0f72867f | ||
|   | 10d72742f5 | ||
|   | 1b9f8a87d3 | ||
|   | d4a630902d | ||
|   | fef5ec874b | ||
|   | f2e347fec1 | ||
|   | cd3c2484ee | ||
|   | 6a396ef5e3 | ||
|   | eec1af1f52 | ||
|   | 99fc77b678 | ||
|   | 8bb311df51 | ||
|   | a77df249c2 | ||
|   | 2883bca257 | ||
|   | dd3af6886b | ||
|   | 33bcf2d1ea | ||
|   | 795fb0eb60 | ||
|   | 9e9d378bf1 | ||
|   | 4a6b0edce6 | ||
|   | 356225af14 | ||
|   | 331305e6c7 | ||
|   | 917b9475a5 | ||
|   | e895fc954b | ||
|   | 6d3e18a6a1 | ||
|   | 79354f4faf | ||
|   | 28f8933c3c | ||
|   | 10356b4041 | ||
|   | 6a732ab1cd | ||
|   | 47322b35ff | ||
|   | 4c6d0386b9 | ||
|   | a448172952 | ||
|   | 244ef0cb8f | ||
|   | 4bf1c23b3c | 
| @@ -142,6 +142,11 @@ id: 'aid' | |||||||
| # Proxy for HTTP/HTTPS | # Proxy for HTTP/HTTPS | ||||||
| #proxy: http://127.0.0.1:3128 | #proxy: http://127.0.0.1:3128 | ||||||
|  |  | ||||||
|  | #proxyBypassHosts: [ | ||||||
|  | #  'example.com', | ||||||
|  | #  '192.0.2.8' | ||||||
|  | #] | ||||||
|  |  | ||||||
| # Proxy for SMTP/SMTPS | # Proxy for SMTP/SMTPS | ||||||
| #proxySmtp: http://127.0.0.1:3128   # use HTTP/1.1 CONNECT | #proxySmtp: http://127.0.0.1:3128   # use HTTP/1.1 CONNECT | ||||||
| #proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 | #proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 | ||||||
|   | |||||||
							
								
								
									
										73
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										73
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,6 +1,78 @@ | |||||||
| ChangeLog | ChangeLog | ||||||
| ========= | ========= | ||||||
|  |  | ||||||
|  | 12.33.0 (2020/4/16) | ||||||
|  | ------------------- | ||||||
|  | ### ✨Improvements | ||||||
|  | * Pagesで円を書くメソッドを追加 | ||||||
|  | * AiScriptのバージョンアップ | ||||||
|  |  | ||||||
|  | ### 🐛Fixes | ||||||
|  | * PagesでAiScript変数があると編集が機能しなくなる問題を修正 | ||||||
|  |  | ||||||
|  | 12.32.0 (2020/4/16) | ||||||
|  | ------------------- | ||||||
|  | ### ✨Improvements | ||||||
|  | * Pagesで画像を描画できるように | ||||||
|  | * AiScriptのバージョンアップ | ||||||
|  | * 0以下のリアクションは送らないように | ||||||
|  |  | ||||||
|  | ### 🐛Fixes | ||||||
|  | * リアクションの修正 | ||||||
|  | * Fix Media List in CW Content | ||||||
|  |  | ||||||
|  | 12.31.0 (2020/4/14) | ||||||
|  | ------------------- | ||||||
|  | ### ✨Improvements | ||||||
|  | * プロキシの除外ホスト指定とオブジェクトストレージへの適用を除外するオプション | ||||||
|  | * AiScript | ||||||
|  | * モデレーション関連機能 | ||||||
|  | * sensitiveではないメディアも非表示にできるように | ||||||
|  | * 投稿のURLプレビューポップアップを改良 | ||||||
|  | * リモートのカスタム絵文字リアクションを表示できるように | ||||||
|  |  | ||||||
|  | ### 🐛Fixes | ||||||
|  | * リアクションカウントがおかしくなることがあるのを修正 | ||||||
|  |  | ||||||
|  | 12.30.0 (2020/4/11) | ||||||
|  | ------------------- | ||||||
|  | ### ✨Improvements | ||||||
|  | * リクエストライブラリをrequestからnode-fetchに変更 | ||||||
|  | * オブジェクトストレージのhttpスキーマリクエストでもProxyが適用されるように | ||||||
|  | * DNSキャッシュとKeep-Alive適用箇所を増やす | ||||||
|  | * ドイツ語と中国語(繁体)を有効に | ||||||
|  | * NSFWを再度隠せるように | ||||||
|  | * Implement AiScript scratchpad (/scratchpad) | ||||||
|  |  | ||||||
|  | ### 🐛Fixes | ||||||
|  | * APのurl処理の修正 | ||||||
|  |  | ||||||
|  | 12.29.0 (2020/4/5) | ||||||
|  | ------------------- | ||||||
|  | ### ✨Improvements | ||||||
|  | * トークン系の乱数ソースではcryptoを使うように | ||||||
|  | * broadcast stream が追加され emojiAdded イベントをサポート | ||||||
|  | * APIリファレンスの高速化等 | ||||||
|  | * Ability to set header image for a Page | ||||||
|  | * ログの改善 | ||||||
|  |  | ||||||
|  | ### 🐛Fixes | ||||||
|  | * アプリ一覧に1回も使用していないアプリが表示されないのを修正 | ||||||
|  | * admin/accounts/createで一般ユーザーがアカウントを作成し放題なのを修正 | ||||||
|  | * 翻訳の未適用箇所を修正 | ||||||
|  | * APIの権限設定漏れを修正 | ||||||
|  | * インストール直後にアクティビティが飛んで来たりするともう初期管理者セットアップがができなくなるのを修正 | ||||||
|  | * リモート投稿でurlがあればそちらをリンクするように修正 | ||||||
|  |  | ||||||
|  | 12.28.0 (2020/3/29) | ||||||
|  | ------------------- | ||||||
|  | ### ✨Improvements | ||||||
|  | * インストールされたアプリのページでアプリの権限を確認できるように | ||||||
|  | * API: api/meta.features.miauthを追加   | ||||||
|  |   MiAuthに対応しているかどうかを確認するために利用できます。   | ||||||
|  |   値はつねにtrueを取ります。 | ||||||
|  | * インスタンス一覧でソートできるように  | ||||||
|  |  | ||||||
| 12.27.1 (2020/03/28) | 12.27.1 (2020/03/28) | ||||||
| ------------------- | ------------------- | ||||||
|  |  | ||||||
| @@ -14,6 +86,7 @@ ChangeLog | |||||||
| * サードパーティーアプリケーションの認証方法にMiAuthを追加 ([Misskey API ドキュメント](https://github.com/syuilo/misskey/blob/b8088dc01a0c53b264c0697082ff5b16b06c4cda/src/docs/api.ja-JP.md#%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A8%E3%81%97%E3%81%A6%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B))   | * サードパーティーアプリケーションの認証方法にMiAuthを追加 ([Misskey API ドキュメント](https://github.com/syuilo/misskey/blob/b8088dc01a0c53b264c0697082ff5b16b06c4cda/src/docs/api.ja-JP.md#%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A8%E3%81%97%E3%81%A6%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B))   | ||||||
|   従来の、API `app/create` => `auth/session/generate` => `auth/session/userkey` を使用する方法は依然として使用可能です。   |   従来の、API `app/create` => `auth/session/generate` => `auth/session/userkey` を使用する方法は依然として使用可能です。   | ||||||
| UIからアプリを作成する画面 (`/dev/apps`) は廃止されました、同等の操作を行いたい場合は API `app/create` で可能です。   | UIからアプリを作成する画面 (`/dev/apps`) は廃止されました、同等の操作を行いたい場合は API `app/create` で可能です。   | ||||||
|  |   MiAuthに対応しているかどうかは`api/meta.features.miauth`で確認できます(12.28.0~)。 | ||||||
| * テーマをインポートする前にプレビューできるように | * テーマをインポートする前にプレビューできるように | ||||||
| * アプリから通知を作成できるように | * アプリから通知を作成できるように | ||||||
| * インストールしたアプリを見たり削除したりできるように | * インストールしたアプリを見たり削除したりできるように | ||||||
|   | |||||||
							
								
								
									
										108
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								README.md
									
									
									
									
									
								
							| @@ -109,100 +109,102 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). | |||||||
| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ||||||
| <!-- PATREON_START --> | <!-- PATREON_START --> | ||||||
| <table><tr> | <table><tr> | ||||||
| <td><img src="https://c8.patreon.com/2/200/20832595" alt="Roujo" width="100"></td> | <td><img src="https://c8.patreon.com/2/200/20832595" alt="Roujo " width="100"></td> | ||||||
| <td><img src="https://c8.patreon.com/2/200/27956229" alt="Oliver Maximilian Seidel" width="100"></td> | <td><img src="https://c8.patreon.com/2/200/27956229" alt="Oliver Maximilian Seidel" width="100"></td> | ||||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weepjp" width="100"></td> | <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weepjp " width="100"></td> | ||||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/605366/c9dc408fdcbf412fb183ca5b06235f8d/1.jpeg?token-time=2145916800&token-hash=oaqsjLqOFjWN5I9hm2epOaTXaEtKwQUy5OW-EpAz6-g%3D" alt="Jon Leibowitz" width="100"></td> | <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/605366/c9dc408fdcbf412fb183ca5b06235f8d/1.jpeg?token-time=2145916800&token-hash=oaqsjLqOFjWN5I9hm2epOaTXaEtKwQUy5OW-EpAz6-g%3D" alt="Jon Leibowitz" 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/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> | ||||||
| </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> | ||||||
| <td><a href="https://www.patreon.com/weepjp">weepjp</a></td> | <td><a href="https://www.patreon.com/weepjp">weepjp </a></td> | ||||||
| <td><a href="https://www.patreon.com/jonleibowitz">Jon Leibowitz</a></td> | <td><a href="https://www.patreon.com/jonleibowitz">Jon Leibowitz</a></td> | ||||||
| <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> | ||||||
| </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://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> | ||||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/8249688/4aacf36b6b244ab1bc6653591b6640df/2.png?token-time=2145916800&token-hash=1ZEf2w6L34253cZXS_HlVevLEENWS9QqrnxGUAYblPo%3D" alt="AureoleArk" width="100"></td> | <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/8249688/4aacf36b6b244ab1bc6653591b6640df/2.png?token-time=2145916800&token-hash=1ZEf2w6L34253cZXS_HlVevLEENWS9QqrnxGUAYblPo%3D" alt="AureoleArk " width="100"></td> | ||||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1.jpg?token-time=2145916800&token-hash=mPLM9CA-riFHx-myr3bLZJuH2xBRHA9se5VbHhLIOuA%3D" alt="osapon" width="100"></td> | <td><img src="https://c8.patreon.com/2/200/21285325" alt="Nie(sha) " width="100"></td> | ||||||
| <td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td> | <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1.jpg?token-time=2145916800&token-hash=mPLM9CA-riFHx-myr3bLZJuH2xBRHA9se5VbHhLIOuA%3D" alt="osapon " 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://c8.patreon.com/2/200/16869916" alt="見当かなみ " 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> | ||||||
| </tr><tr> | </tr><tr> | ||||||
| <td><a href="https://www.patreon.com/user?u=776209">Denshi</a></td> | <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> | ||||||
| <td><a href="https://www.patreon.com/AureoleArk">AureoleArk</a></td> | <td><a href="https://www.patreon.com/AureoleArk">AureoleArk </a></td> | ||||||
| <td><a href="https://www.patreon.com/osapon">osapon</a></td> | <td><a href="https://www.patreon.com/user?u=21285325">Nie(sha) </a></td> | ||||||
| <td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td> | <td><a href="https://www.patreon.com/osapon">osapon </a></td> | ||||||
| <td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61</a></td> | <td><a href="https://www.patreon.com/user?u=16869916">見当かなみ </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> | ||||||
| </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://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/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> |  | ||||||
| </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=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=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> |  | ||||||
| </tr></table> | </tr></table> | ||||||
| <table><tr> | <table><tr> | ||||||
|  | <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> | <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/28295158/cd2451bfb94a449dbf705ef4718cd355/2.jpeg?token-time=2145916800&token-hash=MRv3BxufHPuCyiBSxU5UYmLGvD6YZlhtSFRfMWg2k4U%3D" alt="012" width="100"></td> | <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28295158/cd2451bfb94a449dbf705ef4718cd355/2.jpeg?token-time=2145916800&token-hash=MRv3BxufHPuCyiBSxU5UYmLGvD6YZlhtSFRfMWg2k4U%3D" alt="012 " width="100"></td> | ||||||
| <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9109588/e3cffc48d20a4e43afe04123e696781d/3.png?token-time=2145916800&token-hash=T_VIUA0IFIbleZv4pIjiszZGnQonwn34sLCYFIhakBo%3D" alt="nafuchoco" width="100"></td> | <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9109588/e3cffc48d20a4e43afe04123e696781d/3.png?token-time=2145916800&token-hash=T_VIUA0IFIbleZv4pIjiszZGnQonwn34sLCYFIhakBo%3D" alt="nafuchoco " 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> | ||||||
| </tr><tr> | </tr><tr> | ||||||
|  | <td><a href="https://www.patreon.com/takimura">takimura </a></td> | ||||||
| <td><a href="https://www.patreon.com/aqz">aqz tamaina</a></td> | <td><a href="https://www.patreon.com/aqz">aqz tamaina</a></td> | ||||||
| <td><a href="https://www.patreon.com/user?u=28295158">012</a></td> | <td><a href="https://www.patreon.com/user?u=28295158">012 </a></td> | ||||||
| <td><a href="https://www.patreon.com/nijimiss">nafuchoco</a></td> | <td><a href="https://www.patreon.com/nijimiss">nafuchoco </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> | ||||||
| </tr></table> | </tr></table> | ||||||
| <table><tr> | <table><tr> | ||||||
| <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/9481273/7fa89168e72943859c3d3c96e424ed31/4.jpeg?token-time=2145916800&token-hash=5w1QV1qXe-NdWbdFmp1H7O_-QBsSiV0haumk3XTHIEg%3D" alt="Efertone" width="100"></td> | <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9481273/7fa89168e72943859c3d3c96e424ed31/4.jpeg?token-time=2145916800&token-hash=5w1QV1qXe-NdWbdFmp1H7O_-QBsSiV0haumk3XTHIEg%3D" alt="Efertone " 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> | <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/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/efertone">Efertone</a></td> | <td><a href="https://www.patreon.com/efertone">Efertone </a></td> | ||||||
| <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> | <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> | ||||||
| </tr></table> | </tr></table> | ||||||
|  |  | ||||||
| **Last updated:** Tue, 17 Mar 2020 18:57:08 UTC | **Last updated:** Fri, 03 Apr 2020 11:52:08 UTC | ||||||
| <!-- PATREON_END --> | <!-- PATREON_END --> | ||||||
|  |  | ||||||
| [backer-url]: #backers | [backer-url]: #backers | ||||||
|   | |||||||
| @@ -1,11 +1,12 @@ | |||||||
| --- | --- | ||||||
| _lang_: "Deutsch" | _lang_: "Deutsch" | ||||||
|  | introMisskey: "Willkommen! Misskey ist eine dezentralisierte Open-Source Microblogging-Platform.\nVerfasse \"Notizen\" um mitzuteilen, was gerade passiert oder um Ereignisse mit Anderen zu teilen. 📡\nMit \"Reaktionen\" kannst du außerdem schnell deine Gefühle über Notizen anderer Benutzer zum Ausdruck bringen. 👍\nLass uns eine neue Welt erforschen! 🚀" | ||||||
| monthAndDay: "{day}/{month}" | monthAndDay: "{day}/{month}" | ||||||
| search: "Suchen" | search: "Suchen" | ||||||
| notifications: "Benachrichtigungen" | notifications: "Benachrichtigungen" | ||||||
| username: "Benutzername" | username: "Benutzername" | ||||||
| password: "Passwort" | password: "Passwort" | ||||||
| fetchingAsApObject: "Aus Fediverse holen" | fetchingAsApObject: "Wird aus dem Fediverse angefragt..." | ||||||
| ok: "OK" | ok: "OK" | ||||||
| gotIt: "Verstanden!" | gotIt: "Verstanden!" | ||||||
| cancel: "Abbrechen" | cancel: "Abbrechen" | ||||||
| @@ -16,17 +17,17 @@ noNotifications: "Keine Benachrichtigungen" | |||||||
| instance: "Instanz" | instance: "Instanz" | ||||||
| settings: "Einstellungen" | settings: "Einstellungen" | ||||||
| profile: "Profil" | profile: "Profil" | ||||||
| timeline: "Zeitleiste" | timeline: "Chronik" | ||||||
| noAccountDescription: "Keine Selbsteinführung" | noAccountDescription: "Dieser Nutzer hat seine Profilbeschreibung noch nicht ausgefüllt." | ||||||
| login: "Einloggen" | login: "Einloggen" | ||||||
| loggingIn: "Einloggen in bearbeitung" | loggingIn: "Du wirst eingeloggt..." | ||||||
| logout: "Ausloggen" | logout: "Ausloggen" | ||||||
| signup: "Registrieren" | signup: "Registrieren" | ||||||
| uploading: "Upload läuft" | uploading: "Upload läuft" | ||||||
| save: "Speichern" | save: "Speichern" | ||||||
| users: "Benutzer" | users: "Benutzer" | ||||||
| addUser: "Benutzer hinzufügen" | addUser: "Benutzer hinzufügen" | ||||||
| favorite: "Favoriten" | favorite: "Favorit" | ||||||
| favorites: "Favoriten" | favorites: "Favoriten" | ||||||
| unfavorite: "Aus Favoriten entfernen" | unfavorite: "Aus Favoriten entfernen" | ||||||
| pin: "Anheften" | pin: "Anheften" | ||||||
| @@ -34,14 +35,16 @@ unpin: "Lösen" | |||||||
| copyContent: "Inhalt kopieren" | copyContent: "Inhalt kopieren" | ||||||
| copyLink: "Link kopieren" | copyLink: "Link kopieren" | ||||||
| delete: "Löschen" | delete: "Löschen" | ||||||
|  | deleteAndEdit: "Löschen und Bearbeiten" | ||||||
|  | deleteAndEditConfirm: "Möchtest du diese Notiz wirklich löschen und bearbeiten? Alle Reaktionen, Renotes und Antworten dieser Notiz werden verloren gehen." | ||||||
| addToList: "Zur Liste hinzufügen" | addToList: "Zur Liste hinzufügen" | ||||||
| sendMessage: "Nachricht senden" | sendMessage: "Nachricht senden" | ||||||
| copyUsername: "Benutzernamen kopieren" | copyUsername: "Benutzernamen kopieren" | ||||||
| reply: "Antworten" | reply: "Antworten" | ||||||
| loadMore: "Zeige mehr" | loadMore: "Mehr anzeigen" | ||||||
| youGotNewFollower: "Sie haben einen neuen Follower" | youGotNewFollower: "Du hast einen neuen Follower" | ||||||
| receiveFollowRequest: "Follow Request erhalten." | receiveFollowRequest: "Follow-Anfrage erhalten." | ||||||
| followRequestAccepted: "FollowRequestAkzeptiert" | followRequestAccepted: "Follow-Anfrage akzeptiert" | ||||||
| mentions: "Erwähnungen" | mentions: "Erwähnungen" | ||||||
| directNotes: "Direktnachrichten" | directNotes: "Direktnachrichten" | ||||||
| importAndExport: "Importieren und Exportieren" | importAndExport: "Importieren und Exportieren" | ||||||
| @@ -51,37 +54,39 @@ files: "Dateien" | |||||||
| download: "Download" | download: "Download" | ||||||
| driveFileDeleteConfirm: "Möchtest du die Datei \"{name}\" löschen? Die zugehörige Notiz wird ebenso verschwinden." | driveFileDeleteConfirm: "Möchtest du die Datei \"{name}\" löschen? Die zugehörige Notiz wird ebenso verschwinden." | ||||||
| unfollowConfirm: "Möchtest du {name} nicht mehr folgen?" | unfollowConfirm: "Möchtest du {name} nicht mehr folgen?" | ||||||
|  | exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt." | ||||||
|  | importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen." | ||||||
| lists: "Listen" | lists: "Listen" | ||||||
| noLists: "Keine Liste!" | noLists: "Du hast keine Listen" | ||||||
| note: "Notiz" | note: "Notiz" | ||||||
| notes: "Notizen" | notes: "Notizen" | ||||||
| following: "Folgen" | following: "Folgen" | ||||||
| followers: "Folgende" | followers: "Folgende" | ||||||
| followsYou: "Folgt dir" | followsYou: "Folgt dir" | ||||||
| createList: "Liste erstellen" | createList: "Liste erstellen" | ||||||
| manageLists: "Liste verwalten" | manageLists: "Listen verwalten" | ||||||
| error: "Ein Problem ist aufgetreten" | error: "Ein Problem ist aufgetreten" | ||||||
| retry: "Wiederholen" | retry: "Wiederholen" | ||||||
| enterListName: "Listennamen eingeben" | enterListName: "Listennamen eingeben" | ||||||
| privacy: "Privatsphäre" | privacy: "Privatsphäre" | ||||||
| makeFollowManuallyApprove: "Folgeanfragen benötigen Bestätigung" | makeFollowManuallyApprove: "Follow-Anfragen benötigen Bestätigung" | ||||||
| defaultNoteVisibility: "Die Standardsichtbarkeit" | defaultNoteVisibility: "Standardsichtbarkeit" | ||||||
| follow: "Folgen" | follow: "Folgen" | ||||||
| followRequest: "Follower-Anfragen" | followRequest: "Follow-Anfrage" | ||||||
| followRequests: "Follower-Anfragen" | followRequests: "Follower-Anfragen" | ||||||
| unfollow: "Nicht mehr folgen" | unfollow: "Nicht mehr folgen" | ||||||
| followRequestPending: "Ausstehend" | followRequestPending: "Ausstehende Follow-Anfrage" | ||||||
| enterEmoji: "Gib ein Emoji ein" | enterEmoji: "Gib ein Emoji ein" | ||||||
| renote: "Renote" | renote: "Renote" | ||||||
| unrenote: "Renote zurücknehmen" | unrenote: "Renote zurücknehmen" | ||||||
| quote: "Zitieren" | quote: "Zitieren" | ||||||
| pinnedNote: "Angepinnte Notiz" | pinnedNote: "Angepinnte Notiz" | ||||||
| you: "Du" | you: "Du" | ||||||
| clickToShow: "Klicke zum den Inhalt anzusehen" | clickToShow: "Klicke um diesen Inhalt anzusehen" | ||||||
| sensitive: "Dieser Inhalt ist NSFW" | sensitive: "Dieser Inhalt ist NSFW" | ||||||
| add: "Hinzufügen" | add: "Hinzufügen" | ||||||
| reaction: "Reaktionen" | reaction: "Reaktionen" | ||||||
| reactionSettingDescription: "Weisen Sie Ihre lieblings reaktionen zu, die Sie in den Reaktionenswähler stecken möchten." | reactionSettingDescription: "Gib deine Lieblingsreaktionen ein, um sie der Reaktionsauswahl hinzuzufügen." | ||||||
| rememberNoteVisibility: "Notizsichtbarkeit merken" | rememberNoteVisibility: "Notizsichtbarkeit merken" | ||||||
| attachCancel: "Anhängen abbrechen" | attachCancel: "Anhängen abbrechen" | ||||||
| markAsSensitive: "Als sensitiv markieren" | markAsSensitive: "Als sensitiv markieren" | ||||||
| @@ -93,40 +98,54 @@ block: "Blockieren" | |||||||
| unblock: "Blockierung aufheben" | unblock: "Blockierung aufheben" | ||||||
| suspend: "Sperren" | suspend: "Sperren" | ||||||
| unsuspend: "Sperrung aufheben" | unsuspend: "Sperrung aufheben" | ||||||
| blockConfirm: "Möchtest du diesen Account wirklich blockieren?" | blockConfirm: "Möchtest du diesen Benutzer wirklich blockieren?" | ||||||
| unblockConfirm: "Möchtest du diese Blockierung wirklich aufheben?" | unblockConfirm: "Möchtest du diese Blockierung wirklich aufheben?" | ||||||
| suspendConfirm: "Möchtest du diesen Account wirklich sperren?" | suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?" | ||||||
| unsuspendConfirm: "Möchtest du die Sperrung dieses Accounts wirklich aufheben?" | unsuspendConfirm: "Möchtest du die Sperrung dieses Benutzers wirklich aufheben?" | ||||||
| selectList: "Wähle eine Liste aus" | selectList: "Wähle eine Liste aus" | ||||||
| customEmojis: "Benutzerdefinierte Emojis" | customEmojis: "Benutzerdefinierte Emojis" | ||||||
| emojiName: "Emojiname" | emojiName: "Emojiname" | ||||||
| emojiUrl: "Emoji-URL" | emojiUrl: "Emoji-URL" | ||||||
| addEmoji: "Emoji hinzufügen" | addEmoji: "Emoji hinzufügen" | ||||||
|  | cacheRemoteFiles: "Dateien von anderen Instanzen im Cache speichern" | ||||||
|  | cacheRemoteFilesDescription: "Wenn diese Einstellung deaktiviert ist, werden Dateien anderer Instanzen direkt von dort geladen. Hierdurch wird Speicherplatz gespart, aber mehr Bandbreite verbraucht, da keine Vorschaubilder generiert werden." | ||||||
| flagAsBot: "Als Bot markieren" | flagAsBot: "Als Bot markieren" | ||||||
| flagAsCat: "Als Katze markieren" | flagAsCat: "Als Katze markieren" | ||||||
| autoAcceptFollowed: "Folgeanfragen automatisch akzeptieren" | autoAcceptFollowed: "Follow-Anfragen automatisch akzeptieren" | ||||||
| addAcount: "Benutzerkonto hinzufügen" | addAcount: "Benutzerkonto hinzufügen" | ||||||
| loginFailed: "Login fehlgeschlagen" | loginFailed: "Login fehlgeschlagen" | ||||||
|  | showOnRemote: "Auf Ursprungsinstanz ansehen" | ||||||
| general: "Allgemein" | general: "Allgemein" | ||||||
| wallpaper: "Hintergrund" | wallpaper: "Hintergrund" | ||||||
|  | setWallpaper: "Hintergrund festlegen" | ||||||
| removeWallpaper: "Hintergrund entfernen" | removeWallpaper: "Hintergrund entfernen" | ||||||
| searchWith: "Suche: {q}" | searchWith: "Suche: {q}" | ||||||
| youHaveNoLists: "Du hast keine Listen" | youHaveNoLists: "Du hast keine Listen" | ||||||
| followConfirm: "Möchtest du {name} wirklich folgen?" | followConfirm: "Möchtest du {name} wirklich folgen?" | ||||||
|  | proxyAccount: "Proxy-Benutzerkonto" | ||||||
|  | proxyAccountDescription: "Ein Proxy-Benutzerkonto ist ein Benutzerkonto, das sich für Nutzer unter bestimmten Konditionen wie ein Follower aus einer fremden Instanz verhält. Zum Beispiel wird die Aktivität eines Nutzers aus einer fremden Instanz nicht an diese Instanz übermittelt, falls es keinen Benutzer dieser Instanz gibt, der diesem Nutzer aus fremder Instanz folgt. In diesem Fall folgt stattdessen das Proxy-Benutzerkonto." | ||||||
|  | host: "Host" | ||||||
| selectUser: "Benutzer wählen" | selectUser: "Benutzer wählen" | ||||||
| recipient: "Empfänger" | recipient: "Empfänger" | ||||||
| annotation: "Anmerkung" | annotation: "Anmerkung" | ||||||
| federation: "Föderation" | federation: "Föderation" | ||||||
| instances: "Instanz" | instances: "Instanz" | ||||||
|  | registeredAt: "Registriert am" | ||||||
|  | latestRequestSentAt: "Letzte Anfrage gesendet am" | ||||||
|  | latestRequestReceivedAt: "Letzte Anfrage erhalten am" | ||||||
| latestStatus: "Neuester Status" | latestStatus: "Neuester Status" | ||||||
| storageUsage: "Speicherplatzverbrauch" | storageUsage: "Speicherplatzverbrauch" | ||||||
| charts: "Charts" | charts: "Diagramme" | ||||||
| perHour: "Pro Stunde" | perHour: "Pro Stunde" | ||||||
| perDay: "Pro Tag" | perDay: "Pro Tag" | ||||||
|  | stopActivityDelivery: "Senden von Aktivitäten einstellen" | ||||||
| blockThisInstance: "Diese Instanz blockieren" | blockThisInstance: "Diese Instanz blockieren" | ||||||
|  | operations: "Aktionen" | ||||||
|  | software: "Software" | ||||||
| version: "Version" | version: "Version" | ||||||
| metadata: "Metadaten" | metadata: "Metadaten" | ||||||
| withNFiles: "{n} Datei(en)" | withNFiles: "{n} Datei(en)" | ||||||
|  | monitor: "Beobachten" | ||||||
| jobQueue: "Job-Warteschlange" | jobQueue: "Job-Warteschlange" | ||||||
| cpuAndMemory: "CPU und Arbeitsspeicher" | cpuAndMemory: "CPU und Arbeitsspeicher" | ||||||
| network: "Netzwerk" | network: "Netzwerk" | ||||||
| @@ -137,6 +156,7 @@ clearQueue: "Warteschlange leeren" | |||||||
| clearQueueConfirmTitle: "Möchtest du die Warteschlange wirklich leeren?" | clearQueueConfirmTitle: "Möchtest du die Warteschlange wirklich leeren?" | ||||||
| clearQueueConfirmText: "Jegliche Notizen, die sich noch in der Warteschlange befinden, werden hierdurch nicht föderiert. Diese Aktion wird normalerweise NICHT benötigt." | clearQueueConfirmText: "Jegliche Notizen, die sich noch in der Warteschlange befinden, werden hierdurch nicht föderiert. Diese Aktion wird normalerweise NICHT benötigt." | ||||||
| clearCachedFiles: "Cache leeren" | clearCachedFiles: "Cache leeren" | ||||||
|  | clearCachedFilesConfirm: "Sollen alle im Cache gespeicherten Dateien von anderen Instanzen wirklich gelöscht werden?" | ||||||
| blockedInstances: "Blockierte Instanzen" | blockedInstances: "Blockierte Instanzen" | ||||||
| blockedInstancesDescription: "Gib den Hostnamen der Instanz an, die blockiert werden soll. Blockierte Instanzen können nicht mehr mit dieser kommunizieren." | blockedInstancesDescription: "Gib den Hostnamen der Instanz an, die blockiert werden soll. Blockierte Instanzen können nicht mehr mit dieser kommunizieren." | ||||||
| muteAndBlock: "Stummgeschaltet / Blockiert" | muteAndBlock: "Stummgeschaltet / Blockiert" | ||||||
| @@ -152,11 +172,18 @@ processing: "In Bearbeitung" | |||||||
| preview: "Vorschau" | preview: "Vorschau" | ||||||
| default: "Standard" | default: "Standard" | ||||||
| noCustomEmojis: "Es existieren keine Emojis" | noCustomEmojis: "Es existieren keine Emojis" | ||||||
|  | customEmojisOfRemote: "Emojis von anderen Instanzen" | ||||||
| noJobs: "Es gibt keine Jobs" | noJobs: "Es gibt keine Jobs" | ||||||
| federating: "Föderiert" | federating: "Föderiert" | ||||||
| blocked: "Blockiert" | blocked: "Blockiert" | ||||||
| suspended: "Gesperrt" | suspended: "Gesperrt" | ||||||
| all: "Alles" | all: "Alles" | ||||||
|  | subscribing: "Abonnieren" | ||||||
|  | publishing: "Veröffentlichen" | ||||||
|  | notResponding: "Antwortet nicht" | ||||||
|  | instanceFollowing: "Gefolgt auf der Instanz" | ||||||
|  | instanceFollowers: "Follower der Instanz" | ||||||
|  | instanceUsers: "Benutzer dieser Instanz" | ||||||
| changePassword: "Passwort ändern" | changePassword: "Passwort ändern" | ||||||
| security: "Sicherheit" | security: "Sicherheit" | ||||||
| retypedNotMatch: "Eingaben stimmen nicht überein." | retypedNotMatch: "Eingaben stimmen nicht überein." | ||||||
| @@ -168,44 +195,63 @@ more: "Mehr!" | |||||||
| featured: "Hervorgehoben" | featured: "Hervorgehoben" | ||||||
| usernameOrUserId: "Benutzername oder Benutzer-ID" | usernameOrUserId: "Benutzername oder Benutzer-ID" | ||||||
| noSuchUser: "Benutzer nicht gefunden" | noSuchUser: "Benutzer nicht gefunden" | ||||||
|  | lookup: "Abfragen" | ||||||
| announcements: "Ankündigungen" | announcements: "Ankündigungen" | ||||||
| imageUrl: "Bild-URL" | imageUrl: "Bild-URL" | ||||||
| remove: "Löschen" | remove: "Löschen" | ||||||
| removed: "Erfolgreich gelöscht" | removed: "Erfolgreich gelöscht" | ||||||
| removeAreYouSure: "Möchtest du \"{x}\" wirklich löschen?" | removeAreYouSure: "Möchtest du \"{x}\" wirklich löschen?" | ||||||
| saved: "Gespeichert" | saved: "Gespeichert" | ||||||
| messaging: "Nachrichten" | messaging: "Privatnachrichten" | ||||||
| upload: "Hochladen" | upload: "Hochladen" | ||||||
| fromDrive: "Aus Drive" | fromDrive: "Aus Drive" | ||||||
| fromUrl: "Von einer URL" | fromUrl: "Von einer URL" | ||||||
|  | uploadFromUrl: "Von einer URL hochladen" | ||||||
|  | uploadFromUrlDescription: "URL der hochzuladenden Datei" | ||||||
|  | uploadFromUrlRequested: "Upload angefordert" | ||||||
|  | uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis der Upload abgeschlossen ist." | ||||||
| explore: "Erkunden" | explore: "Erkunden" | ||||||
| games: "Misskey Spiele" | games: "Misskey Spiele" | ||||||
| messageRead: "Gelesen" | messageRead: "Gelesen" | ||||||
| noMoreHistory: "Kein weiterer Verlauf vorhanden" | noMoreHistory: "Kein weiterer Verlauf vorhanden" | ||||||
|  | startMessaging: "Neue Privatnachricht erstellen" | ||||||
| nUsersRead: "Von {n} gelesen" | nUsersRead: "Von {n} gelesen" | ||||||
| agreeTo: "Ich stimme {0} zu" | agreeTo: "Ich stimme {0} zu" | ||||||
| tos: "Nutzungsbedingungen" | tos: "Nutzungsbedingungen" | ||||||
| start: "Anfangen" | start: "Anfangen" | ||||||
| home: "Startseite" | home: "Startseite" | ||||||
|  | remoteUserCaution: "Diese Informationen sind möglicherweise veraltet, da der Benutzer von einer anderen Instanz stammt." | ||||||
| activity: "Aktivität" | activity: "Aktivität" | ||||||
| images: "Bilder" | images: "Bilder" | ||||||
| birthday: "Geburtstag" | birthday: "Geburtstag" | ||||||
| yearsOld: "{age} Jahre alt" | yearsOld: "{age} Jahre alt" | ||||||
| registeredDate: "Registierdatum" | registeredDate: "Registrationsdatum" | ||||||
| location: "Ort" | location: "Ort" | ||||||
| theme: "Farbthemen" | theme: "Farbthemen" | ||||||
|  | themeForLightMode: "Farbthema, das im Hellmodus genutzt wird" | ||||||
|  | themeForDarkMode: "Farbthema, das im Dunkelmodus genutzt wird" | ||||||
|  | light: "Hell" | ||||||
|  | dark: "Dunkel" | ||||||
| lightThemes: "Helle Farbthemen" | lightThemes: "Helle Farbthemen" | ||||||
| darkThemes: "Dunkle Farbthemen" | darkThemes: "Dunkle Farbthemen" | ||||||
|  | syncDeviceDarkMode: "Dunkelmodus mit den Einstellungen deines Gerätes synchronisieren" | ||||||
| drive: "Drive" | drive: "Drive" | ||||||
|  | fileName: "Dateiname" | ||||||
| selectFile: "Datei auswählen" | selectFile: "Datei auswählen" | ||||||
| selectFiles: "Dateien auswählen" | selectFiles: "Dateien auswählen" | ||||||
| renameFile: "Datei umbenennen" | renameFile: "Datei umbenennen" | ||||||
|  | folderName: "Ordnername" | ||||||
| createFolder: "Ordner erstellen" | createFolder: "Ordner erstellen" | ||||||
| renameFolder: "Ordner umbenennen" | renameFolder: "Ordner umbenennen" | ||||||
| deleteFolder: "Ordner löschen" | deleteFolder: "Ordner löschen" | ||||||
| addFile: "Datei hinzufügen" | addFile: "Datei hinzufügen" | ||||||
| emptyDrive: "Drive ist leer" | emptyDrive: "Drive ist leer" | ||||||
| emptyFolder: "Der Ordner ist leer" | emptyFolder: "Der Ordner ist leer" | ||||||
|  | unableToDelete: "Nicht löschbar" | ||||||
|  | inputNewFileName: "Gib einen neuen Dateinamen ein" | ||||||
|  | inputNewFolderName: "Gib einen neuen Ordnernamen ein" | ||||||
|  | circularReferenceFolder: "Der Zielordner ist ein Unterorder des Ordners, den du verschieben möchtest." | ||||||
|  | hasChildFilesOrFolders: "Dieser Ordner kann nicht gelöscht werden, da er nicht leer ist." | ||||||
| copyUrl: "URL kopieren" | copyUrl: "URL kopieren" | ||||||
| rename: "Umbenennen" | rename: "Umbenennen" | ||||||
| avatar: "Profilbild" | avatar: "Profilbild" | ||||||
| @@ -214,26 +260,37 @@ nsfw: "Dieser Inhalt ist NSFW" | |||||||
| disconnectedFromServer: "Verbindung zum Server wurde getrennt" | disconnectedFromServer: "Verbindung zum Server wurde getrennt" | ||||||
| reload: "Aktualisieren" | reload: "Aktualisieren" | ||||||
| doNothing: "Ignorieren" | doNothing: "Ignorieren" | ||||||
|  | reloadConfirm: "Möchtest du die Chronik aktualisieren?" | ||||||
| watch: "Beobachten" | watch: "Beobachten" | ||||||
| unwatch: "Nicht mehr beobachten" | unwatch: "Nicht mehr beobachten" | ||||||
| accept: "Akzeptieren" | accept: "Akzeptieren" | ||||||
| reject: "Ablehnen" | reject: "Ablehnen" | ||||||
|  | normal: "Normal" | ||||||
| instanceName: "Name der Instanz" | instanceName: "Name der Instanz" | ||||||
| instanceDescription: "Beschreibung der Instanz" | instanceDescription: "Beschreibung der Instanz" | ||||||
| maintainerName: "Betreiber" | maintainerName: "Betreiber" | ||||||
| maintainerEmail: "Betreiberemail" | maintainerEmail: "Betreiber-Email" | ||||||
| tosUrl: "URL der Nutzungsbedingungen" | tosUrl: "URL der Nutzungsbedingungen" | ||||||
| thisYear: "Dieses Jahr" | thisYear: "Dieses Jahr" | ||||||
| thisMonth: "Dieser Monat" | thisMonth: "Dieser Monat" | ||||||
| today: "Heute" | today: "Heute" | ||||||
|  | dayX: "{day}" | ||||||
|  | monthX: "{month}" | ||||||
|  | yearX: "{year}" | ||||||
| pages: "Seiten" | pages: "Seiten" | ||||||
| integration: "Integration" | integration: "Integration" | ||||||
| connectSerice: "Verbinden" | connectSerice: "Verbinden" | ||||||
| disconnectSerice: "Trennen" | disconnectSerice: "Trennen" | ||||||
|  | enableLocalTimeline: "Lokale Chronik aktivieren" | ||||||
|  | enableGlobalTimeline: "Globale Chronik aktivieren" | ||||||
|  | disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle Chroniken, auch wenn diese deaktiviert sind." | ||||||
| registration: "Registrieren" | registration: "Registrieren" | ||||||
| enableRegistration: "Registration neuer Benutzer erlauben" | enableRegistration: "Registration neuer Benutzer erlauben" | ||||||
| invite: "Einladen" | invite: "Einladen" | ||||||
|  | proxyRemoteFiles: "Dateien anderer Instanzen durch Proxy leiten" | ||||||
|  | proxyRemoteFilesDescription: "Wenn diese Einstellung aktiviert ist, dann werden Dateien von anderen Instanzen, welche entweder nicht lokal gespeichert sind oder durch Überschreiten des Speicherlimits gelöscht wurden, durch einen Proxy geleitet. Hierbei wird auch ein Vorschaubild generiert. \n Dies hat keinen Effekt auf den Speicherplatz des Servers." | ||||||
| driveCapacityPerLocalAccount: "Drivekapazität pro lokales Benutzerkonto" | driveCapacityPerLocalAccount: "Drivekapazität pro lokales Benutzerkonto" | ||||||
|  | driveCapacityPerRemoteAccount: "Drive-Kapazität pro Benutzer anderer Instanzen" | ||||||
| inMb: "In Megabytes" | inMb: "In Megabytes" | ||||||
| iconUrl: "Icon-URL" | iconUrl: "Icon-URL" | ||||||
| bannerUrl: "Banner-URL" | bannerUrl: "Banner-URL" | ||||||
| @@ -242,6 +299,8 @@ pinnedUsers: "Angepinnte Benutzer" | |||||||
| pinnedUsersDescription: "Gib einen Benutzernamen pro Zeile ein. Diese werden im \"Erkunden\" Tab angezeigt." | pinnedUsersDescription: "Gib einen Benutzernamen pro Zeile ein. Diese werden im \"Erkunden\" Tab angezeigt." | ||||||
| recaptcha: "reCAPTCHA" | recaptcha: "reCAPTCHA" | ||||||
| enableRecaptcha: "reCAPTCHA aktivieren" | enableRecaptcha: "reCAPTCHA aktivieren" | ||||||
|  | recaptchaSiteKey: "Site key" | ||||||
|  | recaptchaSecretKey: "Secret key" | ||||||
| antennas: "Antennen" | antennas: "Antennen" | ||||||
| manageAntennas: "Antennen verwalten" | manageAntennas: "Antennen verwalten" | ||||||
| name: "Name" | name: "Name" | ||||||
| @@ -249,6 +308,8 @@ antennaSource: "Antennenquelle" | |||||||
| antennaKeywords: "Schlüsselwörter, die beobachtet werden sollen" | antennaKeywords: "Schlüsselwörter, die beobachtet werden sollen" | ||||||
| antennaExcludeKeywords: "Schlüsselwörter, die ignoriert werden sollen" | antennaExcludeKeywords: "Schlüsselwörter, die ignoriert werden sollen" | ||||||
| antennaKeywordsDescription: "Mit Leerzeichen für eine \"UND\"-Verknüpfung trennen, durch Zeilenumbrüche für eine \"ODER\"-Verknüpfung trennen." | antennaKeywordsDescription: "Mit Leerzeichen für eine \"UND\"-Verknüpfung trennen, durch Zeilenumbrüche für eine \"ODER\"-Verknüpfung trennen." | ||||||
|  | notifyAntenna: "Über neue Notizen benachrichtigen" | ||||||
|  | withFileAntenna: "Nur Notizen mit Dateien" | ||||||
| serviceworker: "ServiceWorker" | serviceworker: "ServiceWorker" | ||||||
| enableServiceworker: "ServiceWorker aktivieren" | enableServiceworker: "ServiceWorker aktivieren" | ||||||
| antennaUsersDescription: "Benutzernamen getrennt durch Zeilenumbrüche angeben" | antennaUsersDescription: "Benutzernamen getrennt durch Zeilenumbrüche angeben" | ||||||
| @@ -257,19 +318,44 @@ withReplies: "Antworten beinhalten" | |||||||
| connectedTo: "Mit folgenden Benutzerkonten verknüpft" | connectedTo: "Mit folgenden Benutzerkonten verknüpft" | ||||||
| notesAndReplies: "Notizen und Antworten" | notesAndReplies: "Notizen und Antworten" | ||||||
| withFiles: "Dateien beinhalten" | withFiles: "Dateien beinhalten" | ||||||
|  | silence: "Instanzweit stummschalten" | ||||||
|  | silenceConfirm: "Möchtest du diesen Benutzer wirklich instanzweit stummschalten?" | ||||||
|  | unsilence: "Instanzweite Stummschaltung aufheben" | ||||||
|  | unsilenceConfirm: "Möchtest du die instanzweite Stummschaltung dieses Benutzers wirklich aufheben?" | ||||||
| popularUsers: "Beliebte Benutzer" | popularUsers: "Beliebte Benutzer" | ||||||
| recentlyUpdatedUsers: "Vor kurzem aktive Benutzer" | recentlyUpdatedUsers: "Vor kurzem aktive Benutzer" | ||||||
| recentlyRegisteredUsers: "Vor kurzem registrierte Benutzer" | recentlyRegisteredUsers: "Vor kurzem registrierte Benutzer" | ||||||
| recentlyDiscoveredUsers: "Vor kurzem gefundene Benutzer" | recentlyDiscoveredUsers: "Vor kurzem gefundene Benutzer" | ||||||
| exploreUsersCount: "Es gibt {count} Benutzer" | exploreUsersCount: "Es gibt {count} Benutzer" | ||||||
| exploreFediverse: "Das Fediverse erkunden" | exploreFediverse: "Das Fediverse erkunden" | ||||||
|  | popularTags: "Beliebte Schlagwörter" | ||||||
| userList: "Listen" | userList: "Listen" | ||||||
|  | about: "Über" | ||||||
| aboutMisskey: "Über Misskey" | aboutMisskey: "Über Misskey" | ||||||
|  | aboutMisskeyText: "Misskey ist Open-Source-Software die von syuilo seit 2014 entwickelt wird." | ||||||
|  | misskeyMembers: "Misskey wird momentan von den unten aufgelisteten Mitgliedern weiterentwickelt und instand gehalten:" | ||||||
|  | misskeySource: "Der Quelltext ist hier verfügbar:" | ||||||
|  | misskeyTranslation: "Hilf dabei, Misskey zu übersetzen:" | ||||||
|  | misskeyDonate: "Spende an Misskey, um die Weiterentwicklung zu unterstützen:" | ||||||
|  | morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰" | ||||||
|  | patrons: "UnterstützerInnen" | ||||||
|  | administrator: "Administrator" | ||||||
|  | token: "Token" | ||||||
|  | twoStepAuthentication: "Zwei-Faktor-Authentifizierung" | ||||||
|  | moderator: "Moderator" | ||||||
|  | nUsersMentioned: "{n} Benutzer erwähnt" | ||||||
|  | securityKey: "Sicherheitsschlüssel" | ||||||
|  | securityKeyName: "Schlüsselname" | ||||||
|  | registerSecurityKey: "Sicherheitsschlüssel registrieren" | ||||||
|  | lastUsed: "Zuletzt benutzt" | ||||||
|  | unregister: "Deaktivieren" | ||||||
|  | passwordLessLogin: "Passwortloses Anmelden einrichten" | ||||||
| resetPassword: "Passwort zurücksetzen" | resetPassword: "Passwort zurücksetzen" | ||||||
| newPasswordIs: "Das neue Passwort ist \"{password}\"" | newPasswordIs: "Das neue Passwort ist \"{password}\"" | ||||||
|  | post: "Beitrag" | ||||||
| posted: "Gesendet" | posted: "Gesendet" | ||||||
| autoReloadWhenDisconnected: "Automatisch aktualisieren wenn die Serververbindung getrennt wird" | autoReloadWhenDisconnected: "Automatisch aktualisieren wenn die Serververbindung getrennt wird" | ||||||
| autoNoteWatch: "Notiz automatisch beobachten" | autoNoteWatch: "Notizen automatisch beobachten" | ||||||
| autoNoteWatchDescription: "Werde über Notizen, auf die du reagiert oder geantwortet hast, informiert" | autoNoteWatchDescription: "Werde über Notizen, auf die du reagiert oder geantwortet hast, informiert" | ||||||
| reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren" | reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren" | ||||||
| share: "Teilen" | share: "Teilen" | ||||||
| @@ -279,7 +365,21 @@ uploadFolder: "Standardordner für Uploads" | |||||||
| cacheClear: "Cache leeren" | cacheClear: "Cache leeren" | ||||||
| markAsReadAllNotifications: "Alle Benachrichtigungen als gelesen markieren" | markAsReadAllNotifications: "Alle Benachrichtigungen als gelesen markieren" | ||||||
| markAsReadAllUnreadNotes: "Alle Notizen als gelesen markieren" | markAsReadAllUnreadNotes: "Alle Notizen als gelesen markieren" | ||||||
|  | markAsReadAllTalkMessages: "Alle Nachrichten als gelesen markieren" | ||||||
|  | help: "Hilfe" | ||||||
|  | inputMessageHere: "Hier Nachricht eingeben" | ||||||
|  | close: "Schließen" | ||||||
|  | group: "Gruppe" | ||||||
|  | groups: "Gruppen" | ||||||
|  | createGroup: "Gruppe erstellen" | ||||||
|  | ownedGroups: "Eigene Gruppen" | ||||||
|  | joinedGroups: "Beigetretene Gruppen" | ||||||
| invites: "Einladen" | invites: "Einladen" | ||||||
|  | groupName: "Gruppenname" | ||||||
|  | members: "Mitglieder" | ||||||
|  | transfer: "Übertragen" | ||||||
|  | messagingWithUser: "Privatnachrichten mit einem Benutzer" | ||||||
|  | messagingWithGroup: "Privatnachrichten mit einer Gruppe" | ||||||
| title: "Betreff" | title: "Betreff" | ||||||
| text: "Text" | text: "Text" | ||||||
| enable: "Aktivieren" | enable: "Aktivieren" | ||||||
| @@ -288,12 +388,40 @@ retype: "Erneut eingeben" | |||||||
| noteOf: "Notiz von {user}" | noteOf: "Notiz von {user}" | ||||||
| inviteToGroup: "Zu Gruppe einladen" | inviteToGroup: "Zu Gruppe einladen" | ||||||
| maxNoteTextLength: "Maximale Länge von Notizen" | maxNoteTextLength: "Maximale Länge von Notizen" | ||||||
|  | quoteAttached: "Zitiert" | ||||||
|  | quoteQuestion: "Als Zitat anfügen?" | ||||||
|  | noMessagesYet: "Noch keine Nachrichten" | ||||||
|  | newMessageExists: "Du hast eine neue Nachricht" | ||||||
|  | onlyOneFileCanBeAttached: "Es kann pro Nachricht nur eine Datei angehängt werden" | ||||||
|  | signinRequired: "Anmeldung erforderlich" | ||||||
|  | invitationCode: "Einladungscode" | ||||||
|  | checking: "Wird überprüft..." | ||||||
|  | available: "Verfügbar" | ||||||
|  | unavailable: "Unverfügbar" | ||||||
|  | usernameInvalidFormat: "Buchstaben, Zahlen und Unterstriche sind verwendbar." | ||||||
|  | tooShort: "Zu kurz" | ||||||
|  | tooLong: "Zu lang" | ||||||
|  | weakPassword: "Schwaches Passwort" | ||||||
|  | normalPassword: "Standardpasswort" | ||||||
|  | strongPassword: "Starkes Passwort" | ||||||
|  | passwordMatched: "Stimmt überein" | ||||||
|  | passwordNotMatched: "Stimmt nicht überein" | ||||||
|  | signinWith: "Mit {x} anmelden" | ||||||
|  | signinFailed: "Anmeldung fehlgeschlagen. Überprüfe Benutzername und Passswort." | ||||||
|  | tapSecurityKey: "Tippe deinen Sicherheitsschlüssel an" | ||||||
|  | or: "Oder" | ||||||
|  | uiLanguage: "Sprache der Benutzeroberfläche" | ||||||
|  | groupInvited: "Du wurdest in eine Gruppe eingeladen" | ||||||
|  | aboutX: "Über {x}" | ||||||
| useOsNativeEmojis: "Eingebaute Emojis des Betriebssystems benutzen" | useOsNativeEmojis: "Eingebaute Emojis des Betriebssystems benutzen" | ||||||
|  | youHaveNoGroups: "Keine Gruppen vorhanden" | ||||||
| joinOrCreateGroup: "Lass dich zu einer Gruppe einladen oder erstelle deine eigene." | joinOrCreateGroup: "Lass dich zu einer Gruppe einladen oder erstelle deine eigene." | ||||||
| noHistory: "Kein Verlauf" | noHistory: "Kein Verlauf" | ||||||
| disableAnimatedMfm: "MFM, die Animationen enthalten, deaktivieren" | disableAnimatedMfm: "MFM, die Animationen enthalten, deaktivieren" | ||||||
|  | doing: "In Bearbeitung" | ||||||
| category: "Kategorie" | category: "Kategorie" | ||||||
| tags: "Schlagwörter" | tags: "Schlagwörter" | ||||||
|  | docSource: "Quelle dieses Dokuments" | ||||||
| createAccount: "Benutzerkonto erstellen" | createAccount: "Benutzerkonto erstellen" | ||||||
| existingAcount: "Bestehendes Benutzerkonto" | existingAcount: "Bestehendes Benutzerkonto" | ||||||
| regenerate: "Regenerieren" | regenerate: "Regenerieren" | ||||||
| @@ -302,29 +430,81 @@ noFollowRequests: "Du hast keine Follow-Anfragen" | |||||||
| openImageInNewTab: "Bilder in neuem Tab öffnen" | openImageInNewTab: "Bilder in neuem Tab öffnen" | ||||||
| dashboard: "Dashboard" | dashboard: "Dashboard" | ||||||
| local: "Lokal" | local: "Lokal" | ||||||
|  | remote: "Fremd" | ||||||
| total: "Gesamt" | total: "Gesamt" | ||||||
| weekOverWeekChanges: "Wöchentlich" | weekOverWeekChanges: "Wöchentlich" | ||||||
| dayOverDayChanges: "Täglich" | dayOverDayChanges: "Täglich" | ||||||
| accessibility: "Barrierefreiheit" | accessibility: "Barrierefreiheit" | ||||||
| clinetSettings: "Client-Einstellungen" | clinetSettings: "Client-Einstellungen" | ||||||
| accountSettings: "Benutzerkontoeinstellungen" | accountSettings: "Benutzerkonto-Einstellungen" | ||||||
| promotion: "Hervorgehoben" | promotion: "Hervorgehoben" | ||||||
| promote: "Hervorheben" | promote: "Hervorheben" | ||||||
| numberOfDays: "Anzahl der Tage" | numberOfDays: "Anzahl der Tage" | ||||||
| hideThisNote: "Diese Notiz verstecken" | hideThisNote: "Diese Notiz verstecken" | ||||||
|  | showFeaturedNotesInTimeline: "Beliebte Notizen in Chronik anzeigen" | ||||||
|  | objectStorage: "Objektspeicher" | ||||||
|  | useObjectStorage: "Objektspeicher verwenden" | ||||||
|  | objectStorageBaseUrl: "Basis-URL" | ||||||
|  | objectStorageBaseUrlDesc: "URL-Prefix, der zum Konstruieren der Objekt- bzw. Mediareferenz-URL genutzt wird. Falls du ein CDN- oder einen Proxy nutzt, gib dessen URL ein. Ansonsten gib die Adresse, der dir von deinem Anbieter z.B. in dessen Servicehandbuch gegeben wurde, an. Beispielsweise 'https://<bucket>.s3.amazonaws.com' für AWS S3 oder 'https://storage.googleapis.com/<bucket>' für GCS." | ||||||
|  | objectStorageBucket: "Bucket" | ||||||
|  | objectStorageBucketDesc: "Bitte gib den Bucket-Namen an, der bei deinem Anbieter verwendet wird." | ||||||
|  | objectStoragePrefix: "Prefix" | ||||||
|  | objectStoragePrefixDesc: "Dateien werden im Ordner dieses Prefixes gespeichert." | ||||||
|  | objectStorageEndpoint: "Endpoint" | ||||||
|  | objectStorageEndpointDesc: "Dieses Feld leerlassen, falls du AWS S3 verwendest. Ansonsten trage den Endpoint im Format \"<host>\" oder \"<host>:<port>\" an, den Angaben deines Anbieters entsprechend." | ||||||
|  | objectStorageRegion: "Region" | ||||||
|  | objectStorageRegionDesc: "Gib eine Region (wie z.B. \"xx-east-1\") an. Falls dein Anbieter nicht zwischen Regionen unterscheidet, lass dieses Feld leer oder gib \"us-east-1\" an." | ||||||
|  | objectStorageUseSSL: "SSL verwenden" | ||||||
|  | objectStorageUseSSLDesc: "Deaktiviere dies falls du für die API-Verbindungen kein HTTPS verwenden wirst" | ||||||
|  | objectStorageUseProxy: "Über Proxy verbinden" | ||||||
|  | objectStorageUseProxyDesc: "Deaktiviere dies falls du keinen Proxy für den Objektspeicher verwenden wirst" | ||||||
| serverLogs: "Serverprotokolle" | serverLogs: "Serverprotokolle" | ||||||
| deleteAll: "Alle löschen" | deleteAll: "Alle löschen" | ||||||
|  | showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen" | ||||||
|  | newNoteRecived: "Du hast eine neue Notiz empfangen" | ||||||
| sounds: "Töne" | sounds: "Töne" | ||||||
| listen: "Anhören" | listen: "Anhören" | ||||||
| none: "Keine" | none: "Keine" | ||||||
| volume: "Lautstärke" | volume: "Lautstärke" | ||||||
| details: "Details" | details: "Details" | ||||||
| chooseEmoji: "Wähle ein Emoji" | chooseEmoji: "Wähle ein Emoji" | ||||||
|  | unableToProcess: "Der Vorgang konnte nicht abgeschlossen werden." | ||||||
|  | recentUsed: "Vor kurzem verwendet" | ||||||
|  | install: "Installieren" | ||||||
|  | uninstall: "Uninstallieren" | ||||||
|  | installedApps: "Authorisierte Anwendungen" | ||||||
|  | nothing: "Hier gibt es nichts zu sehen" | ||||||
|  | installedDate: "Authorisiert" | ||||||
|  | lastUsedDate: "Zuletzt verwendet" | ||||||
|  | state: "Status" | ||||||
|  | sort: "Sortieren" | ||||||
|  | ascendingOrder: "Aufsteigende Reihenfolge" | ||||||
|  | descendingOrder: "Absteigende Reihenfolge" | ||||||
|  | scratchpad: "Testumgebung" | ||||||
|  | scratchpadDescription: "Die Testumgebung bietet eine experimentale Umgebung für AiScript. Dort kannst du AiScript schreiben, ausführen sowie dessen Auswirkungen auf Misskey überprüfen." | ||||||
|  | output: "Ausgabe" | ||||||
|  | script: "Skript" | ||||||
|  | disablePagesScript: "AiScript auf Seiten deaktivieren" | ||||||
|  | updateRemoteUser: "Informationen über den Benutzer der fremder Instanz aktualisieren" | ||||||
|  | deleteAllFiles: "Alle Dateien löschen" | ||||||
|  | deleteAllFilesConfirm: "Möchtest du wirklich alle Dateien löschen?" | ||||||
|  | removeAllFollowing: "Allen gefolgten Benutzern entfolgen" | ||||||
|  | removeAllFollowingDescription: "Allen Benutzerkonten von {host} entfolgen. Bitte führe dies durch, falls diese Instanz nicht mehr existiert." | ||||||
|  | userSuspended: "Dieser Benutzer wurde gesperrt." | ||||||
|  | userSilenced: "Dieser Benutzer wurde instanzweit stummgeschaltet." | ||||||
|  | _theme: | ||||||
|  |   explore: "Themen erforschen" | ||||||
|  |   install: "Thema installieren" | ||||||
|  |   manage: "Themaverwaltung" | ||||||
|  |   code: "Themencode" | ||||||
|  |   installed: "{name} wurde installiert" | ||||||
|  |   alreadyInstalled: "Dieses Thema ist bereits installiert" | ||||||
|  |   invalid: "Themenformat ist ungültig" | ||||||
| _sfx: | _sfx: | ||||||
|   note: "Notizen" |   note: "Notizen" | ||||||
|   noteMy: "Meine Notizen" |   noteMy: "Meine Notizen" | ||||||
|   notification: "Benachrichtigungen" |   notification: "Benachrichtigungen" | ||||||
|   chat: "Nachrichten" |   chat: "Privatnachrichten" | ||||||
|   chatBg: "Nachrichten (Hintergrund)" |   chatBg: "Nachrichten (Hintergrund)" | ||||||
|   antenna: "Antennen" |   antenna: "Antennen" | ||||||
| _ago: | _ago: | ||||||
| @@ -342,11 +522,79 @@ _time: | |||||||
|   second: "Sekunde" |   second: "Sekunde" | ||||||
|   minute: "Minute" |   minute: "Minute" | ||||||
|   hour: "Stunde" |   hour: "Stunde" | ||||||
|  |   day: "t" | ||||||
|  | _tutorial: | ||||||
|  |   title: "Wie du Misskey verwendest" | ||||||
|  |   step1_1: "Willkommen!" | ||||||
|  |   step1_2: "Diese Seite ist die \"Chronik\". Sie zeigt dir deine geschrieben \"Notizen\" sowie die aller Benutzer, denen du \"folgst\" in chronologischer Reihenfolge." | ||||||
|  |   step1_3: "Deine Chronik sollte momentan leer sein, da du bis jetzt nocht keine Notizen geschrieben hast und auch noch keinen Benutzern folgst." | ||||||
|  |   step2_1: "Lass uns zuerst dein Profil vervollständigen, bevor du Notizen schreibst oder jemandem folgst." | ||||||
|  |   step2_2: "Informationen darüber, wer du bist, macht es anderen leichter zu wissen, ob sie deine Notizen sehen wollen und ob sie dir folgen möchten." | ||||||
|  |   step3_1: "Mit dem Einrichten deines Profils fertig?" | ||||||
|  |   step3_2: "Der nächste Schritt ist das Schreiben einer Notiz. Dies kannst du tun, indem du auf das Stift-Icon auf dem Bildschirm drückst." | ||||||
|  |   step3_3: "Fülle das Fenster aus und drücke auf den Knopf oben rechts zum Senden." | ||||||
|  |   step3_4: "Gibt es nichts, das du momentan sagen möchtest? Versuch's mit \"Hallo Misskey!\"" | ||||||
|  |   step4_1: "Fertig mit dem Senden deiner ersten Notiz?" | ||||||
|  |   step4_2: "Falls deine Notiz nun auf deiner Chronik auftaucht, hast du alles richtig gemacht." | ||||||
|  |   step5_1: "Lass uns nun deiner Chronik etwas mehr Leben einhauchen, indem du einigen anderen Benutzern folgst." | ||||||
|  |   step5_2: "{featured} zeigt dir beliebte Notizen dieser Instanz. In {explore} kannst du beliebte Benutzer finden. Schau dort, ob du Benutzer findest, die dich interessieren." | ||||||
|  |   step5_3: "Um anderen Benutzern zu folgen, klicke auf ihr Profilbild und klicke dann auf den \"Folgen\" Knopf in ihrem Profil." | ||||||
|  |   step5_4: "Wenn der Benutzer neben seinem Namen ein Schloss hat, dann muss er deine Follow-Anfrage manuell bestätigen." | ||||||
|  |   step6_1: "Wenn du nun auch die Notizen anderer Benutzer auf deiner Chronik siehst, hast du alles richtig gemacht." | ||||||
|  |   step6_2: "Du kannst ebenso \"Reaktionen\" verwenden, um schnell auf Notizen anderer Benutzer zu antworten." | ||||||
|  |   step6_3: "Um eine \"Reaktion\" anzufügen, klicke auf das \"+\"-Symbol in der Notiz eines anderen Benutzers und wähle ein Emoji, mit dem du reagieren möchtest." | ||||||
|  |   step7_1: "Glückwunsch! Du hast die Misskey-Einführung abgeschlossen." | ||||||
|  |   step7_2: "Wenn du mehr über Misskey lernen möchtest, schau dich im {help}-Bereich um." | ||||||
|  |   step7_3: "Und nun, viel Spaß mit Misskey! 🚀" | ||||||
|  | _2fa: | ||||||
|  |   alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert" | ||||||
|  |   registerDevice: "Neues Gerät registrieren" | ||||||
|  |   registerKey: "Neuen Sicherheitsschlüssel registrieren" | ||||||
|  |   step1: "Als Erstes, installiere eine Authentifizierungsapp (z.B. {a} oder {b}) auf deinem Gerät." | ||||||
|  |   step2: "Dann, scanne den angezeigten QR-Code mit deinem Gerät." | ||||||
|  |   step3: "Um die Einrichtung abzuschließen, gib den Token ein, der von deiner Authentifizierungsapp angezeigt wird." | ||||||
|  |   step4: "Ab jetzt benötigen alle Loginversuche auch einen Login-Token." | ||||||
|  |   securityKeyInfo: "Du kannst neben Fingerabdruck- oder PIN-Authentifizierung auf deinem Gerät auch Authentifizierung mit FIDO2-kompatiblen Hardware-Sicherheitsschlüsseln einrichten." | ||||||
| _permissions: | _permissions: | ||||||
|   "read:messaging": "Nachrichten lesen" |   "read:account": "Deine Benutzerkontoinformationen lesen" | ||||||
|   "write:messaging": "Nachrichten schicken oder löschen" |   "write:account": "Deine Benutzerkontoinformationen bearbeiten" | ||||||
|   "read:reactions": "Reaktionen sehen" |   "read:blocks": "Die Liste deiner blockierten Benutzer lesen" | ||||||
|  |   "write:blocks": "Die Liste deiner blockierten Benutzer bearbeiten" | ||||||
|  |   "read:drive": "Deine Drive-Dateien und Ordner lesen" | ||||||
|  |   "write:drive": "Deine Drive-Dateien und Ordner bearbeiten oder löschen" | ||||||
|  |   "read:favorites": "Deine Favoriten-Liste lesen" | ||||||
|  |   "write:favorites": "Deine Favoriten-Liste bearbeiten" | ||||||
|  |   "read:following": "Deine Follower-Liste lesen" | ||||||
|  |   "write:following": "Anderen Benutzern folgen oder entfolgen" | ||||||
|  |   "read:messaging": "Privatnachrichten lesen" | ||||||
|  |   "write:messaging": "Privatnachrichten schicken oder löschen" | ||||||
|  |   "read:mutes": "Stummschaltungen lesen" | ||||||
|  |   "write:mutes": "Stummschaltungen bearbeiten" | ||||||
|  |   "write:notes": "Notizen schreiben oder löschen" | ||||||
|  |   "read:notifications": "Benachrichtigungen lesen" | ||||||
|  |   "write:notifications": "Mit Benachrichtigungen arbeiten" | ||||||
|  |   "read:reactions": "Reaktionen lesen" | ||||||
|   "write:reactions": "Reaktionen hinzufügen und bearbeiten" |   "write:reactions": "Reaktionen hinzufügen und bearbeiten" | ||||||
|  |   "write:votes": "In Umfragen abstimmen" | ||||||
|  |   "read:pages": "Deine Seiten lesen" | ||||||
|  |   "write:pages": "Deine Seiten bearbeiten oder löschen" | ||||||
|  |   "read:page-likes": "Liste der Seiten, die mir gefallen, lesen" | ||||||
|  |   "write:page-likes": "Liste der Seiten, die mir gefallen, bearbeiten" | ||||||
|  |   "read:user-groups": "Benutzergruppen lesen" | ||||||
|  |   "write:user-groups": "Benutzergruppen bearbeiten oder löschen" | ||||||
|  | _auth: | ||||||
|  |   shareAccess: "Möchtest du \"{name}\" authorisieren, auf dieses Benuzerkonto zugreifen zu können?" | ||||||
|  |   shareAccessAsk: "Bist du dir sicher, dass du diese Anwendung authorisieren möchtest, auf dein Benutzerkonto zugreifen zu können?" | ||||||
|  |   permissionAsk: "Diese Anwendung erfordert folgende Berechtigungen:" | ||||||
|  |   pleaseGoBack: "Bitte gehe zurück zur Anwendung" | ||||||
|  |   callback: "Rückkehr zur Anwendung" | ||||||
|  |   denied: "Zugriff verweigert" | ||||||
|  | _antennaSources: | ||||||
|  |   all: "Alle Notizen" | ||||||
|  |   homeTimeline: "Notizen von Benutzern, denen gefolgt wird" | ||||||
|  |   users: "Notizen von konkreten Benutzern" | ||||||
|  |   userList: "Notizen von allen Benutzern aus einer Liste" | ||||||
|  |   userGroup: "Notizen von allen Benutzern aus einer Gruppe" | ||||||
| _weekday: | _weekday: | ||||||
|   sunday: "Sonntag" |   sunday: "Sonntag" | ||||||
|   monday: "Montag" |   monday: "Montag" | ||||||
| @@ -358,7 +606,7 @@ _weekday: | |||||||
| _widgets: | _widgets: | ||||||
|   memo: "Memo" |   memo: "Memo" | ||||||
|   notifications: "Benachrichtigungen" |   notifications: "Benachrichtigungen" | ||||||
|   timeline: "Zeitleiste" |   timeline: "Chronik" | ||||||
|   calendar: "Kalender" |   calendar: "Kalender" | ||||||
|   trends: "Trends" |   trends: "Trends" | ||||||
|   clock: "Uhr" |   clock: "Uhr" | ||||||
| @@ -367,20 +615,20 @@ _widgets: | |||||||
|   photos: "Fotos" |   photos: "Fotos" | ||||||
| _cw: | _cw: | ||||||
|   hide: "Ausblenden" |   hide: "Ausblenden" | ||||||
|   show: "Zeige mehr" |   show: "Mehr anzeigen" | ||||||
|   chars: "{count} Zeichen" |   chars: "{count} Zeichen" | ||||||
|   files: "{count} Dateien" |   files: "{count} Dateien" | ||||||
|   poll: "Umfrage" |   poll: "Umfrage" | ||||||
| _poll: | _poll: | ||||||
|   noOnlyOneChoice: "Mindestens zwei Antwortmöglichkeiten werden benötigt." |   noOnlyOneChoice: "Mindestens zwei Antwortmöglichkeiten werden benötigt." | ||||||
|   choiceN: "Auswahl {n}" |   choiceN: "Auswahl {n}" | ||||||
|   noMore: "Du kannst keine weiteren Auswahlen hinzufügen" |   noMore: "Du kannst keine weiteren Auswahlmöglichkeiten hinzufügen" | ||||||
|   canMultipleVote: "Mehrfachantworten erlauben" |   canMultipleVote: "Mehrfachantworten erlauben" | ||||||
|   expiration: "Abstimmung endet am" |   expiration: "Abstimmung endet am" | ||||||
|   infinite: "Nie" |   infinite: "Nie" | ||||||
|   at: "Beenden am..." |   at: "Beenden am..." | ||||||
|   after: "Beenden nach..." |   after: "Beenden nach..." | ||||||
|   deadlineDate: "Enddatum" |   deadlineDate: "Abstimmungsende" | ||||||
|   deadlineTime: "Stunde" |   deadlineTime: "Stunde" | ||||||
|   duration: "Laufzeit" |   duration: "Laufzeit" | ||||||
|   votesCount: "{n} Stimmen" |   votesCount: "{n} Stimmen" | ||||||
| @@ -397,10 +645,11 @@ _visibility: | |||||||
|   public: "Öffentlich" |   public: "Öffentlich" | ||||||
|   publicDescription: "Deine Notiz wird global sichtbar sein" |   publicDescription: "Deine Notiz wird global sichtbar sein" | ||||||
|   home: "Startseite" |   home: "Startseite" | ||||||
|   followers: "Folgende" |   homeDescription: "Deine Notiz wird nur in der Chronik deiner Instanz angezeigt." | ||||||
|  |   followers: "Follower" | ||||||
|   followersDescription: "Nur für Follower sichtbar" |   followersDescription: "Nur für Follower sichtbar" | ||||||
|   specified: "Direkt" |   specified: "Direkt" | ||||||
|   specifiedDescription: "Nur für erwähnte Benutzer sichtbar" |   specifiedDescription: "Nur für bestimmte Benutzer sichtbar" | ||||||
|   localOnly: "Nur Lokal" |   localOnly: "Nur Lokal" | ||||||
| _postForm: | _postForm: | ||||||
|   replyPlaceholder: "Dieser Notiz antworten..." |   replyPlaceholder: "Dieser Notiz antworten..." | ||||||
| @@ -422,9 +671,9 @@ _profile: | |||||||
|   metadataContent: "Inhalt" |   metadataContent: "Inhalt" | ||||||
| _exportOrImport: | _exportOrImport: | ||||||
|   allNotes: "Alle Notizen" |   allNotes: "Alle Notizen" | ||||||
|   followingList: "Folgen" |   followingList: "Gefolgte Benutzer" | ||||||
|   muteList: "Stummschalten" |   muteList: "Stummschaltungen" | ||||||
|   blockingList: "Blockieren" |   blockingList: "Blockierungen" | ||||||
|   userLists: "Listen" |   userLists: "Listen" | ||||||
| _charts: | _charts: | ||||||
|   federationInstancesIncDec: "Unterschied in der Anzahl von förderierenden Instanzen" |   federationInstancesIncDec: "Unterschied in der Anzahl von förderierenden Instanzen" | ||||||
| @@ -434,6 +683,7 @@ _charts: | |||||||
|   activeUsers: "Aktive Benutzer" |   activeUsers: "Aktive Benutzer" | ||||||
|   notesIncDec: "Unterschied in der Anzahl von Notizen" |   notesIncDec: "Unterschied in der Anzahl von Notizen" | ||||||
|   localNotesIncDec: "Unterschied in der Anzahl von lokalen Notizen" |   localNotesIncDec: "Unterschied in der Anzahl von lokalen Notizen" | ||||||
|  |   remoteNotesIncDec: "Differenz in Anzahl der Notizen von anderen Instanzen" | ||||||
|   notesTotal: "Anzahl aller Notizen" |   notesTotal: "Anzahl aller Notizen" | ||||||
|   filesIncDec: "Unterschied in der Anzahl von Dateien" |   filesIncDec: "Unterschied in der Anzahl von Dateien" | ||||||
|   filesTotal: "Anzahl aller Dateien" |   filesTotal: "Anzahl aller Dateien" | ||||||
| @@ -442,26 +692,57 @@ _charts: | |||||||
| _instanceCharts: | _instanceCharts: | ||||||
|   requests: "Anfragen" |   requests: "Anfragen" | ||||||
|   users: "Unterschied in der Anzahl von Benutzern" |   users: "Unterschied in der Anzahl von Benutzern" | ||||||
|  |   usersTotal: "Gesamtanzahl an Benutzern" | ||||||
|   notes: "Unterschied in der Anzahl von Notizen" |   notes: "Unterschied in der Anzahl von Notizen" | ||||||
|  |   notesTotal: "Gesamtanzahl an Notizen" | ||||||
|   ff: "Unterschied in der Anzahl von Followern" |   ff: "Unterschied in der Anzahl von Followern" | ||||||
|  |   ffTotal: "Gesamtanzahl an Followern" | ||||||
|   cacheSize: "Unterschied in der Größe des Caches" |   cacheSize: "Unterschied in der Größe des Caches" | ||||||
|  |   cacheSizeTotal: "Gesamtgröße des Caches" | ||||||
|   files: "Unterschied in der Anzahl der Dateien" |   files: "Unterschied in der Anzahl der Dateien" | ||||||
|  |   filesTotal: "Gesamtanzahl an Dateien" | ||||||
| _timelines: | _timelines: | ||||||
|   home: "Startseite" |   home: "Startseite" | ||||||
|   local: "Lokal" |   local: "Lokal" | ||||||
|  |   social: "Sozial" | ||||||
|   global: "Global" |   global: "Global" | ||||||
| _pages: | _pages: | ||||||
|  |   newPage: "Seite erstellen" | ||||||
|  |   editPage: "Diese Seite bearbeiten" | ||||||
|  |   readPage: "Quelltext-Ansicht" | ||||||
|  |   created: "Seite erfolgreich erstellt" | ||||||
|  |   updated: "Seite erfolgreich aktualisiert" | ||||||
|  |   deleted: "Seite erfolgreich gelöscht" | ||||||
|  |   nameAlreadyExists: "Die angegebene Seiten-URL existiert bereits" | ||||||
|  |   invalidNameTitle: "Die angegebene Seiten-URL ist ungültig" | ||||||
|  |   invalidNameText: "Überprüfe, ob der Seitentitel nicht leer ist" | ||||||
|  |   editThisPage: "Diese Seite bearbeiten" | ||||||
|  |   viewSource: "Quelltext anzeigen" | ||||||
|  |   viewPage: "Seite anschauen" | ||||||
|  |   like: "Gefällt mir" | ||||||
|  |   unlike: "\"Gefällt mir\" entfernen" | ||||||
|  |   my: "Meine Seiten" | ||||||
|  |   liked: "Seiten, die mir gefallen" | ||||||
|  |   inspector: "Inspektor" | ||||||
|   content: "Inhalt" |   content: "Inhalt" | ||||||
|  |   variables: "Variablen" | ||||||
|   title: "Titel" |   title: "Titel" | ||||||
|   url: "Seiten-URL" |   url: "Seiten-URL" | ||||||
|   summary: "Zusammenfassung" |   summary: "Zusammenfassung" | ||||||
|   alignCenter: "Mittig ausrichten" |   alignCenter: "Bestandteile zentrieren" | ||||||
|  |   hideTitleWhenPinned: "Seitentitel ausblenden, wenn an dein Profil angepinnt " | ||||||
|   font: "Schriftart" |   font: "Schriftart" | ||||||
|   fontSerif: "Serif" |   fontSerif: "Serif" | ||||||
|   fontSansSerif: "Sans Serif" |   fontSansSerif: "Sans Serif" | ||||||
|  |   eyeCatchingImageSet: "Vorschaubild festlegen" | ||||||
|  |   eyeCatchingImageRemove: "Vorschaubild entfernen" | ||||||
|   chooseBlock: "Block hinzufügen" |   chooseBlock: "Block hinzufügen" | ||||||
|   selectType: "Wähle einen Typ" |   selectType: "Wähle einen Typ" | ||||||
|   enterVariableName: "Gib einen Namen für deine Variable ein" |   enterVariableName: "Gib einen Namen für deine Variable ein" | ||||||
|  |   variableNameIsAlreadyUsed: "Dieser Name wird bereits von einer anderen Variable verwendet" | ||||||
|  |   contentBlocks: "Inhalt" | ||||||
|  |   inputBlocks: "Eingabe" | ||||||
|  |   specialBlocks: "Spezial" | ||||||
|   blocks: |   blocks: | ||||||
|     text: "Text" |     text: "Text" | ||||||
|     textarea: "Textfeld" |     textarea: "Textfeld" | ||||||
| @@ -476,22 +757,27 @@ _pages: | |||||||
|       text: "Inhalt" |       text: "Inhalt" | ||||||
|     textInput: "Texteingabe" |     textInput: "Texteingabe" | ||||||
|     _textInput: |     _textInput: | ||||||
|  |       name: "Variablenname" | ||||||
|       text: "Titel" |       text: "Titel" | ||||||
|       default: "Standardwert" |       default: "Standardwert" | ||||||
|     textareaInput: "Eingabe des mehrzeiligen Textfelds" |     textareaInput: "Eingabe des mehrzeiligen Textfelds" | ||||||
|     _textareaInput: |     _textareaInput: | ||||||
|  |       name: "Variablenname" | ||||||
|       text: "Titel" |       text: "Titel" | ||||||
|       default: "Standardwert" |       default: "Standardwert" | ||||||
|     numberInput: "Nummereingabe" |     numberInput: "Nummereingabe" | ||||||
|     _numberInput: |     _numberInput: | ||||||
|  |       name: "Variablenname" | ||||||
|       text: "Titel" |       text: "Titel" | ||||||
|       default: "Standardwert" |       default: "Standardwert" | ||||||
|     switch: "Fallunterscheidung" |     switch: "Fallunterscheidung" | ||||||
|     _switch: |     _switch: | ||||||
|  |       name: "Variablenname" | ||||||
|       text: "Titel" |       text: "Titel" | ||||||
|       default: "Standardwert" |       default: "Standardwert" | ||||||
|     counter: "Zähler" |     counter: "Zähler" | ||||||
|     _counter: |     _counter: | ||||||
|  |       name: "Variablenname" | ||||||
|       text: "Titel" |       text: "Titel" | ||||||
|       inc: "Erhöhen um" |       inc: "Erhöhen um" | ||||||
|     _button: |     _button: | ||||||
| @@ -509,13 +795,18 @@ _pages: | |||||||
|           message: "Nachricht, die bei Aktivierung gezeigt werden soll" |           message: "Nachricht, die bei Aktivierung gezeigt werden soll" | ||||||
|           variable: "Variable, die gesendet werden soll" |           variable: "Variable, die gesendet werden soll" | ||||||
|           no-variable: "Keine" |           no-variable: "Keine" | ||||||
|  |         callAiScript: "AiScript ausführen" | ||||||
|  |         _callAiScript: | ||||||
|  |           functionName: "Funktionsname" | ||||||
|     radioButton: "Optionsfeld" |     radioButton: "Optionsfeld" | ||||||
|     _radioButton: |     _radioButton: | ||||||
|  |       name: "Variablenname" | ||||||
|       title: "Titel" |       title: "Titel" | ||||||
|       values: "Auswahlmöglichkeiten (getrennt durch Zeilenumbrüche)" |       values: "Auswahlmöglichkeiten (getrennt durch Zeilenumbrüche)" | ||||||
|       default: "Standardwert" |       default: "Standardwert" | ||||||
|   script: |   script: | ||||||
|     categories: |     categories: | ||||||
|  |       flow: "Steuerung" | ||||||
|       logical: "Logische Operationen" |       logical: "Logische Operationen" | ||||||
|       operation: "Berechnungen" |       operation: "Berechnungen" | ||||||
|       comparison: "Vergleiche" |       comparison: "Vergleiche" | ||||||
| @@ -666,17 +957,26 @@ _pages: | |||||||
|       splitStrByLine: "Text nach Zeilenumbrüchen aufteilen" |       splitStrByLine: "Text nach Zeilenumbrüchen aufteilen" | ||||||
|       _splitStrByLine: |       _splitStrByLine: | ||||||
|         arg1: "Text" |         arg1: "Text" | ||||||
|  |       ref: "Variablen" | ||||||
|  |       aiScriptVar: "AiScript Variablen" | ||||||
|       fn: "Funktionen" |       fn: "Funktionen" | ||||||
|       _fn: |       _fn: | ||||||
|  |         slots: "Slots" | ||||||
|  |         slots-info: "Trenne jeden Slot mit einem Zeilenumbruch" | ||||||
|         arg1: "Ausgabe" |         arg1: "Ausgabe" | ||||||
|       for: "Wiederholen" |       for: "Wiederholen" | ||||||
|       _for: |       _for: | ||||||
|         arg1: "Anzahl der Wiederholungen" |         arg1: "Anzahl der Wiederholungen" | ||||||
|         arg2: "Aktion" |         arg2: "Aktion" | ||||||
|  |     typeError: "Slot {slot} akzeptiert Werte vom Typ \"{expect}\", aber es wurde ein \"{actual}\" Wert angegeben!" | ||||||
|  |     thereIsEmptySlot: "Slot {slot} ist leer!" | ||||||
|     types: |     types: | ||||||
|       string: "Text" |       string: "Text" | ||||||
|       number: "Nummer" |       number: "Nummer" | ||||||
|  |       boolean: "Flag" | ||||||
|       array: "Listen" |       array: "Listen" | ||||||
|       stringArray: "Textliste" |       stringArray: "Textliste" | ||||||
|  |     emptySlot: "Leerer Slot" | ||||||
|     enviromentVariables: "Umgebungsvariable" |     enviromentVariables: "Umgebungsvariable" | ||||||
|     pageVariables: "Seitenelement" |     pageVariables: "Seitenelement" | ||||||
|  |     argVariables: "Eingabe-Slot" | ||||||
|   | |||||||
| @@ -260,11 +260,12 @@ nsfw: "NSFW" | |||||||
| disconnectedFromServer: "Connection to the server was interrupted." | disconnectedFromServer: "Connection to the server was interrupted." | ||||||
| reload: "Refresh" | reload: "Refresh" | ||||||
| doNothing: "Ignore" | doNothing: "Ignore" | ||||||
| reloadConfirm: "Would you like to retry?" | reloadConfirm: "Would you like to refresh timeline?" | ||||||
| watch: "Watch" | watch: "Watch" | ||||||
| unwatch: "Undo Watch" | unwatch: "Undo Watch" | ||||||
| accept: "Accept" | accept: "Accept" | ||||||
| reject: "Reject" | reject: "Reject" | ||||||
|  | normal: "Normal" | ||||||
| instanceName: "Instance name" | instanceName: "Instance name" | ||||||
| instanceDescription: "Instance description" | instanceDescription: "Instance description" | ||||||
| maintainerName: "Maintainer" | maintainerName: "Maintainer" | ||||||
| @@ -319,6 +320,7 @@ notesAndReplies: "Notes and replies" | |||||||
| withFiles: "Media" | withFiles: "Media" | ||||||
| silence: "Silence" | silence: "Silence" | ||||||
| silenceConfirm: "Are you sure that you want to silence this user?" | silenceConfirm: "Are you sure that you want to silence this user?" | ||||||
|  | unsilence: "Unsilence" | ||||||
| unsilenceConfirm: "Are you sure that you want to undo silence of this user?" | unsilenceConfirm: "Are you sure that you want to undo silence of this user?" | ||||||
| popularUsers: "Trending users" | popularUsers: "Trending users" | ||||||
| recentlyUpdatedUsers: "Users with recent activity" | recentlyUpdatedUsers: "Users with recent activity" | ||||||
| @@ -352,7 +354,7 @@ resetPassword: "Reset password" | |||||||
| newPasswordIs: "The new password is \"{password}\"" | newPasswordIs: "The new password is \"{password}\"" | ||||||
| post: "Post" | post: "Post" | ||||||
| posted: "Posted!" | posted: "Posted!" | ||||||
| autoReloadWhenDisconnected: "Auto reload when disconnected from server" | autoReloadWhenDisconnected: "Auto refresh when disconnected from server" | ||||||
| autoNoteWatch: "Watch note automatically" | autoNoteWatch: "Watch note automatically" | ||||||
| autoNoteWatchDescription: "Get notified about the notes which you reactioned or replied." | autoNoteWatchDescription: "Get notified about the notes which you reactioned or replied." | ||||||
| reduceUiAnimation: "Reduce UI animation" | reduceUiAnimation: "Reduce UI animation" | ||||||
| @@ -454,6 +456,8 @@ objectStorageRegion: "Region" | |||||||
| objectStorageRegionDesc: "Specify a region like 'xx-east-1'. If your service does not have distinction about regions, leave it blank or fill with 'us-east-1'." | objectStorageRegionDesc: "Specify a region like 'xx-east-1'. If your service does not have distinction about regions, leave it blank or fill with 'us-east-1'." | ||||||
| objectStorageUseSSL: "Use SSL" | objectStorageUseSSL: "Use SSL" | ||||||
| objectStorageUseSSLDesc: "Turn off this if you are not going to use HTTPS for API connection" | objectStorageUseSSLDesc: "Turn off this if you are not going to use HTTPS for API connection" | ||||||
|  | objectStorageUseProxy: "Connect over Proxy" | ||||||
|  | objectStorageUseProxyDesc: "Turn off this if you are not going to use Proxy for ObjectStorage connection" | ||||||
| serverLogs: "Server logs" | serverLogs: "Server logs" | ||||||
| deleteAll: "Delete all" | deleteAll: "Delete all" | ||||||
| showFixedPostForm: "Display the posting form at the top of the timeline" | showFixedPostForm: "Display the posting form at the top of the timeline" | ||||||
| @@ -476,6 +480,18 @@ state: "State" | |||||||
| sort: "Sort" | sort: "Sort" | ||||||
| ascendingOrder: "Ascending" | ascendingOrder: "Ascending" | ||||||
| descendingOrder: "Descending" | descendingOrder: "Descending" | ||||||
|  | scratchpad: "Scratch pad" | ||||||
|  | scratchpadDescription: "Scratchpad provides experimental environment for AiScript. You can write, execute, and check the results that interact with Misskey." | ||||||
|  | output: "Output" | ||||||
|  | script: "Script" | ||||||
|  | disablePagesScript: "Disable AiScript on Pages" | ||||||
|  | updateRemoteUser: "Update remote user information" | ||||||
|  | deleteAllFiles: "Delete All Files" | ||||||
|  | deleteAllFilesConfirm: "Are you sure that you want to delete all files?" | ||||||
|  | removeAllFollowing: "Withhold All Followings" | ||||||
|  | removeAllFollowingDescription: "Unfollow all accounts from {host}. Please run this if the instance no longer exists." | ||||||
|  | userSuspended: "This user has been suspended." | ||||||
|  | userSilenced: "This user has been silenced." | ||||||
| _theme: | _theme: | ||||||
|   explore: "Explore Themes" |   explore: "Explore Themes" | ||||||
|   install: "Install theme" |   install: "Install theme" | ||||||
| @@ -516,7 +532,7 @@ _tutorial: | |||||||
|   step2_2: "Providing some information about who you are will make it easier for others to follow you back." |   step2_2: "Providing some information about who you are will make it easier for others to follow you back." | ||||||
|   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 the pencil icon on the screen." |   step3_2: "The next step is to post a note. You can do this by pressing the 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 top right to post." | ||||||
|   step3_4: "Have nothing to say? Try \"just setting up my msky\"!" |   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." | ||||||
| @@ -676,15 +692,15 @@ _charts: | |||||||
| _instanceCharts: | _instanceCharts: | ||||||
|   requests: "Requests" |   requests: "Requests" | ||||||
|   users: "Difference in # of users" |   users: "Difference in # of users" | ||||||
|   usersTotal: "Total # of users" |   usersTotal: "Cumulative total # of users" | ||||||
|   notes: "Difference in # of notes" |   notes: "Difference in # of notes" | ||||||
|   notesTotal: "Total # of notes" |   notesTotal: "Cumulative total # of notes" | ||||||
|   ff: "Difference in # of followers" |   ff: "Difference in # of followers" | ||||||
|   ffTotal: "Total # of followers" |   ffTotal: "Cumulative total # of followers" | ||||||
|   cacheSize: "Difference in cache size" |   cacheSize: "Difference in cache size" | ||||||
|   cacheSizeTotal: "Total cache size" |   cacheSizeTotal: "Cumulative total cache size" | ||||||
|   files: "Difference in # of files" |   files: "Difference in # of files" | ||||||
|   filesTotal: "Total # of files" |   filesTotal: "Cumulative total # of files" | ||||||
| _timelines: | _timelines: | ||||||
|   home: "Home" |   home: "Home" | ||||||
|   local: "Local" |   local: "Local" | ||||||
| @@ -754,6 +770,11 @@ _pages: | |||||||
|       name: "Variable name" |       name: "Variable name" | ||||||
|       text: "Title" |       text: "Title" | ||||||
|       default: "Default value" |       default: "Default value" | ||||||
|  |     canvas: "Canvas" | ||||||
|  |     _canvas: | ||||||
|  |       id: "Canvas ID" | ||||||
|  |       width: "Width" | ||||||
|  |       height: "Height" | ||||||
|     switch: "Switch" |     switch: "Switch" | ||||||
|     _switch: |     _switch: | ||||||
|       name: "Variable name" |       name: "Variable name" | ||||||
| @@ -779,6 +800,9 @@ _pages: | |||||||
|           message: "Message to display when activated" |           message: "Message to display when activated" | ||||||
|           variable: "Variable to send" |           variable: "Variable to send" | ||||||
|           no-variable: "None" |           no-variable: "None" | ||||||
|  |         callAiScript: "Invoke AiScript" | ||||||
|  |         _callAiScript: | ||||||
|  |           functionName: "Function name" | ||||||
|     radioButton: "Choice" |     radioButton: "Choice" | ||||||
|     _radioButton: |     _radioButton: | ||||||
|       name: "Variable name" |       name: "Variable name" | ||||||
| @@ -939,6 +963,7 @@ _pages: | |||||||
|       _splitStrByLine: |       _splitStrByLine: | ||||||
|         arg1: "Text" |         arg1: "Text" | ||||||
|       ref: "Variables" |       ref: "Variables" | ||||||
|  |       aiScriptVar: "Variable of AiScript" | ||||||
|       fn: "Functions" |       fn: "Functions" | ||||||
|       _fn: |       _fn: | ||||||
|         slots: "Slots" |         slots: "Slots" | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ lists: "Listas" | |||||||
| noLists: "No tiene listas" | noLists: "No tiene listas" | ||||||
| note: "Notas" | note: "Notas" | ||||||
| notes: "Notas" | notes: "Notas" | ||||||
| following: "Sigue" | following: "Siguiendo" | ||||||
| followers: "Seguidores" | followers: "Seguidores" | ||||||
| followsYou: "Te sigue" | followsYou: "Te sigue" | ||||||
| createList: "Crear lista" | createList: "Crear lista" | ||||||
| @@ -265,6 +265,7 @@ watch: "Ver" | |||||||
| unwatch: "Dejar de ver" | unwatch: "Dejar de ver" | ||||||
| accept: "Aceptar" | accept: "Aceptar" | ||||||
| reject: "Rechazar" | reject: "Rechazar" | ||||||
|  | normal: "Normal" | ||||||
| instanceName: "Nombre de la instancia" | instanceName: "Nombre de la instancia" | ||||||
| instanceDescription: "Descripción de la instancia" | instanceDescription: "Descripción de la instancia" | ||||||
| maintainerName: "Nombre del administrador" | maintainerName: "Nombre del administrador" | ||||||
| @@ -319,6 +320,7 @@ notesAndReplies: "Notas y respuestas" | |||||||
| withFiles: "Adjuntos" | withFiles: "Adjuntos" | ||||||
| silence: "Silenciar" | silence: "Silenciar" | ||||||
| silenceConfirm: "¿Desea silenciar al usuario?" | silenceConfirm: "¿Desea silenciar al usuario?" | ||||||
|  | unsilence: "Dejar de silenciar" | ||||||
| unsilenceConfirm: "¿Desea dejar de silenciar al usuario?" | unsilenceConfirm: "¿Desea dejar de silenciar al usuario?" | ||||||
| popularUsers: "Usuarios populares" | popularUsers: "Usuarios populares" | ||||||
| recentlyUpdatedUsers: "Usuarios activos recientemente" | recentlyUpdatedUsers: "Usuarios activos recientemente" | ||||||
| @@ -454,6 +456,8 @@ objectStorageRegion: "Region" | |||||||
| objectStorageRegionDesc: "Especifique una región como 'xx-east-1'. Si su servicio no tiene distinción sobre regiones, déjelo en blanco o complete con 'us-east-1'." | objectStorageRegionDesc: "Especifique una región como 'xx-east-1'. Si su servicio no tiene distinción sobre regiones, déjelo en blanco o complete con 'us-east-1'." | ||||||
| objectStorageUseSSL: "Usar SSL" | objectStorageUseSSL: "Usar SSL" | ||||||
| objectStorageUseSSLDesc: "Desactive esto si no va a usar HTTPS para la conexión API" | objectStorageUseSSLDesc: "Desactive esto si no va a usar HTTPS para la conexión API" | ||||||
|  | objectStorageUseProxy: "Conectarse a través de Proxy" | ||||||
|  | objectStorageUseProxyDesc: "Desactive esto si no va a usar Proxy para la conexión de Almacenamiento de objetos" | ||||||
| serverLogs: "Registros del servidor" | serverLogs: "Registros del servidor" | ||||||
| deleteAll: "Eliminar todos" | deleteAll: "Eliminar todos" | ||||||
| showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo" | showFixedPostForm: "Mostrar el formulario de las entradas encima de la línea de tiempo" | ||||||
| @@ -472,6 +476,22 @@ installedApps: "Aplicaciones Autorizadas" | |||||||
| nothing: "No hay nada que ver aqui" | nothing: "No hay nada que ver aqui" | ||||||
| installedDate: "Autorizado" | installedDate: "Autorizado" | ||||||
| lastUsedDate: "Utilizado el" | lastUsedDate: "Utilizado el" | ||||||
|  | state: "Estado" | ||||||
|  | sort: "Ordenar" | ||||||
|  | ascendingOrder: "Ascendente" | ||||||
|  | descendingOrder: "Descendente" | ||||||
|  | scratchpad: "Scratch pad" | ||||||
|  | scratchpadDescription: "Scratchpad proporciona un entorno experimental para AiScript. Puede escribir, ejecutar y verificar los resultados que interactúan con Misskey." | ||||||
|  | output: "Salida" | ||||||
|  | script: "Script" | ||||||
|  | disablePagesScript: "Deshabilitar AiScript en Páginas" | ||||||
|  | updateRemoteUser: "Actualizar información de usuario remoto" | ||||||
|  | deleteAllFiles: "Borrar todos los archivos" | ||||||
|  | deleteAllFilesConfirm: "¿Desea borrar todos los archivos?" | ||||||
|  | removeAllFollowing: "Retener todos los siguientes" | ||||||
|  | removeAllFollowingDescription: "Cancelar todos los siguientes del servidor {host}. Ejecutar en caso de que esta instancia haya dejado de existir" | ||||||
|  | userSuspended: "Este usuario ha sido suspendido." | ||||||
|  | userSilenced: "Este usuario ha sido silenciado." | ||||||
| _theme: | _theme: | ||||||
|   explore: "Explorar temas" |   explore: "Explorar temas" | ||||||
|   install: "Instalar tema" |   install: "Instalar tema" | ||||||
| @@ -672,10 +692,15 @@ _charts: | |||||||
| _instanceCharts: | _instanceCharts: | ||||||
|   requests: "Pedidos" |   requests: "Pedidos" | ||||||
|   users: "Variación de usuarios" |   users: "Variación de usuarios" | ||||||
|  |   usersTotal: "Total acumulado de usuarios" | ||||||
|   notes: "Variación de la cantidad de notas" |   notes: "Variación de la cantidad de notas" | ||||||
|  |   notesTotal: "Total acumulado de la cantidad de notas" | ||||||
|   ff: "Variación de cantidad de seguidos/seguidores" |   ff: "Variación de cantidad de seguidos/seguidores" | ||||||
|  |   ffTotal: "Total acumulado de cantidad de seguidos/seguidores" | ||||||
|   cacheSize: "Variación del tamaño de la caché" |   cacheSize: "Variación del tamaño de la caché" | ||||||
|  |   cacheSizeTotal: "Total acumulado del tamaño de la caché" | ||||||
|   files: "Variación de cantidad de archivos" |   files: "Variación de cantidad de archivos" | ||||||
|  |   filesTotal: "Total acumulado de cantidad de archivos" | ||||||
| _timelines: | _timelines: | ||||||
|   home: "Inicio" |   home: "Inicio" | ||||||
|   local: "Local" |   local: "Local" | ||||||
| @@ -745,6 +770,11 @@ _pages: | |||||||
|       name: "Nombre de variable" |       name: "Nombre de variable" | ||||||
|       text: "Título" |       text: "Título" | ||||||
|       default: "Valor predeterminado" |       default: "Valor predeterminado" | ||||||
|  |     canvas: "Lienzo" | ||||||
|  |     _canvas: | ||||||
|  |       id: "Lienzo ID" | ||||||
|  |       width: "Ancho" | ||||||
|  |       height: "Altura" | ||||||
|     switch: "Interruptor" |     switch: "Interruptor" | ||||||
|     _switch: |     _switch: | ||||||
|       name: "Nombre de variable" |       name: "Nombre de variable" | ||||||
| @@ -770,6 +800,9 @@ _pages: | |||||||
|           message: "Mensaje mostrado al apretar" |           message: "Mensaje mostrado al apretar" | ||||||
|           variable: "Variable a enviar" |           variable: "Variable a enviar" | ||||||
|           no-variable: "Ninguna" |           no-variable: "Ninguna" | ||||||
|  |         callAiScript: "Invocar AiScript" | ||||||
|  |         _callAiScript: | ||||||
|  |           functionName: "Nombre de la función" | ||||||
|     radioButton: "Botón de opción" |     radioButton: "Botón de opción" | ||||||
|     _radioButton: |     _radioButton: | ||||||
|       name: "Nombre de variable" |       name: "Nombre de variable" | ||||||
| @@ -930,6 +963,7 @@ _pages: | |||||||
|       _splitStrByLine: |       _splitStrByLine: | ||||||
|         arg1: "Texto" |         arg1: "Texto" | ||||||
|       ref: "Variables" |       ref: "Variables" | ||||||
|  |       aiScriptVar: "Variable de AiScript" | ||||||
|       fn: "funciones" |       fn: "funciones" | ||||||
|       _fn: |       _fn: | ||||||
|         slots: "Slots" |         slots: "Slots" | ||||||
|   | |||||||
| @@ -265,6 +265,7 @@ watch: "Surveiller" | |||||||
| unwatch: "Ne plus surveiller" | unwatch: "Ne plus surveiller" | ||||||
| accept: "Autoriser" | accept: "Autoriser" | ||||||
| reject: "Refuser" | reject: "Refuser" | ||||||
|  | normal: "Normal" | ||||||
| instanceName: "Nom de l’instance" | instanceName: "Nom de l’instance" | ||||||
| instanceDescription: "Description de l’instance" | instanceDescription: "Description de l’instance" | ||||||
| maintainerName: "Nom d'administrateur" | maintainerName: "Nom d'administrateur" | ||||||
| @@ -319,6 +320,7 @@ notesAndReplies: "Notes et Répondres" | |||||||
| withFiles: "Avec fichiers joints" | withFiles: "Avec fichiers joints" | ||||||
| silence: "Mettre en masquer" | silence: "Mettre en masquer" | ||||||
| silenceConfirm: "Mettre l'utilisateur sous masquer ?" | silenceConfirm: "Mettre l'utilisateur sous masquer ?" | ||||||
|  | unsilence: "Annuler la masquer" | ||||||
| unsilenceConfirm: "Voulez-vous annuler le masquer ?" | unsilenceConfirm: "Voulez-vous annuler le masquer ?" | ||||||
| popularUsers: "Utilisateur·rice·s populaires" | popularUsers: "Utilisateur·rice·s populaires" | ||||||
| recentlyUpdatedUsers: "Utilisateur·rice·s actif·ve·s récemment" | recentlyUpdatedUsers: "Utilisateur·rice·s actif·ve·s récemment" | ||||||
| @@ -454,6 +456,8 @@ objectStorageRegion: "Region" | |||||||
| objectStorageRegionDesc: "Spécifiez une région comme 'xx-east-1'. Si votre service ne fait pas de distinction entre les régions, laissez-le vide ou remplissez 'us-east-1'." | objectStorageRegionDesc: "Spécifiez une région comme 'xx-east-1'. Si votre service ne fait pas de distinction entre les régions, laissez-le vide ou remplissez 'us-east-1'." | ||||||
| objectStorageUseSSL: "Utiliser SSL" | objectStorageUseSSL: "Utiliser SSL" | ||||||
| objectStorageUseSSLDesc: "Désactivez-le si vous n'utilisez pas HTTPS pour la connexion API" | objectStorageUseSSLDesc: "Désactivez-le si vous n'utilisez pas HTTPS pour la connexion API" | ||||||
|  | objectStorageUseProxy: "Se connecter via proxy" | ||||||
|  | objectStorageUseProxyDesc: "Désactivez-le si vous n'utilisez pas Proxy pour la connexion de stockage d'objets" | ||||||
| serverLogs: "Journaux serveur" | serverLogs: "Journaux serveur" | ||||||
| deleteAll: "Supprimer tout" | deleteAll: "Supprimer tout" | ||||||
| showFixedPostForm: "Afficher le formulaire en haut du fil d'actualité" | showFixedPostForm: "Afficher le formulaire en haut du fil d'actualité" | ||||||
| @@ -472,6 +476,22 @@ installedApps: "Applications Autorisées" | |||||||
| nothing: "Il n'y a rien à voir ici" | nothing: "Il n'y a rien à voir ici" | ||||||
| installedDate: "Autorisé" | installedDate: "Autorisé" | ||||||
| lastUsedDate: "Dernière utilisation" | lastUsedDate: "Dernière utilisation" | ||||||
|  | state: "État" | ||||||
|  | sort: "Trier" | ||||||
|  | ascendingOrder: "Ascendant" | ||||||
|  | descendingOrder: "Descendant" | ||||||
|  | scratchpad: "Scratch pad" | ||||||
|  | scratchpadDescription: "Scratchpad fournit un environnement expérimental pour AiScript. Vous pouvez écrire, exécuter et vérifier les résultats qui interagissent avec Misskey." | ||||||
|  | output: "Sortie" | ||||||
|  | script: "Script" | ||||||
|  | disablePagesScript: "Désactiver AiScript sur les Pages" | ||||||
|  | updateRemoteUser: "Mettre à jour les informations de l’utilisateur·rice distant·e" | ||||||
|  | deleteAllFiles: "Supprimer tous les fichiers" | ||||||
|  | deleteAllFilesConfirm: "Êtes vous surs de vouloir supprimer tous les fichiers ?" | ||||||
|  | removeAllFollowing: "Retenir tous les abonnements" | ||||||
|  | removeAllFollowingDescription: "Se désabonner de tous les comptes de {host}. Exécutez cette commande si l'instance n'existe plus." | ||||||
|  | userSuspended: "Cette utilisateur·trice a été suspendue." | ||||||
|  | userSilenced: "Cette utilisateur·trice a été masquer." | ||||||
| _theme: | _theme: | ||||||
|   explore: "Explorer les thèmes" |   explore: "Explorer les thèmes" | ||||||
|   install: "Installer un thème" |   install: "Installer un thème" | ||||||
| @@ -659,7 +679,7 @@ _charts: | |||||||
|   federationInstancesIncDec: "Variation du nombre d'instances" |   federationInstancesIncDec: "Variation du nombre d'instances" | ||||||
|   federationInstancesTotal: "Nombre d'instances au total" |   federationInstancesTotal: "Nombre d'instances au total" | ||||||
|   usersIncDec: "Variation du nombre d'utilisateur·rice·s" |   usersIncDec: "Variation du nombre d'utilisateur·rice·s" | ||||||
|   usersTotal: "Nombre d'utilsateur·rice·s au total" |   usersTotal: "Nombre d'utilisateur·rice·s au total" | ||||||
|   activeUsers: "Utilisateur·rice·s actif·ve·s" |   activeUsers: "Utilisateur·rice·s actif·ve·s" | ||||||
|   notesIncDec: "Variation du nombre d'notes" |   notesIncDec: "Variation du nombre d'notes" | ||||||
|   localNotesIncDec: "Variation du nombre de notes local" |   localNotesIncDec: "Variation du nombre de notes local" | ||||||
| @@ -672,10 +692,15 @@ _charts: | |||||||
| _instanceCharts: | _instanceCharts: | ||||||
|   requests: "Requêtes" |   requests: "Requêtes" | ||||||
|   users: "Variation du nombre d'utilisateur·rice·s" |   users: "Variation du nombre d'utilisateur·rice·s" | ||||||
|  |   usersTotal: "Nombre d'utilisateur·rice·s au total cumulé" | ||||||
|   notes: "Variation du nombre d'notes" |   notes: "Variation du nombre d'notes" | ||||||
|  |   notesTotal: "Nombre d'notes au total cumulé" | ||||||
|   ff: "Variation des abonné·e·s" |   ff: "Variation des abonné·e·s" | ||||||
|  |   ffTotal: "Nombre d'abonné·e·s au total cumulé" | ||||||
|   cacheSize: "Variation de la taille du cache" |   cacheSize: "Variation de la taille du cache" | ||||||
|  |   cacheSizeTotal: "La taille du cache au total cumulé" | ||||||
|   files: "Variation du nombre de fichiers" |   files: "Variation du nombre de fichiers" | ||||||
|  |   filesTotal: "Nombre de fichiers au total cumulé" | ||||||
| _timelines: | _timelines: | ||||||
|   home: "Principal" |   home: "Principal" | ||||||
|   local: "Local" |   local: "Local" | ||||||
| @@ -745,6 +770,11 @@ _pages: | |||||||
|       name: "Nom de la variable" |       name: "Nom de la variable" | ||||||
|       text: "Titre" |       text: "Titre" | ||||||
|       default: "Valeur par défaut" |       default: "Valeur par défaut" | ||||||
|  |     canvas: "Toile" | ||||||
|  |     _canvas: | ||||||
|  |       id: "Toile ID" | ||||||
|  |       width: "Largeur" | ||||||
|  |       height: "Hauteur" | ||||||
|     switch: "Basculer" |     switch: "Basculer" | ||||||
|     _switch: |     _switch: | ||||||
|       name: "Nom de la variable" |       name: "Nom de la variable" | ||||||
| @@ -770,6 +800,9 @@ _pages: | |||||||
|           message: "Message à afficher lorsque appuyé" |           message: "Message à afficher lorsque appuyé" | ||||||
|           variable: "Variable à envoyer" |           variable: "Variable à envoyer" | ||||||
|           no-variable: "Rien" |           no-variable: "Rien" | ||||||
|  |         callAiScript: "Appeler AiScript" | ||||||
|  |         _callAiScript: | ||||||
|  |           functionName: "Nom de la fonction" | ||||||
|     radioButton: "Choix" |     radioButton: "Choix" | ||||||
|     _radioButton: |     _radioButton: | ||||||
|       name: "Nom de la variable" |       name: "Nom de la variable" | ||||||
| @@ -930,6 +963,7 @@ _pages: | |||||||
|       _splitStrByLine: |       _splitStrByLine: | ||||||
|         arg1: "Texte" |         arg1: "Texte" | ||||||
|       ref: "Variables" |       ref: "Variables" | ||||||
|  |       aiScriptVar: "Variable d'AiScript" | ||||||
|       fn: "Fonction" |       fn: "Fonction" | ||||||
|       _fn: |       _fn: | ||||||
|         slots: "Slots" |         slots: "Slots" | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ const merge = (...args) => args.reduce((a, c) => ({ | |||||||
| const languages = [ | const languages = [ | ||||||
| 	//'cs-CZ', | 	//'cs-CZ', | ||||||
| 	//'da-DK', | 	//'da-DK', | ||||||
| 	//'de-DE', | 	'de-DE', | ||||||
| 	'en-US', | 	'en-US', | ||||||
| 	'es-ES', | 	'es-ES', | ||||||
| 	'fr-FR', | 	'fr-FR', | ||||||
| @@ -26,7 +26,7 @@ const languages = [ | |||||||
| 	//'nl-NL', | 	//'nl-NL', | ||||||
| 	//'pl-PL', | 	//'pl-PL', | ||||||
| 	'zh-CN', | 	'zh-CN', | ||||||
| 	//'zh-TW', | 	'zh-TW', | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| const primaries = { | const primaries = { | ||||||
|   | |||||||
| @@ -265,6 +265,7 @@ watch: "ウォッチ" | |||||||
| unwatch: "ウォッチ解除" | unwatch: "ウォッチ解除" | ||||||
| accept: "許可" | accept: "許可" | ||||||
| reject: "拒否" | reject: "拒否" | ||||||
|  | normal: "正常" | ||||||
| instanceName: "インスタンス名" | instanceName: "インスタンス名" | ||||||
| instanceDescription: "インスタンスの紹介" | instanceDescription: "インスタンスの紹介" | ||||||
| maintainerName: "管理者の名前" | maintainerName: "管理者の名前" | ||||||
| @@ -319,6 +320,7 @@ notesAndReplies: "投稿と返信" | |||||||
| withFiles: "ファイル付き" | withFiles: "ファイル付き" | ||||||
| silence: "サイレンス" | silence: "サイレンス" | ||||||
| silenceConfirm: "サイレンスしますか?" | silenceConfirm: "サイレンスしますか?" | ||||||
|  | unsilence: "サイレンス解除" | ||||||
| unsilenceConfirm: "サイレンス解除しますか?" | unsilenceConfirm: "サイレンス解除しますか?" | ||||||
| popularUsers: "人気のユーザー" | popularUsers: "人気のユーザー" | ||||||
| recentlyUpdatedUsers: "最近投稿したユーザー" | recentlyUpdatedUsers: "最近投稿したユーザー" | ||||||
| @@ -454,6 +456,8 @@ objectStorageRegion: "Region" | |||||||
| objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。" | objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。" | ||||||
| objectStorageUseSSL: "SSLを使用する" | objectStorageUseSSL: "SSLを使用する" | ||||||
| objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください" | objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください" | ||||||
|  | objectStorageUseProxy: "Proxyを利用する" | ||||||
|  | objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください" | ||||||
| serverLogs: "サーバーログ" | serverLogs: "サーバーログ" | ||||||
| deleteAll: "全て削除" | deleteAll: "全て削除" | ||||||
| showFixedPostForm: "タイムライン上部に投稿フォームを表示する" | showFixedPostForm: "タイムライン上部に投稿フォームを表示する" | ||||||
| @@ -476,6 +480,18 @@ state: "状態" | |||||||
| sort: "ソート" | sort: "ソート" | ||||||
| ascendingOrder: "昇順" | ascendingOrder: "昇順" | ||||||
| descendingOrder: "降順" | descendingOrder: "降順" | ||||||
|  | scratchpad: "スクラッチパッド" | ||||||
|  | scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。" | ||||||
|  | output: "出力" | ||||||
|  | script: "スクリプト" | ||||||
|  | disablePagesScript: "Pagesのスクリプトを無効にする" | ||||||
|  | updateRemoteUser: "リモートユーザー情報の更新" | ||||||
|  | deleteAllFiles: "すべてのファイルを削除" | ||||||
|  | deleteAllFilesConfirm: "すべてのファイルを削除しますか?" | ||||||
|  | removeAllFollowing: "フォローを全解除" | ||||||
|  | removeAllFollowingDescription: "{host}からのフォローをすべて解除します。そのインスタンスがもう存在しなくなった場合などに実行してください。" | ||||||
|  | userSuspended: "このユーザーは凍結されています。" | ||||||
|  | userSilenced: "このユーザーはサイレンスされています。" | ||||||
|  |  | ||||||
| _theme: | _theme: | ||||||
|   explore: "テーマを探す" |   explore: "テーマを探す" | ||||||
| @@ -781,6 +797,12 @@ _pages: | |||||||
|       text: "タイトル" |       text: "タイトル" | ||||||
|       default: "デフォルト値" |       default: "デフォルト値" | ||||||
|  |  | ||||||
|  |     canvas: "キャンバス" | ||||||
|  |     _canvas: | ||||||
|  |       id: "キャンバスID" | ||||||
|  |       width: "幅" | ||||||
|  |       height: "高さ" | ||||||
|  |  | ||||||
|     switch: "スイッチ" |     switch: "スイッチ" | ||||||
|     _switch: |     _switch: | ||||||
|       name: "変数名" |       name: "変数名" | ||||||
| @@ -808,6 +830,9 @@ _pages: | |||||||
|           message: "押したときに表示するメッセージ" |           message: "押したときに表示するメッセージ" | ||||||
|           variable: "送信する変数" |           variable: "送信する変数" | ||||||
|           no-variable: "なし" |           no-variable: "なし" | ||||||
|  |         callAiScript: "AiScript呼び出し" | ||||||
|  |         _callAiScript: | ||||||
|  |           functionName: "関数名" | ||||||
|  |  | ||||||
|     radioButton: "選択肢" |     radioButton: "選択肢" | ||||||
|     _radioButton: |     _radioButton: | ||||||
| @@ -970,6 +995,7 @@ _pages: | |||||||
|       _splitStrByLine: |       _splitStrByLine: | ||||||
|         arg1: "テキスト" |         arg1: "テキスト" | ||||||
|       ref: "変数" |       ref: "変数" | ||||||
|  |       aiScriptVar: "AiScript変数" | ||||||
|       fn: "関数" |       fn: "関数" | ||||||
|       _fn: |       _fn: | ||||||
|         slots: "スロット" |         slots: "スロット" | ||||||
|   | |||||||
| @@ -265,6 +265,7 @@ watch: "지켜보기" | |||||||
| unwatch: "지켜보기 해제" | unwatch: "지켜보기 해제" | ||||||
| accept: "허가" | accept: "허가" | ||||||
| reject: "거부" | reject: "거부" | ||||||
|  | normal: "정상" | ||||||
| instanceName: "인스턴스 이름" | instanceName: "인스턴스 이름" | ||||||
| instanceDescription: "인스턴스 소개" | instanceDescription: "인스턴스 소개" | ||||||
| maintainerName: "관리자 이름" | maintainerName: "관리자 이름" | ||||||
| @@ -319,6 +320,7 @@ notesAndReplies: "글과 답글" | |||||||
| withFiles: "미디어" | withFiles: "미디어" | ||||||
| silence: "사일런스" | silence: "사일런스" | ||||||
| silenceConfirm: "이 계정을 사일런스로 설정하시겠습니까?" | silenceConfirm: "이 계정을 사일런스로 설정하시겠습니까?" | ||||||
|  | unsilence: "사일런스 해제" | ||||||
| unsilenceConfirm: "이 계정의 사일런스를 해제하시겠습니까?" | unsilenceConfirm: "이 계정의 사일런스를 해제하시겠습니까?" | ||||||
| popularUsers: "인기 유저" | popularUsers: "인기 유저" | ||||||
| recentlyUpdatedUsers: "최근 활동한 유저" | recentlyUpdatedUsers: "최근 활동한 유저" | ||||||
| @@ -454,6 +456,8 @@ objectStorageRegion: "Region" | |||||||
| objectStorageRegionDesc: "'xx-east-1'와 같이 region을 지정해주세요. 사용하는 서비스에 region 개념이 없는 경우, 비워 두거나 'us-east-1'으로 설정해 주세요." | objectStorageRegionDesc: "'xx-east-1'와 같이 region을 지정해주세요. 사용하는 서비스에 region 개념이 없는 경우, 비워 두거나 'us-east-1'으로 설정해 주세요." | ||||||
| objectStorageUseSSL: "SSL 사용" | objectStorageUseSSL: "SSL 사용" | ||||||
| objectStorageUseSSLDesc: "API 호출시 HTTPS 를 사용하지 않는 경우 OFF 로 설정해 주세요" | objectStorageUseSSLDesc: "API 호출시 HTTPS 를 사용하지 않는 경우 OFF 로 설정해 주세요" | ||||||
|  | objectStorageUseProxy: "연결에 프록시를 사용" | ||||||
|  | objectStorageUseProxyDesc: "오브젝트 스토리지 API 호출시 프록시를 사용하지 않는 경우 OFF 로 설정해 주세요" | ||||||
| serverLogs: "서버 로그" | serverLogs: "서버 로그" | ||||||
| deleteAll: "모두 삭제" | deleteAll: "모두 삭제" | ||||||
| showFixedPostForm: "타임라인 상단에 글 작성란을 표시" | showFixedPostForm: "타임라인 상단에 글 작성란을 표시" | ||||||
| @@ -476,6 +480,18 @@ state: "상태" | |||||||
| sort: "정렬" | sort: "정렬" | ||||||
| ascendingOrder: "오름차순" | ascendingOrder: "오름차순" | ||||||
| descendingOrder: "내림차순" | descendingOrder: "내림차순" | ||||||
|  | scratchpad: "스크래치 패드" | ||||||
|  | scratchpadDescription: "스크래치 패드는 AiScript 의 테스트 환경을 제공합니다. Misskey 와 상호 작용하는 코드를 작성, 실행 및 결과를 확인할 수 있습니다." | ||||||
|  | output: "출력" | ||||||
|  | script: "스크립트" | ||||||
|  | disablePagesScript: "Pages 에서 AiScript 를 사용하지 않음" | ||||||
|  | updateRemoteUser: "리모트 유저 정보 갱신" | ||||||
|  | deleteAllFiles: "모든 파일 삭제" | ||||||
|  | deleteAllFilesConfirm: "모든 파일을 삭제하시겠습니까?" | ||||||
|  | removeAllFollowing: "모든 팔로잉 해제" | ||||||
|  | removeAllFollowingDescription: "{host}(으)로부터 모든 팔로잉을 해제합니다. 해당 인스턴스가 더 이상 존재하지 않게 된 경우 등에 실행해 주세요." | ||||||
|  | userSuspended: "이 계정은 정지된 상태입니다." | ||||||
|  | userSilenced: "이 계정은 사일런스된 상태입니다." | ||||||
| _theme: | _theme: | ||||||
|   explore: "테마 찾아보기" |   explore: "테마 찾아보기" | ||||||
|   install: "테마 설치" |   install: "테마 설치" | ||||||
| @@ -661,25 +677,30 @@ _exportOrImport: | |||||||
|   userLists: "리스트" |   userLists: "리스트" | ||||||
| _charts: | _charts: | ||||||
|   federationInstancesIncDec: "연합 인스턴스 수 증감" |   federationInstancesIncDec: "연합 인스턴스 수 증감" | ||||||
|   federationInstancesTotal: "총 연합 인스턴스 수" |   federationInstancesTotal: "연합 인스턴스 수 합계" | ||||||
|   usersIncDec: "유저 수 증감" |   usersIncDec: "유저 수 증감" | ||||||
|   usersTotal: "유저 수 합계" |   usersTotal: "유저 수 합계" | ||||||
|   activeUsers: "활성 유저 수" |   activeUsers: "활성 유저 수" | ||||||
|   notesIncDec: "노트 수 증감" |   notesIncDec: "노트 수 증감" | ||||||
|   localNotesIncDec: "로컬 노트 수 증감" |   localNotesIncDec: "로컬 노트 수 증감" | ||||||
|   remoteNotesIncDec: "리모트 노트 수 증감" |   remoteNotesIncDec: "리모트 노트 수 증감" | ||||||
|   notesTotal: "총 노트 수" |   notesTotal: "노트 수 합계" | ||||||
|   filesIncDec: "파일 수 증감" |   filesIncDec: "파일 수 증감" | ||||||
|   filesTotal: "총 파일 수" |   filesTotal: "파일 수 합계" | ||||||
|   storageUsageIncDec: "스토리지 사용량 증감" |   storageUsageIncDec: "스토리지 사용량 증감" | ||||||
|   storageUsageTotal: "총 스토리지 사용량" |   storageUsageTotal: "스토리지 사용량 합계" | ||||||
| _instanceCharts: | _instanceCharts: | ||||||
|   requests: "요청" |   requests: "요청" | ||||||
|   users: "유저 수 증감" |   users: "유저 수 증감" | ||||||
|  |   usersTotal: "누적 유저 수" | ||||||
|   notes: "노트 수 증감" |   notes: "노트 수 증감" | ||||||
|  |   notesTotal: "누적 노트 수" | ||||||
|   ff: "팔로잉/팔로워 증감" |   ff: "팔로잉/팔로워 증감" | ||||||
|  |   ffTotal: "누적 팔로잉/팔로워 수" | ||||||
|   cacheSize: "캐시 용량 증감" |   cacheSize: "캐시 용량 증감" | ||||||
|  |   cacheSizeTotal: "누적 캐시 용량" | ||||||
|   files: "파일 수 증감" |   files: "파일 수 증감" | ||||||
|  |   filesTotal: "누적 파일 수" | ||||||
| _timelines: | _timelines: | ||||||
|   home: "홈" |   home: "홈" | ||||||
|   local: "로컬" |   local: "로컬" | ||||||
| @@ -749,6 +770,11 @@ _pages: | |||||||
|       name: "변수명" |       name: "변수명" | ||||||
|       text: "제목" |       text: "제목" | ||||||
|       default: "기본값" |       default: "기본값" | ||||||
|  |     canvas: "캔버스" | ||||||
|  |     _canvas: | ||||||
|  |       id: "캔버스 ID" | ||||||
|  |       width: "폭" | ||||||
|  |       height: "높이" | ||||||
|     switch: "스위치" |     switch: "스위치" | ||||||
|     _switch: |     _switch: | ||||||
|       name: "변수명" |       name: "변수명" | ||||||
| @@ -774,6 +800,9 @@ _pages: | |||||||
|           message: "눌렀을 때 표시할 페이지" |           message: "눌렀을 때 표시할 페이지" | ||||||
|           variable: "보낼 변수" |           variable: "보낼 변수" | ||||||
|           no-variable: "없음" |           no-variable: "없음" | ||||||
|  |         callAiScript: "AiScript 호출" | ||||||
|  |         _callAiScript: | ||||||
|  |           functionName: "함수명" | ||||||
|     radioButton: "선택지" |     radioButton: "선택지" | ||||||
|     _radioButton: |     _radioButton: | ||||||
|       name: "변수명" |       name: "변수명" | ||||||
| @@ -934,6 +963,7 @@ _pages: | |||||||
|       _splitStrByLine: |       _splitStrByLine: | ||||||
|         arg1: "텍스트" |         arg1: "텍스트" | ||||||
|       ref: "변수" |       ref: "변수" | ||||||
|  |       aiScriptVar: "AiScript 변수" | ||||||
|       fn: "함수" |       fn: "함수" | ||||||
|       _fn: |       _fn: | ||||||
|         slots: "슬롯" |         slots: "슬롯" | ||||||
|   | |||||||
| @@ -230,8 +230,8 @@ location: "位置" | |||||||
| theme: "主题" | theme: "主题" | ||||||
| themeForLightMode: "在轻便模式下使用的主题" | themeForLightMode: "在轻便模式下使用的主题" | ||||||
| themeForDarkMode: "在黑暗模式下使用的主题" | themeForDarkMode: "在黑暗模式下使用的主题" | ||||||
| light: "轻便" | light: "浅色" | ||||||
| dark: "黑暗" | dark: "深色" | ||||||
| lightThemes: "亮色主题" | lightThemes: "亮色主题" | ||||||
| darkThemes: "暗色主题" | darkThemes: "暗色主题" | ||||||
| syncDeviceDarkMode: "将黑暗模式与设备设置同步" | syncDeviceDarkMode: "将黑暗模式与设备设置同步" | ||||||
| @@ -476,6 +476,7 @@ state: "状态" | |||||||
| sort: "排序" | sort: "排序" | ||||||
| ascendingOrder: "升序" | ascendingOrder: "升序" | ||||||
| descendingOrder: "降序" | descendingOrder: "降序" | ||||||
|  | output: "输出" | ||||||
| _theme: | _theme: | ||||||
|   explore: "寻找主题" |   explore: "寻找主题" | ||||||
|   install: "安装主题" |   install: "安装主题" | ||||||
| @@ -486,7 +487,7 @@ _theme: | |||||||
|   invalid: "主题格式错误" |   invalid: "主题格式错误" | ||||||
| _sfx: | _sfx: | ||||||
|   note: "帖子" |   note: "帖子" | ||||||
|   noteMy: "我的笔记" |   noteMy: "我的帖子" | ||||||
|   notification: "通知" |   notification: "通知" | ||||||
|   chat: "聊天" |   chat: "聊天" | ||||||
|   chatBg: "聊天背景" |   chatBg: "聊天背景" | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								migration/1585772678853-ap-url.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								migration/1585772678853-ap-url.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | /* tslint:disable:quotemark class-name indent */ | ||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  |  | ||||||
|  | export class apUrl1585772678853 implements MigrationInterface { | ||||||
|  |     name = 'apUrl1585772678853' | ||||||
|  |  | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<any> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "note" ADD "url" character varying(512)`, undefined); | ||||||
|  |      } | ||||||
|  |  | ||||||
|  |     public async down(queryRunner: QueryRunner): Promise<any> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "url"`, undefined); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								migration/1586624197029-AddObjectStorageUseProxy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								migration/1586624197029-AddObjectStorageUseProxy.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import {MigrationInterface, QueryRunner} from 'typeorm'; | ||||||
|  |  | ||||||
|  | export class AddObjectStorageUseProxy1586624197029 implements MigrationInterface { | ||||||
|  | 		name = 'AddObjectStorageUseProxy1586624197029' | ||||||
|  |  | ||||||
|  | 		public async up(queryRunner: QueryRunner): Promise<void> { | ||||||
|  | 				await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageUseProxy" boolean NOT NULL DEFAULT true`, undefined); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		public async down(queryRunner: QueryRunner): Promise<void> { | ||||||
|  | 				await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageUseProxy"`, undefined); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								migration/1586641139527-remote-reaction.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								migration/1586641139527-remote-reaction.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  |  | ||||||
|  | export class remoteReaction1586641139527 implements MigrationInterface { | ||||||
|  |     name = 'remoteReaction1586641139527' | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<any> { | ||||||
|  |       await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(260)`, undefined); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async down(queryRunner: QueryRunner): Promise<any> { | ||||||
|  |       await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(130)`, undefined); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								migration/1586708940386-pageAiScript.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								migration/1586708940386-pageAiScript.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  |  | ||||||
|  | export class pageAiScript1586708940386 implements MigrationInterface { | ||||||
|  |     name = 'pageAiScript1586708940386' | ||||||
|  |  | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "page" ADD "script" character varying(16384) NOT NULL DEFAULT ''`, undefined); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async down(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "script"`, undefined); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										133
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										133
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
| 	"name": "misskey", | 	"name": "misskey", | ||||||
| 	"author": "syuilo <syuilotan@yahoo.co.jp>", | 	"author": "syuilo <syuilotan@yahoo.co.jp>", | ||||||
| 	"version": "12.28.0", | 	"version": "12.33.0", | ||||||
| 	"codename": "indigo", | 	"codename": "indigo", | ||||||
| 	"repository": { | 	"repository": { | ||||||
| 		"type": "git", | 		"type": "git", | ||||||
| @@ -11,11 +11,13 @@ | |||||||
| 	"private": true, | 	"private": true, | ||||||
| 	"scripts": { | 	"scripts": { | ||||||
| 		"start": "node ./index.js", | 		"start": "node ./index.js", | ||||||
|  | 		"start-product": "cross-env NODE_ENV=production node ./index.js", | ||||||
| 		"init": "npm run migrate", | 		"init": "npm run migrate", | ||||||
| 		"ormconfig": "node ./built/ormconfig.js", | 		"ormconfig": "node ./built/ormconfig.js", | ||||||
| 		"migrate": "ts-node ./node_modules/typeorm/cli.js migration:run", | 		"migrate": "ts-node ./node_modules/typeorm/cli.js migration:run", | ||||||
| 		"migrateandstart": "npm run migrate && npm run start", | 		"migrateandstart": "npm run migrate && npm run start", | ||||||
| 		"build": "webpack && gulp build", | 		"build": "webpack && gulp build", | ||||||
|  | 		"build-product": "cross-env NODE_ENV=production webpack && gulp build", | ||||||
| 		"webpack": "webpack", | 		"webpack": "webpack", | ||||||
| 		"watch": "webpack --watch", | 		"watch": "webpack --watch", | ||||||
| 		"gulp": "gulp build", | 		"gulp": "gulp build", | ||||||
| @@ -30,18 +32,19 @@ | |||||||
| 		"lodash": "^4.17.13" | 		"lodash": "^4.17.13" | ||||||
| 	}, | 	}, | ||||||
| 	"dependencies": { | 	"dependencies": { | ||||||
| 		"@elastic/elasticsearch": "7.6.0", | 		"@elastic/elasticsearch": "7.6.1", | ||||||
| 		"@fortawesome/fontawesome-svg-core": "1.2.27", | 		"@fortawesome/fontawesome-svg-core": "1.2.28", | ||||||
| 		"@fortawesome/free-brands-svg-icons": "5.12.1", | 		"@fortawesome/free-brands-svg-icons": "5.13.0", | ||||||
| 		"@fortawesome/free-regular-svg-icons": "5.12.1", | 		"@fortawesome/free-regular-svg-icons": "5.13.0", | ||||||
| 		"@fortawesome/free-solid-svg-icons": "5.12.1", | 		"@fortawesome/free-solid-svg-icons": "5.13.0", | ||||||
| 		"@fortawesome/vue-fontawesome": "0.1.9", | 		"@fortawesome/vue-fontawesome": "0.1.9", | ||||||
| 		"@juggle/resize-observer": "3.0.2", | 		"@juggle/resize-observer": "3.1.3", | ||||||
| 		"@koa/cors": "3.0.0", | 		"@koa/cors": "3.0.0", | ||||||
| 		"@koa/multer": "2.0.2", | 		"@koa/multer": "2.0.2", | ||||||
| 		"@koa/router": "8.0.8", | 		"@koa/router": "8.0.8", | ||||||
|  | 		"@syuilo/aiscript": "0.3.1", | ||||||
| 		"@types/bcryptjs": "2.4.2", | 		"@types/bcryptjs": "2.4.2", | ||||||
| 		"@types/bull": "3.12.0", | 		"@types/bull": "3.12.1", | ||||||
| 		"@types/cbor": "5.0.0", | 		"@types/cbor": "5.0.0", | ||||||
| 		"@types/dateformat": "3.0.1", | 		"@types/dateformat": "3.0.1", | ||||||
| 		"@types/double-ended-queue": "2.1.1", | 		"@types/double-ended-queue": "2.1.1", | ||||||
| @@ -51,10 +54,10 @@ | |||||||
| 		"@types/gulp-rename": "0.0.33", | 		"@types/gulp-rename": "0.0.33", | ||||||
| 		"@types/gulp-replace": "0.0.31", | 		"@types/gulp-replace": "0.0.31", | ||||||
| 		"@types/is-url": "1.2.28", | 		"@types/is-url": "1.2.28", | ||||||
| 		"@types/js-yaml": "3.12.2", | 		"@types/js-yaml": "3.12.3", | ||||||
| 		"@types/jsdom": "12.2.4", | 		"@types/jsdom": "16.2.0", | ||||||
| 		"@types/katex": "0.11.0", | 		"@types/katex": "0.11.0", | ||||||
| 		"@types/koa": "2.11.1", | 		"@types/koa": "2.11.3", | ||||||
| 		"@types/koa-bodyparser": "4.3.0", | 		"@types/koa-bodyparser": "4.3.0", | ||||||
| 		"@types/koa-compress": "2.0.9", | 		"@types/koa-compress": "2.0.9", | ||||||
| 		"@types/koa-cors": "0.0.0", | 		"@types/koa-cors": "0.0.0", | ||||||
| @@ -68,8 +71,9 @@ | |||||||
| 		"@types/koa__router": "8.0.2", | 		"@types/koa__router": "8.0.2", | ||||||
| 		"@types/lolex": "5.1.0", | 		"@types/lolex": "5.1.0", | ||||||
| 		"@types/markdown-it": "0.0.9", | 		"@types/markdown-it": "0.0.9", | ||||||
| 		"@types/mocha": "7.0.1", | 		"@types/mocha": "7.0.2", | ||||||
| 		"@types/node": "13.7.1", | 		"@types/node": "13.11.0", | ||||||
|  | 		"@types/node-fetch": "2.5.5", | ||||||
| 		"@types/nodemailer": "6.4.0", | 		"@types/nodemailer": "6.4.0", | ||||||
| 		"@types/nprogress": "0.2.0", | 		"@types/nprogress": "0.2.0", | ||||||
| 		"@types/oauth": "0.9.1", | 		"@types/oauth": "0.9.1", | ||||||
| @@ -80,10 +84,8 @@ | |||||||
| 		"@types/qrcode": "1.3.4", | 		"@types/qrcode": "1.3.4", | ||||||
| 		"@types/random-seed": "0.3.3", | 		"@types/random-seed": "0.3.3", | ||||||
| 		"@types/ratelimiter": "2.1.28", | 		"@types/ratelimiter": "2.1.28", | ||||||
| 		"@types/redis": "2.8.15", | 		"@types/redis": "2.8.17", | ||||||
| 		"@types/rename": "1.0.1", | 		"@types/rename": "1.0.1", | ||||||
| 		"@types/request": "2.48.4", |  | ||||||
| 		"@types/request-promise-native": "1.0.17", |  | ||||||
| 		"@types/request-stats": "3.0.0", | 		"@types/request-stats": "3.0.0", | ||||||
| 		"@types/rimraf": "2.0.3", | 		"@types/rimraf": "2.0.3", | ||||||
| 		"@types/seedrandom": "2.4.28", | 		"@types/seedrandom": "2.4.28", | ||||||
| @@ -93,26 +95,26 @@ | |||||||
| 		"@types/systeminformation": "3.54.1", | 		"@types/systeminformation": "3.54.1", | ||||||
| 		"@types/tinycolor2": "1.4.2", | 		"@types/tinycolor2": "1.4.2", | ||||||
| 		"@types/tmp": "0.1.0", | 		"@types/tmp": "0.1.0", | ||||||
| 		"@types/uuid": "3.4.7", | 		"@types/uuid": "7.0.2", | ||||||
| 		"@types/web-push": "3.3.0", | 		"@types/web-push": "3.3.0", | ||||||
| 		"@types/webpack": "4.41.6", | 		"@types/webpack": "4.41.10", | ||||||
| 		"@types/webpack-stream": "3.2.10", | 		"@types/webpack-stream": "3.2.10", | ||||||
| 		"@types/websocket": "1.0.0", | 		"@types/websocket": "1.0.0", | ||||||
| 		"@types/ws": "7.2.1", | 		"@types/ws": "7.2.3", | ||||||
| 		"@typescript-eslint/parser": "2.19.2", | 		"@typescript-eslint/parser": "2.26.0", | ||||||
| 		"agentkeepalive": "4.1.0", | 		"abort-controller": "3.0.0", | ||||||
| 		"animejs": "3.1.0", | 		"animejs": "3.1.0", | ||||||
| 		"apexcharts": "3.15.6", | 		"apexcharts": "3.17.1", | ||||||
| 		"autobind-decorator": "2.4.0", | 		"autobind-decorator": "2.4.0", | ||||||
| 		"autosize": "4.0.2", | 		"autosize": "4.0.2", | ||||||
| 		"autwh": "0.1.0", | 		"autwh": "0.1.0", | ||||||
| 		"aws-sdk": "2.617.0", | 		"aws-sdk": "2.653.0", | ||||||
| 		"bcryptjs": "2.4.3", | 		"bcryptjs": "2.4.3", | ||||||
| 		"bull": "3.12.1", | 		"bull": "3.13.0", | ||||||
| 		"cafy": "15.2.1", | 		"cafy": "15.2.1", | ||||||
| 		"cbor": "5.0.1", | 		"cbor": "5.0.1", | ||||||
| 		"chai": "4.2.0", | 		"chai": "4.2.0", | ||||||
| 		"chalk": "3.0.0", | 		"chalk": "4.0.0", | ||||||
| 		"chart.js": "2.9.3", | 		"chart.js": "2.9.3", | ||||||
| 		"cli-highlight": "2.1.4", | 		"cli-highlight": "2.1.4", | ||||||
| 		"commander": "4.1.1", | 		"commander": "4.1.1", | ||||||
| @@ -124,16 +126,16 @@ | |||||||
| 		"diskusage": "1.1.3", | 		"diskusage": "1.1.3", | ||||||
| 		"double-ended-queue": "2.1.0-0", | 		"double-ended-queue": "2.1.0-0", | ||||||
| 		"eslint": "6.8.0", | 		"eslint": "6.8.0", | ||||||
| 		"eslint-plugin-vue": "6.1.2", | 		"eslint-plugin-vue": "6.2.2", | ||||||
| 		"eventemitter3": "4.0.0", | 		"eventemitter3": "4.0.0", | ||||||
| 		"feed": "4.1.0", | 		"feed": "4.1.0", | ||||||
| 		"fibers": "4.0.2", | 		"fibers": "4.0.2", | ||||||
| 		"file-type": "14.1.2", | 		"file-type": "14.1.4", | ||||||
| 		"fluent-ffmpeg": "2.1.2", | 		"fluent-ffmpeg": "2.1.2", | ||||||
| 		"glob": "7.1.6", | 		"glob": "7.1.6", | ||||||
| 		"gulp": "4.0.2", | 		"gulp": "4.0.2", | ||||||
| 		"gulp-clean-css": "4.2.0", | 		"gulp-clean-css": "4.3.0", | ||||||
| 		"gulp-dart-sass": "0.9.1", | 		"gulp-dart-sass": "1.0.0", | ||||||
| 		"gulp-mocha": "7.0.2", | 		"gulp-mocha": "7.0.2", | ||||||
| 		"gulp-rename": "2.0.0", | 		"gulp-rename": "2.0.0", | ||||||
| 		"gulp-replace": "1.0.0", | 		"gulp-replace": "1.0.0", | ||||||
| @@ -143,21 +145,22 @@ | |||||||
| 		"gulp-typescript": "5.0.1", | 		"gulp-typescript": "5.0.1", | ||||||
| 		"hard-source-webpack-plugin": "0.13.1", | 		"hard-source-webpack-plugin": "0.13.1", | ||||||
| 		"html-minifier": "4.0.0", | 		"html-minifier": "4.0.0", | ||||||
| 		"http-signature": "1.3.1", | 		"http-proxy-agent": "4.0.1", | ||||||
|  | 		"http-signature": "1.3.4", | ||||||
| 		"https-proxy-agent": "5.0.0", | 		"https-proxy-agent": "5.0.0", | ||||||
| 		"insert-text-at-cursor": "0.3.0", | 		"insert-text-at-cursor": "0.3.0", | ||||||
| 		"is-root": "2.1.0", | 		"is-root": "2.1.0", | ||||||
| 		"is-svg": "4.2.1", | 		"is-svg": "4.2.1", | ||||||
| 		"js-yaml": "3.13.1", | 		"js-yaml": "3.13.1", | ||||||
| 		"jsdom": "16.1.0", | 		"jsdom": "16.2.2", | ||||||
| 		"json5": "2.1.1", | 		"json5": "2.1.2", | ||||||
| 		"json5-loader": "3.0.0", | 		"json5-loader": "3.0.0", | ||||||
| 		"jsrsasign": "8.0.12", | 		"jsrsasign": "8.0.13", | ||||||
| 		"katex": "0.11.1", | 		"katex": "0.11.1", | ||||||
| 		"koa": "2.11.0", | 		"koa": "2.11.0", | ||||||
| 		"koa-bodyparser": "4.2.1", | 		"koa-bodyparser": "4.3.0", | ||||||
| 		"koa-compress": "3.0.0", | 		"koa-compress": "3.0.0", | ||||||
| 		"koa-favicon": "2.0.1", | 		"koa-favicon": "2.1.0", | ||||||
| 		"koa-json-body": "5.3.0", | 		"koa-json-body": "5.3.0", | ||||||
| 		"koa-logger": "3.2.1", | 		"koa-logger": "3.2.1", | ||||||
| 		"koa-mount": "4.0.0", | 		"koa-mount": "4.0.0", | ||||||
| @@ -165,24 +168,23 @@ | |||||||
| 		"koa-slow": "2.1.0", | 		"koa-slow": "2.1.0", | ||||||
| 		"koa-views": "6.2.1", | 		"koa-views": "6.2.1", | ||||||
| 		"langmap": "0.0.16", | 		"langmap": "0.0.16", | ||||||
| 		"loader-utils": "1.2.3", |  | ||||||
| 		"lolex": "5.1.2", | 		"lolex": "5.1.2", | ||||||
| 		"lookup-dns-cache": "2.1.0", | 		"lookup-dns-cache": "2.1.0", | ||||||
| 		"markdown-it": "10.0.0", | 		"markdown-it": "10.0.0", | ||||||
| 		"markdown-it-anchor": "5.2.5", | 		"markdown-it-anchor": "5.2.7", | ||||||
| 		"mocha": "7.0.1", | 		"mocha": "7.1.1", | ||||||
| 		"moji": "0.5.1", | 		"moji": "0.5.1", | ||||||
| 		"ms": "2.1.2", | 		"ms": "2.1.2", | ||||||
| 		"multer": "1.4.2", | 		"multer": "1.4.2", | ||||||
| 		"nested-property": "1.0.4", | 		"nested-property": "1.0.4", | ||||||
| 		"node-fetch": "2.6.0", | 		"node-fetch": "2.6.0", | ||||||
| 		"nodemailer": "6.4.2", | 		"nodemailer": "6.4.6", | ||||||
| 		"nprogress": "0.2.0", | 		"nprogress": "0.2.0", | ||||||
| 		"object-assign-deep": "0.4.0", | 		"object-assign-deep": "0.4.0", | ||||||
| 		"os-utils": "0.0.14", | 		"os-utils": "0.0.14", | ||||||
| 		"parse5": "5.1.1", | 		"parse5": "5.1.1", | ||||||
| 		"parsimmon": "1.13.0", | 		"parsimmon": "1.13.0", | ||||||
| 		"pg": "7.18.1", | 		"pg": "8.0.0", | ||||||
| 		"portal-vue": "2.1.7", | 		"portal-vue": "2.1.7", | ||||||
| 		"portscanner": "2.2.0", | 		"portscanner": "2.2.0", | ||||||
| 		"postcss-loader": "3.0.0", | 		"postcss-loader": "3.0.0", | ||||||
| @@ -193,75 +195,74 @@ | |||||||
| 		"promise-sequential": "1.1.1", | 		"promise-sequential": "1.1.1", | ||||||
| 		"pug": "2.0.4", | 		"pug": "2.0.4", | ||||||
| 		"punycode": "2.1.1", | 		"punycode": "2.1.1", | ||||||
| 		"pureimage": "0.1.6", | 		"pureimage": "0.2.1", | ||||||
| 		"qrcode": "1.4.4", | 		"qrcode": "1.4.4", | ||||||
| 		"random-seed": "0.3.0", | 		"random-seed": "0.3.0", | ||||||
| 		"randomcolor": "0.5.4", | 		"randomcolor": "0.5.4", | ||||||
| 		"ratelimiter": "3.4.0", | 		"ratelimiter": "3.4.1", | ||||||
| 		"recaptcha-promise": "0.1.3", | 		"recaptcha-promise": "0.1.3", | ||||||
| 		"reconnecting-websocket": "4.4.0", | 		"reconnecting-websocket": "4.4.0", | ||||||
| 		"redis": "3.0.2", | 		"redis": "3.0.2", | ||||||
| 		"redis-lock": "0.1.4", | 		"redis-lock": "0.1.4", | ||||||
| 		"reflect-metadata": "0.1.13", | 		"reflect-metadata": "0.1.13", | ||||||
| 		"rename": "1.0.4", | 		"rename": "1.0.4", | ||||||
| 		"request": "2.88.2", |  | ||||||
| 		"request-promise-native": "1.0.8", |  | ||||||
| 		"request-stats": "3.0.0", | 		"request-stats": "3.0.0", | ||||||
| 		"require-all": "3.0.0", | 		"require-all": "3.0.0", | ||||||
| 		"rimraf": "3.0.2", | 		"rimraf": "3.0.2", | ||||||
| 		"rndstr": "1.0.0", | 		"rndstr": "1.0.0", | ||||||
| 		"s-age": "1.1.2", | 		"s-age": "1.1.2", | ||||||
| 		"sass": "1.25.0", | 		"sass": "1.26.3", | ||||||
| 		"sass-loader": "8.0.2", | 		"sass-loader": "8.0.2", | ||||||
| 		"seedrandom": "3.0.5", | 		"seedrandom": "3.0.5", | ||||||
| 		"sharp": "0.24.0", | 		"sharp": "0.25.2", | ||||||
| 		"showdown": "1.9.1", | 		"showdown": "1.9.1", | ||||||
| 		"showdown-highlightjs-extension": "0.1.2", | 		"showdown-highlightjs-extension": "0.1.2", | ||||||
| 		"speakeasy": "2.0.0", | 		"speakeasy": "2.0.0", | ||||||
| 		"stringz": "2.0.0", | 		"stringz": "2.1.0", | ||||||
| 		"style-loader": "1.1.3", | 		"style-loader": "1.1.3", | ||||||
| 		"summaly": "2.3.1", | 		"summaly": "2.3.1", | ||||||
| 		"syslog-pro": "1.0.0", | 		"syslog-pro": "1.0.0", | ||||||
| 		"systeminformation": "4.21.2", | 		"systeminformation": "4.23.1", | ||||||
| 		"syuilo-password-strength": "0.0.1", | 		"syuilo-password-strength": "0.0.1", | ||||||
| 		"terser-webpack-plugin": "2.3.4", | 		"terser-webpack-plugin": "2.3.5", | ||||||
| 		"textarea-caret": "3.1.0", | 		"textarea-caret": "3.1.0", | ||||||
| 		"three": "0.113.2", | 		"three": "0.115.0", | ||||||
| 		"tinycolor2": "1.4.1", | 		"tinycolor2": "1.4.1", | ||||||
| 		"tmp": "0.1.0", | 		"tmp": "0.1.0", | ||||||
| 		"ts-loader": "6.2.1", | 		"ts-loader": "6.2.2", | ||||||
| 		"ts-node": "8.6.2", | 		"ts-node": "8.8.1", | ||||||
| 		"tslint": "6.0.0", | 		"tslint": "6.1.1", | ||||||
| 		"tslint-sonarts": "1.9.0", | 		"tslint-sonarts": "1.9.0", | ||||||
| 		"typeorm": "0.2.22", | 		"typeorm": "0.2.24", | ||||||
| 		"typescript": "3.7.5", | 		"typescript": "3.8.3", | ||||||
| 		"ulid": "2.3.0", | 		"ulid": "2.3.0", | ||||||
| 		"url-loader": "3.0.0", | 		"url-loader": "3.0.0", | ||||||
| 		"uuid": "3.4.0", | 		"uuid": "7.0.3", | ||||||
| 		"v-animate-css": "0.0.3", | 		"v-animate-css": "0.0.3", | ||||||
| 		"v-debounce": "0.1.2", | 		"v-debounce": "0.1.2", | ||||||
| 		"vue": "2.6.11", | 		"vue": "2.6.11", | ||||||
| 		"vue-color": "2.7.0", | 		"vue-color": "2.7.1", | ||||||
| 		"vue-content-loading": "1.6.0", | 		"vue-content-loading": "1.6.0", | ||||||
| 		"vue-cropperjs": "4.0.1", | 		"vue-cropperjs": "4.0.1", | ||||||
| 		"vue-i18n": "8.15.3", | 		"vue-i18n": "8.16.0", | ||||||
| 		"vue-json-pretty": "1.6.3", | 		"vue-json-pretty": "1.6.3", | ||||||
| 		"vue-loader": "15.9.0", | 		"vue-loader": "15.9.1", | ||||||
| 		"vue-marquee-text-component": "1.1.1", | 		"vue-marquee-text-component": "1.1.1", | ||||||
| 		"vue-meta": "2.3.2", | 		"vue-meta": "2.3.3", | ||||||
| 		"vue-prism-component": "1.1.1", | 		"vue-prism-component": "1.1.1", | ||||||
| 		"vue-router": "3.1.5", | 		"vue-prism-editor": "0.5.1", | ||||||
|  | 		"vue-router": "3.1.6", | ||||||
| 		"vue-style-loader": "4.1.2", | 		"vue-style-loader": "4.1.2", | ||||||
| 		"vue-svg-inline-loader": "1.4.5", | 		"vue-svg-inline-loader": "1.5.0", | ||||||
| 		"vue-template-compiler": "2.6.11", | 		"vue-template-compiler": "2.6.11", | ||||||
| 		"vuedraggable": "2.23.2", | 		"vuedraggable": "2.23.2", | ||||||
| 		"vuex": "3.1.2", | 		"vuex": "3.1.3", | ||||||
| 		"vuex-persistedstate": "2.7.1", | 		"vuex-persistedstate": "3.0.1", | ||||||
| 		"web-push": "3.4.3", | 		"web-push": "3.4.3", | ||||||
| 		"webpack": "4.41.6", | 		"webpack": "4.42.1", | ||||||
| 		"webpack-cli": "3.3.11", | 		"webpack-cli": "3.3.11", | ||||||
| 		"websocket": "1.0.31", | 		"websocket": "1.0.31", | ||||||
| 		"ws": "7.2.1", | 		"ws": "7.2.3", | ||||||
| 		"xev": "2.0.1" | 		"xev": "2.0.1" | ||||||
| 	}, | 	}, | ||||||
| 	"devDependencies": { | 	"devDependencies": { | ||||||
|   | |||||||
| @@ -99,7 +99,7 @@ async function isPortAvailable(port: number): Promise<boolean> { | |||||||
| function showEnvironment(): void { | function showEnvironment(): void { | ||||||
| 	const env = process.env.NODE_ENV; | 	const env = process.env.NODE_ENV; | ||||||
| 	const logger = bootLogger.createSubLogger('env'); | 	const logger = bootLogger.createSubLogger('env'); | ||||||
| 	logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`); | 	logger.info(typeof env === 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`); | ||||||
|  |  | ||||||
| 	if (env !== 'production') { | 	if (env !== 'production') { | ||||||
| 		logger.warn('The environment is not in production mode.'); | 		logger.warn('The environment is not in production mode.'); | ||||||
|   | |||||||
| @@ -156,7 +156,7 @@ | |||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; | import { faTerminal, faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; | import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { ResizeObserver } from '@juggle/resize-observer'; | import { ResizeObserver } from '@juggle/resize-observer'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| @@ -310,7 +310,7 @@ export default Vue.extend({ | |||||||
| 				title: this.$t('search'), | 				title: this.$t('search'), | ||||||
| 				input: true | 				input: true | ||||||
| 			}).then(async ({ canceled, result: query }) => { | 			}).then(async ({ canceled, result: query }) => { | ||||||
| 				if (canceled || query == null || query == '') return; | 				if (canceled || query == null || query === '') return; | ||||||
|  |  | ||||||
| 				this.searching = true; | 				this.searching = true; | ||||||
| 				search(this, query).finally(() => { | 				search(this, query).finally(() => { | ||||||
| @@ -320,7 +320,7 @@ export default Vue.extend({ | |||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		searchKeypress(e) { | 		searchKeypress(e) { | ||||||
| 			if (e.keyCode == 13) { | 			if (e.keyCode === 13) { | ||||||
| 				this.searchWait = true; | 				this.searchWait = true; | ||||||
| 				search(this, this.searchQuery).finally(() => { | 				search(this, this.searchQuery).finally(() => { | ||||||
| 					this.searchWait = false; | 					this.searchWait = false; | ||||||
| @@ -470,6 +470,11 @@ export default Vue.extend({ | |||||||
| 					to: '/games', | 					to: '/games', | ||||||
| 					icon: faGamepad, | 					icon: faGamepad, | ||||||
| 				}, null] : []), { | 				}, null] : []), { | ||||||
|  | 					type: 'link', | ||||||
|  | 					text: this.$t('scratchpad'), | ||||||
|  | 					to: '/scratchpad', | ||||||
|  | 					icon: faTerminal, | ||||||
|  | 				}, null, { | ||||||
| 					type: 'link', | 					type: 'link', | ||||||
| 					text: this.$t('help'), | 					text: this.$t('help'), | ||||||
| 					to: '/docs', | 					to: '/docs', | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ | |||||||
| 		</style> | 		</style> | ||||||
| 	</head> | 	</head> | ||||||
| 	<body> | 	<body> | ||||||
| 		<redoc spec-url='/api.json'></redoc> | 		<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc> | ||||||
| 		<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script> | 		<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script> | ||||||
| 	</body> | 	</body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ import Vue from 'vue'; | |||||||
| import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; | import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { url as local } from '../config'; | import { url as local } from '../config'; | ||||||
| import MkUrlPreview from './url-preview-popup.vue'; | import MkUrlPreview from './url-preview-popup.vue'; | ||||||
|  | import { isDeviceTouch } from '../scripts/is-device-touch'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	props: { | 	props: { | ||||||
| @@ -61,11 +62,13 @@ export default Vue.extend({ | |||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		onMouseover() { | 		onMouseover() { | ||||||
|  | 			if (isDeviceTouch()) return; | ||||||
| 			clearTimeout(this.showTimer); | 			clearTimeout(this.showTimer); | ||||||
| 			clearTimeout(this.hideTimer); | 			clearTimeout(this.hideTimer); | ||||||
| 			this.showTimer = setTimeout(this.showPreview, 500); | 			this.showTimer = setTimeout(this.showPreview, 500); | ||||||
| 		}, | 		}, | ||||||
| 		onMouseleave() { | 		onMouseleave() { | ||||||
|  | 			if (isDeviceTouch()) return; | ||||||
| 			clearTimeout(this.showTimer); | 			clearTimeout(this.showTimer); | ||||||
| 			clearTimeout(this.hideTimer); | 			clearTimeout(this.hideTimer); | ||||||
| 			this.hideTimer = setTimeout(this.closePreview, 500); | 			this.hideTimer = setTimeout(this.closePreview, 500); | ||||||
|   | |||||||
| @@ -1,23 +1,26 @@ | |||||||
| <template> | <template> | ||||||
| <div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> | <div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="hide" @click="hide = false"> | ||||||
| 	<div> | 	<div> | ||||||
| 		<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b> | 		<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b> | ||||||
| 		<span>{{ $t('clickToShow') }}</span> | 		<span>{{ $t('clickToShow') }}</span> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| <a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else | <div class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else> | ||||||
|  | 	<i><fa :icon="faEyeSlash" @click="hide = true"/></i> | ||||||
|  | 	<a | ||||||
| 		:href="image.url" | 		:href="image.url" | ||||||
| 		:style="style" | 		:style="style" | ||||||
| 		:title="image.name" | 		:title="image.name" | ||||||
| 		@click.prevent="onClick" | 		@click.prevent="onClick" | ||||||
| > | 	> | ||||||
| 		<div v-if="image.type === 'image/gif'">GIF</div> | 		<div v-if="image.type === 'image/gif'">GIF</div> | ||||||
| </a> | 	</a> | ||||||
|  | </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; | import { faExclamationTriangle, faEyeSlash } 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'; | import ImageViewer from './image-viewer.vue'; | ||||||
| @@ -36,7 +39,8 @@ export default Vue.extend({ | |||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			hide: true, | 			hide: true, | ||||||
| 			faExclamationTriangle | 			faExclamationTriangle, | ||||||
|  | 			faEyeSlash | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	computed: { | 	computed: { | ||||||
| @@ -59,6 +63,9 @@ export default Vue.extend({ | |||||||
| 			}; | 			}; | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  | 	created() { | ||||||
|  | 		this.hide = this.image.isSensitive && !this.$store.state.device.alwaysShowNsfw; | ||||||
|  | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		onClick() { | 		onClick() { | ||||||
| 			if (this.$store.state.device.imageNewTab) { | 			if (this.$store.state.device.imageNewTab) { | ||||||
| @@ -78,6 +85,24 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .gqnyydlzavusgskkfvwvjiattxdzsqlf { | .gqnyydlzavusgskkfvwvjiattxdzsqlf { | ||||||
|  | 	position: relative; | ||||||
|  |  | ||||||
|  | 	> i { | ||||||
|  | 		display: block; | ||||||
|  | 		position: absolute; | ||||||
|  | 		border-radius: 6px; | ||||||
|  | 		background-color: var(--fg); | ||||||
|  | 		color: var(--accentLighten); | ||||||
|  | 		font-size: 14px; | ||||||
|  | 		opacity: .5; | ||||||
|  | 		padding: 3px 6px; | ||||||
|  | 		text-align: center; | ||||||
|  | 		cursor: pointer; | ||||||
|  | 		top: 12px; | ||||||
|  | 		right: 12px; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	> a { | ||||||
| 		display: block; | 		display: block; | ||||||
| 		cursor: zoom-in; | 		cursor: zoom-in; | ||||||
| 		overflow: hidden; | 		overflow: hidden; | ||||||
| @@ -101,6 +126,7 @@ export default Vue.extend({ | |||||||
| 			top: 12px; | 			top: 12px; | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| .qjewsnkgzzxlxtzncydssfbgjibiehcy { | .qjewsnkgzzxlxtzncydssfbgjibiehcy { | ||||||
|   | |||||||
| @@ -34,9 +34,7 @@ export default Vue.extend({ | |||||||
| 			default: false | 			default: false | ||||||
| 		}, | 		}, | ||||||
| 		// specify the parent element | 		// specify the parent element | ||||||
| 		parentElement: { | 		parentElement: {} | ||||||
| 			type: Object |  | ||||||
| 		} |  | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| @@ -69,7 +67,7 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 				if (this.$refs.gridOuter) { | 				if (this.$refs.gridOuter) { | ||||||
| 					let height = 287; | 					let height = 287; | ||||||
| 					const parent = this.$props.parentElement || this.$parent.$el; | 					const parent = this.parentElement || this.$parent.$el; | ||||||
|  |  | ||||||
| 					if (this.$refs.gridOuter.clientHeight) { | 					if (this.$refs.gridOuter.clientHeight) { | ||||||
| 						height = this.$refs.gridOuter.clientHeight; | 						height = this.$refs.gridOuter.clientHeight; | ||||||
| @@ -83,6 +81,11 @@ export default Vue.extend({ | |||||||
| 				} | 				} | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
|  | 	}, | ||||||
|  | 	watch: { | ||||||
|  | 		parentElement() { | ||||||
|  | 			this.size(); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -1,24 +1,28 @@ | |||||||
| <template> | <template> | ||||||
| <div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="video.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false"> | <div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="hide" @click="hide = false"> | ||||||
| 	<div> | 	<div> | ||||||
| 		<b><fa icon="exclamation-triangle"/> {{ $t('sensitive') }}</b> | 		<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b> | ||||||
| 		<span>{{ $t('clickToShow') }}</span> | 		<span>{{ $t('clickToShow') }}</span> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| <a class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else | <div class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else> | ||||||
|  | 	<i><fa :icon="faEyeSlash" @click="hide = true"/></i> | ||||||
|  | 	<a | ||||||
| 		:href="video.url" | 		:href="video.url" | ||||||
| 		rel="nofollow noopener" | 		rel="nofollow noopener" | ||||||
| 		target="_blank" | 		target="_blank" | ||||||
| 		:style="imageStyle" | 		:style="imageStyle" | ||||||
| 		:title="video.name" | 		:title="video.name" | ||||||
| > | 	> | ||||||
| 		<fa :icon="faPlayCircle"/> | 		<fa :icon="faPlayCircle"/> | ||||||
| </a> | 	</a> | ||||||
|  | </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import { faPlayCircle } from '@fortawesome/free-regular-svg-icons'; | import { faPlayCircle } from '@fortawesome/free-regular-svg-icons'; | ||||||
|  | import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import i18n from '../i18n'; | import i18n from '../i18n'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| @@ -32,7 +36,9 @@ export default Vue.extend({ | |||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			hide: true, | 			hide: true, | ||||||
| 			faPlayCircle | 			faPlayCircle, | ||||||
|  | 			faExclamationTriangle, | ||||||
|  | 			faEyeSlash | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	computed: { | 	computed: { | ||||||
| @@ -41,12 +47,33 @@ export default Vue.extend({ | |||||||
| 				'background-image': `url(${this.video.thumbnailUrl})` | 				'background-image': `url(${this.video.thumbnailUrl})` | ||||||
| 			}; | 			}; | ||||||
| 		} | 		} | ||||||
| 	} | 	}, | ||||||
|  | 	created() { | ||||||
|  | 		this.hide = this.video.isSensitive && !this.$store.state.device.alwaysShowNsfw; | ||||||
|  | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .kkjnbbplepmiyuadieoenjgutgcmtsvu { | .kkjnbbplepmiyuadieoenjgutgcmtsvu { | ||||||
|  | 	position: relative; | ||||||
|  |  | ||||||
|  | 	> i { | ||||||
|  | 		display: block; | ||||||
|  | 		position: absolute; | ||||||
|  | 		border-radius: 6px; | ||||||
|  | 		background-color: var(--fg); | ||||||
|  | 		color: var(--accentLighten); | ||||||
|  | 		font-size: 14px; | ||||||
|  | 		opacity: .5; | ||||||
|  | 		padding: 3px 6px; | ||||||
|  | 		text-align: center; | ||||||
|  | 		cursor: pointer; | ||||||
|  | 		top: 12px; | ||||||
|  | 		right: 12px; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	> a { | ||||||
| 		display: flex; | 		display: flex; | ||||||
| 		justify-content: center; | 		justify-content: center; | ||||||
| 		align-items: center; | 		align-items: center; | ||||||
| @@ -57,6 +84,7 @@ export default Vue.extend({ | |||||||
| 		background-size: cover; | 		background-size: cover; | ||||||
| 		width: 100%; | 		width: 100%; | ||||||
| 		height: 100%; | 		height: 100%; | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| .icozogqfvdetwohsdglrbswgrejoxbdj { | .icozogqfvdetwohsdglrbswgrejoxbdj { | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ | |||||||
| 		<mk-avatar class="avatar" :user="appearNote.user"/> | 		<mk-avatar class="avatar" :user="appearNote.user"/> | ||||||
| 		<div class="main"> | 		<div class="main"> | ||||||
| 			<x-note-header class="header" :note="appearNote" :mini="true"/> | 			<x-note-header class="header" :note="appearNote" :mini="true"/> | ||||||
| 			<div class="body" v-if="appearNote.deletedAt == null"> | 			<div class="body" v-if="appearNote.deletedAt == null" ref="noteBody"> | ||||||
| 				<p v-if="appearNote.cw != null" class="cw"> | 				<p v-if="appearNote.cw != null" class="cw"> | ||||||
| 				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> | 				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" /> | ||||||
| 					<x-cw-button v-model="showContent" :note="appearNote"/> | 					<x-cw-button v-model="showContent" :note="appearNote"/> | ||||||
| @@ -46,7 +46,7 @@ | |||||||
| 						<a class="rp" v-if="appearNote.renote != null">RN:</a> | 						<a class="rp" v-if="appearNote.renote != null">RN:</a> | ||||||
| 					</div> | 					</div> | ||||||
| 					<div class="files" v-if="appearNote.files.length > 0"> | 					<div class="files" v-if="appearNote.files.length > 0"> | ||||||
| 						<x-media-list :media-list="appearNote.files"/> | 						<x-media-list :media-list="appearNote.files" :parent-element="noteBody"/> | ||||||
| 					</div> | 					</div> | ||||||
| 					<x-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/> | 					<x-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/> | ||||||
| 					<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true" class="url-preview"/> | 					<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true" class="url-preview"/> | ||||||
| @@ -142,6 +142,7 @@ export default Vue.extend({ | |||||||
| 			replies: [], | 			replies: [], | ||||||
| 			showContent: false, | 			showContent: false, | ||||||
| 			hideThisNote: false, | 			hideThisNote: false, | ||||||
|  | 			noteBody: this.$refs.noteBody, | ||||||
| 			faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan | 			faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| @@ -254,6 +255,8 @@ export default Vue.extend({ | |||||||
| 		if (this.$store.getters.isSignedIn) { | 		if (this.$store.getters.isSignedIn) { | ||||||
| 			this.connection.on('_connected_', this.onStreamConnected); | 			this.connection.on('_connected_', this.onStreamConnected); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		this.noteBody = this.$refs.noteBody | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	beforeDestroy() { | 	beforeDestroy() { | ||||||
| @@ -301,6 +304,14 @@ export default Vue.extend({ | |||||||
| 				case 'reacted': { | 				case 'reacted': { | ||||||
| 					const reaction = body.reaction; | 					const reaction = body.reaction; | ||||||
|  |  | ||||||
|  | 					if (body.emoji) { | ||||||
|  | 						const emojis = this.appearNote.emojis || []; | ||||||
|  | 						if (!emojis.includes(body.emoji)) { | ||||||
|  | 							emojis.push(body.emoji); | ||||||
|  | 							Vue.set(this.appearNote, 'emojis', emojis); | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  |  | ||||||
| 					if (this.appearNote.reactions == null) { | 					if (this.appearNote.reactions == null) { | ||||||
| 						Vue.set(this.appearNote, 'reactions', {}); | 						Vue.set(this.appearNote, 'reactions', {}); | ||||||
| 					} | 					} | ||||||
| @@ -517,11 +528,11 @@ export default Vue.extend({ | |||||||
| 					icon: faLink, | 					icon: faLink, | ||||||
| 					text: this.$t('copyLink'), | 					text: this.$t('copyLink'), | ||||||
| 					action: this.copyLink | 					action: this.copyLink | ||||||
| 				}, this.appearNote.uri ? { | 				}, (this.appearNote.url || this.appearNote.uri) ? { | ||||||
| 					icon: faExternalLinkSquareAlt, | 					icon: faExternalLinkSquareAlt, | ||||||
| 					text: this.$t('showOnRemote'), | 					text: this.$t('showOnRemote'), | ||||||
| 					action: () => { | 					action: () => { | ||||||
| 						window.open(this.appearNote.uri, '_blank'); | 						window.open(this.appearNote.url || this.appearNote.uri, '_blank'); | ||||||
| 					} | 					} | ||||||
| 				} : undefined, | 				} : undefined, | ||||||
| 				null, | 				null, | ||||||
| @@ -561,13 +572,13 @@ export default Vue.extend({ | |||||||
| 					}] | 					}] | ||||||
| 					: [] | 					: [] | ||||||
| 				), | 				), | ||||||
| 				...(this.appearNote.userId == this.$store.state.i.id ? [ | 				...(this.appearNote.userId == this.$store.state.i.id || this.$store.state.i.isModerator || this.$store.state.i.isAdmin ? [ | ||||||
| 					null, | 					null, | ||||||
| 					{ | 					this.appearNote.userId == this.$store.state.i.id ? { | ||||||
| 						icon: faEdit, | 						icon: faEdit, | ||||||
| 						text: this.$t('deleteAndEdit'), | 						text: this.$t('deleteAndEdit'), | ||||||
| 						action: this.delEdit | 						action: this.delEdit | ||||||
| 					}, | 					} : undefined, | ||||||
| 					{ | 					{ | ||||||
| 						icon: faTrashAlt, | 						icon: faTrashAlt, | ||||||
| 						text: this.$t('delete'), | 						text: this.$t('delete'), | ||||||
| @@ -585,11 +596,11 @@ export default Vue.extend({ | |||||||
| 					icon: faLink, | 					icon: faLink, | ||||||
| 					text: this.$t('copyLink'), | 					text: this.$t('copyLink'), | ||||||
| 					action: this.copyLink | 					action: this.copyLink | ||||||
| 				}, this.appearNote.uri ? { | 				}, (this.appearNote.url || this.appearNote.uri) ? { | ||||||
| 					icon: faExternalLinkSquareAlt, | 					icon: faExternalLinkSquareAlt, | ||||||
| 					text: this.$t('showOnRemote'), | 					text: this.$t('showOnRemote'), | ||||||
| 					action: () => { | 					action: () => { | ||||||
| 						window.open(this.appearNote.uri, '_blank'); | 						window.open(this.appearNote.url || this.appearNote.uri, '_blank'); | ||||||
| 					} | 					} | ||||||
| 				} : undefined] | 				} : undefined] | ||||||
| 				.filter(x => x !== undefined); | 				.filter(x => x !== undefined); | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ | |||||||
| 			<fa :icon="faReply" v-else-if="notification.type === 'reply'"/> | 			<fa :icon="faReply" v-else-if="notification.type === 'reply'"/> | ||||||
| 			<fa :icon="faAt" v-else-if="notification.type === 'mention'"/> | 			<fa :icon="faAt" v-else-if="notification.type === 'mention'"/> | ||||||
| 			<fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/> | 			<fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/> | ||||||
| 			<x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/> | 			<x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :customEmojis="notification.note.emojis" :no-style="true"/> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="tail"> | 	<div class="tail"> | ||||||
|   | |||||||
| @@ -35,6 +35,7 @@ export default Vue.extend({ | |||||||
| 	border: solid var(--lineWidth) var(--urlPreviewBorder); | 	border: solid var(--lineWidth) var(--urlPreviewBorder); | ||||||
| 	border-radius: 4px; | 	border-radius: 4px; | ||||||
| 	overflow: hidden; | 	overflow: hidden; | ||||||
|  | 	border: 1px solid var(--divider); | ||||||
|  |  | ||||||
| 	&:hover { | 	&:hover { | ||||||
| 		text-decoration: none; | 		text-decoration: none; | ||||||
| @@ -42,9 +43,8 @@ export default Vue.extend({ | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	> .thumbnail { | 	> .thumbnail { | ||||||
| 		position: absolute; | 		width: 100%; | ||||||
| 		width: 100px; | 		height: 200px; | ||||||
| 		height: 100%; |  | ||||||
| 		background-position: center; | 		background-position: center; | ||||||
| 		background-size: cover; | 		background-size: cover; | ||||||
| 		display: flex; | 		display: flex; | ||||||
|   | |||||||
| @@ -17,10 +17,11 @@ import XTextarea from './page.textarea.vue'; | |||||||
| import XPost from './page.post.vue'; | import XPost from './page.post.vue'; | ||||||
| import XCounter from './page.counter.vue'; | import XCounter from './page.counter.vue'; | ||||||
| import XRadioButton from './page.radio-button.vue'; | import XRadioButton from './page.radio-button.vue'; | ||||||
|  | import XCanvas from './page.canvas.vue'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton | 		XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas | ||||||
| 	}, | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		value: { | 		value: { | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ export default Vue.extend({ | |||||||
| 					text: this.script.interpolate(this.value.content) | 					text: this.script.interpolate(this.value.content) | ||||||
| 				}); | 				}); | ||||||
| 			} else if (this.value.action === 'resetRandom') { | 			} else if (this.value.action === 'resetRandom') { | ||||||
| 				this.script.aiScript.updateRandomSeed(Math.random()); | 				this.script.aoiScript.updateRandomSeed(Math.random()); | ||||||
| 				this.script.eval(); | 				this.script.eval(); | ||||||
| 			} else if (this.value.action === 'pushEvent') { | 			} else if (this.value.action === 'pushEvent') { | ||||||
| 				this.$root.api('page-push', { | 				this.$root.api('page-push', { | ||||||
| @@ -43,6 +43,8 @@ export default Vue.extend({ | |||||||
| 					type: 'success', | 					type: 'success', | ||||||
| 					text: this.script.interpolate(this.value.message) | 					text: this.script.interpolate(this.value.message) | ||||||
| 				}); | 				}); | ||||||
|  | 			} else if (this.value.action === 'callAiScript') { | ||||||
|  | 				this.script.callAiScript(this.value.fn); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								src/client/components/page/page.canvas.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/client/components/page/page.canvas.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | <template> | ||||||
|  | <div> | ||||||
|  | 	<canvas ref="canvas" class="ysrxegms" :width="value.width" :height="value.height"/> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	props: { | ||||||
|  | 		value: { | ||||||
|  | 			required: true | ||||||
|  | 		}, | ||||||
|  | 		script: { | ||||||
|  | 			required: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	mounted() { | ||||||
|  | 		this.script.aoiScript.registerCanvas(this.value.name, this.$refs.canvas); | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .ysrxegms { | ||||||
|  | 	display: block; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -27,7 +27,7 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
| 	watch: { | 	watch: { | ||||||
| 		v() { | 		v() { | ||||||
| 			this.script.aiScript.updatePageVar(this.value.name, this.v); | 			this.script.aoiScript.updatePageVar(this.value.name, this.v); | ||||||
| 			this.script.eval(); | 			this.script.eval(); | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
| 	watch: { | 	watch: { | ||||||
| 		v() { | 		v() { | ||||||
| 			this.script.aiScript.updatePageVar(this.value.name, this.v); | 			this.script.aoiScript.updatePageVar(this.value.name, this.v); | ||||||
| 			this.script.eval(); | 			this.script.eval(); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
| 	watch: { | 	watch: { | ||||||
| 		v() { | 		v() { | ||||||
| 			this.script.aiScript.updatePageVar(this.value.name, this.v); | 			this.script.aoiScript.updatePageVar(this.value.name, this.v); | ||||||
| 			this.script.eval(); | 			this.script.eval(); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
| 	watch: { | 	watch: { | ||||||
| 		v() { | 		v() { | ||||||
| 			this.script.aiScript.updatePageVar(this.value.name, this.v); | 			this.script.aoiScript.updatePageVar(this.value.name, this.v); | ||||||
| 			this.script.eval(); | 			this.script.eval(); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
| 	watch: { | 	watch: { | ||||||
| 		v() { | 		v() { | ||||||
| 			this.script.aiScript.updatePageVar(this.value.name, this.v); | 			this.script.aoiScript.updatePageVar(this.value.name, this.v); | ||||||
| 			this.script.eval(); | 			this.script.eval(); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
| 	watch: { | 	watch: { | ||||||
| 		v() { | 		v() { | ||||||
| 			this.script.aiScript.updatePageVar(this.value.name, this.v); | 			this.script.aoiScript.updatePageVar(this.value.name, this.v); | ||||||
| 			this.script.eval(); | 			this.script.eval(); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -6,30 +6,31 @@ | |||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import i18n from '../../i18n'; | import { AiScript, parse, values } from '@syuilo/aiscript'; | ||||||
| import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons'; | import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faHeart } from '@fortawesome/free-regular-svg-icons'; | import { faHeart } from '@fortawesome/free-regular-svg-icons'; | ||||||
|  | import i18n from '../../i18n'; | ||||||
| import XBlock from './page.block.vue'; | import XBlock from './page.block.vue'; | ||||||
| import { ASEvaluator } from '../../scripts/aiscript/evaluator'; | import { ASEvaluator } from '../../scripts/aoiscript/evaluator'; | ||||||
| import { collectPageVars } from '../../scripts/collect-page-vars'; | import { collectPageVars } from '../../scripts/collect-page-vars'; | ||||||
| import { url } from '../../config'; | import { url } from '../../config'; | ||||||
|  |  | ||||||
| class Script { | class Script { | ||||||
| 	public aiScript: ASEvaluator; | 	public aoiScript: ASEvaluator; | ||||||
| 	private onError: any; | 	private onError: any; | ||||||
| 	public vars: Record<string, any>; | 	public vars: Record<string, any>; | ||||||
| 	public page: Record<string, any>; | 	public page: Record<string, any>; | ||||||
|  |  | ||||||
| 	constructor(page, aiScript, onError) { | 	constructor(page, aoiScript, onError) { | ||||||
| 		this.page = page; | 		this.page = page; | ||||||
| 		this.aiScript = aiScript; | 		this.aoiScript = aoiScript; | ||||||
| 		this.onError = onError; | 		this.onError = onError; | ||||||
| 		this.eval(); | 		this.eval(); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public eval() { | 	public eval() { | ||||||
| 		try { | 		try { | ||||||
| 			this.vars = this.aiScript.evaluateVars(); | 			this.vars = this.aoiScript.evaluateVars(); | ||||||
| 		} catch (e) { | 		} catch (e) { | ||||||
| 			this.onError(e); | 			this.onError(e); | ||||||
| 		} | 		} | ||||||
| @@ -38,10 +39,16 @@ class Script { | |||||||
| 	public interpolate(str: string) { | 	public interpolate(str: string) { | ||||||
| 		if (str == null) return null; | 		if (str == null) return null; | ||||||
| 		return str.replace(/{(.+?)}/g, match => { | 		return str.replace(/{(.+?)}/g, match => { | ||||||
| 			const v = this.vars[match.slice(1, -1).trim()]; | 			const v = this.vars ? this.vars[match.slice(1, -1).trim()] : null; | ||||||
| 			return v == null ? 'NULL' : v.toString(); | 			return v == null ? 'NULL' : v.toString(); | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	public callAiScript(fn: string) { | ||||||
|  | 		try { | ||||||
|  | 			if (this.aoiScript.aiscript) this.aoiScript.aiscript.execFn(this.aoiScript.aiscript.scope.get(fn), []); | ||||||
|  | 		} catch (e) {} | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| @@ -67,14 +74,53 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 	created() { | 	created() { | ||||||
| 		const pageVars = this.getPageVars(); | 		const pageVars = this.getPageVars(); | ||||||
| 		this.script = new Script(this.page, new ASEvaluator(this.page.variables, pageVars, { | 		 | ||||||
|  | 		this.script = new Script(this.page, new ASEvaluator(this, this.page.variables, pageVars, { | ||||||
| 			randomSeed: Math.random(), | 			randomSeed: Math.random(), | ||||||
| 			visitor: this.$store.state.i, | 			visitor: this.$store.state.i, | ||||||
| 			page: this.page, | 			page: this.page, | ||||||
| 			url: url | 			url: url, | ||||||
|  | 			enableAiScript: !this.$store.state.device.disablePagesScript | ||||||
| 		}), e => { | 		}), e => { | ||||||
| 			console.dir(e); | 			console.dir(e); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
|  | 		if (this.script.aoiScript.aiscript) this.script.aoiScript.aiscript.scope.opts.onUpdated = (name, value) => { | ||||||
|  | 			this.script.eval(); | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	mounted() { | ||||||
|  | 		this.$nextTick(() => { | ||||||
|  | 			if (this.script.page.script && this.script.aoiScript.aiscript) { | ||||||
|  | 				let ast; | ||||||
|  | 				try { | ||||||
|  | 					ast = parse(this.script.page.script); | ||||||
|  | 				} catch (e) { | ||||||
|  | 					console.error(e); | ||||||
|  | 					/*this.$root.dialog({ | ||||||
|  | 						type: 'error', | ||||||
|  | 						text: 'Syntax error :(' | ||||||
|  | 					});*/ | ||||||
|  | 					return; | ||||||
|  | 				} | ||||||
|  | 				this.script.aoiScript.aiscript.exec(ast).then(() => { | ||||||
|  | 					this.script.eval(); | ||||||
|  | 				}).catch(e => { | ||||||
|  | 					console.error(e); | ||||||
|  | 					/*this.$root.dialog({ | ||||||
|  | 						type: 'error', | ||||||
|  | 						text: e | ||||||
|  | 					});*/ | ||||||
|  | 				}); | ||||||
|  | 			} else { | ||||||
|  | 				this.script.eval(); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	beforeDestroy() { | ||||||
|  | 		if (this.script.aoiScript.aiscript) this.script.aoiScript.aiscript.abort(); | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	methods: { | 	methods: { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
| <mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :is-reaction="true" :normal="true" :no-style="noStyle"/> | <mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :customEmojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| @@ -12,6 +12,10 @@ export default Vue.extend({ | |||||||
| 			type: String, | 			type: String, | ||||||
| 			required: true | 			required: true | ||||||
| 		}, | 		}, | ||||||
|  | 		customEmojis: { | ||||||
|  | 			required: false, | ||||||
|  | 			default: () => [] | ||||||
|  | 		}, | ||||||
| 		noStyle: { | 		noStyle: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
| <button | <button | ||||||
| 	class="hkzvhatu _button" | 	class="hkzvhatu _button" | ||||||
| 	:class="{ reacted: note.myReaction == reaction }" | 	:class="{ reacted: note.myReaction == reaction, canToggle }" | ||||||
| 	@click="toggleReaction(reaction)" | 	@click="toggleReaction(reaction)" | ||||||
| 	v-if="count > 0" | 	v-if="count > 0" | ||||||
| 	@mouseover="onMouseover" | 	@mouseover="onMouseover" | ||||||
| @@ -9,7 +9,7 @@ | |||||||
| 	ref="reaction" | 	ref="reaction" | ||||||
| 	v-particle | 	v-particle | ||||||
| > | > | ||||||
| 	<x-reaction-icon :reaction="reaction" ref="icon"/> | 	<x-reaction-icon :reaction="reaction" :customEmojis="note.emojis" ref="icon"/> | ||||||
| 	<span>{{ count }}</span> | 	<span>{{ count }}</span> | ||||||
| </button> | </button> | ||||||
| </template> | </template> | ||||||
| @@ -40,11 +40,6 @@ export default Vue.extend({ | |||||||
| 			type: Object, | 			type: Object, | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
| 		canToggle: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false, |  | ||||||
| 			default: true, |  | ||||||
| 		}, |  | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| @@ -57,6 +52,9 @@ export default Vue.extend({ | |||||||
| 		isMe(): boolean { | 		isMe(): boolean { | ||||||
| 			return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId; | 			return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId; | ||||||
| 		}, | 		}, | ||||||
|  | 		canToggle(): boolean { | ||||||
|  | 			return !this.reaction.match(/@\w/); | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		if (!this.isInitial) this.anime(); | 		if (!this.isInitial) this.anime(); | ||||||
| @@ -144,6 +142,18 @@ export default Vue.extend({ | |||||||
| 	padding: 0 6px; | 	padding: 0 6px; | ||||||
| 	border-radius: 4px; | 	border-radius: 4px; | ||||||
|  |  | ||||||
|  | 	&.canToggle { | ||||||
|  | 		background: rgba(0, 0, 0, 0.05); | ||||||
|  |  | ||||||
|  | 		&:hover { | ||||||
|  | 			background: rgba(0, 0, 0, 0.1); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	&:not(.canToggle) { | ||||||
|  | 		cursor: default; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	&.reacted { | 	&.reacted { | ||||||
| 		background: var(--accent); | 		background: var(--accent); | ||||||
|  |  | ||||||
| @@ -152,14 +162,6 @@ export default Vue.extend({ | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	&:not(.reacted) { |  | ||||||
| 		background: rgba(0, 0, 0, 0.05); |  | ||||||
|  |  | ||||||
| 		&:hover { |  | ||||||
| 			background: rgba(0, 0, 0, 0.1); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	> span { | 	> span { | ||||||
| 		font-size: 0.9em; | 		font-size: 0.9em; | ||||||
| 		line-height: 32px; | 		line-height: 32px; | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		const rect = this.source.getBoundingClientRect(); | 		const rect = this.source.getBoundingClientRect(); | ||||||
| 		const x = ((rect.left + (this.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset; | 		const x = Math.max((rect.left + (this.source.offsetWidth / 2)) - (300 / 2), 6) + window.pageXOffset; | ||||||
| 		const y = rect.top + this.source.offsetHeight + window.pageYOffset; | 		const y = rect.top + this.source.offsetHeight + window.pageYOffset; | ||||||
|  |  | ||||||
| 		this.top = y; | 		this.top = y; | ||||||
| @@ -50,6 +50,7 @@ export default Vue.extend({ | |||||||
| 	position: absolute; | 	position: absolute; | ||||||
| 	z-index: 11000; | 	z-index: 11000; | ||||||
| 	width: 500px; | 	width: 500px; | ||||||
|  | 	max-width: calc(90vw - 12px); | ||||||
| 	overflow: hidden; | 	overflow: hidden; | ||||||
| 	pointer-events: none; | 	pointer-events: none; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; | |||||||
| import { toUnicode as decodePunycode } from 'punycode'; | import { toUnicode as decodePunycode } from 'punycode'; | ||||||
| import { url as local } from '../config'; | import { url as local } from '../config'; | ||||||
| import MkUrlPreview from './url-preview-popup.vue'; | import MkUrlPreview from './url-preview-popup.vue'; | ||||||
|  | import { isDeviceTouch } from '../scripts/is-device-touch'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	props: { | 	props: { | ||||||
| @@ -92,11 +93,13 @@ export default Vue.extend({ | |||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		onMouseover() { | 		onMouseover() { | ||||||
|  | 			if (isDeviceTouch()) return; | ||||||
| 			clearTimeout(this.showTimer); | 			clearTimeout(this.showTimer); | ||||||
| 			clearTimeout(this.hideTimer); | 			clearTimeout(this.hideTimer); | ||||||
| 			this.showTimer = setTimeout(this.showPreview, 500); | 			this.showTimer = setTimeout(this.showPreview, 500); | ||||||
| 		}, | 		}, | ||||||
| 		onMouseleave() { | 		onMouseleave() { | ||||||
|  | 			if (isDeviceTouch()) return; | ||||||
| 			clearTimeout(this.showTimer); | 			clearTimeout(this.showTimer); | ||||||
| 			clearTimeout(this.hideTimer); | 			clearTimeout(this.hideTimer); | ||||||
| 			this.hideTimer = setTimeout(this.closePreview, 500); | 			this.hideTimer = setTimeout(this.closePreview, 500); | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ | |||||||
|  |  | ||||||
| 	<div class="efvhhmdq"> | 	<div class="efvhhmdq"> | ||||||
| 		<div class="no-users" v-if="empty"> | 		<div class="no-users" v-if="empty"> | ||||||
| 			<p>{{ $t('no-users') }}</p> | 			<p>{{ $t('noUsers') }}</p> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="user" v-for="user in users" :key="user.id"> | 		<div class="user" v-for="user in users" :key="user.id"> | ||||||
| 			<mk-avatar class="avatar" :user="user"/> | 			<mk-avatar class="avatar" :user="user"/> | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers } from '@fortawesome/free-solid-svg-icons'; | import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons'; | import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import i18n from '../i18n'; | import i18n from '../i18n'; | ||||||
| import XMenu from './menu.vue'; | import XMenu from './menu.vue'; | ||||||
| @@ -60,8 +60,12 @@ export default Vue.extend({ | |||||||
| 				action: this.toggleBlock | 				action: this.toggleBlock | ||||||
| 			}]); | 			}]); | ||||||
|  |  | ||||||
| 			if (this.$store.state.i.isAdmin) { | 			if (this.$store.getters.isSignedIn && (this.$store.state.i.isAdmin || this.$store.state.i.isModerator)) { | ||||||
| 				menu = menu.concat([null, { | 				menu = menu.concat([null, { | ||||||
|  | 					icon: faMicrophoneSlash, | ||||||
|  | 					text: this.user.isSilenced ? this.$t('unsilence') : this.$t('silence'), | ||||||
|  | 					action: this.toggleSilence | ||||||
|  | 				}, { | ||||||
| 					icon: faSnowflake, | 					icon: faSnowflake, | ||||||
| 					text: this.user.isSuspended ? this.$t('unsuspend') : this.$t('suspend'), | 					text: this.user.isSuspended ? this.$t('unsuspend') : this.$t('suspend'), | ||||||
| 					action: this.toggleSuspend | 					action: this.toggleSuspend | ||||||
| @@ -194,6 +198,25 @@ export default Vue.extend({ | |||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | 		async toggleSilence() { | ||||||
|  | 			if (!await this.getConfirmed(this.$t(this.user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return; | ||||||
|  |  | ||||||
|  | 			this.$root.api(this.user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', { | ||||||
|  | 				userId: this.user.id | ||||||
|  | 			}).then(() => { | ||||||
|  | 				this.user.isSilenced = !this.user.isSilenced; | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'success', | ||||||
|  | 					iconOnly: true, autoClose: true | ||||||
|  | 				}); | ||||||
|  | 			}, e => { | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'error', | ||||||
|  | 					text: e | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
| 		async toggleSuspend() { | 		async toggleSuspend() { | ||||||
| 			if (!await this.getConfirmed(this.$t(this.user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return; | 			if (!await this.getConfirmed(this.$t(this.user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,105 +0,0 @@ | |||||||
| <template> |  | ||||||
| <x-window @closed="() => { $emit('closed'); destroyDom(); }" :avatar="user"> |  | ||||||
| 	<template #header><mk-user-name :user="user"/></template> |  | ||||||
| 	<div class="vrcsvlkm"> |  | ||||||
| 		<mk-button @click="resetPassword()" primary>{{ $t('resetPassword') }}</mk-button> |  | ||||||
| 		<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="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch> |  | ||||||
| 	</div> |  | ||||||
| </x-window> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import i18n from '../i18n'; |  | ||||||
| import MkButton from './ui/button.vue'; |  | ||||||
| import MkSwitch from './ui/switch.vue'; |  | ||||||
| import XWindow from './window.vue'; |  | ||||||
|  |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	i18n, |  | ||||||
|  |  | ||||||
| 	components: { |  | ||||||
| 		MkButton, |  | ||||||
| 		MkSwitch, |  | ||||||
| 		XWindow, |  | ||||||
| 	}, |  | ||||||
|  |  | ||||||
| 	props: { |  | ||||||
| 		user: { |  | ||||||
| 			type: Object, |  | ||||||
| 			required: true |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
|  |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			moderator: this.user.isModerator, |  | ||||||
| 			silenced: this.user.isSilenced, |  | ||||||
| 			suspended: this.user.isSuspended, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
|  |  | ||||||
| 	methods: { |  | ||||||
| 		async resetPassword() { |  | ||||||
| 			const dialog = this.$root.dialog({ |  | ||||||
| 				type: 'waiting', |  | ||||||
| 				iconOnly: true |  | ||||||
| 			}); |  | ||||||
|  |  | ||||||
| 			this.$root.api('admin/reset-password', { |  | ||||||
| 				userId: this.user.id, |  | ||||||
| 			}).then(({ password }) => { |  | ||||||
| 				this.$root.dialog({ |  | ||||||
| 					type: 'success', |  | ||||||
| 					text: this.$t('newPasswordIs', { password }) |  | ||||||
| 				}); |  | ||||||
| 			}).catch(e => { |  | ||||||
| 				this.$root.dialog({ |  | ||||||
| 					type: 'error', |  | ||||||
| 					text: e |  | ||||||
| 				}); |  | ||||||
| 			}).finally(() => { |  | ||||||
| 				dialog.close(); |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
|  |  | ||||||
| 		async toggleSilence() { |  | ||||||
| 			const confirm = await this.$root.dialog({ |  | ||||||
| 				type: 'warning', |  | ||||||
| 				showCancelButton: true, |  | ||||||
| 				text: this.silenced ? this.$t('silenceConfirm') : this.$t('unsilenceConfirm'), |  | ||||||
| 			}); |  | ||||||
| 			if (confirm.canceled) { |  | ||||||
| 				this.silenced = !this.silenced; |  | ||||||
| 			} else { |  | ||||||
| 				this.$root.api(this.silenced ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id }); |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
|  |  | ||||||
| 		async toggleSuspend() { |  | ||||||
| 			const confirm = await this.$root.dialog({ |  | ||||||
| 				type: 'warning', |  | ||||||
| 				showCancelButton: true, |  | ||||||
| 				text: this.suspended ? this.$t('suspendConfirm') : this.$t('unsuspendConfirm'), |  | ||||||
| 			}); |  | ||||||
| 			if (confirm.canceled) { |  | ||||||
| 				this.suspended = !this.suspended; |  | ||||||
| 			} else { |  | ||||||
| 				this.$root.api(this.suspended ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id }); |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
|  |  | ||||||
| 		async toggleModerator() { |  | ||||||
| 			this.$root.api(this.moderator ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id }); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .vrcsvlkm { |  | ||||||
|  |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @@ -18,8 +18,9 @@ import PostFormDialog from './components/post-form-dialog.vue'; | |||||||
| import Dialog from './components/dialog.vue'; | import Dialog from './components/dialog.vue'; | ||||||
| import Menu from './components/menu.vue'; | import Menu from './components/menu.vue'; | ||||||
| import { router } from './router'; | import { router } from './router'; | ||||||
| import { applyTheme, lightTheme, builtinThemes } from './theme'; | import { applyTheme, lightTheme } from './theme'; | ||||||
| import { isDeviceDarkmode } from './scripts/is-device-darkmode'; | import { isDeviceDarkmode } from './scripts/is-device-darkmode'; | ||||||
|  | import createStore from './store'; | ||||||
|  |  | ||||||
| Vue.use(Vuex); | Vue.use(Vuex); | ||||||
| Vue.use(VueHotkey); | Vue.use(VueHotkey); | ||||||
| @@ -134,36 +135,39 @@ document.body.setAttribute('ontouchstart', ''); | |||||||
| // アプリ基底要素マウント | // アプリ基底要素マウント | ||||||
| document.body.innerHTML = '<div id="app"></div>'; | document.body.innerHTML = '<div id="app"></div>'; | ||||||
|  |  | ||||||
| const os = new MiOS(); | const store = createStore(); | ||||||
|  |  | ||||||
|  | const os = new MiOS(store); | ||||||
|  |  | ||||||
| os.init(async () => { | os.init(async () => { | ||||||
| 	window.addEventListener('storage', e => { | 	window.addEventListener('storage', e => { | ||||||
| 		if (e.key === 'vuex') { | 		if (e.key === 'vuex') { | ||||||
| 			os.store.replaceState(JSON.parse(localStorage['vuex'])); | 			store.replaceState(JSON.parse(localStorage['vuex'])); | ||||||
| 		} else if (e.key === 'i') { | 		} else if (e.key === 'i') { | ||||||
| 			location.reload(); | 			location.reload(); | ||||||
| 		} | 		} | ||||||
| 	}, false) | 	}, false) | ||||||
|  |  | ||||||
| 	os.store.watch(state => state.device.darkMode, darkMode => { | 	store.watch(state => state.device.darkMode, darkMode => { | ||||||
| 		// TODO: このファイルでbuiltinThemesを参照するとcode splittingが効かず、初回読み込み時に全てのテーマコードを読み込むことになってしまい無駄なので何とかする | 		import('./theme').then(({ builtinThemes }) => { | ||||||
| 		const themes = builtinThemes.concat(os.store.state.device.themes); | 			const themes = builtinThemes.concat(store.state.device.themes); | ||||||
| 		applyTheme(themes.find(x => x.id === (darkMode ? os.store.state.device.darkTheme : os.store.state.device.lightTheme))); | 			applyTheme(themes.find(x => x.id === (darkMode ? store.state.device.darkTheme : store.state.device.lightTheme))); | ||||||
|  | 		}); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	//#region Sync dark mode | 	//#region Sync dark mode | ||||||
| 	if (os.store.state.device.syncDeviceDarkMode) { | 	if (store.state.device.syncDeviceDarkMode) { | ||||||
| 		os.store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() }); | 		store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() }); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { | 	window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { | ||||||
| 		if (os.store.state.device.syncDeviceDarkMode) { | 		if (store.state.device.syncDeviceDarkMode) { | ||||||
| 			os.store.commit('device/set', { key: 'darkMode', value: mql.matches }); | 			store.commit('device/set', { key: 'darkMode', value: mql.matches }); | ||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
| 	//#endregion | 	//#endregion | ||||||
|  |  | ||||||
| 	if ('Notification' in window && os.store.getters.isSignedIn) { | 	if ('Notification' in window && store.getters.isSignedIn) { | ||||||
| 		// 許可を得ていなかったらリクエスト | 		// 許可を得ていなかったらリクエスト | ||||||
| 		if (Notification.permission === 'default') { | 		if (Notification.permission === 'default') { | ||||||
| 			Notification.requestPermission(); | 			Notification.requestPermission(); | ||||||
| @@ -171,7 +175,7 @@ os.init(async () => { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const app = new Vue({ | 	const app = new Vue({ | ||||||
| 		store: os.store, | 		store: store, | ||||||
| 		metaInfo: { | 		metaInfo: { | ||||||
| 			title: null, | 			title: null, | ||||||
| 			titleTemplate: title => title ? `${title} | ${(instanceName || 'Misskey')}` : (instanceName || 'Misskey') | 			titleTemplate: title => title ? `${title} | ${(instanceName || 'Misskey')}` : (instanceName || 'Misskey') | ||||||
| @@ -183,7 +187,7 @@ os.init(async () => { | |||||||
| 			}; | 			}; | ||||||
| 		}, | 		}, | ||||||
| 		methods: { | 		methods: { | ||||||
| 			api: os.api, | 			api: (endpoint: string, data: { [x: string]: any } = {}, token?) => store.dispatch('api', { endpoint, data, token }), | ||||||
| 			signout: os.signout, | 			signout: os.signout, | ||||||
| 			new(vm, props) { | 			new(vm, props) { | ||||||
| 				const x = new vm({ | 				const x = new vm({ | ||||||
| @@ -234,58 +238,63 @@ os.init(async () => { | |||||||
| 	// マウント | 	// マウント | ||||||
| 	app.$mount('#app'); | 	app.$mount('#app'); | ||||||
|  |  | ||||||
| 	if (app.$store.getters.isSignedIn) { | 	os.stream.on('emojiAdded', data => { | ||||||
|  | 		// TODO | ||||||
|  | 		//store.commit('instance/set', ); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	if (store.getters.isSignedIn) { | ||||||
| 		const main = os.stream.useSharedConnection('main'); | 		const main = os.stream.useSharedConnection('main'); | ||||||
|  |  | ||||||
| 		// 自分の情報が更新されたとき | 		// 自分の情報が更新されたとき | ||||||
| 		main.on('meUpdated', i => { | 		main.on('meUpdated', i => { | ||||||
| 			app.$store.dispatch('mergeMe', i); | 			store.dispatch('mergeMe', i); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('readAllNotifications', () => { | 		main.on('readAllNotifications', () => { | ||||||
| 			app.$store.dispatch('mergeMe', { | 			store.dispatch('mergeMe', { | ||||||
| 				hasUnreadNotification: false | 				hasUnreadNotification: false | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('unreadNotification', () => { | 		main.on('unreadNotification', () => { | ||||||
| 			app.$store.dispatch('mergeMe', { | 			store.dispatch('mergeMe', { | ||||||
| 				hasUnreadNotification: true | 				hasUnreadNotification: true | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('unreadMention', () => { | 		main.on('unreadMention', () => { | ||||||
| 			app.$store.dispatch('mergeMe', { | 			store.dispatch('mergeMe', { | ||||||
| 				hasUnreadMentions: true | 				hasUnreadMentions: true | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('readAllUnreadMentions', () => { | 		main.on('readAllUnreadMentions', () => { | ||||||
| 			app.$store.dispatch('mergeMe', { | 			store.dispatch('mergeMe', { | ||||||
| 				hasUnreadMentions: false | 				hasUnreadMentions: false | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('unreadSpecifiedNote', () => { | 		main.on('unreadSpecifiedNote', () => { | ||||||
| 			app.$store.dispatch('mergeMe', { | 			store.dispatch('mergeMe', { | ||||||
| 				hasUnreadSpecifiedNotes: true | 				hasUnreadSpecifiedNotes: true | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('readAllUnreadSpecifiedNotes', () => { | 		main.on('readAllUnreadSpecifiedNotes', () => { | ||||||
| 			app.$store.dispatch('mergeMe', { | 			store.dispatch('mergeMe', { | ||||||
| 				hasUnreadSpecifiedNotes: false | 				hasUnreadSpecifiedNotes: false | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('readAllMessagingMessages', () => { | 		main.on('readAllMessagingMessages', () => { | ||||||
| 			app.$store.dispatch('mergeMe', { | 			store.dispatch('mergeMe', { | ||||||
| 				hasUnreadMessagingMessage: false | 				hasUnreadMessagingMessage: false | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('unreadMessagingMessage', () => { | 		main.on('unreadMessagingMessage', () => { | ||||||
| 			app.$store.dispatch('mergeMe', { | 			store.dispatch('mergeMe', { | ||||||
| 				hasUnreadMessagingMessage: true | 				hasUnreadMessagingMessage: true | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
| @@ -293,13 +302,13 @@ os.init(async () => { | |||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('readAllAntennas', () => { | 		main.on('readAllAntennas', () => { | ||||||
| 			app.$store.dispatch('mergeMe', { | 			store.dispatch('mergeMe', { | ||||||
| 				hasUnreadAntenna: false | 				hasUnreadAntenna: false | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('unreadAntenna', () => { | 		main.on('unreadAntenna', () => { | ||||||
| 			app.$store.dispatch('mergeMe', { | 			store.dispatch('mergeMe', { | ||||||
| 				hasUnreadAntenna: true | 				hasUnreadAntenna: true | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
| @@ -307,13 +316,13 @@ os.init(async () => { | |||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('readAllAnnouncements', () => { | 		main.on('readAllAnnouncements', () => { | ||||||
| 			app.$store.dispatch('mergeMe', { | 			store.dispatch('mergeMe', { | ||||||
| 				hasUnreadAnnouncement: false | 				hasUnreadAnnouncement: false | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		main.on('clientSettingUpdated', x => { | 		main.on('clientSettingUpdated', x => { | ||||||
| 			app.$store.commit('settings/set', { | 			store.commit('settings/set', { | ||||||
| 				key: x.key, | 				key: x.key, | ||||||
| 				value: x.value | 				value: x.value | ||||||
| 			}); | 			}); | ||||||
|   | |||||||
| @@ -2,16 +2,11 @@ import autobind from 'autobind-decorator'; | |||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import { EventEmitter } from 'eventemitter3'; | import { EventEmitter } from 'eventemitter3'; | ||||||
|  |  | ||||||
| import initStore from './store'; |  | ||||||
| import { apiUrl, version } 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'; | ||||||
|  | import store from './store'; | ||||||
| //#region api requests |  | ||||||
| let spinner = null; |  | ||||||
| let pending = 0; |  | ||||||
| //#endregion |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Misskey Operating System |  * Misskey Operating System | ||||||
| @@ -19,7 +14,7 @@ let pending = 0; | |||||||
| export default class MiOS extends EventEmitter { | export default class MiOS extends EventEmitter { | ||||||
| 	public app: Vue; | 	public app: Vue; | ||||||
|  |  | ||||||
| 	public store: ReturnType<typeof initStore>; | 	public store: ReturnType<typeof store>; | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * A connection manager of home stream | 	 * A connection manager of home stream | ||||||
| @@ -31,6 +26,11 @@ export default class MiOS extends EventEmitter { | |||||||
| 	 */ | 	 */ | ||||||
| 	private swRegistration: ServiceWorkerRegistration = null; | 	private swRegistration: ServiceWorkerRegistration = null; | ||||||
|  |  | ||||||
|  | 	constructor(vuex: MiOS['store']) { | ||||||
|  | 		super(); | ||||||
|  | 		this.store = vuex; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	@autobind | 	@autobind | ||||||
| 	public signout() { | 	public signout() { | ||||||
| 		this.store.dispatch('logout'); | 		this.store.dispatch('logout'); | ||||||
| @@ -52,8 +52,6 @@ export default class MiOS extends EventEmitter { | |||||||
| 			}); | 			}); | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
| 		this.store = initStore(this); |  | ||||||
|  |  | ||||||
| 		// ユーザーをフェッチしてコールバックする | 		// ユーザーをフェッチしてコールバックする | ||||||
| 		const fetchme = (token, cb) => { | 		const fetchme = (token, cb) => { | ||||||
| 			let me = null; | 			let me = null; | ||||||
| @@ -187,16 +185,19 @@ export default class MiOS extends EventEmitter { | |||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				// Register | 				// Register | ||||||
| 				this.api('sw/register', { | 				this.store.dispatch('api', { | ||||||
|  | 					endpoint: 'sw/register', | ||||||
|  | 					data: { | ||||||
| 						endpoint: subscription.endpoint, | 						endpoint: subscription.endpoint, | ||||||
| 						auth: encode(subscription.getKey('auth')), | 						auth: encode(subscription.getKey('auth')), | ||||||
| 						publickey: encode(subscription.getKey('p256dh')) | 						publickey: encode(subscription.getKey('p256dh')) | ||||||
|  | 					} | ||||||
| 				}); | 				}); | ||||||
| 			}) | 			}) | ||||||
| 			// When subscribe failed | 			// When subscribe failed | ||||||
| 			.catch(async (err: Error) => { | 			.catch(async (err: Error) => { | ||||||
| 				// 通知が許可されていなかったとき | 				// 通知が許可されていなかったとき | ||||||
| 				if (err.name == 'NotAllowedError') { | 				if (err.name === 'NotAllowedError') { | ||||||
| 					return; | 					return; | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| @@ -214,52 +215,6 @@ export default class MiOS extends EventEmitter { | |||||||
| 		// Register service worker | 		// Register service worker | ||||||
| 		navigator.serviceWorker.register(sw); | 		navigator.serviceWorker.register(sw); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * Misskey APIにリクエストします |  | ||||||
| 	 * @param endpoint エンドポイント名 |  | ||||||
| 	 * @param data パラメータ |  | ||||||
| 	 */ |  | ||||||
| 	@autobind |  | ||||||
| 	public api(endpoint: string, data: { [x: string]: any } = {}, token?): Promise<{ [x: string]: any }> { |  | ||||||
| 		if (++pending === 1) { |  | ||||||
| 			spinner = document.createElement('div'); |  | ||||||
| 			spinner.setAttribute('id', 'wait'); |  | ||||||
| 			document.body.appendChild(spinner); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		const onFinally = () => { |  | ||||||
| 			if (--pending === 0) spinner.parentNode.removeChild(spinner); |  | ||||||
| 		}; |  | ||||||
|  |  | ||||||
| 		const promise = new Promise((resolve, reject) => { |  | ||||||
| 			// Append a credential |  | ||||||
| 			if (this.store.getters.isSignedIn) (data as any).i = this.store.state.i.token; |  | ||||||
| 			if (token) (data as any).i = token; |  | ||||||
|  |  | ||||||
| 			// Send request |  | ||||||
| 			fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { |  | ||||||
| 				method: 'POST', |  | ||||||
| 				body: JSON.stringify(data), |  | ||||||
| 				credentials: 'omit', |  | ||||||
| 				cache: 'no-cache' |  | ||||||
| 			}).then(async (res) => { |  | ||||||
| 				const body = res.status === 204 ? null : await res.json(); |  | ||||||
|  |  | ||||||
| 				if (res.status === 200) { |  | ||||||
| 					resolve(body); |  | ||||||
| 				} else if (res.status === 204) { |  | ||||||
| 					resolve(); |  | ||||||
| 				} else { |  | ||||||
| 					reject(body.error); |  | ||||||
| 				} |  | ||||||
| 			}).catch(reject); |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		promise.then(onFinally, onFinally); |  | ||||||
|  |  | ||||||
| 		return promise; |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -99,10 +99,19 @@ | |||||||
| 			<span class="label">{{ $t('operations') }}</span> | 			<span class="label">{{ $t('operations') }}</span> | ||||||
| 			<mk-switch v-model="isSuspended" class="switch">{{ $t('stopActivityDelivery') }}</mk-switch> | 			<mk-switch v-model="isSuspended" class="switch">{{ $t('stopActivityDelivery') }}</mk-switch> | ||||||
| 			<mk-switch :value="isBlocked" class="switch" @change="changeBlock">{{ $t('blockThisInstance') }}</mk-switch> | 			<mk-switch :value="isBlocked" class="switch" @change="changeBlock">{{ $t('blockThisInstance') }}</mk-switch> | ||||||
|  | 			<details> | ||||||
|  | 				<summary>{{ $t('deleteAllFiles') }}</summary> | ||||||
|  | 				<mk-button @click="deleteAllFiles()" style="margin: 0.5em 0 0.5em 0;"><fa :icon="faTrashAlt"/> {{ $t('deleteAllFiles') }}</mk-button> | ||||||
|  | 			</details> | ||||||
|  | 			<details> | ||||||
|  | 				<summary>{{ $t('removeAllFollowing') }}</summary> | ||||||
|  | 				<mk-button @click="removeAllFollowing()" style="margin: 0.5em 0 0.5em 0;"><fa :icon="faMinusCircle"/> {{ $t('removeAllFollowing') }}</mk-button> | ||||||
|  | 				<mk-info warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</mk-info> | ||||||
|  | 			</details> | ||||||
| 		</div> | 		</div> | ||||||
| 		<details class="metadata"> | 		<details class="metadata"> | ||||||
| 			<summary class="label">{{ $t('metadata') }}</summary> | 			<summary class="label">{{ $t('metadata') }}</summary> | ||||||
| 			<pre><code>{{ JSON.stringify(instance.metadata, null, 2) }}</code></pre> | 			<pre><code>{{ JSON.stringify(instance, null, 2) }}</code></pre> | ||||||
| 		</details> | 		</details> | ||||||
| 	</div> | 	</div> | ||||||
| </x-window> | </x-window> | ||||||
| @@ -112,11 +121,13 @@ | |||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import Chart from 'chart.js'; | import Chart from 'chart.js'; | ||||||
| import i18n from '../../i18n'; | import i18n from '../../i18n'; | ||||||
| import { faTimes, faCrosshairs, faCloudDownloadAlt, faCloudUploadAlt, faUsers, faPencilAlt, faFileImage, faDatabase, faTrafficLight, faLongArrowAltUp, faLongArrowAltDown } from '@fortawesome/free-solid-svg-icons'; | import { faTimes, faCrosshairs, faCloudDownloadAlt, faCloudUploadAlt, faUsers, faPencilAlt, faFileImage, faDatabase, faTrafficLight, faLongArrowAltUp, faLongArrowAltDown, faMinusCircle, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import XWindow from '../../components/window.vue'; | import XWindow from '../../components/window.vue'; | ||||||
| import MkUsersDialog from '../../components/users-dialog.vue'; | import MkUsersDialog from '../../components/users-dialog.vue'; | ||||||
| import MkSelect from '../../components/ui/select.vue'; | import MkSelect from '../../components/ui/select.vue'; | ||||||
|  | import MkButton from '../../components/ui/button.vue'; | ||||||
| import MkSwitch from '../../components/ui/switch.vue'; | import MkSwitch from '../../components/ui/switch.vue'; | ||||||
|  | import MkInfo from '../../components/ui/info.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)); | ||||||
| @@ -135,7 +146,9 @@ export default Vue.extend({ | |||||||
| 	components: { | 	components: { | ||||||
| 		XWindow, | 		XWindow, | ||||||
| 		MkSelect, | 		MkSelect, | ||||||
|  | 		MkButton, | ||||||
| 		MkSwitch, | 		MkSwitch, | ||||||
|  | 		MkInfo, | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	props: { | 	props: { | ||||||
| @@ -153,7 +166,7 @@ export default Vue.extend({ | |||||||
| 			chartInstance: null, | 			chartInstance: null, | ||||||
| 			chartSrc: 'requests', | 			chartSrc: 'requests', | ||||||
| 			chartSpan: 'hour', | 			chartSpan: 'hour', | ||||||
| 			faTimes, faCrosshairs, faCloudDownloadAlt, faCloudUploadAlt, faUsers, faPencilAlt, faFileImage, faDatabase, faTrafficLight, faLongArrowAltUp, faLongArrowAltDown | 			faTimes, faCrosshairs, faCloudDownloadAlt, faCloudUploadAlt, faUsers, faPencilAlt, faFileImage, faDatabase, faTrafficLight, faLongArrowAltUp, faLongArrowAltDown, faMinusCircle, faTrashAlt | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| @@ -239,6 +252,28 @@ export default Vue.extend({ | |||||||
| 			this.chartSrc = src; | 			this.chartSrc = src; | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | 		removeAllFollowing() { | ||||||
|  | 			this.$root.api('admin/federation/remove-all-following', { | ||||||
|  | 				host: this.instance.host | ||||||
|  | 			}).then(() => { | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'success', | ||||||
|  | 					iconOnly: true, autoClose: true | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		deleteAllFiles() { | ||||||
|  | 			this.$root.api('admin/federation/delete-all-files', { | ||||||
|  | 				host: this.instance.host | ||||||
|  | 			}).then(() => { | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'success', | ||||||
|  | 					iconOnly: true, autoClose: true | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
| 		renderChart() { | 		renderChart() { | ||||||
| 			if (this.chartInstance) { | 			if (this.chartInstance) { | ||||||
| 				this.chartInstance.destroy(); | 				this.chartInstance.destroy(); | ||||||
|   | |||||||
| @@ -116,6 +116,7 @@ | |||||||
| 					<mk-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Secret key</mk-input> | 					<mk-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa :icon="faKey"/></template>Secret key</mk-input> | ||||||
| 				</div> | 				</div> | ||||||
| 				<mk-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('objectStorageUseSSL') }}<template #desc>{{ $t('objectStorageUseSSLDesc') }}</template></mk-switch> | 				<mk-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('objectStorageUseSSL') }}<template #desc>{{ $t('objectStorageUseSSLDesc') }}</template></mk-switch> | ||||||
|  | 				<mk-switch v-model="objectStorageUseProxy" :disabled="!useObjectStorage">{{ $t('objectStorageUseProxy') }}<template #desc>{{ $t('objectStorageUseProxyDesc') }}</template></mk-switch> | ||||||
| 			</template> | 			</template> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="_footer"> | 		<div class="_footer"> | ||||||
| @@ -249,6 +250,7 @@ export default Vue.extend({ | |||||||
| 			objectStorageAccessKey: null, | 			objectStorageAccessKey: null, | ||||||
| 			objectStorageSecretKey: null, | 			objectStorageSecretKey: null, | ||||||
| 			objectStorageUseSSL: false, | 			objectStorageUseSSL: false, | ||||||
|  | 			objectStorageUseProxy: false, | ||||||
| 			enableTwitterIntegration: false, | 			enableTwitterIntegration: false, | ||||||
| 			twitterConsumerKey: null, | 			twitterConsumerKey: null, | ||||||
| 			twitterConsumerSecret: null, | 			twitterConsumerSecret: null, | ||||||
| @@ -303,6 +305,7 @@ export default Vue.extend({ | |||||||
| 		this.objectStorageAccessKey = this.meta.objectStorageAccessKey; | 		this.objectStorageAccessKey = this.meta.objectStorageAccessKey; | ||||||
| 		this.objectStorageSecretKey = this.meta.objectStorageSecretKey; | 		this.objectStorageSecretKey = this.meta.objectStorageSecretKey; | ||||||
| 		this.objectStorageUseSSL = this.meta.objectStorageUseSSL; | 		this.objectStorageUseSSL = this.meta.objectStorageUseSSL; | ||||||
|  | 		this.objectStorageUseProxy = this.meta.objectStorageUseProxy; | ||||||
| 		this.enableTwitterIntegration = this.meta.enableTwitterIntegration; | 		this.enableTwitterIntegration = this.meta.enableTwitterIntegration; | ||||||
| 		this.twitterConsumerKey = this.meta.twitterConsumerKey; | 		this.twitterConsumerKey = this.meta.twitterConsumerKey; | ||||||
| 		this.twitterConsumerSecret = this.meta.twitterConsumerSecret; | 		this.twitterConsumerSecret = this.meta.twitterConsumerSecret; | ||||||
| @@ -411,6 +414,7 @@ export default Vue.extend({ | |||||||
| 				objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null, | 				objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null, | ||||||
| 				objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null, | 				objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null, | ||||||
| 				objectStorageUseSSL: this.objectStorageUseSSL, | 				objectStorageUseSSL: this.objectStorageUseSSL, | ||||||
|  | 				objectStorageUseProxy: this.objectStorageUseProxy, | ||||||
| 				enableTwitterIntegration: this.enableTwitterIntegration, | 				enableTwitterIntegration: this.enableTwitterIntegration, | ||||||
| 				twitterConsumerKey: this.twitterConsumerKey, | 				twitterConsumerKey: this.twitterConsumerKey, | ||||||
| 				twitterConsumerSecret: this.twitterConsumerSecret, | 				twitterConsumerSecret: this.twitterConsumerSecret, | ||||||
|   | |||||||
							
								
								
									
										209
									
								
								src/client/pages/instance/users.user.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								src/client/pages/instance/users.user.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | |||||||
|  | <template> | ||||||
|  | <div class="vrcsvlkm" v-if="user && info"> | ||||||
|  | 	<portal to="title" v-if="user"><mk-user-name :user="user" :nowrap="false" class="name"/></portal> | ||||||
|  | 	<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal> | ||||||
|  |  | ||||||
|  | 	<section class="_card"> | ||||||
|  | 		<div class="_title"> | ||||||
|  | 			<mk-avatar class="avatar" :user="user"/> | ||||||
|  | 			<mk-user-name class="name" :user="user"/> | ||||||
|  | 			<span class="acct">@{{ user | acct }}</span> | ||||||
|  | 			<span class="staff" v-if="user.isAdmin"><fa :icon="faBookmark"/></span> | ||||||
|  | 			<span class="staff" v-if="user.isModerator"><fa :icon="farBookmark"/></span> | ||||||
|  | 			<span class="punished" v-if="user.isSilenced"><fa :icon="faMicrophoneSlash"/></span> | ||||||
|  | 			<span class="punished" v-if="user.isSuspended"><fa :icon="faSnowflake"/></span> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="_content actions"> | ||||||
|  | 			<div style="flex: 1; padding-left: 1em;"> | ||||||
|  | 				<mk-switch v-if="user.host == null && $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="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch> | ||||||
|  | 			</div> | ||||||
|  | 			<div style="flex: 1; padding-left: 1em;"> | ||||||
|  | 				<mk-button @click="openProfile"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('profile')}}</mk-button> | ||||||
|  | 				<mk-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('updateRemoteUser') }}</mk-button> | ||||||
|  | 				<mk-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('resetPassword') }}</mk-button> | ||||||
|  | 				<mk-button @click="deleteAllFiles"><fa :icon="faTrashAlt"/> {{ $t('deleteAllFiles') }}</mk-button> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="_content rawdata"> | ||||||
|  | 			<pre><code>{{ JSON.stringify(info, null, 2) }}</code></pre> | ||||||
|  | 		</div> | ||||||
|  | 	</section> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import { faTimes, faBookmark, faKey, faSync, faMicrophoneSlash, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import { faSnowflake, faTrashAlt, faBookmark as farBookmark  } from '@fortawesome/free-regular-svg-icons'; | ||||||
|  | import MkButton from '../../components/ui/button.vue'; | ||||||
|  | import MkSwitch from '../../components/ui/switch.vue'; | ||||||
|  | import i18n from '../../i18n'; | ||||||
|  | import Progress from '../../scripts/loading'; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	i18n, | ||||||
|  |  | ||||||
|  | 	components: { | ||||||
|  | 		MkButton, | ||||||
|  | 		MkSwitch, | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			user: null, | ||||||
|  | 			info: null, | ||||||
|  | 			moderator: false, | ||||||
|  | 			silenced: false, | ||||||
|  | 			suspended: false, | ||||||
|  | 			faTimes, faBookmark, farBookmark, faKey, faSync, faMicrophoneSlash, faSnowflake, faTrashAlt, faExternalLinkSquareAlt | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	watch: { | ||||||
|  | 		$route: 'fetch' | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	created() { | ||||||
|  | 		this.fetch(); | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	methods: { | ||||||
|  | 		async fetch() { | ||||||
|  | 			Progress.start(); | ||||||
|  | 			this.user = await this.$root.api('users/show', { userId: this.$route.params.user }); | ||||||
|  | 			this.info = await this.$root.api('admin/show-user', { userId: this.$route.params.user }); | ||||||
|  | 			this.moderator = this.info.isModerator; | ||||||
|  | 			this.silenced = this.info.isSilenced; | ||||||
|  | 			this.suspended = this.info.isSuspended; | ||||||
|  | 			Progress.done(); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		/** 処理対象ユーザーの情報を更新する */ | ||||||
|  | 		async refreshUser() { | ||||||
|  | 			this.user = await this.$root.api('users/show', { userId: this.user.id }); | ||||||
|  | 			this.info = await this.$root.api('admin/show-user', { userId: this.user.id }); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		openProfile() { | ||||||
|  | 			window.open(Vue.filter('userPage')(this.user, null, true), '_blank'); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		async updateRemoteUser() { | ||||||
|  | 			await this.$root.api('admin/update-remote-user', { userId: this.user.id }).then(res => { | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'success', | ||||||
|  | 					iconOnly: true, autoClose: true | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 			await this.refreshUser(); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		async resetPassword() { | ||||||
|  | 			const dialog = this.$root.dialog({ | ||||||
|  | 				type: 'waiting', | ||||||
|  | 				iconOnly: true | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			this.$root.api('admin/reset-password', { | ||||||
|  | 				userId: this.user.id, | ||||||
|  | 			}).then(({ password }) => { | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'success', | ||||||
|  | 					text: this.$t('newPasswordIs', { password }) | ||||||
|  | 				}); | ||||||
|  | 			}).catch(e => { | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'error', | ||||||
|  | 					text: e | ||||||
|  | 				}); | ||||||
|  | 			}).finally(() => { | ||||||
|  | 				dialog.close(); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		async toggleSilence() { | ||||||
|  | 			const confirm = await this.$root.dialog({ | ||||||
|  | 				type: 'warning', | ||||||
|  | 				showCancelButton: true, | ||||||
|  | 				text: this.silenced ? this.$t('silenceConfirm') : this.$t('unsilenceConfirm'), | ||||||
|  | 			}); | ||||||
|  | 			if (confirm.canceled) { | ||||||
|  | 				this.silenced = !this.silenced; | ||||||
|  | 			} else { | ||||||
|  | 				await this.$root.api(this.silenced ? 'admin/silence-user' : 'admin/unsilence-user', { userId: this.user.id }); | ||||||
|  | 				await this.refreshUser(); | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		async toggleSuspend() { | ||||||
|  | 			const confirm = await this.$root.dialog({ | ||||||
|  | 				type: 'warning', | ||||||
|  | 				showCancelButton: true, | ||||||
|  | 				text: this.suspended ? this.$t('suspendConfirm') : this.$t('unsuspendConfirm'), | ||||||
|  | 			}); | ||||||
|  | 			if (confirm.canceled) { | ||||||
|  | 				this.suspended = !this.suspended; | ||||||
|  | 			} else { | ||||||
|  | 				await this.$root.api(this.suspended ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: this.user.id }); | ||||||
|  | 				await this.refreshUser(); | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		async toggleModerator() { | ||||||
|  | 			await this.$root.api(this.moderator ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: this.user.id }); | ||||||
|  | 			await this.refreshUser(); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		async deleteAllFiles() { | ||||||
|  | 			const confirm = await this.$root.dialog({ | ||||||
|  | 				type: 'warning', | ||||||
|  | 				showCancelButton: true, | ||||||
|  | 				text: this.$t('deleteAllFilesConfirm'), | ||||||
|  | 			}); | ||||||
|  | 			if (confirm.canceled) return; | ||||||
|  | 			const process = async () => { | ||||||
|  | 				await this.$root.api('admin/delete-all-files-of-a-user', { userId: this.user.id }); | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'success', | ||||||
|  | 					iconOnly: true, autoClose: true | ||||||
|  | 				}); | ||||||
|  | 			}; | ||||||
|  | 			await process().catch(e => { | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'error', | ||||||
|  | 					text: e.toString() | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 			await this.refreshUser(); | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .vrcsvlkm { | ||||||
|  | 	display: flex; | ||||||
|  | 	flex-direction: column; | ||||||
|  |  | ||||||
|  | 	> ._card { | ||||||
|  | 		> .actions { | ||||||
|  | 			display: flex; | ||||||
|  | 			box-sizing: border-box; | ||||||
|  | 			text-align: left; | ||||||
|  | 			align-items: center; | ||||||
|  | 			margin-top: 16px; | ||||||
|  | 			margin-bottom: 16px; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		> .rawdata { | ||||||
|  | 			> pre > code { | ||||||
|  | 				display: block; | ||||||
|  | 				width: 100%; | ||||||
|  | 				height: 100%; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -12,19 +12,65 @@ | |||||||
| 			<mk-button @click="showUser()" primary><fa :icon="faSearch"/> {{ $t('lookup') }}</mk-button> | 			<mk-button @click="showUser()" primary><fa :icon="faSearch"/> {{ $t('lookup') }}</mk-button> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="_footer"> | 		<div class="_footer"> | ||||||
| 			<mk-button inline primary @click="search()"><fa :icon="faSearch"/> {{ $t('search') }}</mk-button> | 			<mk-button inline primary @click="searchUser()"><fa :icon="faSearch"/> {{ $t('search') }}</mk-button> | ||||||
| 		</div> | 		</div> | ||||||
| 	</section> | 	</section> | ||||||
|  |  | ||||||
| 	<section class="_card users"> | 	<section class="_card users"> | ||||||
| 		<div class="_title"><fa :icon="faUsers"/> {{ $t('users') }}</div> | 		<div class="_title"><fa :icon="faUsers"/> {{ $t('users') }}</div> | ||||||
|  | 		<div class="_content"> | ||||||
|  | 			<div class="inputs" style="display: flex;"> | ||||||
|  | 				<mk-select v-model="sort" style="margin: 0; flex: 1;"> | ||||||
|  | 					<template #label>{{ $t('sort') }}</template> | ||||||
|  | 					<option value="-createdAt">{{ $t('registeredDate') }} ({{ $t('ascendingOrder') }})</option> | ||||||
|  | 					<option value="+createdAt">{{ $t('registeredDate') }} ({{ $t('descendingOrder') }})</option> | ||||||
|  | 					<option value="-updatedAt">{{ $t('lastUsed') }} ({{ $t('ascendingOrder') }})</option> | ||||||
|  | 					<option value="+updatedAt">{{ $t('lastUsed') }} ({{ $t('descendingOrder') }})</option> | ||||||
|  | 				</mk-select> | ||||||
|  | 				<mk-select v-model="state" style="margin: 0; flex: 1;"> | ||||||
|  | 					<template #label>{{ $t('state') }}</template> | ||||||
|  | 					<option value="all">{{ $t('all') }}</option> | ||||||
|  | 					<option value="available">{{ $t('normal') }}</option> | ||||||
|  | 					<option value="admin">{{ $t('administrator') }}</option> | ||||||
|  | 					<option value="moderator">{{ $t('moderator') }}</option> | ||||||
|  | 					<option value="silenced">{{ $t('silence') }}</option> | ||||||
|  | 					<option value="suspended">{{ $t('suspend') }}</option> | ||||||
|  | 				</mk-select> | ||||||
|  | 				<mk-select v-model="origin" style="margin: 0; flex: 1;"> | ||||||
|  | 					<template #label>{{ $t('instance') }}</template> | ||||||
|  | 					<option value="combined">{{ $t('all') }}</option> | ||||||
|  | 					<option value="local">{{ $t('local') }}</option> | ||||||
|  | 					<option value="remote">{{ $t('remote') }}</option> | ||||||
|  | 				</mk-select> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="inputs" style="display: flex; padding-top: 1.2em;"> | ||||||
|  | 				<mk-input v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @input="$refs.users.reload()"> | ||||||
|  | 					<span>{{ $t('username') }}</span> | ||||||
|  | 				</mk-input> | ||||||
|  | 				<mk-input v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @input="$refs.users.reload()" :disabled="pagination.params().origin === 'local'"> | ||||||
|  | 					<span>{{ $t('host') }}</span> | ||||||
|  | 				</mk-input> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
| 		<div class="_content _list"> | 		<div class="_content _list"> | ||||||
| 			<mk-pagination :pagination="pagination" #default="{items}" class="users" ref="users" :auto-margin="false"> | 			<mk-pagination :pagination="pagination" #default="{items}" class="users" ref="users" :auto-margin="false"> | ||||||
| 				<button class="user _button _listItem" v-for="(user, i) in items" :key="user.id" @click="show(user)"> | 				<button class="user _button _listItem" v-for="(user, i) in items" :key="user.id" @click="show(user)"> | ||||||
| 					<mk-avatar :user="user" class="avatar"/> | 					<mk-avatar class="avatar" :user="user" :disable-link="true"/> | ||||||
| 					<div class="body"> | 					<div class="body"> | ||||||
| 						<mk-user-name :user="user" class="name"/> | 						<header> | ||||||
| 						<mk-acct :user="user" class="acct"/> | 							<mk-user-name class="name" :user="user"/> | ||||||
|  | 							<span class="acct">@{{ user | acct }}</span> | ||||||
|  | 							<span class="staff" v-if="user.isAdmin"><fa :icon="faBookmark"/></span> | ||||||
|  | 							<span class="staff" v-if="user.isModerator"><fa :icon="farBookmark"/></span> | ||||||
|  | 							<span class="punished" v-if="user.isSilenced"><fa :icon="faMicrophoneSlash"/></span> | ||||||
|  | 							<span class="punished" v-if="user.isSuspended"><fa :icon="faSnowflake"/></span> | ||||||
|  | 						</header> | ||||||
|  | 						<div> | ||||||
|  | 							<span>{{ $t('lastUsed') }}: <mk-time :time="user.updatedAt" mode="detail"/></span> | ||||||
|  | 						</div> | ||||||
|  | 						<div> | ||||||
|  | 							<span>{{ $t('registeredDate') }}: <mk-time :time="user.createdAt" mode="detail"/></span> | ||||||
|  | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 				</button> | 				</button> | ||||||
| 			</mk-pagination> | 			</mk-pagination> | ||||||
| @@ -38,12 +84,13 @@ | |||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import { faPlus, faUsers, faSearch } from '@fortawesome/free-solid-svg-icons'; | import { faPlus, faUsers, faSearch, faBookmark, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import { faSnowflake, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import parseAcct from '../../../misc/acct/parse'; | import parseAcct from '../../../misc/acct/parse'; | ||||||
| import MkButton from '../../components/ui/button.vue'; | import MkButton from '../../components/ui/button.vue'; | ||||||
| import MkInput from '../../components/ui/input.vue'; | import MkInput from '../../components/ui/input.vue'; | ||||||
|  | import MkSelect from '../../components/ui/select.vue'; | ||||||
| import MkPagination from '../../components/ui/pagination.vue'; | import MkPagination from '../../components/ui/pagination.vue'; | ||||||
| import MkUserModerateDialog from '../../components/user-moderate-dialog.vue'; |  | ||||||
| import MkUserSelect from '../../components/user-select.vue'; | import MkUserSelect from '../../components/user-select.vue'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| @@ -56,24 +103,46 @@ export default Vue.extend({ | |||||||
| 	components: { | 	components: { | ||||||
| 		MkButton, | 		MkButton, | ||||||
| 		MkInput, | 		MkInput, | ||||||
|  | 		MkSelect, | ||||||
| 		MkPagination, | 		MkPagination, | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  | 			target: '', | ||||||
|  | 			sort: '+createdAt', | ||||||
|  | 			state: 'all', | ||||||
|  | 			origin: 'local', | ||||||
|  | 			searchUsername: '', | ||||||
|  | 			searchHost: '', | ||||||
| 			pagination: { | 			pagination: { | ||||||
| 				endpoint: 'admin/show-users', | 				endpoint: 'admin/show-users', | ||||||
| 				limit: 10, | 				limit: 10, | ||||||
| 				params: () => ({ | 				params: () => ({ | ||||||
| 					sort: '+createdAt' | 					sort: this.sort, | ||||||
|  | 					state: this.state, | ||||||
|  | 					origin: this.origin, | ||||||
|  | 					username: this.searchUsername, | ||||||
|  | 					hostname: this.searchHost, | ||||||
| 				}), | 				}), | ||||||
| 				offsetMode: true | 				offsetMode: true | ||||||
| 			}, | 			}, | ||||||
| 			target: '', | 			faPlus, faUsers, faSearch, faBookmark, farBookmark, faMicrophoneSlash, faSnowflake | ||||||
| 			faPlus, faUsers, faSearch |  | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | 	watch: { | ||||||
|  | 		sort() { | ||||||
|  | 			this.$refs.users.reload(); | ||||||
|  | 		}, | ||||||
|  | 		state() { | ||||||
|  | 			this.$refs.users.reload(); | ||||||
|  | 		}, | ||||||
|  | 		origin() { | ||||||
|  | 			this.$refs.users.reload(); | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  |  | ||||||
| 	methods: { | 	methods: { | ||||||
| 		/** テキストエリアのユーザーを解決する */ | 		/** テキストエリアのユーザーを解決する */ | ||||||
| 		fetchUser() { | 		fetchUser() { | ||||||
| @@ -105,12 +174,16 @@ export default Vue.extend({ | |||||||
| 		/** テキストエリアから処理対象ユーザーを設定する */ | 		/** テキストエリアから処理対象ユーザーを設定する */ | ||||||
| 		async showUser() { | 		async showUser() { | ||||||
| 			const user = await this.fetchUser(); | 			const user = await this.fetchUser(); | ||||||
| 			this.$root.api('admin/show-user', { userId: user.id }).then(info => { | 			this.show(user); | ||||||
| 				this.show(user, info); |  | ||||||
| 			}); |  | ||||||
| 			this.target = ''; | 			this.target = ''; | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | 		searchUser() { | ||||||
|  | 			this.$root.new(MkUserSelect, {}).$once('selected', user => { | ||||||
|  | 				this.show(user); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
| 		async addUser() { | 		async addUser() { | ||||||
| 			const { canceled: canceled1, result: username } = await this.$root.dialog({ | 			const { canceled: canceled1, result: username } = await this.$root.dialog({ | ||||||
| 				title: this.$t('username'), | 				title: this.$t('username'), | ||||||
| @@ -148,19 +221,8 @@ export default Vue.extend({ | |||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		async show(user, info) { | 		async show(user) { | ||||||
| 			if (info == null) info = await this.$root.api('admin/show-user', { userId: user.id }); | 			this.$router.push('./users/' + user.id); | ||||||
| 			this.$root.new(MkUserModerateDialog, { |  | ||||||
| 				user: { ...user, ...info } |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
|  |  | ||||||
| 		search() { |  | ||||||
| 			this.$root.new(MkUserSelect, {}).$once('selected', user => { |  | ||||||
| 				this.$root.api('admin/show-user', { userId: user.id }).then(info => { |  | ||||||
| 					this.show(user, info); |  | ||||||
| 				}); |  | ||||||
| 			}); |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| @@ -182,20 +244,38 @@ export default Vue.extend({ | |||||||
| 					align-items: center; | 					align-items: center; | ||||||
|  |  | ||||||
| 					> .avatar { | 					> .avatar { | ||||||
| 						width: 50px; | 						width: 64px; | ||||||
| 						height: 50px; | 						height: 64px; | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 					> .body { | 					> .body { | ||||||
|  | 						margin-left: 0.3em; | ||||||
| 						padding: 8px; | 						padding: 8px; | ||||||
|  | 						flex: 1; | ||||||
|  |  | ||||||
|  | 						@media (max-width 500px) { | ||||||
|  | 							font-size: 14px; | ||||||
|  | 						} | ||||||
|  |  | ||||||
|  | 						> header { | ||||||
| 							> .name { | 							> .name { | ||||||
| 							display: block; |  | ||||||
| 								font-weight: bold; | 								font-weight: bold; | ||||||
| 							} | 							} | ||||||
|  |  | ||||||
| 							> .acct { | 							> .acct { | ||||||
| 							opacity: 0.5; | 								margin-left: 8px; | ||||||
|  | 								opacity: 0.7; | ||||||
|  | 							} | ||||||
|  |  | ||||||
|  | 							> .staff { | ||||||
|  | 								margin-left: 0.5em; | ||||||
|  | 								color: var(--badge); | ||||||
|  | 							} | ||||||
|  |  | ||||||
|  | 							> .punished { | ||||||
|  | 								margin-left: 0.5em; | ||||||
|  | 								color: #4dabf7; | ||||||
|  | 							} | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ | |||||||
| 		<x-notes v-if="showNext" ref="next" :pagination="next"/> | 		<x-notes v-if="showNext" ref="next" :pagination="next"/> | ||||||
| 		<hr v-if="showNext"/> | 		<hr v-if="showNext"/> | ||||||
|  |  | ||||||
| 		<mk-remote-caution v-if="note.user.host != null" :href="note.uri" style="margin-bottom: var(--margin)"/> | 		<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/> | ||||||
| 		<x-note :note="note" :key="note.id" :detail="true"/> | 		<x-note :note="note" :key="note.id" :detail="true"/> | ||||||
| 		<div v-if="error"> | 		<div v-if="error"> | ||||||
| 			<mk-error @retry="fetch()"/> | 			<mk-error @retry="fetch()"/> | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ | |||||||
| 			<option value="dialog">{{ $t('_pages.blocks._button._action.dialog') }}</option> | 			<option value="dialog">{{ $t('_pages.blocks._button._action.dialog') }}</option> | ||||||
| 			<option value="resetRandom">{{ $t('_pages.blocks._button._action.resetRandom') }}</option> | 			<option value="resetRandom">{{ $t('_pages.blocks._button._action.resetRandom') }}</option> | ||||||
| 			<option value="pushEvent">{{ $t('_pages.blocks._button._action.pushEvent') }}</option> | 			<option value="pushEvent">{{ $t('_pages.blocks._button._action.pushEvent') }}</option> | ||||||
|  | 			<option value="callAiScript">{{ $t('_pages.blocks._button._action.callAiScript') }}</option> | ||||||
| 		</mk-select> | 		</mk-select> | ||||||
| 		<template v-if="value.action === 'dialog'"> | 		<template v-if="value.action === 'dialog'"> | ||||||
| 			<mk-input v-model="value.content"><span>{{ $t('_pages.blocks._button._action._dialog.content') }}</span></mk-input> | 			<mk-input v-model="value.content"><span>{{ $t('_pages.blocks._button._action._dialog.content') }}</span></mk-input> | ||||||
| @@ -20,15 +21,18 @@ | |||||||
| 			<mk-select v-model="value.var"> | 			<mk-select v-model="value.var"> | ||||||
| 				<template #label>{{ $t('_pages.blocks._button._action._pushEvent.variable') }}</template> | 				<template #label>{{ $t('_pages.blocks._button._action._pushEvent.variable') }}</template> | ||||||
| 				<option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option> | 				<option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option> | ||||||
| 				<option v-for="v in aiScript.getVarsByType()" :value="v.name">{{ v.name }}</option> | 				<option v-for="v in aoiScript.getVarsByType()" :value="v.name">{{ v.name }}</option> | ||||||
| 				<optgroup :label="$t('_pages.script.pageVariables')"> | 				<optgroup :label="$t('_pages.script.pageVariables')"> | ||||||
| 					<option v-for="v in aiScript.getPageVarsByType()" :value="v">{{ v }}</option> | 					<option v-for="v in aoiScript.getPageVarsByType()" :value="v">{{ v }}</option> | ||||||
| 				</optgroup> | 				</optgroup> | ||||||
| 				<optgroup :label="$t('_pages.script.enviromentVariables')"> | 				<optgroup :label="$t('_pages.script.enviromentVariables')"> | ||||||
| 					<option v-for="v in aiScript.getEnvVarsByType()" :value="v">{{ v }}</option> | 					<option v-for="v in aoiScript.getEnvVarsByType()" :value="v">{{ v }}</option> | ||||||
| 				</optgroup> | 				</optgroup> | ||||||
| 			</mk-select> | 			</mk-select> | ||||||
| 		</template> | 		</template> | ||||||
|  | 		<template v-else-if="value.action === 'callAiScript'"> | ||||||
|  | 			<mk-input v-model="value.fn"><span>{{ $t('_pages.blocks._button._action._callAiScript.functionName') }}</span></mk-input> | ||||||
|  | 		</template> | ||||||
| 	</section> | 	</section> | ||||||
| </x-container> | </x-container> | ||||||
| </template> | </template> | ||||||
| @@ -53,7 +57,7 @@ export default Vue.extend({ | |||||||
| 		value: { | 		value: { | ||||||
| 			required: true | 			required: true | ||||||
| 		}, | 		}, | ||||||
| 		aiScript: { | 		aoiScript: { | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| @@ -72,6 +76,7 @@ export default Vue.extend({ | |||||||
| 		if (this.value.message == null) Vue.set(this.value, 'message', null); | 		if (this.value.message == null) Vue.set(this.value, 'message', null); | ||||||
| 		if (this.value.primary == null) Vue.set(this.value, 'primary', false); | 		if (this.value.primary == null) Vue.set(this.value, 'primary', false); | ||||||
| 		if (this.value.var == null) Vue.set(this.value, 'var', null); | 		if (this.value.var == null) Vue.set(this.value, 'var', null); | ||||||
|  | 		if (this.value.fn == null) Vue.set(this.value, 'fn', null); | ||||||
| 	}, | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								src/client/pages/page-editor/els/page-editor.el.canvas.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/client/pages/page-editor/els/page-editor.el.canvas.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | <template> | ||||||
|  | <x-container @remove="() => $emit('remove')" :draggable="true"> | ||||||
|  | 	<template #header><fa :icon="faPaintBrush"/> {{ $t('_pages.blocks.canvas') }}</template> | ||||||
|  |  | ||||||
|  | 	<section style="padding: 0 16px 0 16px;"> | ||||||
|  | 		<mk-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('_pages.blocks._canvas.id') }}</span></mk-input> | ||||||
|  | 		<mk-input v-model="value.width" type="number"><span>{{ $t('_pages.blocks._canvas.width') }}</span><template #suffix>px</template></mk-input> | ||||||
|  | 		<mk-input v-model="value.height" type="number"><span>{{ $t('_pages.blocks._canvas.height') }}</span><template #suffix>px</template></mk-input> | ||||||
|  | 	</section> | ||||||
|  | </x-container> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import { faPaintBrush, faMagic } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import i18n from '../../../i18n'; | ||||||
|  | import XContainer from '../page-editor.container.vue'; | ||||||
|  | import MkInput from '../../../components/ui/input.vue'; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	i18n, | ||||||
|  |  | ||||||
|  | 	components: { | ||||||
|  | 		XContainer, MkInput | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	props: { | ||||||
|  | 		value: { | ||||||
|  | 			required: true | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			faPaintBrush, faMagic | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	created() { | ||||||
|  | 		if (this.value.name == null) Vue.set(this.value, 'name', ''); | ||||||
|  | 		if (this.value.width == null) Vue.set(this.value, 'width', 300); | ||||||
|  | 		if (this.value.height == null) Vue.set(this.value, 'height', 200); | ||||||
|  | 	}, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
| @@ -2,7 +2,7 @@ | |||||||
| <x-container @remove="() => $emit('remove')" :draggable="true"> | <x-container @remove="() => $emit('remove')" :draggable="true"> | ||||||
| 	<template #header><fa :icon="faQuestion"/> {{ $t('_pages.blocks.if') }}</template> | 	<template #header><fa :icon="faQuestion"/> {{ $t('_pages.blocks.if') }}</template> | ||||||
| 	<template #func> | 	<template #func> | ||||||
| 		<button @click="add()"> | 		<button @click="add()" class="_button"> | ||||||
| 			<fa :icon="faPlus"/> | 			<fa :icon="faPlus"/> | ||||||
| 		</button> | 		</button> | ||||||
| 	</template> | 	</template> | ||||||
| @@ -10,16 +10,16 @@ | |||||||
| 	<section class="romcojzs"> | 	<section class="romcojzs"> | ||||||
| 		<mk-select v-model="value.var"> | 		<mk-select v-model="value.var"> | ||||||
| 			<template #label>{{ $t('_pages.blocks._if.variable') }}</template> | 			<template #label>{{ $t('_pages.blocks._if.variable') }}</template> | ||||||
| 			<option v-for="v in aiScript.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option> | 			<option v-for="v in aoiScript.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option> | ||||||
| 			<optgroup :label="$t('_pages.script.pageVariables')"> | 			<optgroup :label="$t('_pages.script.pageVariables')"> | ||||||
| 				<option v-for="v in aiScript.getPageVarsByType('boolean')" :value="v">{{ v }}</option> | 				<option v-for="v in aoiScript.getPageVarsByType('boolean')" :value="v">{{ v }}</option> | ||||||
| 			</optgroup> | 			</optgroup> | ||||||
| 			<optgroup :label="$t('_pages.script.enviromentVariables')"> | 			<optgroup :label="$t('_pages.script.enviromentVariables')"> | ||||||
| 				<option v-for="v in aiScript.getEnvVarsByType('boolean')" :value="v">{{ v }}</option> | 				<option v-for="v in aoiScript.getEnvVarsByType('boolean')" :value="v">{{ v }}</option> | ||||||
| 			</optgroup> | 			</optgroup> | ||||||
| 		</mk-select> | 		</mk-select> | ||||||
|  |  | ||||||
| 		<x-blocks class="children" v-model="value.children" :ai-script="aiScript"/> | 		<x-blocks class="children" v-model="value.children" :aoi-script="aoiScript"/> | ||||||
| 	</section> | 	</section> | ||||||
| </x-container> | </x-container> | ||||||
| </template> | </template> | ||||||
| @@ -45,7 +45,7 @@ export default Vue.extend({ | |||||||
| 		value: { | 		value: { | ||||||
| 			required: true | 			required: true | ||||||
| 		}, | 		}, | ||||||
| 		aiScript: { | 		aoiScript: { | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
| @@ -2,16 +2,16 @@ | |||||||
| <x-container @remove="() => $emit('remove')" :draggable="true"> | <x-container @remove="() => $emit('remove')" :draggable="true"> | ||||||
| 	<template #header><fa :icon="faStickyNote"/> {{ value.title }}</template> | 	<template #header><fa :icon="faStickyNote"/> {{ value.title }}</template> | ||||||
| 	<template #func> | 	<template #func> | ||||||
| 		<button @click="rename()"> | 		<button @click="rename()" class="_button"> | ||||||
| 			<fa :icon="faPencilAlt"/> | 			<fa :icon="faPencilAlt"/> | ||||||
| 		</button> | 		</button> | ||||||
| 		<button @click="add()"> | 		<button @click="add()" class="_button"> | ||||||
| 			<fa :icon="faPlus"/> | 			<fa :icon="faPlus"/> | ||||||
| 		</button> | 		</button> | ||||||
| 	</template> | 	</template> | ||||||
|  |  | ||||||
| 	<section class="ilrvjyvi"> | 	<section class="ilrvjyvi"> | ||||||
| 		<x-blocks class="children" v-model="value.children" :ai-script="aiScript"/> | 		<x-blocks class="children" v-model="value.children" :aoi-script="aoiScript"/> | ||||||
| 	</section> | 	</section> | ||||||
| </x-container> | </x-container> | ||||||
| </template> | </template> | ||||||
| @@ -37,7 +37,7 @@ export default Vue.extend({ | |||||||
| 		value: { | 		value: { | ||||||
| 			required: true | 			required: true | ||||||
| 		}, | 		}, | ||||||
| 		aiScript: { | 		aoiScript: { | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| <x-container @remove="() => $emit('remove')" :draggable="true"> | <x-container @remove="() => $emit('remove')" :draggable="true"> | ||||||
| 	<template #header><fa :icon="faAlignLeft"/> {{ $t('_pages.blocks.text') }}</template> | 	<template #header><fa :icon="faAlignLeft"/> {{ $t('_pages.blocks.text') }}</template> | ||||||
|  |  | ||||||
| 	<section class="ihymsbbe"> | 	<section class="vckmsadr"> | ||||||
| 		<textarea v-model="value.text"></textarea> | 		<textarea v-model="value.text"></textarea> | ||||||
| 	</section> | 	</section> | ||||||
| </x-container> | </x-container> | ||||||
| @@ -40,7 +40,7 @@ export default Vue.extend({ | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .ihymsbbe { | .vckmsadr { | ||||||
| 	> textarea { | 	> textarea { | ||||||
| 		display: block; | 		display: block; | ||||||
| 		-webkit-appearance: none; | 		-webkit-appearance: none; | ||||||
| @@ -55,6 +55,7 @@ export default Vue.extend({ | |||||||
| 		background: transparent; | 		background: transparent; | ||||||
| 		color: var(--fg); | 		color: var(--fg); | ||||||
| 		font-size: 14px; | 		font-size: 14px; | ||||||
|  | 		box-sizing: border-box; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -55,6 +55,7 @@ export default Vue.extend({ | |||||||
| 		background: transparent; | 		background: transparent; | ||||||
| 		color: var(--fg); | 		color: var(--fg); | ||||||
| 		font-size: 14px; | 		font-size: 14px; | ||||||
|  | 		box-sizing: border-box; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <x-draggable tag="div" :list="blocks" handle=".drag-handle" :group="{ name: 'blocks' }" animation="150" swap-threshold="0.5"> | <x-draggable tag="div" :list="blocks" handle=".drag-handle" :group="{ name: 'blocks' }" animation="150" swap-threshold="0.5"> | ||||||
| 	<component v-for="block in blocks" :is="'x-' + block.type" :value="block" @input="updateItem" @remove="() => removeItem(block)" :key="block.id" :ai-script="aiScript"/> | 	<component v-for="block in blocks" :is="'x-' + block.type" :value="block" @input="updateItem" @remove="() => removeItem(block)" :key="block.id" :aoi-script="aoiScript"/> | ||||||
| </x-draggable> | </x-draggable> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -20,10 +20,11 @@ import XIf from './els/page-editor.el.if.vue'; | |||||||
| import XPost from './els/page-editor.el.post.vue'; | import XPost from './els/page-editor.el.post.vue'; | ||||||
| import XCounter from './els/page-editor.el.counter.vue'; | import XCounter from './els/page-editor.el.counter.vue'; | ||||||
| import XRadioButton from './els/page-editor.el.radio-button.vue'; | import XRadioButton from './els/page-editor.el.radio-button.vue'; | ||||||
|  | import XCanvas from './els/page-editor.el.canvas.vue'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XDraggable, XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton | 		XDraggable, XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton, XCanvas | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	props: { | 	props: { | ||||||
| @@ -31,7 +32,7 @@ export default Vue.extend({ | |||||||
| 			type: Array, | 			type: Array, | ||||||
| 			required: true | 			required: true | ||||||
| 		}, | 		}, | ||||||
| 		aiScript: { | 		aoiScript: { | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ | |||||||
| 	</header> | 	</header> | ||||||
| 	<p v-show="showBody" class="error" v-if="error != null">{{ $t('_pages.script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p> | 	<p v-show="showBody" class="error" v-if="error != null">{{ $t('_pages.script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p> | ||||||
| 	<p v-show="showBody" class="warn" v-if="warn != null">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p> | 	<p v-show="showBody" class="warn" v-if="warn != null">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p> | ||||||
| 	<div v-show="showBody"> | 	<div v-show="showBody" class="body"> | ||||||
| 		<slot></slot> | 		<slot></slot> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| @@ -148,5 +148,17 @@ export default Vue.extend({ | |||||||
| 		padding: 16px 16px 0 16px; | 		padding: 16px 16px 0 16px; | ||||||
| 		font-size: 14px; | 		font-size: 14px; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	> .body { | ||||||
|  | 		::v-deep .juejbjww, ::v-deep .eiipwacr { | ||||||
|  | 			&:not(.inline):first-child { | ||||||
|  | 				margin-top: 28px; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			&:not(.inline):last-child { | ||||||
|  | 				margin-bottom: 20px; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| <x-container :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn" :draggable="draggable"> | <x-container :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn" :draggable="draggable"> | ||||||
| 	<template #header><fa v-if="icon" :icon="icon"/> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template> | 	<template #header><fa v-if="icon" :icon="icon"/> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template> | ||||||
| 	<template #func> | 	<template #func> | ||||||
| 		<button @click="changeType()"> | 		<button @click="changeType()" class="_button"> | ||||||
| 			<fa :icon="faPencilAlt"/> | 			<fa :icon="faPencilAlt"/> | ||||||
| 		</button> | 		</button> | ||||||
| 	</template> | 	</template> | ||||||
| @@ -24,30 +24,33 @@ | |||||||
| 	</section> | 	</section> | ||||||
| 	<section v-else-if="value.type === 'ref'" class="hpdwcrvs"> | 	<section v-else-if="value.type === 'ref'" class="hpdwcrvs"> | ||||||
| 		<select v-model="value.value"> | 		<select v-model="value.value"> | ||||||
| 			<option v-for="v in aiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option> | 			<option v-for="v in aoiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option> | ||||||
| 			<optgroup :label="$t('_pages.script.argVariables')"> | 			<optgroup :label="$t('_pages.script.argVariables')"> | ||||||
| 				<option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option> | 				<option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option> | ||||||
| 			</optgroup> | 			</optgroup> | ||||||
| 			<optgroup :label="$t('_pages.script.pageVariables')"> | 			<optgroup :label="$t('_pages.script.pageVariables')"> | ||||||
| 				<option v-for="v in aiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> | 				<option v-for="v in aoiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> | ||||||
| 			</optgroup> | 			</optgroup> | ||||||
| 			<optgroup :label="$t('_pages.script.enviromentVariables')"> | 			<optgroup :label="$t('_pages.script.enviromentVariables')"> | ||||||
| 				<option v-for="v in aiScript.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> | 				<option v-for="v in aoiScript.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> | ||||||
| 			</optgroup> | 			</optgroup> | ||||||
| 		</select> | 		</select> | ||||||
| 	</section> | 	</section> | ||||||
|  | 	<section v-else-if="value.type === 'aiScriptVar'" class="tbwccoaw"> | ||||||
|  | 		<input v-model="value.value"/> | ||||||
|  | 	</section> | ||||||
| 	<section v-else-if="value.type === 'fn'" class="" style="padding:0 16px 16px 16px;"> | 	<section v-else-if="value.type === 'fn'" class="" style="padding:0 16px 16px 16px;"> | ||||||
| 		<mk-textarea v-model="slots"> | 		<mk-textarea v-model="slots"> | ||||||
| 			<span>{{ $t('_pages.script.blocks._fn.slots') }}</span> | 			<span>{{ $t('_pages.script.blocks._fn.slots') }}</span> | ||||||
| 			<template #desc>{{ $t('_pages.script.blocks._fn.slots-info') }}</template> | 			<template #desc>{{ $t('_pages.script.blocks._fn.slots-info') }}</template> | ||||||
| 		</mk-textarea> | 		</mk-textarea> | ||||||
| 		<x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/> | 		<x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :aoi-script="aoiScript" :fn-slots="value.value.slots" :name="name"/> | ||||||
| 	</section> | 	</section> | ||||||
| 	<section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;"> | 	<section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;"> | ||||||
| 		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aiScript.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :ai-script="aiScript" :name="name" :key="i"/> | 		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aoiScript.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :aoi-script="aoiScript" :name="name" :key="i"/> | ||||||
| 	</section> | 	</section> | ||||||
| 	<section v-else class="" style="padding:16px;"> | 	<section v-else class="" style="padding:16px;"> | ||||||
| 		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`_pages.script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/> | 		<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`_pages.script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :aoi-script="aoiScript" :name="name" :fn-slots="fnSlots" :key="i"/> | ||||||
| 	</section> | 	</section> | ||||||
| </x-container> | </x-container> | ||||||
| </template> | </template> | ||||||
| @@ -59,7 +62,7 @@ import { v4 as uuid } from 'uuid'; | |||||||
| import i18n from '../../i18n'; | import i18n from '../../i18n'; | ||||||
| import XContainer from './page-editor.container.vue'; | import XContainer from './page-editor.container.vue'; | ||||||
| import MkTextarea from '../../components/ui/textarea.vue'; | import MkTextarea from '../../components/ui/textarea.vue'; | ||||||
| import { isLiteralBlock, funcDefs, blockDefs } from '../../scripts/aiscript/index'; | import { isLiteralBlock, funcDefs, blockDefs } from '../../scripts/aoiscript/index'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	i18n, | 	i18n, | ||||||
| @@ -85,7 +88,7 @@ export default Vue.extend({ | |||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
| 		}, | 		}, | ||||||
| 		aiScript: { | 		aoiScript: { | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
| 		name: { | 		name: { | ||||||
| @@ -153,7 +156,7 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 			if (this.value.type && this.value.type.startsWith('fn:')) { | 			if (this.value.type && this.value.type.startsWith('fn:')) { | ||||||
| 				const fnName = this.value.type.split(':')[1]; | 				const fnName = this.value.type.split(':')[1]; | ||||||
| 				const fn = this.aiScript.getVarByName(fnName); | 				const fn = this.aoiScript.getVarByName(fnName); | ||||||
|  |  | ||||||
| 				const empties = []; | 				const empties = []; | ||||||
| 				for (let i = 0; i < fn.value.slots.length; i++) { | 				for (let i = 0; i < fn.value.slots.length; i++) { | ||||||
| @@ -199,9 +202,9 @@ export default Vue.extend({ | |||||||
| 			deep: true | 			deep: true | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		this.$watch('aiScript.variables', () => { | 		this.$watch('aoiScript.variables', () => { | ||||||
| 			if (this.type != null && this.value) { | 			if (this.type != null && this.value) { | ||||||
| 				this.error = this.aiScript.typeCheck(this.value); | 				this.error = this.aoiScript.typeCheck(this.value); | ||||||
| 			} | 			} | ||||||
| 		}, { | 		}, { | ||||||
| 			deep: true | 			deep: true | ||||||
| @@ -223,7 +226,7 @@ export default Vue.extend({ | |||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		_getExpectedType(slot: number) { | 		_getExpectedType(slot: number) { | ||||||
| 			return this.aiScript.getExpectedType(this.value, slot); | 			return this.aoiScript.getExpectedType(this.value, slot); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| @@ -258,6 +261,7 @@ export default Vue.extend({ | |||||||
| 		font-size: 16px; | 		font-size: 16px; | ||||||
| 		background: transparent; | 		background: transparent; | ||||||
| 		color: var(--fg); | 		color: var(--fg); | ||||||
|  | 		box-sizing: border-box; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	> textarea { | 	> textarea { | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ | |||||||
| 				</div> | 				</div> | ||||||
| 			</template> | 			</template> | ||||||
|  |  | ||||||
| 			<x-blocks class="content" v-model="content" :ai-script="aiScript"/> | 			<x-blocks class="content" v-model="content" :aoi-script="aoiScript"/> | ||||||
|  |  | ||||||
| 			<mk-button @click="add()" v-if="!readonly"><fa :icon="faPlus"/></mk-button> | 			<mk-button @click="add()" v-if="!readonly"><fa :icon="faPlus"/></mk-button> | ||||||
| 		</section> | 		</section> | ||||||
| @@ -62,7 +62,7 @@ | |||||||
| 					@input="v => updateVariable(v)" | 					@input="v => updateVariable(v)" | ||||||
| 					@remove="() => removeVariable(variable)" | 					@remove="() => removeVariable(variable)" | ||||||
| 					:key="variable.name" | 					:key="variable.name" | ||||||
| 					:ai-script="aiScript" | 					:aoi-script="aoiScript" | ||||||
| 					:name="variable.name" | 					:name="variable.name" | ||||||
| 					:title="variable.name" | 					:title="variable.name" | ||||||
| 					:draggable="true" | 					:draggable="true" | ||||||
| @@ -73,11 +73,10 @@ | |||||||
| 		</div> | 		</div> | ||||||
| 	</mk-container> | 	</mk-container> | ||||||
|  |  | ||||||
| 	<mk-container :body-togglable="true" :expanded="false"> | 	<mk-container :body-togglable="true" :expanded="true"> | ||||||
| 		<template #header><fa :icon="faCode"/> {{ $t('_pages.inspector') }}</template> | 		<template #header><fa :icon="faCode"/> {{ $t('script') }}</template> | ||||||
| 		<div style="padding:0 32px 32px 32px;"> | 		<div> | ||||||
| 			<mk-textarea :value="JSON.stringify(content, null, 2)" readonly tall>{{ $t('_pages.content') }}</mk-textarea> | 			<prism-editor v-model="script" :line-numbers="false" language="js"/> | ||||||
| 			<mk-textarea :value="JSON.stringify(variables, null, 2)" readonly tall>{{ $t('_pages.variables') }}</mk-textarea> |  | ||||||
| 		</div> | 		</div> | ||||||
| 	</mk-container> | 	</mk-container> | ||||||
| </div> | </div> | ||||||
| @@ -86,6 +85,9 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import * as XDraggable from 'vuedraggable'; | import * as XDraggable from 'vuedraggable'; | ||||||
|  | import "prismjs"; | ||||||
|  | import "prismjs/themes/prism.css"; | ||||||
|  | import PrismEditor from 'vue-prism-editor'; | ||||||
| import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; | import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; | import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| @@ -98,16 +100,17 @@ import MkButton from '../../components/ui/button.vue'; | |||||||
| import MkSelect from '../../components/ui/select.vue'; | import MkSelect from '../../components/ui/select.vue'; | ||||||
| import MkSwitch from '../../components/ui/switch.vue'; | import MkSwitch from '../../components/ui/switch.vue'; | ||||||
| import MkInput from '../../components/ui/input.vue'; | import MkInput from '../../components/ui/input.vue'; | ||||||
| import { blockDefs } from '../../scripts/aiscript/index'; | import { blockDefs } from '../../scripts/aoiscript/index'; | ||||||
| import { ASTypeChecker } from '../../scripts/aiscript/type-checker'; | import { ASTypeChecker } from '../../scripts/aoiscript/type-checker'; | ||||||
| import { url } from '../../config'; | import { url } from '../../config'; | ||||||
| import { collectPageVars } from '../../scripts/collect-page-vars'; | import { collectPageVars } from '../../scripts/collect-page-vars'; | ||||||
|  | import { selectDriveFile } from '../../scripts/select-drive-file'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	i18n, | 	i18n, | ||||||
|  |  | ||||||
| 	components: { | 	components: { | ||||||
| 		XDraggable, XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput | 		XDraggable, XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput, PrismEditor | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	props: { | 	props: { | ||||||
| @@ -142,7 +145,8 @@ export default Vue.extend({ | |||||||
| 			alignCenter: false, | 			alignCenter: false, | ||||||
| 			hideTitleWhenPinned: false, | 			hideTitleWhenPinned: false, | ||||||
| 			variables: [], | 			variables: [], | ||||||
| 			aiScript: null, | 			aoiScript: null, | ||||||
|  | 			script: '', | ||||||
| 			showOptions: false, | 			showOptions: false, | ||||||
| 			url, | 			url, | ||||||
| 			faPlus, faICursor, faSave, faStickyNote, faMagic, faCog, faTrashAlt, faExternalLinkSquareAlt, faCode | 			faPlus, faICursor, faSave, faStickyNote, faMagic, faCog, faTrashAlt, faExternalLinkSquareAlt, faCode | ||||||
| @@ -162,14 +166,14 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	async created() { | 	async created() { | ||||||
| 		this.aiScript = new ASTypeChecker(); | 		this.aoiScript = new ASTypeChecker(); | ||||||
|  |  | ||||||
| 		this.$watch('variables', () => { | 		this.$watch('variables', () => { | ||||||
| 			this.aiScript.variables = this.variables; | 			this.aoiScript.variables = this.variables; | ||||||
| 		}, { deep: true }); | 		}, { deep: true }); | ||||||
|  |  | ||||||
| 		this.$watch('content', () => { | 		this.$watch('content', () => { | ||||||
| 			this.aiScript.pageVars = collectPageVars(this.content); | 			this.aoiScript.pageVars = collectPageVars(this.content); | ||||||
| 		}, { deep: true }); | 		}, { deep: true }); | ||||||
|  |  | ||||||
| 		if (this.initPageId) { | 		if (this.initPageId) { | ||||||
| @@ -192,6 +196,7 @@ export default Vue.extend({ | |||||||
| 			this.currentName = this.page.name; | 			this.currentName = this.page.name; | ||||||
| 			this.summary = this.page.summary; | 			this.summary = this.page.summary; | ||||||
| 			this.font = this.page.font; | 			this.font = this.page.font; | ||||||
|  | 			this.script = this.page.script; | ||||||
| 			this.hideTitleWhenPinned = this.page.hideTitleWhenPinned; | 			this.hideTitleWhenPinned = this.page.hideTitleWhenPinned; | ||||||
| 			this.alignCenter = this.page.alignCenter; | 			this.alignCenter = this.page.alignCenter; | ||||||
| 			this.content = this.page.content; | 			this.content = this.page.content; | ||||||
| @@ -222,6 +227,7 @@ export default Vue.extend({ | |||||||
| 				name: this.name.trim(), | 				name: this.name.trim(), | ||||||
| 				summary: this.summary, | 				summary: this.summary, | ||||||
| 				font: this.font, | 				font: this.font, | ||||||
|  | 				script: this.script, | ||||||
| 				hideTitleWhenPinned: this.hideTitleWhenPinned, | 				hideTitleWhenPinned: this.hideTitleWhenPinned, | ||||||
| 				alignCenter: this.alignCenter, | 				alignCenter: this.alignCenter, | ||||||
| 				content: this.content, | 				content: this.content, | ||||||
| @@ -316,7 +322,7 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 			name = name.trim(); | 			name = name.trim(); | ||||||
|  |  | ||||||
| 			if (this.aiScript.isUsedName(name)) { | 			if (this.aoiScript.isUsedName(name)) { | ||||||
| 				this.$root.dialog({ | 				this.$root.dialog({ | ||||||
| 					type: 'error', | 					type: 'error', | ||||||
| 					text: this.$t('_pages.variableNameIsAlreadyUsed') | 					text: this.$t('_pages.variableNameIsAlreadyUsed') | ||||||
| @@ -345,6 +351,7 @@ export default Vue.extend({ | |||||||
| 					{ value: 'text', text: this.$t('_pages.blocks.text') }, | 					{ value: 'text', text: this.$t('_pages.blocks.text') }, | ||||||
| 					{ value: 'image', text: this.$t('_pages.blocks.image') }, | 					{ value: 'image', text: this.$t('_pages.blocks.image') }, | ||||||
| 					{ value: 'textarea', text: this.$t('_pages.blocks.textarea') }, | 					{ value: 'textarea', text: this.$t('_pages.blocks.textarea') }, | ||||||
|  | 					{ value: 'canvas', text: this.$t('_pages.blocks.canvas') }, | ||||||
| 				] | 				] | ||||||
| 			}, { | 			}, { | ||||||
| 				label: this.$t('_pages.inputBlocks'), | 				label: this.$t('_pages.inputBlocks'), | ||||||
| @@ -381,7 +388,7 @@ export default Vue.extend({ | |||||||
| 				} else { | 				} else { | ||||||
| 					list.push({ | 					list.push({ | ||||||
| 						category: block.category, | 						category: block.category, | ||||||
| 						label: this.$t(`script.categories.${block.category}`), | 						label: this.$t(`_pages.script.categories.${block.category}`), | ||||||
| 						items: [{ | 						items: [{ | ||||||
| 							value: block.type, | 							value: block.type, | ||||||
| 							text: this.$t(`_pages.script.blocks.${block.type}`) | 							text: this.$t(`_pages.script.blocks.${block.type}`) | ||||||
| @@ -393,7 +400,7 @@ export default Vue.extend({ | |||||||
| 			const userFns = this.variables.filter(x => x.type === 'fn'); | 			const userFns = this.variables.filter(x => x.type === 'fn'); | ||||||
| 			if (userFns.length > 0) { | 			if (userFns.length > 0) { | ||||||
| 				list.unshift({ | 				list.unshift({ | ||||||
| 					label: this.$t(`script.categories.fn`), | 					label: this.$t(`_pages.script.categories.fn`), | ||||||
| 					items: userFns.map(v => ({ | 					items: userFns.map(v => ({ | ||||||
| 						value: 'fn:' + v.name, | 						value: 'fn:' + v.name, | ||||||
| 						text: v.name | 						text: v.name | ||||||
| @@ -405,9 +412,7 @@ export default Vue.extend({ | |||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		setEyeCatchingImage() { | 		setEyeCatchingImage() { | ||||||
| 			this.$chooseDriveFile({ | 			selectDriveFile(this.$root, false).then(file => { | ||||||
| 				multiple: false |  | ||||||
| 			}).then(file => { |  | ||||||
| 				this.eyeCatchingImageId = file.id; | 				this.eyeCatchingImageId = file.id; | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| @@ -424,8 +429,6 @@ export default Vue.extend({ | |||||||
| 	margin-bottom: var(--margin); | 	margin-bottom: var(--margin); | ||||||
|  |  | ||||||
| 	> header { | 	> header { | ||||||
| 		background: var(--faceHeader); |  | ||||||
|  |  | ||||||
| 		> .title { | 		> .title { | ||||||
| 			z-index: 1; | 			z-index: 1; | ||||||
| 			margin: 0; | 			margin: 0; | ||||||
| @@ -433,8 +436,7 @@ export default Vue.extend({ | |||||||
| 			line-height: 42px; | 			line-height: 42px; | ||||||
| 			font-size: 0.9em; | 			font-size: 0.9em; | ||||||
| 			font-weight: bold; | 			font-weight: bold; | ||||||
| 			color: var(--faceHeaderText); | 			box-shadow: 0 1px rgba(#000, 0.07); | ||||||
| 			box-shadow: 0 var(--lineWidth) rgba(#000, 0.07); |  | ||||||
|  |  | ||||||
| 			> [data-icon] { | 			> [data-icon] { | ||||||
| 				margin-right: 6px; | 				margin-right: 6px; | ||||||
|   | |||||||
| @@ -5,6 +5,9 @@ | |||||||
|  |  | ||||||
| 	<div class="_card" v-if="page" :key="page.id"> | 	<div class="_card" v-if="page" :key="page.id"> | ||||||
| 		<div class="_title">{{ page.title }}</div> | 		<div class="_title">{{ page.title }}</div> | ||||||
|  | 		<div class="banner"> | ||||||
|  | 			<img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/> | ||||||
|  | 		</div> | ||||||
| 		<div class="_content"> | 		<div class="_content"> | ||||||
| 			<x-page :page="page"/> | 			<x-page :page="page"/> | ||||||
| 		</div> | 		</div> | ||||||
| @@ -115,6 +118,13 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .xcukqgmh { | .xcukqgmh { | ||||||
|  | 	> ._card > .banner { | ||||||
|  | 		> img { | ||||||
|  | 			display: block; | ||||||
|  | 			width: 100%; | ||||||
|  | 			height: 120px; | ||||||
|  | 			object-fit: cover; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -65,6 +65,7 @@ | |||||||
| 				<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template> | 				<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template> | ||||||
| 			</mk-switch> | 			</mk-switch> | ||||||
| 			<mk-switch v-model="showFixedPostForm">{{ $t('showFixedPostForm') }}</mk-switch> | 			<mk-switch v-model="showFixedPostForm">{{ $t('showFixedPostForm') }}</mk-switch> | ||||||
|  | 			<mk-switch v-model="disablePagesScript">{{ $t('disablePagesScript') }}</mk-switch> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="_content"> | 		<div class="_content"> | ||||||
| 			<mk-select v-model="lang"> | 			<mk-select v-model="lang"> | ||||||
| @@ -171,6 +172,11 @@ export default Vue.extend({ | |||||||
| 			set(value) { this.$store.commit('device/set', { key: 'imageNewTab', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'imageNewTab', value }); } | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | 		disablePagesScript: { | ||||||
|  | 			get() { return this.$store.state.device.disablePagesScript; }, | ||||||
|  | 			set(value) { this.$store.commit('device/set', { key: 'disablePagesScript', value }); } | ||||||
|  | 		}, | ||||||
|  |  | ||||||
| 		showFixedPostForm: { | 		showFixedPostForm: { | ||||||
| 			get() { return this.$store.state.device.showFixedPostForm; }, | 			get() { return this.$store.state.device.showFixedPostForm; }, | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'showFixedPostForm', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'showFixedPostForm', value }); } | ||||||
|   | |||||||
							
								
								
									
										140
									
								
								src/client/pages/scratchpad.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/client/pages/scratchpad.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | <template> | ||||||
|  | <div class=""> | ||||||
|  | 	<portal to="icon"><fa :icon="faTerminal"/></portal> | ||||||
|  | 	<portal to="title">{{ $t('scratchpad') }}</portal> | ||||||
|  |  | ||||||
|  | 	<div class="_panel"> | ||||||
|  | 		<prism-editor v-model="code" :line-numbers="false" language="js"/> | ||||||
|  | 		<mk-button style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><fa :icon="faPlay"/></mk-button> | ||||||
|  | 	</div> | ||||||
|  |  | ||||||
|  | 	<mk-container :body-togglable="true"> | ||||||
|  | 		<template #header><fa fixed-width/>{{ $t('output') }}</template> | ||||||
|  | 		<div class="bepmlvbi"> | ||||||
|  | 			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div> | ||||||
|  | 		</div> | ||||||
|  | 	</mk-container> | ||||||
|  |  | ||||||
|  | 	<section class="_card" style="margin-top: var(--margin);"> | ||||||
|  | 		<div class="_content">{{ $t('scratchpadDescription') }}</div> | ||||||
|  | 	</section> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import { faTerminal, faPlay } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import "prismjs"; | ||||||
|  | import "prismjs/themes/prism.css"; | ||||||
|  | import PrismEditor from 'vue-prism-editor'; | ||||||
|  | import { AiScript, parse, utils, values } from '@syuilo/aiscript'; | ||||||
|  | import i18n from '../i18n'; | ||||||
|  | import MkContainer from '../components/ui/container.vue'; | ||||||
|  | import MkButton from '../components/ui/button.vue'; | ||||||
|  | import { createAiScriptEnv } from '../scripts/create-aiscript-env'; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	i18n, | ||||||
|  |  | ||||||
|  | 	metaInfo() { | ||||||
|  | 		return { | ||||||
|  | 			title: this.$t('scratchpad') as string | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	components: { | ||||||
|  | 		MkContainer, | ||||||
|  | 		MkButton, | ||||||
|  | 		PrismEditor, | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			code: '', | ||||||
|  | 			logs: [], | ||||||
|  | 			faTerminal, faPlay | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	watch: { | ||||||
|  | 		code() { | ||||||
|  | 			localStorage.setItem('scratchpad', this.code); | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	created() { | ||||||
|  | 		const saved = localStorage.getItem('scratchpad'); | ||||||
|  | 		if (saved) { | ||||||
|  | 			this.code = saved; | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	methods: { | ||||||
|  | 		async run() { | ||||||
|  | 			this.logs = []; | ||||||
|  | 			const aiscript = new AiScript(createAiScriptEnv(this, { | ||||||
|  | 				storageKey: 'scratchpad' | ||||||
|  | 			}), { | ||||||
|  | 				in: (q) => { | ||||||
|  | 					return new Promise(ok => { | ||||||
|  | 						this.$root.dialog({ | ||||||
|  | 							title: q, | ||||||
|  | 							input: {} | ||||||
|  | 						}).then(({ canceled, result: a }) => { | ||||||
|  | 							ok(a); | ||||||
|  | 						}); | ||||||
|  | 					}); | ||||||
|  | 				}, | ||||||
|  | 				out: (value) => { | ||||||
|  | 					this.logs.push({ | ||||||
|  | 						id: Math.random(), | ||||||
|  | 						text: value.type === 'str' ? value.value : utils.valToString(value), | ||||||
|  | 						print: true | ||||||
|  | 					}); | ||||||
|  | 				}, | ||||||
|  | 				log: (type, params) => { | ||||||
|  | 					switch (type) { | ||||||
|  | 						case 'end': this.logs.push({ | ||||||
|  | 							id: Math.random(), | ||||||
|  | 							text: utils.valToString(params.val, true), | ||||||
|  | 							print: false | ||||||
|  | 						}); break; | ||||||
|  | 						default: break; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			let ast; | ||||||
|  | 			try { | ||||||
|  | 				ast = parse(this.code); | ||||||
|  | 			} catch (e) { | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'error', | ||||||
|  | 					text: 'Syntax error :(' | ||||||
|  | 				}); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			try { | ||||||
|  | 				await aiscript.exec(ast); | ||||||
|  | 			} catch (e) { | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'error', | ||||||
|  | 					text: e | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .bepmlvbi { | ||||||
|  | 	padding: 16px; | ||||||
|  |  | ||||||
|  | 	> .log { | ||||||
|  | 		&:not(.print) { | ||||||
|  | 			opacity: 0.7; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -8,7 +8,7 @@ | |||||||
| 			:href="image.note | notePage" | 			:href="image.note | notePage" | ||||||
| 		></a> | 		></a> | ||||||
| 	</div> | 	</div> | ||||||
| 	<p class="empty" v-if="!fetching && images.length == 0">{{ $t('no-photos') }}</p> | 	<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ | |||||||
| 	<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal> | 	<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal> | ||||||
|  |  | ||||||
| 	<mk-remote-caution v-if="user.host != null" :href="user.url" style="margin-bottom: var(--margin)"/> | 	<mk-remote-caution v-if="user.host != null" :href="user.url" style="margin-bottom: var(--margin)"/> | ||||||
|  | 	<div class="punished _panel" v-if="user.isSuspended"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</div> | ||||||
|  | 	<div class="punished _panel" v-if="user.isSilenced"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div> | ||||||
| 	<div class="profile _panel" :key="user.id"> | 	<div class="profile _panel" :key="user.id"> | ||||||
| 		<div class="banner-container" :style="style"> | 		<div class="banner-container" :style="style"> | ||||||
| 			<div class="banner" ref="banner" :style="style"></div> | 			<div class="banner" ref="banner" :style="style"></div> | ||||||
| @@ -105,7 +107,7 @@ | |||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import { faEllipsisH, faRobot, faLock, faBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker } from '@fortawesome/free-solid-svg-icons'; | import { faExclamationTriangle, faEllipsisH, faRobot, faLock, faBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faCalendarAlt, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons'; | import { faCalendarAlt, faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import * as age from 's-age'; | import * as age from 's-age'; | ||||||
| import XUserTimeline from './index.timeline.vue'; | import XUserTimeline from './index.timeline.vue'; | ||||||
| @@ -139,7 +141,7 @@ export default Vue.extend({ | |||||||
| 			user: null, | 			user: null, | ||||||
| 			error: null, | 			error: null, | ||||||
| 			parallaxAnimationId: null, | 			parallaxAnimationId: null, | ||||||
| 			faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt | 			faExclamationTriangle, faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| @@ -217,6 +219,12 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .mk-user-page { | .mk-user-page { | ||||||
|  |  | ||||||
|  | 	> .punished { | ||||||
|  | 		font-size: 0.8em; | ||||||
|  | 		padding: 16px; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	> .profile { | 	> .profile { | ||||||
| 		position: relative; | 		position: relative; | ||||||
| 		margin-bottom: var(--margin); | 		margin-bottom: var(--margin); | ||||||
|   | |||||||
| @@ -48,9 +48,11 @@ export const router = new VueRouter({ | |||||||
| 		{ path: '/my/antennas', component: page('my-antennas/index') }, | 		{ path: '/my/antennas', component: page('my-antennas/index') }, | ||||||
| 		{ path: '/my/apps', component: page('apps') }, | 		{ path: '/my/apps', component: page('apps') }, | ||||||
| 		{ path: '/preferences', component: page('preferences/index') }, | 		{ path: '/preferences', component: page('preferences/index') }, | ||||||
|  | 		{ path: '/scratchpad', component: page('scratchpad') }, | ||||||
| 		{ path: '/instance', component: page('instance/index') }, | 		{ path: '/instance', component: page('instance/index') }, | ||||||
| 		{ path: '/instance/emojis', component: page('instance/emojis') }, | 		{ path: '/instance/emojis', component: page('instance/emojis') }, | ||||||
| 		{ path: '/instance/users', component: page('instance/users') }, | 		{ path: '/instance/users', component: page('instance/users') }, | ||||||
|  | 		{ path: '/instance/users/:user', component: page('instance/users.user') }, | ||||||
| 		{ path: '/instance/files', component: page('instance/files') }, | 		{ path: '/instance/files', component: page('instance/files') }, | ||||||
| 		{ path: '/instance/queue', component: page('instance/queue') }, | 		{ path: '/instance/queue', component: page('instance/queue') }, | ||||||
| 		{ path: '/instance/settings', component: page('instance/settings') }, | 		{ path: '/instance/settings', component: page('instance/settings') }, | ||||||
|   | |||||||
| @@ -2,6 +2,8 @@ import autobind from 'autobind-decorator'; | |||||||
| import * as seedrandom from 'seedrandom'; | import * as seedrandom from 'seedrandom'; | ||||||
| import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.'; | import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.'; | ||||||
| import { version } from '../../config'; | import { version } from '../../config'; | ||||||
|  | import { AiScript, utils, parse, values } from '@syuilo/aiscript'; | ||||||
|  | import { createAiScriptEnv } from '../create-aiscript-env'; | ||||||
| 
 | 
 | ||||||
| type Fn = { | type Fn = { | ||||||
| 	slots: string[]; | 	slots: string[]; | ||||||
| @@ -9,22 +11,76 @@ type Fn = { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * AiScript evaluator |  * AoiScript evaluator | ||||||
|  */ |  */ | ||||||
| export class ASEvaluator { | export class ASEvaluator { | ||||||
| 	private variables: Variable[]; | 	private variables: Variable[]; | ||||||
| 	private pageVars: PageVar[]; | 	private pageVars: PageVar[]; | ||||||
| 	private envVars: Record<keyof typeof envVarsDef, any>; | 	private envVars: Record<keyof typeof envVarsDef, any>; | ||||||
|  | 	public aiscript?: AiScript; | ||||||
|  | 	private pageVarUpdatedCallback; | ||||||
|  | 	private canvases: Record<string, HTMLCanvasElement> = {}; | ||||||
| 
 | 
 | ||||||
| 	private opts: { | 	private opts: { | ||||||
| 		randomSeed: string; visitor?: any; page?: any; url?: string; | 		randomSeed: string; visitor?: any; page?: any; url?: string; | ||||||
|  | 		enableAiScript: boolean; | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	constructor(variables: Variable[], pageVars: PageVar[], opts: ASEvaluator['opts']) { | 	constructor(vm: any, variables: Variable[], pageVars: PageVar[], opts: ASEvaluator['opts']) { | ||||||
| 		this.variables = variables; | 		this.variables = variables; | ||||||
| 		this.pageVars = pageVars; | 		this.pageVars = pageVars; | ||||||
| 		this.opts = opts; | 		this.opts = opts; | ||||||
| 
 | 
 | ||||||
|  | 		if (this.opts.enableAiScript) { | ||||||
|  | 			this.aiscript = new AiScript({ ...createAiScriptEnv(vm, { | ||||||
|  | 				storageKey: 'pages:' + opts.page.id | ||||||
|  | 			}), ...{ | ||||||
|  | 				'MkPages:updated': values.FN_NATIVE(([callback]) => { | ||||||
|  | 					this.pageVarUpdatedCallback = callback; | ||||||
|  | 				}), | ||||||
|  | 				'MkPages:get_canvas': values.FN_NATIVE(([id]) => { | ||||||
|  | 					utils.assertString(id); | ||||||
|  | 					const canvas = this.canvases[id.value]; | ||||||
|  | 					const ctx = canvas.getContext('2d'); | ||||||
|  | 					return values.OBJ(new Map([ | ||||||
|  | 						['clear_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.clearRect(x.value, y.value, width.value, height.value) })], | ||||||
|  | 						['fill_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.fillRect(x.value, y.value, width.value, height.value) })], | ||||||
|  | 						['stroke_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.strokeRect(x.value, y.value, width.value, height.value) })], | ||||||
|  | 						['fill_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.fillText(text.value, x.value, y.value, width ? width.value : undefined) })], | ||||||
|  | 						['stroke_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.strokeText(text.value, x.value, y.value, width ? width.value : undefined) })], | ||||||
|  | 						['set_line_width', values.FN_NATIVE(([width]) => { ctx.lineWidth = width.value })], | ||||||
|  | 						['set_font', values.FN_NATIVE(([font]) => { ctx.font = font.value })], | ||||||
|  | 						['set_fill_style', values.FN_NATIVE(([style]) => { ctx.fillStyle = style.value })], | ||||||
|  | 						['set_stroke_style', values.FN_NATIVE(([style]) => { ctx.strokeStyle = style.value })], | ||||||
|  | 						['begin_path', values.FN_NATIVE(() => { ctx.beginPath() })], | ||||||
|  | 						['close_path', values.FN_NATIVE(() => { ctx.closePath() })], | ||||||
|  | 						['move_to', values.FN_NATIVE(([x, y]) => { ctx.moveTo(x.value, y.value) })], | ||||||
|  | 						['line_to', values.FN_NATIVE(([x, y]) => { ctx.lineTo(x.value, y.value) })], | ||||||
|  | 						['arc', values.FN_NATIVE(([x, y, radius, startAngle, endAngle]) => { ctx.arc(x.value, y.value, radius.value, startAngle.value, endAngle.value) })], | ||||||
|  | 						['fill', values.FN_NATIVE(() => { ctx.fill() })], | ||||||
|  | 						['stroke', values.FN_NATIVE(() => { ctx.stroke() })], | ||||||
|  | 					])); | ||||||
|  | 				}) | ||||||
|  | 			}}, { | ||||||
|  | 				in: (q) => { | ||||||
|  | 					return new Promise(ok => { | ||||||
|  | 						vm.$root.dialog({ | ||||||
|  | 							title: q, | ||||||
|  | 							input: {} | ||||||
|  | 						}).then(({ canceled, result: a }) => { | ||||||
|  | 							ok(a); | ||||||
|  | 						}); | ||||||
|  | 					}); | ||||||
|  | 				}, | ||||||
|  | 				out: (value) => { | ||||||
|  | 					console.log(value); | ||||||
|  | 				}, | ||||||
|  | 				log: (type, params) => { | ||||||
|  | 				}, | ||||||
|  | 				maxStep: 16384 | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		const date = new Date(); | 		const date = new Date(); | ||||||
| 
 | 
 | ||||||
| 		this.envVars = { | 		this.envVars = { | ||||||
| @@ -41,17 +97,25 @@ export class ASEvaluator { | |||||||
| 			IS_CAT: opts.visitor ? opts.visitor.isCat : false, | 			IS_CAT: opts.visitor ? opts.visitor.isCat : false, | ||||||
| 			SEED: opts.randomSeed ? opts.randomSeed : '', | 			SEED: opts.randomSeed ? opts.randomSeed : '', | ||||||
| 			YMD: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`, | 			YMD: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`, | ||||||
|  | 			AISCRIPT_DISABLED: !this.opts.enableAiScript, | ||||||
| 			NULL: null | 			NULL: null | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	public registerCanvas(id: string, canvas: any) { | ||||||
|  | 		this.canvases[id] = canvas; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	@autobind | 	@autobind | ||||||
| 	public updatePageVar(name: string, value: any) { | 	public updatePageVar(name: string, value: any) { | ||||||
| 		const pageVar = this.pageVars.find(v => v.name === name); | 		const pageVar = this.pageVars.find(v => v.name === name); | ||||||
| 		if (pageVar !== undefined) { | 		if (pageVar !== undefined) { | ||||||
| 			pageVar.value = value; | 			pageVar.value = value; | ||||||
|  | 			if (this.pageVarUpdatedCallback) { | ||||||
|  | 				if (this.aiscript) this.aiscript.execFn(this.pageVarUpdatedCallback, [values.STR(name), utils.jsToVal(value)]); | ||||||
|  | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			throw new AiScriptError(`No such page var '${name}'`); | 			throw new AoiScriptError(`No such page var '${name}'`); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @@ -110,6 +174,18 @@ export class ASEvaluator { | |||||||
| 			return scope.getState(block.value); | 			return scope.getState(block.value); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		if (block.type === 'aiScriptVar') { | ||||||
|  | 			if (this.aiscript) { | ||||||
|  | 				try { | ||||||
|  | 					return utils.valToJs(this.aiscript.scope.get(block.value)); | ||||||
|  | 				} catch (e) { | ||||||
|  | 					return null; | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				return null; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		if (isFnBlock(block)) { // ユーザー関数定義
 | 		if (isFnBlock(block)) { // ユーザー関数定義
 | ||||||
| 			return { | 			return { | ||||||
| 				slots: block.value.slots.map(x => x.name), | 				slots: block.value.slots.map(x => x.name), | ||||||
| @@ -206,14 +282,14 @@ export class ASEvaluator { | |||||||
| 		const fnName = block.type; | 		const fnName = block.type; | ||||||
| 		const fn = (funcs as any)[fnName]; | 		const fn = (funcs as any)[fnName]; | ||||||
| 		if (fn == null) { | 		if (fn == null) { | ||||||
| 			throw new AiScriptError(`No such function '${fnName}'`); | 			throw new AoiScriptError(`No such function '${fnName}'`); | ||||||
| 		} else { | 		} else { | ||||||
| 			return fn(...block.args.map(x => this.evaluate(x, scope))); | 			return fn(...block.args.map(x => this.evaluate(x, scope))); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class AiScriptError extends Error { | class AoiScriptError extends Error { | ||||||
| 	public info?: any; | 	public info?: any; | ||||||
| 
 | 
 | ||||||
| 	constructor(message: string, info?: any) { | 	constructor(message: string, info?: any) { | ||||||
| @@ -223,7 +299,7 @@ class AiScriptError extends Error { | |||||||
| 
 | 
 | ||||||
| 		// Maintains proper stack trace for where our error was thrown (only available on V8)
 | 		// Maintains proper stack trace for where our error was thrown (only available on V8)
 | ||||||
| 		if (Error.captureStackTrace) { | 		if (Error.captureStackTrace) { | ||||||
| 			Error.captureStackTrace(this, AiScriptError); | 			Error.captureStackTrace(this, AoiScriptError); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -256,7 +332,7 @@ class Scope { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		throw new AiScriptError( | 		throw new AoiScriptError( | ||||||
| 			`No such variable '${name}' in scope '${this.name}'`, { | 			`No such variable '${name}' in scope '${this.name}'`, { | ||||||
| 				scope: this.layerdStates | 				scope: this.layerdStates | ||||||
| 			}); | 			}); | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| /** | /** | ||||||
|  * AiScript |  * AoiScript | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
| @@ -95,6 +95,7 @@ export const literalDefs: Record<string, { out: any; category: string; icon: any | |||||||
| 	textList:      { out: 'stringArray', category: 'value', icon: faList, }, | 	textList:      { out: 'stringArray', category: 'value', icon: faList, }, | ||||||
| 	number:        { out: 'number',      category: 'value', icon: faSortNumericUp, }, | 	number:        { out: 'number',      category: 'value', icon: faSortNumericUp, }, | ||||||
| 	ref:           { out: null,          category: 'value', icon: faMagic, }, | 	ref:           { out: null,          category: 'value', icon: faMagic, }, | ||||||
|  | 	aiScriptVar:   { out: null,          category: 'value', icon: faMagic, }, | ||||||
| 	fn:            { out: 'function',    category: 'value', icon: faSquareRootAlt, }, | 	fn:            { out: 'function',    category: 'value', icon: faSquareRootAlt, }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @@ -127,6 +128,7 @@ export const envVarsDef: Record<string, Type> = { | |||||||
| 	IS_CAT: 'boolean', | 	IS_CAT: 'boolean', | ||||||
| 	SEED: null, | 	SEED: null, | ||||||
| 	YMD: 'string', | 	YMD: 'string', | ||||||
|  | 	AISCRIPT_DISABLED: 'boolean', | ||||||
| 	NULL: null, | 	NULL: null, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @@ -8,7 +8,7 @@ type TypeError = { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * AiScript type checker |  * AoiScript type checker | ||||||
|  */ |  */ | ||||||
| export class ASTypeChecker { | export class ASTypeChecker { | ||||||
| 	public variables: Variable[]; | 	public variables: Variable[]; | ||||||
| @@ -110,6 +110,7 @@ export class ASTypeChecker { | |||||||
| 
 | 
 | ||||||
| 			return null; | 			return null; | ||||||
| 		} | 		} | ||||||
|  | 		if (v.type === 'aiScriptVar') return null; | ||||||
| 		if (v.type === 'fn') return null; // todo
 | 		if (v.type === 'fn') return null; // todo
 | ||||||
| 		if (v.type.startsWith('fn:')) return null; // todo
 | 		if (v.type.startsWith('fn:')) return null; // todo
 | ||||||
| 
 | 
 | ||||||
							
								
								
									
										40
									
								
								src/client/scripts/create-aiscript-env.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/client/scripts/create-aiscript-env.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | import { utils, values } from '@syuilo/aiscript'; | ||||||
|  |  | ||||||
|  | export function createAiScriptEnv(vm, opts) { | ||||||
|  | 	let apiRequests = 0; | ||||||
|  | 	return { | ||||||
|  | 		USER_ID: values.STR(vm.$store.state.i.id), | ||||||
|  | 		USER_USERNAME: values.STR(vm.$store.state.i.username), | ||||||
|  | 		'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { | ||||||
|  | 			await vm.$root.dialog({ | ||||||
|  | 				type: type ? type.value : 'info', | ||||||
|  | 				title: title.value, | ||||||
|  | 				text: text.value, | ||||||
|  | 			}); | ||||||
|  | 		}), | ||||||
|  | 		'Mk:confirm': values.FN_NATIVE(async ([title, text]) => { | ||||||
|  | 			const confirm = await vm.$root.dialog({ | ||||||
|  | 				type: 'warning', | ||||||
|  | 				showCancelButton: true, | ||||||
|  | 				title: title.value, | ||||||
|  | 				text: text.value, | ||||||
|  | 			}); | ||||||
|  | 			return confirm.canceled ? values.FALSE : values.TRUE | ||||||
|  | 		}), | ||||||
|  | 		'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => { | ||||||
|  | 			apiRequests++; | ||||||
|  | 			if (apiRequests > 16) return values.NULL; | ||||||
|  | 			const res = await vm.$root.api(ep.value, utils.valToJs(param), token || null); | ||||||
|  | 			return utils.jsToVal(res); | ||||||
|  | 		}), | ||||||
|  | 		'Mk:save': values.FN_NATIVE(([key, value]) => { | ||||||
|  | 			utils.assertString(key); | ||||||
|  | 			localStorage.setItem('aiscript:' + opts.storageKey + ':' + key.value, JSON.stringify(utils.valToJs(value))); | ||||||
|  | 			return values.NULL; | ||||||
|  | 		}), | ||||||
|  | 		'Mk:load': values.FN_NATIVE(([key]) => { | ||||||
|  | 			utils.assertString(key); | ||||||
|  | 			return utils.jsToVal(JSON.parse(localStorage.getItem('aiscript:' + opts.storageKey + ':' + key.value))); | ||||||
|  | 		}), | ||||||
|  | 	}; | ||||||
|  | } | ||||||
| @@ -57,9 +57,9 @@ const ignoreElemens = ['input', 'textarea']; | |||||||
| function match(e: KeyboardEvent, patterns: action['patterns']): boolean { | function match(e: KeyboardEvent, patterns: action['patterns']): boolean { | ||||||
| 	const key = e.code.toLowerCase(); | 	const key = e.code.toLowerCase(); | ||||||
| 	return patterns.some(pattern => pattern.which.includes(key) && | 	return patterns.some(pattern => pattern.which.includes(key) && | ||||||
| 		pattern.ctrl == e.ctrlKey && | 		pattern.ctrl === e.ctrlKey && | ||||||
| 		pattern.shift == e.shiftKey && | 		pattern.shift === e.shiftKey && | ||||||
| 		pattern.alt == e.altKey && | 		pattern.alt === e.altKey && | ||||||
| 		!e.metaKey | 		!e.metaKey | ||||||
| 	); | 	); | ||||||
| } | } | ||||||
| @@ -80,6 +80,7 @@ export default { | |||||||
| 				el._keyHandler = (e: KeyboardEvent) => { | 				el._keyHandler = (e: KeyboardEvent) => { | ||||||
| 					const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : []; | 					const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : []; | ||||||
| 					if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return; | 					if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return; | ||||||
|  | 					if (document.activeElement && document.activeElement.attributes['contenteditable']) return; | ||||||
|  |  | ||||||
| 					for (const action of actions) { | 					for (const action of actions) { | ||||||
| 						const matched = match(e, action.patterns); | 						const matched = match(e, action.patterns); | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								src/client/scripts/is-device-touch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/client/scripts/is-device-touch.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | export function isDeviceTouch(): boolean { | ||||||
|  | 	return 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0; | ||||||
|  | } | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| export default (input: string): string[] => { | export default (input: string): string[] => { | ||||||
| 	if (Object.keys(aliases).some(a => a.toLowerCase() == input.toLowerCase())) { | 	if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) { | ||||||
| 		const codes = aliases[input]; | 		const codes = aliases[input]; | ||||||
| 		return Array.isArray(codes) ? codes : [codes]; | 		return Array.isArray(codes) ? codes : [codes]; | ||||||
| 	} else { | 	} else { | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ export default (opts) => ({ | |||||||
|  |  | ||||||
| 	computed: { | 	computed: { | ||||||
| 		empty(): boolean { | 		empty(): boolean { | ||||||
| 			return this.items.length == 0 && !this.fetching && this.inited; | 			return this.items.length === 0 && !this.fetching && this.inited; | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		error(): boolean { | 		error(): boolean { | ||||||
|   | |||||||
| @@ -47,9 +47,9 @@ export async function search(v: any, q: string) { | |||||||
| 				uri: q | 				uri: q | ||||||
| 			}); | 			}); | ||||||
| 			dialog.close(); | 			dialog.close(); | ||||||
| 			if (res.type == 'User') { | 			if (res.type === 'User') { | ||||||
| 				v.$router.push(`/@${res.object.username}@${res.object.host}`); | 				v.$router.push(`/@${res.object.username}@${res.object.host}`); | ||||||
| 			} else if (res.type == 'Note') { | 			} else if (res.type === 'Note') { | ||||||
| 				v.$router.push(`/notes/${res.object.id}`); | 				v.$router.push(`/notes/${res.object.id}`); | ||||||
| 			} | 			} | ||||||
| 		} catch (e) { | 		} catch (e) { | ||||||
|   | |||||||
| @@ -68,7 +68,7 @@ export default class Stream extends EventEmitter { | |||||||
| 	 */ | 	 */ | ||||||
| 	@autobind | 	@autobind | ||||||
| 	private onOpen() { | 	private onOpen() { | ||||||
| 		const isReconnect = this.state == 'reconnecting'; | 		const isReconnect = this.state === 'reconnecting'; | ||||||
|  |  | ||||||
| 		this.state = 'connected'; | 		this.state = 'connected'; | ||||||
| 		this.emit('_connected_'); | 		this.emit('_connected_'); | ||||||
| @@ -87,7 +87,7 @@ export default class Stream extends EventEmitter { | |||||||
| 	 */ | 	 */ | ||||||
| 	@autobind | 	@autobind | ||||||
| 	private onClose() { | 	private onClose() { | ||||||
| 		if (this.state == 'connected') { | 		if (this.state === 'connected') { | ||||||
| 			this.state = 'reconnecting'; | 			this.state = 'reconnecting'; | ||||||
| 			this.emit('_disconnected_'); | 			this.emit('_disconnected_'); | ||||||
| 		} | 		} | ||||||
| @@ -100,7 +100,7 @@ export default class Stream extends EventEmitter { | |||||||
| 	private onMessage(message) { | 	private onMessage(message) { | ||||||
| 		const { type, body } = JSON.parse(message.data); | 		const { type, body } = JSON.parse(message.data); | ||||||
|  |  | ||||||
| 		if (type == 'channel') { | 		if (type === 'channel') { | ||||||
| 			const id = body.id; | 			const id = body.id; | ||||||
|  |  | ||||||
| 			let connections: Connection[]; | 			let connections: Connection[]; | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import Vuex from 'vuex'; | import Vuex from 'vuex'; | ||||||
| import createPersistedState from 'vuex-persistedstate'; | import createPersistedState from 'vuex-persistedstate'; | ||||||
| import * as nestedProperty from 'nested-property'; | import * as nestedProperty from 'nested-property'; | ||||||
|  | import { apiUrl } from './config'; | ||||||
| import MiOS from './mios'; |  | ||||||
|  |  | ||||||
| const defaultSettings = { | const defaultSettings = { | ||||||
| 	tutorial: 0, | 	tutorial: 0, | ||||||
| @@ -43,6 +42,7 @@ const defaultDeviceSettings = { | |||||||
| 	animatedMfm: true, | 	animatedMfm: true, | ||||||
| 	imageNewTab: false, | 	imageNewTab: false, | ||||||
| 	showFixedPostForm: false, | 	showFixedPostForm: false, | ||||||
|  | 	disablePagesScript: true, | ||||||
| 	sfxVolume: 0.3, | 	sfxVolume: 0.3, | ||||||
| 	sfxNote: 'syuilo/down', | 	sfxNote: 'syuilo/down', | ||||||
| 	sfxNoteMy: 'syuilo/up', | 	sfxNoteMy: 'syuilo/up', | ||||||
| @@ -57,13 +57,15 @@ function copy<T>(data: T): T { | |||||||
| 	return JSON.parse(JSON.stringify(data)); | 	return JSON.parse(JSON.stringify(data)); | ||||||
| } | } | ||||||
|  |  | ||||||
| export default (os: MiOS) => new Vuex.Store({ | export default () => new Vuex.Store({ | ||||||
| 	plugins: [createPersistedState({ | 	plugins: [createPersistedState({ | ||||||
| 		paths: ['i', 'device', 'deviceUser', 'settings', 'instance'] | 		paths: ['i', 'device', 'deviceUser', 'settings', 'instance'] | ||||||
| 	})], | 	})], | ||||||
|  |  | ||||||
| 	state: { | 	state: { | ||||||
| 		i: null, | 		i: null, | ||||||
|  | 		pendingApiRequestsCount: 0, | ||||||
|  | 		spinner: null | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	getters: { | 	getters: { | ||||||
| @@ -121,6 +123,47 @@ export default (os: MiOS) => new Vuex.Store({ | |||||||
| 				ctx.commit('settings/init', me.clientData); | 				ctx.commit('settings/init', me.clientData); | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | 		api(ctx, { endpoint, data, token }) { | ||||||
|  | 			if (++ctx.state.pendingApiRequestsCount === 1) { | ||||||
|  | 				// TODO: spinnerの表示はstoreでやらない | ||||||
|  | 				ctx.state.spinner = document.createElement('div'); | ||||||
|  | 				ctx.state.spinner.setAttribute('id', 'wait'); | ||||||
|  | 				document.body.appendChild(ctx.state.spinner); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			const onFinally = () => { | ||||||
|  | 				if (--ctx.state.pendingApiRequestsCount === 0) ctx.state.spinner.parentNode.removeChild(ctx.state.spinner); | ||||||
|  | 			}; | ||||||
|  |  | ||||||
|  | 			const promise = new Promise((resolve, reject) => { | ||||||
|  | 				// Append a credential | ||||||
|  | 				if (ctx.getters.isSignedIn) (data as any).i = ctx.state.i.token; | ||||||
|  | 				if (token !== undefined) (data as any).i = token; | ||||||
|  |  | ||||||
|  | 				// Send request | ||||||
|  | 				fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { | ||||||
|  | 					method: 'POST', | ||||||
|  | 					body: JSON.stringify(data), | ||||||
|  | 					credentials: 'omit', | ||||||
|  | 					cache: 'no-cache' | ||||||
|  | 				}).then(async (res) => { | ||||||
|  | 					const body = res.status === 204 ? null : await res.json(); | ||||||
|  |  | ||||||
|  | 					if (res.status === 200) { | ||||||
|  | 						resolve(body); | ||||||
|  | 					} else if (res.status === 204) { | ||||||
|  | 						resolve(); | ||||||
|  | 					} else { | ||||||
|  | 						reject(body.error); | ||||||
|  | 					} | ||||||
|  | 				}).catch(reject); | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			promise.then(onFinally, onFinally); | ||||||
|  |  | ||||||
|  | 			return promise; | ||||||
|  | 		} | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	modules: { | 	modules: { | ||||||
| @@ -139,9 +182,12 @@ export default (os: MiOS) => new Vuex.Store({ | |||||||
|  |  | ||||||
| 			actions: { | 			actions: { | ||||||
| 				async fetch(ctx) { | 				async fetch(ctx) { | ||||||
| 					const meta = await os.api('meta', { | 					const meta = await ctx.dispatch('api', { | ||||||
|  | 						endpoint: 'meta', | ||||||
|  | 						data: { | ||||||
| 							detail: false | 							detail: false | ||||||
| 					}); | 						} | ||||||
|  | 					}, { root: true }); | ||||||
|  |  | ||||||
| 					ctx.commit('set', meta); | 					ctx.commit('set', meta); | ||||||
| 				} | 				} | ||||||
| @@ -212,7 +258,7 @@ export default (os: MiOS) => new Vuex.Store({ | |||||||
| 				}, | 				}, | ||||||
|  |  | ||||||
| 				updateWidget(state, x) { | 				updateWidget(state, x) { | ||||||
| 					const w = state.widgets.find(w => w.id == x.id); | 					const w = state.widgets.find(w => w.id === x.id); | ||||||
| 					if (w) { | 					if (w) { | ||||||
| 						w.data = x.data; | 						w.data = x.data; | ||||||
| 					} | 					} | ||||||
| @@ -246,10 +292,13 @@ export default (os: MiOS) => new Vuex.Store({ | |||||||
| 					ctx.commit('set', x); | 					ctx.commit('set', x); | ||||||
|  |  | ||||||
| 					if (ctx.rootGetters.isSignedIn) { | 					if (ctx.rootGetters.isSignedIn) { | ||||||
| 						os.api('i/update-client-setting', { | 						ctx.dispatch('api', { | ||||||
|  | 							endpoint: 'i/update-client-setting', | ||||||
|  | 							data: { | ||||||
| 								name: x.key, | 								name: x.key, | ||||||
| 								value: x.value | 								value: x.value | ||||||
| 						}); | 							} | ||||||
|  | 						}, { root: true }); | ||||||
| 					} | 					} | ||||||
| 				}, | 				}, | ||||||
| 			} | 			} | ||||||
|   | |||||||
| @@ -51,14 +51,14 @@ export default Vue.extend({ | |||||||
| 				weekday: date.getDay() | 				weekday: date.getDay() | ||||||
| 			}; | 			}; | ||||||
|  |  | ||||||
| 			d.v = peak == 0 ? 0 : d.total / (peak / 2); | 			d.v = peak === 0 ? 0 : d.total / (peak / 2); | ||||||
| 			if (d.v > 1) d.v = 1; | 			if (d.v > 1) d.v = 1; | ||||||
| 			const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170; | 			const ch = d.date.weekday === 0 || d.date.weekday === 6 ? 275 : 170; | ||||||
| 			const cs = d.v * 100; | 			const cs = d.v * 100; | ||||||
| 			const cl = 15 + ((1 - d.v) * 80); | 			const cl = 15 + ((1 - d.v) * 80); | ||||||
| 			d.color = `hsl(${ch}, ${cs}%, ${cl}%)`; | 			d.color = `hsl(${ch}, ${cs}%, ${cl}%)`; | ||||||
|  |  | ||||||
| 			if (d.date.weekday == 0) x--; | 			if (d.date.weekday === 0) x--; | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ export default define({ | |||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		func() { | 		func() { | ||||||
| 			if (this.props.design == 2) { | 			if (this.props.design === 2) { | ||||||
| 				this.props.design = 0; | 				this.props.design = 0; | ||||||
| 			} else { | 			} else { | ||||||
| 				this.props.design++; | 				this.props.design++; | ||||||
| @@ -68,7 +68,7 @@ export default define({ | |||||||
| 			this.save(); | 			this.save(); | ||||||
| 		}, | 		}, | ||||||
| 		toggleView() { | 		toggleView() { | ||||||
| 			if (this.props.view == 1) { | 			if (this.props.view === 1) { | ||||||
| 				this.props.view = 0; | 				this.props.view = 0; | ||||||
| 			} else { | 			} else { | ||||||
| 				this.props.view++; | 				this.props.view++; | ||||||
|   | |||||||
| @@ -65,7 +65,7 @@ export default define({ | |||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		func() { | 		func() { | ||||||
| 			if (this.props.design == 2) { | 			if (this.props.design === 2) { | ||||||
| 				this.props.design = 0; | 				this.props.design = 0; | ||||||
| 			} else { | 			} else { | ||||||
| 				this.props.design++; | 				this.props.design++; | ||||||
| @@ -102,7 +102,7 @@ export default define({ | |||||||
| 			this.monthP = monthNumer / monthDenom * 100; | 			this.monthP = monthNumer / monthDenom * 100; | ||||||
| 			this.yearP  = yearNumer  / yearDenom  * 100; | 			this.yearP  = yearNumer  / yearDenom  * 100; | ||||||
|  |  | ||||||
| 			this.isHoliday = now.getDay() == 0 || now.getDay() == 6; | 			this.isHoliday = now.getDay() === 0 || now.getDay() === 6; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<mk-container :show-header="props.design === 0" :naked="props.design === 2" :class="$style.root" :data-melt="props.design == 2"> | 	<mk-container :show-header="props.design === 0" :naked="props.design === 2" :class="$style.root" :data-melt="props.design === 2"> | ||||||
| 		<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template> | 		<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template> | ||||||
|  |  | ||||||
| 		<div class=""> | 		<div class=""> | ||||||
| @@ -66,7 +66,7 @@ export default define({ | |||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		func() { | 		func() { | ||||||
| 			if (this.props.design == 2) { | 			if (this.props.design === 2) { | ||||||
| 				this.props.design = 0; | 				this.props.design = 0; | ||||||
| 			} else { | 			} else { | ||||||
| 				this.props.design++; | 				this.props.design++; | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ const dir = `${__dirname}/../../.config`; | |||||||
| /** | /** | ||||||
|  * Path of configuration file |  * Path of configuration file | ||||||
|  */ |  */ | ||||||
| const path = process.env.NODE_ENV == 'test' | const path = process.env.NODE_ENV === 'test' | ||||||
| 	? `${dir}/test.yml` | 	? `${dir}/test.yml` | ||||||
| 	: `${dir}/default.yml`; | 	: `${dir}/default.yml`; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -35,6 +35,7 @@ export type Source = { | |||||||
|  |  | ||||||
| 	proxy?: string; | 	proxy?: string; | ||||||
| 	proxySmtp?: string; | 	proxySmtp?: string; | ||||||
|  | 	proxyBypassHosts?: string[]; | ||||||
|  |  | ||||||
| 	accesslog?: string; | 	accesslog?: string; | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								src/docs/aiscript.ja-JP.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/docs/aiscript.ja-JP.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | # AiScript | ||||||
|  |  | ||||||
|  | ## 関数 | ||||||
|  | デフォルトで値渡しです。 | ||||||
| @@ -27,6 +27,8 @@ APIを使い始めるには、まずアクセストークンを取得する必 | |||||||
|  |  | ||||||
| UUIDを生成する。以後これをセッションIDと呼びます。 | UUIDを生成する。以後これをセッションIDと呼びます。 | ||||||
|  |  | ||||||
|  | > このセッションIDは毎回生成し、使いまわさないようにしてください。 | ||||||
|  |  | ||||||
| #### Step 2 | #### Step 2 | ||||||
|  |  | ||||||
| `{_URL_}/miauth/{session}`をユーザーのブラウザで表示させる。`{session}`の部分は、セッションIDに置き換えてください。 | `{_URL_}/miauth/{session}`をユーザーのブラウザで表示させる。`{session}`の部分は、セッションIDに置き換えてください。 | ||||||
|   | |||||||
| @@ -7,4 +7,4 @@ | |||||||
|  |  | ||||||
| ユーザーからの入力を受け取るには、ページに「ユーザー入力」ブロックを設置し、「変数名」に入力を格納したい変数名を設定します(変数は自動で作成されます)。その変数を使ってユーザー入力に応じた動作を行えます。 | ユーザーからの入力を受け取るには、ページに「ユーザー入力」ブロックを設置し、「変数名」に入力を格納したい変数名を設定します(変数は自動で作成されます)。その変数を使ってユーザー入力に応じた動作を行えます。 | ||||||
|  |  | ||||||
| 関数を使うと、値の算出処理を再利用可能な形にまとめることができます。関数を作るには、「関数」タイプの変数を作成します。関数にはスロット(引数)を設定することができ、スロットの値は関数内で変数として利用可能です。また、AiScript標準で関数を引数に取る関数(高階関数と呼ばれます)も存在します。関数は予め定義しておくほかに、このような高階関数のスロットに即席でセットすることもできます。 | 関数を使うと、値の算出処理を再利用可能な形にまとめることができます。関数を作るには、「関数」タイプの変数を作成します。関数にはスロット(引数)を設定することができ、スロットの値は関数内で変数として利用可能です。また、関数を引数に取る関数(高階関数と呼ばれます)も存在します。関数は予め定義しておくほかに、このような高階関数のスロットに即席でセットすることもできます。 | ||||||
|   | |||||||
| @@ -226,10 +226,10 @@ export default class Reversi { | |||||||
| 				// 座標が指し示す位置がボード外に出たとき | 				// 座標が指し示す位置がボード外に出たとき | ||||||
| 				if (this.opts.loopedBoard && this.transformXyToPos( | 				if (this.opts.loopedBoard && this.transformXyToPos( | ||||||
| 					(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth), | 					(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth), | ||||||
| 					(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) == initPos) | 					(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos) | ||||||
| 						// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ) | 						// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ) | ||||||
| 					return found; | 					return found; | ||||||
| 				else if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) | 				else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight) | ||||||
| 					return []; // 挟めないことが確定 (盤面外に到達) | 					return []; // 挟めないことが確定 (盤面外に到達) | ||||||
|  |  | ||||||
| 				const pos = this.transformXyToPos(x, y); | 				const pos = this.transformXyToPos(x, y); | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { parseFragment, DefaultTreeDocumentFragment } from 'parse5'; | import { parseFragment, DefaultTreeDocumentFragment } from 'parse5'; | ||||||
| import { urlRegex } from './prelude'; | import { urlRegex } from './prelude'; | ||||||
|  |  | ||||||
| export function fromHtml(html: string): string { | export function fromHtml(html: string, hashtagNames?: string[]): string { | ||||||
| 	const dom = parseFragment(html) as DefaultTreeDocumentFragment; | 	const dom = parseFragment(html) as DefaultTreeDocumentFragment; | ||||||
|  |  | ||||||
| 	let text = ''; | 	let text = ''; | ||||||
| @@ -13,7 +13,7 @@ export function fromHtml(html: string): string { | |||||||
| 	return text.trim(); | 	return text.trim(); | ||||||
|  |  | ||||||
| 	function getText(node: any): string { | 	function getText(node: any): string { | ||||||
| 		if (node.nodeName == '#text') return node.value; | 		if (node.nodeName === '#text') return node.value; | ||||||
|  |  | ||||||
| 		if (node.childNodes) { | 		if (node.childNodes) { | ||||||
| 			return node.childNodes.map((n: any) => getText(n)).join(''); | 			return node.childNodes.map((n: any) => getText(n)).join(''); | ||||||
| @@ -34,29 +34,27 @@ export function fromHtml(html: string): string { | |||||||
|  |  | ||||||
| 			case 'a': | 			case 'a': | ||||||
| 				const txt = getText(node); | 				const txt = getText(node); | ||||||
| 				const rel = node.attrs.find((x: any) => x.name == 'rel'); | 				const rel = node.attrs.find((x: any) => x.name === 'rel'); | ||||||
| 				const href = node.attrs.find((x: any) => x.name == 'href'); | 				const href = node.attrs.find((x: any) => x.name === 'href'); | ||||||
| 				const _class = node.attrs.find((x: any) => x.name == 'class'); |  | ||||||
| 				const isHashtag = rel?.value?.match('tag') || _class?.value?.match('hashtag'); |  | ||||||
|  |  | ||||||
| 				// ハッシュタグ / hrefがない / txtがURL | 				// ハッシュタグ | ||||||
| 				if (isHashtag || !href || href.value == txt) { | 				if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { | ||||||
| 					text += isHashtag || txt.match(urlRegex) ? txt : `<${txt}>`; | 					text += txt; | ||||||
| 				// メンション | 				// メンション | ||||||
| 				} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) { | 				} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) { | ||||||
| 					const part = txt.split('@'); | 					const part = txt.split('@'); | ||||||
|  |  | ||||||
| 					if (part.length == 2) { | 					if (part.length === 2) { | ||||||
| 						//#region ホスト名部分が省略されているので復元する | 						//#region ホスト名部分が省略されているので復元する | ||||||
| 						const acct = `${txt}@${(new URL(href.value)).hostname}`; | 						const acct = `${txt}@${(new URL(href.value)).hostname}`; | ||||||
| 						text += acct; | 						text += acct; | ||||||
| 						//#endregion | 						//#endregion | ||||||
| 					} else if (part.length == 3) { | 					} else if (part.length === 3) { | ||||||
| 						text += txt; | 						text += txt; | ||||||
| 					} | 					} | ||||||
| 				// その他 | 				// その他 | ||||||
| 				} else { | 				} else { | ||||||
| 					text += `[${txt}](${href.value})`; | 					text += (!href || (txt === href.value && txt.match(urlRegex))) ? txt : `[${txt}](${href.value})`; | ||||||
| 				} | 				} | ||||||
| 				break; | 				break; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ export const mfmLanguage = P.createLanguage({ | |||||||
| 		r.center, | 		r.center, | ||||||
| 	), | 	), | ||||||
| 	startOfLine: () => P((input, i) => { | 	startOfLine: () => P((input, i) => { | ||||||
| 		if (i == 0 || input[i] == '\n' || input[i - 1] == '\n') { | 		if (i === 0 || input[i] === '\n' || input[i - 1] === '\n') { | ||||||
| 			return P.makeSuccess(i, null); | 			return P.makeSuccess(i, null); | ||||||
| 		} else { | 		} else { | ||||||
| 			return P.makeFailure(i, 'not newline'); | 			return P.makeFailure(i, 'not newline'); | ||||||
| @@ -50,7 +50,7 @@ export const mfmLanguage = P.createLanguage({ | |||||||
| 		if (!text.match(/^>[\s\S]+?/)) return P.makeFailure(i, 'not a quote'); | 		if (!text.match(/^>[\s\S]+?/)) return P.makeFailure(i, 'not a quote'); | ||||||
| 		const quote = takeWhile(line => line.startsWith('>'), text.split('\n')); | 		const quote = takeWhile(line => line.startsWith('>'), text.split('\n')); | ||||||
| 		const qInner = quote.join('\n').replace(/^>/gm, '').replace(/^ /gm, ''); | 		const qInner = quote.join('\n').replace(/^>/gm, '').replace(/^ /gm, ''); | ||||||
| 		if (qInner == '') return P.makeFailure(i, 'not a quote'); | 		if (qInner === '') return P.makeFailure(i, 'not a quote'); | ||||||
| 		const contents = r.root.tryParse(qInner); | 		const contents = r.root.tryParse(qInner); | ||||||
| 		return P.makeSuccess(i + quote.join('\n').length + 1, createTree('quote', contents, {})); | 		return P.makeSuccess(i + quote.join('\n').length + 1, createTree('quote', contents, {})); | ||||||
| 	})), | 	})), | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import { MfmForest, MfmTree } from './prelude'; | |||||||
| import { createTree, createLeaf } from '../prelude/tree'; | import { createTree, createLeaf } from '../prelude/tree'; | ||||||
|  |  | ||||||
| function isEmptyTextTree(t: MfmTree): boolean { | function isEmptyTextTree(t: MfmTree): boolean { | ||||||
| 	return t.node.type == 'text' && t.node.props.text === ''; | 	return t.node.type === 'text' && t.node.props.text === ''; | ||||||
| } | } | ||||||
|  |  | ||||||
| function concatTextTrees(ts: MfmForest): MfmTree { | function concatTextTrees(ts: MfmForest): MfmTree { | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { MfmForest } from './prelude'; | |||||||
| import { normalize } from './normalize'; | import { normalize } from './normalize'; | ||||||
|  |  | ||||||
| export function parse(source: string | null): MfmForest | null { | export function parse(source: string | null): MfmForest | null { | ||||||
| 	if (source == null || source == '') { | 	if (source == null || source === '') { | ||||||
| 		return null; | 		return null; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -11,7 +11,7 @@ export function parse(source: string | null): MfmForest | null { | |||||||
| } | } | ||||||
|  |  | ||||||
| export function parsePlain(source: string | null): MfmForest | null { | export function parsePlain(source: string | null): MfmForest | null { | ||||||
| 	if (source == null || source == '') { | 	if (source == null || source === '') { | ||||||
| 		return null; | 		return null; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { createTemp } from './create-temp'; | import { createTemp } from './create-temp'; | ||||||
| import { downloadUrl } from './donwload-url'; | import { downloadUrl } from './download-url'; | ||||||
| import { detectType } from './get-file-info'; | import { detectType } from './get-file-info'; | ||||||
|  |  | ||||||
| export async function detectUrlMime(url: string) { | export async function detectUrlMime(url: string) { | ||||||
|   | |||||||
| @@ -1,59 +0,0 @@ | |||||||
| import * as fs from 'fs'; |  | ||||||
| import * as request from 'request'; |  | ||||||
| import config from '../config'; |  | ||||||
| import * as chalk from 'chalk'; |  | ||||||
| import Logger from '../services/logger'; |  | ||||||
|  |  | ||||||
| export async function downloadUrl(url: string, path: string) { |  | ||||||
| 	const logger = new Logger('download'); |  | ||||||
|  |  | ||||||
| 	await new Promise((res, rej) => { |  | ||||||
| 		logger.info(`Downloading ${chalk.cyan(url)} ...`); |  | ||||||
|  |  | ||||||
| 		const writable = fs.createWriteStream(path); |  | ||||||
|  |  | ||||||
| 		writable.on('finish', () => { |  | ||||||
| 			logger.succ(`Download finished: ${chalk.cyan(url)}`); |  | ||||||
| 			res(); |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		writable.on('error', error => { |  | ||||||
| 			logger.error(`Download failed: ${chalk.cyan(url)}: ${error}`, { |  | ||||||
| 				url: url, |  | ||||||
| 				e: error |  | ||||||
| 			}); |  | ||||||
| 			rej(error); |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		const req = request({ |  | ||||||
| 			url: new URL(url).href, // https://github.com/syuilo/misskey/issues/2637 |  | ||||||
| 			proxy: config.proxy, |  | ||||||
| 			timeout: 10 * 1000, |  | ||||||
| 			forever: true, |  | ||||||
| 			headers: { |  | ||||||
| 				'User-Agent': config.userAgent |  | ||||||
| 			} |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		req.pipe(writable); |  | ||||||
|  |  | ||||||
| 		req.on('response', response => { |  | ||||||
| 			if (response.statusCode !== 200) { |  | ||||||
| 				logger.error(`Got ${response.statusCode} (${url})`); |  | ||||||
| 				writable.close(); |  | ||||||
| 				rej(response.statusCode); |  | ||||||
| 			} |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		req.on('error', error => { |  | ||||||
| 			logger.error(`Failed to start download: ${chalk.cyan(url)}: ${error}`, { |  | ||||||
| 				url: url, |  | ||||||
| 				e: error |  | ||||||
| 			}); |  | ||||||
| 			writable.close(); |  | ||||||
| 			rej(error); |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		logger.succ(`Downloaded to: ${path}`); |  | ||||||
| 	}); |  | ||||||
| } |  | ||||||
| @@ -2,7 +2,7 @@ import * as fs from 'fs'; | |||||||
| import * as util from 'util'; | import * as util from 'util'; | ||||||
| import Logger from '../services/logger'; | import Logger from '../services/logger'; | ||||||
| import { createTemp } from './create-temp'; | import { createTemp } from './create-temp'; | ||||||
| import { downloadUrl } from './donwload-url'; | import { downloadUrl } from './download-url'; | ||||||
|  |  | ||||||
| const logger = new Logger('download-text-file'); | const logger = new Logger('download-text-file'); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										39
									
								
								src/misc/download-url.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/misc/download-url.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | import * as fs from 'fs'; | ||||||
|  | import * as stream from 'stream'; | ||||||
|  | import * as util from 'util'; | ||||||
|  | import fetch from 'node-fetch'; | ||||||
|  | import { getAgentByUrl } from './fetch'; | ||||||
|  | import { AbortController } from 'abort-controller'; | ||||||
|  | import config from '../config'; | ||||||
|  | import * as chalk from 'chalk'; | ||||||
|  | import Logger from '../services/logger'; | ||||||
|  |  | ||||||
|  | const pipeline = util.promisify(stream.pipeline); | ||||||
|  |  | ||||||
|  | export async function downloadUrl(url: string, path: string) { | ||||||
|  | 	const logger = new Logger('download'); | ||||||
|  |  | ||||||
|  | 	logger.info(`Downloading ${chalk.cyan(url)} ...`); | ||||||
|  | 	const controller = new AbortController(); | ||||||
|  | 	setTimeout(() => { | ||||||
|  | 		controller.abort(); | ||||||
|  | 	}, 11 * 1000); | ||||||
|  |  | ||||||
|  | 	const response = await fetch(new URL(url).href, { | ||||||
|  | 		headers: { | ||||||
|  | 			'User-Agent': config.userAgent | ||||||
|  | 		}, | ||||||
|  | 		timeout: 10 * 1000, | ||||||
|  | 		signal: controller.signal, | ||||||
|  | 		agent: getAgentByUrl, | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	if (!response.ok) { | ||||||
|  | 		logger.error(`Got ${response.status} (${url})`); | ||||||
|  | 		throw response.status; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	await pipeline(response.body, fs.createWriteStream(path)); | ||||||
|  |  | ||||||
|  | 	logger.succ(`Download finished: ${chalk.cyan(url)}`); | ||||||
|  | } | ||||||
							
								
								
									
										72
									
								
								src/misc/fetch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/misc/fetch.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | import * as http from 'http'; | ||||||
|  | import * as https from 'https'; | ||||||
|  | import * as cache from 'lookup-dns-cache'; | ||||||
|  | import fetch, { HeadersInit } from 'node-fetch'; | ||||||
|  | import { HttpProxyAgent } from 'http-proxy-agent'; | ||||||
|  | import { HttpsProxyAgent } from 'https-proxy-agent'; | ||||||
|  | import config from '../config'; | ||||||
|  |  | ||||||
|  | export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: HeadersInit) { | ||||||
|  | 	const res = await fetch(url, { | ||||||
|  | 		headers: Object.assign({ | ||||||
|  | 			'User-Agent': config.userAgent, | ||||||
|  | 			Accept: accept | ||||||
|  | 		}, headers || {}), | ||||||
|  | 		timeout, | ||||||
|  | 		agent: getAgentByUrl, | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	if (!res.ok) { | ||||||
|  | 		throw { | ||||||
|  | 			name: `StatusError`, | ||||||
|  | 			statusCode: res.status, | ||||||
|  | 			message: `${res.status} ${res.statusText}`, | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return await res.json(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get http non-proxy agent | ||||||
|  |  */ | ||||||
|  | const _http = new http.Agent({ | ||||||
|  | 	keepAlive: true, | ||||||
|  | 	keepAliveMsecs: 30 * 1000, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get https non-proxy agent | ||||||
|  |  */ | ||||||
|  | const _https = new https.Agent({ | ||||||
|  | 	keepAlive: true, | ||||||
|  | 	keepAliveMsecs: 30 * 1000, | ||||||
|  | 	lookup: cache.lookup, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get http proxy or non-proxy agent | ||||||
|  |  */ | ||||||
|  | export const httpAgent = config.proxy | ||||||
|  | 	? new HttpProxyAgent(config.proxy) | ||||||
|  | 	: _http; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get https proxy or non-proxy agent | ||||||
|  |  */ | ||||||
|  | export const httpsAgent = config.proxy | ||||||
|  | 	? new HttpsProxyAgent(config.proxy) | ||||||
|  | 	: _https; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get agent by URL | ||||||
|  |  * @param url URL | ||||||
|  |  * @param bypassProxy Allways bypass proxy | ||||||
|  |  */ | ||||||
|  | export function getAgentByUrl(url: URL, bypassProxy = false) { | ||||||
|  | 	if (bypassProxy || (config.proxyBypassHosts || []).includes(url.hostname)) { | ||||||
|  | 		return url.protocol == 'http:' ? _http : _https; | ||||||
|  | 	} else { | ||||||
|  | 		return url.protocol == 'http:' ? httpAgent : httpsAgent; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1,10 +1,14 @@ | |||||||
| import * as fs from 'fs'; | import * as fs from 'fs'; | ||||||
| import * as crypto from 'crypto'; | import * as crypto from 'crypto'; | ||||||
|  | import * as stream from 'stream'; | ||||||
|  | import * as util from 'util'; | ||||||
| import * as fileType from 'file-type'; | import * as fileType from 'file-type'; | ||||||
| import isSvg from 'is-svg'; | import isSvg from 'is-svg'; | ||||||
| import * as probeImageSize from 'probe-image-size'; | import * as probeImageSize from 'probe-image-size'; | ||||||
| import * as sharp from 'sharp'; | import * as sharp from 'sharp'; | ||||||
|  |  | ||||||
|  | const pipeline = util.promisify(stream.pipeline); | ||||||
|  |  | ||||||
| export type FileInfo = { | export type FileInfo = { | ||||||
| 	size: number; | 	size: number; | ||||||
| 	md5: string; | 	md5: string; | ||||||
| @@ -138,32 +142,17 @@ export async function checkSvg(path: string) { | |||||||
|  * Get file size |  * Get file size | ||||||
|  */ |  */ | ||||||
| export async function getFileSize(path: string): Promise<number> { | export async function getFileSize(path: string): Promise<number> { | ||||||
| 	return new Promise<number>((res, rej) => { | 	const getStat = util.promisify(fs.stat); | ||||||
| 		fs.stat(path, (err, stats) => { | 	return (await getStat(path)).size; | ||||||
| 			if (err) return rej(err); |  | ||||||
| 			res(stats.size); |  | ||||||
| 		}); |  | ||||||
| 	}); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Calculate MD5 hash |  * Calculate MD5 hash | ||||||
|  */ |  */ | ||||||
| async function calcHash(path: string): Promise<string> { | async function calcHash(path: string): Promise<string> { | ||||||
| 	return new Promise<string>((res, rej) => { | 	const hash = crypto.createHash('md5').setEncoding('hex'); | ||||||
| 		const readable = fs.createReadStream(path); | 	await pipeline(fs.createReadStream(path), hash); | ||||||
| 		const hash = crypto.createHash('md5'); | 	return hash.read(); | ||||||
| 		const chunks: Buffer[] = []; |  | ||||||
| 		readable |  | ||||||
| 			.on('error', rej) |  | ||||||
| 			.pipe(hash) |  | ||||||
| 			.on('error', rej) |  | ||||||
| 			.on('data', chunk => chunks.push(chunk)) |  | ||||||
| 			.on('end', () => { |  | ||||||
| 				const buffer = Buffer.concat(chunks); |  | ||||||
| 				res(buffer.toString('hex')); |  | ||||||
| 			}); |  | ||||||
| 	}); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { emojiRegex } from './emoji-regex'; | import { emojiRegex } from './emoji-regex'; | ||||||
| import { fetchMeta } from './fetch-meta'; | import { fetchMeta } from './fetch-meta'; | ||||||
| import { Emojis } from '../models'; | import { Emojis } from '../models'; | ||||||
|  | import { toPunyNullable } from './convert-host'; | ||||||
|  |  | ||||||
| const legacies: Record<string, string> = { | const legacies: Record<string, string> = { | ||||||
| 	'like':     '👍', | 	'like':     '👍', | ||||||
| @@ -25,6 +26,8 @@ export function convertLegacyReactions(reactions: Record<string, number>) { | |||||||
| 	const _reactions = {} as Record<string, number>; | 	const _reactions = {} as Record<string, number>; | ||||||
|  |  | ||||||
| 	for (const reaction of Object.keys(reactions)) { | 	for (const reaction of Object.keys(reactions)) { | ||||||
|  | 		if (reactions[reaction] <= 0) continue; | ||||||
|  |  | ||||||
| 		if (Object.keys(legacies).includes(reaction)) { | 		if (Object.keys(legacies).includes(reaction)) { | ||||||
| 			if (_reactions[legacies[reaction]]) { | 			if (_reactions[legacies[reaction]]) { | ||||||
| 				_reactions[legacies[reaction]] += reactions[reaction]; | 				_reactions[legacies[reaction]] += reactions[reaction]; | ||||||
| @@ -40,12 +43,20 @@ export function convertLegacyReactions(reactions: Record<string, number>) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return _reactions; | 	const _reactions2 = {} as Record<string, number>; | ||||||
|  |  | ||||||
|  | 	for (const reaction of Object.keys(_reactions)) { | ||||||
|  | 		_reactions2[decodeReaction(reaction).reaction] = _reactions[reaction]; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return _reactions2; | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function toDbReaction(reaction?: string | null): Promise<string> { | export async function toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> { | ||||||
| 	if (reaction == null) return await getFallbackReaction(); | 	if (reaction == null) return await getFallbackReaction(); | ||||||
|  |  | ||||||
|  | 	reacterHost = toPunyNullable(reacterHost); | ||||||
|  |  | ||||||
| 	// 文字列タイプのリアクションを絵文字に変換 | 	// 文字列タイプのリアクションを絵文字に変換 | ||||||
| 	if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; | 	if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; | ||||||
|  |  | ||||||
| @@ -59,20 +70,60 @@ export async function toDbReaction(reaction?: string | null): Promise<string> { | |||||||
| 		return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); | 		return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const custom = reaction.match(/^:([\w+-]+):$/); | 	const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); | ||||||
| 	if (custom) { | 	if (custom) { | ||||||
|  | 		const name = custom[1]; | ||||||
| 		const emoji = await Emojis.findOne({ | 		const emoji = await Emojis.findOne({ | ||||||
| 			host: null, | 			host: reacterHost || null, | ||||||
| 			name: custom[1], | 			name, | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		if (emoji) return reaction; | 		if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:` | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return await getFallbackReaction(); | 	return await getFallbackReaction(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type DecodedReaction = { | ||||||
|  | 	/** | ||||||
|  | 	 * リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.') | ||||||
|  | 	 */ | ||||||
|  | 	reaction: string; | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * name (カスタム絵文字の場合name, Emojiクエリに使う) | ||||||
|  | 	 */ | ||||||
|  | 	name?: string; | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * host (カスタム絵文字の場合host, Emojiクエリに使う) | ||||||
|  | 	 */ | ||||||
|  | 	host?: string | null; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export function decodeReaction(str: string): DecodedReaction { | ||||||
|  | 	const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/); | ||||||
|  |  | ||||||
|  | 	if (custom) { | ||||||
|  | 		const name = custom[1]; | ||||||
|  | 		const host = custom[2] || null; | ||||||
|  |  | ||||||
|  | 		return { | ||||||
|  | 			reaction: `:${name}@${host || '.'}:`,	// ローカル分は@以降を省略するのではなく.にする | ||||||
|  | 			name, | ||||||
|  | 			host | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return { | ||||||
|  | 		reaction: str, | ||||||
|  | 		name: undefined, | ||||||
|  | 		host: undefined | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  |  | ||||||
| export function convertLegacyReaction(reaction: string): string { | export function convertLegacyReaction(reaction: string): string { | ||||||
|  | 	reaction = decodeReaction(reaction).reaction; | ||||||
| 	if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; | 	if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; | ||||||
| 	return reaction; | 	return reaction; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								src/misc/secure-rndstr.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/misc/secure-rndstr.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import * as crypto from 'crypto'; | ||||||
|  |  | ||||||
|  | const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; | ||||||
|  | const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; | ||||||
|  |  | ||||||
|  | export function secureRndstr(length = 32, useLU = true): string { | ||||||
|  | 	const chars = useLU ? LU_CHARS : L_CHARS; | ||||||
|  | 	const chars_len = chars.length; | ||||||
|  |  | ||||||
|  | 	let str = ''; | ||||||
|  |  | ||||||
|  | 	for (let i = 0; i < length; i++) { | ||||||
|  | 		let rand = Math.floor((crypto.randomBytes(1).readUInt8(0) / 0xFF) * chars_len); | ||||||
|  | 		if (rand === chars_len) { | ||||||
|  | 			rand = chars_len - 1; | ||||||
|  | 		} | ||||||
|  | 		str += chars.charAt(rand); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return str; | ||||||
|  | } | ||||||
| @@ -348,4 +348,9 @@ export class Meta { | |||||||
| 		default: true, | 		default: true, | ||||||
| 	}) | 	}) | ||||||
| 	public objectStorageUseSSL: boolean; | 	public objectStorageUseSSL: boolean; | ||||||
|  |  | ||||||
|  | 	@Column('boolean', { | ||||||
|  | 		default: true, | ||||||
|  | 	}) | ||||||
|  | 	public objectStorageUseProxy: boolean; | ||||||
| } | } | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user