mirror of
https://github.com/MisskeyIO/misskey
synced 2024-11-27 14:28:49 +09:00
Merge branch 'develop'
This commit is contained in:
commit
d0d5068f72
27
CHANGELOG.md
27
CHANGELOG.md
@ -2,12 +2,37 @@
|
|||||||
## 12.x.x (unreleased)
|
## 12.x.x (unreleased)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
- ページロードエラーページにリロードボタンを追加
|
|
||||||
|
|
||||||
### Bugfixes
|
### 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)
|
## 12.92.0 (2021/10/16)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
@ -9,9 +9,9 @@ It will also allow the reader to use the translation tool of their preference if
|
|||||||
## Issues
|
## Issues
|
||||||
Before creating an issue, please check the following:
|
Before creating an issue, please check the following:
|
||||||
- To avoid duplication, please search for similar issues before creating a new issue.
|
- To avoid duplication, please search for similar issues before creating a new issue.
|
||||||
- Do not use Issues as a question.
|
- Do not use Issues to ask questions or troubleshooting.
|
||||||
- Issues should only be used to feature requests, suggestions, and report problems.
|
- Issues should only be used to feature requests, suggestions, and bug tracking.
|
||||||
- Please ask questions in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3).
|
- Please ask questions or troubleshooting in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3).
|
||||||
|
|
||||||
## Before implementation
|
## 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.
|
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始まり**。
|
SQLでは配列のインデックスは**1始まり**。
|
||||||
`[a, b, c]`の `a`にアクセスしたいなら`[0]`ではなく`[1]`と書く
|
`[a, b, c]`の `a`にアクセスしたいなら`[0]`ではなく`[1]`と書く
|
||||||
|
|
||||||
|
### null IN
|
||||||
|
nullが含まれる可能性のあるカラムにINするときは、そのままだとおかしくなるのでORなどでnullのハンドリングをしよう。
|
||||||
|
|
||||||
### `undefined`にご用心
|
### `undefined`にご用心
|
||||||
MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。
|
MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。
|
||||||
MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください
|
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
|
: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
|
: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: "تنزيل"
|
download: "تنزيل"
|
||||||
driveFileDeleteConfirm: "أمتأكد من حذف ملف {name}؟ كل الملاحظات المُرفق بها هذا الملف ستحذف."
|
driveFileDeleteConfirm: "أمتأكد من حذف ملف {name}؟ كل الملاحظات المُرفق بها هذا الملف ستحذف."
|
||||||
unfollowConfirm: "أمتأكد من إلغاء متابعة {name}؟"
|
unfollowConfirm: "أمتأكد من إلغاء متابعة {name}؟"
|
||||||
|
importRequested: "يستغرق الاستيراد بعض الوقت"
|
||||||
lists: "القوائم"
|
lists: "القوائم"
|
||||||
noLists: "ليس لديك أية قائمة"
|
noLists: "ليس لديك أية قائمة"
|
||||||
note: "ملاحظة"
|
note: "ملاحظة"
|
||||||
@ -76,6 +77,7 @@ error: "خطأ"
|
|||||||
somethingHappened: "حدث خطأ"
|
somethingHappened: "حدث خطأ"
|
||||||
retry: "حاول مجددًا"
|
retry: "حاول مجددًا"
|
||||||
pageLoadError: "فشل تحميل الصفحة"
|
pageLoadError: "فشل تحميل الصفحة"
|
||||||
|
serverIsDead: "الخادم لا يستجيب، حاول بعد قليل"
|
||||||
enterListName: "اسم القائمة"
|
enterListName: "اسم القائمة"
|
||||||
privacy: "الخصوصية"
|
privacy: "الخصوصية"
|
||||||
makeFollowManuallyApprove: "القبول يدويا طلبات الإشتراك"
|
makeFollowManuallyApprove: "القبول يدويا طلبات الإشتراك"
|
||||||
@ -97,6 +99,7 @@ add: "إضافة"
|
|||||||
reaction: "تفاعل"
|
reaction: "تفاعل"
|
||||||
rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات"
|
rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات"
|
||||||
attachCancel: "أزل المرفق"
|
attachCancel: "أزل المرفق"
|
||||||
|
markAsSensitive: "علّمه كمحتوى حساس"
|
||||||
enterFileName: "ادخل اسم الملف"
|
enterFileName: "ادخل اسم الملف"
|
||||||
mute: "اكتم"
|
mute: "اكتم"
|
||||||
unmute: "إلغاء الكتم"
|
unmute: "إلغاء الكتم"
|
||||||
@ -109,15 +112,20 @@ unblockConfirm: "أمتأكد من إلغاء حجب هذا الحساب؟"
|
|||||||
selectList: "اختر قائمة"
|
selectList: "اختر قائمة"
|
||||||
editWidgetsExit: "تم"
|
editWidgetsExit: "تم"
|
||||||
customEmojis: "إيموجي مخصص"
|
customEmojis: "إيموجي مخصص"
|
||||||
|
emoji: "الوجوه التعبيرية"
|
||||||
|
emojis: "الوجوه التعبيرية"
|
||||||
|
emojiName: "اسم الوجه التعبيري"
|
||||||
|
emojiUrl: "رابط الوجه التعبيري"
|
||||||
addEmoji: "إضافة إيموجي"
|
addEmoji: "إضافة إيموجي"
|
||||||
|
settingGuide: "الإعدادات المستحسنة"
|
||||||
cacheRemoteFiles: "خزن مؤقتا الملفات البعيدة"
|
cacheRemoteFiles: "خزن مؤقتا الملفات البعيدة"
|
||||||
autoAcceptFollowed: "اقبل طلبات المتابعة تلقائيا من الحسابات المتابَعة"
|
autoAcceptFollowed: "اقبل طلبات المتابعة تلقائيا من الحسابات المتابَعة"
|
||||||
loginFailed: "فشل الولوج"
|
loginFailed: "فشل الولوج"
|
||||||
showOnRemote: "رؤيته على مثيل الخادم البُعدي"
|
showOnRemote: "رؤيته على مثيل الخادم البُعدي"
|
||||||
general: "الرئيسية"
|
general: "الرئيسية"
|
||||||
wallpaper: "خلفية الشاشة"
|
wallpaper: "الخلفية"
|
||||||
setWallpaper: "استخدم خلفية الشاشة"
|
setWallpaper: "عيّن خلفية"
|
||||||
removeWallpaper: "إزالة خلفية الشاشة"
|
removeWallpaper: "أزل الخلفية"
|
||||||
searchWith: "البحث: {q}"
|
searchWith: "البحث: {q}"
|
||||||
youHaveNoLists: "لا تمتلك أية قائمة"
|
youHaveNoLists: "لا تمتلك أية قائمة"
|
||||||
followConfirm: "أتريد متابعة {name}؟"
|
followConfirm: "أتريد متابعة {name}؟"
|
||||||
@ -182,7 +190,7 @@ removeAreYouSure: "متأكد من أنك تريد حذف {x}؟"
|
|||||||
deleteAreYouSure: "متأكد من أنك تريد حذف {x}؟"
|
deleteAreYouSure: "متأكد من أنك تريد حذف {x}؟"
|
||||||
resetAreYouSure: "هل تريد إعادة التعيين؟"
|
resetAreYouSure: "هل تريد إعادة التعيين؟"
|
||||||
saved: "تم حفظه"
|
saved: "تم حفظه"
|
||||||
messaging: "الدردشة"
|
messaging: "المحادثة"
|
||||||
upload: "تحميل"
|
upload: "تحميل"
|
||||||
fromDrive: "من المخزن"
|
fromDrive: "من المخزن"
|
||||||
fromUrl: "من عنوان URL"
|
fromUrl: "من عنوان URL"
|
||||||
@ -194,7 +202,7 @@ explore: "استكشاف"
|
|||||||
games: "ألعاب Misskey"
|
games: "ألعاب Misskey"
|
||||||
messageRead: "مقروءة"
|
messageRead: "مقروءة"
|
||||||
noMoreHistory: "لا يوجد المزيد من التاريخ"
|
noMoreHistory: "لا يوجد المزيد من التاريخ"
|
||||||
startMessaging: "ابدأ الدردشة"
|
startMessaging: "ابدأ محادثة"
|
||||||
nUsersRead: "تمت القراءة من {n}"
|
nUsersRead: "تمت القراءة من {n}"
|
||||||
agreeTo: "اوافق على {0}"
|
agreeTo: "اوافق على {0}"
|
||||||
tos: "شروط الخدمة"
|
tos: "شروط الخدمة"
|
||||||
@ -266,7 +274,17 @@ disablingTimelinesInfo: "سيتمكن المسؤولون ومن تعديل دا
|
|||||||
registration: "إنشاء حساب"
|
registration: "إنشاء حساب"
|
||||||
enableRegistration: "تفعيل إنشاء الحسابات الجديدة"
|
enableRegistration: "تفعيل إنشاء الحسابات الجديدة"
|
||||||
invite: "دعوة"
|
invite: "دعوة"
|
||||||
|
driveCapacityPerLocalAccount: "حصة التخزين لكل مستخدم محلي"
|
||||||
|
driveCapacityPerRemoteAccount: "حصة التخزين لكل مستخدم بعيد"
|
||||||
|
inMb: "بالميغابايت"
|
||||||
|
iconUrl: "رابط الأيقونة"
|
||||||
|
bannerUrl: "رابط صورة اللافتة"
|
||||||
|
backgroundImageUrl: "رابط صورة الخلفية"
|
||||||
basicInfo: "المعلومات الأساسية "
|
basicInfo: "المعلومات الأساسية "
|
||||||
|
pinnedUsers: "المستخدمون المثبتون"
|
||||||
|
pinnedUsersDescription: "قائمة المستخدمين المثبتين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده."
|
||||||
|
pinnedPages: "الصفحات المثبتة"
|
||||||
|
pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تثبيتها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده."
|
||||||
pinnedNotes: "ملاحظة مدبسة"
|
pinnedNotes: "ملاحظة مدبسة"
|
||||||
hcaptchaSiteKey: "مفتاح الموقع"
|
hcaptchaSiteKey: "مفتاح الموقع"
|
||||||
hcaptchaSecretKey: "المفتاح السري"
|
hcaptchaSecretKey: "المفتاح السري"
|
||||||
@ -279,12 +297,19 @@ manageAntennas: "إدارة الهوائيات"
|
|||||||
name: "الإسم"
|
name: "الإسم"
|
||||||
antennaSource: "مصدر الهوائي"
|
antennaSource: "مصدر الهوائي"
|
||||||
antennaKeywords: "الكلمات المفتاحية للإستقبال"
|
antennaKeywords: "الكلمات المفتاحية للإستقبال"
|
||||||
|
notifyAntenna: "نبهني بصول ملاحظات جديدة"
|
||||||
|
withFileAntenna: "ملاحظات تحوي ملفات فقط"
|
||||||
|
caseSensitive: "حساسية حالة الأحرف"
|
||||||
withReplies: "بالردود"
|
withReplies: "بالردود"
|
||||||
notesAndReplies: "الملاحظات والردود"
|
notesAndReplies: "الملاحظات والردود"
|
||||||
withFiles: "بالمرفقات"
|
withFiles: "بالمرفقات"
|
||||||
silence: "اكتم"
|
silence: "اكتم"
|
||||||
unsilence: "إلغاء الكتم"
|
unsilence: "إلغاء الكتم"
|
||||||
popularUsers: "المستخدمون الشائعون"
|
popularUsers: "المستخدمون الشائعون"
|
||||||
|
recentlyUpdatedUsers: "أصحاب النشاطات الأخيرة"
|
||||||
|
recentlyRegisteredUsers: "المستخدمون المنضمون حديثًا"
|
||||||
|
recentlyDiscoveredUsers: "المستخدمون المكتشفون حديثًا"
|
||||||
|
exploreUsersCount: "يوجد {count} مستخدم(ا)"
|
||||||
exploreFediverse: "استكشف الفديفرس"
|
exploreFediverse: "استكشف الفديفرس"
|
||||||
popularTags: "الوسوم الرائجة"
|
popularTags: "الوسوم الرائجة"
|
||||||
userList: "القوائم"
|
userList: "القوائم"
|
||||||
@ -297,11 +322,13 @@ moderator: "مشرِف"
|
|||||||
nUsersMentioned: "{n} مستخدمين تمت الإشارة إليهم"
|
nUsersMentioned: "{n} مستخدمين تمت الإشارة إليهم"
|
||||||
securityKey: "مفتاح الأمان"
|
securityKey: "مفتاح الأمان"
|
||||||
securityKeyName: "اسم المفتاح"
|
securityKeyName: "اسم المفتاح"
|
||||||
|
registerSecurityKey: "سجل مفتاح أمان"
|
||||||
lastUsed: "آخر استخدام"
|
lastUsed: "آخر استخدام"
|
||||||
unregister: "إلغاء التسجيل"
|
unregister: "إلغاء التسجيل"
|
||||||
passwordLessLogin: "لِج مِن دون كلمة سرية"
|
passwordLessLogin: "لِج مِن دون كلمة سرية"
|
||||||
resetPassword: "أعد تعيين كلمتك السرية"
|
resetPassword: "أعد تعيين كلمتك السرية"
|
||||||
newPasswordIs: "كلمتك السرية الجديدة هي {password}"
|
newPasswordIs: "كلمتك السرية الجديدة هي {password}"
|
||||||
|
reduceUiAnimation: "قلص تأثيرات الواجهة"
|
||||||
share: "شارِك"
|
share: "شارِك"
|
||||||
notFound: "غير موجود"
|
notFound: "غير موجود"
|
||||||
cacheClear: "مسح ذاكرة التخزين المؤقت"
|
cacheClear: "مسح ذاكرة التخزين المؤقت"
|
||||||
@ -316,8 +343,8 @@ invites: "دعوة"
|
|||||||
groupName: "اسم الفريق"
|
groupName: "اسم الفريق"
|
||||||
members: "الأعضاء"
|
members: "الأعضاء"
|
||||||
transfer: "نقل"
|
transfer: "نقل"
|
||||||
messagingWithUser: "الدردشة مع مستخدم آخر"
|
messagingWithUser: "تحدث مع مستخدم"
|
||||||
messagingWithGroup: "دردشة جماعية"
|
messagingWithGroup: "محادثة جماعية"
|
||||||
title: "العنوان"
|
title: "العنوان"
|
||||||
text: "النص"
|
text: "النص"
|
||||||
enable: "تشغيل"
|
enable: "تشغيل"
|
||||||
@ -362,28 +389,43 @@ total: "المجموع"
|
|||||||
weekOverWeekChanges: "أسبوعيا"
|
weekOverWeekChanges: "أسبوعيا"
|
||||||
dayOverDayChanges: "يوميا"
|
dayOverDayChanges: "يوميا"
|
||||||
appearance: "المظهر"
|
appearance: "المظهر"
|
||||||
|
clientSettings: "إعدادات العميل"
|
||||||
accountSettings: "إعدادات الحساب"
|
accountSettings: "إعدادات الحساب"
|
||||||
promotion: "ترقية"
|
promotion: "ترقية"
|
||||||
promote: "روِّج"
|
promote: "روِّج"
|
||||||
numberOfDays: "عدد الأيام"
|
numberOfDays: "عدد الأيام"
|
||||||
hideThisNote: "إخفاء هذه الملاحظة"
|
hideThisNote: "إخفاء هذه الملاحظة"
|
||||||
|
objectStorageBaseUrl: "الرابط الأساسي"
|
||||||
|
objectStoragePrefix: "البادئة"
|
||||||
|
objectStorageEndpoint: "نقطة النهاية"
|
||||||
|
objectStorageRegion: "المنطقة"
|
||||||
|
objectStorageUseSSL: "استخدم SSL"
|
||||||
|
objectStorageUseProxy: "اتصل عبر وكيل"
|
||||||
|
serverLogs: "سجلات الخادم"
|
||||||
deleteAll: "حذف الكل"
|
deleteAll: "حذف الكل"
|
||||||
|
showFixedPostForm: "أظهر نموذج الكتابة في أعلى الصفحة"
|
||||||
|
newNoteRecived: "هناك ملاحظات جديدة"
|
||||||
sounds: "الرنات"
|
sounds: "الرنات"
|
||||||
listen: "استمع"
|
listen: "استمع"
|
||||||
none: "لا شيء"
|
none: "لا شيء"
|
||||||
|
showInPage: "اعرض في الصفحة"
|
||||||
volume: "مستوى الصوت"
|
volume: "مستوى الصوت"
|
||||||
details: "التفاصيل"
|
details: "التفاصيل"
|
||||||
chooseEmoji: "اختر إيموجي"
|
chooseEmoji: "اختر إيموجي"
|
||||||
|
unableToProcess: "يتعذر إكمال العملية"
|
||||||
recentUsed: "المستخدمة مؤخرا"
|
recentUsed: "المستخدمة مؤخرا"
|
||||||
install: "التثبيت"
|
install: "التثبيت"
|
||||||
uninstall: "إلغاء التثبيت"
|
uninstall: "إلغاء التثبيت"
|
||||||
installedApps: "التطبيقات المُخوّلة"
|
installedApps: "التطبيقات المُخوّلة"
|
||||||
|
nothing: "لا يوجد شيء هنا"
|
||||||
lastUsedDate: "آخر استخدام"
|
lastUsedDate: "آخر استخدام"
|
||||||
state: "الحالة"
|
state: "الحالة"
|
||||||
sort: "ترتيب حسب"
|
sort: "ترتيب حسب"
|
||||||
output: "الخارجة"
|
output: "الخارجة"
|
||||||
updateRemoteUser: "تحديث المعلومات عن المستخدم البعيد"
|
updateRemoteUser: "تحديث المعلومات عن المستخدم البعيد"
|
||||||
deleteAllFiles: "حذف كافة الملفات"
|
deleteAllFiles: "حذف كافة الملفات"
|
||||||
|
deleteAllFilesConfirm: "أتريد حذف كل الملفات؟"
|
||||||
|
removeAllFollowing: "ألغ متابعة كل المتابِعين"
|
||||||
userSuspended: "تم تعليق هذا المستخدم."
|
userSuspended: "تم تعليق هذا المستخدم."
|
||||||
userSilenced: "تم إسكات هذا المستخدم."
|
userSilenced: "تم إسكات هذا المستخدم."
|
||||||
addItem: "إضافة عنصر"
|
addItem: "إضافة عنصر"
|
||||||
@ -419,7 +461,40 @@ makeActive: "تفعيل"
|
|||||||
display: "المظهر"
|
display: "المظهر"
|
||||||
copy: "نسخ"
|
copy: "نسخ"
|
||||||
metrics: "المقاييس"
|
metrics: "المقاييس"
|
||||||
|
fileIdOrUrl: "معرف الملف أو رابط"
|
||||||
|
chatOpenBehavior: "سلوك نفاذة المحادثة عند فتحها"
|
||||||
|
behavior: "السلوك"
|
||||||
|
sample: "مثال"
|
||||||
|
abuseReports: "البلاغات"
|
||||||
|
reportAbuse: "البلاغات"
|
||||||
|
reportAbuseOf: "أبلغ عن {name}"
|
||||||
|
fillAbuseReportDescription: "أكتب بالتفصيل سبب الإبلاغ، إذا كنت تبلغ عن ملاحظة أرفق رابط لها."
|
||||||
|
abuseReported: "أُرسل البلاغ، شكرًا لك"
|
||||||
|
send: "أرسل"
|
||||||
|
abuseMarkAsResolved: "علّم البلاغ كمحلول"
|
||||||
|
openInNewTab: "افتح في لسان جديد"
|
||||||
|
defaultNavigationBehaviour: "سلوك الملاحة الافتراضي"
|
||||||
|
waitingFor: "في انتظار {x}"
|
||||||
|
random: "عشوائي"
|
||||||
|
system: "النظام"
|
||||||
|
switchUi: "بدّل واجهة المستخدم"
|
||||||
|
createNew: "أنشِئ جديد"
|
||||||
|
optional: "اختياري"
|
||||||
public: "للعامة"
|
public: "للعامة"
|
||||||
|
i18nInfo: "يترجم متطوعون ميسكي إلى عدة لغات، يمكنك المساعدة عبر {link}"
|
||||||
|
manageAccessTokens: "إدارة رموز الوصول"
|
||||||
|
accountInfo: "معلومات الحساب"
|
||||||
|
notesCount: "عدد الملاحظات"
|
||||||
|
repliesCount: "عدد الردود المرسلة"
|
||||||
|
repliedCount: "عدد الردود المستلمة"
|
||||||
|
followingCount: "عدد الحسابات المتابَعة"
|
||||||
|
followersCount: "عدد المتابِعين"
|
||||||
|
sentReactionsCount: "عدد الانفعالات المرسلة"
|
||||||
|
receivedReactionsCount: "عدد الانفعالات المستلمة"
|
||||||
|
pollVotesCount: "عدد الاستطلاعات المرسلة"
|
||||||
|
pollVotedCount: "عدد الاستطلاعات المستلمة"
|
||||||
|
yes: "نعم"
|
||||||
|
no: "لا"
|
||||||
currentVersion: "الإصدار الحالي"
|
currentVersion: "الإصدار الحالي"
|
||||||
latestVersion: "آخر نسخة مستقرة"
|
latestVersion: "آخر نسخة مستقرة"
|
||||||
usageAmount: "الإستخدام"
|
usageAmount: "الإستخدام"
|
||||||
@ -433,6 +508,7 @@ gallery: "المعرض"
|
|||||||
expiration: "ينتهي استطلاع الرأي في"
|
expiration: "ينتهي استطلاع الرأي في"
|
||||||
middle: "متوسط"
|
middle: "متوسط"
|
||||||
global: "الشامل"
|
global: "الشامل"
|
||||||
|
sent: "أرسل"
|
||||||
_docs:
|
_docs:
|
||||||
admin: "إدارة "
|
admin: "إدارة "
|
||||||
_email:
|
_email:
|
||||||
@ -459,12 +535,12 @@ _theme:
|
|||||||
alpha: "الشفافية"
|
alpha: "الشفافية"
|
||||||
keys:
|
keys:
|
||||||
mention: "أشر الى"
|
mention: "أشر الى"
|
||||||
messageBg: "خلفية الدردشة"
|
messageBg: "خلفية المحادثة"
|
||||||
_sfx:
|
_sfx:
|
||||||
note: "الملاحظات"
|
note: "الملاحظات"
|
||||||
noteMy: "ملاحظتي"
|
noteMy: "ملاحظتي"
|
||||||
notification: "الإشعارات"
|
notification: "الإشعارات"
|
||||||
chat: "الدردشة"
|
chat: "المحادثة"
|
||||||
_ago:
|
_ago:
|
||||||
unknown: "مجهول"
|
unknown: "مجهول"
|
||||||
future: "المستقبَل"
|
future: "المستقبَل"
|
||||||
|
@ -797,6 +797,8 @@ unread: "Ungelesen"
|
|||||||
filter: "Filter"
|
filter: "Filter"
|
||||||
controllPanel: "Systemsteuerung"
|
controllPanel: "Systemsteuerung"
|
||||||
manageAccounts: "Benutzerkonten verwalten"
|
manageAccounts: "Benutzerkonten verwalten"
|
||||||
|
makeReactionsPublic: "Reaktionsverlauf veröffentlichen"
|
||||||
|
makeReactionsPublicDescription: "Jeder wird die Liste deiner gesendeten Reaktionen einsehen können."
|
||||||
_signup:
|
_signup:
|
||||||
almostThere: "Fast geschafft"
|
almostThere: "Fast geschafft"
|
||||||
emailAddressInfo: "Bitte gib deine Email-Adresse ein."
|
emailAddressInfo: "Bitte gib deine Email-Adresse ein."
|
||||||
|
@ -797,6 +797,8 @@ unread: "Unread"
|
|||||||
filter: "Filter"
|
filter: "Filter"
|
||||||
controllPanel: "Control Panel"
|
controllPanel: "Control Panel"
|
||||||
manageAccounts: "Manage Accounts"
|
manageAccounts: "Manage Accounts"
|
||||||
|
makeReactionsPublic: "Set reaction history to public"
|
||||||
|
makeReactionsPublicDescription: "This will make the list of all your past reactions publicly visible."
|
||||||
_signup:
|
_signup:
|
||||||
almostThere: "Almost there"
|
almostThere: "Almost there"
|
||||||
emailAddressInfo: "Please enter your email address."
|
emailAddressInfo: "Please enter your email address."
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
_lang_: "Esperanto"
|
_lang_: "Esperanto"
|
||||||
headlineMisskey: "Jen la reto konektata de notoj"
|
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. 🚀"
|
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"
|
search: "Serĉi"
|
||||||
notifications: "Sciigoj"
|
notifications: "Sciigoj"
|
||||||
username: "Uzantnomo"
|
username: "Uzantnomo"
|
||||||
@ -20,10 +20,10 @@ instance: "Nodo"
|
|||||||
settings: "Agordoj"
|
settings: "Agordoj"
|
||||||
basicSettings: "Ĝeneralaj agordoj"
|
basicSettings: "Ĝeneralaj agordoj"
|
||||||
otherSettings: "Aliaj agordoj"
|
otherSettings: "Aliaj agordoj"
|
||||||
openInWindow: "Malfermi en nova fenestro"
|
openInWindow: "Malfermi en fenestro"
|
||||||
profile: "Profilo"
|
profile: "Profilo"
|
||||||
timeline: "Templinio"
|
timeline: "Templinio"
|
||||||
noAccountDescription: "Ĉi tiu uzanto ne skribis vivpriskribon."
|
noAccountDescription: "Neniu priskribo"
|
||||||
login: "Ensaluti"
|
login: "Ensaluti"
|
||||||
loggingIn: "Ensalutado…"
|
loggingIn: "Ensalutado…"
|
||||||
logout: "Elsaluti"
|
logout: "Elsaluti"
|
||||||
@ -44,7 +44,7 @@ copyContent: "Kopii enhavon"
|
|||||||
copyLink: "Kopii ligilon"
|
copyLink: "Kopii ligilon"
|
||||||
delete: "Forviŝi"
|
delete: "Forviŝi"
|
||||||
deleteAndEdit: "Redakti foriginte"
|
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"
|
addToList: "Aldoni al listo"
|
||||||
sendMessage: "Sendi mesaĝon"
|
sendMessage: "Sendi mesaĝon"
|
||||||
copyUsername: "Kopii uzantnomon"
|
copyUsername: "Kopii uzantnomon"
|
||||||
@ -89,7 +89,7 @@ renote: "Plusendi la noton"
|
|||||||
unrenote: "Malfari plusendadon"
|
unrenote: "Malfari plusendadon"
|
||||||
renoted: "Sukcese plusendita"
|
renoted: "Sukcese plusendita"
|
||||||
cantRenote: "Oni ne povas plusendi la noton."
|
cantRenote: "Oni ne povas plusendi la noton."
|
||||||
cantReRenote: "Plusendado ne estas plusendebla."
|
cantReRenote: "Plusendo de noto ne estas plusendebla."
|
||||||
quote: "Citi"
|
quote: "Citi"
|
||||||
pinnedNote: "Alpinglita noto"
|
pinnedNote: "Alpinglita noto"
|
||||||
pinned: "Alpingli"
|
pinned: "Alpingli"
|
||||||
@ -220,7 +220,7 @@ remoteUserCaution: "Ĉi tiuj infomoj ne estas tute ekzaktaj pro transa uzanto."
|
|||||||
activity: "Aktiveco"
|
activity: "Aktiveco"
|
||||||
images: "Bildoj"
|
images: "Bildoj"
|
||||||
birthday: "Naskiĝdato"
|
birthday: "Naskiĝdato"
|
||||||
registeredDate: "Registriĝdato"
|
registeredDate: "Dato de registriĝo"
|
||||||
location: "Loko"
|
location: "Loko"
|
||||||
theme: "Koloraro"
|
theme: "Koloraro"
|
||||||
light: "Luma"
|
light: "Luma"
|
||||||
@ -262,7 +262,7 @@ thisYear: "Ĉi-jare"
|
|||||||
thisMonth: "Ĉi-monate"
|
thisMonth: "Ĉi-monate"
|
||||||
today: "Hodiaŭ"
|
today: "Hodiaŭ"
|
||||||
dayX: "{day}a"
|
dayX: "{day}a"
|
||||||
monthX: "{month}"
|
monthX: "La {month}a monato"
|
||||||
yearX: "La jaro {year}"
|
yearX: "La jaro {year}"
|
||||||
pages: "Paĝoj"
|
pages: "Paĝoj"
|
||||||
connectService: "Konekti"
|
connectService: "Konekti"
|
||||||
@ -317,6 +317,7 @@ nUsersMentioned: "{n} uzanto(j) menciis"
|
|||||||
securityKey: "Sekureca ŝlosilo"
|
securityKey: "Sekureca ŝlosilo"
|
||||||
securityKeyName: "Nomo de la ŝlosilo"
|
securityKeyName: "Nomo de la ŝlosilo"
|
||||||
lastUsed: "Plej malnove uzita"
|
lastUsed: "Plej malnove uzita"
|
||||||
|
unregister: "Malregistriĝi"
|
||||||
passwordLessLogin: "Ensaluti sen pasvorto"
|
passwordLessLogin: "Ensaluti sen pasvorto"
|
||||||
resetPassword: "Restarigi pasvorton"
|
resetPassword: "Restarigi pasvorton"
|
||||||
newPasswordIs: "La nova pasvorto estas {password}."
|
newPasswordIs: "La nova pasvorto estas {password}."
|
||||||
@ -516,7 +517,7 @@ clear: "Vakigi"
|
|||||||
goBack: "Reiri antaŭ"
|
goBack: "Reiri antaŭ"
|
||||||
addDescription: "Priskribi"
|
addDescription: "Priskribi"
|
||||||
info: "Informoj"
|
info: "Informoj"
|
||||||
userInfo: "La informoj de uzanto"
|
userInfo: "Informoj de uzanto"
|
||||||
unknown: "Nekonata"
|
unknown: "Nekonata"
|
||||||
online: "Surkonektita"
|
online: "Surkonektita"
|
||||||
offline: "Forkonektita"
|
offline: "Forkonektita"
|
||||||
@ -688,13 +689,13 @@ _antennaSources:
|
|||||||
all: "Ĉiuj notoj"
|
all: "Ĉiuj notoj"
|
||||||
homeTimeline: "Notoj far uzantoj kiujn vi sekvas"
|
homeTimeline: "Notoj far uzantoj kiujn vi sekvas"
|
||||||
_weekday:
|
_weekday:
|
||||||
sunday: "dimanĉo"
|
sunday: "Dimanĉo"
|
||||||
monday: "lundo"
|
monday: "Lundo"
|
||||||
tuesday: "mardo"
|
tuesday: "Mardo"
|
||||||
wednesday: "merkredo"
|
wednesday: "Merkredo"
|
||||||
thursday: "ĵaŭdo"
|
thursday: "Ĵaŭdo"
|
||||||
friday: "vendredo"
|
friday: "Vendredo"
|
||||||
saturday: "sabato"
|
saturday: "Sabato"
|
||||||
_widgets:
|
_widgets:
|
||||||
notifications: "Sciigoj"
|
notifications: "Sciigoj"
|
||||||
timeline: "Templinio"
|
timeline: "Templinio"
|
||||||
|
@ -365,7 +365,7 @@ withFiles: "Avec fichiers joints"
|
|||||||
silence: "Mettre en sourdine"
|
silence: "Mettre en sourdine"
|
||||||
silenceConfirm: "Êtes-vous sûr·e de vouloir mettre l’utilisateur·rice en sourdine ?"
|
silenceConfirm: "Êtes-vous sûr·e de vouloir mettre l’utilisateur·rice en sourdine ?"
|
||||||
unsilence: "Annuler la 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"
|
popularUsers: "Utilisateur·rice·s populaires"
|
||||||
recentlyUpdatedUsers: "Utilisateur·rice·s actif·ve·s récemment"
|
recentlyUpdatedUsers: "Utilisateur·rice·s actif·ve·s récemment"
|
||||||
recentlyRegisteredUsers: "Utilisateur·rice·s récemment inscrit·e·s"
|
recentlyRegisteredUsers: "Utilisateur·rice·s récemment inscrit·e·s"
|
||||||
@ -766,6 +766,7 @@ middle: "Moyen"
|
|||||||
low: "Basse"
|
low: "Basse"
|
||||||
emailNotConfiguredWarning: "Vous n'avez pas configuré d'adresse e-mail."
|
emailNotConfiguredWarning: "Vous n'avez pas configuré d'adresse e-mail."
|
||||||
ratio: "Ratio"
|
ratio: "Ratio"
|
||||||
|
previewNoteText: "Voir l'aperçu"
|
||||||
customCss: "CSS personnalisé"
|
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."
|
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"
|
global: "Global"
|
||||||
@ -789,6 +790,8 @@ pubSub: "Comptes Pub/Sub"
|
|||||||
lastCommunication: "Dernière communication"
|
lastCommunication: "Dernière communication"
|
||||||
resolved: "Résolu"
|
resolved: "Résolu"
|
||||||
unresolved: "En attente"
|
unresolved: "En attente"
|
||||||
|
itsOn: "Activé"
|
||||||
|
itsOff: "Désactivé"
|
||||||
emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte"
|
emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte"
|
||||||
unread: "Non lu"
|
unread: "Non lu"
|
||||||
filter: "Filtre"
|
filter: "Filtre"
|
||||||
|
@ -797,6 +797,8 @@ unread: "未読"
|
|||||||
filter: "フィルタ"
|
filter: "フィルタ"
|
||||||
controllPanel: "コントロールパネル"
|
controllPanel: "コントロールパネル"
|
||||||
manageAccounts: "アカウントを管理"
|
manageAccounts: "アカウントを管理"
|
||||||
|
makeReactionsPublic: "リアクション一覧を公開する"
|
||||||
|
makeReactionsPublicDescription: "あなたがしたリアクション一覧を誰でも見れるようにします。"
|
||||||
|
|
||||||
_signup:
|
_signup:
|
||||||
almostThere: "ほとんど完了です"
|
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",
|
"name": "misskey",
|
||||||
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
||||||
"version": "12.92.0",
|
"version": "12.93.0",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -60,6 +60,9 @@
|
|||||||
"@types/jsonld": "1.5.6",
|
"@types/jsonld": "1.5.6",
|
||||||
"@types/katex": "0.11.1",
|
"@types/katex": "0.11.1",
|
||||||
"@types/koa": "2.13.4",
|
"@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-bodyparser": "4.3.3",
|
||||||
"@types/koa-cors": "0.0.2",
|
"@types/koa-cors": "0.0.2",
|
||||||
"@types/koa-favicon": "2.0.21",
|
"@types/koa-favicon": "2.0.21",
|
||||||
@ -67,9 +70,6 @@
|
|||||||
"@types/koa-mount": "4.0.1",
|
"@types/koa-mount": "4.0.1",
|
||||||
"@types/koa-send": "4.1.3",
|
"@types/koa-send": "4.1.3",
|
||||||
"@types/koa-views": "7.0.0",
|
"@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/markdown-it": "12.2.3",
|
||||||
"@types/matter-js": "0.17.5",
|
"@types/matter-js": "0.17.5",
|
||||||
"@types/mocha": "8.2.3",
|
"@types/mocha": "8.2.3",
|
||||||
@ -119,7 +119,9 @@
|
|||||||
"cafy": "15.2.1",
|
"cafy": "15.2.1",
|
||||||
"cbor": "8.0.2",
|
"cbor": "8.0.2",
|
||||||
"chalk": "4.1.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",
|
"cli-highlight": "2.1.11",
|
||||||
"compare-versions": "3.6.0",
|
"compare-versions": "3.6.0",
|
||||||
"concurrently": "6.3.0",
|
"concurrently": "6.3.0",
|
||||||
@ -127,6 +129,7 @@
|
|||||||
"crc-32": "1.2.0",
|
"crc-32": "1.2.0",
|
||||||
"css-loader": "6.4.0",
|
"css-loader": "6.4.0",
|
||||||
"cssnano": "5.0.8",
|
"cssnano": "5.0.8",
|
||||||
|
"date-fns": "2.25.0",
|
||||||
"dateformat": "4.5.1",
|
"dateformat": "4.5.1",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"eslint": "8.0.1",
|
"eslint": "8.0.1",
|
||||||
@ -208,6 +211,7 @@
|
|||||||
"seedrandom": "3.0.5",
|
"seedrandom": "3.0.5",
|
||||||
"sharp": "0.29.1",
|
"sharp": "0.29.1",
|
||||||
"speakeasy": "2.0.0",
|
"speakeasy": "2.0.0",
|
||||||
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"style-loader": "3.3.0",
|
"style-loader": "3.3.0",
|
||||||
"summaly": "2.4.1",
|
"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">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
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';
|
import { debounce } from 'throttle-debounce';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
@ -22,7 +22,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
const label = this.$slots.desc();
|
|
||||||
let options = this.$slots.default();
|
let options = this.$slots.default();
|
||||||
|
|
||||||
// なぜかFragmentになることがあるため
|
// なぜかFragmentになることがあるため
|
||||||
@ -31,7 +30,6 @@ export default defineComponent({
|
|||||||
return h('div', {
|
return h('div', {
|
||||||
class: 'novjtcto'
|
class: 'novjtcto'
|
||||||
}, [
|
}, [
|
||||||
h('div', { class: 'label' }, label),
|
|
||||||
...options.map(option => h(MkRadio, {
|
...options.map(option => h(MkRadio, {
|
||||||
key: option.key,
|
key: option.key,
|
||||||
value: option.props.value,
|
value: option.props.value,
|
||||||
@ -45,16 +43,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.novjtcto {
|
.novjtcto {
|
||||||
> .label {
|
|
||||||
font-size: 0.85em;
|
|
||||||
padding: 0 0 8px 12px;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="vblkjoeq">
|
<div class="vblkjoeq">
|
||||||
<div class="label" @click="focus"><slot name="label"></slot></div>
|
<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>
|
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
|
||||||
<select ref="inputEl"
|
<select class="select" ref="inputEl"
|
||||||
v-model="v"
|
v-model="v"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:required="required"
|
:required="required"
|
||||||
@ -25,7 +25,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
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({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@ -81,6 +82,7 @@ export default defineComponent({
|
|||||||
const inputEl = ref(null);
|
const inputEl = ref(null);
|
||||||
const prefixEl = ref(null);
|
const prefixEl = ref(null);
|
||||||
const suffixEl = ref(null);
|
const suffixEl = ref(null);
|
||||||
|
const container = ref(null);
|
||||||
|
|
||||||
const focus = () => inputEl.value.focus();
|
const focus = () => inputEl.value.focus();
|
||||||
const onInput = (ev) => {
|
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 {
|
return {
|
||||||
v,
|
v,
|
||||||
focused,
|
focused,
|
||||||
@ -141,8 +184,10 @@ export default defineComponent({
|
|||||||
inputEl,
|
inputEl,
|
||||||
prefixEl,
|
prefixEl,
|
||||||
suffixEl,
|
suffixEl,
|
||||||
|
container,
|
||||||
focus,
|
focus,
|
||||||
onInput,
|
onInput,
|
||||||
|
onClick,
|
||||||
updated,
|
updated,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -174,8 +219,15 @@ export default defineComponent({
|
|||||||
> .input {
|
> .input {
|
||||||
$height: 42px;
|
$height: 42px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
> select {
|
&:hover {
|
||||||
|
> .select {
|
||||||
|
border-color: var(--inputBorderHover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .select {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
display: block;
|
display: block;
|
||||||
@ -195,10 +247,7 @@ export default defineComponent({
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.1s ease-out;
|
transition: border-color 0.1s ease-out;
|
||||||
|
pointer-events: none;
|
||||||
&:hover {
|
|
||||||
border-color: var(--inputBorderHover);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .prefix,
|
> .prefix,
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
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';
|
import { debounce } from 'throttle-debounce';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
@ -27,8 +27,7 @@ export default defineComponent({
|
|||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
customEmojis: {
|
customEmojis: {
|
||||||
required: false,
|
required: false
|
||||||
default: () => []
|
|
||||||
},
|
},
|
||||||
isReaction: {
|
isReaction: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -58,10 +57,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
ce() {
|
ce() {
|
||||||
let ce = [];
|
return this.customEmojis || this.$instance?.emojis || [];
|
||||||
if (this.customEmojis) ce = ce.concat(this.customEmojis);
|
|
||||||
if (this.$instance && this.$instance.emojis) ce = ce.concat(this.$instance.emojis);
|
|
||||||
return ce;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -203,6 +203,12 @@ export default defineComponent({
|
|||||||
|
|
||||||
&.thin {
|
&.thin {
|
||||||
--height: 50px;
|
--height: 50px;
|
||||||
|
|
||||||
|
> .buttons {
|
||||||
|
> .button {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.slim {
|
&.slim {
|
||||||
|
@ -24,35 +24,26 @@
|
|||||||
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
|
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</MkSelect>
|
</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="hour">{{ $ts.perHour }}</option>
|
||||||
<option value="day">{{ $ts.perDay }}</option>
|
<option value="day">{{ $ts.perDay }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
<canvas ref="chart"></canvas>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, markRaw } from 'vue';
|
import { defineComponent, onMounted, ref, watch } from 'vue';
|
||||||
import Chart from 'chart.js';
|
import MkSelect from '@client/components/form/select.vue';
|
||||||
import MkSelect from './form/select.vue';
|
import MkChart from '@client/components/chart.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 * as os from '@client/os';
|
import * as os from '@client/os';
|
||||||
|
import { defaultStore } from '@client/store';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
MkSelect
|
MkSelect,
|
||||||
|
MkChart,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -68,463 +59,15 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
setup() {
|
||||||
|
const chartSpan = ref<'hour' | 'day'>('hour');
|
||||||
|
const chartSrc = ref('notes');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notesLocalWoW: 0,
|
chartSrc,
|
||||||
notesLocalDoD: 0,
|
chartSpan,
|
||||||
notesRemoteWoW: 0,
|
};
|
||||||
notesRemoteDoD: 0,
|
|
||||||
usersLocalWoW: 0,
|
|
||||||
usersLocalDoD: 0,
|
|
||||||
usersRemoteWoW: 0,
|
|
||||||
usersRemoteDoD: 0,
|
|
||||||
now: null,
|
|
||||||
chart: null,
|
|
||||||
chartInstance: null,
|
|
||||||
chartSrc: 'notes',
|
|
||||||
chartSpan: 'hour',
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
|
@ -185,7 +185,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (style == null) {
|
if (style == null) {
|
||||||
return h('span', {}, ['[', token.props.name, ' ', ...genEl(token.children), ']']);
|
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']);
|
||||||
} else {
|
} else {
|
||||||
return h('span', {
|
return h('span', {
|
||||||
style: 'display: inline-block;' + style,
|
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,
|
type: String,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
initialVisibility: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
initialFiles: {
|
||||||
|
type: Array,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
initialLocalOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
visibleUsers: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
initialNote: {
|
initialNote: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
instant: {
|
share: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
@ -150,8 +167,7 @@ export default defineComponent({
|
|||||||
showPreview: false,
|
showPreview: false,
|
||||||
cw: null,
|
cw: null,
|
||||||
localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
|
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,
|
visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number],
|
||||||
visibleUsers: [],
|
|
||||||
autocomplete: null,
|
autocomplete: null,
|
||||||
draghover: false,
|
draghover: false,
|
||||||
quoteId: null,
|
quoteId: null,
|
||||||
@ -246,6 +262,18 @@ export default defineComponent({
|
|||||||
this.text = this.initialText;
|
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) {
|
if (this.mention) {
|
||||||
this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
|
this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
|
||||||
this.text += ' ';
|
this.text += ' ';
|
||||||
@ -321,7 +349,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
this.$nextTick(() => {
|
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];
|
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
|
||||||
if (draft) {
|
if (draft) {
|
||||||
this.text = draft.data.text;
|
this.text = draft.data.text;
|
||||||
@ -582,8 +610,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
saveDraft() {
|
saveDraft() {
|
||||||
if (this.instant) return;
|
|
||||||
|
|
||||||
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
|
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
|
||||||
|
|
||||||
data[this.draftKey] = {
|
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 {
|
> button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 10px 8px;
|
padding: 10px 8px;
|
||||||
border-radius: 6px;
|
border-radius: var(--radius);
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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">
|
<header v-if="showHeader" ref="header">
|
||||||
<div class="title"><slot name="header"></slot></div>
|
<div class="title"><slot name="header"></slot></div>
|
||||||
<div class="sub">
|
<div class="sub">
|
||||||
@ -36,6 +36,11 @@ export default defineComponent({
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
thin: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
naked: {
|
naked: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@ -226,7 +231,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.max-width_380px {
|
&.max-width_380px, &.thin {
|
||||||
> header {
|
> header {
|
||||||
> .title {
|
> .title {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="rrevdjwt" :class="{ center: align === 'center' }"
|
<div class="rrevdjwt" :class="{ center: align === 'center' }"
|
||||||
|
:style="{ width: width ? width + 'px' : null }"
|
||||||
ref="items"
|
ref="items"
|
||||||
@contextmenu.self="e => e.preventDefault()"
|
@contextmenu.self="e => e.preventDefault()"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
@ -59,6 +60,10 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
requried: false
|
requried: false
|
||||||
},
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
data() {
|
data() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<MkPopup ref="popup" :src="src" @closed="$emit('closed')">
|
<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>
|
</MkPopup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -24,6 +24,10 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
viaKeyboard: {
|
viaKeyboard: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false
|
required: false
|
||||||
|
@ -120,7 +120,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
> .items {
|
> .items {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
grid-gap: 8px;
|
grid-gap: 8px;
|
||||||
padding: 0 16px;
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
let dispose;
|
let dispose;
|
||||||
popup(import('@client/components/ui/popup-menu.vue'), {
|
popup(import('@client/components/ui/popup-menu.vue'), {
|
||||||
items,
|
items,
|
||||||
src,
|
src,
|
||||||
|
width: options?.width,
|
||||||
align: options?.align,
|
align: options?.align,
|
||||||
viaKeyboard: options?.viaKeyboard
|
viaKeyboard: options?.viaKeyboard
|
||||||
}, {
|
}, {
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
|
<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/instance/settings" 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="/instance/bot-protection" 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>
|
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
|
||||||
</div>
|
</div>
|
||||||
@ -93,47 +93,47 @@ export default defineComponent({
|
|||||||
items: [{
|
items: [{
|
||||||
icon: 'fas fa-tachometer-alt',
|
icon: 'fas fa-tachometer-alt',
|
||||||
text: i18n.locale.dashboard,
|
text: i18n.locale.dashboard,
|
||||||
to: '/instance/overview',
|
to: '/admin/overview',
|
||||||
active: page.value === 'overview',
|
active: page.value === 'overview',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-users',
|
icon: 'fas fa-users',
|
||||||
text: i18n.locale.users,
|
text: i18n.locale.users,
|
||||||
to: '/instance/users',
|
to: '/admin/users',
|
||||||
active: page.value === 'users',
|
active: page.value === 'users',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-laugh',
|
icon: 'fas fa-laugh',
|
||||||
text: i18n.locale.customEmojis,
|
text: i18n.locale.customEmojis,
|
||||||
to: '/instance/emojis',
|
to: '/admin/emojis',
|
||||||
active: page.value === 'emojis',
|
active: page.value === 'emojis',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-globe',
|
icon: 'fas fa-globe',
|
||||||
text: i18n.locale.federation,
|
text: i18n.locale.federation,
|
||||||
to: '/instance/federation',
|
to: '/admin/federation',
|
||||||
active: page.value === 'federation',
|
active: page.value === 'federation',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-clipboard-list',
|
icon: 'fas fa-clipboard-list',
|
||||||
text: i18n.locale.jobQueue,
|
text: i18n.locale.jobQueue,
|
||||||
to: '/instance/queue',
|
to: '/admin/queue',
|
||||||
active: page.value === 'queue',
|
active: page.value === 'queue',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-cloud',
|
icon: 'fas fa-cloud',
|
||||||
text: i18n.locale.files,
|
text: i18n.locale.files,
|
||||||
to: '/instance/files',
|
to: '/admin/files',
|
||||||
active: page.value === 'files',
|
active: page.value === 'files',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-broadcast-tower',
|
icon: 'fas fa-broadcast-tower',
|
||||||
text: i18n.locale.announcements,
|
text: i18n.locale.announcements,
|
||||||
to: '/instance/announcements',
|
to: '/admin/announcements',
|
||||||
active: page.value === 'announcements',
|
active: page.value === 'announcements',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-audio-description',
|
icon: 'fas fa-audio-description',
|
||||||
text: i18n.locale.ads,
|
text: i18n.locale.ads,
|
||||||
to: '/instance/ads',
|
to: '/admin/ads',
|
||||||
active: page.value === 'ads',
|
active: page.value === 'ads',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-exclamation-circle',
|
icon: 'fas fa-exclamation-circle',
|
||||||
text: i18n.locale.abuseReports,
|
text: i18n.locale.abuseReports,
|
||||||
to: '/instance/abuses',
|
to: '/admin/abuses',
|
||||||
active: page.value === 'abuses',
|
active: page.value === 'abuses',
|
||||||
}],
|
}],
|
||||||
}, {
|
}, {
|
||||||
@ -141,57 +141,57 @@ export default defineComponent({
|
|||||||
items: [{
|
items: [{
|
||||||
icon: 'fas fa-cog',
|
icon: 'fas fa-cog',
|
||||||
text: i18n.locale.general,
|
text: i18n.locale.general,
|
||||||
to: '/instance/settings',
|
to: '/admin/settings',
|
||||||
active: page.value === 'settings',
|
active: page.value === 'settings',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-cloud',
|
icon: 'fas fa-cloud',
|
||||||
text: i18n.locale.files,
|
text: i18n.locale.files,
|
||||||
to: '/instance/files-settings',
|
to: '/admin/files-settings',
|
||||||
active: page.value === 'files-settings',
|
active: page.value === 'files-settings',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-envelope',
|
icon: 'fas fa-envelope',
|
||||||
text: i18n.locale.emailServer,
|
text: i18n.locale.emailServer,
|
||||||
to: '/instance/email-settings',
|
to: '/admin/email-settings',
|
||||||
active: page.value === 'email-settings',
|
active: page.value === 'email-settings',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-cloud',
|
icon: 'fas fa-cloud',
|
||||||
text: i18n.locale.objectStorage,
|
text: i18n.locale.objectStorage,
|
||||||
to: '/instance/object-storage',
|
to: '/admin/object-storage',
|
||||||
active: page.value === 'object-storage',
|
active: page.value === 'object-storage',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-lock',
|
icon: 'fas fa-lock',
|
||||||
text: i18n.locale.security,
|
text: i18n.locale.security,
|
||||||
to: '/instance/security',
|
to: '/admin/security',
|
||||||
active: page.value === 'security',
|
active: page.value === 'security',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-bolt',
|
icon: 'fas fa-bolt',
|
||||||
text: 'ServiceWorker',
|
text: 'ServiceWorker',
|
||||||
to: '/instance/service-worker',
|
to: '/admin/service-worker',
|
||||||
active: page.value === 'service-worker',
|
active: page.value === 'service-worker',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-globe',
|
icon: 'fas fa-globe',
|
||||||
text: i18n.locale.relays,
|
text: i18n.locale.relays,
|
||||||
to: '/instance/relays',
|
to: '/admin/relays',
|
||||||
active: page.value === 'relays',
|
active: page.value === 'relays',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-share-alt',
|
icon: 'fas fa-share-alt',
|
||||||
text: i18n.locale.integration,
|
text: i18n.locale.integration,
|
||||||
to: '/instance/integrations',
|
to: '/admin/integrations',
|
||||||
active: page.value === 'integrations',
|
active: page.value === 'integrations',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-ban',
|
icon: 'fas fa-ban',
|
||||||
text: i18n.locale.instanceBlocking,
|
text: i18n.locale.instanceBlocking,
|
||||||
to: '/instance/instance-block',
|
to: '/admin/instance-block',
|
||||||
active: page.value === 'instance-block',
|
active: page.value === 'instance-block',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-ghost',
|
icon: 'fas fa-ghost',
|
||||||
text: i18n.locale.proxyAccount,
|
text: i18n.locale.proxyAccount,
|
||||||
to: '/instance/proxy-account',
|
to: '/admin/proxy-account',
|
||||||
active: page.value === 'proxy-account',
|
active: page.value === 'proxy-account',
|
||||||
}, {
|
}, {
|
||||||
icon: 'fas fa-cogs',
|
icon: 'fas fa-cogs',
|
||||||
text: i18n.locale.other,
|
text: i18n.locale.other,
|
||||||
to: '/instance/other-settings',
|
to: '/admin/other-settings',
|
||||||
active: page.value === 'other-settings',
|
active: page.value === 'other-settings',
|
||||||
}],
|
}],
|
||||||
}, {
|
}, {
|
||||||
@ -199,13 +199,8 @@ export default defineComponent({
|
|||||||
items: [{
|
items: [{
|
||||||
icon: 'fas fa-database',
|
icon: 'fas fa-database',
|
||||||
text: i18n.locale.database,
|
text: i18n.locale.database,
|
||||||
to: '/instance/database',
|
to: '/admin/database',
|
||||||
active: page.value === 'database',
|
active: page.value === 'database',
|
||||||
}, {
|
|
||||||
icon: 'fas fa-stream',
|
|
||||||
text: i18n.locale.logs,
|
|
||||||
to: '/instance/logs',
|
|
||||||
active: page.value === 'logs',
|
|
||||||
}],
|
}],
|
||||||
}]);
|
}]);
|
||||||
const component = computed(() => {
|
const component = computed(() => {
|
||||||
@ -220,7 +215,6 @@ export default defineComponent({
|
|||||||
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
|
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
|
||||||
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
|
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
|
||||||
case 'database': return defineAsyncComponent(() => import('./database.vue'));
|
case 'database': return defineAsyncComponent(() => import('./database.vue'));
|
||||||
case 'logs': return defineAsyncComponent(() => import('./logs.vue'));
|
|
||||||
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
|
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
|
||||||
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
|
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
|
||||||
case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
|
case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
|
@ -78,17 +78,17 @@
|
|||||||
<span class="label">{{ $ts.charts }}</span>
|
<span class="label">{{ $ts.charts }}</span>
|
||||||
<div class="selects">
|
<div class="selects">
|
||||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||||
<option value="requests">{{ $ts._instanceCharts.requests }}</option>
|
<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
|
||||||
<option value="users">{{ $ts._instanceCharts.users }}</option>
|
<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
|
||||||
<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
||||||
<option value="notes">{{ $ts._instanceCharts.notes }}</option>
|
<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
|
||||||
<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
||||||
<option value="ff">{{ $ts._instanceCharts.ff }}</option>
|
<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
|
||||||
<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
||||||
<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
||||||
<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
||||||
<option value="drive-files">{{ $ts._instanceCharts.files }}</option>
|
<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
|
||||||
<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="chartSpan" style="margin: 0;">
|
<MkSelect v-model="chartSpan" style="margin: 0;">
|
||||||
<option value="hour">{{ $ts.perHour }}</option>
|
<option value="hour">{{ $ts.perHour }}</option>
|
||||||
@ -97,7 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<canvas :ref="setChart"></canvas>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="operations section">
|
<div class="operations section">
|
||||||
@ -124,28 +124,17 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, markRaw } from 'vue';
|
import { defineComponent, markRaw } from 'vue';
|
||||||
import Chart from 'chart.js';
|
|
||||||
import XModalWindow from '@client/components/ui/modal-window.vue';
|
import XModalWindow from '@client/components/ui/modal-window.vue';
|
||||||
import MkUsersDialog from '@client/components/users-dialog.vue';
|
import MkUsersDialog from '@client/components/users-dialog.vue';
|
||||||
import MkSelect from '@client/components/form/select.vue';
|
import MkSelect from '@client/components/form/select.vue';
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
import MkSwitch from '@client/components/form/switch.vue';
|
import MkSwitch from '@client/components/form/switch.vue';
|
||||||
import MkInfo from '@client/components/ui/info.vue';
|
import MkInfo from '@client/components/ui/info.vue';
|
||||||
|
import MkChart from '@client/components/chart.vue';
|
||||||
import bytes from '@client/filters/bytes';
|
import bytes from '@client/filters/bytes';
|
||||||
import number from '@client/filters/number';
|
import number from '@client/filters/number';
|
||||||
import * as os from '@client/os';
|
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({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
XModalWindow,
|
XModalWindow,
|
||||||
@ -153,6 +142,7 @@ export default defineComponent({
|
|||||||
MkButton,
|
MkButton,
|
||||||
MkSwitch,
|
MkSwitch,
|
||||||
MkInfo,
|
MkInfo,
|
||||||
|
MkChart,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -167,42 +157,12 @@ export default defineComponent({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isSuspended: this.instance.isSuspended,
|
isSuspended: this.instance.isSuspended,
|
||||||
now: null,
|
|
||||||
canvas: null,
|
|
||||||
chart: null,
|
|
||||||
chartInstance: null,
|
|
||||||
chartSrc: 'requests',
|
chartSrc: 'requests',
|
||||||
chartSpan: 'hour',
|
chartSpan: 'hour',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
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() {
|
meta() {
|
||||||
return this.$instance;
|
return this.$instance;
|
||||||
},
|
},
|
||||||
@ -219,49 +179,15 @@ export default defineComponent({
|
|||||||
isSuspended: this.isSuspended
|
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: {
|
methods: {
|
||||||
setChart(el) {
|
|
||||||
this.canvas = el;
|
|
||||||
},
|
|
||||||
|
|
||||||
changeBlock(e) {
|
changeBlock(e) {
|
||||||
os.api('admin/update-meta', {
|
os.api('admin/update-meta', {
|
||||||
blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
|
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() {
|
removeAllFollowing() {
|
||||||
os.apiWithDialog('admin/federation/remove-all-following', {
|
os.apiWithDialog('admin/federation/remove-all-following', {
|
||||||
host: this.instance.host
|
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() {
|
showFollowing() {
|
||||||
os.modal(MkUsersDialog, {
|
os.modal(MkUsersDialog, {
|
||||||
title: this.$ts.instanceFollowing,
|
title: this.$ts.instanceFollowing,
|
@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<FormBase>
|
<FormBase>
|
||||||
<FormSuspense :p="init">
|
<FormSuspense :p="init">
|
||||||
<FormLink to="/instance/integrations/twitter">
|
<FormLink to="/admin/integrations/twitter">
|
||||||
<i class="fab fa-twitter"></i> Twitter
|
<i class="fab fa-twitter"></i> Twitter
|
||||||
<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
|
<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
|
||||||
</FormLink>
|
</FormLink>
|
||||||
<FormLink to="/instance/integrations/github">
|
<FormLink to="/admin/integrations/github">
|
||||||
<i class="fab fa-github"></i> GitHub
|
<i class="fab fa-github"></i> GitHub
|
||||||
<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
|
<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
|
||||||
</FormLink>
|
</FormLink>
|
||||||
<FormLink to="/instance/integrations/discord">
|
<FormLink to="/admin/integrations/discord">
|
||||||
<i class="fab fa-discord"></i> Discord
|
<i class="fab fa-discord"></i> Discord
|
||||||
<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
|
<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
|
||||||
</FormLink>
|
</FormLink>
|
@ -52,7 +52,21 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, markRaw } from 'vue';
|
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 MkButton from '@client/components/ui/button.vue';
|
||||||
import MkSelect from '@client/components/form/select.vue';
|
import MkSelect from '@client/components/form/select.vue';
|
||||||
import MkInput from '@client/components/form/input.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 number from '@client/filters/number';
|
||||||
import MkInstanceInfo from './instance.vue';
|
import MkInstanceInfo from './instance.vue';
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle
|
||||||
|
);
|
||||||
|
|
||||||
const alpha = (hex, a) => {
|
const alpha = (hex, a) => {
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(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 r = parseInt(result[1], 16);
|
||||||
@ -116,7 +145,7 @@ export default defineComponent({
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.fetchJobs();
|
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 => {
|
os.api('admin/server-info', {}).then(res => {
|
||||||
this.serverInfo = res;
|
this.serverInfo = res;
|
||||||
@ -157,7 +186,7 @@ export default defineComponent({
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'CPU',
|
label: 'CPU',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#86b300',
|
borderColor: '#86b300',
|
||||||
backgroundColor: alpha('#86b300', 0.1),
|
backgroundColor: alpha('#86b300', 0.1),
|
||||||
@ -165,7 +194,7 @@ export default defineComponent({
|
|||||||
}, {
|
}, {
|
||||||
label: 'MEM (active)',
|
label: 'MEM (active)',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#935dbf',
|
borderColor: '#935dbf',
|
||||||
backgroundColor: alpha('#935dbf', 0.02),
|
backgroundColor: alpha('#935dbf', 0.02),
|
||||||
@ -173,7 +202,7 @@ export default defineComponent({
|
|||||||
}, {
|
}, {
|
||||||
label: 'MEM (used)',
|
label: 'MEM (used)',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#935dbf',
|
borderColor: '#935dbf',
|
||||||
borderDash: [5, 5],
|
borderDash: [5, 5],
|
||||||
@ -198,7 +227,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
xAxes: [{
|
x: {
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: false,
|
display: false,
|
||||||
color: this.gridColor,
|
color: this.gridColor,
|
||||||
@ -207,8 +236,8 @@ export default defineComponent({
|
|||||||
ticks: {
|
ticks: {
|
||||||
display: false,
|
display: false,
|
||||||
}
|
}
|
||||||
}],
|
},
|
||||||
yAxes: [{
|
y: {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: true,
|
display: true,
|
||||||
@ -219,7 +248,7 @@ export default defineComponent({
|
|||||||
display: false,
|
display: false,
|
||||||
max: 100
|
max: 100
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
intersect: false,
|
intersect: false,
|
||||||
@ -238,7 +267,7 @@ export default defineComponent({
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'In',
|
label: 'In',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#94a029',
|
borderColor: '#94a029',
|
||||||
backgroundColor: alpha('#94a029', 0.1),
|
backgroundColor: alpha('#94a029', 0.1),
|
||||||
@ -246,7 +275,7 @@ export default defineComponent({
|
|||||||
}, {
|
}, {
|
||||||
label: 'Out',
|
label: 'Out',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#ff9156',
|
borderColor: '#ff9156',
|
||||||
backgroundColor: alpha('#ff9156', 0.1),
|
backgroundColor: alpha('#ff9156', 0.1),
|
||||||
@ -270,7 +299,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
xAxes: [{
|
x: {
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: false,
|
display: false,
|
||||||
color: this.gridColor,
|
color: this.gridColor,
|
||||||
@ -279,8 +308,8 @@ export default defineComponent({
|
|||||||
ticks: {
|
ticks: {
|
||||||
display: false
|
display: false
|
||||||
}
|
}
|
||||||
}],
|
},
|
||||||
yAxes: [{
|
y: {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: true,
|
display: true,
|
||||||
@ -290,7 +319,7 @@ export default defineComponent({
|
|||||||
ticks: {
|
ticks: {
|
||||||
display: false,
|
display: false,
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
intersect: false,
|
intersect: false,
|
||||||
@ -309,7 +338,7 @@ export default defineComponent({
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Read',
|
label: 'Read',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#94a029',
|
borderColor: '#94a029',
|
||||||
backgroundColor: alpha('#94a029', 0.1),
|
backgroundColor: alpha('#94a029', 0.1),
|
||||||
@ -317,7 +346,7 @@ export default defineComponent({
|
|||||||
}, {
|
}, {
|
||||||
label: 'Write',
|
label: 'Write',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#ff9156',
|
borderColor: '#ff9156',
|
||||||
backgroundColor: alpha('#ff9156', 0.1),
|
backgroundColor: alpha('#ff9156', 0.1),
|
||||||
@ -341,7 +370,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
xAxes: [{
|
x: {
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: false,
|
display: false,
|
||||||
color: this.gridColor,
|
color: this.gridColor,
|
||||||
@ -350,8 +379,8 @@ export default defineComponent({
|
|||||||
ticks: {
|
ticks: {
|
||||||
display: false
|
display: false
|
||||||
}
|
}
|
||||||
}],
|
},
|
||||||
yAxes: [{
|
y: {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: true,
|
display: true,
|
||||||
@ -361,7 +390,7 @@ export default defineComponent({
|
|||||||
ticks: {
|
ticks: {
|
||||||
display: false,
|
display: false,
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
intersect: false,
|
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() {
|
fetchJobs() {
|
||||||
os.api('admin/queue/deliver-delayed', {}).then(jobs => {
|
os.api('admin/queue/deliver-delayed', {}).then(jobs => {
|
||||||
this.jobs = 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>
|
<template>
|
||||||
<FormBase>
|
<FormBase>
|
||||||
<FormSuspense :p="init">
|
<FormSuspense :p="init">
|
||||||
<FormLink to="/instance/bot-protection">
|
<FormLink to="/admin/bot-protection">
|
||||||
<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
|
<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
|
||||||
<template #suffix v-if="enableHcaptcha">hCaptcha</template>
|
<template #suffix v-if="enableHcaptcha">hCaptcha</template>
|
||||||
<template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template>
|
<template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template>
|
@ -65,13 +65,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'search'">
|
<div v-else-if="tab === 'search'">
|
||||||
<div class="_isolated">
|
<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 #prefix><i class="fas fa-search"></i></template>
|
||||||
<template #label>{{ $ts.searchUser }}</template>
|
<template #label>{{ $ts.searchUser }}</template>
|
||||||
</MkInput>
|
</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>
|
</div>
|
||||||
|
|
||||||
<XUserList v-if="query" class="_gap" :pagination="searchPagination" ref="search"/>
|
<XUserList v-if="searchQuery" class="_gap" :pagination="searchPagination" ref="search"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
@ -83,6 +88,7 @@ import { computed, defineComponent } from 'vue';
|
|||||||
import XUserList from '@client/components/user-list.vue';
|
import XUserList from '@client/components/user-list.vue';
|
||||||
import MkFolder from '@client/components/ui/folder.vue';
|
import MkFolder from '@client/components/ui/folder.vue';
|
||||||
import MkInput from '@client/components/form/input.vue';
|
import MkInput from '@client/components/form/input.vue';
|
||||||
|
import MkRadios from '@client/components/form/radios.vue';
|
||||||
import number from '@client/filters/number';
|
import number from '@client/filters/number';
|
||||||
import * as os from '@client/os';
|
import * as os from '@client/os';
|
||||||
import * as symbols from '@client/symbols';
|
import * as symbols from '@client/symbols';
|
||||||
@ -92,6 +98,7 @@ export default defineComponent({
|
|||||||
XUserList,
|
XUserList,
|
||||||
MkFolder,
|
MkFolder,
|
||||||
MkInput,
|
MkInput,
|
||||||
|
MkRadios,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -158,14 +165,16 @@ export default defineComponent({
|
|||||||
searchPagination: {
|
searchPagination: {
|
||||||
endpoint: 'users/search',
|
endpoint: 'users/search',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: computed(() => (this.query && this.query !== '') ? {
|
params: computed(() => (this.searchQuery && this.searchQuery !== '') ? {
|
||||||
query: this.query
|
query: this.searchQuery,
|
||||||
|
origin: this.searchOrigin,
|
||||||
} : null)
|
} : null)
|
||||||
},
|
},
|
||||||
tagsLocal: [],
|
tagsLocal: [],
|
||||||
tagsRemote: [],
|
tagsRemote: [],
|
||||||
stats: null,
|
stats: null,
|
||||||
query: null,
|
searchQuery: null,
|
||||||
|
searchOrigin: 'combined',
|
||||||
num: number,
|
num: number,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -65,17 +65,17 @@
|
|||||||
<div class="_debobigegoPanel cmhjzshl">
|
<div class="_debobigegoPanel cmhjzshl">
|
||||||
<div class="selects">
|
<div class="selects">
|
||||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||||
<option value="requests">{{ $ts._instanceCharts.requests }}</option>
|
<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
|
||||||
<option value="users">{{ $ts._instanceCharts.users }}</option>
|
<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
|
||||||
<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
||||||
<option value="notes">{{ $ts._instanceCharts.notes }}</option>
|
<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
|
||||||
<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
||||||
<option value="ff">{{ $ts._instanceCharts.ff }}</option>
|
<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
|
||||||
<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
||||||
<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
||||||
<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
||||||
<option value="drive-files">{{ $ts._instanceCharts.files }}</option>
|
<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
|
||||||
<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="chartSpan" style="margin: 0;">
|
<MkSelect v-model="chartSpan" style="margin: 0;">
|
||||||
<option value="hour">{{ $ts.perHour }}</option>
|
<option value="hour">{{ $ts.perHour }}</option>
|
||||||
@ -83,7 +83,7 @@
|
|||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<canvas :ref="setChart"></canvas>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -135,7 +135,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
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 FormObjectView from '@client/components/debobigego/object-view.vue';
|
||||||
import FormTextarea from '@client/components/debobigego/textarea.vue';
|
import FormTextarea from '@client/components/debobigego/textarea.vue';
|
||||||
import FormLink from '@client/components/debobigego/link.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 number from '@client/filters/number';
|
||||||
import bytes from '@client/filters/bytes';
|
import bytes from '@client/filters/bytes';
|
||||||
import * as symbols from '@client/symbols';
|
import * as symbols from '@client/symbols';
|
||||||
import MkInstanceInfo from '@client/pages/instance/instance.vue';
|
import MkInstanceInfo from '@client/pages/admin/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)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@ -173,6 +162,7 @@ export default defineComponent({
|
|||||||
FormKeyValueView,
|
FormKeyValueView,
|
||||||
FormSuspense,
|
FormSuspense,
|
||||||
MkSelect,
|
MkSelect,
|
||||||
|
MkChart,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@ -199,53 +189,11 @@ export default defineComponent({
|
|||||||
dnsPromiseFactory: () => os.api('federation/dns', {
|
dnsPromiseFactory: () => os.api('federation/dns', {
|
||||||
host: this.host
|
host: this.host
|
||||||
}),
|
}),
|
||||||
now: null,
|
chartSrc: 'instance-requests',
|
||||||
canvas: null,
|
|
||||||
chart: null,
|
|
||||||
chartInstance: null,
|
|
||||||
chartSrc: 'requests',
|
|
||||||
chartSpan: 'hour',
|
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() {
|
mounted() {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
},
|
},
|
||||||
@ -258,190 +206,6 @@ export default defineComponent({
|
|||||||
this.instance = await os.api('federation/show-instance', {
|
this.instance = await os.api('federation/show-instance', {
|
||||||
host: this.host
|
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() {
|
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>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ $ts._exportOrImport.muteList }}</template>
|
<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>
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ $ts._exportOrImport.blockingList }}</template>
|
<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="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>
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -58,11 +60,11 @@ export default defineComponent({
|
|||||||
methods: {
|
methods: {
|
||||||
doExport(target) {
|
doExport(target) {
|
||||||
os.api(
|
os.api(
|
||||||
target == 'notes' ? 'i/export-notes' :
|
target === 'notes' ? 'i/export-notes' :
|
||||||
target == 'following' ? 'i/export-following' :
|
target === 'following' ? 'i/export-following' :
|
||||||
target == 'blocking' ? 'i/export-blocking' :
|
target === 'blocking' ? 'i/export-blocking' :
|
||||||
target == 'user-lists' ? 'i/export-user-lists' :
|
target === 'user-lists' ? 'i/export-user-lists' :
|
||||||
target == 'mute' ? 'i/export-mute' :
|
target === 'muting' ? 'i/export-mute' :
|
||||||
null, {})
|
null, {})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
os.dialog({
|
os.dialog({
|
||||||
@ -81,8 +83,10 @@ export default defineComponent({
|
|||||||
const file = await selectFile(e.currentTarget || e.target);
|
const file = await selectFile(e.currentTarget || e.target);
|
||||||
|
|
||||||
os.api(
|
os.api(
|
||||||
target == 'following' ? 'i/import-following' :
|
target === 'following' ? 'i/import-following' :
|
||||||
target == 'user-lists' ? 'i/import-user-lists' :
|
target === 'user-lists' ? 'i/import-user-lists' :
|
||||||
|
target === 'muting' ? 'i/import-muting' :
|
||||||
|
target === 'blocking' ? 'i/import-blocking' :
|
||||||
null, {
|
null, {
|
||||||
fileId: file.id
|
fileId: file.id
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
<FormSwitch v-model="autoAcceptFollowed" :disabled="!isLocked" @update:modelValue="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch>
|
<FormSwitch v-model="autoAcceptFollowed" :disabled="!isLocked" @update:modelValue="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch>
|
||||||
<template #caption>{{ $ts.lockedAccountInfo }}</template>
|
<template #caption>{{ $ts.lockedAccountInfo }}</template>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<FormSwitch v-model="publicReactions" @update:modelValue="save()">
|
||||||
|
{{ $ts.makeReactionsPublic }}
|
||||||
|
<template #desc>{{ $ts.makeReactionsPublicDescription }}</template>
|
||||||
|
</FormSwitch>
|
||||||
<FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
|
<FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
|
||||||
{{ $ts.hideOnlineStatus }}
|
{{ $ts.hideOnlineStatus }}
|
||||||
<template #desc>{{ $ts.hideOnlineStatusDescription }}</template>
|
<template #desc>{{ $ts.hideOnlineStatusDescription }}</template>
|
||||||
@ -64,6 +68,7 @@ export default defineComponent({
|
|||||||
noCrawle: false,
|
noCrawle: false,
|
||||||
isExplorable: false,
|
isExplorable: false,
|
||||||
hideOnlineStatus: false,
|
hideOnlineStatus: false,
|
||||||
|
publicReactions: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -80,6 +85,7 @@ export default defineComponent({
|
|||||||
this.noCrawle = this.$i.noCrawle;
|
this.noCrawle = this.$i.noCrawle;
|
||||||
this.isExplorable = this.$i.isExplorable;
|
this.isExplorable = this.$i.isExplorable;
|
||||||
this.hideOnlineStatus = this.$i.hideOnlineStatus;
|
this.hideOnlineStatus = this.$i.hideOnlineStatus;
|
||||||
|
this.publicReactions = this.$i.publicReactions;
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -94,6 +100,7 @@ export default defineComponent({
|
|||||||
noCrawle: !!this.noCrawle,
|
noCrawle: !!this.noCrawle,
|
||||||
isExplorable: !!this.isExplorable,
|
isExplorable: !!this.isExplorable,
|
||||||
hideOnlineStatus: !!this.hideOnlineStatus,
|
hideOnlineStatus: !!this.hideOnlineStatus,
|
||||||
|
publicReactions: !!this.publicReactions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,13 @@
|
|||||||
</optgroup>
|
</optgroup>
|
||||||
</FormSelect>
|
</FormSelect>
|
||||||
<template v-if="selectedTheme">
|
<template v-if="selectedTheme">
|
||||||
<FormInput readonly :value="selectedTheme.author">
|
<FormInput readonly :modelValue="selectedTheme.author">
|
||||||
<span>{{ $ts.author }}</span>
|
<span>{{ $ts.author }}</span>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
<FormTextarea readonly :value="selectedTheme.desc" v-if="selectedTheme.desc">
|
<FormTextarea readonly :modelValue="selectedTheme.desc" v-if="selectedTheme.desc">
|
||||||
<span>{{ $ts._theme.description }}</span>
|
<span>{{ $ts._theme.description }}</span>
|
||||||
</FormTextarea>
|
</FormTextarea>
|
||||||
<FormTextarea readonly tall :value="selectedThemeCode">
|
<FormTextarea readonly tall :modelValue="selectedThemeCode">
|
||||||
<span>{{ $ts._theme.code }}</span>
|
<span>{{ $ts._theme.code }}</span>
|
||||||
<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template>
|
<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template>
|
||||||
</FormTextarea>
|
</FormTextarea>
|
||||||
@ -28,12 +28,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import * as JSON5 from 'json5';
|
import * as JSON5 from 'json5';
|
||||||
import FormTextarea from '@client/components/form/textarea.vue';
|
import FormTextarea from '@client/components/debobigego/textarea.vue';
|
||||||
import FormSelect from '@client/components/form/select.vue';
|
import FormSelect from '@client/components/debobigego/select.vue';
|
||||||
import FormRadios from '@client/components/form/radios.vue';
|
import FormRadios from '@client/components/debobigego/radios.vue';
|
||||||
import FormBase from '@client/components/debobigego/base.vue';
|
import FormBase from '@client/components/debobigego/base.vue';
|
||||||
import FormGroup from '@client/components/debobigego/group.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 FormButton from '@client/components/debobigego/button.vue';
|
||||||
import { Theme, builtinThemes } from '@client/scripts/theme';
|
import { Theme, builtinThemes } from '@client/scripts/theme';
|
||||||
import copyToClipboard from '@client/scripts/copy-to-clipboard';
|
import copyToClipboard from '@client/scripts/copy-to-clipboard';
|
||||||
|
@ -1,22 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="">
|
||||||
<section class="_section">
|
<section class="_section">
|
||||||
<div class="_title" v-if="title">{{ title }}</div>
|
|
||||||
<div class="_content">
|
<div class="_content">
|
||||||
<XPostForm v-if="!posted" fixed :instant="true" :initial-text="initialText" @posted="posted = true" class="_panel"/>
|
<XPostForm
|
||||||
<MkButton v-else primary @click="close()">{{ $ts.close }}</MkButton>
|
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>
|
||||||
<div class="_footer" v-if="url">{{ url }}</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
// SPECIFICATION: /src/docs/ja-JP/advanced/share-page.md
|
||||||
|
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
import XPostForm from '@client/components/post-form.vue';
|
import XPostForm from '@client/components/post-form.vue';
|
||||||
import * as os from '@client/os';
|
import * as os from '@client/os';
|
||||||
|
import { noteVisibilities } from '@/types';
|
||||||
|
import { parseAcct } from '@/misc/acct';
|
||||||
import * as symbols from '@client/symbols';
|
import * as symbols from '@client/symbols';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@ -30,35 +46,139 @@ export default defineComponent({
|
|||||||
title: this.$ts.share,
|
title: this.$ts.share,
|
||||||
icon: 'fas fa-share-alt'
|
icon: 'fas fa-share-alt'
|
||||||
},
|
},
|
||||||
title: null,
|
state: 'fetching' as 'fetching' | 'writing' | 'posted',
|
||||||
text: null,
|
|
||||||
url: null,
|
|
||||||
initialText: null,
|
|
||||||
posted: false,
|
|
||||||
|
|
||||||
|
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);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
this.title = urlParams.get('title');
|
this.title = urlParams.get('title');
|
||||||
this.text = urlParams.get('text');
|
const text = urlParams.get('text');
|
||||||
this.url = urlParams.get('url');
|
const url = urlParams.get('url');
|
||||||
|
|
||||||
let text = '';
|
let noteText = '';
|
||||||
if (this.title) text += `【${this.title}】\n`;
|
if (this.title) noteText += `[ ${this.title} ]\n`;
|
||||||
if (this.text) text += `${this.text}\n`;
|
// Googleニュース対策
|
||||||
if (this.url) text += `${this.url}`;
|
if (text?.startsWith(`${this.title}.\n`)) noteText += text.replace(`${this.title}.\n`, '');
|
||||||
this.initialText = text.trim();
|
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: {
|
methods: {
|
||||||
close() {
|
close() {
|
||||||
window.close()
|
window.close();
|
||||||
|
|
||||||
|
// 閉じなければ100ms後タイムラインに
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$router.push('/');
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.close {
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -65,4 +65,11 @@ export default defineComponent({
|
|||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
._fitSide_ .yrzkoczt {
|
||||||
|
> .tab {
|
||||||
|
padding-left: var(--margin);
|
||||||
|
padding-right: var(--margin);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -181,6 +181,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
|
<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"/>
|
<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"/>
|
<XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
|
||||||
<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
|
<XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
|
||||||
<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
|
<XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
|
||||||
@ -223,6 +224,7 @@ export default defineComponent({
|
|||||||
MkTab,
|
MkTab,
|
||||||
MkInfo,
|
MkInfo,
|
||||||
XFollowList: defineAsyncComponent(() => import('./follow-list.vue')),
|
XFollowList: defineAsyncComponent(() => import('./follow-list.vue')),
|
||||||
|
XReactions: defineAsyncComponent(() => import('./reactions.vue')),
|
||||||
XClips: defineAsyncComponent(() => import('./clips.vue')),
|
XClips: defineAsyncComponent(() => import('./clips.vue')),
|
||||||
XPages: defineAsyncComponent(() => import('./pages.vue')),
|
XPages: defineAsyncComponent(() => import('./pages.vue')),
|
||||||
XGallery: defineAsyncComponent(() => import('./gallery.vue')),
|
XGallery: defineAsyncComponent(() => import('./gallery.vue')),
|
||||||
@ -268,7 +270,12 @@ export default defineComponent({
|
|||||||
title: this.$ts.overview,
|
title: this.$ts.overview,
|
||||||
icon: 'fas fa-home',
|
icon: 'fas fa-home',
|
||||||
onClick: () => { this.$router.push('/@' + getAcct(this.user)); },
|
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',
|
active: this.page === 'clips',
|
||||||
title: this.$ts.clips,
|
title: this.$ts.clips,
|
||||||
icon: 'fas fa-paperclip',
|
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/antennas/:antennaId', component: page('my-antennas/edit'), props: true },
|
||||||
{ path: '/my/clips', component: page('my-clips/index') },
|
{ path: '/my/clips', component: page('my-clips/index') },
|
||||||
{ path: '/scratchpad', component: page('scratchpad') },
|
{ path: '/scratchpad', component: page('scratchpad') },
|
||||||
{ path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) },
|
{ path: '/admin/:page(.*)?', component: page('admin/index'), props: route => ({ initialPage: route.params.page || null }) },
|
||||||
{ path: '/instance', component: page('instance/index') },
|
{ path: '/admin', component: page('admin/index') },
|
||||||
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
|
{ 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: '/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 }) },
|
{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import * as tinycolor from 'tinycolor2';
|
import * as tinycolor from 'tinycolor2';
|
||||||
import Chart from 'chart.js';
|
|
||||||
import { Hpml } from './evaluator';
|
import { Hpml } from './evaluator';
|
||||||
import { values, utils } from '@syuilo/aiscript';
|
import { values, utils } from '@syuilo/aiscript';
|
||||||
import { Fn, HpmlScope } from '.';
|
import { Fn, HpmlScope } from '.';
|
||||||
import { Expr } from './expr';
|
import { Expr } from './expr';
|
||||||
import * as seedrandom from 'seedrandom';
|
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
|
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
|
||||||
Chart.pluginService.register({
|
Chart.pluginService.register({
|
||||||
beforeDraw: (chart, easing) => {
|
beforeDraw: (chart, easing) => {
|
||||||
@ -18,6 +18,7 @@ Chart.pluginService.register({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
export function initAiLib(hpml: Hpml) {
|
export function initAiLib(hpml: Hpml) {
|
||||||
return {
|
return {
|
||||||
@ -49,11 +50,12 @@ export function initAiLib(hpml: Hpml) {
|
|||||||
]));
|
]));
|
||||||
}),
|
}),
|
||||||
'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
|
'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
|
||||||
|
/* TODO
|
||||||
utils.assertString(id);
|
utils.assertString(id);
|
||||||
utils.assertObject(opts);
|
utils.assertObject(opts);
|
||||||
const canvas = hpml.canvases[id.value];
|
const canvas = hpml.canvases[id.value];
|
||||||
const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
|
const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
|
||||||
Chart.defaults.global.defaultFontColor = '#555';
|
Chart.defaults.color = '#555';
|
||||||
const chart = new Chart(canvas, {
|
const chart = new Chart(canvas, {
|
||||||
type: opts.value.get('type').value,
|
type: opts.value.get('type').value,
|
||||||
data: {
|
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-apricot.json5'),
|
||||||
require('@client/themes/l-rainy.json5'),
|
require('@client/themes/l-rainy.json5'),
|
||||||
require('@client/themes/l-vivid.json5'),
|
require('@client/themes/l-vivid.json5'),
|
||||||
|
require('@client/themes/l-sushi.json5'),
|
||||||
|
|
||||||
require('@client/themes/d-dark.json5'),
|
require('@client/themes/d-dark.json5'),
|
||||||
require('@client/themes/d-persimmon.json5'),
|
require('@client/themes/d-persimmon.json5'),
|
||||||
require('@client/themes/d-astro.json5'),
|
require('@client/themes/d-astro.json5'),
|
||||||
require('@client/themes/d-future.json5'),
|
require('@client/themes/d-future.json5'),
|
||||||
require('@client/themes/d-botanical.json5'),
|
require('@client/themes/d-botanical.json5'),
|
||||||
|
require('@client/themes/d-pumpkin.json5'),
|
||||||
require('@client/themes/d-black.json5'),
|
require('@client/themes/d-black.json5'),
|
||||||
] as Theme[];
|
] as Theme[];
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
|
id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
|
||||||
base: 'dark',
|
base: 'dark',
|
||||||
name: 'Mi Astro',
|
name: 'Mi Astro Dark',
|
||||||
author: 'syuilo',
|
author: 'syuilo',
|
||||||
props: {
|
props: {
|
||||||
bg: '#232125',
|
bg: '#232125',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
|
id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
|
||||||
|
|
||||||
name: 'Mi Future',
|
name: 'Mi Future Dark',
|
||||||
author: 'syuilo',
|
author: 'syuilo',
|
||||||
|
|
||||||
base: 'dark',
|
base: 'dark',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
|
id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
|
||||||
|
|
||||||
name: 'Mi Persimmon',
|
name: 'Mi Persimmon Dark',
|
||||||
author: 'syuilo',
|
author: 'syuilo',
|
||||||
|
|
||||||
base: 'dark',
|
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',
|
id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
|
||||||
|
|
||||||
name: 'Mi Apricot',
|
name: 'Mi Apricot Light',
|
||||||
author: 'syuilo',
|
author: 'syuilo',
|
||||||
|
|
||||||
base: 'light',
|
base: 'light',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96',
|
id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96',
|
||||||
|
|
||||||
name: 'Mi Rainy',
|
name: 'Mi Rainy Light',
|
||||||
author: 'syuilo',
|
author: 'syuilo',
|
||||||
|
|
||||||
base: 'light',
|
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',
|
id: '6128c2a9-5c54-43fe-a47d-17942356470b',
|
||||||
|
|
||||||
name: 'Mi Vivid',
|
name: 'Mi Vivid Light',
|
||||||
author: 'syuilo',
|
author: 'syuilo',
|
||||||
|
|
||||||
base: 'light',
|
base: 'light',
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
<div class="divider"></div>
|
<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>
|
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
|
||||||
</MkA>
|
</MkA>
|
||||||
<button class="item _button" @click="more" v-click-anime>
|
<button class="item _button" @click="more" v-click-anime>
|
||||||
|
@ -100,7 +100,7 @@ export default defineComponent({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
instant: {
|
share: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
@ -277,7 +277,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
this.$nextTick(() => {
|
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];
|
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
|
||||||
if (draft) {
|
if (draft) {
|
||||||
this.text = draft.data.text;
|
this.text = draft.data.text;
|
||||||
@ -507,8 +507,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
saveDraft() {
|
saveDraft() {
|
||||||
if (this.instant) return;
|
|
||||||
|
|
||||||
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
|
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
|
||||||
|
|
||||||
data[this.draftKey] = {
|
data[this.draftKey] = {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user