Merge branch 'develop'
This commit is contained in:
		
						commit
						d0d5068f72
					
				
					 178 changed files with 4547 additions and 3170 deletions
				
			
		
							
								
								
									
										27
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										27
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -2,12 +2,37 @@ | |||
| ## 12.x.x (unreleased) | ||||
| 
 | ||||
| ### Improvements | ||||
| - ページロードエラーページにリロードボタンを追加 | ||||
| 
 | ||||
| ### Bugfixes | ||||
| 
 | ||||
| --> | ||||
| 
 | ||||
| ## 12.93.0 (2021/10/23) | ||||
| 
 | ||||
| ### Improvements | ||||
| - クライアント: コントロールパネルのパフォーマンスを改善 | ||||
| - クライアント: 自分のリアクション一覧を見れるように | ||||
| 	- 設定により、リアクション一覧を全員に公開することも可能 | ||||
| - クライアント: ユーザー検索の精度を強化 | ||||
| - クライアント: 新しいライトテーマを追加 | ||||
| - クライアント: 新しいダークテーマを追加 | ||||
| - API: ユーザーのリアクション一覧を取得する users/reactions を追加 | ||||
| - API: users/search および users/search-by-username-and-host を強化 | ||||
| - ミュート及びブロックのインポートを行えるように | ||||
| - クライアント: /share のクエリでリプライやファイル等の情報を渡せるように | ||||
| - チャートのsyncを毎日0時に自動で行うように | ||||
| 
 | ||||
| ### Bugfixes | ||||
| - クライアント: テーマの管理が行えない問題を修正 | ||||
| - API: アプリケーション通知が取得できない問題を修正 | ||||
| - クライアント: リモートノートで意図せずローカルカスタム絵文字が使われてしまうことがあるのを修正 | ||||
| - ActivityPub: not reacted な Undo.Like がinboxに滞留するのを修正 | ||||
| 
 | ||||
| ### Changes | ||||
| - 連合の考慮に問題があることなどが分かったため、モデレーターをブロックできない仕様を廃止しました | ||||
| - データベースにログを保存しないようになりました | ||||
| 	- ログを永続化したい場合はsyslogを利用してください | ||||
| 
 | ||||
| ## 12.92.0 (2021/10/16) | ||||
| 
 | ||||
| ### Improvements | ||||
|  |  | |||
|  | @ -9,9 +9,9 @@ It will also allow the reader to use the translation tool of their preference if | |||
| ## Issues | ||||
| Before creating an issue, please check the following: | ||||
| - To avoid duplication, please search for similar issues before creating a new issue. | ||||
| - Do not use Issues as a question. | ||||
| 	- Issues should only be used to feature requests, suggestions, and report problems. | ||||
| 	- Please ask questions in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3). | ||||
| - Do not use Issues to ask questions or troubleshooting. | ||||
| 	- Issues should only be used to feature requests, suggestions, and bug tracking. | ||||
| 	- Please ask questions or troubleshooting in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3). | ||||
| 
 | ||||
| ## Before implementation | ||||
| When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented. | ||||
|  | @ -171,6 +171,9 @@ const users = userIds.length > 0 ? await Users.find({ | |||
| SQLでは配列のインデックスは**1始まり**。 | ||||
| `[a, b, c]`の `a`にアクセスしたいなら`[0]`ではなく`[1]`と書く | ||||
| 
 | ||||
| ### null IN | ||||
| nullが含まれる可能性のあるカラムにINするときは、そのままだとおかしくなるのでORなどでnullのハンドリングをしよう。 | ||||
| 
 | ||||
| ### `undefined`にご用心 | ||||
| MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。 | ||||
| MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください | ||||
|  |  | |||
|  | @ -65,7 +65,7 @@ Organize and store your files! Want to post a picture you have already uploaded? | |||
| 
 | ||||
| :package: Create your own instance | ||||
| ---------------------------------------------------------------- | ||||
| Please see the [Setup and Installation Guide](./docs/setup.en.md). | ||||
| Please see the [Setup and Installation Guide](https://misskey-hub.net/docs/install/install.html). | ||||
| 
 | ||||
| :wrench: Contribution | ||||
| ---------------------------------------------------------------- | ||||
|  |  | |||
|  | @ -1,33 +0,0 @@ | |||
| # Docs | ||||
| These docs are for contributors of Misskey or admins of instance of Misskey. | ||||
| Docs for users are located in `src/docs`. | ||||
| 
 | ||||
| これらのドキュメントはMisskeyの開発者またはMisskeyインスタンス運営者向けです。 | ||||
| 利用者向けのドキュメントは`src/docs`にあります。 | ||||
| 
 | ||||
| 这些文档是为 Misskey 的贡献者,或是 Misskey 实例的管理者准备的。 | ||||
| 为用户准备的文档放置在 `src/docs` 文件夹中。 | ||||
| 
 | ||||
| ## 日本語版 | ||||
| 
 | ||||
| - [Misskey構築の手引き](./setup.ja.md) | ||||
| - [運営ガイド](./manage.ja.md) | ||||
| - [Dockerを使ったMisskey構築方法](./docker.ja.md) | ||||
| 
 | ||||
| ## English Version | ||||
| 
 | ||||
| - [Misskey Setup and Installation Guide](./setup.en.md) | ||||
| - [Management guide](./manage.en.md) | ||||
| - [Docker Guide](./docker.en.md) | ||||
| 
 | ||||
| ## Française Version | ||||
| 
 | ||||
| - [Guide d'installation et de configuration de Misskey](./setup.fr.md) | ||||
| - [Guide d'administration](./manage.fr.md) | ||||
| - [Guide Docker](./docker.fr.md) | ||||
| 
 | ||||
| ## 简体中文版 | ||||
| 
 | ||||
| - [Misskey 设置和安装指南](./setup.zh.md) | ||||
| - [运营指南](./manage.zh.md) | ||||
| - [Docker 部署指南](./docker.zh.md) | ||||
|  | @ -1,97 +0,0 @@ | |||
| Docker Guide | ||||
| ================================================================ | ||||
| 
 | ||||
| This guide describes how to install and setup Misskey with Docker. | ||||
| 
 | ||||
| - [Japanese version also available - 日本語版もあります](./docker.ja.md) | ||||
| - [Simplified Chinese version also available - 简体中文版同样可用](./docker.zh.md) | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| *1.* Download Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. Clone Misskey repository's master branch. | ||||
| 
 | ||||
| 	`git clone -b master git://github.com/misskey-dev/misskey.git` | ||||
| 
 | ||||
| 2. Move to misskey directory. | ||||
| 
 | ||||
| 	`cd misskey` | ||||
| 
 | ||||
| 3. Checkout to the [latest release](https://github.com/misskey-dev/misskey/releases/latest) tag. | ||||
| 
 | ||||
| 	`git checkout master` | ||||
| 
 | ||||
| *2.* Configure Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| Create configuration files with following: | ||||
| 
 | ||||
| ```bash | ||||
| cd .config | ||||
| cp example.yml default.yml | ||||
| cp docker_example.env docker.env | ||||
| ``` | ||||
| 
 | ||||
| ### `default.yml` | ||||
| 
 | ||||
| Edit this file the same as non-Docker environment.   | ||||
| However hostname of Postgresql, Redis and Elasticsearch are not `localhost`, they are set in `docker-compose.yml`.   | ||||
| The following is default hostname: | ||||
| 
 | ||||
| | Service       | Hostname | | ||||
| |---------------|----------| | ||||
| | Postgresql    | `db`     | | ||||
| | Redis         | `redis`  | | ||||
| | Elasticsearch | `es`     | | ||||
| 
 | ||||
| ### `docker.env` | ||||
| 
 | ||||
| Configure Postgresql in this file.   | ||||
| The minimum required settings are: | ||||
| 
 | ||||
| | name                | Description   | | ||||
| |---------------------|---------------| | ||||
| | `POSTGRES_PASSWORD` | Password      | | ||||
| | `POSTGRES_USER`     | Username      | | ||||
| | `POSTGRES_DB`       | Database name | | ||||
| 
 | ||||
| *3.* Configure Docker | ||||
| ---------------------------------------------------------------- | ||||
| Edit `docker-compose.yml`. | ||||
| 
 | ||||
| *4.* Build Misskey | ||||
| ---------------------------------------------------------------- | ||||
| Build misskey with the following: | ||||
| 
 | ||||
| `docker-compose build` | ||||
| 
 | ||||
| *5.* Init DB | ||||
| ---------------------------------------------------------------- | ||||
| ``` shell | ||||
| docker-compose run --rm web yarn run init | ||||
| ``` | ||||
| 
 | ||||
| *6.* That is it. | ||||
| ---------------------------------------------------------------- | ||||
| Well done! Now you have an environment to run Misskey. | ||||
| 
 | ||||
| ### Launch normally | ||||
| Just `docker-compose up -d`. GLHF! | ||||
| 
 | ||||
| ### How to update your Misskey server to the latest version | ||||
| 1. `git stash` | ||||
| 2. `git checkout master` | ||||
| 3. `git pull` | ||||
| 4. `git submodule update --init` | ||||
| 5. `git stash pop` | ||||
| 6. `docker-compose build` | ||||
| 7. Check [ChangeLog](../CHANGELOG.md) for migration information | ||||
| 8. `docker-compose stop && docker-compose up -d` | ||||
| 
 | ||||
| ### How to execute [cli commands](manage.en.md): | ||||
| `docker-compose run --rm web node built/tools/mark-admin @example` | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| If you have any questions or trouble, feel free to contact us! | ||||
|  | @ -1,91 +0,0 @@ | |||
| Guide Docker | ||||
| ================================================================ | ||||
| 
 | ||||
| Ce guide explique comment installer et configurer Misskey avec Docker. | ||||
| 
 | ||||
| - [Version japonaise également disponible - Japanese version also available - 日本語版もあります](./docker.ja.md)   | ||||
| - [Version anglaise également disponible - English version also available - 英語版もあります](./docker.en.md) | ||||
| - [Version Chinois simplifié également disponible - Simplified Chinese version also available - 简体中文版同样可用](./docker.zh.md) | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| *1.* Télécharger Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. Clone le dépôt de Misskey sur la branche master. | ||||
| 
 | ||||
| 	`git clone -b master git://github.com/misskey-dev/misskey.git` | ||||
| 
 | ||||
| 2. Naviguez dans le dossier du dépôt. | ||||
| 
 | ||||
| 	`cd misskey` | ||||
| 
 | ||||
| 3. Checkout sur le tag de la [dernière version](https://github.com/misskey-dev/misskey/releases/latest). | ||||
| 
 | ||||
| 	`git checkout master` | ||||
| 
 | ||||
| *2.* Configuration de Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. `cp .config/example.yml .config/default.yml` Copiez le fichier `.config/example.yml` et renommez-le `default.yml`. | ||||
| 2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` Copie le fichier `.config/mongo_initdb_example.js` et le renomme en `mongo_initdb.js`. | ||||
| 3. Editez `default.yml` et `mongo_initdb.js`. | ||||
| 
 | ||||
| *3.* Configurer Docker | ||||
| ---------------------------------------------------------------- | ||||
| Editez `docker-compose.yml`. | ||||
| 
 | ||||
| *4.* Contruire Misskey | ||||
| ---------------------------------------------------------------- | ||||
| Contruire l'image Docker avec: | ||||
| 
 | ||||
| `docker-compose build` | ||||
| 
 | ||||
| *5.* C'est tout ! | ||||
| ---------------------------------------------------------------- | ||||
| Parfait, Vous avez un environnement prêt pour démarrer Misskey. | ||||
| 
 | ||||
| ### Lancer normalement | ||||
| Utilisez la commande `docker-compose up -d`. GLHF! | ||||
| 
 | ||||
| ### How to update your Misskey server to the latest version | ||||
| 1. `git stash` | ||||
| 2. `git checkout master` | ||||
| 3. `git pull` | ||||
| 4. `git submodule update --init` | ||||
| 5. `git stash pop` | ||||
| 6. `docker-compose build` | ||||
| 7. Consultez le [ChangeLog](../CHANGELOG.md) pour avoir les éventuelles informations de migration | ||||
| 8. `docker-compose stop && docker-compose up -d` | ||||
| 
 | ||||
| ### Comment exécuter des [commandes](manage.fr.md) | ||||
| `docker-compose run --rm web node built/tools/mark-admin @example` | ||||
| 
 | ||||
| ### Configuration d'ElasticSearch (pour la fonction de recherche) | ||||
| *1.* Préparation de l'environnement | ||||
| ---------------------------------------------------------------- | ||||
| 1. Permet de créer le dossier d'accueil de la base ElasticSearch aves les bons droits | ||||
| 
 | ||||
| 	`mkdir elasticsearch && chown 1000:1000 elasticsearch` | ||||
| 
 | ||||
| 2. Augmente la valeur max du paramètre map_count du système (valeur minimum pour pouvoir lancer ES) | ||||
| 
 | ||||
| 	`sysctl -w vm.max_map_count=262144` | ||||
| 
 | ||||
| *2.* Après lancement du docker-compose, initialisation de la base ElasticSearch | ||||
| ---------------------------------------------------------------- | ||||
| 1. Connexion dans le conteneur web | ||||
| 
 | ||||
| 	`docker-compose -it web /bin/sh` | ||||
| 
 | ||||
| 2. Ajout du paquet curl | ||||
| 
 | ||||
| 	`apk add curl` | ||||
| 
 | ||||
| 3. Création de la base ES | ||||
| 
 | ||||
| 	`curl -X PUT "es:9200/misskey" -H 'Content-Type: application/json' -d'{ "settings" : { "index" : { } }}'` | ||||
| 
 | ||||
| 4. `exit` | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| Si vous avez des questions ou des problèmes, n'hésitez pas à nous contacter ! | ||||
|  | @ -1,98 +0,0 @@ | |||
| Dockerを使ったMisskey構築方法 | ||||
| ================================================================ | ||||
| 
 | ||||
| このガイドはDockerを使ったMisskeyセットアップ方法について解説します。 | ||||
| 
 | ||||
| - [英語版もあります - English version also available](./docker.en.md) | ||||
| - [简体中文版同样可用 - Simplified Chinese version also available](./docker.zh.md) | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| *1.* Misskeyのダウンロード | ||||
| ---------------------------------------------------------------- | ||||
| 1. masterブランチからMisskeyレポジトリをクローン | ||||
| 
 | ||||
| 	`git clone -b master git://github.com/misskey-dev/misskey.git` | ||||
| 
 | ||||
| 2. misskeyディレクトリに移動 | ||||
| 
 | ||||
| 	`cd misskey` | ||||
| 
 | ||||
| 3. [最新のリリース](https://github.com/misskey-dev/misskey/releases/latest)を確認 | ||||
| 
 | ||||
| 	`git checkout master` | ||||
| 
 | ||||
| *2.* 設定ファイルの作成と編集 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| 下記コマンドで設定ファイルを作成してください。 | ||||
| 
 | ||||
| ```bash | ||||
| cd .config | ||||
| cp example.yml default.yml | ||||
| cp docker_example.env docker.env | ||||
| ``` | ||||
| 
 | ||||
| ### `default.yml`の編集 | ||||
| 
 | ||||
| 非Docker環境と同じ様に編集してください。   | ||||
| ただし、Postgresql、RedisとElasticsearchのホストは`localhost`ではなく、`docker-compose.yml`で設定されたサービス名になっています。   | ||||
| 標準設定では次の通りです。 | ||||
| 
 | ||||
| | サービス       | ホスト名 | | ||||
| |---------------|---------| | ||||
| | Postgresql    |`db`     | | ||||
| | Redis         |`redis`  | | ||||
| | Elasticsearch |`es`     | | ||||
| 
 | ||||
| ### `docker.env`の編集 | ||||
| 
 | ||||
| このファイルはPostgresqlの設定を記述します。   | ||||
| 最低限記述する必要がある設定は次の通りです。 | ||||
| 
 | ||||
| | 設定                 | 内容         | | ||||
| |---------------------|--------------| | ||||
| | `POSTGRES_PASSWORD` | パスワード    | | ||||
| | `POSTGRES_USER`     | ユーザー名    | | ||||
| | `POSTGRES_DB`       | データベース名 | | ||||
| 
 | ||||
| *3.* Dockerの設定 | ||||
| ---------------------------------------------------------------- | ||||
| `docker-compose.yml`を編集してください。 | ||||
| 
 | ||||
| *4.* Misskeyのビルド | ||||
| ---------------------------------------------------------------- | ||||
| 次のコマンドでMisskeyをビルドしてください: | ||||
| 
 | ||||
| `docker-compose build` | ||||
| 
 | ||||
| *5.* データベースを初期化 | ||||
| ---------------------------------------------------------------- | ||||
| ``` shell | ||||
| docker-compose run --rm web yarn run init | ||||
| ``` | ||||
| 
 | ||||
| *6.* 以上です! | ||||
| ---------------------------------------------------------------- | ||||
| お疲れ様でした。これでMisskeyを動かす準備は整いました。 | ||||
| 
 | ||||
| ### 通常起動 | ||||
| `docker-compose up -d`するだけです。GLHF! | ||||
| 
 | ||||
| ### Misskeyを最新バージョンにアップデートする方法: | ||||
| 1. `git stash` | ||||
| 2. `git checkout master` | ||||
| 3. `git pull` | ||||
| 4. `git submodule update --init` | ||||
| 5. `git stash pop` | ||||
| 6. `docker-compose build` | ||||
| 7. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する | ||||
| 8. `docker-compose stop && docker-compose up -d` | ||||
| 
 | ||||
| ### cliコマンドを実行する方法: | ||||
| 
 | ||||
| `docker-compose run --rm web node built/tools/mark-admin @example` | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| なにかお困りのことがありましたらお気軽にご連絡ください。 | ||||
|  | @ -1,97 +0,0 @@ | |||
| Docker 部署指南 | ||||
| ================================================================ | ||||
| 
 | ||||
| 这份指南描述了如何使用Docker安装并设置 Misskey 。 | ||||
| 
 | ||||
| - [日本語版もあります - Japanese version also available](./docker.ja.md) | ||||
| - [英語版もあります - English version also available](./docker.en.md) | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| *1.* 下载 Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. 克隆 Misskey 项目的 master 分支。 | ||||
| 
 | ||||
| 	`git clone -b master git://github.com/misskey-dev/misskey.git` | ||||
| 
 | ||||
| 2. 进入 misskey 文件夹。 | ||||
| 
 | ||||
| 	`cd misskey` | ||||
| 
 | ||||
| 3. 检查 [最新发布版](https://github.com/misskey-dev/misskey/releases/latest) 标签。 | ||||
| 
 | ||||
| 	`git checkout master` | ||||
| 
 | ||||
| *2.* 配置 Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| 可以按照如下方式创建配置文件: | ||||
| 
 | ||||
| ``` bash | ||||
| cd .config | ||||
| cp example.yml default.yml | ||||
| cp docker_example.env docker.env | ||||
| ``` | ||||
| 
 | ||||
| ### `default.yml` | ||||
| 
 | ||||
| 这个文件的编辑工作基本与非 Docker 环境的版本相同。 | ||||
| 但请注意, Postgresql、 Redis 和 Elasticsearch 的 **主机名(hostname)** 配置不应该是 `localhost` ,它们被设置在 `docker-compose.yml` 文件中。 | ||||
| 以下是默认的主机名: | ||||
| 
 | ||||
| | 服务          | 主机名   | | ||||
| |---------------|----------| | ||||
| | Postgresql    | `db`     | | ||||
| | Redis         | `redis`  | | ||||
| | Elasticsearch | `es`     | | ||||
| 
 | ||||
| ### `docker.env` | ||||
| 
 | ||||
| 在这个文件中配置 Postgresql 。 | ||||
| 至少需要如下这些配置: | ||||
| 
 | ||||
| | 名称                |  描述         | | ||||
| |---------------------|---------------| | ||||
| | `POSTGRES_PASSWORD` |  数据库密码   | | ||||
| | `POSTGRES_USER`     |  数据库用户名 | | ||||
| | `POSTGRES_DB`       |  数据库名     | | ||||
| 
 | ||||
| *3.* 配置 Docker | ||||
| ---------------------------------------------------------------- | ||||
| 编辑 `docker-compose.yml` 文件。 | ||||
| 
 | ||||
| *4.* 构建 Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 使用如下的方式构建Misskey: | ||||
| 
 | ||||
| `docker-compose build` | ||||
| 
 | ||||
| *5.* 初始化数据库 | ||||
| ---------------------------------------------------------------- | ||||
| ``` bash | ||||
| docker-compose run --rm web yarn run init | ||||
| ``` | ||||
| 
 | ||||
| *6.* 完成了! | ||||
| ---------------------------------------------------------------- | ||||
| 干得不错!现在您拥有了一个可以运行Misskey的环境啦。 | ||||
| 
 | ||||
| ### 正常启动 | ||||
| 只需要 `docker-compose up -d` 即可。玩得愉快! | ||||
| 
 | ||||
| ### 如何将您的 Misskey 服务器升级至最新版本 | ||||
| 1. `git stash` | ||||
| 2. `git checkout master` | ||||
| 3. `git pull` | ||||
| 4. `git submodule update --init` | ||||
| 5. `git stash pop` | ||||
| 6. `docker-compose build` | ||||
| 7. 检查 [更新日志](../CHANGELOG.md) 以获取升级迁移信息。 | ||||
| 8. `docker-compose stop && docker-compose up -d` | ||||
| 
 | ||||
| ### 如何执行 [控制台指令](manage.zh.md): | ||||
| `docker-compose run --rm web node built/tools/mark-admin @example` | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| 如果您有任何疑问或是困惑,欢迎与我们联系! | ||||
|  | @ -1,71 +0,0 @@ | |||
| # Sample nginx configuration for Misskey | ||||
| # | ||||
| # 1. Replace example.tld to your domain | ||||
| # 2. Copy to /etc/nginx/sites-available/ and then symlink from /etc/nginx/sites-enabled/ | ||||
| #    or copy to /etc/nginx/conf.d/ | ||||
| 
 | ||||
| # For WebSocket | ||||
| map $http_upgrade $connection_upgrade { | ||||
|     default upgrade; | ||||
|     ''      close; | ||||
| } | ||||
| 
 | ||||
| proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off; | ||||
| 
 | ||||
| server { | ||||
|     listen 80; | ||||
|     listen [::]:80; | ||||
|     server_name example.tld; | ||||
| 
 | ||||
|     # For SSL domain validation | ||||
|     root /var/www/html; | ||||
|     location /.well-known/acme-challenge/ { allow all; } | ||||
|     location /.well-known/pki-validation/ { allow all; } | ||||
|     location / { return 301 https://$server_name$request_uri; } | ||||
| } | ||||
| 
 | ||||
| server { | ||||
|     listen 443 ssl http2; | ||||
|     listen [::]:443 ssl http2; | ||||
|     server_name example.tld; | ||||
|     ssl_session_cache shared:ssl_session_cache:10m; | ||||
| 
 | ||||
|     # To use Let's Encrypt certificate | ||||
|     ssl_certificate     /etc/letsencrypt/live/example.tld/fullchain.pem; | ||||
|     ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem; | ||||
| 
 | ||||
|     # To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate) | ||||
|     #ssl_certificate     /etc/ssl/certs/ssl-cert-snakeoil.pem; | ||||
|     #ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; | ||||
| 
 | ||||
|     # SSL protocol settings | ||||
|     ssl_protocols TLSv1.2; | ||||
|     ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES128-SHA; | ||||
|     ssl_prefer_server_ciphers on; | ||||
| 
 | ||||
|     # Change to your upload limit | ||||
|     client_max_body_size 80m; | ||||
| 
 | ||||
|     # Proxy to Node | ||||
|     location / { | ||||
|         proxy_pass http://127.0.0.1:3000; | ||||
|         proxy_set_header Host $host; | ||||
|         proxy_http_version 1.1; | ||||
|         proxy_redirect off; | ||||
| 
 | ||||
|         # If it's behind another reverse proxy or CDN, remove the following. | ||||
|         proxy_set_header X-Real-IP $remote_addr; | ||||
|         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||||
|         proxy_set_header X-Forwarded-Proto https; | ||||
| 
 | ||||
|         # For WebSocket | ||||
|         proxy_set_header Upgrade $http_upgrade; | ||||
|         proxy_set_header Connection $connection_upgrade; | ||||
| 
 | ||||
|         # Cache settings | ||||
|         proxy_cache cache1; | ||||
|         proxy_cache_lock on; | ||||
|         proxy_cache_use_stale updating; | ||||
|         add_header X-Cache $upstream_cache_status; | ||||
|     } | ||||
| } | ||||
|  | @ -1,14 +0,0 @@ | |||
| # Management guide | ||||
| 
 | ||||
| ## Check the status of the job queue | ||||
| coming soon | ||||
| 
 | ||||
| ## Mark as 'admin' user | ||||
| ``` shell | ||||
| node built/tools/mark-admin (Username) | ||||
| ``` | ||||
| 
 | ||||
| e.g. | ||||
| ``` shell | ||||
| node built/tools/mark-admin @syuilo | ||||
| ``` | ||||
|  | @ -1,14 +0,0 @@ | |||
| # Guide d'administration | ||||
| 
 | ||||
| ## Vérifier le status de la file d'attente des taches | ||||
| coming soon | ||||
| 
 | ||||
| ## Marquer un utilisateur en tant que 'admin' | ||||
| ``` shell | ||||
| node built/tools/mark-admin (nom d'utilisateur) | ||||
| ``` | ||||
| 
 | ||||
| Exemple : | ||||
| ``` shell | ||||
| node built/tools/mark-admin @syuilo | ||||
| ``` | ||||
|  | @ -1,14 +0,0 @@ | |||
| # 運営ガイド | ||||
| 
 | ||||
| ## ジョブキューの状態を調べる | ||||
| coming soon | ||||
| 
 | ||||
| ## 管理者ユーザーを設定する | ||||
| ``` shell | ||||
| node built/tools/mark-admin (ユーザー名) | ||||
| ``` | ||||
| 
 | ||||
| 例: | ||||
| ``` shell | ||||
| node built/tools/mark-admin @syuilo | ||||
| ``` | ||||
|  | @ -1,14 +0,0 @@ | |||
| # 运营指南 | ||||
| 
 | ||||
| ## 检查任务队列的状态 | ||||
| 即将到来…… | ||||
| 
 | ||||
| ## 设置用户为管理员 | ||||
| ``` shell | ||||
| node built/tools/mark-admin (用户名) | ||||
| ``` | ||||
| 
 | ||||
| 样例 | ||||
| ``` shell | ||||
| node built/tools/mark-admin @syuilo | ||||
| ``` | ||||
|  | @ -1,28 +0,0 @@ | |||
| GitHub Actionsを使用してDocker Hubへpushする方法 | ||||
| ================================================================ | ||||
| 
 | ||||
| [/.github/workflows/docker.yml](/.github/workflows/docker.yml) に   | ||||
| GitHub ActionによりDocker Hubへpushするワークフローが記述されています。 | ||||
| 
 | ||||
| オリジナルリポジトリでは、リリースされたタイミングで `latest`, `<リリース名>` それぞれのタグでDocker Hubにpushされます。   | ||||
| ※ Docker Hub に`<ブランチ名>`のようなタグがあるかもしれませんが、こちらは自動push対象ではありません。 | ||||
| 
 | ||||
| Fork先でこのワークフローを実行すると失敗します。 | ||||
| 
 | ||||
| 以下では、Fork先で自分のDocker Hubリポジトリにpushするようにする方法を記述します。 | ||||
| 
 | ||||
| ## 自分のDocker Hubリポジトリにpushするように設定する方法 | ||||
| 
 | ||||
| 1. Docker Hubでリポジトリを作成します。 | ||||
| 2. ワークフローファイルの [images](https://github.com/misskey-dev/misskey/blob/53f3b779bf16abcda4f6e026c51384f3b8fbcc62/.github/workflows/docker.yml#L20) を作成したリポジトリに置き換えます。 | ||||
| 3. GitHubにて [暗号化されたシークレット](https://docs.github.com/ja/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository) を作成します。   | ||||
|    作成が必要なのは `DOCKER_USERNAME` と `DOCKER_PASSWORD` で、それぞれDocker Hubのユーザーとパスワードになります。 | ||||
| 
 | ||||
| ## pushする方法 | ||||
| 
 | ||||
| 上記設定によりリリース時に自動的にDocker Hubにpushされるようになります。   | ||||
| 具体的には、GitHubのリリース機能でリリースしたタイミングで `latest`, `<リリース名>` それぞれのタグでDocker Hubにpushされます。 | ||||
| 
 | ||||
| また、GitHub上から手動でpushすることも出来ます。   | ||||
| それを行うには、Actions => Publish Docker image => Run workflow からbranchを選択してワークフローを実行します。   | ||||
| ただし、この場合作成されるタグは`<ブランチ名>`になります。 | ||||
							
								
								
									
										147
									
								
								docs/setup.en.md
									
										
									
									
									
								
							
							
						
						
									
										147
									
								
								docs/setup.en.md
									
										
									
									
									
								
							|  | @ -1,147 +0,0 @@ | |||
| Misskey Setup and Installation Guide | ||||
| ================================================================ | ||||
| 
 | ||||
| We thank you for your interest in setting up your Misskey server! | ||||
| This guide describes how to install and setup Misskey. | ||||
| 
 | ||||
| - [Japanese version also available - 日本語版もあります](./setup.ja.md) | ||||
| - [Simplified Chinese version also available - 简体中文版同样可用](./setup.zh.md) | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| *1.* Create Misskey user | ||||
| ---------------------------------------------------------------- | ||||
| Running misskey as root is not a good idea so we create a user for that. | ||||
| In debian for exemple : | ||||
| 
 | ||||
| ``` | ||||
| adduser --disabled-password --disabled-login misskey | ||||
| ``` | ||||
| 
 | ||||
| *2.* Install dependencies | ||||
| ---------------------------------------------------------------- | ||||
| Please install and setup these softwares: | ||||
| 
 | ||||
| #### Dependencies :package: | ||||
| * **[Node.js](https://nodejs.org/en/)** (12.x, 14.x) | ||||
| * **[PostgreSQL](https://www.postgresql.org/)** (12.x / 13.x is preferred) | ||||
| * **[Redis](https://redis.io/)** | ||||
| 
 | ||||
| ##### Optional | ||||
| * [Yarn](https://yarnpkg.com/) *Optional but recommended for security reason. If you won't install it, use `npx yarn` instead of `yarn`.* | ||||
| * [Elasticsearch](https://www.elastic.co/) - required to enable the search feature | ||||
| * [FFmpeg](https://www.ffmpeg.org/) | ||||
| 
 | ||||
| *3.* Install Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. Connect to misskey user. | ||||
| 
 | ||||
| 	`su - misskey` | ||||
| 
 | ||||
| 2. Clone the misskey repo from master branch. | ||||
| 
 | ||||
| 	`git clone -b master git://github.com/misskey-dev/misskey.git` | ||||
| 
 | ||||
| 3. Navigate to misskey directory | ||||
| 
 | ||||
| 	`cd misskey` | ||||
| 
 | ||||
| 4. Checkout to the [latest release](https://github.com/misskey-dev/misskey/releases/latest) | ||||
| 
 | ||||
| 	`git checkout master` | ||||
| 
 | ||||
| 5. Install misskey dependencies. | ||||
| 
 | ||||
| 	`yarn` | ||||
| 
 | ||||
| *4.* Configure Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. Copy the `.config/example.yml` and rename it to `default.yml`. | ||||
| 
 | ||||
| 	`cp .config/example.yml .config/default.yml` | ||||
| 
 | ||||
| 2. Edit `default.yml` | ||||
| 
 | ||||
| *5.* Build Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| Build misskey with the following: | ||||
| 
 | ||||
| `NODE_ENV=production yarn build` | ||||
| 
 | ||||
| If you're on Debian, you will need to install the `build-essential`, `python` package. | ||||
| 
 | ||||
| If you're still encountering errors about some modules, use node-gyp: | ||||
| 
 | ||||
| 1. `npx node-gyp configure` | ||||
| 2. `npx node-gyp build` | ||||
| 3. `NODE_ENV=production yarn build` | ||||
| 
 | ||||
| *6.* Init DB | ||||
| ---------------------------------------------------------------- | ||||
| ``` shell | ||||
| yarn run init | ||||
| ``` | ||||
| 
 | ||||
| *7.* That is it. | ||||
| ---------------------------------------------------------------- | ||||
| Well done! Now, you have an environment that run to Misskey. | ||||
| 
 | ||||
| ### Launch normally | ||||
| Just `NODE_ENV=production npm start`. GLHF! | ||||
| 
 | ||||
| ### Launch with systemd | ||||
| 
 | ||||
| 1. Create a systemd service here | ||||
| 
 | ||||
| 	`/etc/systemd/system/misskey.service` | ||||
| 
 | ||||
| 2. Edit it, and paste this and save: | ||||
| 
 | ||||
| 	``` | ||||
| 	[Unit] | ||||
| 	Description=Misskey daemon | ||||
| 
 | ||||
| 	[Service] | ||||
| 	Type=simple | ||||
| 	User=misskey | ||||
| 	ExecStart=/usr/bin/npm start | ||||
| 	WorkingDirectory=/home/misskey/misskey | ||||
| 	Environment="NODE_ENV=production" | ||||
| 	TimeoutSec=60 | ||||
| 	StandardOutput=syslog | ||||
| 	StandardError=syslog | ||||
| 	SyslogIdentifier=misskey | ||||
| 	Restart=always | ||||
| 
 | ||||
| 	[Install] | ||||
| 	WantedBy=multi-user.target | ||||
| 	``` | ||||
| 
 | ||||
| 3. Reload systemd and enable the misskey service. | ||||
| 
 | ||||
| 	`systemctl daemon-reload ; systemctl enable misskey` | ||||
| 
 | ||||
| 4. Start the misskey service. | ||||
| 
 | ||||
| 	`systemctl start misskey` | ||||
| 
 | ||||
| You can check if the service is running with `systemctl status misskey`. | ||||
| 
 | ||||
| ### How to update your Misskey server to the latest version | ||||
| 1. `git checkout master` | ||||
| 2. `git pull` | ||||
| 3. `git submodule update --init` | ||||
| 4. `yarn install` | ||||
| 5. `NODE_ENV=production yarn build` | ||||
| 6. `yarn migrate` | ||||
| 7. Restart your Misskey process to apply changes | ||||
| 8. Enjoy | ||||
| 
 | ||||
| If you encounter any problems with updating, please try the following: | ||||
| 1. `yarn clean` or `yarn cleanall` | ||||
| 2. Retry update (Don't forget `yarn install` | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| If you have any questions or troubles, feel free to contact us! | ||||
							
								
								
									
										136
									
								
								docs/setup.fr.md
									
										
									
									
									
								
							
							
						
						
									
										136
									
								
								docs/setup.fr.md
									
										
									
									
									
								
							|  | @ -1,136 +0,0 @@ | |||
| Guide d'installation et de configuration de Misskey | ||||
| ================================================================ | ||||
| 
 | ||||
| Nous vous remerçions de l'intrêt que vous manifestez pour l'installation de votre propre instance Misskey ! | ||||
| Ce guide décrit les étapes à suivre afin d'installer et de configurer une instance Misskey. | ||||
| 
 | ||||
| - [La version en japonnais est également disponible sur - 日本語版もあります](./setup.ja.md) | ||||
| - [Version anglaise également disponible - English version also available - 英語版もあります](./setup.en.md) | ||||
| - [Version Chinois simplifié également disponible - Simplified Chinese version also available - 简体中文版同样可用](./setup.zh.md) | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| *1.* Création de l'utilisateur Misskey | ||||
| ---------------------------------------------------------------- | ||||
| Executer misskey en tant que super-utilisateur étant une mauvaise idée, nous allons créer un utilisateur dédié. | ||||
| Sous Debian, par exemple : | ||||
| 
 | ||||
| ``` | ||||
| adduser --disabled-password --disabled-login misskey | ||||
| ``` | ||||
| 
 | ||||
| *2.* Installation des dépendances | ||||
| ---------------------------------------------------------------- | ||||
| Installez les paquets suivants : | ||||
| 
 | ||||
| #### Dépendences :package: | ||||
| * **[Node.js](https://nodejs.org/en/)** (12.x, 14.x) | ||||
| * **[PostgreSQL](https://www.postgresql.org/)** (>= 10) | ||||
| * **[Redis](https://redis.io/)** | ||||
| 
 | ||||
| ##### Optionnels | ||||
| * [Yarn](https://yarnpkg.com/) - *recommander pour des raisons de sécurité. Si vous ne l'installez pas, utilisez `npx yarn` au lieu de` yarn`.* | ||||
| * [Elasticsearch](https://www.elastic.co/) - *requis pour pouvoir activer la fonctionnalité de recherche.* | ||||
| * [FFmpeg](https://www.ffmpeg.org/) | ||||
| 
 | ||||
| *3.* Installation de Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. Basculez vers l'utilisateur misskey. | ||||
| 
 | ||||
| 	`su - misskey` | ||||
| 
 | ||||
| 2. Clonez la branche master du dépôt misskey. | ||||
| 
 | ||||
| 	`git clone -b master git://github.com/misskey-dev/misskey.git` | ||||
| 
 | ||||
| 3. Accédez au dossier misskey. | ||||
| 
 | ||||
| 	`cd misskey` | ||||
| 
 | ||||
| 4. Checkout sur le tag de la [version la plus récente](https://github.com/misskey-dev/misskey/releases/latest) | ||||
| 
 | ||||
| 	`git checkout master` | ||||
|   | ||||
| 5. Installez les dépendances de misskey. | ||||
| 
 | ||||
| 	`yarn install` | ||||
| 
 | ||||
| *4.* Création du fichier de configuration | ||||
| ---------------------------------------------------------------- | ||||
| 1. Copiez le fichier `.config/example.yml` et renommez-le`default.yml`. | ||||
| 
 | ||||
| 	`cp .config/example.yml .config/default.yml` | ||||
| 
 | ||||
| 2. Editez le fichier `default.yml` | ||||
| 
 | ||||
| *5.* Construction de Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| Construisez Misskey comme ceci : | ||||
| 
 | ||||
| `NODE_ENV=production yarn build` | ||||
| 
 | ||||
| Si vous êtes sous Debian, vous serez amené à installer les paquets `build-essential` et `python`. | ||||
| 
 | ||||
| Si vous rencontrez des erreurs concernant certains modules, utilisez node-gyp: | ||||
| 
 | ||||
| 1. `npx node-gyp configure` | ||||
| 2. `npx node-gyp build` | ||||
| 3. `NODE_ENV=production yarn build` | ||||
| 
 | ||||
| *6.* C'est tout. | ||||
| ---------------------------------------------------------------- | ||||
| Excellent ! Maintenant, vous avez un environnement prêt pour lancer Misskey | ||||
| 
 | ||||
| ### Lancement conventionnel | ||||
| Lancez tout simplement `NODE_ENV=production yarn start`. Bonne chance et amusez-vous bien ! | ||||
| 
 | ||||
| ### Démarrage avec systemd | ||||
| 
 | ||||
| 1. Créez un service systemd sur | ||||
| 
 | ||||
| 	`/etc/systemd/system/misskey.service` | ||||
| 
 | ||||
| 2. Editez-le puis copiez et coller ceci dans le fichier : | ||||
| 
 | ||||
| 	``` | ||||
| 	[Unit] | ||||
| 	Description=Misskey daemon | ||||
| 
 | ||||
| 	[Service] | ||||
| 	Type=simple | ||||
| 	User=misskey | ||||
| 	ExecStart=/usr/bin/npm start | ||||
| 	WorkingDirectory=/home/misskey/misskey | ||||
| 	Environment="NODE_ENV=production" | ||||
| 	TimeoutSec=60 | ||||
| 	StandardOutput=syslog | ||||
| 	StandardError=syslog | ||||
| 	SyslogIdentifier=misskey | ||||
| 	Restart=always | ||||
| 
 | ||||
| 	[Install] | ||||
| 	WantedBy=multi-user.target | ||||
| 	``` | ||||
| 
 | ||||
| 3. Redémarre systemd et active le service misskey. | ||||
| 
 | ||||
| 	`systemctl daemon-reload ; systemctl enable misskey` | ||||
| 
 | ||||
| 4. Démarre le service misskey. | ||||
| 
 | ||||
| 	`systemctl start misskey` | ||||
| 
 | ||||
| Vous pouvez vérifier si le service a démarré en utilisant la commande `systemctl status misskey`. | ||||
| 
 | ||||
| ### Méthode de mise à jour vers la plus récente version de Misskey | ||||
| 1. `git checkout master` | ||||
| 2. `git pull` | ||||
| 3. `git submodule update --init` | ||||
| 4. `yarn install` | ||||
| 5. `NODE_ENV=production yarn build` | ||||
| 6. `yarn migrate` | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| Si vous rencontrez des difficultés ou avez d'autres questions, n'hésitez pas à nous contacter ! | ||||
							
								
								
									
										145
									
								
								docs/setup.ja.md
									
										
									
									
									
								
							
							
						
						
									
										145
									
								
								docs/setup.ja.md
									
										
									
									
									
								
							|  | @ -1,145 +0,0 @@ | |||
| Misskey構築の手引き | ||||
| ================================================================ | ||||
| 
 | ||||
| Misskeyサーバーの構築にご関心をお寄せいただきありがとうございます! | ||||
| このガイドではMisskeyのインストール・セットアップ方法について解説します。 | ||||
| 
 | ||||
| - [英語版もあります - English version also available](./setup.en.md) | ||||
| - [简体中文版同样可用 - Simplified Chinese version also available](./setup.zh.md) | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| *1.* Misskeyユーザーの作成 | ||||
| ---------------------------------------------------------------- | ||||
| Misskeyはrootユーザーで実行しない方がよいため、代わりにユーザーを作成します。 | ||||
| Debianの例: | ||||
| 
 | ||||
| ``` | ||||
| adduser --disabled-password --disabled-login misskey | ||||
| ``` | ||||
| 
 | ||||
| *2.* 依存関係をインストールする | ||||
| ---------------------------------------------------------------- | ||||
| これらのソフトウェアをインストール・設定してください: | ||||
| 
 | ||||
| #### 依存関係 :package: | ||||
| * **[Node.js](https://nodejs.org/en/)** (12.x, 14.x) | ||||
| * **[PostgreSQL](https://www.postgresql.org/)** (10以上) | ||||
| * **[Redis](https://redis.io/)** | ||||
| 
 | ||||
| ##### オプション | ||||
| * [Yarn](https://yarnpkg.com/) | ||||
| 	* セキュリティの観点から推奨されます。 yarn をインストールしない方針の場合は、文章中の `yarn` を適宜 `npx yarn` と読み替えてください。 | ||||
| * [Elasticsearch](https://www.elastic.co/) | ||||
| 	* 検索機能を有効にするためにはインストールが必要です。 | ||||
| * [FFmpeg](https://www.ffmpeg.org/) | ||||
| 
 | ||||
| *3.* Misskeyのインストール | ||||
| ---------------------------------------------------------------- | ||||
| 1. misskeyユーザーを使用 | ||||
| 
 | ||||
| 	`su - misskey` | ||||
| 
 | ||||
| 2. masterブランチからMisskeyレポジトリをクローン | ||||
| 
 | ||||
| 	`git clone -b master git://github.com/misskey-dev/misskey.git` | ||||
| 
 | ||||
| 3. misskeyディレクトリに移動 | ||||
| 
 | ||||
| 	`cd misskey` | ||||
| 
 | ||||
| 4. [最新のリリース](https://github.com/misskey-dev/misskey/releases/latest)を確認 | ||||
| 
 | ||||
| 	`git checkout master` | ||||
| 
 | ||||
| 5. Misskeyの依存パッケージをインストール | ||||
| 
 | ||||
| 	`yarn install` | ||||
| 
 | ||||
| *4.* 設定ファイルを作成する | ||||
| ---------------------------------------------------------------- | ||||
| 1. `.config/example.yml`をコピーし名前を`default.yml`にする。 | ||||
| 
 | ||||
| 	`cp .config/example.yml .config/default.yml` | ||||
| 
 | ||||
| 2. `default.yml` を編集する。 | ||||
| 
 | ||||
| *5.* Misskeyのビルド | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| 次のコマンドでMisskeyをビルドしてください: | ||||
| 
 | ||||
| `NODE_ENV=production yarn build` | ||||
| 
 | ||||
| Debianをお使いであれば、`build-essential`パッケージをインストールする必要があります。 | ||||
| 
 | ||||
| 何らかのモジュールでエラーが発生する場合はnode-gypを使ってください: | ||||
| 1. `npx node-gyp configure` | ||||
| 2. `npx node-gyp build` | ||||
| 3. `NODE_ENV=production yarn build` | ||||
| 
 | ||||
| *6.* データベースを初期化 | ||||
| ---------------------------------------------------------------- | ||||
| ``` shell | ||||
| yarn run init | ||||
| ``` | ||||
| 
 | ||||
| *7.* 以上です! | ||||
| ---------------------------------------------------------------- | ||||
| お疲れ様でした。これでMisskeyを動かす準備は整いました。 | ||||
| 
 | ||||
| ### 通常起動 | ||||
| `NODE_ENV=production yarn start`するだけです。GLHF! | ||||
| 
 | ||||
| ### systemdを用いた起動 | ||||
| 1. systemdサービスのファイルを作成 | ||||
| 
 | ||||
| 	`/etc/systemd/system/misskey.service` | ||||
| 
 | ||||
| 2. エディタで開き、以下のコードを貼り付けて保存: | ||||
| 
 | ||||
| 	``` | ||||
| 	[Unit] | ||||
| 	Description=Misskey daemon | ||||
| 
 | ||||
| 	[Service] | ||||
| 	Type=simple | ||||
| 	User=misskey | ||||
| 	ExecStart=/usr/bin/npm start | ||||
| 	WorkingDirectory=/home/misskey/misskey | ||||
| 	Environment="NODE_ENV=production" | ||||
| 	TimeoutSec=60 | ||||
| 	StandardOutput=syslog | ||||
| 	StandardError=syslog | ||||
| 	SyslogIdentifier=misskey | ||||
| 	Restart=always | ||||
| 
 | ||||
| 	[Install] | ||||
| 	WantedBy=multi-user.target | ||||
| 	``` | ||||
| 
 | ||||
| 	CentOSで1024以下のポートを使用してMisskeyを使用する場合は`ExecStart=/usr/bin/sudo /usr/bin/npm start`に変更する必要があります。 | ||||
| 
 | ||||
| 3. systemdを再読み込みしmisskeyサービスを有効化 | ||||
| 
 | ||||
| 	`systemctl daemon-reload; systemctl enable misskey` | ||||
| 
 | ||||
| 4. misskeyサービスの起動 | ||||
| 
 | ||||
| 	`systemctl start misskey` | ||||
| 
 | ||||
| `systemctl status misskey`と入力すると、サービスの状態を調べることができます。 | ||||
| 
 | ||||
| ### Misskeyを最新バージョンにアップデートする方法: | ||||
| 1. `git checkout master` | ||||
| 2. `git pull` | ||||
| 3. `git submodule update --init` | ||||
| 4. `yarn install` | ||||
| 5. `NODE_ENV=production yarn build` | ||||
| 6. `yarn migrate` | ||||
| 
 | ||||
| なにか問題が発生した場合は、`yarn clean`または`yarn cleanall`すると直る場合があります。 | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| なにかお困りのことがありましたらお気軽にご連絡ください。 | ||||
							
								
								
									
										147
									
								
								docs/setup.zh.md
									
										
									
									
									
								
							
							
						
						
									
										147
									
								
								docs/setup.zh.md
									
										
									
									
									
								
							|  | @ -1,147 +0,0 @@ | |||
| Misskey 设置和安装指南 | ||||
| ================================================================ | ||||
| 
 | ||||
| 非常感谢您对构建 Misskey 服务器的关注! | ||||
| 这份指南描述了 Misskey 的安装与设置流程。 | ||||
| 
 | ||||
| - [日本語版もあります - Japanese version also available](./setup.ja.md) | ||||
| - [英語版もあります - English version also available](./setup.en.md) | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| *1.* 创建 Misskey 用户 | ||||
| ---------------------------------------------------------------- | ||||
| 直接使用 root 用户来运行 misskey 也许并不是一个好主意,因此我们有必要创建一个专用的用户。 | ||||
| 以 Debian 为例: | ||||
| 
 | ||||
| ``` bash | ||||
| adduser --disabled-password --disabled-login misskey | ||||
| ``` | ||||
| 
 | ||||
| *2.* 安装依赖 | ||||
| ---------------------------------------------------------------- | ||||
| 请安装并设置如下这些软件: | ||||
| 
 | ||||
| #### Dependencies :package: | ||||
| * **[Node.js](https://nodejs.org/en/)** (12.x, 14.x) | ||||
| * **[PostgreSQL](https://www.postgresql.org/)** (>= 10) | ||||
| * **[Redis](https://redis.io/)** | ||||
| 
 | ||||
| ##### Optional | ||||
| * [Yarn](https://yarnpkg.com/) *可选,但出于安全因素考虑还是推荐安装。如果您没有安装, 您需要使用 `npx yarn` 来代替 `yarn`.* | ||||
| * [Elasticsearch](https://www.elastic.co/) - 为了启用搜索功能,这个搜索引擎是有必要的。 | ||||
| * [FFmpeg](https://www.ffmpeg.org/) | ||||
| 
 | ||||
| *3.* 安装 Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. 连接至 misskey 用户. | ||||
| 
 | ||||
| 	`su - misskey` | ||||
| 
 | ||||
| 2. 克隆 Misskey 项目的 master 分支。 | ||||
| 
 | ||||
| 	`git clone -b master git://github.com/misskey-dev/misskey.git` | ||||
| 
 | ||||
| 3. 进入 misskey 文件夹。 | ||||
| 
 | ||||
| 	`cd misskey` | ||||
| 
 | ||||
| 4. 检查 [最新发布版](https://github.com/misskey-dev/misskey/releases/latest) 标签。 | ||||
| 
 | ||||
| 	`git checkout master` | ||||
| 
 | ||||
| 5. 安装 Misskey 的依赖。 | ||||
| 
 | ||||
| 	`yarn` | ||||
| 
 | ||||
| *4.* 配置 Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. 复制 `.config/example.yml` 并重命名为 `default.yml`。 | ||||
| 
 | ||||
| 	`cp .config/example.yml .config/default.yml` | ||||
| 
 | ||||
| 2. 编辑 `default.yml` | ||||
| 
 | ||||
| *5.* 构建 Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| 使用如下的指令构建 Misskey : | ||||
| 
 | ||||
| `NODE_ENV=production yarn build` | ||||
| 
 | ||||
| 如果您使用的是 Debian , 您需要安装 `build-essential`, `python` 环境包。 | ||||
| 
 | ||||
| 如果您仍然遇到有关某些模块的错误,您可以使用 node-gyp: | ||||
| 
 | ||||
| 1. `npx node-gyp configure` | ||||
| 2. `npx node-gyp build` | ||||
| 3. `NODE_ENV=production yarn build` | ||||
| 
 | ||||
| *6.* 初始化数据库 | ||||
| ---------------------------------------------------------------- | ||||
| ``` bash | ||||
| yarn run init | ||||
| ``` | ||||
| 
 | ||||
| *7.* 完成了! | ||||
| ---------------------------------------------------------------- | ||||
| 干得不错!现在您拥有了一个可以运行Misskey的环境啦。 | ||||
| 
 | ||||
| ### 正常启动 | ||||
| 只需要 `NODE_ENV=production npm start` 即可。玩得愉快! | ||||
| 
 | ||||
| ### 使用 systemd 来启动 | ||||
| 
 | ||||
| 1. 在此处创建一个 systemd 服务: | ||||
| 
 | ||||
| 	`/etc/systemd/system/misskey.service` | ||||
| 
 | ||||
| 2. 编辑它,粘贴如下内容并保存: | ||||
| 
 | ||||
| 	``` | ||||
| 	[Unit] | ||||
| 	Description=Misskey daemon | ||||
| 
 | ||||
| 	[Service] | ||||
| 	Type=simple | ||||
| 	User=misskey | ||||
| 	ExecStart=/usr/bin/npm start | ||||
| 	WorkingDirectory=/home/misskey/misskey | ||||
| 	Environment="NODE_ENV=production" | ||||
| 	TimeoutSec=60 | ||||
| 	StandardOutput=syslog | ||||
| 	StandardError=syslog | ||||
| 	SyslogIdentifier=misskey | ||||
| 	Restart=always | ||||
| 
 | ||||
| 	[Install] | ||||
| 	WantedBy=multi-user.target | ||||
| 	``` | ||||
| 
 | ||||
| 3. 重启 systemd 并设置 misskey 服务自动启动: | ||||
| 
 | ||||
| 	`systemctl daemon-reload ; systemctl enable misskey` | ||||
| 
 | ||||
| 4. 启动 misskey 服务: | ||||
| 
 | ||||
| 	`systemctl start misskey` | ||||
| 
 | ||||
| 您可以使用 `systemctl status misskey` 来检查服务是否正在运行。 | ||||
| 
 | ||||
| ### 如何将您的 Misskey 服务器升级至最新版本 | ||||
| 1. `git checkout master` | ||||
| 2. `git pull` | ||||
| 3. `git submodule update --init` | ||||
| 4. `yarn install` | ||||
| 5. `NODE_ENV=production yarn build` | ||||
| 6. `yarn migrate` | ||||
| 7. 重启您的 Misskey 进程来应用改变。 | ||||
| 8. 尽情享受吧! | ||||
| 
 | ||||
| 如果您在更新时遇到任何问题,请尝试以下操作: | ||||
| 1. `yarn clean` 或是 `yarn cleanall` | ||||
| 2. 重试升级 (请不要忘记 `yarn install` ) | ||||
| 
 | ||||
| ---------------------------------------------------------------- | ||||
| 
 | ||||
| 如果您有任何疑问或是困惑,欢迎与我们联系! | ||||
|  | @ -63,6 +63,7 @@ files: "الملفات" | |||
| download: "تنزيل" | ||||
| driveFileDeleteConfirm: "أمتأكد من حذف ملف {name}؟ كل الملاحظات المُرفق بها هذا الملف ستحذف." | ||||
| unfollowConfirm: "أمتأكد من إلغاء متابعة {name}؟" | ||||
| importRequested: "يستغرق الاستيراد بعض الوقت" | ||||
| lists: "القوائم" | ||||
| noLists: "ليس لديك أية قائمة" | ||||
| note: "ملاحظة" | ||||
|  | @ -76,6 +77,7 @@ error: "خطأ" | |||
| somethingHappened: "حدث خطأ" | ||||
| retry: "حاول مجددًا" | ||||
| pageLoadError: "فشل تحميل الصفحة" | ||||
| serverIsDead: "الخادم لا يستجيب، حاول بعد قليل" | ||||
| enterListName: "اسم القائمة" | ||||
| privacy: "الخصوصية" | ||||
| makeFollowManuallyApprove: "القبول يدويا طلبات الإشتراك" | ||||
|  | @ -97,6 +99,7 @@ add: "إضافة" | |||
| reaction: "تفاعل" | ||||
| rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات" | ||||
| attachCancel: "أزل المرفق" | ||||
| markAsSensitive: "علّمه كمحتوى حساس" | ||||
| enterFileName: "ادخل اسم الملف" | ||||
| mute: "اكتم" | ||||
| unmute: "إلغاء الكتم" | ||||
|  | @ -109,15 +112,20 @@ unblockConfirm: "أمتأكد من إلغاء حجب هذا الحساب؟" | |||
| selectList: "اختر قائمة" | ||||
| editWidgetsExit: "تم" | ||||
| customEmojis: "إيموجي مخصص" | ||||
| emoji: "الوجوه التعبيرية" | ||||
| emojis: "الوجوه التعبيرية" | ||||
| emojiName: "اسم الوجه التعبيري" | ||||
| emojiUrl: "رابط الوجه التعبيري" | ||||
| addEmoji: "إضافة إيموجي" | ||||
| settingGuide: "الإعدادات المستحسنة" | ||||
| cacheRemoteFiles: "خزن مؤقتا الملفات البعيدة" | ||||
| autoAcceptFollowed: "اقبل طلبات المتابعة تلقائيا من الحسابات المتابَعة" | ||||
| loginFailed: "فشل الولوج" | ||||
| showOnRemote: "رؤيته على مثيل الخادم البُعدي" | ||||
| general: "الرئيسية" | ||||
| wallpaper: "خلفية الشاشة" | ||||
| setWallpaper: "استخدم خلفية الشاشة" | ||||
| removeWallpaper: "إزالة خلفية الشاشة" | ||||
| wallpaper: "الخلفية" | ||||
| setWallpaper: "عيّن خلفية" | ||||
| removeWallpaper: "أزل الخلفية" | ||||
| searchWith: "البحث: {q}" | ||||
| youHaveNoLists: "لا تمتلك أية قائمة" | ||||
| followConfirm: "أتريد متابعة {name}؟" | ||||
|  | @ -182,7 +190,7 @@ removeAreYouSure: "متأكد من أنك تريد حذف {x}؟" | |||
| deleteAreYouSure: "متأكد من أنك تريد حذف {x}؟" | ||||
| resetAreYouSure: "هل تريد إعادة التعيين؟" | ||||
| saved: "تم حفظه" | ||||
| messaging: "الدردشة" | ||||
| messaging: "المحادثة" | ||||
| upload: "تحميل" | ||||
| fromDrive: "من المخزن" | ||||
| fromUrl: "من عنوان URL" | ||||
|  | @ -194,7 +202,7 @@ explore: "استكشاف" | |||
| games: "ألعاب Misskey" | ||||
| messageRead: "مقروءة" | ||||
| noMoreHistory: "لا يوجد المزيد من التاريخ" | ||||
| startMessaging: "ابدأ الدردشة" | ||||
| startMessaging: "ابدأ محادثة" | ||||
| nUsersRead: "تمت القراءة من {n}" | ||||
| agreeTo: "اوافق على {0}" | ||||
| tos: "شروط الخدمة" | ||||
|  | @ -266,7 +274,17 @@ disablingTimelinesInfo: "سيتمكن المسؤولون ومن تعديل دا | |||
| registration: "إنشاء حساب" | ||||
| enableRegistration: "تفعيل إنشاء الحسابات الجديدة" | ||||
| invite: "دعوة" | ||||
| driveCapacityPerLocalAccount: "حصة التخزين لكل مستخدم محلي" | ||||
| driveCapacityPerRemoteAccount: "حصة التخزين لكل مستخدم بعيد" | ||||
| inMb: "بالميغابايت" | ||||
| iconUrl: "رابط الأيقونة" | ||||
| bannerUrl: "رابط صورة اللافتة" | ||||
| backgroundImageUrl: "رابط صورة الخلفية" | ||||
| basicInfo: "المعلومات الأساسية " | ||||
| pinnedUsers: "المستخدمون المثبتون" | ||||
| pinnedUsersDescription: "قائمة المستخدمين المثبتين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده." | ||||
| pinnedPages: "الصفحات المثبتة" | ||||
| pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تثبيتها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده." | ||||
| pinnedNotes: "ملاحظة مدبسة" | ||||
| hcaptchaSiteKey: "مفتاح الموقع" | ||||
| hcaptchaSecretKey: "المفتاح السري" | ||||
|  | @ -279,12 +297,19 @@ manageAntennas: "إدارة الهوائيات" | |||
| name: "الإسم" | ||||
| antennaSource: "مصدر الهوائي" | ||||
| antennaKeywords: "الكلمات المفتاحية للإستقبال" | ||||
| notifyAntenna: "نبهني بصول ملاحظات جديدة" | ||||
| withFileAntenna: "ملاحظات تحوي ملفات فقط" | ||||
| caseSensitive: "حساسية حالة الأحرف" | ||||
| withReplies: "بالردود" | ||||
| notesAndReplies: "الملاحظات والردود" | ||||
| withFiles: "بالمرفقات" | ||||
| silence: "اكتم" | ||||
| unsilence: "إلغاء الكتم" | ||||
| popularUsers: "المستخدمون الشائعون" | ||||
| recentlyUpdatedUsers: "أصحاب النشاطات الأخيرة" | ||||
| recentlyRegisteredUsers: "المستخدمون المنضمون حديثًا" | ||||
| recentlyDiscoveredUsers: "المستخدمون المكتشفون حديثًا" | ||||
| exploreUsersCount: "يوجد {count} مستخدم(ا)" | ||||
| exploreFediverse: "استكشف الفديفرس" | ||||
| popularTags: "الوسوم الرائجة" | ||||
| userList: "القوائم" | ||||
|  | @ -297,11 +322,13 @@ moderator: "مشرِف" | |||
| nUsersMentioned: "{n} مستخدمين تمت الإشارة إليهم" | ||||
| securityKey: "مفتاح الأمان" | ||||
| securityKeyName: "اسم المفتاح" | ||||
| registerSecurityKey: "سجل مفتاح أمان" | ||||
| lastUsed: "آخر استخدام" | ||||
| unregister: "إلغاء التسجيل" | ||||
| passwordLessLogin: "لِج مِن دون كلمة سرية" | ||||
| resetPassword: "أعد تعيين كلمتك السرية" | ||||
| newPasswordIs: "كلمتك السرية الجديدة هي {password}" | ||||
| reduceUiAnimation: "قلص تأثيرات الواجهة" | ||||
| share: "شارِك" | ||||
| notFound: "غير موجود" | ||||
| cacheClear: "مسح ذاكرة التخزين المؤقت" | ||||
|  | @ -316,8 +343,8 @@ invites: "دعوة" | |||
| groupName: "اسم الفريق" | ||||
| members: "الأعضاء" | ||||
| transfer: "نقل" | ||||
| messagingWithUser: "الدردشة مع مستخدم آخر" | ||||
| messagingWithGroup: "دردشة جماعية" | ||||
| messagingWithUser: "تحدث مع مستخدم" | ||||
| messagingWithGroup: "محادثة جماعية" | ||||
| title: "العنوان" | ||||
| text: "النص" | ||||
| enable: "تشغيل" | ||||
|  | @ -362,28 +389,43 @@ total: "المجموع" | |||
| weekOverWeekChanges: "أسبوعيا" | ||||
| dayOverDayChanges: "يوميا" | ||||
| appearance: "المظهر" | ||||
| clientSettings: "إعدادات العميل" | ||||
| accountSettings: "إعدادات الحساب" | ||||
| promotion: "ترقية" | ||||
| promote: "روِّج" | ||||
| numberOfDays: "عدد الأيام" | ||||
| hideThisNote: "إخفاء هذه الملاحظة" | ||||
| objectStorageBaseUrl: "الرابط الأساسي" | ||||
| objectStoragePrefix: "البادئة" | ||||
| objectStorageEndpoint: "نقطة النهاية" | ||||
| objectStorageRegion: "المنطقة" | ||||
| objectStorageUseSSL: "استخدم SSL" | ||||
| objectStorageUseProxy: "اتصل عبر وكيل" | ||||
| serverLogs: "سجلات الخادم" | ||||
| deleteAll: "حذف الكل" | ||||
| showFixedPostForm: "أظهر نموذج الكتابة في أعلى الصفحة" | ||||
| newNoteRecived: "هناك ملاحظات جديدة" | ||||
| sounds: "الرنات" | ||||
| listen: "استمع" | ||||
| none: "لا شيء" | ||||
| showInPage: "اعرض في الصفحة" | ||||
| volume: "مستوى الصوت" | ||||
| details: "التفاصيل" | ||||
| chooseEmoji: "اختر إيموجي" | ||||
| unableToProcess: "يتعذر إكمال العملية" | ||||
| recentUsed: "المستخدمة مؤخرا" | ||||
| install: "التثبيت" | ||||
| uninstall: "إلغاء التثبيت" | ||||
| installedApps: "التطبيقات المُخوّلة" | ||||
| nothing: "لا يوجد شيء هنا" | ||||
| lastUsedDate: "آخر استخدام" | ||||
| state: "الحالة" | ||||
| sort: "ترتيب حسب" | ||||
| output: "الخارجة" | ||||
| updateRemoteUser: "تحديث المعلومات عن المستخدم البعيد" | ||||
| deleteAllFiles: "حذف كافة الملفات" | ||||
| deleteAllFilesConfirm: "أتريد حذف كل الملفات؟" | ||||
| removeAllFollowing: "ألغ متابعة كل المتابِعين" | ||||
| userSuspended: "تم تعليق هذا المستخدم." | ||||
| userSilenced: "تم إسكات هذا المستخدم." | ||||
| addItem: "إضافة عنصر" | ||||
|  | @ -419,7 +461,40 @@ makeActive: "تفعيل" | |||
| display: "المظهر" | ||||
| copy: "نسخ" | ||||
| metrics: "المقاييس" | ||||
| fileIdOrUrl: "معرف الملف أو رابط" | ||||
| chatOpenBehavior: "سلوك نفاذة المحادثة عند فتحها" | ||||
| behavior: "السلوك" | ||||
| sample: "مثال" | ||||
| abuseReports: "البلاغات" | ||||
| reportAbuse: "البلاغات" | ||||
| reportAbuseOf: "أبلغ عن {name}" | ||||
| fillAbuseReportDescription: "أكتب بالتفصيل سبب الإبلاغ، إذا كنت تبلغ عن ملاحظة أرفق رابط لها." | ||||
| abuseReported: "أُرسل البلاغ، شكرًا لك" | ||||
| send: "أرسل" | ||||
| abuseMarkAsResolved: "علّم البلاغ كمحلول" | ||||
| openInNewTab: "افتح في لسان جديد" | ||||
| defaultNavigationBehaviour: "سلوك الملاحة الافتراضي" | ||||
| waitingFor: "في انتظار {x}" | ||||
| random: "عشوائي" | ||||
| system: "النظام" | ||||
| switchUi: "بدّل واجهة المستخدم" | ||||
| createNew: "أنشِئ جديد" | ||||
| optional: "اختياري" | ||||
| public: "للعامة" | ||||
| i18nInfo: "يترجم متطوعون ميسكي إلى عدة لغات، يمكنك المساعدة عبر {link}" | ||||
| manageAccessTokens: "إدارة رموز الوصول" | ||||
| accountInfo: "معلومات الحساب" | ||||
| notesCount: "عدد الملاحظات" | ||||
| repliesCount: "عدد الردود المرسلة" | ||||
| repliedCount: "عدد الردود المستلمة" | ||||
| followingCount: "عدد الحسابات المتابَعة" | ||||
| followersCount: "عدد المتابِعين" | ||||
| sentReactionsCount: "عدد الانفعالات المرسلة" | ||||
| receivedReactionsCount: "عدد الانفعالات المستلمة" | ||||
| pollVotesCount: "عدد الاستطلاعات المرسلة" | ||||
| pollVotedCount: "عدد الاستطلاعات المستلمة" | ||||
| yes: "نعم" | ||||
| no: "لا" | ||||
| currentVersion: "الإصدار الحالي" | ||||
| latestVersion: "آخر نسخة مستقرة" | ||||
| usageAmount: "الإستخدام" | ||||
|  | @ -433,6 +508,7 @@ gallery: "المعرض" | |||
| expiration: "ينتهي استطلاع الرأي في" | ||||
| middle: "متوسط" | ||||
| global: "الشامل" | ||||
| sent: "أرسل" | ||||
| _docs: | ||||
|   admin: "إدارة " | ||||
| _email: | ||||
|  | @ -459,12 +535,12 @@ _theme: | |||
|   alpha: "الشفافية" | ||||
|   keys: | ||||
|     mention: "أشر الى" | ||||
|     messageBg: "خلفية الدردشة" | ||||
|     messageBg: "خلفية المحادثة" | ||||
| _sfx: | ||||
|   note: "الملاحظات" | ||||
|   noteMy: "ملاحظتي" | ||||
|   notification: "الإشعارات" | ||||
|   chat: "الدردشة" | ||||
|   chat: "المحادثة" | ||||
| _ago: | ||||
|   unknown: "مجهول" | ||||
|   future: "المستقبَل" | ||||
|  |  | |||
|  | @ -797,6 +797,8 @@ unread: "Ungelesen" | |||
| filter: "Filter" | ||||
| controllPanel: "Systemsteuerung" | ||||
| manageAccounts: "Benutzerkonten verwalten" | ||||
| makeReactionsPublic: "Reaktionsverlauf veröffentlichen" | ||||
| makeReactionsPublicDescription: "Jeder wird die Liste deiner gesendeten Reaktionen einsehen können." | ||||
| _signup: | ||||
|   almostThere: "Fast geschafft" | ||||
|   emailAddressInfo: "Bitte gib deine Email-Adresse ein." | ||||
|  |  | |||
|  | @ -797,6 +797,8 @@ unread: "Unread" | |||
| filter: "Filter" | ||||
| controllPanel: "Control Panel" | ||||
| manageAccounts: "Manage Accounts" | ||||
| makeReactionsPublic: "Set reaction history to public" | ||||
| makeReactionsPublicDescription: "This will make the list of all your past reactions publicly visible." | ||||
| _signup: | ||||
|   almostThere: "Almost there" | ||||
|   emailAddressInfo: "Please enter your email address." | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| _lang_: "Esperanto" | ||||
| headlineMisskey: "Jen la reto konektata de notoj" | ||||
| introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza etbloga servo.\nKreu \"noto\"n por paroli vian penson al iuj ĉirkaŭ vi. 📡\nLa funkcion \"reago\" ebligas esprimi rapide vian senton pri ies noto en Fediverso. 👍\nBonvole esploru novan mondon. 🚀" | ||||
| monthAndDay: "La {day}-a de la {month}-a monato" | ||||
| monthAndDay: "{{day}}/{{month}}" | ||||
| search: "Serĉi" | ||||
| notifications: "Sciigoj" | ||||
| username: "Uzantnomo" | ||||
|  | @ -20,10 +20,10 @@ instance: "Nodo" | |||
| settings: "Agordoj" | ||||
| basicSettings: "Ĝeneralaj agordoj" | ||||
| otherSettings: "Aliaj agordoj" | ||||
| openInWindow: "Malfermi en nova fenestro" | ||||
| openInWindow: "Malfermi en fenestro" | ||||
| profile: "Profilo" | ||||
| timeline: "Templinio" | ||||
| noAccountDescription: "Ĉi tiu uzanto ne skribis vivpriskribon." | ||||
| noAccountDescription: "Neniu priskribo" | ||||
| login: "Ensaluti" | ||||
| loggingIn: "Ensalutado…" | ||||
| logout: "Elsaluti" | ||||
|  | @ -44,7 +44,7 @@ copyContent: "Kopii enhavon" | |||
| copyLink: "Kopii ligilon" | ||||
| delete: "Forviŝi" | ||||
| deleteAndEdit: "Redakti foriginte" | ||||
| deleteAndEditConfirm: "Ĉu vi certas, ke vi volas forigi kaj redakti la noton? Tio forviŝos reagojn, notojn plusendintajn, kaj respondojn ĉiujn de ĝi." | ||||
| deleteAndEditConfirm: "Ĉu vi certas, ke vi volas forigi kaj redakti la noton? Tio forviŝos reagojn, plusendojn, kaj respondojn ĉiujn de ĝi." | ||||
| addToList: "Aldoni al listo" | ||||
| sendMessage: "Sendi mesaĝon" | ||||
| copyUsername: "Kopii uzantnomon" | ||||
|  | @ -89,7 +89,7 @@ renote: "Plusendi la noton" | |||
| unrenote: "Malfari plusendadon" | ||||
| renoted: "Sukcese plusendita" | ||||
| cantRenote: "Oni ne povas plusendi la noton." | ||||
| cantReRenote: "Plusendado ne estas plusendebla." | ||||
| cantReRenote: "Plusendo de noto ne estas plusendebla." | ||||
| quote: "Citi" | ||||
| pinnedNote: "Alpinglita noto" | ||||
| pinned: "Alpingli" | ||||
|  | @ -220,7 +220,7 @@ remoteUserCaution: "Ĉi tiuj infomoj ne estas tute ekzaktaj pro transa uzanto." | |||
| activity: "Aktiveco" | ||||
| images: "Bildoj" | ||||
| birthday: "Naskiĝdato" | ||||
| registeredDate: "Registriĝdato" | ||||
| registeredDate: "Dato de registriĝo" | ||||
| location: "Loko" | ||||
| theme: "Koloraro" | ||||
| light: "Luma" | ||||
|  | @ -262,7 +262,7 @@ thisYear: "Ĉi-jare" | |||
| thisMonth: "Ĉi-monate" | ||||
| today: "Hodiaŭ" | ||||
| dayX: "{day}a" | ||||
| monthX: "{month}" | ||||
| monthX: "La {month}a monato" | ||||
| yearX: "La jaro {year}" | ||||
| pages: "Paĝoj" | ||||
| connectService: "Konekti" | ||||
|  | @ -317,6 +317,7 @@ nUsersMentioned: "{n} uzanto(j) menciis" | |||
| securityKey: "Sekureca ŝlosilo" | ||||
| securityKeyName: "Nomo de la ŝlosilo" | ||||
| lastUsed: "Plej malnove uzita" | ||||
| unregister: "Malregistriĝi" | ||||
| passwordLessLogin: "Ensaluti sen pasvorto" | ||||
| resetPassword: "Restarigi pasvorton" | ||||
| newPasswordIs: "La nova pasvorto estas {password}." | ||||
|  | @ -516,7 +517,7 @@ clear: "Vakigi" | |||
| goBack: "Reiri antaŭ" | ||||
| addDescription: "Priskribi" | ||||
| info: "Informoj" | ||||
| userInfo: "La informoj de uzanto" | ||||
| userInfo: "Informoj de uzanto" | ||||
| unknown: "Nekonata" | ||||
| online: "Surkonektita" | ||||
| offline: "Forkonektita" | ||||
|  | @ -688,13 +689,13 @@ _antennaSources: | |||
|   all: "Ĉiuj notoj" | ||||
|   homeTimeline: "Notoj far uzantoj kiujn vi sekvas" | ||||
| _weekday: | ||||
|   sunday: "dimanĉo" | ||||
|   monday: "lundo" | ||||
|   tuesday: "mardo" | ||||
|   wednesday: "merkredo" | ||||
|   thursday: "ĵaŭdo" | ||||
|   friday: "vendredo" | ||||
|   saturday: "sabato" | ||||
|   sunday: "Dimanĉo" | ||||
|   monday: "Lundo" | ||||
|   tuesday: "Mardo" | ||||
|   wednesday: "Merkredo" | ||||
|   thursday: "Ĵaŭdo" | ||||
|   friday: "Vendredo" | ||||
|   saturday: "Sabato" | ||||
| _widgets: | ||||
|   notifications: "Sciigoj" | ||||
|   timeline: "Templinio" | ||||
|  |  | |||
|  | @ -365,7 +365,7 @@ withFiles: "Avec fichiers joints" | |||
| silence: "Mettre en sourdine" | ||||
| silenceConfirm: "Êtes-vous sûr·e de vouloir mettre l’utilisateur·rice en sourdine ?" | ||||
| unsilence: "Annuler la sourdine" | ||||
| unsilenceConfirm: "Êtes-vous sûr·e de vouloir annuler la mise en sourdine de cette utilisateur·rice ?" | ||||
| unsilenceConfirm: "Êtes-vous sûr·e de vouloir annuler la mise en sourdine de cet·te utilisateur·rice ?" | ||||
| popularUsers: "Utilisateur·rice·s populaires" | ||||
| recentlyUpdatedUsers: "Utilisateur·rice·s actif·ve·s récemment" | ||||
| recentlyRegisteredUsers: "Utilisateur·rice·s récemment inscrit·e·s" | ||||
|  | @ -766,6 +766,7 @@ middle: "Moyen" | |||
| low: "Basse" | ||||
| emailNotConfiguredWarning: "Vous n'avez pas configuré d'adresse e-mail." | ||||
| ratio: "Ratio" | ||||
| previewNoteText: "Voir l'aperçu" | ||||
| customCss: "CSS personnalisé" | ||||
| customCssWarn: "Utilisez cette fonctionnalité uniquement si vous savez exactement ce que vous faites. Une configuration inadaptée peut empêcher le client de s'exécuter normalement." | ||||
| global: "Global" | ||||
|  | @ -789,6 +790,8 @@ pubSub: "Comptes Pub/Sub" | |||
| lastCommunication: "Dernière communication" | ||||
| resolved: "Résolu" | ||||
| unresolved: "En attente" | ||||
| itsOn: "Activé" | ||||
| itsOff: "Désactivé" | ||||
| emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte" | ||||
| unread: "Non lu" | ||||
| filter: "Filtre" | ||||
|  |  | |||
|  | @ -797,6 +797,8 @@ unread: "未読" | |||
| filter: "フィルタ" | ||||
| controllPanel: "コントロールパネル" | ||||
| manageAccounts: "アカウントを管理" | ||||
| makeReactionsPublic: "リアクション一覧を公開する" | ||||
| makeReactionsPublicDescription: "あなたがしたリアクション一覧を誰でも見れるようにします。" | ||||
| 
 | ||||
| _signup: | ||||
|   almostThere: "ほとんど完了です" | ||||
|  |  | |||
							
								
								
									
										14
									
								
								migration/1634486652000-user-public-reactions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								migration/1634486652000-user-public-reactions.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| import {MigrationInterface, QueryRunner} from "typeorm"; | ||||
| 
 | ||||
| export class userPublicReactions1634486652000 implements MigrationInterface { | ||||
|     name = 'userPublicReactions1634486652000' | ||||
| 
 | ||||
|     public async up(queryRunner: QueryRunner): Promise<void> { | ||||
| 			await queryRunner.query(`ALTER TABLE "user_profile" ADD "publicReactions" boolean NOT NULL DEFAULT false`); | ||||
|     } | ||||
| 
 | ||||
|     public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "publicReactions"`); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										13
									
								
								migration/1634902659689-delete-log.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								migration/1634902659689-delete-log.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| import {MigrationInterface, QueryRunner} from "typeorm"; | ||||
| 
 | ||||
| export class deleteLog1634902659689 implements MigrationInterface { | ||||
|     name = 'deleteLog1634902659689' | ||||
| 
 | ||||
|     public async up(queryRunner: QueryRunner): Promise<void> { | ||||
| 			await queryRunner.query(`DROP TABLE "log"`); | ||||
|     } | ||||
| 
 | ||||
|     public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										14
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										14
									
								
								package.json
									
										
									
									
									
								
							|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	"name": "misskey", | ||||
| 	"author": "syuilo <syuilotan@yahoo.co.jp>", | ||||
| 	"version": "12.92.0", | ||||
| 	"version": "12.93.0", | ||||
| 	"codename": "indigo", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
|  | @ -60,6 +60,9 @@ | |||
| 		"@types/jsonld": "1.5.6", | ||||
| 		"@types/katex": "0.11.1", | ||||
| 		"@types/koa": "2.13.4", | ||||
| 		"@types/koa__cors": "3.0.3", | ||||
| 		"@types/koa__multer": "2.0.3", | ||||
| 		"@types/koa__router": "8.0.8", | ||||
| 		"@types/koa-bodyparser": "4.3.3", | ||||
| 		"@types/koa-cors": "0.0.2", | ||||
| 		"@types/koa-favicon": "2.0.21", | ||||
|  | @ -67,9 +70,6 @@ | |||
| 		"@types/koa-mount": "4.0.1", | ||||
| 		"@types/koa-send": "4.1.3", | ||||
| 		"@types/koa-views": "7.0.0", | ||||
| 		"@types/koa__cors": "3.0.3", | ||||
| 		"@types/koa__multer": "2.0.3", | ||||
| 		"@types/koa__router": "8.0.8", | ||||
| 		"@types/markdown-it": "12.2.3", | ||||
| 		"@types/matter-js": "0.17.5", | ||||
| 		"@types/mocha": "8.2.3", | ||||
|  | @ -119,7 +119,9 @@ | |||
| 		"cafy": "15.2.1", | ||||
| 		"cbor": "8.0.2", | ||||
| 		"chalk": "4.1.2", | ||||
| 		"chart.js": "2.9.4", | ||||
| 		"chart.js": "3.5.1", | ||||
| 		"chartjs-adapter-date-fns": "2.0.0", | ||||
| 		"chartjs-plugin-zoom": "1.1.1", | ||||
| 		"cli-highlight": "2.1.11", | ||||
| 		"compare-versions": "3.6.0", | ||||
| 		"concurrently": "6.3.0", | ||||
|  | @ -127,6 +129,7 @@ | |||
| 		"crc-32": "1.2.0", | ||||
| 		"css-loader": "6.4.0", | ||||
| 		"cssnano": "5.0.8", | ||||
| 		"date-fns": "2.25.0", | ||||
| 		"dateformat": "4.5.1", | ||||
| 		"escape-regexp": "0.0.1", | ||||
| 		"eslint": "8.0.1", | ||||
|  | @ -208,6 +211,7 @@ | |||
| 		"seedrandom": "3.0.5", | ||||
| 		"sharp": "0.29.1", | ||||
| 		"speakeasy": "2.0.0", | ||||
| 		"strict-event-emitter-types": "2.0.0", | ||||
| 		"stringz": "2.1.0", | ||||
| 		"style-loader": "3.3.0", | ||||
| 		"summaly": "2.4.1", | ||||
|  |  | |||
							
								
								
									
										628
									
								
								src/client/components/chart.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										628
									
								
								src/client/components/chart.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,628 @@ | |||
| <template> | ||||
| <canvas ref="chartEl"></canvas> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, onMounted, ref, watch, PropType } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| } from 'chart.js'; | ||||
| import 'chartjs-adapter-date-fns'; | ||||
| import { enUS } from 'date-fns/locale'; | ||||
| import zoomPlugin from 'chartjs-plugin-zoom'; | ||||
| import * as os from '@client/os'; | ||||
| import { defaultStore } from '@client/store'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| 	zoomPlugin, | ||||
| ); | ||||
| 
 | ||||
| const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); | ||||
| const negate = arr => arr.map(x => -x); | ||||
| const alpha = (hex, a) => { | ||||
| 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; | ||||
| 	const r = parseInt(result[1], 16); | ||||
| 	const g = parseInt(result[2], 16); | ||||
| 	const b = parseInt(result[3], 16); | ||||
| 	return `rgba(${r}, ${g}, ${b}, ${a})`; | ||||
| }; | ||||
| 
 | ||||
| const colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560']; | ||||
| const getColor = (i) => { | ||||
| 	return colors[i % colors.length]; | ||||
| }; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		src: { | ||||
| 			type: String, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		args: { | ||||
| 			type: Object, | ||||
| 			required: false, | ||||
| 		}, | ||||
| 		limit: { | ||||
| 			type: Number, | ||||
| 			required: false, | ||||
| 			default: 90 | ||||
| 		}, | ||||
| 		span: { | ||||
| 			type: String as PropType<'hour' | 'day'>, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		detailed: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	setup(props) { | ||||
| 		const now = new Date(); | ||||
| 		let chartInstance: Chart = null; | ||||
| 		let data: { | ||||
| 			series: { | ||||
| 				name: string; | ||||
| 				type: 'line' | 'area'; | ||||
| 				color?: string; | ||||
| 				borderDash?: number[]; | ||||
| 				hidden?: boolean; | ||||
| 				data: { | ||||
| 					x: number; | ||||
| 					y: number; | ||||
| 				}[]; | ||||
| 			}[]; | ||||
| 		} = null; | ||||
| 
 | ||||
| 		const chartEl = ref<HTMLCanvasElement>(null); | ||||
| 		const fetching = ref(true); | ||||
| 
 | ||||
| 		const getDate = (ago: number) => { | ||||
| 			const y = now.getFullYear(); | ||||
| 			const m = now.getMonth(); | ||||
| 			const d = now.getDate(); | ||||
| 			const h = now.getHours(); | ||||
| 
 | ||||
| 			return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); | ||||
| 		}; | ||||
| 
 | ||||
| 		const format = (arr) => { | ||||
| 			return arr.map((v, i) => ({ | ||||
| 				x: getDate(i).getTime(), | ||||
| 				y: v | ||||
| 			})); | ||||
| 		}; | ||||
| 
 | ||||
| 		const render = () => { | ||||
| 			if (chartInstance) { | ||||
| 				chartInstance.destroy(); | ||||
| 			} | ||||
| 
 | ||||
| 			const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; | ||||
| 
 | ||||
| 			// フォントカラー | ||||
| 			Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||
| 
 | ||||
| 			chartInstance = new Chart(chartEl.value, { | ||||
| 				type: 'line', | ||||
| 				data: { | ||||
| 					labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(), | ||||
| 					datasets: data.series.map((x, i) => ({ | ||||
| 						parsing: false, | ||||
| 						label: x.name, | ||||
| 						data: x.data.slice().reverse(), | ||||
| 						pointRadius: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: x.color ? x.color : getColor(i), | ||||
| 						borderDash: x.borderDash || [], | ||||
| 						borderJoinStyle: 'round', | ||||
| 						backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1), | ||||
| 						fill: x.type === 'area', | ||||
| 						hidden: !!x.hidden, | ||||
| 					})), | ||||
| 				}, | ||||
| 				options: { | ||||
| 					aspectRatio: 2.5, | ||||
| 					layout: { | ||||
| 						padding: { | ||||
| 							left: 16, | ||||
| 							right: 16, | ||||
| 							top: 16, | ||||
| 							bottom: 8, | ||||
| 						}, | ||||
| 					}, | ||||
| 					scales: { | ||||
| 						x: { | ||||
| 							type: 'time', | ||||
| 							time: { | ||||
| 								stepSize: 1, | ||||
| 								unit: props.span === 'day' ? 'month' : 'day', | ||||
| 							}, | ||||
| 							grid: { | ||||
| 								display: props.detailed, | ||||
| 								color: gridColor, | ||||
| 								borderColor: 'rgb(0, 0, 0, 0)', | ||||
| 							}, | ||||
| 							ticks: { | ||||
| 								display: props.detailed, | ||||
| 							}, | ||||
| 							adapters: { | ||||
| 								date: { | ||||
| 									locale: enUS, | ||||
| 								}, | ||||
| 							}, | ||||
| 							min: getDate(props.limit).getTime(), | ||||
| 						}, | ||||
| 						y: { | ||||
| 							position: 'left', | ||||
| 							grid: { | ||||
| 								color: gridColor, | ||||
| 								borderColor: 'rgb(0, 0, 0, 0)', | ||||
| 							}, | ||||
| 							ticks: { | ||||
| 								display: props.detailed, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 					interaction: { | ||||
| 						intersect: false, | ||||
| 					}, | ||||
| 					plugins: { | ||||
| 						legend: { | ||||
| 							position: 'bottom', | ||||
| 							labels: { | ||||
| 								boxWidth: 16, | ||||
| 							}, | ||||
| 						}, | ||||
| 						tooltip: { | ||||
| 							mode: 'index', | ||||
| 							animation: { | ||||
| 								duration: 0, | ||||
| 							}, | ||||
| 						}, | ||||
| 						zoom: { | ||||
| 							pan: { | ||||
| 								enabled: true, | ||||
| 							}, | ||||
| 							zoom: { | ||||
| 								wheel: { | ||||
| 									enabled: true, | ||||
| 								}, | ||||
| 								pinch: { | ||||
| 									enabled: true, | ||||
| 								}, | ||||
| 								drag: { | ||||
| 									enabled: false, | ||||
| 								}, | ||||
| 								mode: 'x', | ||||
| 							}, | ||||
| 							limits: { | ||||
| 								x: { | ||||
| 									min: 'original', | ||||
| 									max: 'original', | ||||
| 								}, | ||||
| 								y: { | ||||
| 									min: 'original', | ||||
| 									max: 'original', | ||||
| 								}, | ||||
| 							} | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}); | ||||
| 		}; | ||||
| 
 | ||||
| 		const exportData = () => { | ||||
| 			// TODO | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchFederationInstancesChart = async (total: boolean): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/federation', { limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Instances', | ||||
| 					type: 'area', | ||||
| 					data: format(total | ||||
| 						? raw.instance.total | ||||
| 						: sum(raw.instance.inc, negate(raw.instance.dec)) | ||||
| 					), | ||||
| 				}], | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchNotesChart = async (type: string): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/notes', { limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'All', | ||||
| 					type: 'line', | ||||
| 					borderDash: [5, 5], | ||||
| 					data: format(type == 'combined' | ||||
| 						? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)) | ||||
| 						: sum(raw[type].inc, negate(raw[type].dec)) | ||||
| 					), | ||||
| 				}, { | ||||
| 					name: 'Renotes', | ||||
| 					type: 'area', | ||||
| 					data: format(type == 'combined' | ||||
| 						? sum(raw.local.diffs.renote, raw.remote.diffs.renote) | ||||
| 						: raw[type].diffs.renote | ||||
| 					), | ||||
| 				}, { | ||||
| 					name: 'Replies', | ||||
| 					type: 'area', | ||||
| 					data: format(type == 'combined' | ||||
| 						? sum(raw.local.diffs.reply, raw.remote.diffs.reply) | ||||
| 						: raw[type].diffs.reply | ||||
| 					), | ||||
| 				}, { | ||||
| 					name: 'Normal', | ||||
| 					type: 'area', | ||||
| 					data: format(type == 'combined' | ||||
| 						? sum(raw.local.diffs.normal, raw.remote.diffs.normal) | ||||
| 						: raw[type].diffs.normal | ||||
| 					), | ||||
| 				}], | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchNotesTotalChart = async (): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/notes', { limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Combined', | ||||
| 					type: 'line', | ||||
| 					data: format(sum(raw.local.total, raw.remote.total)), | ||||
| 				}, { | ||||
| 					name: 'Local', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.local.total), | ||||
| 				}, { | ||||
| 					name: 'Remote', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.remote.total), | ||||
| 				}], | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchUsersChart = async (total: boolean): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/users', { limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Combined', | ||||
| 					type: 'line', | ||||
| 					data: format(total | ||||
| 						? sum(raw.local.total, raw.remote.total) | ||||
| 						: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)) | ||||
| 					), | ||||
| 				}, { | ||||
| 					name: 'Local', | ||||
| 					type: 'area', | ||||
| 					data: format(total | ||||
| 						? raw.local.total | ||||
| 						: sum(raw.local.inc, negate(raw.local.dec)) | ||||
| 					), | ||||
| 				}, { | ||||
| 					name: 'Remote', | ||||
| 					type: 'area', | ||||
| 					data: format(total | ||||
| 						? raw.remote.total | ||||
| 						: sum(raw.remote.inc, negate(raw.remote.dec)) | ||||
| 					), | ||||
| 				}], | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchActiveUsersChart = async (): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Combined', | ||||
| 					type: 'line', | ||||
| 					data: format(sum(raw.local.users, raw.remote.users)), | ||||
| 				}, { | ||||
| 					name: 'Local', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.local.users), | ||||
| 				}, { | ||||
| 					name: 'Remote', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.remote.users), | ||||
| 				}], | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchDriveChart = async (): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				bytes: true, | ||||
| 				series: [{ | ||||
| 					name: 'All', | ||||
| 					type: 'line', | ||||
| 					borderDash: [5, 5], | ||||
| 					data: format( | ||||
| 						sum( | ||||
| 							raw.local.incSize, | ||||
| 							negate(raw.local.decSize), | ||||
| 							raw.remote.incSize, | ||||
| 							negate(raw.remote.decSize) | ||||
| 						) | ||||
| 					), | ||||
| 				}, { | ||||
| 					name: 'Local +', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.local.incSize), | ||||
| 				}, { | ||||
| 					name: 'Local -', | ||||
| 					type: 'area', | ||||
| 					data: format(negate(raw.local.decSize)), | ||||
| 				}, { | ||||
| 					name: 'Remote +', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.remote.incSize), | ||||
| 				}, { | ||||
| 					name: 'Remote -', | ||||
| 					type: 'area', | ||||
| 					data: format(negate(raw.remote.decSize)), | ||||
| 				}], | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchDriveTotalChart = async (): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				bytes: true, | ||||
| 				series: [{ | ||||
| 					name: 'Combined', | ||||
| 					type: 'line', | ||||
| 					data: format(sum(raw.local.totalSize, raw.remote.totalSize)), | ||||
| 				}, { | ||||
| 					name: 'Local', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.local.totalSize), | ||||
| 				}, { | ||||
| 					name: 'Remote', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.remote.totalSize), | ||||
| 				}], | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchDriveFilesChart = async (): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'All', | ||||
| 					type: 'line', | ||||
| 					borderDash: [5, 5], | ||||
| 					data: format( | ||||
| 						sum( | ||||
| 							raw.local.incCount, | ||||
| 							negate(raw.local.decCount), | ||||
| 							raw.remote.incCount, | ||||
| 							negate(raw.remote.decCount) | ||||
| 						) | ||||
| 					), | ||||
| 				}, { | ||||
| 					name: 'Local +', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.local.incCount), | ||||
| 				}, { | ||||
| 					name: 'Local -', | ||||
| 					type: 'area', | ||||
| 					data: format(negate(raw.local.decCount)), | ||||
| 				}, { | ||||
| 					name: 'Remote +', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.remote.incCount), | ||||
| 				}, { | ||||
| 					name: 'Remote -', | ||||
| 					type: 'area', | ||||
| 					data: format(negate(raw.remote.decCount)), | ||||
| 				}], | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchDriveFilesTotalChart = async (): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Combined', | ||||
| 					type: 'line', | ||||
| 					data: format(sum(raw.local.totalCount, raw.remote.totalCount)), | ||||
| 				}, { | ||||
| 					name: 'Local', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.local.totalCount), | ||||
| 				}, { | ||||
| 					name: 'Remote', | ||||
| 					type: 'area', | ||||
| 					data: format(raw.remote.totalCount), | ||||
| 				}], | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchInstanceRequestsChart = async (): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'In', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					data: format(raw.requests.received) | ||||
| 				}, { | ||||
| 					name: 'Out (succ)', | ||||
| 					type: 'area', | ||||
| 					color: '#00E396', | ||||
| 					data: format(raw.requests.succeeded) | ||||
| 				}, { | ||||
| 					name: 'Out (fail)', | ||||
| 					type: 'area', | ||||
| 					color: '#FEB019', | ||||
| 					data: format(raw.requests.failed) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchInstanceUsersChart = async (total: boolean): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Users', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					data: format(total | ||||
| 						? raw.users.total | ||||
| 						: sum(raw.users.inc, negate(raw.users.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchInstanceNotesChart = async (total: boolean): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Notes', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					data: format(total | ||||
| 						? raw.notes.total | ||||
| 						: sum(raw.notes.inc, negate(raw.notes.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchInstanceFfChart = async (total: boolean): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Following', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					data: format(total | ||||
| 						? raw.following.total | ||||
| 						: sum(raw.following.inc, negate(raw.following.dec)) | ||||
| 					) | ||||
| 				}, { | ||||
| 					name: 'Followers', | ||||
| 					type: 'area', | ||||
| 					color: '#00E396', | ||||
| 					data: format(total | ||||
| 						? raw.followers.total | ||||
| 						: sum(raw.followers.inc, negate(raw.followers.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				bytes: true, | ||||
| 				series: [{ | ||||
| 					name: 'Drive usage', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					data: format(total | ||||
| 						? raw.drive.totalUsage | ||||
| 						: sum(raw.drive.incUsage, negate(raw.drive.decUsage)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof data> => { | ||||
| 			const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Drive files', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					data: format(total | ||||
| 						? raw.drive.totalFiles | ||||
| 						: sum(raw.drive.incFiles, negate(raw.drive.decFiles)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		const fetchAndRender = async () => { | ||||
| 			const fetchData = () => { | ||||
| 				switch (props.src) { | ||||
| 					case 'federation-instances': return fetchFederationInstancesChart(false); | ||||
| 					case 'federation-instances-total': return fetchFederationInstancesChart(true); | ||||
| 					case 'users': return fetchUsersChart(false); | ||||
| 					case 'users-total': return fetchUsersChart(true); | ||||
| 					case 'active-users': return fetchActiveUsersChart(); | ||||
| 					case 'notes': return fetchNotesChart('combined'); | ||||
| 					case 'local-notes': return fetchNotesChart('local'); | ||||
| 					case 'remote-notes': return fetchNotesChart('remote'); | ||||
| 					case 'notes-total': return fetchNotesTotalChart(); | ||||
| 					case 'drive': return fetchDriveChart(); | ||||
| 					case 'drive-total': return fetchDriveTotalChart(); | ||||
| 					case 'drive-files': return fetchDriveFilesChart(); | ||||
| 					case 'drive-files-total': return fetchDriveFilesTotalChart(); | ||||
| 					 | ||||
| 					case 'instance-requests': return fetchInstanceRequestsChart(); | ||||
| 					case 'instance-users': return fetchInstanceUsersChart(false); | ||||
| 					case 'instance-users-total': return fetchInstanceUsersChart(true); | ||||
| 					case 'instance-notes': return fetchInstanceNotesChart(false); | ||||
| 					case 'instance-notes-total': return fetchInstanceNotesChart(true); | ||||
| 					case 'instance-ff': return fetchInstanceFfChart(false); | ||||
| 					case 'instance-ff-total': return fetchInstanceFfChart(true); | ||||
| 					case 'instance-drive-usage': return fetchInstanceDriveUsageChart(false); | ||||
| 					case 'instance-drive-usage-total': return fetchInstanceDriveUsageChart(true); | ||||
| 					case 'instance-drive-files': return fetchInstanceDriveFilesChart(false); | ||||
| 					case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true); | ||||
| 				} | ||||
| 			}; | ||||
| 			fetching.value = true; | ||||
| 			data = await fetchData(); | ||||
| 			fetching.value = false; | ||||
| 			render(); | ||||
| 		}; | ||||
| 
 | ||||
| 		watch(() => [props.src, props.span], fetchAndRender); | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			fetchAndRender(); | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			chartEl, | ||||
| 		}; | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
|  | @ -33,7 +33,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; | ||||
| import MkButton from '../ui/button.vue'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import { debounce } from 'throttle-debounce'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|  |  | |||
|  | @ -22,7 +22,6 @@ export default defineComponent({ | |||
| 		} | ||||
| 	}, | ||||
| 	render() { | ||||
| 		const label = this.$slots.desc(); | ||||
| 		let options = this.$slots.default(); | ||||
| 
 | ||||
| 		// なぜかFragmentになることがあるため | ||||
|  | @ -31,7 +30,6 @@ export default defineComponent({ | |||
| 		return h('div', { | ||||
| 			class: 'novjtcto' | ||||
| 		}, [ | ||||
| 			h('div', { class: 'label' }, label), | ||||
| 			...options.map(option => h(MkRadio, { | ||||
| 				key: option.key, | ||||
| 				value: option.props.value, | ||||
|  | @ -45,16 +43,6 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss"> | ||||
| .novjtcto { | ||||
| 	> .label { | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 0 0 8px 12px; | ||||
| 		user-select: none; | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			display: none; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&:first-child { | ||||
| 		margin-top: 0; | ||||
| 	} | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| <template> | ||||
| <div class="vblkjoeq"> | ||||
| 	<div class="label" @click="focus"><slot name="label"></slot></div> | ||||
| 	<div class="input" :class="{ inline, disabled, focused }"> | ||||
| 	<div class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick" ref="container"> | ||||
| 		<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div> | ||||
| 		<select ref="inputEl" | ||||
| 		<select class="select" ref="inputEl" | ||||
| 			v-model="v" | ||||
| 			:disabled="disabled" | ||||
| 			:required="required" | ||||
|  | @ -25,7 +25,8 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; | ||||
| import MkButton from '../ui/button.vue'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import * as os from '@client/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
|  | @ -81,6 +82,7 @@ export default defineComponent({ | |||
| 		const inputEl = ref(null); | ||||
| 		const prefixEl = ref(null); | ||||
| 		const suffixEl = ref(null); | ||||
| 		const container = ref(null); | ||||
| 
 | ||||
| 		const focus = () => inputEl.value.focus(); | ||||
| 		const onInput = (ev) => { | ||||
|  | @ -132,6 +134,47 @@ export default defineComponent({ | |||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		const onClick = (ev: MouseEvent) => { | ||||
| 			focused.value = true; | ||||
| 
 | ||||
| 			const menu = []; | ||||
| 			let options = context.slots.default(); | ||||
| 
 | ||||
| 			for (const optionOrOptgroup of options) { | ||||
| 				if (optionOrOptgroup.type === 'optgroup') { | ||||
| 					const optgroup = optionOrOptgroup; | ||||
| 					menu.push({ | ||||
| 						type: 'label', | ||||
| 						text: optgroup.props.label, | ||||
| 					}); | ||||
| 					for (const option of optgroup.children) { | ||||
| 						menu.push({ | ||||
| 							text: option.children, | ||||
| 							active: v.value === option.props.value, | ||||
| 							action: () => { | ||||
| 								v.value = option.props.value; | ||||
| 							}, | ||||
| 						}); | ||||
| 					} | ||||
| 				} else { | ||||
| 					const option = optionOrOptgroup; | ||||
| 					menu.push({ | ||||
| 						text: option.children, | ||||
| 						active: v.value === option.props.value, | ||||
| 						action: () => { | ||||
| 							v.value = option.props.value; | ||||
| 						}, | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			os.popupMenu(menu, container.value, { | ||||
| 				width: container.value.offsetWidth, | ||||
| 			}).then(() => { | ||||
| 				focused.value = false; | ||||
| 			}); | ||||
| 		}; | ||||
| 
 | ||||
| 		return { | ||||
| 			v, | ||||
| 			focused, | ||||
|  | @ -141,8 +184,10 @@ export default defineComponent({ | |||
| 			inputEl, | ||||
| 			prefixEl, | ||||
| 			suffixEl, | ||||
| 			container, | ||||
| 			focus, | ||||
| 			onInput, | ||||
| 			onClick, | ||||
| 			updated, | ||||
| 		}; | ||||
| 	}, | ||||
|  | @ -174,8 +219,15 @@ export default defineComponent({ | |||
| 	> .input { | ||||
| 		$height: 42px; | ||||
| 		position: relative; | ||||
| 		cursor: pointer; | ||||
| 
 | ||||
| 		> select { | ||||
| 		&:hover { | ||||
| 			> .select { | ||||
| 				border-color: var(--inputBorderHover); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .select { | ||||
| 			appearance: none; | ||||
| 			-webkit-appearance: none; | ||||
| 			display: block; | ||||
|  | @ -195,10 +247,7 @@ export default defineComponent({ | |||
| 			box-sizing: border-box; | ||||
| 			cursor: pointer; | ||||
| 			transition: border-color 0.1s ease-out; | ||||
| 
 | ||||
| 			&:hover { | ||||
| 				border-color: var(--inputBorderHover); | ||||
| 			} | ||||
| 			pointer-events: none; | ||||
| 		} | ||||
| 
 | ||||
| 		> .prefix, | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; | ||||
| import MkButton from '../ui/button.vue'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import { debounce } from 'throttle-debounce'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|  |  | |||
|  | @ -27,8 +27,7 @@ export default defineComponent({ | |||
| 			default: false | ||||
| 		}, | ||||
| 		customEmojis: { | ||||
| 			required: false, | ||||
| 			default: () => [] | ||||
| 			required: false | ||||
| 		}, | ||||
| 		isReaction: { | ||||
| 			type: Boolean, | ||||
|  | @ -58,10 +57,7 @@ export default defineComponent({ | |||
| 		}, | ||||
| 
 | ||||
| 		ce() { | ||||
| 			let ce = []; | ||||
| 			if (this.customEmojis) ce = ce.concat(this.customEmojis); | ||||
| 			if (this.$instance && this.$instance.emojis) ce = ce.concat(this.$instance.emojis); | ||||
| 			return ce; | ||||
| 			return this.customEmojis || this.$instance?.emojis || []; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -203,6 +203,12 @@ export default defineComponent({ | |||
| 
 | ||||
| 	&.thin { | ||||
| 		--height: 50px; | ||||
| 
 | ||||
| 		> .buttons { | ||||
| 			> .button { | ||||
| 				font-size: 0.9em; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.slim { | ||||
|  |  | |||
|  | @ -24,35 +24,26 @@ | |||
| 				<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option> | ||||
| 			</optgroup> | ||||
| 		</MkSelect> | ||||
| 		<MkSelect v-model="chartSpan" style="margin: 0;"> | ||||
| 		<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;"> | ||||
| 			<option value="hour">{{ $ts.perHour }}</option> | ||||
| 			<option value="day">{{ $ts.perDay }}</option> | ||||
| 		</MkSelect> | ||||
| 	</div> | ||||
| 	<canvas ref="chart"></canvas> | ||||
| 	<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, markRaw } from 'vue'; | ||||
| import Chart from 'chart.js'; | ||||
| import MkSelect from './form/select.vue'; | ||||
| import number from '@client/filters/number'; | ||||
| 
 | ||||
| const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); | ||||
| const negate = arr => arr.map(x => -x); | ||||
| const alpha = (hex, a) => { | ||||
| 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; | ||||
| 	const r = parseInt(result[1], 16); | ||||
| 	const g = parseInt(result[2], 16); | ||||
| 	const b = parseInt(result[3], 16); | ||||
| 	return `rgba(${r}, ${g}, ${b}, ${a})`; | ||||
| }; | ||||
| import { defineComponent, onMounted, ref, watch } from 'vue'; | ||||
| import MkSelect from '@client/components/form/select.vue'; | ||||
| import MkChart from '@client/components/chart.vue'; | ||||
| import * as os from '@client/os'; | ||||
| import { defaultStore } from '@client/store'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkSelect | ||||
| 		MkSelect, | ||||
| 		MkChart, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
|  | @ -68,463 +59,15 @@ export default defineComponent({ | |||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 	setup() { | ||||
| 		const chartSpan = ref<'hour' | 'day'>('hour'); | ||||
| 		const chartSrc = ref('notes'); | ||||
| 
 | ||||
| 		return { | ||||
| 			notesLocalWoW: 0, | ||||
| 			notesLocalDoD: 0, | ||||
| 			notesRemoteWoW: 0, | ||||
| 			notesRemoteDoD: 0, | ||||
| 			usersLocalWoW: 0, | ||||
| 			usersLocalDoD: 0, | ||||
| 			usersRemoteWoW: 0, | ||||
| 			usersRemoteDoD: 0, | ||||
| 			now: null, | ||||
| 			chart: null, | ||||
| 			chartInstance: null, | ||||
| 			chartSrc: 'notes', | ||||
| 			chartSpan: 'hour', | ||||
| 		} | ||||
| 			chartSrc, | ||||
| 			chartSpan, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		data(): any { | ||||
| 			if (this.chart == null) return null; | ||||
| 			switch (this.chartSrc) { | ||||
| 				case 'federation-instances': return this.federationInstancesChart(false); | ||||
| 				case 'federation-instances-total': return this.federationInstancesChart(true); | ||||
| 				case 'users': return this.usersChart(false); | ||||
| 				case 'users-total': return this.usersChart(true); | ||||
| 				case 'active-users': return this.activeUsersChart(); | ||||
| 				case 'notes': return this.notesChart('combined'); | ||||
| 				case 'local-notes': return this.notesChart('local'); | ||||
| 				case 'remote-notes': return this.notesChart('remote'); | ||||
| 				case 'notes-total': return this.notesTotalChart(); | ||||
| 				case 'drive': return this.driveChart(); | ||||
| 				case 'drive-total': return this.driveTotalChart(); | ||||
| 				case 'drive-files': return this.driveFilesChart(); | ||||
| 				case 'drive-files-total': return this.driveFilesTotalChart(); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		stats(): any[] { | ||||
| 			const stats = | ||||
| 				this.chartSpan == 'day' ? this.chart.perDay : | ||||
| 				this.chartSpan == 'hour' ? this.chart.perHour : | ||||
| 				null; | ||||
| 
 | ||||
| 			return stats; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		chartSrc() { | ||||
| 			this.renderChart(); | ||||
| 		}, | ||||
| 
 | ||||
| 		chartSpan() { | ||||
| 			this.renderChart(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	async created() { | ||||
| 		this.now = new Date(); | ||||
| 
 | ||||
| 		this.fetchChart(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async fetchChart() { | ||||
| 			const [perHour, perDay] = await Promise.all([Promise.all([ | ||||
| 				os.api('charts/federation', { limit: this.chartLimit, span: 'hour' }), | ||||
| 				os.api('charts/users', { limit: this.chartLimit, span: 'hour' }), | ||||
| 				os.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }), | ||||
| 				os.api('charts/notes', { limit: this.chartLimit, span: 'hour' }), | ||||
| 				os.api('charts/drive', { limit: this.chartLimit, span: 'hour' }), | ||||
| 			]), Promise.all([ | ||||
| 				os.api('charts/federation', { limit: this.chartLimit, span: 'day' }), | ||||
| 				os.api('charts/users', { limit: this.chartLimit, span: 'day' }), | ||||
| 				os.api('charts/active-users', { limit: this.chartLimit, span: 'day' }), | ||||
| 				os.api('charts/notes', { limit: this.chartLimit, span: 'day' }), | ||||
| 				os.api('charts/drive', { limit: this.chartLimit, span: 'day' }), | ||||
| 			])]); | ||||
| 
 | ||||
| 			const chart = { | ||||
| 				perHour: { | ||||
| 					federation: perHour[0], | ||||
| 					users: perHour[1], | ||||
| 					activeUsers: perHour[2], | ||||
| 					notes: perHour[3], | ||||
| 					drive: perHour[4], | ||||
| 				}, | ||||
| 				perDay: { | ||||
| 					federation: perDay[0], | ||||
| 					users: perDay[1], | ||||
| 					activeUsers: perDay[2], | ||||
| 					notes: perDay[3], | ||||
| 					drive: perDay[4], | ||||
| 				} | ||||
| 			}; | ||||
| 
 | ||||
| 			this.chart = chart; | ||||
| 
 | ||||
| 			this.renderChart(); | ||||
| 		}, | ||||
| 
 | ||||
| 		renderChart() { | ||||
| 			if (this.chartInstance) { | ||||
| 				this.chartInstance.destroy(); | ||||
| 			} | ||||
| 
 | ||||
| 			// TODO: var(--panel)の色が暗いか明るいかで判定する | ||||
| 			const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; | ||||
| 
 | ||||
| 			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||
| 			this.chartInstance = markRaw(new Chart(this.$refs.chart, { | ||||
| 				type: 'line', | ||||
| 				data: { | ||||
| 					labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), | ||||
| 					datasets: this.data.series.map(x => ({ | ||||
| 						label: x.name, | ||||
| 						data: x.data.slice().reverse(), | ||||
| 						pointRadius: 0, | ||||
| 						lineTension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: x.color, | ||||
| 						borderDash: x.borderDash || [], | ||||
| 						backgroundColor: alpha(x.color, 0.1), | ||||
| 						fill: x.fill == null ? true : x.fill, | ||||
| 						hidden: !!x.hidden | ||||
| 					})) | ||||
| 				}, | ||||
| 				options: { | ||||
| 					aspectRatio: 2.5, | ||||
| 					layout: { | ||||
| 						padding: { | ||||
| 							left: 16, | ||||
| 							right: 16, | ||||
| 							top: 16, | ||||
| 							bottom: 8 | ||||
| 						} | ||||
| 					}, | ||||
| 					legend: { | ||||
| 						position: 'bottom', | ||||
| 						labels: { | ||||
| 							boxWidth: 16, | ||||
| 						} | ||||
| 					}, | ||||
| 					scales: { | ||||
| 						xAxes: [{ | ||||
| 							type: 'time', | ||||
| 							time: { | ||||
| 								stepSize: 1, | ||||
| 								unit: this.chartSpan == 'day' ? 'month' : 'day', | ||||
| 							}, | ||||
| 							gridLines: { | ||||
| 								display: this.detailed, | ||||
| 								color: gridColor, | ||||
| 								zeroLineColor: gridColor, | ||||
| 							}, | ||||
| 							ticks: { | ||||
| 								display: this.detailed | ||||
| 							} | ||||
| 						}], | ||||
| 						yAxes: [{ | ||||
| 							position: 'left', | ||||
| 							gridLines: { | ||||
| 								color: gridColor, | ||||
| 								zeroLineColor: gridColor, | ||||
| 							}, | ||||
| 							ticks: { | ||||
| 								display: this.detailed | ||||
| 							} | ||||
| 						}] | ||||
| 					}, | ||||
| 					tooltips: { | ||||
| 						intersect: false, | ||||
| 						mode: 'index', | ||||
| 					} | ||||
| 				} | ||||
| 			})); | ||||
| 		}, | ||||
| 
 | ||||
| 		getDate(ago: number) { | ||||
| 			const y = this.now.getFullYear(); | ||||
| 			const m = this.now.getMonth(); | ||||
| 			const d = this.now.getDate(); | ||||
| 			const h = this.now.getHours(); | ||||
| 
 | ||||
| 			return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); | ||||
| 		}, | ||||
| 
 | ||||
| 		format(arr) { | ||||
| 			const now = Date.now(); | ||||
| 			return arr.map((v, i) => ({ | ||||
| 				x: new Date(now - ((this.chartSpan == 'day' ? 86400000 :3600000 ) * i)), | ||||
| 				y: v | ||||
| 			})); | ||||
| 		}, | ||||
| 
 | ||||
| 		federationInstancesChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Instances', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.federation.instance.total | ||||
| 						: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		notesChart(type: string): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'All', | ||||
| 					type: 'line', | ||||
| 					color: '#008FFB', | ||||
| 					borderDash: [5, 5], | ||||
| 					fill: false, | ||||
| 					data: this.format(type == 'combined' | ||||
| 						? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec)) | ||||
| 						: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec)) | ||||
| 					) | ||||
| 				}, { | ||||
| 					name: 'Renotes', | ||||
| 					type: 'area', | ||||
| 					color: '#00E396', | ||||
| 					data: this.format(type == 'combined' | ||||
| 						? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote) | ||||
| 						: this.stats.notes[type].diffs.renote | ||||
| 					) | ||||
| 				}, { | ||||
| 					name: 'Replies', | ||||
| 					type: 'area', | ||||
| 					color: '#FEB019', | ||||
| 					data: this.format(type == 'combined' | ||||
| 						? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply) | ||||
| 						: this.stats.notes[type].diffs.reply | ||||
| 					) | ||||
| 				}, { | ||||
| 					name: 'Normal', | ||||
| 					type: 'area', | ||||
| 					color: '#FF4560', | ||||
| 					data: this.format(type == 'combined' | ||||
| 						? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal) | ||||
| 						: this.stats.notes[type].diffs.normal | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		notesTotalChart(): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Combined', | ||||
| 					type: 'line', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total)) | ||||
| 				}, { | ||||
| 					name: 'Local', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					hidden: true, | ||||
| 					data: this.format(this.stats.notes.local.total) | ||||
| 				}, { | ||||
| 					name: 'Remote', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					hidden: true, | ||||
| 					data: this.format(this.stats.notes.remote.total) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		usersChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Combined', | ||||
| 					type: 'line', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? sum(this.stats.users.local.total, this.stats.users.remote.total) | ||||
| 						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec)) | ||||
| 					) | ||||
| 				}, { | ||||
| 					name: 'Local', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					hidden: true, | ||||
| 					data: this.format(total | ||||
| 						? this.stats.users.local.total | ||||
| 						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec)) | ||||
| 					) | ||||
| 				}, { | ||||
| 					name: 'Remote', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					hidden: true, | ||||
| 					data: this.format(total | ||||
| 						? this.stats.users.remote.total | ||||
| 						: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		activeUsersChart(): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Combined', | ||||
| 					type: 'line', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count)) | ||||
| 				}, { | ||||
| 					name: 'Local', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					hidden: true, | ||||
| 					data: this.format(this.stats.activeUsers.local.count) | ||||
| 				}, { | ||||
| 					name: 'Remote', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					hidden: true, | ||||
| 					data: this.format(this.stats.activeUsers.remote.count) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		driveChart(): any { | ||||
| 			return { | ||||
| 				bytes: true, | ||||
| 				series: [{ | ||||
| 					name: 'All', | ||||
| 					type: 'line', | ||||
| 					color: '#09d8e2', | ||||
| 					borderDash: [5, 5], | ||||
| 					fill: false, | ||||
| 					data: this.format( | ||||
| 						sum( | ||||
| 							this.stats.drive.local.incSize, | ||||
| 							negate(this.stats.drive.local.decSize), | ||||
| 							this.stats.drive.remote.incSize, | ||||
| 							negate(this.stats.drive.remote.decSize) | ||||
| 						) | ||||
| 					) | ||||
| 				}, { | ||||
| 					name: 'Local +', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(this.stats.drive.local.incSize) | ||||
| 				}, { | ||||
| 					name: 'Local -', | ||||
| 					type: 'area', | ||||
| 					color: '#FF4560', | ||||
| 					data: this.format(negate(this.stats.drive.local.decSize)) | ||||
| 				}, { | ||||
| 					name: 'Remote +', | ||||
| 					type: 'area', | ||||
| 					color: '#00E396', | ||||
| 					data: this.format(this.stats.drive.remote.incSize) | ||||
| 				}, { | ||||
| 					name: 'Remote -', | ||||
| 					type: 'area', | ||||
| 					color: '#FEB019', | ||||
| 					data: this.format(negate(this.stats.drive.remote.decSize)) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		driveTotalChart(): any { | ||||
| 			return { | ||||
| 				bytes: true, | ||||
| 				series: [{ | ||||
| 					name: 'Combined', | ||||
| 					type: 'line', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize)) | ||||
| 				}, { | ||||
| 					name: 'Local', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					hidden: true, | ||||
| 					data: this.format(this.stats.drive.local.totalSize) | ||||
| 				}, { | ||||
| 					name: 'Remote', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					hidden: true, | ||||
| 					data: this.format(this.stats.drive.remote.totalSize) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		driveFilesChart(): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'All', | ||||
| 					type: 'line', | ||||
| 					color: '#09d8e2', | ||||
| 					borderDash: [5, 5], | ||||
| 					fill: false, | ||||
| 					data: this.format( | ||||
| 						sum( | ||||
| 							this.stats.drive.local.incCount, | ||||
| 							negate(this.stats.drive.local.decCount), | ||||
| 							this.stats.drive.remote.incCount, | ||||
| 							negate(this.stats.drive.remote.decCount) | ||||
| 						) | ||||
| 					) | ||||
| 				}, { | ||||
| 					name: 'Local +', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(this.stats.drive.local.incCount) | ||||
| 				}, { | ||||
| 					name: 'Local -', | ||||
| 					type: 'area', | ||||
| 					color: '#FF4560', | ||||
| 					data: this.format(negate(this.stats.drive.local.decCount)) | ||||
| 				}, { | ||||
| 					name: 'Remote +', | ||||
| 					type: 'area', | ||||
| 					color: '#00E396', | ||||
| 					data: this.format(this.stats.drive.remote.incCount) | ||||
| 				}, { | ||||
| 					name: 'Remote -', | ||||
| 					type: 'area', | ||||
| 					color: '#FEB019', | ||||
| 					data: this.format(negate(this.stats.drive.remote.decCount)) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		driveFilesTotalChart(): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Combined', | ||||
| 					type: 'line', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount)) | ||||
| 				}, { | ||||
| 					name: 'Local', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					hidden: true, | ||||
| 					data: this.format(this.stats.drive.local.totalCount) | ||||
| 				}, { | ||||
| 					name: 'Remote', | ||||
| 					type: 'area', | ||||
| 					color: '#008FFB', | ||||
| 					hidden: true, | ||||
| 					data: this.format(this.stats.drive.remote.totalCount) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		number | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -185,7 +185,7 @@ export default defineComponent({ | |||
| 						} | ||||
| 					} | ||||
| 					if (style == null) { | ||||
| 						return h('span', {}, ['[', token.props.name, ' ', ...genEl(token.children), ']']); | ||||
| 						return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']); | ||||
| 					} else { | ||||
| 						return h('span', { | ||||
| 							style: 'display: inline-block;' + style, | ||||
|  |  | |||
							
								
								
									
										47
									
								
								src/client/components/number-diff.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/client/components/number-diff.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | |||
| <template> | ||||
| <span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }"> | ||||
| 	<slot name="before"></slot>{{ isPlus ? '+' : '' }}{{ number(value) }}<slot name="after"></slot> | ||||
| </span> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| import number from '@client/filters/number'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		value: { | ||||
| 			type: Number, | ||||
| 			required: true | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	setup(props) { | ||||
| 		const isPlus = computed(() => props.value > 0); | ||||
| 		const isMinus = computed(() => props.value < 0); | ||||
| 		const isZero = computed(() => props.value === 0); | ||||
| 		return { | ||||
| 			isPlus, | ||||
| 			isMinus, | ||||
| 			isZero, | ||||
| 			number, | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .ceaaebcd { | ||||
| 	&.isPlus { | ||||
| 		color: var(--success); | ||||
| 	} | ||||
| 
 | ||||
| 	&.isMinus { | ||||
| 		color: var(--error); | ||||
| 	} | ||||
| 
 | ||||
| 	&.isZero { | ||||
| 		opacity: 0.5; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -117,11 +117,28 @@ export default defineComponent({ | |||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		initialVisibility: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		initialFiles: { | ||||
| 			type: Array, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		initialLocalOnly: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		visibleUsers: { | ||||
| 			type: Array, | ||||
| 			required: false, | ||||
| 			default: () => [] | ||||
| 		}, | ||||
| 		initialNote: { | ||||
| 			type: Object, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		instant: { | ||||
| 		share: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
|  | @ -150,8 +167,7 @@ export default defineComponent({ | |||
| 			showPreview: false, | ||||
| 			cw: null, | ||||
| 			localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly, | ||||
| 			visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility, | ||||
| 			visibleUsers: [], | ||||
| 			visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number], | ||||
| 			autocomplete: null, | ||||
| 			draghover: false, | ||||
| 			quoteId: null, | ||||
|  | @ -246,6 +262,18 @@ export default defineComponent({ | |||
| 			this.text = this.initialText; | ||||
| 		} | ||||
| 
 | ||||
| 		if (this.initialVisibility) { | ||||
| 			this.visibility = this.initialVisibility; | ||||
| 		} | ||||
| 
 | ||||
| 		if (this.initialFiles) { | ||||
| 			this.files = this.initialFiles; | ||||
| 		} | ||||
| 
 | ||||
| 		if (typeof this.initialLocalOnly === 'boolean') { | ||||
| 			this.localOnly = this.initialLocalOnly; | ||||
| 		} | ||||
| 
 | ||||
| 		if (this.mention) { | ||||
| 			this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`; | ||||
| 			this.text += ' '; | ||||
|  | @ -321,7 +349,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 		this.$nextTick(() => { | ||||
| 			// 書きかけの投稿を復元 | ||||
| 			if (!this.instant && !this.mention && !this.specified) { | ||||
| 			if (!this.share && !this.mention && !this.specified) { | ||||
| 				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey]; | ||||
| 				if (draft) { | ||||
| 					this.text = draft.data.text; | ||||
|  | @ -582,8 +610,6 @@ export default defineComponent({ | |||
| 		}, | ||||
| 
 | ||||
| 		saveDraft() { | ||||
| 			if (this.instant) return; | ||||
| 
 | ||||
| 			const data = JSON.parse(localStorage.getItem('drafts') || '{}'); | ||||
| 
 | ||||
| 			data[this.draftKey] = { | ||||
|  |  | |||
							
								
								
									
										212
									
								
								src/client/components/queue-chart.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								src/client/components/queue-chart.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,212 @@ | |||
| <template> | ||||
| <canvas ref="chartEl"></canvas> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, onMounted, onUnmounted, ref } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| } from 'chart.js'; | ||||
| import number from '@client/filters/number'; | ||||
| import * as os from '@client/os'; | ||||
| import { defaultStore } from '@client/store'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
| 	LineElement, | ||||
| 	BarElement, | ||||
| 	PointElement, | ||||
| 	BarController, | ||||
| 	LineController, | ||||
| 	CategoryScale, | ||||
| 	LinearScale, | ||||
| 	TimeScale, | ||||
| 	Legend, | ||||
| 	Title, | ||||
| 	Tooltip, | ||||
| 	SubTitle, | ||||
| 	Filler, | ||||
| ); | ||||
| 
 | ||||
| const alpha = (hex, a) => { | ||||
| 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; | ||||
| 	const r = parseInt(result[1], 16); | ||||
| 	const g = parseInt(result[2], 16); | ||||
| 	const b = parseInt(result[3], 16); | ||||
| 	return `rgba(${r}, ${g}, ${b}, ${a})`; | ||||
| }; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		domain: { | ||||
| 			type: String, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		connection: { | ||||
| 			required: true, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	setup(props) { | ||||
| 		const chartEl = ref<HTMLCanvasElement>(null); | ||||
| 
 | ||||
| 		const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; | ||||
| 
 | ||||
| 		// フォントカラー | ||||
| 		Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			const chartInstance = new Chart(chartEl.value, { | ||||
| 				type: 'line', | ||||
| 				data: { | ||||
| 					labels: [], | ||||
| 					datasets: [{ | ||||
| 						label: 'Process', | ||||
| 						pointRadius: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: '#00E396', | ||||
| 						backgroundColor: alpha('#00E396', 0.1), | ||||
| 						data: [] | ||||
| 					}, { | ||||
| 						label: 'Active', | ||||
| 						pointRadius: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: '#00BCD4', | ||||
| 						backgroundColor: alpha('#00BCD4', 0.1), | ||||
| 						data: [] | ||||
| 					}, { | ||||
| 						label: 'Waiting', | ||||
| 						pointRadius: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: '#FFB300', | ||||
| 						backgroundColor: alpha('#FFB300', 0.1), | ||||
| 						data: [] | ||||
| 					}, { | ||||
| 						label: 'Delayed', | ||||
| 						pointRadius: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: '#E53935', | ||||
| 						borderDash: [5, 5], | ||||
| 						fill: false, | ||||
| 						data: [] | ||||
| 					}], | ||||
| 				}, | ||||
| 				options: { | ||||
| 					aspectRatio: 2.5, | ||||
| 					layout: { | ||||
| 						padding: { | ||||
| 							left: 16, | ||||
| 							right: 16, | ||||
| 							top: 16, | ||||
| 							bottom: 8, | ||||
| 						}, | ||||
| 					}, | ||||
| 					scales: { | ||||
| 						x: { | ||||
| 							grid: { | ||||
| 								display: false, | ||||
| 								color: gridColor, | ||||
| 								borderColor: 'rgb(0, 0, 0, 0)', | ||||
| 							}, | ||||
| 							ticks: { | ||||
| 								display: false, | ||||
| 							}, | ||||
| 						}, | ||||
| 						y: { | ||||
| 							grid: { | ||||
| 								color: gridColor, | ||||
| 								borderColor: 'rgb(0, 0, 0, 0)', | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 					interaction: { | ||||
| 						intersect: false, | ||||
| 					}, | ||||
| 					plugins: { | ||||
| 						legend: { | ||||
| 							position: 'bottom', | ||||
| 							labels: { | ||||
| 								boxWidth: 16, | ||||
| 							}, | ||||
| 						}, | ||||
| 						tooltip: { | ||||
| 							mode: 'index', | ||||
| 							animation: { | ||||
| 								duration: 0, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}); | ||||
| 
 | ||||
| 			const onStats = (stats) => { | ||||
| 				chartInstance.data.labels.push(''); | ||||
| 				chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick); | ||||
| 				chartInstance.data.datasets[1].data.push(stats[props.domain].active); | ||||
| 				chartInstance.data.datasets[2].data.push(stats[props.domain].waiting); | ||||
| 				chartInstance.data.datasets[3].data.push(stats[props.domain].delayed); | ||||
| 				if (chartInstance.data.datasets[0].data.length > 200) { | ||||
| 					chartInstance.data.labels.shift(); | ||||
| 					chartInstance.data.datasets[0].data.shift(); | ||||
| 					chartInstance.data.datasets[1].data.shift(); | ||||
| 					chartInstance.data.datasets[2].data.shift(); | ||||
| 					chartInstance.data.datasets[3].data.shift(); | ||||
| 				} | ||||
| 				chartInstance.update(); | ||||
| 			}; | ||||
| 
 | ||||
| 			const onStatsLog = (statsLog) => { | ||||
| 				for (const stats of [...statsLog].reverse()) { | ||||
| 					chartInstance.data.labels.push(''); | ||||
| 					chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick); | ||||
| 					chartInstance.data.datasets[1].data.push(stats[props.domain].active); | ||||
| 					chartInstance.data.datasets[2].data.push(stats[props.domain].waiting); | ||||
| 					chartInstance.data.datasets[3].data.push(stats[props.domain].delayed); | ||||
| 					if (chartInstance.data.datasets[0].data.length > 200) { | ||||
| 						chartInstance.data.labels.shift(); | ||||
| 						chartInstance.data.datasets[0].data.shift(); | ||||
| 						chartInstance.data.datasets[1].data.shift(); | ||||
| 						chartInstance.data.datasets[2].data.shift(); | ||||
| 						chartInstance.data.datasets[3].data.shift(); | ||||
| 					} | ||||
| 				} | ||||
| 				chartInstance.update(); | ||||
| 			}; | ||||
| 
 | ||||
| 			props.connection.on('stats', onStats); | ||||
| 			props.connection.on('statsLog', onStatsLog); | ||||
| 
 | ||||
| 			onUnmounted(() => { | ||||
| 				props.connection.off('stats', onStats); | ||||
| 				props.connection.off('statsLog', onStatsLog); | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			chartEl, | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| 
 | ||||
| </style> | ||||
|  | @ -36,7 +36,7 @@ export default defineComponent({ | |||
| 	> button { | ||||
| 		flex: 1; | ||||
| 		padding: 10px 8px; | ||||
| 		border-radius: 6px; | ||||
| 		border-radius: var(--radius); | ||||
| 
 | ||||
| 		&:disabled { | ||||
| 			opacity: 1 !important; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <div class="ukygtjoj _block" :class="{ naked, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }"> | ||||
| <div class="ukygtjoj _panel" :class="{ naked, thin, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }"> | ||||
| 	<header v-if="showHeader" ref="header"> | ||||
| 		<div class="title"><slot name="header"></slot></div> | ||||
| 		<div class="sub"> | ||||
|  | @ -36,6 +36,11 @@ export default defineComponent({ | |||
| 			required: false, | ||||
| 			default: true | ||||
| 		}, | ||||
| 		thin: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		naked: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
|  | @ -226,7 +231,7 @@ export default defineComponent({ | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_380px { | ||||
| 	&.max-width_380px, &.thin { | ||||
| 		> header { | ||||
| 			> .title { | ||||
| 				padding: 8px 10px; | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <div class="rrevdjwt" :class="{ center: align === 'center' }" | ||||
| 	:style="{ width: width ? width + 'px' : null }" | ||||
| 	ref="items" | ||||
| 	@contextmenu.self="e => e.preventDefault()" | ||||
| 	v-hotkey="keymap" | ||||
|  | @ -59,6 +60,10 @@ export default defineComponent({ | |||
| 			type: String, | ||||
| 			requried: false | ||||
| 		}, | ||||
| 		width: { | ||||
| 			type: Number, | ||||
| 			required: false | ||||
| 		}, | ||||
| 	}, | ||||
| 	emits: ['close'], | ||||
| 	data() { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <MkPopup ref="popup" :src="src" @closed="$emit('closed')"> | ||||
| 	<MkMenu :items="items" :align="align" @close="$refs.popup.close()" class="_popup _shadow"/> | ||||
| 	<MkMenu :items="items" :align="align" :width="width" @close="$refs.popup.close()" class="_popup _shadow"/> | ||||
| </MkPopup> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -24,6 +24,10 @@ export default defineComponent({ | |||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		width: { | ||||
| 			type: Number, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		viaKeyboard: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
|  |  | |||
|  | @ -120,7 +120,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 			> .items { | ||||
| 				display: grid; | ||||
| 				grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); | ||||
| 				grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); | ||||
| 				grid-gap: 8px; | ||||
| 				padding: 0 16px; | ||||
| 
 | ||||
|  |  | |||
|  | @ -372,12 +372,17 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) { | ||||
| export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { | ||||
| 	align?: string; | ||||
| 	width?: number; | ||||
| 	viaKeyboard?: boolean; | ||||
| }) { | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		let dispose; | ||||
| 		popup(import('@client/components/ui/popup-menu.vue'), { | ||||
| 			items, | ||||
| 			src, | ||||
| 			width: options?.width, | ||||
| 			align: options?.align, | ||||
| 			viaKeyboard: options?.viaKeyboard | ||||
| 		}, { | ||||
|  |  | |||
|  | @ -7,8 +7,8 @@ | |||
| 			<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/instance/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo> | ||||
| 		<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/instance/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo> | ||||
| 		<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo> | ||||
| 		<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo> | ||||
| 
 | ||||
| 		<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu> | ||||
| 	</div> | ||||
|  | @ -93,47 +93,47 @@ export default defineComponent({ | |||
| 			items: [{ | ||||
| 				icon: 'fas fa-tachometer-alt', | ||||
| 				text: i18n.locale.dashboard, | ||||
| 				to: '/instance/overview', | ||||
| 				to: '/admin/overview', | ||||
| 				active: page.value === 'overview', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-users', | ||||
| 				text: i18n.locale.users, | ||||
| 				to: '/instance/users', | ||||
| 				to: '/admin/users', | ||||
| 				active: page.value === 'users', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-laugh', | ||||
| 				text: i18n.locale.customEmojis, | ||||
| 				to: '/instance/emojis', | ||||
| 				to: '/admin/emojis', | ||||
| 				active: page.value === 'emojis', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-globe', | ||||
| 				text: i18n.locale.federation, | ||||
| 				to: '/instance/federation', | ||||
| 				to: '/admin/federation', | ||||
| 				active: page.value === 'federation', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-clipboard-list', | ||||
| 				text: i18n.locale.jobQueue, | ||||
| 				to: '/instance/queue', | ||||
| 				to: '/admin/queue', | ||||
| 				active: page.value === 'queue', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-cloud', | ||||
| 				text: i18n.locale.files, | ||||
| 				to: '/instance/files', | ||||
| 				to: '/admin/files', | ||||
| 				active: page.value === 'files', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-broadcast-tower', | ||||
| 				text: i18n.locale.announcements, | ||||
| 				to: '/instance/announcements', | ||||
| 				to: '/admin/announcements', | ||||
| 				active: page.value === 'announcements', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-audio-description', | ||||
| 				text: i18n.locale.ads, | ||||
| 				to: '/instance/ads', | ||||
| 				to: '/admin/ads', | ||||
| 				active: page.value === 'ads', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-exclamation-circle', | ||||
| 				text: i18n.locale.abuseReports, | ||||
| 				to: '/instance/abuses', | ||||
| 				to: '/admin/abuses', | ||||
| 				active: page.value === 'abuses', | ||||
| 			}], | ||||
| 		}, { | ||||
|  | @ -141,57 +141,57 @@ export default defineComponent({ | |||
| 			items: [{ | ||||
| 				icon: 'fas fa-cog', | ||||
| 				text: i18n.locale.general, | ||||
| 				to: '/instance/settings', | ||||
| 				to: '/admin/settings', | ||||
| 				active: page.value === 'settings', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-cloud', | ||||
| 				text: i18n.locale.files, | ||||
| 				to: '/instance/files-settings', | ||||
| 				to: '/admin/files-settings', | ||||
| 				active: page.value === 'files-settings', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-envelope', | ||||
| 				text: i18n.locale.emailServer, | ||||
| 				to: '/instance/email-settings', | ||||
| 				to: '/admin/email-settings', | ||||
| 				active: page.value === 'email-settings', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-cloud', | ||||
| 				text: i18n.locale.objectStorage, | ||||
| 				to: '/instance/object-storage', | ||||
| 				to: '/admin/object-storage', | ||||
| 				active: page.value === 'object-storage', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-lock', | ||||
| 				text: i18n.locale.security, | ||||
| 				to: '/instance/security', | ||||
| 				to: '/admin/security', | ||||
| 				active: page.value === 'security', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-bolt', | ||||
| 				text: 'ServiceWorker', | ||||
| 				to: '/instance/service-worker', | ||||
| 				to: '/admin/service-worker', | ||||
| 				active: page.value === 'service-worker', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-globe', | ||||
| 				text: i18n.locale.relays, | ||||
| 				to: '/instance/relays', | ||||
| 				to: '/admin/relays', | ||||
| 				active: page.value === 'relays', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-share-alt', | ||||
| 				text: i18n.locale.integration, | ||||
| 				to: '/instance/integrations', | ||||
| 				to: '/admin/integrations', | ||||
| 				active: page.value === 'integrations', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-ban', | ||||
| 				text: i18n.locale.instanceBlocking, | ||||
| 				to: '/instance/instance-block', | ||||
| 				to: '/admin/instance-block', | ||||
| 				active: page.value === 'instance-block', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-ghost', | ||||
| 				text: i18n.locale.proxyAccount, | ||||
| 				to: '/instance/proxy-account', | ||||
| 				to: '/admin/proxy-account', | ||||
| 				active: page.value === 'proxy-account', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-cogs', | ||||
| 				text: i18n.locale.other, | ||||
| 				to: '/instance/other-settings', | ||||
| 				to: '/admin/other-settings', | ||||
| 				active: page.value === 'other-settings', | ||||
| 			}], | ||||
| 		}, { | ||||
|  | @ -199,13 +199,8 @@ export default defineComponent({ | |||
| 			items: [{ | ||||
| 				icon: 'fas fa-database', | ||||
| 				text: i18n.locale.database, | ||||
| 				to: '/instance/database', | ||||
| 				to: '/admin/database', | ||||
| 				active: page.value === 'database', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-stream', | ||||
| 				text: i18n.locale.logs, | ||||
| 				to: '/instance/logs', | ||||
| 				active: page.value === 'logs', | ||||
| 			}], | ||||
| 		}]); | ||||
| 		const component = computed(() => { | ||||
|  | @ -220,7 +215,6 @@ export default defineComponent({ | |||
| 				case 'announcements': return defineAsyncComponent(() => import('./announcements.vue')); | ||||
| 				case 'ads': return defineAsyncComponent(() => import('./ads.vue')); | ||||
| 				case 'database': return defineAsyncComponent(() => import('./database.vue')); | ||||
| 				case 'logs': return defineAsyncComponent(() => import('./logs.vue')); | ||||
| 				case 'abuses': return defineAsyncComponent(() => import('./abuses.vue')); | ||||
| 				case 'settings': return defineAsyncComponent(() => import('./settings.vue')); | ||||
| 				case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue')); | ||||
|  | @ -78,17 +78,17 @@ | |||
| 				<span class="label">{{ $ts.charts }}</span> | ||||
| 				<div class="selects"> | ||||
| 					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> | ||||
| 						<option value="requests">{{ $ts._instanceCharts.requests }}</option> | ||||
| 						<option value="users">{{ $ts._instanceCharts.users }}</option> | ||||
| 						<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option> | ||||
| 						<option value="notes">{{ $ts._instanceCharts.notes }}</option> | ||||
| 						<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option> | ||||
| 						<option value="ff">{{ $ts._instanceCharts.ff }}</option> | ||||
| 						<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option> | ||||
| 						<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option> | ||||
| 						<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option> | ||||
| 						<option value="drive-files">{{ $ts._instanceCharts.files }}</option> | ||||
| 						<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option> | ||||
| 						<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option> | ||||
| 						<option value="instance-users">{{ $ts._instanceCharts.users }}</option> | ||||
| 						<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option> | ||||
| 						<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option> | ||||
| 						<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option> | ||||
| 						<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option> | ||||
| 						<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option> | ||||
| 						<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option> | ||||
| 						<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option> | ||||
| 						<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option> | ||||
| 						<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option> | ||||
| 					</MkSelect> | ||||
| 					<MkSelect v-model="chartSpan" style="margin: 0;"> | ||||
| 						<option value="hour">{{ $ts.perHour }}</option> | ||||
|  | @ -97,7 +97,7 @@ | |||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="chart"> | ||||
| 				<canvas :ref="setChart"></canvas> | ||||
| 				<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="operations section"> | ||||
|  | @ -124,28 +124,17 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, markRaw } from 'vue'; | ||||
| import Chart from 'chart.js'; | ||||
| import XModalWindow from '@client/components/ui/modal-window.vue'; | ||||
| import MkUsersDialog from '@client/components/users-dialog.vue'; | ||||
| import MkSelect from '@client/components/form/select.vue'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import MkSwitch from '@client/components/form/switch.vue'; | ||||
| import MkInfo from '@client/components/ui/info.vue'; | ||||
| import MkChart from '@client/components/chart.vue'; | ||||
| import bytes from '@client/filters/bytes'; | ||||
| import number from '@client/filters/number'; | ||||
| import * as os from '@client/os'; | ||||
| 
 | ||||
| const chartLimit = 90; | ||||
| const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); | ||||
| const negate = arr => arr.map(x => -x); | ||||
| const alpha = hex => { | ||||
| 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; | ||||
| 	const r = parseInt(result[1], 16); | ||||
| 	const g = parseInt(result[2], 16); | ||||
| 	const b = parseInt(result[3], 16); | ||||
| 	return `rgba(${r}, ${g}, ${b}, 0.1)`; | ||||
| }; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XModalWindow, | ||||
|  | @ -153,6 +142,7 @@ export default defineComponent({ | |||
| 		MkButton, | ||||
| 		MkSwitch, | ||||
| 		MkInfo, | ||||
| 		MkChart, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
|  | @ -167,42 +157,12 @@ export default defineComponent({ | |||
| 	data() { | ||||
| 		return { | ||||
| 			isSuspended: this.instance.isSuspended, | ||||
| 			now: null, | ||||
| 			canvas: null, | ||||
| 			chart: null, | ||||
| 			chartInstance: null, | ||||
| 			chartSrc: 'requests', | ||||
| 			chartSpan: 'hour', | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		data(): any { | ||||
| 			if (this.chart == null) return null; | ||||
| 			switch (this.chartSrc) { | ||||
| 				case 'requests': return this.requestsChart(); | ||||
| 				case 'users': return this.usersChart(false); | ||||
| 				case 'users-total': return this.usersChart(true); | ||||
| 				case 'notes': return this.notesChart(false); | ||||
| 				case 'notes-total': return this.notesChart(true); | ||||
| 				case 'ff': return this.ffChart(false); | ||||
| 				case 'ff-total': return this.ffChart(true); | ||||
| 				case 'drive-usage': return this.driveUsageChart(false); | ||||
| 				case 'drive-usage-total': return this.driveUsageChart(true); | ||||
| 				case 'drive-files': return this.driveFilesChart(false); | ||||
| 				case 'drive-files-total': return this.driveFilesChart(true); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		stats(): any[] { | ||||
| 			const stats = | ||||
| 				this.chartSpan == 'day' ? this.chart.perDay : | ||||
| 				this.chartSpan == 'hour' ? this.chart.perHour : | ||||
| 				null; | ||||
| 
 | ||||
| 			return stats; | ||||
| 		}, | ||||
| 
 | ||||
| 		meta() { | ||||
| 			return this.$instance; | ||||
| 		}, | ||||
|  | @ -219,49 +179,15 @@ export default defineComponent({ | |||
| 				isSuspended: this.isSuspended | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		chartSrc() { | ||||
| 			this.renderChart(); | ||||
| 		}, | ||||
| 
 | ||||
| 		chartSpan() { | ||||
| 			this.renderChart(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	async created() { | ||||
| 		this.now = new Date(); | ||||
| 
 | ||||
| 		const [perHour, perDay] = await Promise.all([ | ||||
| 			os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }), | ||||
| 			os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }), | ||||
| 		]); | ||||
| 
 | ||||
| 		const chart = { | ||||
| 			perHour: perHour, | ||||
| 			perDay: perDay | ||||
| 		}; | ||||
| 
 | ||||
| 		this.chart = chart; | ||||
| 
 | ||||
| 		this.renderChart(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		setChart(el) { | ||||
| 			this.canvas = el; | ||||
| 		}, | ||||
| 
 | ||||
| 		changeBlock(e) { | ||||
| 			os.api('admin/update-meta', { | ||||
| 				blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host) | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		setSrc(src) { | ||||
| 			this.chartSrc = src; | ||||
| 		}, | ||||
| 
 | ||||
| 		removeAllFollowing() { | ||||
| 			os.apiWithDialog('admin/federation/remove-all-following', { | ||||
| 				host: this.instance.host | ||||
|  | @ -274,170 +200,6 @@ export default defineComponent({ | |||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		renderChart() { | ||||
| 			if (this.chartInstance) { | ||||
| 				this.chartInstance.destroy(); | ||||
| 			} | ||||
| 
 | ||||
| 			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||
| 			this.chartInstance = markRaw(new Chart(this.canvas, { | ||||
| 				type: 'line', | ||||
| 				data: { | ||||
| 					labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), | ||||
| 					datasets: this.data.series.map(x => ({ | ||||
| 						label: x.name, | ||||
| 						data: x.data.slice().reverse(), | ||||
| 						pointRadius: 0, | ||||
| 						lineTension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: x.color, | ||||
| 						backgroundColor: alpha(x.color), | ||||
| 					})) | ||||
| 				}, | ||||
| 				options: { | ||||
| 					aspectRatio: 2.5, | ||||
| 					layout: { | ||||
| 						padding: { | ||||
| 							left: 16, | ||||
| 							right: 16, | ||||
| 							top: 16, | ||||
| 							bottom: 0 | ||||
| 						} | ||||
| 					}, | ||||
| 					legend: { | ||||
| 						position: 'bottom', | ||||
| 						labels: { | ||||
| 							boxWidth: 16, | ||||
| 						} | ||||
| 					}, | ||||
| 					scales: { | ||||
| 						xAxes: [{ | ||||
| 							gridLines: { | ||||
| 								display: false | ||||
| 							}, | ||||
| 							ticks: { | ||||
| 								display: false | ||||
| 							} | ||||
| 						}], | ||||
| 						yAxes: [{ | ||||
| 							position: 'right', | ||||
| 							ticks: { | ||||
| 								display: false | ||||
| 							} | ||||
| 						}] | ||||
| 					}, | ||||
| 					tooltips: { | ||||
| 						intersect: false, | ||||
| 						mode: 'index', | ||||
| 					} | ||||
| 				} | ||||
| 			})); | ||||
| 		}, | ||||
| 
 | ||||
| 		getDate(ago: number) { | ||||
| 			const y = this.now.getFullYear(); | ||||
| 			const m = this.now.getMonth(); | ||||
| 			const d = this.now.getDate(); | ||||
| 			const h = this.now.getHours(); | ||||
| 
 | ||||
| 			return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); | ||||
| 		}, | ||||
| 
 | ||||
| 		format(arr) { | ||||
| 			return arr; | ||||
| 		}, | ||||
| 
 | ||||
| 		requestsChart(): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'In', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(this.stats.requests.received) | ||||
| 				}, { | ||||
| 					name: 'Out (succ)', | ||||
| 					color: '#00E396', | ||||
| 					data: this.format(this.stats.requests.succeeded) | ||||
| 				}, { | ||||
| 					name: 'Out (fail)', | ||||
| 					color: '#FEB019', | ||||
| 					data: this.format(this.stats.requests.failed) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		usersChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Users', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.users.total | ||||
| 						: sum(this.stats.users.inc, negate(this.stats.users.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		notesChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Notes', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.notes.total | ||||
| 						: sum(this.stats.notes.inc, negate(this.stats.notes.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		ffChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Following', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.following.total | ||||
| 						: sum(this.stats.following.inc, negate(this.stats.following.dec)) | ||||
| 					) | ||||
| 				}, { | ||||
| 					name: 'Followers', | ||||
| 					color: '#00E396', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.followers.total | ||||
| 						: sum(this.stats.followers.inc, negate(this.stats.followers.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		driveUsageChart(total: boolean): any { | ||||
| 			return { | ||||
| 				bytes: true, | ||||
| 				series: [{ | ||||
| 					name: 'Drive usage', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.drive.totalUsage | ||||
| 						: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		driveFilesChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Drive files', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.drive.totalFiles | ||||
| 						: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		showFollowing() { | ||||
| 			os.modal(MkUsersDialog, { | ||||
| 				title: this.$ts.instanceFollowing, | ||||
|  | @ -1,15 +1,15 @@ | |||
| <template> | ||||
| <FormBase> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<FormLink to="/instance/integrations/twitter"> | ||||
| 		<FormLink to="/admin/integrations/twitter"> | ||||
| 			<i class="fab fa-twitter"></i> Twitter | ||||
| 			<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template> | ||||
| 		</FormLink> | ||||
| 		<FormLink to="/instance/integrations/github"> | ||||
| 		<FormLink to="/admin/integrations/github"> | ||||
| 			<i class="fab fa-github"></i> GitHub | ||||
| 			<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template> | ||||
| 		</FormLink> | ||||
| 		<FormLink to="/instance/integrations/discord"> | ||||
| 		<FormLink to="/admin/integrations/discord"> | ||||
| 			<i class="fab fa-discord"></i> Discord | ||||
| 			<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template> | ||||
| 		</FormLink> | ||||
|  | @ -52,7 +52,21 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, markRaw } from 'vue'; | ||||
| import Chart from 'chart.js'; | ||||
| import { | ||||
|   Chart, | ||||
|   ArcElement, | ||||
|   LineElement, | ||||
|   BarElement, | ||||
|   PointElement, | ||||
|   BarController, | ||||
|   LineController, | ||||
|   CategoryScale, | ||||
|   LinearScale, | ||||
|   Legend, | ||||
|   Title, | ||||
|   Tooltip, | ||||
|   SubTitle | ||||
| } from 'chart.js'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import MkSelect from '@client/components/form/select.vue'; | ||||
| import MkInput from '@client/components/form/input.vue'; | ||||
|  | @ -64,6 +78,21 @@ import bytes from '@client/filters/bytes'; | |||
| import number from '@client/filters/number'; | ||||
| import MkInstanceInfo from './instance.vue'; | ||||
| 
 | ||||
| Chart.register( | ||||
|   ArcElement, | ||||
|   LineElement, | ||||
|   BarElement, | ||||
|   PointElement, | ||||
|   BarController, | ||||
|   LineController, | ||||
|   CategoryScale, | ||||
|   LinearScale, | ||||
|   Legend, | ||||
|   Title, | ||||
|   Tooltip, | ||||
|   SubTitle | ||||
| ); | ||||
| 
 | ||||
| const alpha = (hex, a) => { | ||||
| 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; | ||||
| 	const r = parseInt(result[1], 16); | ||||
|  | @ -116,7 +145,7 @@ export default defineComponent({ | |||
| 	mounted() { | ||||
| 		this.fetchJobs(); | ||||
| 
 | ||||
| 		Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||
| 		Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||
| 
 | ||||
| 		os.api('admin/server-info', {}).then(res => { | ||||
| 			this.serverInfo = res; | ||||
|  | @ -157,7 +186,7 @@ export default defineComponent({ | |||
| 					datasets: [{ | ||||
| 						label: 'CPU', | ||||
| 						pointRadius: 0, | ||||
| 						lineTension: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: '#86b300', | ||||
| 						backgroundColor: alpha('#86b300', 0.1), | ||||
|  | @ -165,7 +194,7 @@ export default defineComponent({ | |||
| 					}, { | ||||
| 						label: 'MEM (active)', | ||||
| 						pointRadius: 0, | ||||
| 						lineTension: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: '#935dbf', | ||||
| 						backgroundColor: alpha('#935dbf', 0.02), | ||||
|  | @ -173,7 +202,7 @@ export default defineComponent({ | |||
| 					}, { | ||||
| 						label: 'MEM (used)', | ||||
| 						pointRadius: 0, | ||||
| 						lineTension: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: '#935dbf', | ||||
| 						borderDash: [5, 5], | ||||
|  | @ -198,7 +227,7 @@ export default defineComponent({ | |||
| 						} | ||||
| 					}, | ||||
| 					scales: { | ||||
| 						xAxes: [{ | ||||
| 						x: { | ||||
| 							gridLines: { | ||||
| 								display: false, | ||||
| 								color: this.gridColor, | ||||
|  | @ -207,8 +236,8 @@ export default defineComponent({ | |||
| 							ticks: { | ||||
| 								display: false, | ||||
| 							} | ||||
| 						}], | ||||
| 						yAxes: [{ | ||||
| 						}, | ||||
| 						y: { | ||||
| 							position: 'right', | ||||
| 							gridLines: { | ||||
| 								display: true, | ||||
|  | @ -219,7 +248,7 @@ export default defineComponent({ | |||
| 								display: false, | ||||
| 								max: 100 | ||||
| 							} | ||||
| 						}] | ||||
| 						} | ||||
| 					}, | ||||
| 					tooltips: { | ||||
| 						intersect: false, | ||||
|  | @ -238,7 +267,7 @@ export default defineComponent({ | |||
| 					datasets: [{ | ||||
| 						label: 'In', | ||||
| 						pointRadius: 0, | ||||
| 						lineTension: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: '#94a029', | ||||
| 						backgroundColor: alpha('#94a029', 0.1), | ||||
|  | @ -246,7 +275,7 @@ export default defineComponent({ | |||
| 					}, { | ||||
| 						label: 'Out', | ||||
| 						pointRadius: 0, | ||||
| 						lineTension: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: '#ff9156', | ||||
| 						backgroundColor: alpha('#ff9156', 0.1), | ||||
|  | @ -270,7 +299,7 @@ export default defineComponent({ | |||
| 						} | ||||
| 					}, | ||||
| 					scales: { | ||||
| 						xAxes: [{ | ||||
| 						x: { | ||||
| 							gridLines: { | ||||
| 								display: false, | ||||
| 								color: this.gridColor, | ||||
|  | @ -279,8 +308,8 @@ export default defineComponent({ | |||
| 							ticks: { | ||||
| 								display: false | ||||
| 							} | ||||
| 						}], | ||||
| 						yAxes: [{ | ||||
| 						}, | ||||
| 						y: { | ||||
| 							position: 'right', | ||||
| 							gridLines: { | ||||
| 								display: true, | ||||
|  | @ -290,7 +319,7 @@ export default defineComponent({ | |||
| 							ticks: { | ||||
| 								display: false, | ||||
| 							} | ||||
| 						}] | ||||
| 						} | ||||
| 					}, | ||||
| 					tooltips: { | ||||
| 						intersect: false, | ||||
|  | @ -309,7 +338,7 @@ export default defineComponent({ | |||
| 					datasets: [{ | ||||
| 						label: 'Read', | ||||
| 						pointRadius: 0, | ||||
| 						lineTension: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: '#94a029', | ||||
| 						backgroundColor: alpha('#94a029', 0.1), | ||||
|  | @ -317,7 +346,7 @@ export default defineComponent({ | |||
| 					}, { | ||||
| 						label: 'Write', | ||||
| 						pointRadius: 0, | ||||
| 						lineTension: 0, | ||||
| 						tension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: '#ff9156', | ||||
| 						backgroundColor: alpha('#ff9156', 0.1), | ||||
|  | @ -341,7 +370,7 @@ export default defineComponent({ | |||
| 						} | ||||
| 					}, | ||||
| 					scales: { | ||||
| 						xAxes: [{ | ||||
| 						x: { | ||||
| 							gridLines: { | ||||
| 								display: false, | ||||
| 								color: this.gridColor, | ||||
|  | @ -350,8 +379,8 @@ export default defineComponent({ | |||
| 							ticks: { | ||||
| 								display: false | ||||
| 							} | ||||
| 						}], | ||||
| 						yAxes: [{ | ||||
| 						}, | ||||
| 						y: { | ||||
| 							position: 'right', | ||||
| 							gridLines: { | ||||
| 								display: true, | ||||
|  | @ -361,7 +390,7 @@ export default defineComponent({ | |||
| 							ticks: { | ||||
| 								display: false, | ||||
| 							} | ||||
| 						}] | ||||
| 						} | ||||
| 					}, | ||||
| 					tooltips: { | ||||
| 						intersect: false, | ||||
|  | @ -371,18 +400,6 @@ export default defineComponent({ | |||
| 			})); | ||||
| 		}, | ||||
| 
 | ||||
| 		async showInstanceInfo(q) { | ||||
| 			let instance = q; | ||||
| 			if (typeof q === 'string') { | ||||
| 				instance = await os.api('federation/show-instance', { | ||||
| 					host: q | ||||
| 				}); | ||||
| 			} | ||||
| 			os.popup(MkInstanceInfo, { | ||||
| 				instance: instance | ||||
| 			}, {}, 'closed'); | ||||
| 		}, | ||||
| 
 | ||||
| 		fetchJobs() { | ||||
| 			os.api('admin/queue/deliver-delayed', {}).then(jobs => { | ||||
| 				this.jobs = jobs; | ||||
							
								
								
									
										242
									
								
								src/client/pages/admin/overview.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								src/client/pages/admin/overview.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,242 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<MkHeader :info="header"/> | ||||
| 
 | ||||
| 	<div class="edbbcaef" v-size="{ max: [880] }"> | ||||
| 		<div v-if="stats" class="cfcdecdf" style="margin: var(--margin)"> | ||||
| 			<div class="number _panel"> | ||||
| 				<div class="label">Users</div> | ||||
| 				<div class="value _monospace"> | ||||
| 					{{ number(stats.originalUsersCount) }} | ||||
| 					<MkNumberDiff v-if="usersComparedToThePrevDay != null" class="diff" :value="usersComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="number _panel"> | ||||
| 				<div class="label">Notes</div> | ||||
| 				<div class="value _monospace"> | ||||
| 					{{ number(stats.originalNotesCount) }} | ||||
| 					<MkNumberDiff v-if="notesComparedToThePrevDay != null" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<MkContainer :foldable="true" class="charts"> | ||||
| 			<template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template> | ||||
| 			<div style="padding-top: 12px;"> | ||||
| 				<MkInstanceStats :chart-limit="500" :detailed="true"/> | ||||
| 			</div> | ||||
| 		</MkContainer> | ||||
| 
 | ||||
| 		<div class="queue"> | ||||
| 			<MkContainer :foldable="true" :thin="true" class="deliver"> | ||||
| 				<template #header>Queue: deliver</template> | ||||
| 				<MkQueueChart :connection="queueStatsConnection" domain="deliver"/> | ||||
| 			</MkContainer> | ||||
| 			<MkContainer :foldable="true" :thin="true" class="inbox"> | ||||
| 				<template #header>Queue: inbox</template> | ||||
| 				<MkQueueChart :connection="queueStatsConnection" domain="inbox"/> | ||||
| 			</MkContainer> | ||||
| 		</div> | ||||
| 
 | ||||
| 			<!--<XMetrics/>--> | ||||
| 
 | ||||
| 		<MkFolder style="margin: var(--margin)"> | ||||
| 			<template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template> | ||||
| 			<div class="cfcdecdf"> | ||||
| 				<div class="number _panel"> | ||||
| 					<div class="label">Misskey</div> | ||||
| 					<div class="value _monospace">{{ version }}</div> | ||||
| 				</div> | ||||
| 				<div class="number _panel" v-if="serverInfo"> | ||||
| 					<div class="label">Node.js</div> | ||||
| 					<div class="value _monospace">{{ serverInfo.node }}</div> | ||||
| 				</div> | ||||
| 				<div class="number _panel" v-if="serverInfo"> | ||||
| 					<div class="label">PostgreSQL</div> | ||||
| 					<div class="value _monospace">{{ serverInfo.psql }}</div> | ||||
| 				</div> | ||||
| 				<div class="number _panel" v-if="serverInfo"> | ||||
| 					<div class="label">Redis</div> | ||||
| 					<div class="value _monospace">{{ serverInfo.redis }}</div> | ||||
| 				</div> | ||||
| 				<div class="number _panel"> | ||||
| 					<div class="label">Vue</div> | ||||
| 					<div class="value _monospace">{{ vueVersion }}</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</MkFolder> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent, markRaw, version as vueVersion } from 'vue'; | ||||
| import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; | ||||
| import MkInstanceStats from '@client/components/instance-stats.vue'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import MkSelect from '@client/components/form/select.vue'; | ||||
| import MkNumberDiff from '@client/components/number-diff.vue'; | ||||
| import MkContainer from '@client/components/ui/container.vue'; | ||||
| import MkFolder from '@client/components/ui/folder.vue'; | ||||
| import MkQueueChart from '@client/components/queue-chart.vue'; | ||||
| import { version, url } from '@client/config'; | ||||
| import bytes from '@client/filters/bytes'; | ||||
| import number from '@client/filters/number'; | ||||
| import MkInstanceInfo from './instance.vue'; | ||||
| import XMetrics from './metrics.vue'; | ||||
| import * as os from '@client/os'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkNumberDiff, | ||||
| 		FormKeyValueView, | ||||
| 		MkInstanceStats, | ||||
| 		MkContainer, | ||||
| 		MkFolder, | ||||
| 		MkQueueChart, | ||||
| 		XMetrics, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.dashboard, | ||||
| 				icon: 'fas fa-tachometer-alt', | ||||
| 				bg: 'var(--bg)', | ||||
| 			}, | ||||
| 			header: { | ||||
| 				title: this.$ts.dashboard, | ||||
| 				icon: 'fas fa-tachometer-alt', | ||||
| 			}, | ||||
| 			version, | ||||
| 			vueVersion, | ||||
| 			url, | ||||
| 			stats: null, | ||||
| 			meta: null, | ||||
| 			serverInfo: null, | ||||
| 			usersComparedToThePrevDay: null, | ||||
| 			notesComparedToThePrevDay: null, | ||||
| 			fetchJobs: () => os.api('admin/queue/deliver-delayed', {}), | ||||
| 			fetchModLogs: () => os.api('admin/show-moderation-logs', {}), | ||||
| 			queueStatsConnection: markRaw(os.stream.useChannel('queueStats')), | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	async mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 
 | ||||
| 		os.api('meta', { detail: true }).then(meta => { | ||||
| 			this.meta = meta; | ||||
| 		}); | ||||
| 		 | ||||
| 		os.api('stats', {}).then(stats => { | ||||
| 			this.stats = stats; | ||||
| 
 | ||||
| 			os.api('charts/users', { limit: 2, span: 'day' }).then(chart => { | ||||
| 				this.usersComparedToThePrevDay = this.stats.originalUsersCount - chart.local.total[1]; | ||||
| 			}); | ||||
| 
 | ||||
| 			os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => { | ||||
| 				this.notesComparedToThePrevDay = this.stats.originalNotesCount - chart.local.total[1]; | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		os.api('admin/server-info', {}).then(serverInfo => { | ||||
| 			this.serverInfo = serverInfo; | ||||
| 		}); | ||||
| 
 | ||||
| 		this.$nextTick(() => { | ||||
| 			this.queueStatsConnection.send('requestLog', { | ||||
| 				id: Math.random().toString().substr(2, 8), | ||||
| 				length: 200 | ||||
| 			}); | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeUnmount() { | ||||
| 		this.queueStatsConnection.dispose(); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async showInstanceInfo(q) { | ||||
| 			let instance = q; | ||||
| 			if (typeof q === 'string') { | ||||
| 				instance = await os.api('federation/show-instance', { | ||||
| 					host: q | ||||
| 				}); | ||||
| 			} | ||||
| 			os.popup(MkInstanceInfo, { | ||||
| 				instance: instance | ||||
| 			}, {}, 'closed'); | ||||
| 		}, | ||||
| 
 | ||||
| 		bytes, | ||||
| 
 | ||||
| 		number, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .edbbcaef { | ||||
| 	.cfcdecdf { | ||||
| 		display: grid; | ||||
| 		grid-gap: 8px; | ||||
| 		grid-template-columns: repeat(auto-fill,minmax(150px,1fr)); | ||||
| 
 | ||||
| 		> .number { | ||||
| 			padding: 12px 16px; | ||||
| 
 | ||||
| 			> .label { | ||||
| 				opacity: 0.7; | ||||
| 				font-size: 0.8em; | ||||
| 			} | ||||
| 
 | ||||
| 			> .value { | ||||
| 				font-weight: bold; | ||||
| 				font-size: 1.2em; | ||||
| 
 | ||||
| 				> .diff { | ||||
| 					font-size: 0.8em; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .charts { | ||||
| 		margin: var(--margin); | ||||
| 	} | ||||
| 
 | ||||
| 	> .queue { | ||||
| 		margin: var(--margin); | ||||
| 		display: flex; | ||||
| 
 | ||||
| 		> .deliver, | ||||
| 		> .inbox { | ||||
| 			flex: 1; | ||||
| 			width: 50%; | ||||
| 
 | ||||
| 			&:not(:first-child) { | ||||
| 				margin-left: var(--margin); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_800px { | ||||
| 		> .queue { | ||||
| 			display: block; | ||||
| 
 | ||||
| 			> .deliver, | ||||
| 			> .inbox { | ||||
| 				&:not(:first-child) { | ||||
| 					margin-top: var(--margin); | ||||
| 					margin-left: 0; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										102
									
								
								src/client/pages/admin/queue.chart.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/client/pages/admin/queue.chart.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,102 @@ | |||
| <template> | ||||
| <div class="_debobigegoItem"> | ||||
| 	<div class="_debobigegoLabel"><slot name="title"></slot></div> | ||||
| 	<div class="_debobigegoPanel pumxzjhg"> | ||||
| 		<div class="_table status"> | ||||
| 			<div class="_row"> | ||||
| 				<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div> | ||||
| 				<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div> | ||||
| 				<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div> | ||||
| 				<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class=""> | ||||
| 			<MkQueueChart :domain="domain" :connection="connection"/> | ||||
| 		</div> | ||||
| 		<div class="jobs"> | ||||
| 			<div v-if="jobs.length > 0"> | ||||
| 				<div v-for="job in jobs" :key="job[0]"> | ||||
| 					<span>{{ job[0] }}</span> | ||||
| 					<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue'; | ||||
| import number from '@client/filters/number'; | ||||
| import MkQueueChart from '@client/components/queue-chart.vue'; | ||||
| import * as os from '@client/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkQueueChart | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		domain: { | ||||
| 			type: String, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		connection: { | ||||
| 			required: true, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	setup(props) { | ||||
| 		const activeSincePrevTick = ref(0); | ||||
| 		const active = ref(0); | ||||
| 		const waiting = ref(0); | ||||
| 		const delayed = ref(0); | ||||
| 		const jobs = ref([]); | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => { | ||||
| 				jobs.value = jobs; | ||||
| 			}); | ||||
| 
 | ||||
| 			const onStats = (stats) => { | ||||
| 				activeSincePrevTick.value = stats[props.domain].activeSincePrevTick; | ||||
| 				active.value = stats[props.domain].active; | ||||
| 				waiting.value = stats[props.domain].waiting; | ||||
| 				delayed.value = stats[props.domain].delayed; | ||||
| 			}; | ||||
| 
 | ||||
| 			props.connection.on('stats', onStats); | ||||
| 
 | ||||
| 			onUnmounted(() => { | ||||
| 				props.connection.off('stats', onStats); | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			jobs, | ||||
| 			activeSincePrevTick, | ||||
| 			active, | ||||
| 			waiting, | ||||
| 			delayed, | ||||
| 			number, | ||||
| 		}; | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .pumxzjhg { | ||||
| 	> .status { | ||||
| 		padding: 16px; | ||||
| 		border-bottom: solid 0.5px var(--divider); | ||||
| 	} | ||||
| 
 | ||||
| 	> .jobs { | ||||
| 		padding: 16px; | ||||
| 		border-top: solid 0.5px var(--divider); | ||||
| 		max-height: 180px; | ||||
| 		overflow: auto; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <FormBase> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<FormLink to="/instance/bot-protection"> | ||||
| 		<FormLink to="/admin/bot-protection"> | ||||
| 			<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }} | ||||
| 			<template #suffix v-if="enableHcaptcha">hCaptcha</template> | ||||
| 			<template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template> | ||||
|  | @ -65,13 +65,18 @@ | |||
| 			</div> | ||||
| 			<div v-else-if="tab === 'search'"> | ||||
| 				<div class="_isolated"> | ||||
| 					<MkInput v-model="query" :debounce="true" type="search"> | ||||
| 					<MkInput v-model="searchQuery" :debounce="true" type="search"> | ||||
| 						<template #prefix><i class="fas fa-search"></i></template> | ||||
| 						<template #label>{{ $ts.searchUser }}</template> | ||||
| 					</MkInput> | ||||
| 					<MkRadios v-model="searchOrigin"> | ||||
| 						<option value="local">{{ $ts.local }}</option> | ||||
| 						<option value="remote">{{ $ts.remote }}</option> | ||||
| 						<option value="both">{{ $ts.both }}</option> | ||||
| 					</MkRadios> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<XUserList v-if="query" class="_gap" :pagination="searchPagination" ref="search"/> | ||||
| 				<XUserList v-if="searchQuery" class="_gap" :pagination="searchPagination" ref="search"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
|  | @ -83,6 +88,7 @@ import { computed, defineComponent } from 'vue'; | |||
| import XUserList from '@client/components/user-list.vue'; | ||||
| import MkFolder from '@client/components/ui/folder.vue'; | ||||
| import MkInput from '@client/components/form/input.vue'; | ||||
| import MkRadios from '@client/components/form/radios.vue'; | ||||
| import number from '@client/filters/number'; | ||||
| import * as os from '@client/os'; | ||||
| import * as symbols from '@client/symbols'; | ||||
|  | @ -92,6 +98,7 @@ export default defineComponent({ | |||
| 		XUserList, | ||||
| 		MkFolder, | ||||
| 		MkInput, | ||||
| 		MkRadios, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
|  | @ -158,14 +165,16 @@ export default defineComponent({ | |||
| 			searchPagination: { | ||||
| 				endpoint: 'users/search', | ||||
| 				limit: 10, | ||||
| 				params: computed(() => (this.query && this.query !== '') ? { | ||||
| 					query: this.query | ||||
| 				params: computed(() => (this.searchQuery && this.searchQuery !== '') ? { | ||||
| 					query: this.searchQuery, | ||||
| 					origin: this.searchOrigin, | ||||
| 				} : null) | ||||
| 			}, | ||||
| 			tagsLocal: [], | ||||
| 			tagsRemote: [], | ||||
| 			stats: null, | ||||
| 			query: null, | ||||
| 			searchQuery: null, | ||||
| 			searchOrigin: 'combined', | ||||
| 			num: number, | ||||
| 		}; | ||||
| 	}, | ||||
|  |  | |||
|  | @ -65,17 +65,17 @@ | |||
| 			<div class="_debobigegoPanel cmhjzshl"> | ||||
| 				<div class="selects"> | ||||
| 					<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> | ||||
| 						<option value="requests">{{ $ts._instanceCharts.requests }}</option> | ||||
| 						<option value="users">{{ $ts._instanceCharts.users }}</option> | ||||
| 						<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option> | ||||
| 						<option value="notes">{{ $ts._instanceCharts.notes }}</option> | ||||
| 						<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option> | ||||
| 						<option value="ff">{{ $ts._instanceCharts.ff }}</option> | ||||
| 						<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option> | ||||
| 						<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option> | ||||
| 						<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option> | ||||
| 						<option value="drive-files">{{ $ts._instanceCharts.files }}</option> | ||||
| 						<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option> | ||||
| 						<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option> | ||||
| 						<option value="instance-users">{{ $ts._instanceCharts.users }}</option> | ||||
| 						<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option> | ||||
| 						<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option> | ||||
| 						<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option> | ||||
| 						<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option> | ||||
| 						<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option> | ||||
| 						<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option> | ||||
| 						<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option> | ||||
| 						<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option> | ||||
| 						<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option> | ||||
| 					</MkSelect> | ||||
| 					<MkSelect v-model="chartSpan" style="margin: 0;"> | ||||
| 						<option value="hour">{{ $ts.perHour }}</option> | ||||
|  | @ -83,7 +83,7 @@ | |||
| 					</MkSelect> | ||||
| 				</div> | ||||
| 				<div class="chart"> | ||||
| 					<canvas :ref="setChart"></canvas> | ||||
| 					<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | @ -135,7 +135,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineAsyncComponent, defineComponent } from 'vue'; | ||||
| import Chart from 'chart.js'; | ||||
| import MkChart from '@client/components/chart.vue'; | ||||
| import FormObjectView from '@client/components/debobigego/object-view.vue'; | ||||
| import FormTextarea from '@client/components/debobigego/textarea.vue'; | ||||
| import FormLink from '@client/components/debobigego/link.vue'; | ||||
|  | @ -149,18 +149,7 @@ import * as os from '@client/os'; | |||
| import number from '@client/filters/number'; | ||||
| import bytes from '@client/filters/bytes'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| import MkInstanceInfo from '@client/pages/instance/instance.vue'; | ||||
| 
 | ||||
| const chartLimit = 90; | ||||
| const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); | ||||
| const negate = arr => arr.map(x => -x); | ||||
| const alpha = hex => { | ||||
| 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; | ||||
| 	const r = parseInt(result[1], 16); | ||||
| 	const g = parseInt(result[2], 16); | ||||
| 	const b = parseInt(result[3], 16); | ||||
| 	return `rgba(${r}, ${g}, ${b}, 0.1)`; | ||||
| }; | ||||
| import MkInstanceInfo from '@client/pages/admin/instance.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
|  | @ -173,6 +162,7 @@ export default defineComponent({ | |||
| 		FormKeyValueView, | ||||
| 		FormSuspense, | ||||
| 		MkSelect, | ||||
| 		MkChart, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
|  | @ -199,53 +189,11 @@ export default defineComponent({ | |||
| 			dnsPromiseFactory: () => os.api('federation/dns', { | ||||
| 				host: this.host | ||||
| 			}), | ||||
| 			now: null, | ||||
| 			canvas: null, | ||||
| 			chart: null, | ||||
| 			chartInstance: null, | ||||
| 			chartSrc: 'requests', | ||||
| 			chartSrc: 'instance-requests', | ||||
| 			chartSpan: 'hour', | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		data(): any { | ||||
| 			if (this.chart == null) return null; | ||||
| 			switch (this.chartSrc) { | ||||
| 				case 'requests': return this.requestsChart(); | ||||
| 				case 'users': return this.usersChart(false); | ||||
| 				case 'users-total': return this.usersChart(true); | ||||
| 				case 'notes': return this.notesChart(false); | ||||
| 				case 'notes-total': return this.notesChart(true); | ||||
| 				case 'ff': return this.ffChart(false); | ||||
| 				case 'ff-total': return this.ffChart(true); | ||||
| 				case 'drive-usage': return this.driveUsageChart(false); | ||||
| 				case 'drive-usage-total': return this.driveUsageChart(true); | ||||
| 				case 'drive-files': return this.driveFilesChart(false); | ||||
| 				case 'drive-files-total': return this.driveFilesChart(true); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		stats(): any[] { | ||||
| 			const stats = | ||||
| 				this.chartSpan == 'day' ? this.chart.perDay : | ||||
| 				this.chartSpan == 'hour' ? this.chart.perHour : | ||||
| 				null; | ||||
| 
 | ||||
| 			return stats; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		chartSrc() { | ||||
| 			this.renderChart(); | ||||
| 		}, | ||||
| 
 | ||||
| 		chartSpan() { | ||||
| 			this.renderChart(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.fetch(); | ||||
| 	}, | ||||
|  | @ -258,190 +206,6 @@ export default defineComponent({ | |||
| 			this.instance = await os.api('federation/show-instance', { | ||||
| 				host: this.host | ||||
| 			}); | ||||
| 
 | ||||
| 			this.now = new Date(); | ||||
| 
 | ||||
| 			const [perHour, perDay] = await Promise.all([ | ||||
| 				os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }), | ||||
| 				os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }), | ||||
| 			]); | ||||
| 
 | ||||
| 			const chart = { | ||||
| 				perHour: perHour, | ||||
| 				perDay: perDay | ||||
| 			}; | ||||
| 
 | ||||
| 			this.chart = chart; | ||||
| 
 | ||||
| 			this.renderChart(); | ||||
| 		}, | ||||
| 
 | ||||
| 		setChart(el) { | ||||
| 			this.canvas = el; | ||||
| 		}, | ||||
| 
 | ||||
| 		renderChart() { | ||||
| 			if (this.chartInstance) { | ||||
| 				this.chartInstance.destroy(); | ||||
| 			} | ||||
| 
 | ||||
| 			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||
| 			this.chartInstance = new Chart(this.canvas, { | ||||
| 				type: 'line', | ||||
| 				data: { | ||||
| 					labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), | ||||
| 					datasets: this.data.series.map(x => ({ | ||||
| 						label: x.name, | ||||
| 						data: x.data.slice().reverse(), | ||||
| 						pointRadius: 0, | ||||
| 						lineTension: 0, | ||||
| 						borderWidth: 2, | ||||
| 						borderColor: x.color, | ||||
| 						backgroundColor: alpha(x.color), | ||||
| 					})) | ||||
| 				}, | ||||
| 				options: { | ||||
| 					aspectRatio: 2.5, | ||||
| 					layout: { | ||||
| 						padding: { | ||||
| 							left: 16, | ||||
| 							right: 16, | ||||
| 							top: 16, | ||||
| 							bottom: 16 | ||||
| 						} | ||||
| 					}, | ||||
| 					legend: { | ||||
| 						position: 'bottom', | ||||
| 						labels: { | ||||
| 							boxWidth: 16, | ||||
| 						} | ||||
| 					}, | ||||
| 					scales: { | ||||
| 						xAxes: [{ | ||||
| 							gridLines: { | ||||
| 								display: false | ||||
| 							}, | ||||
| 							ticks: { | ||||
| 								display: false | ||||
| 							} | ||||
| 						}], | ||||
| 						yAxes: [{ | ||||
| 							position: 'right', | ||||
| 							ticks: { | ||||
| 								display: false | ||||
| 							} | ||||
| 						}] | ||||
| 					}, | ||||
| 					tooltips: { | ||||
| 						intersect: false, | ||||
| 						mode: 'index', | ||||
| 					} | ||||
| 				} | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		getDate(ago: number) { | ||||
| 			const y = this.now.getFullYear(); | ||||
| 			const m = this.now.getMonth(); | ||||
| 			const d = this.now.getDate(); | ||||
| 			const h = this.now.getHours(); | ||||
| 
 | ||||
| 			return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); | ||||
| 		}, | ||||
| 
 | ||||
| 		format(arr) { | ||||
| 			return arr; | ||||
| 		}, | ||||
| 
 | ||||
| 		requestsChart(): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'In', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(this.stats.requests.received) | ||||
| 				}, { | ||||
| 					name: 'Out (succ)', | ||||
| 					color: '#00E396', | ||||
| 					data: this.format(this.stats.requests.succeeded) | ||||
| 				}, { | ||||
| 					name: 'Out (fail)', | ||||
| 					color: '#FEB019', | ||||
| 					data: this.format(this.stats.requests.failed) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		usersChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Users', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.users.total | ||||
| 						: sum(this.stats.users.inc, negate(this.stats.users.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		notesChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Notes', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.notes.total | ||||
| 						: sum(this.stats.notes.inc, negate(this.stats.notes.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		ffChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Following', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.following.total | ||||
| 						: sum(this.stats.following.inc, negate(this.stats.following.dec)) | ||||
| 					) | ||||
| 				}, { | ||||
| 					name: 'Followers', | ||||
| 					color: '#00E396', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.followers.total | ||||
| 						: sum(this.stats.followers.inc, negate(this.stats.followers.dec)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		driveUsageChart(total: boolean): any { | ||||
| 			return { | ||||
| 				bytes: true, | ||||
| 				series: [{ | ||||
| 					name: 'Drive usage', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.drive.totalUsage | ||||
| 						: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		driveFilesChart(total: boolean): any { | ||||
| 			return { | ||||
| 				series: [{ | ||||
| 					name: 'Drive files', | ||||
| 					color: '#008FFB', | ||||
| 					data: this.format(total | ||||
| 						? this.stats.drive.totalFiles | ||||
| 						: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles)) | ||||
| 					) | ||||
| 				}] | ||||
| 			}; | ||||
| 		}, | ||||
| 
 | ||||
| 		info() { | ||||
|  |  | |||
|  | @ -1,97 +0,0 @@ | |||
| <template> | ||||
| <div class="_section"> | ||||
| 	<div class="_inputs"> | ||||
| 		<MkInput v-model="logDomain" :debounce="true"> | ||||
| 			<template #label>{{ $ts.domain }}</template> | ||||
| 		</MkInput> | ||||
| 		<MkSelect v-model="logLevel"> | ||||
| 			<template #label>Level</template> | ||||
| 			<option value="all">All</option> | ||||
| 			<option value="info">Info</option> | ||||
| 			<option value="success">Success</option> | ||||
| 			<option value="warning">Warning</option> | ||||
| 			<option value="error">Error</option> | ||||
| 			<option value="debug">Debug</option> | ||||
| 		</MkSelect> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="logs"> | ||||
| 		<code v-for="log in logs" :key="log.id" :class="log.level"> | ||||
| 			<details> | ||||
| 				<summary><MkTime :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary> | ||||
| 				<!--<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>--> | ||||
| 			</details> | ||||
| 		</code> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<MkButton @click="deleteAllLogs()" primary><i class="fas fa-trash-alt"></i> {{ $ts.deleteAll }}</MkButton> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import MkInput from '@client/components/form/input.vue'; | ||||
| import MkSelect from '@client/components/form/select.vue'; | ||||
| import MkTextarea from '@client/components/form/textarea.vue'; | ||||
| import * as os from '@client/os'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, | ||||
| 		MkInput, | ||||
| 		MkSelect, | ||||
| 		MkTextarea, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.serverLogs, | ||||
| 				icon: 'fas fa-stream' | ||||
| 			}, | ||||
| 			logs: [], | ||||
| 			logLevel: 'all', | ||||
| 			logDomain: '', | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		logLevel() { | ||||
| 			this.logs = []; | ||||
| 			this.fetchLogs(); | ||||
| 		}, | ||||
| 		logDomain() { | ||||
| 			this.logs = []; | ||||
| 			this.fetchLogs(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.fetchLogs(); | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetchLogs() { | ||||
| 			os.api('admin/logs', { | ||||
| 				level: this.logLevel === 'all' ? null : this.logLevel, | ||||
| 				domain: this.logDomain === '' ? null : this.logDomain, | ||||
| 				limit: 30 | ||||
| 			}).then(logs => { | ||||
| 				this.logs = logs.reverse(); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		deleteAllLogs() { | ||||
| 			os.apiWithDialog('admin/delete-logs'); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | @ -1,127 +0,0 @@ | |||
| <template> | ||||
| <FormBase> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<FormSuspense :p="fetchStats" v-slot="{ result: stats }"> | ||||
| 			<FormGroup> | ||||
| 				<FormKeyValueView> | ||||
| 					<template #key>Users</template> | ||||
| 					<template #value>{{ number(stats.originalUsersCount) }}</template> | ||||
| 				</FormKeyValueView> | ||||
| 				<FormKeyValueView> | ||||
| 					<template #key>Notes</template> | ||||
| 					<template #value>{{ number(stats.originalNotesCount) }}</template> | ||||
| 				</FormKeyValueView> | ||||
| 			</FormGroup> | ||||
| 		</FormSuspense> | ||||
| 	 | ||||
| 		<div class="_debobigegoItem"> | ||||
| 			<div class="_debobigegoPanel"> | ||||
| 				<MkInstanceStats :chart-limit="300" :detailed="true"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<XMetrics/> | ||||
| 
 | ||||
| 		<FormSuspense :p="fetchServerInfo" v-slot="{ result: serverInfo }"> | ||||
| 			<FormGroup> | ||||
| 				<FormKeyValueView> | ||||
| 					<template #key>Node.js</template> | ||||
| 					<template #value>{{ serverInfo.node }}</template> | ||||
| 				</FormKeyValueView> | ||||
| 				<FormKeyValueView> | ||||
| 					<template #key>PostgreSQL</template> | ||||
| 					<template #value>{{ serverInfo.psql }}</template> | ||||
| 				</FormKeyValueView> | ||||
| 				<FormKeyValueView> | ||||
| 					<template #key>Redis</template> | ||||
| 					<template #value>{{ serverInfo.redis }}</template> | ||||
| 				</FormKeyValueView> | ||||
| 			</FormGroup> | ||||
| 		</FormSuspense> | ||||
| 	</FormSuspense> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent, markRaw } from 'vue'; | ||||
| import FormKeyValueView from '@client/components/debobigego/key-value-view.vue'; | ||||
| import FormInput from '@client/components/debobigego/input.vue'; | ||||
| import FormButton from '@client/components/debobigego/button.vue'; | ||||
| import FormBase from '@client/components/debobigego/base.vue'; | ||||
| import FormGroup from '@client/components/debobigego/group.vue'; | ||||
| import FormTextarea from '@client/components/debobigego/textarea.vue'; | ||||
| import FormInfo from '@client/components/debobigego/info.vue'; | ||||
| import FormSuspense from '@client/components/debobigego/suspense.vue'; | ||||
| import MkInstanceStats from '@client/components/instance-stats.vue'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import MkSelect from '@client/components/form/select.vue'; | ||||
| import MkInput from '@client/components/form/input.vue'; | ||||
| import MkContainer from '@client/components/ui/container.vue'; | ||||
| import MkFolder from '@client/components/ui/folder.vue'; | ||||
| import { version, url } from '@client/config'; | ||||
| import bytes from '@client/filters/bytes'; | ||||
| import number from '@client/filters/number'; | ||||
| import MkInstanceInfo from './instance.vue'; | ||||
| import XMetrics from './metrics.vue'; | ||||
| import * as os from '@client/os'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormBase, | ||||
| 		FormSuspense, | ||||
| 		FormGroup, | ||||
| 		FormInfo, | ||||
| 		FormKeyValueView, | ||||
| 		MkInstanceStats, | ||||
| 		XMetrics, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.overview, | ||||
| 				icon: 'fas fa-tachometer-alt', | ||||
| 				bg: 'var(--bg)', | ||||
| 			}, | ||||
| 			page: 'index', | ||||
| 			version, | ||||
| 			url, | ||||
| 			stats: null, | ||||
| 			meta: null, | ||||
| 			fetchStats: () => os.api('stats', {}), | ||||
| 			fetchServerInfo: () => os.api('admin/server-info', {}), | ||||
| 			fetchJobs: () => os.api('admin/queue/deliver-delayed', {}), | ||||
| 			fetchModLogs: () => os.api('admin/show-moderation-logs', {}), | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	async mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		async init() { | ||||
| 			this.meta = await os.api('meta', { detail: true }); | ||||
| 		}, | ||||
| 	 | ||||
| 		async showInstanceInfo(q) { | ||||
| 			let instance = q; | ||||
| 			if (typeof q === 'string') { | ||||
| 				instance = await os.api('federation/show-instance', { | ||||
| 					host: q | ||||
| 				}); | ||||
| 			} | ||||
| 			os.popup(MkInstanceInfo, { | ||||
| 				instance: instance | ||||
| 			}, {}, 'closed'); | ||||
| 		}, | ||||
| 
 | ||||
| 		bytes, | ||||
| 
 | ||||
| 		number, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | @ -1,218 +0,0 @@ | |||
| <template> | ||||
| <div class="_debobigegoItem"> | ||||
| 	<div class="_debobigegoLabel"><slot name="title"></slot></div> | ||||
| 	<div class="_debobigegoPanel pumxzjhg"> | ||||
| 		<div class="_table status"> | ||||
| 			<div class="_row"> | ||||
| 				<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div> | ||||
| 				<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div> | ||||
| 				<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div> | ||||
| 				<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class=""> | ||||
| 			<canvas ref="chart"></canvas> | ||||
| 		</div> | ||||
| 		<div class="jobs"> | ||||
| 			<div v-if="jobs.length > 0"> | ||||
| 				<div v-for="job in jobs" :key="job[0]"> | ||||
| 					<span>{{ job[0] }}</span> | ||||
| 					<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, markRaw } from 'vue'; | ||||
| import Chart from 'chart.js'; | ||||
| import number from '@client/filters/number'; | ||||
| 
 | ||||
| const alpha = (hex, a) => { | ||||
| 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; | ||||
| 	const r = parseInt(result[1], 16); | ||||
| 	const g = parseInt(result[2], 16); | ||||
| 	const b = parseInt(result[3], 16); | ||||
| 	return `rgba(${r}, ${g}, ${b}, ${a})`; | ||||
| }; | ||||
| import * as os from '@client/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		domain: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 		connection: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			chart: null, | ||||
| 			jobs: [], | ||||
| 			activeSincePrevTick: 0, | ||||
| 			active: 0, | ||||
| 			waiting: 0, | ||||
| 			delayed: 0, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.fetchJobs(); | ||||
| 
 | ||||
| 		// TODO: var(--panel)の色が暗いか明るいかで判定する | ||||
| 		const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; | ||||
| 
 | ||||
| 		Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||
| 
 | ||||
| 		this.chart = markRaw(new Chart(this.$refs.chart, { | ||||
| 			type: 'line', | ||||
| 			data: { | ||||
| 				labels: [], | ||||
| 				datasets: [{ | ||||
| 					label: 'Process', | ||||
| 					pointRadius: 0, | ||||
| 					lineTension: 0, | ||||
| 					borderWidth: 2, | ||||
| 					borderColor: '#00E396', | ||||
| 					backgroundColor: alpha('#00E396', 0.1), | ||||
| 					data: [] | ||||
| 				}, { | ||||
| 					label: 'Active', | ||||
| 					pointRadius: 0, | ||||
| 					lineTension: 0, | ||||
| 					borderWidth: 2, | ||||
| 					borderColor: '#00BCD4', | ||||
| 					backgroundColor: alpha('#00BCD4', 0.1), | ||||
| 					data: [] | ||||
| 				}, { | ||||
| 					label: 'Waiting', | ||||
| 					pointRadius: 0, | ||||
| 					lineTension: 0, | ||||
| 					borderWidth: 2, | ||||
| 					borderColor: '#FFB300', | ||||
| 					backgroundColor: alpha('#FFB300', 0.1), | ||||
| 					data: [] | ||||
| 				}, { | ||||
| 					label: 'Delayed', | ||||
| 					pointRadius: 0, | ||||
| 					lineTension: 0, | ||||
| 					borderWidth: 2, | ||||
| 					borderColor: '#E53935', | ||||
| 					borderDash: [5, 5], | ||||
| 					fill: false, | ||||
| 					data: [] | ||||
| 				}] | ||||
| 			}, | ||||
| 			options: { | ||||
| 				aspectRatio: 3, | ||||
| 				layout: { | ||||
| 					padding: { | ||||
| 						left: 16, | ||||
| 						right: 16, | ||||
| 						top: 16, | ||||
| 						bottom: 12 | ||||
| 					} | ||||
| 				}, | ||||
| 				legend: { | ||||
| 					position: 'bottom', | ||||
| 					labels: { | ||||
| 						boxWidth: 16, | ||||
| 					} | ||||
| 				}, | ||||
| 				scales: { | ||||
| 					xAxes: [{ | ||||
| 						gridLines: { | ||||
| 							display: false, | ||||
| 							color: gridColor, | ||||
| 							zeroLineColor: gridColor, | ||||
| 						}, | ||||
| 						ticks: { | ||||
| 							display: false | ||||
| 						} | ||||
| 					}], | ||||
| 					yAxes: [{ | ||||
| 						position: 'right', | ||||
| 						gridLines: { | ||||
| 							display: true, | ||||
| 							color: gridColor, | ||||
| 							zeroLineColor: gridColor, | ||||
| 						}, | ||||
| 						ticks: { | ||||
| 							display: false, | ||||
| 						} | ||||
| 					}] | ||||
| 				}, | ||||
| 				tooltips: { | ||||
| 					intersect: false, | ||||
| 					mode: 'index', | ||||
| 				} | ||||
| 			} | ||||
| 		})); | ||||
| 
 | ||||
| 		this.connection.on('stats', this.onStats); | ||||
| 		this.connection.on('statsLog', this.onStatsLog); | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeUnmount() { | ||||
| 		this.connection.off('stats', this.onStats); | ||||
| 		this.connection.off('statsLog', this.onStatsLog); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		onStats(stats) { | ||||
| 			this.activeSincePrevTick = stats[this.domain].activeSincePrevTick; | ||||
| 			this.active = stats[this.domain].active; | ||||
| 			this.waiting = stats[this.domain].waiting; | ||||
| 			this.delayed = stats[this.domain].delayed; | ||||
| 			this.chart.data.labels.push(''); | ||||
| 			this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick); | ||||
| 			this.chart.data.datasets[1].data.push(stats[this.domain].active); | ||||
| 			this.chart.data.datasets[2].data.push(stats[this.domain].waiting); | ||||
| 			this.chart.data.datasets[3].data.push(stats[this.domain].delayed); | ||||
| 			if (this.chart.data.datasets[0].data.length > 200) { | ||||
| 				this.chart.data.labels.shift(); | ||||
| 				this.chart.data.datasets[0].data.shift(); | ||||
| 				this.chart.data.datasets[1].data.shift(); | ||||
| 				this.chart.data.datasets[2].data.shift(); | ||||
| 				this.chart.data.datasets[3].data.shift(); | ||||
| 			} | ||||
| 			this.chart.update(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onStatsLog(statsLog) { | ||||
| 			for (const stats of [...statsLog].reverse()) { | ||||
| 				this.onStats(stats); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		fetchJobs() { | ||||
| 			os.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => { | ||||
| 				this.jobs = jobs; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		number | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .pumxzjhg { | ||||
| 	> .status { | ||||
| 		padding: 16px; | ||||
| 		border-bottom: solid 0.5px var(--divider); | ||||
| 	} | ||||
| 
 | ||||
| 	> .jobs { | ||||
| 		padding: 16px; | ||||
| 		border-top: solid 0.5px var(--divider); | ||||
| 		max-height: 180px; | ||||
| 		overflow: auto; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -16,11 +16,13 @@ | |||
| 	</FormSection> | ||||
| 	<FormSection> | ||||
| 		<template #label>{{ $ts._exportOrImport.muteList }}</template> | ||||
| 		<MkButton :class="$style.button" inline @click="doExport('mute')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="doExport('muting')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="doImport('muting', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> | ||||
| 	</FormSection> | ||||
| 	<FormSection> | ||||
| 		<template #label>{{ $ts._exportOrImport.blockingList }}</template> | ||||
| 		<MkButton :class="$style.button" inline @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="doImport('blocking', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> | ||||
| 	</FormSection> | ||||
| </div> | ||||
| </template> | ||||
|  | @ -58,11 +60,11 @@ export default defineComponent({ | |||
| 	methods: { | ||||
| 		doExport(target) { | ||||
| 			os.api( | ||||
| 				target == 'notes' ? 'i/export-notes' : | ||||
| 				target == 'following' ? 'i/export-following' : | ||||
| 				target == 'blocking' ? 'i/export-blocking' : | ||||
| 				target == 'user-lists' ? 'i/export-user-lists' : | ||||
| 				target == 'mute' ? 'i/export-mute' : | ||||
| 				target === 'notes' ? 'i/export-notes' : | ||||
| 				target === 'following' ? 'i/export-following' : | ||||
| 				target === 'blocking' ? 'i/export-blocking' : | ||||
| 				target === 'user-lists' ? 'i/export-user-lists' : | ||||
| 				target === 'muting' ? 'i/export-mute' : | ||||
| 				null, {}) | ||||
| 			.then(() => { | ||||
| 				os.dialog({ | ||||
|  | @ -81,8 +83,10 @@ export default defineComponent({ | |||
| 			const file = await selectFile(e.currentTarget || e.target); | ||||
| 			 | ||||
| 			os.api( | ||||
| 				target == 'following' ? 'i/import-following' : | ||||
| 				target == 'user-lists' ? 'i/import-user-lists' : | ||||
| 				target === 'following' ? 'i/import-following' : | ||||
| 				target === 'user-lists' ? 'i/import-user-lists' : | ||||
| 				target === 'muting' ? 'i/import-muting' : | ||||
| 				target === 'blocking' ? 'i/import-blocking' : | ||||
| 				null, { | ||||
| 					fileId: file.id | ||||
| 			}).then(() => { | ||||
|  |  | |||
|  | @ -5,6 +5,10 @@ | |||
| 		<FormSwitch v-model="autoAcceptFollowed" :disabled="!isLocked" @update:modelValue="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch> | ||||
| 		<template #caption>{{ $ts.lockedAccountInfo }}</template> | ||||
| 	</FormGroup> | ||||
| 	<FormSwitch v-model="publicReactions" @update:modelValue="save()"> | ||||
| 		{{ $ts.makeReactionsPublic }} | ||||
| 		<template #desc>{{ $ts.makeReactionsPublicDescription }}</template> | ||||
| 	</FormSwitch> | ||||
| 	<FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()"> | ||||
| 		{{ $ts.hideOnlineStatus }} | ||||
| 		<template #desc>{{ $ts.hideOnlineStatusDescription }}</template> | ||||
|  | @ -64,6 +68,7 @@ export default defineComponent({ | |||
| 			noCrawle: false, | ||||
| 			isExplorable: false, | ||||
| 			hideOnlineStatus: false, | ||||
| 			publicReactions: false, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -80,6 +85,7 @@ export default defineComponent({ | |||
| 		this.noCrawle = this.$i.noCrawle; | ||||
| 		this.isExplorable = this.$i.isExplorable; | ||||
| 		this.hideOnlineStatus = this.$i.hideOnlineStatus; | ||||
| 		this.publicReactions = this.$i.publicReactions; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
|  | @ -94,6 +100,7 @@ export default defineComponent({ | |||
| 				noCrawle: !!this.noCrawle, | ||||
| 				isExplorable: !!this.isExplorable, | ||||
| 				hideOnlineStatus: !!this.hideOnlineStatus, | ||||
| 				publicReactions: !!this.publicReactions, | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
|  | @ -10,13 +10,13 @@ | |||
| 		</optgroup> | ||||
| 	</FormSelect> | ||||
| 	<template v-if="selectedTheme"> | ||||
| 		<FormInput readonly :value="selectedTheme.author"> | ||||
| 		<FormInput readonly :modelValue="selectedTheme.author"> | ||||
| 			<span>{{ $ts.author }}</span> | ||||
| 		</FormInput> | ||||
| 		<FormTextarea readonly :value="selectedTheme.desc" v-if="selectedTheme.desc"> | ||||
| 		<FormTextarea readonly :modelValue="selectedTheme.desc" v-if="selectedTheme.desc"> | ||||
| 			<span>{{ $ts._theme.description }}</span> | ||||
| 		</FormTextarea> | ||||
| 		<FormTextarea readonly tall :value="selectedThemeCode"> | ||||
| 		<FormTextarea readonly tall :modelValue="selectedThemeCode"> | ||||
| 			<span>{{ $ts._theme.code }}</span> | ||||
| 			<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template> | ||||
| 		</FormTextarea> | ||||
|  | @ -28,12 +28,12 @@ | |||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import * as JSON5 from 'json5'; | ||||
| import FormTextarea from '@client/components/form/textarea.vue'; | ||||
| import FormSelect from '@client/components/form/select.vue'; | ||||
| import FormRadios from '@client/components/form/radios.vue'; | ||||
| import FormTextarea from '@client/components/debobigego/textarea.vue'; | ||||
| import FormSelect from '@client/components/debobigego/select.vue'; | ||||
| import FormRadios from '@client/components/debobigego/radios.vue'; | ||||
| import FormBase from '@client/components/debobigego/base.vue'; | ||||
| import FormGroup from '@client/components/debobigego/group.vue'; | ||||
| import FormInput from '@client/components/form/input.vue'; | ||||
| import FormInput from '@client/components/debobigego/input.vue'; | ||||
| import FormButton from '@client/components/debobigego/button.vue'; | ||||
| import { Theme, builtinThemes } from '@client/scripts/theme'; | ||||
| import copyToClipboard from '@client/scripts/copy-to-clipboard'; | ||||
|  |  | |||
|  | @ -1,22 +1,38 @@ | |||
| <template> | ||||
| <div class=""> | ||||
| 	<section class="_section"> | ||||
| 		<div class="_title" v-if="title">{{ title }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<XPostForm v-if="!posted" fixed :instant="true" :initial-text="initialText" @posted="posted = true" class="_panel"/> | ||||
| 			<MkButton v-else primary @click="close()">{{ $ts.close }}</MkButton> | ||||
| 			<XPostForm | ||||
| 				v-if="state === 'writing'" | ||||
| 				fixed | ||||
| 				:share="true" | ||||
| 				:initial-text="initialText" | ||||
| 				:initial-visibility="visibility" | ||||
| 				:initial-files="files" | ||||
| 				:initial-local-only="localOnly" | ||||
| 				:reply="reply" | ||||
| 				:renote="renote" | ||||
| 				:visible-users="visibleUsers" | ||||
| 				@posted="state = 'posted'" | ||||
| 				class="_panel" | ||||
| 			/> | ||||
| 			<MkButton v-else-if="state === 'posted'" primary @click="close()" class="close">{{ $ts.close }}</MkButton> | ||||
| 		</div> | ||||
| 		<div class="_footer" v-if="url">{{ url }}</div> | ||||
| 	</section> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| // SPECIFICATION: /src/docs/ja-JP/advanced/share-page.md | ||||
| 
 | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkButton from '@client/components/ui/button.vue'; | ||||
| import XPostForm from '@client/components/post-form.vue'; | ||||
| import * as os from '@client/os'; | ||||
| import { noteVisibilities } from '@/types'; | ||||
| import { parseAcct } from '@/misc/acct'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
|  | @ -30,35 +46,139 @@ export default defineComponent({ | |||
| 				title: this.$ts.share, | ||||
| 				icon: 'fas fa-share-alt' | ||||
| 			}, | ||||
| 			title: null, | ||||
| 			text: null, | ||||
| 			url: null, | ||||
| 			initialText: null, | ||||
| 			posted: false, | ||||
| 			state: 'fetching' as 'fetching' | 'writing' | 'posted', | ||||
| 
 | ||||
| 			title: null as string | null, | ||||
| 			initialText: null as string | null, | ||||
| 			reply: null as Misskey.entities.Note | null, | ||||
| 			renote: null as Misskey.entities.Note | null, | ||||
| 			visibility: null as string | null, | ||||
| 			localOnly: null as boolean | null, | ||||
| 			files: [] as Misskey.entities.DriveFile[], | ||||
| 			visibleUsers: [] as Misskey.entities.User[], | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 	async created() { | ||||
| 		const urlParams = new URLSearchParams(window.location.search); | ||||
| 
 | ||||
| 		this.title = urlParams.get('title'); | ||||
| 		this.text = urlParams.get('text'); | ||||
| 		this.url = urlParams.get('url'); | ||||
| 		 | ||||
| 		let text = ''; | ||||
| 		if (this.title) text += `【${this.title}】\n`; | ||||
| 		if (this.text) text += `${this.text}\n`; | ||||
| 		if (this.url) text += `${this.url}`; | ||||
| 		this.initialText = text.trim(); | ||||
| 		const text = urlParams.get('text'); | ||||
| 		const url = urlParams.get('url'); | ||||
| 
 | ||||
| 		let noteText = ''; | ||||
| 		if (this.title) noteText += `[ ${this.title} ]\n`; | ||||
| 		// Googleニュース対策 | ||||
| 		if (text?.startsWith(`${this.title}.\n`)) noteText += text.replace(`${this.title}.\n`, ''); | ||||
| 		else if (text && this.title !== text) noteText += `${text}\n`; | ||||
| 		if (url) noteText += `${url}`; | ||||
| 		this.initialText = noteText.trim(); | ||||
| 
 | ||||
| 		const visibility = urlParams.get('visibility'); | ||||
| 		if (noteVisibilities.includes(visibility)) { | ||||
| 			this.visibility = visibility; | ||||
| 		} | ||||
| 
 | ||||
| 		if (this.visibility === 'specified') { | ||||
| 			const visibleUserIds = urlParams.get('visibleUserIds'); | ||||
| 			const visibleAccts = urlParams.get('visibleAccts'); | ||||
| 			await Promise.all( | ||||
| 				[ | ||||
| 					...(visibleUserIds ? visibleUserIds.split(',').map(userId => ({ userId })) : []), | ||||
| 					...(visibleAccts ? visibleAccts.split(',').map(parseAcct) : []) | ||||
| 				] | ||||
| 				// TypeScriptの指示通りに変換する | ||||
| 				.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q) | ||||
| 				.map(q => os.api('users/show', q) | ||||
| 					.then(user => { | ||||
| 						this.visibleUsers.push(user); | ||||
| 					}, () => { | ||||
| 						console.error(`Invalid user query: ${JSON.stringify(q)}`); | ||||
| 					}) | ||||
| 				) | ||||
| 			); | ||||
| 		} | ||||
| 
 | ||||
| 		const localOnly = urlParams.get('localOnly'); | ||||
| 		if (localOnly === '0') this.localOnly = false; | ||||
| 		else if (localOnly === '1') this.localOnly = true; | ||||
| 
 | ||||
| 		try { | ||||
| 			//#region Reply | ||||
| 			const replyId = urlParams.get('replyId'); | ||||
| 			const replyUri = urlParams.get('replyUri'); | ||||
| 			if (replyId) { | ||||
| 				this.reply = await os.api('notes/show', { | ||||
| 					noteId: replyId | ||||
| 				}); | ||||
| 			} else if (replyUri) { | ||||
| 				const obj = await os.api('ap/show', { | ||||
| 					uri: replyUri | ||||
| 				}); | ||||
| 				if (obj.type === 'Note') { | ||||
| 					this.reply = obj.object; | ||||
| 				} | ||||
| 			} | ||||
| 			//#endregion | ||||
| 
 | ||||
| 			//#region Renote | ||||
| 			const renoteId = urlParams.get('renoteId'); | ||||
| 			const renoteUri = urlParams.get('renoteUri'); | ||||
| 			if (renoteId) { | ||||
| 				this.renote = await os.api('notes/show', { | ||||
| 					noteId: renoteId | ||||
| 				}); | ||||
| 			} else if (renoteUri) { | ||||
| 				const obj = await os.api('ap/show', { | ||||
| 					uri: renoteUri | ||||
| 				}); | ||||
| 				if (obj.type === 'Note') { | ||||
| 					this.renote = obj.object; | ||||
| 				} | ||||
| 			} | ||||
| 			//#endregion | ||||
| 
 | ||||
| 			//#region Drive files | ||||
| 			const fileIds = urlParams.get('fileIds'); | ||||
| 			if (fileIds) { | ||||
| 				await Promise.all( | ||||
| 					fileIds.split(',') | ||||
| 					.map(fileId => os.api('drive/files/show', { fileId }) | ||||
| 						.then(file => { | ||||
| 							this.files.push(file); | ||||
| 						}, () => { | ||||
| 							console.error(`Failed to fetch a file ${fileId}`); | ||||
| 						}) | ||||
| 					) | ||||
| 				); | ||||
| 			} | ||||
| 			//#endregion | ||||
| 		} catch (e) { | ||||
| 			os.dialog({ | ||||
| 				type: 'error', | ||||
| 				title: e.message, | ||||
| 				text: e.name | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		this.state = 'writing'; | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		close() { | ||||
| 			window.close() | ||||
| 			window.close(); | ||||
| 
 | ||||
| 			// 閉じなければ100ms後タイムラインに | ||||
| 			setTimeout(() => { | ||||
| 				this.$router.push('/'); | ||||
| 			}, 100); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .close { | ||||
| 	margin: 16px auto; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -65,4 +65,11 @@ export default defineComponent({ | |||
| 		background: var(--bg); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| ._fitSide_ .yrzkoczt { | ||||
| 	> .tab { | ||||
| 		padding-left: var(--margin); | ||||
| 		padding-right: var(--margin); | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -181,6 +181,7 @@ | |||
| 				</template> | ||||
| 				<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/> | ||||
| 				<XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/> | ||||
| 				<XReactions v-else-if="page === 'reactions'" :user="user" class="_gap"/> | ||||
| 				<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/> | ||||
| 				<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/> | ||||
| 				<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/> | ||||
|  | @ -223,6 +224,7 @@ export default defineComponent({ | |||
| 		MkTab, | ||||
| 		MkInfo, | ||||
| 		XFollowList: defineAsyncComponent(() => import('./follow-list.vue')), | ||||
| 		XReactions: defineAsyncComponent(() => import('./reactions.vue')), | ||||
| 		XClips: defineAsyncComponent(() => import('./clips.vue')), | ||||
| 		XPages: defineAsyncComponent(() => import('./pages.vue')), | ||||
| 		XGallery: defineAsyncComponent(() => import('./gallery.vue')), | ||||
|  | @ -268,7 +270,12 @@ export default defineComponent({ | |||
| 					title: this.$ts.overview, | ||||
| 					icon: 'fas fa-home', | ||||
| 					onClick: () => { this.$router.push('/@' + getAcct(this.user)); }, | ||||
| 				}, { | ||||
| 				}, ...(this.$i && (this.$i.id === this.user.id)) || this.user.publicReactions ? [{ | ||||
| 					active: this.page === 'reactions', | ||||
| 					title: this.$ts.reaction, | ||||
| 					icon: 'fas fa-laugh', | ||||
| 					onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/reactions'); }, | ||||
| 				}] : [], { | ||||
| 					active: this.page === 'clips', | ||||
| 					title: this.$ts.clips, | ||||
| 					icon: 'fas fa-paperclip', | ||||
|  |  | |||
							
								
								
									
										81
									
								
								src/client/pages/user/reactions.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/client/pages/user/reactions.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,81 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<MkPagination :pagination="pagination" #default="{items}" ref="list"> | ||||
| 		<div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb"> | ||||
| 			<div class="header"> | ||||
| 				<MkAvatar class="avatar" :user="user"/> | ||||
| 				<MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/> | ||||
| 				<MkTime :time="item.createdAt" class="createdAt"/> | ||||
| 			</div> | ||||
| 			<MkNote :note="item.note" @update:note="updated(note, $event)" :key="item.id"/> | ||||
| 		</div> | ||||
| 	</MkPagination> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkPagination from '@client/components/ui/pagination.vue'; | ||||
| import MkNote from '@client/components/note.vue'; | ||||
| import MkReactionIcon from '@client/components/reaction-icon.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkPagination, | ||||
| 		MkNote, | ||||
| 		MkReactionIcon, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			pagination: { | ||||
| 				endpoint: 'users/reactions', | ||||
| 				limit: 20, | ||||
| 				params: { | ||||
| 					userId: this.user.id, | ||||
| 				} | ||||
| 			}, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		user() { | ||||
| 			this.$refs.list.reload(); | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .afdcfbfb { | ||||
| 	> .header { | ||||
| 		display: flex; | ||||
| 		align-items: center; | ||||
| 		padding: 8px 16px; | ||||
| 		margin-bottom: 8px; | ||||
| 		border-bottom: solid 2px var(--divider); | ||||
| 
 | ||||
| 		> .avatar { | ||||
| 			width: 24px; | ||||
| 			height: 24px; | ||||
| 			margin-right: 8px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .reaction { | ||||
| 			width: 32px; | ||||
| 			height: 32px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .createdAt { | ||||
| 			margin-left: auto; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -70,8 +70,8 @@ const defaultRoutes = [ | |||
| 	{ path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true }, | ||||
| 	{ path: '/my/clips', component: page('my-clips/index') }, | ||||
| 	{ path: '/scratchpad', component: page('scratchpad') }, | ||||
| 	{ path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) }, | ||||
| 	{ path: '/instance', component: page('instance/index') }, | ||||
| 	{ path: '/admin/:page(.*)?', component: page('admin/index'), props: route => ({ initialPage: route.params.page || null }) }, | ||||
| 	{ path: '/admin', component: page('admin/index') }, | ||||
| 	{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) }, | ||||
| 	{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) }, | ||||
| 	{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) }, | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import * as tinycolor from 'tinycolor2'; | ||||
| import Chart from 'chart.js'; | ||||
| import { Hpml } from './evaluator'; | ||||
| import { values, utils } from '@syuilo/aiscript'; | ||||
| import { Fn, HpmlScope } from '.'; | ||||
| import { Expr } from './expr'; | ||||
| import * as seedrandom from 'seedrandom'; | ||||
| 
 | ||||
| /* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color | ||||
| // https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
 | ||||
| Chart.pluginService.register({ | ||||
| 	beforeDraw: (chart, easing) => { | ||||
|  | @ -18,6 +18,7 @@ Chart.pluginService.register({ | |||
| 		} | ||||
| 	} | ||||
| }); | ||||
| */ | ||||
| 
 | ||||
| export function initAiLib(hpml: Hpml) { | ||||
| 	return { | ||||
|  | @ -49,11 +50,12 @@ export function initAiLib(hpml: Hpml) { | |||
| 			])); | ||||
| 		}), | ||||
| 		'MkPages:chart': values.FN_NATIVE(([id, opts]) => { | ||||
| 			/* TODO | ||||
| 			utils.assertString(id); | ||||
| 			utils.assertObject(opts); | ||||
| 			const canvas = hpml.canvases[id.value]; | ||||
| 			const color = getComputedStyle(document.documentElement).getPropertyValue('--accent'); | ||||
| 			Chart.defaults.global.defaultFontColor = '#555'; | ||||
| 			Chart.defaults.color = '#555'; | ||||
| 			const chart = new Chart(canvas, { | ||||
| 				type: opts.value.get('type').value, | ||||
| 				data: { | ||||
|  | @ -122,6 +124,7 @@ export function initAiLib(hpml: Hpml) { | |||
| 					}) | ||||
| 				} | ||||
| 			}); | ||||
| 			*/ | ||||
| 		}) | ||||
| 	}; | ||||
| } | ||||
|  |  | |||
|  | @ -20,12 +20,14 @@ export const builtinThemes = [ | |||
| 	require('@client/themes/l-apricot.json5'), | ||||
| 	require('@client/themes/l-rainy.json5'), | ||||
| 	require('@client/themes/l-vivid.json5'), | ||||
| 	require('@client/themes/l-sushi.json5'), | ||||
| 
 | ||||
| 	require('@client/themes/d-dark.json5'), | ||||
| 	require('@client/themes/d-persimmon.json5'), | ||||
| 	require('@client/themes/d-astro.json5'), | ||||
| 	require('@client/themes/d-future.json5'), | ||||
| 	require('@client/themes/d-botanical.json5'), | ||||
| 	require('@client/themes/d-pumpkin.json5'), | ||||
| 	require('@client/themes/d-black.json5'), | ||||
| ] as Theme[]; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea', | ||||
| 	base: 'dark', | ||||
| 	name: 'Mi Astro', | ||||
| 	name: 'Mi Astro Dark', | ||||
| 	author: 'syuilo', | ||||
| 	props: { | ||||
| 		bg: '#232125', | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	id: '32a637ef-b47a-4775-bb7b-bacbb823f865', | ||||
| 
 | ||||
| 	name: 'Mi Future', | ||||
| 	name: 'Mi Future Dark', | ||||
| 	author: 'syuilo', | ||||
| 
 | ||||
| 	base: 'dark', | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	id: 'c503d768-7c70-4db2-a4e6-08264304bc8d', | ||||
| 
 | ||||
| 	name: 'Mi Persimmon', | ||||
| 	name: 'Mi Persimmon Dark', | ||||
| 	author: 'syuilo', | ||||
| 
 | ||||
| 	base: 'dark', | ||||
|  |  | |||
							
								
								
									
										88
									
								
								src/client/themes/d-pumpkin.json5
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/client/themes/d-pumpkin.json5
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | |||
| { | ||||
| 	id: '0b64fef3-02c7-20b5-dd87-b3f77e2b4301', | ||||
| 
 | ||||
| 	name: 'Mi Pumpkin Dark', | ||||
| 	author: 'syuilo', | ||||
| 
 | ||||
| 	base: 'dark', | ||||
| 
 | ||||
| 	props: { | ||||
| 		X2: ':darken<2<@panel', | ||||
| 		X3: 'rgba(255, 255, 255, 0.05)', | ||||
| 		X4: 'rgba(255, 255, 255, 0.1)', | ||||
| 		X5: 'rgba(255, 255, 255, 0.05)', | ||||
| 		X6: 'rgba(255, 255, 255, 0.15)', | ||||
| 		X7: 'rgba(255, 255, 255, 0.05)', | ||||
| 		X8: ':lighten<5<@accent', | ||||
| 		X9: ':darken<5<@accent', | ||||
| 		bg: 'rgb(37, 32, 47)', | ||||
| 		fg: '#e0d5c0', | ||||
| 		X10: ':alpha<0.4<@accent', | ||||
| 		X11: 'rgba(0, 0, 0, 0.3)', | ||||
| 		X12: 'rgba(255, 255, 255, 0.1)', | ||||
| 		X13: 'rgba(255, 255, 255, 0.15)', | ||||
| 		X14: ':alpha<0.5<@navBg', | ||||
| 		X15: ':alpha<0<@panel', | ||||
| 		X16: ':alpha<0.7<@panel', | ||||
| 		X17: ':alpha<0.8<@bg', | ||||
| 		cwBg: '#687390', | ||||
| 		cwFg: '#393f4f', | ||||
| 		link: 'rgb(172, 193, 68)', | ||||
| 		warn: '#ecb637', | ||||
| 		badge: '#31b1ce', | ||||
| 		error: '#ec4137', | ||||
| 		focus: ':alpha<0.3<@accent', | ||||
| 		navBg: '@panel', | ||||
| 		navFg: '@fg', | ||||
| 		panel: ':lighten<3<@bg', | ||||
| 		popup: ':lighten<3<@panel', | ||||
| 		accent: 'rgb(242, 133, 36)', | ||||
| 		header: ':alpha<0.7<@panel', | ||||
| 		infoBg: '#253142', | ||||
| 		infoFg: '#fff', | ||||
| 		renote: 'rgb(110, 179, 72)', | ||||
| 		shadow: 'rgba(0, 0, 0, 0.3)', | ||||
| 		divider: 'rgba(255, 255, 255, 0.1)', | ||||
| 		hashtag: 'rgb(188, 90, 255)', | ||||
| 		mention: 'rgb(72, 179, 139)', | ||||
| 		modalBg: 'rgba(0, 0, 0, 0.5)', | ||||
| 		success: '#86b300', | ||||
| 		buttonBg: 'rgba(255, 255, 255, 0.05)', | ||||
| 		switchBg: 'rgba(255, 255, 255, 0.15)', | ||||
| 		acrylicBg: ':alpha<0.5<@bg', | ||||
| 		cwHoverBg: '#707b97', | ||||
| 		indicator: '@accent', | ||||
| 		mentionMe: '@accent', | ||||
| 		messageBg: '@bg', | ||||
| 		navActive: '@accent', | ||||
| 		accentedBg: ':alpha<0.15<@accent', | ||||
| 		fgOnAccent: '#000', | ||||
| 		infoWarnBg: '#42321c', | ||||
| 		infoWarnFg: '#ffbd3e', | ||||
| 		navHoverFg: ':lighten<17<@fg', | ||||
| 		dateLabelFg: '@fg', | ||||
| 		inputBorder: 'rgba(255, 255, 255, 0.1)', | ||||
| 		panelBorder: '" solid 1px var(--divider)', | ||||
| 		accentDarken: ':darken<10<@accent', | ||||
| 		acrylicPanel: ':alpha<0.5<@panel', | ||||
| 		navIndicator: '@indicator', | ||||
| 		accentLighten: ':lighten<10<@accent', | ||||
| 		buttonHoverBg: 'rgba(255, 255, 255, 0.1)', | ||||
| 		driveFolderBg: ':alpha<0.3<@accent', | ||||
| 		fgHighlighted: ':lighten<3<@fg', | ||||
| 		fgTransparent: ':alpha<0.5<@fg', | ||||
| 		panelHeaderBg: ':lighten<3<@panel', | ||||
| 		panelHeaderFg: '@fg', | ||||
| 		buttonGradateA: '@accent', | ||||
| 		buttonGradateB: ':hue<20<@accent', | ||||
| 		htmlThemeColor: '@bg', | ||||
| 		panelHighlight: ':lighten<3<@panel', | ||||
| 		listItemHoverBg: 'rgba(255, 255, 255, 0.03)', | ||||
| 		scrollbarHandle: 'rgba(255, 255, 255, 0.2)', | ||||
| 		inputBorderHover: 'rgba(255, 255, 255, 0.2)', | ||||
| 		wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', | ||||
| 		fgTransparentWeak: ':alpha<0.75<@fg', | ||||
| 		panelHeaderDivider: 'rgba(0, 0, 0, 0)', | ||||
| 		scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', | ||||
| 	}, | ||||
| } | ||||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b', | ||||
| 
 | ||||
| 	name: 'Mi Apricot', | ||||
| 	name: 'Mi Apricot Light', | ||||
| 	author: 'syuilo', | ||||
| 
 | ||||
| 	base: 'light', | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96', | ||||
| 
 | ||||
| 	name: 'Mi Rainy', | ||||
| 	name: 'Mi Rainy Light', | ||||
| 	author: 'syuilo', | ||||
| 
 | ||||
| 	base: 'light', | ||||
|  |  | |||
							
								
								
									
										18
									
								
								src/client/themes/l-sushi.json5
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/client/themes/l-sushi.json5
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| { | ||||
| 	id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c', | ||||
| 
 | ||||
| 	name: 'Mi Sushi Light', | ||||
| 	author: 'syuilo', | ||||
| 
 | ||||
| 	base: 'light', | ||||
| 
 | ||||
| 	props: { | ||||
| 		accent: '#e36749', | ||||
| 		bg: '#f0eee9', | ||||
| 		fg: '#5f5f5f', | ||||
| 		renote: '@accent', | ||||
| 		link: '@accent', | ||||
| 		mention: '@accent', | ||||
| 		hashtag: '#229e82', | ||||
| 	}, | ||||
| } | ||||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	id: '6128c2a9-5c54-43fe-a47d-17942356470b', | ||||
| 
 | ||||
| 	name: 'Mi Vivid', | ||||
| 	name: 'Mi Vivid Light', | ||||
| 	author: 'syuilo', | ||||
| 
 | ||||
| 	base: 'light', | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ | |||
| 					</component> | ||||
| 				</template> | ||||
| 				<div class="divider"></div> | ||||
| 				<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" v-click-anime> | ||||
| 				<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" v-click-anime> | ||||
| 					<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span> | ||||
| 				</MkA> | ||||
| 				<button class="item _button" @click="more" v-click-anime> | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ export default defineComponent({ | |||
| 			type: Object, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		instant: { | ||||
| 		share: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
|  | @ -277,7 +277,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 		this.$nextTick(() => { | ||||
| 			// 書きかけの投稿を復元 | ||||
| 			if (!this.instant && !this.mention && !this.specified) { | ||||
| 			if (!this.share && !this.mention && !this.specified) { | ||||
| 				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey]; | ||||
| 				if (draft) { | ||||
| 					this.text = draft.data.text; | ||||
|  | @ -507,8 +507,6 @@ export default defineComponent({ | |||
| 		}, | ||||
| 
 | ||||
| 		saveDraft() { | ||||
| 			if (this.instant) return; | ||||
| 
 | ||||
| 			const data = JSON.parse(localStorage.getItem('drafts') || '{}'); | ||||
| 
 | ||||
| 			data[this.draftKey] = { | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue