Compare commits
159 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d0d5068f72 | ||
![]() |
d70b8275b4 | ||
![]() |
7253f83ca3 | ||
![]() |
6171f27f8d | ||
![]() |
96b5684fa0 | ||
![]() |
4df409f77b | ||
![]() |
0f122884cc | ||
![]() |
f29c9fe22c | ||
![]() |
93230815f5 | ||
![]() |
632af91878 | ||
![]() |
81a0ee4b2d | ||
![]() |
967c655de0 | ||
![]() |
4bb19b9596 | ||
![]() |
d43b17519e | ||
![]() |
76a881df39 | ||
![]() |
f84ad3e1ea | ||
![]() |
9d11c29c3b | ||
![]() |
90b62a3e95 | ||
![]() |
4e4c559db6 | ||
![]() |
e7660bc8db | ||
![]() |
cfcc626de2 | ||
![]() |
b7a5602003 | ||
![]() |
b3a4d1795b | ||
![]() |
23de94785d | ||
![]() |
69b56f6658 | ||
![]() |
5ca6e6b5df | ||
![]() |
98c26dfff8 | ||
![]() |
a4e3136611 | ||
![]() |
ea8e6d88ab | ||
![]() |
202cb38c40 | ||
![]() |
d3c7ddd2f4 | ||
![]() |
bc19cd77ad | ||
![]() |
0005de6a98 | ||
![]() |
386d3cd997 | ||
![]() |
2d196b6779 | ||
![]() |
0cc055de3a | ||
![]() |
a96e5277db | ||
![]() |
7413634734 | ||
![]() |
1bfb176667 | ||
![]() |
4a90c7aef8 | ||
![]() |
b0992de59d | ||
![]() |
37e666817f | ||
![]() |
a607149b19 | ||
![]() |
3a11dba24f | ||
![]() |
c76a384b41 | ||
![]() |
dec69cc67b | ||
![]() |
0c21ae226b | ||
![]() |
835aad44bb | ||
![]() |
8a1f3a4c0b | ||
![]() |
8b646822fc | ||
![]() |
6d3e2b9386 | ||
![]() |
73cdf5ca81 | ||
![]() |
133936652d | ||
![]() |
66470b4937 | ||
![]() |
3f688a728b | ||
![]() |
0d306e9d41 | ||
![]() |
1e8132e610 | ||
![]() |
d672fccef4 | ||
![]() |
c194eddb1b | ||
![]() |
4ba4062519 | ||
![]() |
23753ec75a | ||
![]() |
d184f73160 | ||
![]() |
482081c41b | ||
![]() |
03b04acb16 | ||
![]() |
345a9d3525 | ||
![]() |
aee816ced9 | ||
![]() |
1e28081aa3 | ||
![]() |
ff0521e3aa | ||
![]() |
8cabc5953e | ||
![]() |
4a766a19cf | ||
![]() |
bd8052fedb | ||
![]() |
5fb4538315 | ||
![]() |
e78f16bcc4 | ||
![]() |
fe62f3508b | ||
![]() |
c11c22fc73 | ||
![]() |
056ab675cf | ||
![]() |
f00d543447 | ||
![]() |
72b616a990 | ||
![]() |
8ee4b180f9 | ||
![]() |
46e4b07a87 | ||
![]() |
3ec6101b16 | ||
![]() |
955b3e313b | ||
![]() |
3dc70f9878 | ||
![]() |
8e2be5e9a7 | ||
![]() |
46f3736f44 | ||
![]() |
dc12b189de | ||
![]() |
3f95bd53cd | ||
![]() |
23de45cea5 | ||
![]() |
ba6959b8c1 | ||
![]() |
91c9a6390c | ||
![]() |
a99478e2ea | ||
![]() |
12635da473 | ||
![]() |
36170c816a | ||
![]() |
50bad84747 | ||
![]() |
abd3efa318 | ||
![]() |
816493e01f | ||
![]() |
a0c9fd75d7 | ||
![]() |
94bf7101f8 | ||
![]() |
46424f63f2 | ||
![]() |
c33e93c662 | ||
![]() |
49b43eb3c8 | ||
![]() |
6b22b7a31f | ||
![]() |
b77167a4a1 | ||
![]() |
79a591d72d | ||
![]() |
8006e7a34d | ||
![]() |
abc45ded9b | ||
![]() |
ec05c07321 | ||
![]() |
27c056cbbf | ||
![]() |
b3779875d0 | ||
![]() |
748a451e23 | ||
![]() |
8b1999dc5b | ||
![]() |
a38e4b0b14 | ||
![]() |
129f652dc2 | ||
![]() |
5bf69476f6 | ||
![]() |
597c9761cb | ||
![]() |
b875cc9949 | ||
![]() |
e568c3888f | ||
![]() |
67875e2afa | ||
![]() |
dd17065129 | ||
![]() |
8d05ef3058 | ||
![]() |
66369b4b1d | ||
![]() |
9a33495694 | ||
![]() |
f0b2eaf70d | ||
![]() |
ef67f3eee6 | ||
![]() |
9cab659392 | ||
![]() |
dcd216daff | ||
![]() |
8f673d80d4 | ||
![]() |
3e81ebf8e9 | ||
![]() |
19d531922d | ||
![]() |
9109ae02a7 | ||
![]() |
8d3fe0c5c2 | ||
![]() |
f9185f201a | ||
![]() |
027380c013 | ||
![]() |
a73a787753 | ||
![]() |
bd9df789d1 | ||
![]() |
4fd4132f5e | ||
![]() |
68aa1312f5 | ||
![]() |
7974dbf477 | ||
![]() |
834fb3bebd | ||
![]() |
18fa317ee7 | ||
![]() |
414f1d1158 | ||
![]() |
c5e5a9b8ef | ||
![]() |
1ac1a968b9 | ||
![]() |
0d3a36e519 | ||
![]() |
ebce02c253 | ||
![]() |
78b400e8b0 | ||
![]() |
8d93f148be | ||
![]() |
4b8a2d2a6b | ||
![]() |
5fd549656b | ||
![]() |
a70dbb7e74 | ||
![]() |
a75f3fb87c | ||
![]() |
67e2768c3e | ||
![]() |
ece3ac967d | ||
![]() |
da71d8f4af | ||
![]() |
ac93af8eb5 | ||
![]() |
76cdbe74ba | ||
![]() |
ce4ea5071f | ||
![]() |
8a558eed36 | ||
![]() |
ce32cd576b |
33
.github/workflows/docker-develop.yml
vendored
Normal file
33
.github/workflows/docker-develop.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Publish Docker image (develop)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: misskey/misskey
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: misskey/misskey:develop
|
||||
labels: develop
|
10
.github/workflows/nodejs.yml
vendored
10
.github/workflows/nodejs.yml
vendored
@@ -16,16 +16,16 @@ jobs:
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:10-alpine
|
||||
image: postgres:12.2-alpine
|
||||
ports:
|
||||
- 5432:5432
|
||||
- 54312:5432
|
||||
env:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:alpine
|
||||
image: redis:4.0-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
- 56312:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Check yarn.lock
|
||||
run: git diff --exit-code yarn.lock
|
||||
- name: Copy Configure
|
||||
run: cp .circleci/misskey/*.yml .config
|
||||
run: cp test/test.yml .config
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Test
|
||||
|
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -1,12 +1,10 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ducksoupdev.vue2",
|
||||
"editorconfig.editorconfig",
|
||||
"eg2.vscode-npm-script",
|
||||
"hollowtree.vue-snippets",
|
||||
"ms-vscode.typescript-javascript-grammar",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"octref.vetur",
|
||||
"johnsoncodehk.volar",
|
||||
"sysoev.language-stylus"
|
||||
]
|
||||
}
|
||||
|
63
CHANGELOG.md
63
CHANGELOG.md
@@ -7,6 +7,69 @@
|
||||
|
||||
-->
|
||||
|
||||
## 12.93.0 (2021/10/23)
|
||||
|
||||
### Improvements
|
||||
- クライアント: コントロールパネルのパフォーマンスを改善
|
||||
- クライアント: 自分のリアクション一覧を見れるように
|
||||
- 設定により、リアクション一覧を全員に公開することも可能
|
||||
- クライアント: ユーザー検索の精度を強化
|
||||
- クライアント: 新しいライトテーマを追加
|
||||
- クライアント: 新しいダークテーマを追加
|
||||
- API: ユーザーのリアクション一覧を取得する users/reactions を追加
|
||||
- API: users/search および users/search-by-username-and-host を強化
|
||||
- ミュート及びブロックのインポートを行えるように
|
||||
- クライアント: /share のクエリでリプライやファイル等の情報を渡せるように
|
||||
- チャートのsyncを毎日0時に自動で行うように
|
||||
|
||||
### Bugfixes
|
||||
- クライアント: テーマの管理が行えない問題を修正
|
||||
- API: アプリケーション通知が取得できない問題を修正
|
||||
- クライアント: リモートノートで意図せずローカルカスタム絵文字が使われてしまうことがあるのを修正
|
||||
- ActivityPub: not reacted な Undo.Like がinboxに滞留するのを修正
|
||||
|
||||
### Changes
|
||||
- 連合の考慮に問題があることなどが分かったため、モデレーターをブロックできない仕様を廃止しました
|
||||
- データベースにログを保存しないようになりました
|
||||
- ログを永続化したい場合はsyslogを利用してください
|
||||
|
||||
## 12.92.0 (2021/10/16)
|
||||
|
||||
### Improvements
|
||||
- アカウント登録にメールアドレスの設定を必須にするオプション
|
||||
- クライアント: 全体的なUIのブラッシュアップ
|
||||
- クライアント: MFM関数構文のサジェストを実装
|
||||
- クライアント: ノート本文を投稿フォーム内でプレビューできるように
|
||||
- クライアント: 未読の通知のみ表示する機能
|
||||
- クライアント: 通知ページで通知の種類によるフィルタ
|
||||
- クライアント: アニメーションを減らす設定の適用範囲を拡充
|
||||
- クライアント: 新しいダークテーマを追加
|
||||
- クライアント: テーマコンパイラに hue と saturate 関数を追加
|
||||
- ActivityPub: HTML -> MFMの変換を強化
|
||||
- API: グループから抜ける users/groups/leave エンドポイントを実装
|
||||
- API: i/notifications に unreadOnly オプションを追加
|
||||
- API: ap系のエンドポイントをログイン必須化+レートリミット追加
|
||||
- MFM: Add tag syntaxes of bold <b></b> and strikethrough <s></s>
|
||||
|
||||
### Bugfixes
|
||||
- Fix createDeleteAccountJob
|
||||
- admin inbox queue does not show individual jobs
|
||||
- クライアント: ヘッダーのタブが折り返される問題を修正
|
||||
- クライアント: ヘッダーにタブが表示されている状態でタイトルをクリックしたときにタブ選択が表示されるのを修正
|
||||
- クライアント: ユーザーページのタブが機能していない問題を修正
|
||||
- クライアント: ピン留めユーザーの設定項目がない問題を修正
|
||||
- クライアント: Deck UIにおいて、重ねたカラムの片方を畳んだ状態で右に出すと表示が壊れる問題を修正
|
||||
- API: 管理者およびモデレーターをブロックできてしまう問題を修正
|
||||
- MFM: Mentions in the link label are parsed as text
|
||||
- MFM: Add a property to the URL node indicating whether it was enclosed in <>
|
||||
- MFM: Disallows < and > in hashtags
|
||||
|
||||
### Changes
|
||||
- 保守性やユーザビリティの観点から、Misskeyのコマンドラインオプションが削除されました。
|
||||
- 必要であれば、代わりに環境変数で設定することができます
|
||||
- MFM: パフォーマンス、保守性、構文誤認識抑制の観点から、旧関数構文のサポートが削除されました。
|
||||
- 旧構文(`[foo bar]`)を使用せず、現行の構文(`$[foo bar]`)を使用してください。
|
||||
|
||||
## 12.91.0 (2021/09/22)
|
||||
|
||||
### Improvements
|
||||
|
@@ -9,9 +9,9 @@ It will also allow the reader to use the translation tool of their preference if
|
||||
## Issues
|
||||
Before creating an issue, please check the following:
|
||||
- To avoid duplication, please search for similar issues before creating a new issue.
|
||||
- Do not use Issues as a question.
|
||||
- Issues should only be used to feature requests, suggestions, and report problems.
|
||||
- Please ask questions in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3).
|
||||
- Do not use Issues to ask questions or troubleshooting.
|
||||
- Issues should only be used to feature requests, suggestions, and bug tracking.
|
||||
- Please ask questions or troubleshooting in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3).
|
||||
|
||||
## Before implementation
|
||||
When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented.
|
||||
@@ -57,6 +57,17 @@ If your language is not listed in Crowdin, please open an issue.
|
||||
- Test codes are located in [`/test`](/test).
|
||||
|
||||
### Run test
|
||||
Create a config file.
|
||||
```
|
||||
cp test/test.yml .config/
|
||||
```
|
||||
Prepare DB/Redis for testing.
|
||||
```
|
||||
docker-compose -f test/docker-compose.yml up
|
||||
```
|
||||
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
||||
|
||||
Run all test.
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
@@ -160,6 +171,9 @@ const users = userIds.length > 0 ? await Users.find({
|
||||
SQLでは配列のインデックスは**1始まり**。
|
||||
`[a, b, c]`の `a`にアクセスしたいなら`[0]`ではなく`[1]`と書く
|
||||
|
||||
### null IN
|
||||
nullが含まれる可能性のあるカラムにINするときは、そのままだとおかしくなるのでORなどでnullのハンドリングをしよう。
|
||||
|
||||
### `undefined`にご用心
|
||||
MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。
|
||||
MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください
|
||||
@@ -177,6 +191,10 @@ npx ts-node ./node_modules/typeorm/cli.js migration:generate -n 変更の名前
|
||||
### JSONのimportに気を付けよう
|
||||
TypeScriptでjsonをimportすると、tscでコンパイルするときにそのjsonファイルも一緒にdistディレクトリに吐き出されてしまう。この挙動により、意図せずファイルの書き換えが発生することがあるので、jsonをimportするときは書き換えられても良いものかどうか確認すること。書き換えされて欲しくない場合は、importで読み込むのではなく、`fs.readFileSync`などの関数を使って読み込むようにすればよい。
|
||||
|
||||
### コンポーネントのスタイル定義でmarginを持たせない
|
||||
コンポーネント自身がmarginを設定するのは問題の元となることはよく知られている
|
||||
marginはそのコンポーネントを使う側が設定する
|
||||
|
||||
## その他
|
||||
### HTMLのクラス名で follow という単語は使わない
|
||||
広告ブロッカーで誤ってブロックされる
|
||||
|
@@ -65,7 +65,7 @@ Organize and store your files! Want to post a picture you have already uploaded?
|
||||
|
||||
:package: Create your own instance
|
||||
----------------------------------------------------------------
|
||||
Please see the [Setup and Installation Guide](./docs/setup.en.md).
|
||||
Please see the [Setup and Installation Guide](https://misskey-hub.net/docs/install/install.html).
|
||||
|
||||
:wrench: Contribution
|
||||
----------------------------------------------------------------
|
||||
|
@@ -1,33 +0,0 @@
|
||||
# Docs
|
||||
These docs are for contributors of Misskey or admins of instance of Misskey.
|
||||
Docs for users are located in `src/docs`.
|
||||
|
||||
これらのドキュメントはMisskeyの開発者またはMisskeyインスタンス運営者向けです。
|
||||
利用者向けのドキュメントは`src/docs`にあります。
|
||||
|
||||
这些文档是为 Misskey 的贡献者,或是 Misskey 实例的管理者准备的。
|
||||
为用户准备的文档放置在 `src/docs` 文件夹中。
|
||||
|
||||
## 日本語版
|
||||
|
||||
- [Misskey構築の手引き](./setup.ja.md)
|
||||
- [運営ガイド](./manage.ja.md)
|
||||
- [Dockerを使ったMisskey構築方法](./docker.ja.md)
|
||||
|
||||
## English Version
|
||||
|
||||
- [Misskey Setup and Installation Guide](./setup.en.md)
|
||||
- [Management guide](./manage.en.md)
|
||||
- [Docker Guide](./docker.en.md)
|
||||
|
||||
## Française Version
|
||||
|
||||
- [Guide d'installation et de configuration de Misskey](./setup.fr.md)
|
||||
- [Guide d'administration](./manage.fr.md)
|
||||
- [Guide Docker](./docker.fr.md)
|
||||
|
||||
## 简体中文版
|
||||
|
||||
- [Misskey 设置和安装指南](./setup.zh.md)
|
||||
- [运营指南](./manage.zh.md)
|
||||
- [Docker 部署指南](./docker.zh.md)
|
@@ -1,97 +0,0 @@
|
||||
Docker Guide
|
||||
================================================================
|
||||
|
||||
This guide describes how to install and setup Misskey with Docker.
|
||||
|
||||
- [Japanese version also available - 日本語版もあります](./docker.ja.md)
|
||||
- [Simplified Chinese version also available - 简体中文版同样可用](./docker.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Download Misskey
|
||||
----------------------------------------------------------------
|
||||
1. Clone Misskey repository's master branch.
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
2. Move to misskey directory.
|
||||
|
||||
`cd misskey`
|
||||
|
||||
3. Checkout to the [latest release](https://github.com/misskey-dev/misskey/releases/latest) tag.
|
||||
|
||||
`git checkout master`
|
||||
|
||||
*2.* Configure Misskey
|
||||
----------------------------------------------------------------
|
||||
|
||||
Create configuration files with following:
|
||||
|
||||
```bash
|
||||
cd .config
|
||||
cp example.yml default.yml
|
||||
cp docker_example.env docker.env
|
||||
```
|
||||
|
||||
### `default.yml`
|
||||
|
||||
Edit this file the same as non-Docker environment.
|
||||
However hostname of Postgresql, Redis and Elasticsearch are not `localhost`, they are set in `docker-compose.yml`.
|
||||
The following is default hostname:
|
||||
|
||||
| Service | Hostname |
|
||||
|---------------|----------|
|
||||
| Postgresql | `db` |
|
||||
| Redis | `redis` |
|
||||
| Elasticsearch | `es` |
|
||||
|
||||
### `docker.env`
|
||||
|
||||
Configure Postgresql in this file.
|
||||
The minimum required settings are:
|
||||
|
||||
| name | Description |
|
||||
|---------------------|---------------|
|
||||
| `POSTGRES_PASSWORD` | Password |
|
||||
| `POSTGRES_USER` | Username |
|
||||
| `POSTGRES_DB` | Database name |
|
||||
|
||||
*3.* Configure Docker
|
||||
----------------------------------------------------------------
|
||||
Edit `docker-compose.yml`.
|
||||
|
||||
*4.* Build Misskey
|
||||
----------------------------------------------------------------
|
||||
Build misskey with the following:
|
||||
|
||||
`docker-compose build`
|
||||
|
||||
*5.* Init DB
|
||||
----------------------------------------------------------------
|
||||
``` shell
|
||||
docker-compose run --rm web yarn run init
|
||||
```
|
||||
|
||||
*6.* That is it.
|
||||
----------------------------------------------------------------
|
||||
Well done! Now you have an environment to run Misskey.
|
||||
|
||||
### Launch normally
|
||||
Just `docker-compose up -d`. GLHF!
|
||||
|
||||
### How to update your Misskey server to the latest version
|
||||
1. `git stash`
|
||||
2. `git checkout master`
|
||||
3. `git pull`
|
||||
4. `git submodule update --init`
|
||||
5. `git stash pop`
|
||||
6. `docker-compose build`
|
||||
7. Check [ChangeLog](../CHANGELOG.md) for migration information
|
||||
8. `docker-compose stop && docker-compose up -d`
|
||||
|
||||
### How to execute [cli commands](manage.en.md):
|
||||
`docker-compose run --rm web node built/tools/mark-admin @example`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
If you have any questions or trouble, feel free to contact us!
|
@@ -1,91 +0,0 @@
|
||||
Guide Docker
|
||||
================================================================
|
||||
|
||||
Ce guide explique comment installer et configurer Misskey avec Docker.
|
||||
|
||||
- [Version japonaise également disponible - Japanese version also available - 日本語版もあります](./docker.ja.md)
|
||||
- [Version anglaise également disponible - English version also available - 英語版もあります](./docker.en.md)
|
||||
- [Version Chinois simplifié également disponible - Simplified Chinese version also available - 简体中文版同样可用](./docker.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Télécharger Misskey
|
||||
----------------------------------------------------------------
|
||||
1. Clone le dépôt de Misskey sur la branche master.
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
2. Naviguez dans le dossier du dépôt.
|
||||
|
||||
`cd misskey`
|
||||
|
||||
3. Checkout sur le tag de la [dernière version](https://github.com/misskey-dev/misskey/releases/latest).
|
||||
|
||||
`git checkout master`
|
||||
|
||||
*2.* Configuration de Misskey
|
||||
----------------------------------------------------------------
|
||||
1. `cp .config/example.yml .config/default.yml` Copiez le fichier `.config/example.yml` et renommez-le `default.yml`.
|
||||
2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` Copie le fichier `.config/mongo_initdb_example.js` et le renomme en `mongo_initdb.js`.
|
||||
3. Editez `default.yml` et `mongo_initdb.js`.
|
||||
|
||||
*3.* Configurer Docker
|
||||
----------------------------------------------------------------
|
||||
Editez `docker-compose.yml`.
|
||||
|
||||
*4.* Contruire Misskey
|
||||
----------------------------------------------------------------
|
||||
Contruire l'image Docker avec:
|
||||
|
||||
`docker-compose build`
|
||||
|
||||
*5.* C'est tout !
|
||||
----------------------------------------------------------------
|
||||
Parfait, Vous avez un environnement prêt pour démarrer Misskey.
|
||||
|
||||
### Lancer normalement
|
||||
Utilisez la commande `docker-compose up -d`. GLHF!
|
||||
|
||||
### How to update your Misskey server to the latest version
|
||||
1. `git stash`
|
||||
2. `git checkout master`
|
||||
3. `git pull`
|
||||
4. `git submodule update --init`
|
||||
5. `git stash pop`
|
||||
6. `docker-compose build`
|
||||
7. Consultez le [ChangeLog](../CHANGELOG.md) pour avoir les éventuelles informations de migration
|
||||
8. `docker-compose stop && docker-compose up -d`
|
||||
|
||||
### Comment exécuter des [commandes](manage.fr.md)
|
||||
`docker-compose run --rm web node built/tools/mark-admin @example`
|
||||
|
||||
### Configuration d'ElasticSearch (pour la fonction de recherche)
|
||||
*1.* Préparation de l'environnement
|
||||
----------------------------------------------------------------
|
||||
1. Permet de créer le dossier d'accueil de la base ElasticSearch aves les bons droits
|
||||
|
||||
`mkdir elasticsearch && chown 1000:1000 elasticsearch`
|
||||
|
||||
2. Augmente la valeur max du paramètre map_count du système (valeur minimum pour pouvoir lancer ES)
|
||||
|
||||
`sysctl -w vm.max_map_count=262144`
|
||||
|
||||
*2.* Après lancement du docker-compose, initialisation de la base ElasticSearch
|
||||
----------------------------------------------------------------
|
||||
1. Connexion dans le conteneur web
|
||||
|
||||
`docker-compose -it web /bin/sh`
|
||||
|
||||
2. Ajout du paquet curl
|
||||
|
||||
`apk add curl`
|
||||
|
||||
3. Création de la base ES
|
||||
|
||||
`curl -X PUT "es:9200/misskey" -H 'Content-Type: application/json' -d'{ "settings" : { "index" : { } }}'`
|
||||
|
||||
4. `exit`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
Si vous avez des questions ou des problèmes, n'hésitez pas à nous contacter !
|
@@ -1,98 +0,0 @@
|
||||
Dockerを使ったMisskey構築方法
|
||||
================================================================
|
||||
|
||||
このガイドはDockerを使ったMisskeyセットアップ方法について解説します。
|
||||
|
||||
- [英語版もあります - English version also available](./docker.en.md)
|
||||
- [简体中文版同样可用 - Simplified Chinese version also available](./docker.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Misskeyのダウンロード
|
||||
----------------------------------------------------------------
|
||||
1. masterブランチからMisskeyレポジトリをクローン
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
2. misskeyディレクトリに移動
|
||||
|
||||
`cd misskey`
|
||||
|
||||
3. [最新のリリース](https://github.com/misskey-dev/misskey/releases/latest)を確認
|
||||
|
||||
`git checkout master`
|
||||
|
||||
*2.* 設定ファイルの作成と編集
|
||||
----------------------------------------------------------------
|
||||
|
||||
下記コマンドで設定ファイルを作成してください。
|
||||
|
||||
```bash
|
||||
cd .config
|
||||
cp example.yml default.yml
|
||||
cp docker_example.env docker.env
|
||||
```
|
||||
|
||||
### `default.yml`の編集
|
||||
|
||||
非Docker環境と同じ様に編集してください。
|
||||
ただし、Postgresql、RedisとElasticsearchのホストは`localhost`ではなく、`docker-compose.yml`で設定されたサービス名になっています。
|
||||
標準設定では次の通りです。
|
||||
|
||||
| サービス | ホスト名 |
|
||||
|---------------|---------|
|
||||
| Postgresql |`db` |
|
||||
| Redis |`redis` |
|
||||
| Elasticsearch |`es` |
|
||||
|
||||
### `docker.env`の編集
|
||||
|
||||
このファイルはPostgresqlの設定を記述します。
|
||||
最低限記述する必要がある設定は次の通りです。
|
||||
|
||||
| 設定 | 内容 |
|
||||
|---------------------|--------------|
|
||||
| `POSTGRES_PASSWORD` | パスワード |
|
||||
| `POSTGRES_USER` | ユーザー名 |
|
||||
| `POSTGRES_DB` | データベース名 |
|
||||
|
||||
*3.* Dockerの設定
|
||||
----------------------------------------------------------------
|
||||
`docker-compose.yml`を編集してください。
|
||||
|
||||
*4.* Misskeyのビルド
|
||||
----------------------------------------------------------------
|
||||
次のコマンドでMisskeyをビルドしてください:
|
||||
|
||||
`docker-compose build`
|
||||
|
||||
*5.* データベースを初期化
|
||||
----------------------------------------------------------------
|
||||
``` shell
|
||||
docker-compose run --rm web yarn run init
|
||||
```
|
||||
|
||||
*6.* 以上です!
|
||||
----------------------------------------------------------------
|
||||
お疲れ様でした。これでMisskeyを動かす準備は整いました。
|
||||
|
||||
### 通常起動
|
||||
`docker-compose up -d`するだけです。GLHF!
|
||||
|
||||
### Misskeyを最新バージョンにアップデートする方法:
|
||||
1. `git stash`
|
||||
2. `git checkout master`
|
||||
3. `git pull`
|
||||
4. `git submodule update --init`
|
||||
5. `git stash pop`
|
||||
6. `docker-compose build`
|
||||
7. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
|
||||
8. `docker-compose stop && docker-compose up -d`
|
||||
|
||||
### cliコマンドを実行する方法:
|
||||
|
||||
`docker-compose run --rm web node built/tools/mark-admin @example`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
なにかお困りのことがありましたらお気軽にご連絡ください。
|
@@ -1,97 +0,0 @@
|
||||
Docker 部署指南
|
||||
================================================================
|
||||
|
||||
这份指南描述了如何使用Docker安装并设置 Misskey 。
|
||||
|
||||
- [日本語版もあります - Japanese version also available](./docker.ja.md)
|
||||
- [英語版もあります - English version also available](./docker.en.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* 下载 Misskey
|
||||
----------------------------------------------------------------
|
||||
1. 克隆 Misskey 项目的 master 分支。
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
2. 进入 misskey 文件夹。
|
||||
|
||||
`cd misskey`
|
||||
|
||||
3. 检查 [最新发布版](https://github.com/misskey-dev/misskey/releases/latest) 标签。
|
||||
|
||||
`git checkout master`
|
||||
|
||||
*2.* 配置 Misskey
|
||||
----------------------------------------------------------------
|
||||
|
||||
可以按照如下方式创建配置文件:
|
||||
|
||||
``` bash
|
||||
cd .config
|
||||
cp example.yml default.yml
|
||||
cp docker_example.env docker.env
|
||||
```
|
||||
|
||||
### `default.yml`
|
||||
|
||||
这个文件的编辑工作基本与非 Docker 环境的版本相同。
|
||||
但请注意, Postgresql、 Redis 和 Elasticsearch 的 **主机名(hostname)** 配置不应该是 `localhost` ,它们被设置在 `docker-compose.yml` 文件中。
|
||||
以下是默认的主机名:
|
||||
|
||||
| 服务 | 主机名 |
|
||||
|---------------|----------|
|
||||
| Postgresql | `db` |
|
||||
| Redis | `redis` |
|
||||
| Elasticsearch | `es` |
|
||||
|
||||
### `docker.env`
|
||||
|
||||
在这个文件中配置 Postgresql 。
|
||||
至少需要如下这些配置:
|
||||
|
||||
| 名称 | 描述 |
|
||||
|---------------------|---------------|
|
||||
| `POSTGRES_PASSWORD` | 数据库密码 |
|
||||
| `POSTGRES_USER` | 数据库用户名 |
|
||||
| `POSTGRES_DB` | 数据库名 |
|
||||
|
||||
*3.* 配置 Docker
|
||||
----------------------------------------------------------------
|
||||
编辑 `docker-compose.yml` 文件。
|
||||
|
||||
*4.* 构建 Misskey
|
||||
----------------------------------------------------------------
|
||||
使用如下的方式构建Misskey:
|
||||
|
||||
`docker-compose build`
|
||||
|
||||
*5.* 初始化数据库
|
||||
----------------------------------------------------------------
|
||||
``` bash
|
||||
docker-compose run --rm web yarn run init
|
||||
```
|
||||
|
||||
*6.* 完成了!
|
||||
----------------------------------------------------------------
|
||||
干得不错!现在您拥有了一个可以运行Misskey的环境啦。
|
||||
|
||||
### 正常启动
|
||||
只需要 `docker-compose up -d` 即可。玩得愉快!
|
||||
|
||||
### 如何将您的 Misskey 服务器升级至最新版本
|
||||
1. `git stash`
|
||||
2. `git checkout master`
|
||||
3. `git pull`
|
||||
4. `git submodule update --init`
|
||||
5. `git stash pop`
|
||||
6. `docker-compose build`
|
||||
7. 检查 [更新日志](../CHANGELOG.md) 以获取升级迁移信息。
|
||||
8. `docker-compose stop && docker-compose up -d`
|
||||
|
||||
### 如何执行 [控制台指令](manage.zh.md):
|
||||
`docker-compose run --rm web node built/tools/mark-admin @example`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
如果您有任何疑问或是困惑,欢迎与我们联系!
|
@@ -1,71 +0,0 @@
|
||||
# Sample nginx configuration for Misskey
|
||||
#
|
||||
# 1. Replace example.tld to your domain
|
||||
# 2. Copy to /etc/nginx/sites-available/ and then symlink from /etc/nginx/sites-enabled/
|
||||
# or copy to /etc/nginx/conf.d/
|
||||
|
||||
# For WebSocket
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name example.tld;
|
||||
|
||||
# For SSL domain validation
|
||||
root /var/www/html;
|
||||
location /.well-known/acme-challenge/ { allow all; }
|
||||
location /.well-known/pki-validation/ { allow all; }
|
||||
location / { return 301 https://$server_name$request_uri; }
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name example.tld;
|
||||
ssl_session_cache shared:ssl_session_cache:10m;
|
||||
|
||||
# To use Let's Encrypt certificate
|
||||
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
|
||||
|
||||
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
|
||||
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
|
||||
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
|
||||
|
||||
# SSL protocol settings
|
||||
ssl_protocols TLSv1.2;
|
||||
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES128-SHA;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
# Change to your upload limit
|
||||
client_max_body_size 80m;
|
||||
|
||||
# Proxy to Node
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_redirect off;
|
||||
|
||||
# If it's behind another reverse proxy or CDN, remove the following.
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
|
||||
# For WebSocket
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# Cache settings
|
||||
proxy_cache cache1;
|
||||
proxy_cache_lock on;
|
||||
proxy_cache_use_stale updating;
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
# Management guide
|
||||
|
||||
## Check the status of the job queue
|
||||
coming soon
|
||||
|
||||
## Mark as 'admin' user
|
||||
``` shell
|
||||
node built/tools/mark-admin (Username)
|
||||
```
|
||||
|
||||
e.g.
|
||||
``` shell
|
||||
node built/tools/mark-admin @syuilo
|
||||
```
|
@@ -1,14 +0,0 @@
|
||||
# Guide d'administration
|
||||
|
||||
## Vérifier le status de la file d'attente des taches
|
||||
coming soon
|
||||
|
||||
## Marquer un utilisateur en tant que 'admin'
|
||||
``` shell
|
||||
node built/tools/mark-admin (nom d'utilisateur)
|
||||
```
|
||||
|
||||
Exemple :
|
||||
``` shell
|
||||
node built/tools/mark-admin @syuilo
|
||||
```
|
@@ -1,14 +0,0 @@
|
||||
# 運営ガイド
|
||||
|
||||
## ジョブキューの状態を調べる
|
||||
coming soon
|
||||
|
||||
## 管理者ユーザーを設定する
|
||||
``` shell
|
||||
node built/tools/mark-admin (ユーザー名)
|
||||
```
|
||||
|
||||
例:
|
||||
``` shell
|
||||
node built/tools/mark-admin @syuilo
|
||||
```
|
@@ -1,14 +0,0 @@
|
||||
# 运营指南
|
||||
|
||||
## 检查任务队列的状态
|
||||
即将到来……
|
||||
|
||||
## 设置用户为管理员
|
||||
``` shell
|
||||
node built/tools/mark-admin (用户名)
|
||||
```
|
||||
|
||||
样例
|
||||
``` shell
|
||||
node built/tools/mark-admin @syuilo
|
||||
```
|
@@ -1,28 +0,0 @@
|
||||
GitHub Actionsを使用してDocker Hubへpushする方法
|
||||
================================================================
|
||||
|
||||
[/.github/workflows/docker.yml](/.github/workflows/docker.yml) に
|
||||
GitHub ActionによりDocker Hubへpushするワークフローが記述されています。
|
||||
|
||||
オリジナルリポジトリでは、リリースされたタイミングで `latest`, `<リリース名>` それぞれのタグでDocker Hubにpushされます。
|
||||
※ Docker Hub に`<ブランチ名>`のようなタグがあるかもしれませんが、こちらは自動push対象ではありません。
|
||||
|
||||
Fork先でこのワークフローを実行すると失敗します。
|
||||
|
||||
以下では、Fork先で自分のDocker Hubリポジトリにpushするようにする方法を記述します。
|
||||
|
||||
## 自分のDocker Hubリポジトリにpushするように設定する方法
|
||||
|
||||
1. Docker Hubでリポジトリを作成します。
|
||||
2. ワークフローファイルの [images](https://github.com/misskey-dev/misskey/blob/53f3b779bf16abcda4f6e026c51384f3b8fbcc62/.github/workflows/docker.yml#L20) を作成したリポジトリに置き換えます。
|
||||
3. GitHubにて [暗号化されたシークレット](https://docs.github.com/ja/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository) を作成します。
|
||||
作成が必要なのは `DOCKER_USERNAME` と `DOCKER_PASSWORD` で、それぞれDocker Hubのユーザーとパスワードになります。
|
||||
|
||||
## pushする方法
|
||||
|
||||
上記設定によりリリース時に自動的にDocker Hubにpushされるようになります。
|
||||
具体的には、GitHubのリリース機能でリリースしたタイミングで `latest`, `<リリース名>` それぞれのタグでDocker Hubにpushされます。
|
||||
|
||||
また、GitHub上から手動でpushすることも出来ます。
|
||||
それを行うには、Actions => Publish Docker image => Run workflow からbranchを選択してワークフローを実行します。
|
||||
ただし、この場合作成されるタグは`<ブランチ名>`になります。
|
147
docs/setup.en.md
147
docs/setup.en.md
@@ -1,147 +0,0 @@
|
||||
Misskey Setup and Installation Guide
|
||||
================================================================
|
||||
|
||||
We thank you for your interest in setting up your Misskey server!
|
||||
This guide describes how to install and setup Misskey.
|
||||
|
||||
- [Japanese version also available - 日本語版もあります](./setup.ja.md)
|
||||
- [Simplified Chinese version also available - 简体中文版同样可用](./setup.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Create Misskey user
|
||||
----------------------------------------------------------------
|
||||
Running misskey as root is not a good idea so we create a user for that.
|
||||
In debian for exemple :
|
||||
|
||||
```
|
||||
adduser --disabled-password --disabled-login misskey
|
||||
```
|
||||
|
||||
*2.* Install dependencies
|
||||
----------------------------------------------------------------
|
||||
Please install and setup these softwares:
|
||||
|
||||
#### Dependencies :package:
|
||||
* **[Node.js](https://nodejs.org/en/)** (12.x, 14.x)
|
||||
* **[PostgreSQL](https://www.postgresql.org/)** (12.x / 13.x is preferred)
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### Optional
|
||||
* [Yarn](https://yarnpkg.com/) *Optional but recommended for security reason. If you won't install it, use `npx yarn` instead of `yarn`.*
|
||||
* [Elasticsearch](https://www.elastic.co/) - required to enable the search feature
|
||||
* [FFmpeg](https://www.ffmpeg.org/)
|
||||
|
||||
*3.* Install Misskey
|
||||
----------------------------------------------------------------
|
||||
1. Connect to misskey user.
|
||||
|
||||
`su - misskey`
|
||||
|
||||
2. Clone the misskey repo from master branch.
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
3. Navigate to misskey directory
|
||||
|
||||
`cd misskey`
|
||||
|
||||
4. Checkout to the [latest release](https://github.com/misskey-dev/misskey/releases/latest)
|
||||
|
||||
`git checkout master`
|
||||
|
||||
5. Install misskey dependencies.
|
||||
|
||||
`yarn`
|
||||
|
||||
*4.* Configure Misskey
|
||||
----------------------------------------------------------------
|
||||
1. Copy the `.config/example.yml` and rename it to `default.yml`.
|
||||
|
||||
`cp .config/example.yml .config/default.yml`
|
||||
|
||||
2. Edit `default.yml`
|
||||
|
||||
*5.* Build Misskey
|
||||
----------------------------------------------------------------
|
||||
|
||||
Build misskey with the following:
|
||||
|
||||
`NODE_ENV=production yarn build`
|
||||
|
||||
If you're on Debian, you will need to install the `build-essential`, `python` package.
|
||||
|
||||
If you're still encountering errors about some modules, use node-gyp:
|
||||
|
||||
1. `npx node-gyp configure`
|
||||
2. `npx node-gyp build`
|
||||
3. `NODE_ENV=production yarn build`
|
||||
|
||||
*6.* Init DB
|
||||
----------------------------------------------------------------
|
||||
``` shell
|
||||
yarn run init
|
||||
```
|
||||
|
||||
*7.* That is it.
|
||||
----------------------------------------------------------------
|
||||
Well done! Now, you have an environment that run to Misskey.
|
||||
|
||||
### Launch normally
|
||||
Just `NODE_ENV=production npm start`. GLHF!
|
||||
|
||||
### Launch with systemd
|
||||
|
||||
1. Create a systemd service here
|
||||
|
||||
`/etc/systemd/system/misskey.service`
|
||||
|
||||
2. Edit it, and paste this and save:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Misskey daemon
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=misskey
|
||||
ExecStart=/usr/bin/npm start
|
||||
WorkingDirectory=/home/misskey/misskey
|
||||
Environment="NODE_ENV=production"
|
||||
TimeoutSec=60
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=misskey
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
3. Reload systemd and enable the misskey service.
|
||||
|
||||
`systemctl daemon-reload ; systemctl enable misskey`
|
||||
|
||||
4. Start the misskey service.
|
||||
|
||||
`systemctl start misskey`
|
||||
|
||||
You can check if the service is running with `systemctl status misskey`.
|
||||
|
||||
### How to update your Misskey server to the latest version
|
||||
1. `git checkout master`
|
||||
2. `git pull`
|
||||
3. `git submodule update --init`
|
||||
4. `yarn install`
|
||||
5. `NODE_ENV=production yarn build`
|
||||
6. `yarn migrate`
|
||||
7. Restart your Misskey process to apply changes
|
||||
8. Enjoy
|
||||
|
||||
If you encounter any problems with updating, please try the following:
|
||||
1. `yarn clean` or `yarn cleanall`
|
||||
2. Retry update (Don't forget `yarn install`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
If you have any questions or troubles, feel free to contact us!
|
136
docs/setup.fr.md
136
docs/setup.fr.md
@@ -1,136 +0,0 @@
|
||||
Guide d'installation et de configuration de Misskey
|
||||
================================================================
|
||||
|
||||
Nous vous remerçions de l'intrêt que vous manifestez pour l'installation de votre propre instance Misskey !
|
||||
Ce guide décrit les étapes à suivre afin d'installer et de configurer une instance Misskey.
|
||||
|
||||
- [La version en japonnais est également disponible sur - 日本語版もあります](./setup.ja.md)
|
||||
- [Version anglaise également disponible - English version also available - 英語版もあります](./setup.en.md)
|
||||
- [Version Chinois simplifié également disponible - Simplified Chinese version also available - 简体中文版同样可用](./setup.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Création de l'utilisateur Misskey
|
||||
----------------------------------------------------------------
|
||||
Executer misskey en tant que super-utilisateur étant une mauvaise idée, nous allons créer un utilisateur dédié.
|
||||
Sous Debian, par exemple :
|
||||
|
||||
```
|
||||
adduser --disabled-password --disabled-login misskey
|
||||
```
|
||||
|
||||
*2.* Installation des dépendances
|
||||
----------------------------------------------------------------
|
||||
Installez les paquets suivants :
|
||||
|
||||
#### Dépendences :package:
|
||||
* **[Node.js](https://nodejs.org/en/)** (12.x, 14.x)
|
||||
* **[PostgreSQL](https://www.postgresql.org/)** (>= 10)
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### Optionnels
|
||||
* [Yarn](https://yarnpkg.com/) - *recommander pour des raisons de sécurité. Si vous ne l'installez pas, utilisez `npx yarn` au lieu de` yarn`.*
|
||||
* [Elasticsearch](https://www.elastic.co/) - *requis pour pouvoir activer la fonctionnalité de recherche.*
|
||||
* [FFmpeg](https://www.ffmpeg.org/)
|
||||
|
||||
*3.* Installation de Misskey
|
||||
----------------------------------------------------------------
|
||||
1. Basculez vers l'utilisateur misskey.
|
||||
|
||||
`su - misskey`
|
||||
|
||||
2. Clonez la branche master du dépôt misskey.
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
3. Accédez au dossier misskey.
|
||||
|
||||
`cd misskey`
|
||||
|
||||
4. Checkout sur le tag de la [version la plus récente](https://github.com/misskey-dev/misskey/releases/latest)
|
||||
|
||||
`git checkout master`
|
||||
|
||||
5. Installez les dépendances de misskey.
|
||||
|
||||
`yarn install`
|
||||
|
||||
*4.* Création du fichier de configuration
|
||||
----------------------------------------------------------------
|
||||
1. Copiez le fichier `.config/example.yml` et renommez-le`default.yml`.
|
||||
|
||||
`cp .config/example.yml .config/default.yml`
|
||||
|
||||
2. Editez le fichier `default.yml`
|
||||
|
||||
*5.* Construction de Misskey
|
||||
----------------------------------------------------------------
|
||||
|
||||
Construisez Misskey comme ceci :
|
||||
|
||||
`NODE_ENV=production yarn build`
|
||||
|
||||
Si vous êtes sous Debian, vous serez amené à installer les paquets `build-essential` et `python`.
|
||||
|
||||
Si vous rencontrez des erreurs concernant certains modules, utilisez node-gyp:
|
||||
|
||||
1. `npx node-gyp configure`
|
||||
2. `npx node-gyp build`
|
||||
3. `NODE_ENV=production yarn build`
|
||||
|
||||
*6.* C'est tout.
|
||||
----------------------------------------------------------------
|
||||
Excellent ! Maintenant, vous avez un environnement prêt pour lancer Misskey
|
||||
|
||||
### Lancement conventionnel
|
||||
Lancez tout simplement `NODE_ENV=production yarn start`. Bonne chance et amusez-vous bien !
|
||||
|
||||
### Démarrage avec systemd
|
||||
|
||||
1. Créez un service systemd sur
|
||||
|
||||
`/etc/systemd/system/misskey.service`
|
||||
|
||||
2. Editez-le puis copiez et coller ceci dans le fichier :
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Misskey daemon
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=misskey
|
||||
ExecStart=/usr/bin/npm start
|
||||
WorkingDirectory=/home/misskey/misskey
|
||||
Environment="NODE_ENV=production"
|
||||
TimeoutSec=60
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=misskey
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
3. Redémarre systemd et active le service misskey.
|
||||
|
||||
`systemctl daemon-reload ; systemctl enable misskey`
|
||||
|
||||
4. Démarre le service misskey.
|
||||
|
||||
`systemctl start misskey`
|
||||
|
||||
Vous pouvez vérifier si le service a démarré en utilisant la commande `systemctl status misskey`.
|
||||
|
||||
### Méthode de mise à jour vers la plus récente version de Misskey
|
||||
1. `git checkout master`
|
||||
2. `git pull`
|
||||
3. `git submodule update --init`
|
||||
4. `yarn install`
|
||||
5. `NODE_ENV=production yarn build`
|
||||
6. `yarn migrate`
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
Si vous rencontrez des difficultés ou avez d'autres questions, n'hésitez pas à nous contacter !
|
145
docs/setup.ja.md
145
docs/setup.ja.md
@@ -1,145 +0,0 @@
|
||||
Misskey構築の手引き
|
||||
================================================================
|
||||
|
||||
Misskeyサーバーの構築にご関心をお寄せいただきありがとうございます!
|
||||
このガイドではMisskeyのインストール・セットアップ方法について解説します。
|
||||
|
||||
- [英語版もあります - English version also available](./setup.en.md)
|
||||
- [简体中文版同样可用 - Simplified Chinese version also available](./setup.zh.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* Misskeyユーザーの作成
|
||||
----------------------------------------------------------------
|
||||
Misskeyはrootユーザーで実行しない方がよいため、代わりにユーザーを作成します。
|
||||
Debianの例:
|
||||
|
||||
```
|
||||
adduser --disabled-password --disabled-login misskey
|
||||
```
|
||||
|
||||
*2.* 依存関係をインストールする
|
||||
----------------------------------------------------------------
|
||||
これらのソフトウェアをインストール・設定してください:
|
||||
|
||||
#### 依存関係 :package:
|
||||
* **[Node.js](https://nodejs.org/en/)** (12.x, 14.x)
|
||||
* **[PostgreSQL](https://www.postgresql.org/)** (10以上)
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### オプション
|
||||
* [Yarn](https://yarnpkg.com/)
|
||||
* セキュリティの観点から推奨されます。 yarn をインストールしない方針の場合は、文章中の `yarn` を適宜 `npx yarn` と読み替えてください。
|
||||
* [Elasticsearch](https://www.elastic.co/)
|
||||
* 検索機能を有効にするためにはインストールが必要です。
|
||||
* [FFmpeg](https://www.ffmpeg.org/)
|
||||
|
||||
*3.* Misskeyのインストール
|
||||
----------------------------------------------------------------
|
||||
1. misskeyユーザーを使用
|
||||
|
||||
`su - misskey`
|
||||
|
||||
2. masterブランチからMisskeyレポジトリをクローン
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
3. misskeyディレクトリに移動
|
||||
|
||||
`cd misskey`
|
||||
|
||||
4. [最新のリリース](https://github.com/misskey-dev/misskey/releases/latest)を確認
|
||||
|
||||
`git checkout master`
|
||||
|
||||
5. Misskeyの依存パッケージをインストール
|
||||
|
||||
`yarn install`
|
||||
|
||||
*4.* 設定ファイルを作成する
|
||||
----------------------------------------------------------------
|
||||
1. `.config/example.yml`をコピーし名前を`default.yml`にする。
|
||||
|
||||
`cp .config/example.yml .config/default.yml`
|
||||
|
||||
2. `default.yml` を編集する。
|
||||
|
||||
*5.* Misskeyのビルド
|
||||
----------------------------------------------------------------
|
||||
|
||||
次のコマンドでMisskeyをビルドしてください:
|
||||
|
||||
`NODE_ENV=production yarn build`
|
||||
|
||||
Debianをお使いであれば、`build-essential`パッケージをインストールする必要があります。
|
||||
|
||||
何らかのモジュールでエラーが発生する場合はnode-gypを使ってください:
|
||||
1. `npx node-gyp configure`
|
||||
2. `npx node-gyp build`
|
||||
3. `NODE_ENV=production yarn build`
|
||||
|
||||
*6.* データベースを初期化
|
||||
----------------------------------------------------------------
|
||||
``` shell
|
||||
yarn run init
|
||||
```
|
||||
|
||||
*7.* 以上です!
|
||||
----------------------------------------------------------------
|
||||
お疲れ様でした。これでMisskeyを動かす準備は整いました。
|
||||
|
||||
### 通常起動
|
||||
`NODE_ENV=production yarn start`するだけです。GLHF!
|
||||
|
||||
### systemdを用いた起動
|
||||
1. systemdサービスのファイルを作成
|
||||
|
||||
`/etc/systemd/system/misskey.service`
|
||||
|
||||
2. エディタで開き、以下のコードを貼り付けて保存:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Misskey daemon
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=misskey
|
||||
ExecStart=/usr/bin/npm start
|
||||
WorkingDirectory=/home/misskey/misskey
|
||||
Environment="NODE_ENV=production"
|
||||
TimeoutSec=60
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=misskey
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
CentOSで1024以下のポートを使用してMisskeyを使用する場合は`ExecStart=/usr/bin/sudo /usr/bin/npm start`に変更する必要があります。
|
||||
|
||||
3. systemdを再読み込みしmisskeyサービスを有効化
|
||||
|
||||
`systemctl daemon-reload; systemctl enable misskey`
|
||||
|
||||
4. misskeyサービスの起動
|
||||
|
||||
`systemctl start misskey`
|
||||
|
||||
`systemctl status misskey`と入力すると、サービスの状態を調べることができます。
|
||||
|
||||
### Misskeyを最新バージョンにアップデートする方法:
|
||||
1. `git checkout master`
|
||||
2. `git pull`
|
||||
3. `git submodule update --init`
|
||||
4. `yarn install`
|
||||
5. `NODE_ENV=production yarn build`
|
||||
6. `yarn migrate`
|
||||
|
||||
なにか問題が発生した場合は、`yarn clean`または`yarn cleanall`すると直る場合があります。
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
なにかお困りのことがありましたらお気軽にご連絡ください。
|
147
docs/setup.zh.md
147
docs/setup.zh.md
@@ -1,147 +0,0 @@
|
||||
Misskey 设置和安装指南
|
||||
================================================================
|
||||
|
||||
非常感谢您对构建 Misskey 服务器的关注!
|
||||
这份指南描述了 Misskey 的安装与设置流程。
|
||||
|
||||
- [日本語版もあります - Japanese version also available](./setup.ja.md)
|
||||
- [英語版もあります - English version also available](./setup.en.md)
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
*1.* 创建 Misskey 用户
|
||||
----------------------------------------------------------------
|
||||
直接使用 root 用户来运行 misskey 也许并不是一个好主意,因此我们有必要创建一个专用的用户。
|
||||
以 Debian 为例:
|
||||
|
||||
``` bash
|
||||
adduser --disabled-password --disabled-login misskey
|
||||
```
|
||||
|
||||
*2.* 安装依赖
|
||||
----------------------------------------------------------------
|
||||
请安装并设置如下这些软件:
|
||||
|
||||
#### Dependencies :package:
|
||||
* **[Node.js](https://nodejs.org/en/)** (12.x, 14.x)
|
||||
* **[PostgreSQL](https://www.postgresql.org/)** (>= 10)
|
||||
* **[Redis](https://redis.io/)**
|
||||
|
||||
##### Optional
|
||||
* [Yarn](https://yarnpkg.com/) *可选,但出于安全因素考虑还是推荐安装。如果您没有安装, 您需要使用 `npx yarn` 来代替 `yarn`.*
|
||||
* [Elasticsearch](https://www.elastic.co/) - 为了启用搜索功能,这个搜索引擎是有必要的。
|
||||
* [FFmpeg](https://www.ffmpeg.org/)
|
||||
|
||||
*3.* 安装 Misskey
|
||||
----------------------------------------------------------------
|
||||
1. 连接至 misskey 用户.
|
||||
|
||||
`su - misskey`
|
||||
|
||||
2. 克隆 Misskey 项目的 master 分支。
|
||||
|
||||
`git clone -b master git://github.com/misskey-dev/misskey.git`
|
||||
|
||||
3. 进入 misskey 文件夹。
|
||||
|
||||
`cd misskey`
|
||||
|
||||
4. 检查 [最新发布版](https://github.com/misskey-dev/misskey/releases/latest) 标签。
|
||||
|
||||
`git checkout master`
|
||||
|
||||
5. 安装 Misskey 的依赖。
|
||||
|
||||
`yarn`
|
||||
|
||||
*4.* 配置 Misskey
|
||||
----------------------------------------------------------------
|
||||
1. 复制 `.config/example.yml` 并重命名为 `default.yml`。
|
||||
|
||||
`cp .config/example.yml .config/default.yml`
|
||||
|
||||
2. 编辑 `default.yml`
|
||||
|
||||
*5.* 构建 Misskey
|
||||
----------------------------------------------------------------
|
||||
|
||||
使用如下的指令构建 Misskey :
|
||||
|
||||
`NODE_ENV=production yarn build`
|
||||
|
||||
如果您使用的是 Debian , 您需要安装 `build-essential`, `python` 环境包。
|
||||
|
||||
如果您仍然遇到有关某些模块的错误,您可以使用 node-gyp:
|
||||
|
||||
1. `npx node-gyp configure`
|
||||
2. `npx node-gyp build`
|
||||
3. `NODE_ENV=production yarn build`
|
||||
|
||||
*6.* 初始化数据库
|
||||
----------------------------------------------------------------
|
||||
``` bash
|
||||
yarn run init
|
||||
```
|
||||
|
||||
*7.* 完成了!
|
||||
----------------------------------------------------------------
|
||||
干得不错!现在您拥有了一个可以运行Misskey的环境啦。
|
||||
|
||||
### 正常启动
|
||||
只需要 `NODE_ENV=production npm start` 即可。玩得愉快!
|
||||
|
||||
### 使用 systemd 来启动
|
||||
|
||||
1. 在此处创建一个 systemd 服务:
|
||||
|
||||
`/etc/systemd/system/misskey.service`
|
||||
|
||||
2. 编辑它,粘贴如下内容并保存:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Misskey daemon
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=misskey
|
||||
ExecStart=/usr/bin/npm start
|
||||
WorkingDirectory=/home/misskey/misskey
|
||||
Environment="NODE_ENV=production"
|
||||
TimeoutSec=60
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=misskey
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
3. 重启 systemd 并设置 misskey 服务自动启动:
|
||||
|
||||
`systemctl daemon-reload ; systemctl enable misskey`
|
||||
|
||||
4. 启动 misskey 服务:
|
||||
|
||||
`systemctl start misskey`
|
||||
|
||||
您可以使用 `systemctl status misskey` 来检查服务是否正在运行。
|
||||
|
||||
### 如何将您的 Misskey 服务器升级至最新版本
|
||||
1. `git checkout master`
|
||||
2. `git pull`
|
||||
3. `git submodule update --init`
|
||||
4. `yarn install`
|
||||
5. `NODE_ENV=production yarn build`
|
||||
6. `yarn migrate`
|
||||
7. 重启您的 Misskey 进程来应用改变。
|
||||
8. 尽情享受吧!
|
||||
|
||||
如果您在更新时遇到任何问题,请尝试以下操作:
|
||||
1. `yarn clean` 或是 `yarn cleanall`
|
||||
2. 重试升级 (请不要忘记 `yarn install` )
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
如果您有任何疑问或是困惑,欢迎与我们联系!
|
@@ -6,6 +6,7 @@ search: "البحث"
|
||||
notifications: "الإشعارات"
|
||||
username: "اسم المستخدم"
|
||||
password: "الكلمة السرية"
|
||||
forgotPassword: "نسيتَ كلمة السر"
|
||||
fetchingAsApObject: "جارٍ جلبه مِن الفديفرس…"
|
||||
ok: " حسناً"
|
||||
gotIt: "فهِمت"
|
||||
@@ -62,6 +63,7 @@ files: "الملفات"
|
||||
download: "تنزيل"
|
||||
driveFileDeleteConfirm: "أمتأكد من حذف ملف {name}؟ كل الملاحظات المُرفق بها هذا الملف ستحذف."
|
||||
unfollowConfirm: "أمتأكد من إلغاء متابعة {name}؟"
|
||||
importRequested: "يستغرق الاستيراد بعض الوقت"
|
||||
lists: "القوائم"
|
||||
noLists: "ليس لديك أية قائمة"
|
||||
note: "ملاحظة"
|
||||
@@ -75,6 +77,7 @@ error: "خطأ"
|
||||
somethingHappened: "حدث خطأ"
|
||||
retry: "حاول مجددًا"
|
||||
pageLoadError: "فشل تحميل الصفحة"
|
||||
serverIsDead: "الخادم لا يستجيب، حاول بعد قليل"
|
||||
enterListName: "اسم القائمة"
|
||||
privacy: "الخصوصية"
|
||||
makeFollowManuallyApprove: "القبول يدويا طلبات الإشتراك"
|
||||
@@ -96,6 +99,7 @@ add: "إضافة"
|
||||
reaction: "تفاعل"
|
||||
rememberNoteVisibility: "تذكر إعدادت مدى رؤية الملاحظات"
|
||||
attachCancel: "أزل المرفق"
|
||||
markAsSensitive: "علّمه كمحتوى حساس"
|
||||
enterFileName: "ادخل اسم الملف"
|
||||
mute: "اكتم"
|
||||
unmute: "إلغاء الكتم"
|
||||
@@ -108,15 +112,20 @@ unblockConfirm: "أمتأكد من إلغاء حجب هذا الحساب؟"
|
||||
selectList: "اختر قائمة"
|
||||
editWidgetsExit: "تم"
|
||||
customEmojis: "إيموجي مخصص"
|
||||
emoji: "الوجوه التعبيرية"
|
||||
emojis: "الوجوه التعبيرية"
|
||||
emojiName: "اسم الوجه التعبيري"
|
||||
emojiUrl: "رابط الوجه التعبيري"
|
||||
addEmoji: "إضافة إيموجي"
|
||||
settingGuide: "الإعدادات المستحسنة"
|
||||
cacheRemoteFiles: "خزن مؤقتا الملفات البعيدة"
|
||||
autoAcceptFollowed: "اقبل طلبات المتابعة تلقائيا من الحسابات المتابَعة"
|
||||
loginFailed: "فشل الولوج"
|
||||
showOnRemote: "رؤيته على مثيل الخادم البُعدي"
|
||||
general: "الرئيسية"
|
||||
wallpaper: "خلفية الشاشة"
|
||||
setWallpaper: "استخدم خلفية الشاشة"
|
||||
removeWallpaper: "إزالة خلفية الشاشة"
|
||||
wallpaper: "الخلفية"
|
||||
setWallpaper: "عيّن خلفية"
|
||||
removeWallpaper: "أزل الخلفية"
|
||||
searchWith: "البحث: {q}"
|
||||
youHaveNoLists: "لا تمتلك أية قائمة"
|
||||
followConfirm: "أتريد متابعة {name}؟"
|
||||
@@ -181,7 +190,7 @@ removeAreYouSure: "متأكد من أنك تريد حذف {x}؟"
|
||||
deleteAreYouSure: "متأكد من أنك تريد حذف {x}؟"
|
||||
resetAreYouSure: "هل تريد إعادة التعيين؟"
|
||||
saved: "تم حفظه"
|
||||
messaging: "الدردشة"
|
||||
messaging: "المحادثة"
|
||||
upload: "تحميل"
|
||||
fromDrive: "من المخزن"
|
||||
fromUrl: "من عنوان URL"
|
||||
@@ -193,7 +202,7 @@ explore: "استكشاف"
|
||||
games: "ألعاب Misskey"
|
||||
messageRead: "مقروءة"
|
||||
noMoreHistory: "لا يوجد المزيد من التاريخ"
|
||||
startMessaging: "ابدأ الدردشة"
|
||||
startMessaging: "ابدأ محادثة"
|
||||
nUsersRead: "تمت القراءة من {n}"
|
||||
agreeTo: "اوافق على {0}"
|
||||
tos: "شروط الخدمة"
|
||||
@@ -265,7 +274,17 @@ disablingTimelinesInfo: "سيتمكن المسؤولون ومن تعديل دا
|
||||
registration: "إنشاء حساب"
|
||||
enableRegistration: "تفعيل إنشاء الحسابات الجديدة"
|
||||
invite: "دعوة"
|
||||
driveCapacityPerLocalAccount: "حصة التخزين لكل مستخدم محلي"
|
||||
driveCapacityPerRemoteAccount: "حصة التخزين لكل مستخدم بعيد"
|
||||
inMb: "بالميغابايت"
|
||||
iconUrl: "رابط الأيقونة"
|
||||
bannerUrl: "رابط صورة اللافتة"
|
||||
backgroundImageUrl: "رابط صورة الخلفية"
|
||||
basicInfo: "المعلومات الأساسية "
|
||||
pinnedUsers: "المستخدمون المثبتون"
|
||||
pinnedUsersDescription: "قائمة المستخدمين المثبتين في لسان \"استكشف\" ، اجعل كل اسم مستخدم في سطر لوحده."
|
||||
pinnedPages: "الصفحات المثبتة"
|
||||
pinnedPagesDescription: "أدخل مسار الصفحات التي تريد تثبيتها في أعلى هذا الموقع، اجعل كل مسار في سطر لوحده."
|
||||
pinnedNotes: "ملاحظة مدبسة"
|
||||
hcaptchaSiteKey: "مفتاح الموقع"
|
||||
hcaptchaSecretKey: "المفتاح السري"
|
||||
@@ -278,12 +297,19 @@ manageAntennas: "إدارة الهوائيات"
|
||||
name: "الإسم"
|
||||
antennaSource: "مصدر الهوائي"
|
||||
antennaKeywords: "الكلمات المفتاحية للإستقبال"
|
||||
notifyAntenna: "نبهني بصول ملاحظات جديدة"
|
||||
withFileAntenna: "ملاحظات تحوي ملفات فقط"
|
||||
caseSensitive: "حساسية حالة الأحرف"
|
||||
withReplies: "بالردود"
|
||||
notesAndReplies: "الملاحظات والردود"
|
||||
withFiles: "بالمرفقات"
|
||||
silence: "اكتم"
|
||||
unsilence: "إلغاء الكتم"
|
||||
popularUsers: "المستخدمون الشائعون"
|
||||
recentlyUpdatedUsers: "أصحاب النشاطات الأخيرة"
|
||||
recentlyRegisteredUsers: "المستخدمون المنضمون حديثًا"
|
||||
recentlyDiscoveredUsers: "المستخدمون المكتشفون حديثًا"
|
||||
exploreUsersCount: "يوجد {count} مستخدم(ا)"
|
||||
exploreFediverse: "استكشف الفديفرس"
|
||||
popularTags: "الوسوم الرائجة"
|
||||
userList: "القوائم"
|
||||
@@ -296,11 +322,13 @@ moderator: "مشرِف"
|
||||
nUsersMentioned: "{n} مستخدمين تمت الإشارة إليهم"
|
||||
securityKey: "مفتاح الأمان"
|
||||
securityKeyName: "اسم المفتاح"
|
||||
registerSecurityKey: "سجل مفتاح أمان"
|
||||
lastUsed: "آخر استخدام"
|
||||
unregister: "إلغاء التسجيل"
|
||||
passwordLessLogin: "لِج مِن دون كلمة سرية"
|
||||
resetPassword: "أعد تعيين كلمتك السرية"
|
||||
newPasswordIs: "كلمتك السرية الجديدة هي {password}"
|
||||
reduceUiAnimation: "قلص تأثيرات الواجهة"
|
||||
share: "شارِك"
|
||||
notFound: "غير موجود"
|
||||
cacheClear: "مسح ذاكرة التخزين المؤقت"
|
||||
@@ -315,8 +343,8 @@ invites: "دعوة"
|
||||
groupName: "اسم الفريق"
|
||||
members: "الأعضاء"
|
||||
transfer: "نقل"
|
||||
messagingWithUser: "الدردشة مع مستخدم آخر"
|
||||
messagingWithGroup: "دردشة جماعية"
|
||||
messagingWithUser: "تحدث مع مستخدم"
|
||||
messagingWithGroup: "محادثة جماعية"
|
||||
title: "العنوان"
|
||||
text: "النص"
|
||||
enable: "تشغيل"
|
||||
@@ -361,28 +389,43 @@ total: "المجموع"
|
||||
weekOverWeekChanges: "أسبوعيا"
|
||||
dayOverDayChanges: "يوميا"
|
||||
appearance: "المظهر"
|
||||
clientSettings: "إعدادات العميل"
|
||||
accountSettings: "إعدادات الحساب"
|
||||
promotion: "ترقية"
|
||||
promote: "روِّج"
|
||||
numberOfDays: "عدد الأيام"
|
||||
hideThisNote: "إخفاء هذه الملاحظة"
|
||||
objectStorageBaseUrl: "الرابط الأساسي"
|
||||
objectStoragePrefix: "البادئة"
|
||||
objectStorageEndpoint: "نقطة النهاية"
|
||||
objectStorageRegion: "المنطقة"
|
||||
objectStorageUseSSL: "استخدم SSL"
|
||||
objectStorageUseProxy: "اتصل عبر وكيل"
|
||||
serverLogs: "سجلات الخادم"
|
||||
deleteAll: "حذف الكل"
|
||||
showFixedPostForm: "أظهر نموذج الكتابة في أعلى الصفحة"
|
||||
newNoteRecived: "هناك ملاحظات جديدة"
|
||||
sounds: "الرنات"
|
||||
listen: "استمع"
|
||||
none: "لا شيء"
|
||||
showInPage: "اعرض في الصفحة"
|
||||
volume: "مستوى الصوت"
|
||||
details: "التفاصيل"
|
||||
chooseEmoji: "اختر إيموجي"
|
||||
unableToProcess: "يتعذر إكمال العملية"
|
||||
recentUsed: "المستخدمة مؤخرا"
|
||||
install: "التثبيت"
|
||||
uninstall: "إلغاء التثبيت"
|
||||
installedApps: "التطبيقات المُخوّلة"
|
||||
nothing: "لا يوجد شيء هنا"
|
||||
lastUsedDate: "آخر استخدام"
|
||||
state: "الحالة"
|
||||
sort: "ترتيب حسب"
|
||||
output: "الخارجة"
|
||||
updateRemoteUser: "تحديث المعلومات عن المستخدم البعيد"
|
||||
deleteAllFiles: "حذف كافة الملفات"
|
||||
deleteAllFilesConfirm: "أتريد حذف كل الملفات؟"
|
||||
removeAllFollowing: "ألغ متابعة كل المتابِعين"
|
||||
userSuspended: "تم تعليق هذا المستخدم."
|
||||
userSilenced: "تم إسكات هذا المستخدم."
|
||||
addItem: "إضافة عنصر"
|
||||
@@ -418,7 +461,40 @@ makeActive: "تفعيل"
|
||||
display: "المظهر"
|
||||
copy: "نسخ"
|
||||
metrics: "المقاييس"
|
||||
fileIdOrUrl: "معرف الملف أو رابط"
|
||||
chatOpenBehavior: "سلوك نفاذة المحادثة عند فتحها"
|
||||
behavior: "السلوك"
|
||||
sample: "مثال"
|
||||
abuseReports: "البلاغات"
|
||||
reportAbuse: "البلاغات"
|
||||
reportAbuseOf: "أبلغ عن {name}"
|
||||
fillAbuseReportDescription: "أكتب بالتفصيل سبب الإبلاغ، إذا كنت تبلغ عن ملاحظة أرفق رابط لها."
|
||||
abuseReported: "أُرسل البلاغ، شكرًا لك"
|
||||
send: "أرسل"
|
||||
abuseMarkAsResolved: "علّم البلاغ كمحلول"
|
||||
openInNewTab: "افتح في لسان جديد"
|
||||
defaultNavigationBehaviour: "سلوك الملاحة الافتراضي"
|
||||
waitingFor: "في انتظار {x}"
|
||||
random: "عشوائي"
|
||||
system: "النظام"
|
||||
switchUi: "بدّل واجهة المستخدم"
|
||||
createNew: "أنشِئ جديد"
|
||||
optional: "اختياري"
|
||||
public: "للعامة"
|
||||
i18nInfo: "يترجم متطوعون ميسكي إلى عدة لغات، يمكنك المساعدة عبر {link}"
|
||||
manageAccessTokens: "إدارة رموز الوصول"
|
||||
accountInfo: "معلومات الحساب"
|
||||
notesCount: "عدد الملاحظات"
|
||||
repliesCount: "عدد الردود المرسلة"
|
||||
repliedCount: "عدد الردود المستلمة"
|
||||
followingCount: "عدد الحسابات المتابَعة"
|
||||
followersCount: "عدد المتابِعين"
|
||||
sentReactionsCount: "عدد الانفعالات المرسلة"
|
||||
receivedReactionsCount: "عدد الانفعالات المستلمة"
|
||||
pollVotesCount: "عدد الاستطلاعات المرسلة"
|
||||
pollVotedCount: "عدد الاستطلاعات المستلمة"
|
||||
yes: "نعم"
|
||||
no: "لا"
|
||||
currentVersion: "الإصدار الحالي"
|
||||
latestVersion: "آخر نسخة مستقرة"
|
||||
usageAmount: "الإستخدام"
|
||||
@@ -432,6 +508,7 @@ gallery: "المعرض"
|
||||
expiration: "ينتهي استطلاع الرأي في"
|
||||
middle: "متوسط"
|
||||
global: "الشامل"
|
||||
sent: "أرسل"
|
||||
_docs:
|
||||
admin: "إدارة "
|
||||
_email:
|
||||
@@ -458,12 +535,12 @@ _theme:
|
||||
alpha: "الشفافية"
|
||||
keys:
|
||||
mention: "أشر الى"
|
||||
messageBg: "خلفية الدردشة"
|
||||
messageBg: "خلفية المحادثة"
|
||||
_sfx:
|
||||
note: "الملاحظات"
|
||||
noteMy: "ملاحظتي"
|
||||
notification: "الإشعارات"
|
||||
chat: "الدردشة"
|
||||
chat: "المحادثة"
|
||||
_ago:
|
||||
unknown: "مجهول"
|
||||
future: "المستقبَل"
|
||||
|
@@ -81,6 +81,8 @@ somethingHappened: "Ein Fehler ist aufgetreten"
|
||||
retry: "Wiederholen"
|
||||
pageLoadError: "Laden der Seite fehlgeschlagen."
|
||||
pageLoadErrorDescription: "Dieser Fehler wird meist durch Netzwerkfehler oder den Browser-Cache verursacht. Bitte leere den Cache oder versuche es nach einiger Zeit erneut."
|
||||
serverIsDead: "Dieser Server antwortet nicht. Bitte warte einen Moment und versuche es dann erneut."
|
||||
youShouldUpgradeClient: "Bitte aktualisiere diese Seite, um eine neuere Version deines Clients zu verwenden."
|
||||
enterListName: "Name der Liste eingeben"
|
||||
privacy: "Privatsphäre"
|
||||
makeFollowManuallyApprove: "Follow-Anfragen benötigen Bestätigung"
|
||||
@@ -545,7 +547,7 @@ invisibleNote: "Private Notiz"
|
||||
enableInfiniteScroll: "Automatisch mehr Notizen laden"
|
||||
visibility: "Sichtbarkeit"
|
||||
poll: "Umfrage"
|
||||
useCw: "Inhalt verbergen"
|
||||
useCw: "Inhaltswarnung verwenden"
|
||||
enablePlayer: "Video-Player öffnen"
|
||||
disablePlayer: "Video-Player schließen"
|
||||
expandTweet: "Tweet ausklappen"
|
||||
@@ -764,6 +766,7 @@ middle: "Mittel"
|
||||
low: "Niedrig"
|
||||
emailNotConfiguredWarning: "Keine Email-Adresse hinterlegt"
|
||||
ratio: "Verhältnis"
|
||||
previewNoteText: "Vorschau anzeigen"
|
||||
customCss: "Benutzerdefiniertes CSS"
|
||||
customCssWarn: "Verwende diese Einstellung nur, wenn du weißt, was sie tut. Ungültige Eingaben können dazu führen, dass der Client nicht mehr normal funktioniert."
|
||||
global: "Global"
|
||||
@@ -782,11 +785,24 @@ translatedFrom: "Aus {x} übersetzt"
|
||||
accountDeletionInProgress: "Löschung des Benutzerkontos momentan in Bearbeitung"
|
||||
usernameInfo: "Ein Name, durch den dein Benutzerkonto auf diesem Server identifiziert werden kann. Du kannst das Alphabet (a~z, A~Z), Ziffern (0~9) oder Unterstriche (_) verwenden. Benutzernamen können später nicht geändert werden."
|
||||
aiChanMode: "Ai Modus"
|
||||
keepCw: "Inhaltswarnung beibehalten"
|
||||
keepCw: "Inhaltswarnungen beibehalten"
|
||||
pubSub: "Pub/Sub Benutzerkonten"
|
||||
lastCommunication: "Letzte Kommunikation"
|
||||
resolved: "Gelöst"
|
||||
unresolved: "Ungelöst"
|
||||
itsOn: "Eingeschaltet"
|
||||
itsOff: "Ausgeschaltet"
|
||||
emailRequiredForSignup: "Angaben einer Email-Adresse als benötigt markieren"
|
||||
unread: "Ungelesen"
|
||||
filter: "Filter"
|
||||
controllPanel: "Systemsteuerung"
|
||||
manageAccounts: "Benutzerkonten verwalten"
|
||||
makeReactionsPublic: "Reaktionsverlauf veröffentlichen"
|
||||
makeReactionsPublicDescription: "Jeder wird die Liste deiner gesendeten Reaktionen einsehen können."
|
||||
_signup:
|
||||
almostThere: "Fast geschafft"
|
||||
emailAddressInfo: "Bitte gib deine Email-Adresse ein."
|
||||
emailSent: "An deine Email-Adresse ({email}) wurde soeben eine Bestätigungsmail geschickt. Bitte klicke auf den enthaltenen Link, um die Erstellung deines Benutzerkontos abzuschließen."
|
||||
_accountDelete:
|
||||
accountDelete: "Benutzerkonto löschen"
|
||||
mayTakeTime: "Da die Löschung eines Benutzerkontos ein aufwendiger Prozess ist, kann dessen Dauer davon abhängen, wie viel Inhalt in diesem erstellt wurde oder wie viele Dateien hochgeladen wurden."
|
||||
@@ -901,6 +917,8 @@ _mfm:
|
||||
fontDescription: "Setzt die Schriftart des Inhaltes fest."
|
||||
rainbow: "Regenbogen"
|
||||
rainbowDescription: "Lässt den Inhalt in Regenbogenfarben erscheinen."
|
||||
sparkle: "Glitzer"
|
||||
sparkleDescription: "Verleiht Inhalt einen glitzernden Partikeleffekt."
|
||||
_reversi:
|
||||
reversi: "Reversi"
|
||||
gameSettings: "Spieleinstellungen"
|
||||
@@ -1024,9 +1042,9 @@ _theme:
|
||||
infoFg: "Text von Informationen"
|
||||
infoWarnBg: "Hintergrund von Warnungen"
|
||||
infoWarnFg: "Text von Warnungen"
|
||||
cwBg: "Hintergrund von verborgenen Inhalten"
|
||||
cwFg: "Text von verborgenen Inhalten"
|
||||
cwHoverBg: "Hintergrund von verborgenen Inhalten (Mouseover)"
|
||||
cwBg: "Hintergrund des Inhaltswarnungsknopfs"
|
||||
cwFg: "Text des Inhaltswarnungsknopfs"
|
||||
cwHoverBg: "Hintergrund des Inhaltswarnungsknopfs (Mouseover)"
|
||||
toastBg: "Hintergrund von Benachrichtigungen"
|
||||
toastFg: "Text von Benachrichtigungen"
|
||||
buttonBg: "Hintergrund von Schaltflächen"
|
||||
@@ -1173,7 +1191,7 @@ _widgets:
|
||||
aiscript: "AiScript-Konsole"
|
||||
aichan: "Ai"
|
||||
_cw:
|
||||
hide: "Verbergen"
|
||||
hide: "Inhalt verbergen"
|
||||
show: "Inhalt anzeigen"
|
||||
chars: "{count} Zeichen"
|
||||
files: "{count} Datei(en)"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
---
|
||||
_lang_: "English"
|
||||
headlineMisskey: "A network connected by notes"
|
||||
introMisskey: "Welcome! Misskey is an open source, decentralized microblogging service.\nCreate \"notes\" to share what your thoughts with everyone around you. 📡\nWith \"reactions\", you can also quickly express your feelings about everyone's notes. 👍\nLet's explore a new world! 🚀"
|
||||
introMisskey: "Welcome! Misskey is an open source, decentralized microblogging service.\nCreate \"notes\" to share your thoughts with everyone around you. 📡\nWith \"reactions\", you can also quickly express your feelings about everyone's notes. 👍\nLet's explore a new world! 🚀"
|
||||
monthAndDay: "{month}/{day}"
|
||||
search: "Search"
|
||||
notifications: "Notifications"
|
||||
@@ -81,6 +81,8 @@ somethingHappened: "An error occurred"
|
||||
retry: "Retry"
|
||||
pageLoadError: "Failed to load page."
|
||||
pageLoadErrorDescription: "This is normally caused by network errors or the browser's cache. Try clearing the cache and then try again after waiting a little while."
|
||||
serverIsDead: "This server is not responding. Please wait for a while and try again."
|
||||
youShouldUpgradeClient: "To view this page, please refresh to update your client."
|
||||
enterListName: "Enter a list name"
|
||||
privacy: "Privacy"
|
||||
makeFollowManuallyApprove: "Follow requests require approval"
|
||||
@@ -764,6 +766,7 @@ middle: "Medium"
|
||||
low: "Low"
|
||||
emailNotConfiguredWarning: "Email address not set."
|
||||
ratio: "Ratio"
|
||||
previewNoteText: "Show preview"
|
||||
customCss: "Custom CSS"
|
||||
customCssWarn: "This setting should only be used if you know what it does. Entering improper values may cause the client to stop functioning normally."
|
||||
global: "Global"
|
||||
@@ -782,11 +785,24 @@ translatedFrom: "Translated from {x}"
|
||||
accountDeletionInProgress: "Account deletion is currently in progress"
|
||||
usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later."
|
||||
aiChanMode: "Ai Mode"
|
||||
keepCw: "Keep Content Warning"
|
||||
keepCw: "Keep Content Warnings"
|
||||
pubSub: "Pub/Sub Accounts"
|
||||
lastCommunication: "Last communication"
|
||||
resolved: "Resolved"
|
||||
unresolved: "Unresolved"
|
||||
itsOn: "Enabled"
|
||||
itsOff: "Disabled"
|
||||
emailRequiredForSignup: "Require email address for sign-up"
|
||||
unread: "Unread"
|
||||
filter: "Filter"
|
||||
controllPanel: "Control Panel"
|
||||
manageAccounts: "Manage Accounts"
|
||||
makeReactionsPublic: "Set reaction history to public"
|
||||
makeReactionsPublicDescription: "This will make the list of all your past reactions publicly visible."
|
||||
_signup:
|
||||
almostThere: "Almost there"
|
||||
emailAddressInfo: "Please enter your email address."
|
||||
emailSent: "A confirmation email has been sent to your email address ({email}). Please click the included link to complete account creation."
|
||||
_accountDelete:
|
||||
accountDelete: "Delete Account"
|
||||
mayTakeTime: "As account deletion is a resource-heavy process, it may take some time to complete depending on how much content you have created and how many files you have uploaded."
|
||||
|
@@ -1,8 +1,8 @@
|
||||
---
|
||||
_lang_: "Esperanto"
|
||||
headlineMisskey: "Jen la reto konektita de notoj"
|
||||
introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza mikrobloga servo.\nKreu \"noto\"n por diskonigu tion kio nun okazas, aŭ por parolu pri vi. 📡\nUzu la funkcion \"reago\" por esprimu rapide vian senton pri ies noto. 👍\nBonvole esploru novan mondon. 🚀"
|
||||
monthAndDay: "{day}a/{month}"
|
||||
headlineMisskey: "Jen la reto konektata de notoj"
|
||||
introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza etbloga servo.\nKreu \"noto\"n por paroli vian penson al iuj ĉirkaŭ vi. 📡\nLa funkcion \"reago\" ebligas esprimi rapide vian senton pri ies noto en Fediverso. 👍\nBonvole esploru novan mondon. 🚀"
|
||||
monthAndDay: "{{day}}/{{month}}"
|
||||
search: "Serĉi"
|
||||
notifications: "Sciigoj"
|
||||
username: "Uzantnomo"
|
||||
@@ -20,10 +20,10 @@ instance: "Nodo"
|
||||
settings: "Agordoj"
|
||||
basicSettings: "Ĝeneralaj agordoj"
|
||||
otherSettings: "Aliaj agordoj"
|
||||
openInWindow: "Malfermi en nova fenestro"
|
||||
openInWindow: "Malfermi en fenestro"
|
||||
profile: "Profilo"
|
||||
timeline: "Templinio"
|
||||
noAccountDescription: "Ĉi tiu uzanto ne skribis vivpriskribon."
|
||||
noAccountDescription: "Neniu priskribo"
|
||||
login: "Ensaluti"
|
||||
loggingIn: "Ensalutado…"
|
||||
logout: "Elsaluti"
|
||||
@@ -33,18 +33,18 @@ save: "Konservi"
|
||||
users: "Uzantoj"
|
||||
addUser: "Aldoni uzanton"
|
||||
favorite: "Preferi"
|
||||
favorites: "Preferataĵoj"
|
||||
favorites: "Preferaĵoj"
|
||||
unfavorite: "Malpreferi"
|
||||
favorited: "Aldonita al preferataĵoj"
|
||||
alreadyFavorited: "Ankoraŭ aldonita al via listo de preferaĵoj."
|
||||
cantFavorite: "Ne aldonita al preferataĵoj"
|
||||
pin: "Alpingli al la profilo"
|
||||
favorited: "Aldonita al via listo de preferaĵoj."
|
||||
alreadyFavorited: "Ĝi jam estis aldonita al via listo de preferaĵoj."
|
||||
cantFavorite: "Ne aldonita al via listo de preferaĵoj."
|
||||
pin: "Alpingli"
|
||||
unpin: "Depingli"
|
||||
copyContent: "Kopii enhavon"
|
||||
copyLink: "Kopii ligilon"
|
||||
delete: "Forviŝi"
|
||||
deleteAndEdit: "Forviŝi kaj redakti"
|
||||
deleteAndEditConfirm: "Ĉu vi certas, ke vi volas forigi kaj redakti la noton? Kun tio foriĝos reagoj, plusendaĵoj, kaj respondoj ĉiuj de ĝi."
|
||||
deleteAndEdit: "Redakti foriginte"
|
||||
deleteAndEditConfirm: "Ĉu vi certas, ke vi volas forigi kaj redakti la noton? Tio forviŝos reagojn, plusendojn, kaj respondojn ĉiujn de ĝi."
|
||||
addToList: "Aldoni al listo"
|
||||
sendMessage: "Sendi mesaĝon"
|
||||
copyUsername: "Kopii uzantnomon"
|
||||
@@ -57,17 +57,17 @@ receiveFollowRequest: "Peto de sekvado estas ricevita"
|
||||
followRequestAccepted: "La peto de sekvado akceptita"
|
||||
mention: "Mencioj"
|
||||
mentions: "Al vi"
|
||||
directNotes: "Notoj rektaj"
|
||||
directNotes: "Rekte senditaj"
|
||||
importAndExport: "Importi/eksporti"
|
||||
import: "Importi"
|
||||
export: "Eksporti"
|
||||
files: "Dosieroj"
|
||||
download: "Elŝuti"
|
||||
driveFileDeleteConfirm: "Ĉu vi certas, ke vi volas forviŝi la dosieron \"{name}\"? Pro tio forviŝiĝos ankaŭ la notoj kiuj enhavas ĝin."
|
||||
unfollowConfirm: "Ĉu vi certas, ke vi volas ne plu sekvi {name}'(o)n?"
|
||||
unfollowConfirm: "Ĉu vi certas, ke vi volas ĉesi sekvi {name}'(o)n?"
|
||||
lists: "Listoj"
|
||||
noLists: "Neniu listo"
|
||||
note: "Notoj"
|
||||
note: "Sendi"
|
||||
notes: "Notoj"
|
||||
following: "Sekvatoj"
|
||||
followers: "Sekvantoj"
|
||||
@@ -89,16 +89,16 @@ renote: "Plusendi la noton"
|
||||
unrenote: "Malfari plusendadon"
|
||||
renoted: "Sukcese plusendita"
|
||||
cantRenote: "Oni ne povas plusendi la noton."
|
||||
cantReRenote: "Plusendado ne estas plusendebla."
|
||||
cantReRenote: "Plusendo de noto ne estas plusendebla."
|
||||
quote: "Citi"
|
||||
pinnedNote: "Alpinglita noto"
|
||||
pinned: "Alpingli al la profilo"
|
||||
pinned: "Alpingli"
|
||||
you: "Vi"
|
||||
clickToShow: "Klaku por malkaŝu"
|
||||
sensitive: "Enhavo ne estas deca por laborejo (NSFW)"
|
||||
add: "Aldoni"
|
||||
reaction: "Reagoj"
|
||||
rememberNoteVisibility: "Rememoru la videblecon de la noto laste sendita"
|
||||
rememberNoteVisibility: "Rememoru videblecon de la noto laste sendita "
|
||||
attachCancel: "Deigi aldonaĵon"
|
||||
markAsSensitive: "Troviĝi NSFW"
|
||||
unmarkAsSensitive: "Ne troviĝi NSFW"
|
||||
@@ -137,7 +137,7 @@ removeWallpaper: "Forviŝi ekranfonon. "
|
||||
searchWith: "Serĉi: {q}"
|
||||
youHaveNoLists: "Vi ne havas listojn."
|
||||
followConfirm: "Ĉu vi certas ke vi volas sekvi {name}'(o)n?"
|
||||
host: "Gastiganto"
|
||||
host: "Gastigo"
|
||||
selectUser: "Elekti uzanton"
|
||||
recipient: "Ricevonto"
|
||||
annotation: "Komentarioj"
|
||||
@@ -220,7 +220,7 @@ remoteUserCaution: "Ĉi tiuj infomoj ne estas tute ekzaktaj pro transa uzanto."
|
||||
activity: "Aktiveco"
|
||||
images: "Bildoj"
|
||||
birthday: "Naskiĝdato"
|
||||
registeredDate: "Registriĝdato"
|
||||
registeredDate: "Dato de registriĝo"
|
||||
location: "Loko"
|
||||
theme: "Koloraro"
|
||||
light: "Luma"
|
||||
@@ -254,7 +254,7 @@ unwatch: "Malobservi"
|
||||
accept: "Permesi"
|
||||
normal: "Normala"
|
||||
instanceName: "Nomo de la nodo"
|
||||
instanceDescription: "Mempriskribo de la nodo "
|
||||
instanceDescription: "Priskribo de la nodo "
|
||||
maintainerName: "Nomo de la administranto"
|
||||
maintainerEmail: "Retpoŝto de la administranto"
|
||||
tosUrl: "URL de kondiĉoj de uzado"
|
||||
@@ -262,7 +262,7 @@ thisYear: "Ĉi-jare"
|
||||
thisMonth: "Ĉi-monate"
|
||||
today: "Hodiaŭ"
|
||||
dayX: "{day}a"
|
||||
monthX: "{month}"
|
||||
monthX: "La {month}a monato"
|
||||
yearX: "La jaro {year}"
|
||||
pages: "Paĝoj"
|
||||
connectService: "Konekti"
|
||||
@@ -317,12 +317,14 @@ nUsersMentioned: "{n} uzanto(j) menciis"
|
||||
securityKey: "Sekureca ŝlosilo"
|
||||
securityKeyName: "Nomo de la ŝlosilo"
|
||||
lastUsed: "Plej malnove uzita"
|
||||
unregister: "Malregistriĝi"
|
||||
passwordLessLogin: "Ensaluti sen pasvorto"
|
||||
resetPassword: "Restarigi pasvorton"
|
||||
newPasswordIs: "La nova pasvorto estas {password}."
|
||||
share: "Diskonigi"
|
||||
notFound: "Ne trovita"
|
||||
cacheClear: "Malplenigi staplon"
|
||||
markAsReadAllNotifications: "Marki ĉiujn sciigojn kiel legito"
|
||||
help: "Manlibro de uzado"
|
||||
inputMessageHere: "Entajpu masaĝo tie ĉi"
|
||||
close: "Fermi"
|
||||
@@ -344,10 +346,10 @@ quoteAttached: "Kun citaĵo"
|
||||
quoteQuestion: "Ĉu vi aldonas citaĵon?"
|
||||
noMessagesYet: "Ankoraŭ neniu mesaĝo"
|
||||
newMessageExists: "Vi ricevis novan mesaĝon."
|
||||
onlyOneFileCanBeAttached: "Vi povas aldoni nur unu dosieron po unu mesaĝo."
|
||||
onlyOneFileCanBeAttached: "Oni povas aldoni nur unu dosieron po mesaĝo."
|
||||
signinRequired: "Bonvolu ensaluti"
|
||||
invitations: "Inviti"
|
||||
invitationCode: "Kodo de invito"
|
||||
invitationCode: "Invita kodo"
|
||||
unavailable: "Ne disponebla"
|
||||
passwordMatched: "Konforma"
|
||||
passwordNotMatched: "Nekonforma"
|
||||
@@ -417,7 +419,7 @@ enablePlayer: "Vidi videon"
|
||||
disablePlayer: "Fermi videon"
|
||||
expandTweet: "Disvolvi pepon"
|
||||
themeEditor: "Redaktilo de koloraroj"
|
||||
description: "Priskribe"
|
||||
description: "Priskribo"
|
||||
describeFile: "Priskribi la bildon"
|
||||
enterFileDescription: "Priskribu"
|
||||
author: "Aŭtoro"
|
||||
@@ -433,7 +435,7 @@ emailServer: "Retpoŝta servilo"
|
||||
email: "Retpoŝto"
|
||||
emailAddress: "Retpoŝta adreso"
|
||||
smtpConfig: "Agordoj de SMTP servilo"
|
||||
smtpHost: "Gastiganto"
|
||||
smtpHost: "Gastigo"
|
||||
smtpPort: "Pordo"
|
||||
smtpUser: "Uzantnomo"
|
||||
smtpPass: "Pasvorto"
|
||||
@@ -481,7 +483,7 @@ notSet: "Ne elektita"
|
||||
emailVerified: "Via retpoŝto estis kontrolita."
|
||||
noteFavoritesCount: "La nombro de notoj preferataj"
|
||||
pageLikesCount: "La nombro de paĝoj kiun la uzanto preferas"
|
||||
pageLikedCount: "La nombro de uzantoj kiuj preferas la paĝon"
|
||||
pageLikedCount: "La nombro de uzantoj, kiuj preferas paĝon de ĉi tiu uzanto"
|
||||
contact: "Kontakto"
|
||||
makeExplorable: "Videbligi konton sur la paĝo \"Esplori\""
|
||||
duplicate: "Duobligi"
|
||||
@@ -509,13 +511,13 @@ inUse: "Uzata"
|
||||
editCode: "Redakti kodon"
|
||||
emailNotification: "Sciigoj per retpoŝto"
|
||||
inChannelSearch: "Serĉi en kanalo"
|
||||
useReactionPickerForContextMenu: "Oni malfermas reago-elektilon per dekstro-kliki"
|
||||
useReactionPickerForContextMenu: "Malfermi reago-elektilon per dekstro-klaki"
|
||||
typingUsers: "{users} nun entajpas…"
|
||||
clear: "Vakigi"
|
||||
goBack: "Reiri antaŭ"
|
||||
addDescription: "Priskribi"
|
||||
info: "Informoj"
|
||||
userInfo: "La informoj de uzanto"
|
||||
userInfo: "Informoj de uzanto"
|
||||
unknown: "Nekonata"
|
||||
online: "Surkonektita"
|
||||
offline: "Forkonektita"
|
||||
@@ -540,6 +542,7 @@ troubleshooting: "Problemsolvi"
|
||||
learnMore: "Lernu pli"
|
||||
translate: "Traduki"
|
||||
translatedFrom: "Tradukita el {x}"
|
||||
controllPanel: "Ŝaltpodio"
|
||||
_docs:
|
||||
continueReading: "Legi plu"
|
||||
features: "Funkcioj"
|
||||
@@ -614,8 +617,8 @@ _wordMute:
|
||||
mutedNotes: "Silentigitaj notoj"
|
||||
_theme:
|
||||
manage: "Administri kolorarojn"
|
||||
code: "Kodo de koloraro"
|
||||
description: "Priskribe"
|
||||
code: "Kolorara kodo"
|
||||
description: "Priskribo"
|
||||
color: "Koloro"
|
||||
darken: "Malbrileco"
|
||||
lighten: "Brileco"
|
||||
@@ -667,8 +670,8 @@ _permissions:
|
||||
"write:blocks": "Redakti vian liston de blokitoj"
|
||||
"read:drive": "Legi vian diskon"
|
||||
"write:drive": "Ĉia operacio por skribi, forviŝi, aŭ alimaniere ŝanĝi la informon de dosiero en via disko de Misskey"
|
||||
"read:favorites": "Vidi vian liston de preferataĵoj"
|
||||
"write:favorites": "Redakti vian liston de preferataĵoj."
|
||||
"read:favorites": "Vidi vian liston de preferaĵoj"
|
||||
"write:favorites": "Redakti vian liston de preferaĵoj"
|
||||
"read:following": "Vidi la infomaciojn pri tio, kion vi sekvas"
|
||||
"write:following": "Sekvi aŭ malsekvi alian uzanton"
|
||||
"read:messaging": "Vidi vian retbabiladon"
|
||||
@@ -686,13 +689,13 @@ _antennaSources:
|
||||
all: "Ĉiuj notoj"
|
||||
homeTimeline: "Notoj far uzantoj kiujn vi sekvas"
|
||||
_weekday:
|
||||
sunday: "dimanĉo"
|
||||
monday: "lundo"
|
||||
tuesday: "mardo"
|
||||
wednesday: "merkredo"
|
||||
thursday: "ĵaŭdo"
|
||||
friday: "vendredo"
|
||||
saturday: "sabato"
|
||||
sunday: "Dimanĉo"
|
||||
monday: "Lundo"
|
||||
tuesday: "Mardo"
|
||||
wednesday: "Merkredo"
|
||||
thursday: "Ĵaŭdo"
|
||||
friday: "Vendredo"
|
||||
saturday: "Sabato"
|
||||
_widgets:
|
||||
notifications: "Sciigoj"
|
||||
timeline: "Templinio"
|
||||
@@ -715,23 +718,24 @@ _poll:
|
||||
vote: "Baloti"
|
||||
closed: "Oni jam balotis ĝin"
|
||||
_visibility:
|
||||
public: "Publika"
|
||||
publicDescription: "Via noto estos videbla de ĉiuj uzantoj"
|
||||
home: "Hejma"
|
||||
homeDescription: "Dissendi nur sur hejma templinio"
|
||||
followers: "Sekvantoj"
|
||||
followersDescription: "Nur al sekvantoj al mi"
|
||||
specified: "Rekta"
|
||||
specifiedDescription: "Publikigi nur al specifaj uzantoj"
|
||||
followers: "Nur al sekvantoj"
|
||||
followersDescription: "Publiki nur al viaj sekvantoj"
|
||||
specified: "Rekte"
|
||||
specifiedDescription: "Montri nur al specifaj uzantoj"
|
||||
localOnly: "Nur loka"
|
||||
localOnlyDescription: "Ne montri al transaj uzantoj"
|
||||
_postForm:
|
||||
replyPlaceholder: "Respondi tiun noton…"
|
||||
quotePlaceholder: "Citi tiun noton…"
|
||||
replyPlaceholder: "Respondi la noton…"
|
||||
quotePlaceholder: "Citi la noton…"
|
||||
channelPlaceholder: "Mencii en kanalo…"
|
||||
_profile:
|
||||
name: "Nomo"
|
||||
username: "Uzantnomo"
|
||||
description: "Pri mi"
|
||||
description: "Sinprezento"
|
||||
metadata: "Kromaj informoj"
|
||||
metadataEdit: "Redakti kromaj informoj"
|
||||
changeAvatar: "Ŝanĝi profilbildon"
|
||||
@@ -746,7 +750,7 @@ _charts:
|
||||
federationInstancesTotal: "La totala nombro de nodoj kunfederantaj"
|
||||
usersTotal: "La totala nombro de la uzantoj"
|
||||
activeUsers: "La nombro de la uzantoj aktivaj"
|
||||
notesTotal: "La totala nombro de la notoj"
|
||||
notesTotal: "La totala nombro de notoj"
|
||||
filesTotal: "La totala nombro de la dosieroj"
|
||||
_timelines:
|
||||
home: "Hejma"
|
||||
@@ -859,16 +863,21 @@ _pages:
|
||||
argVariables: "Eniga junto"
|
||||
_notification:
|
||||
fileUploaded: "La dosiero sukcese alŝutiĝis."
|
||||
youGotMention: "{name} mencis"
|
||||
youGotReply: "{name} respondis"
|
||||
youGotQuote: "{name} citis"
|
||||
youRenoted: "{name} plusendis"
|
||||
youGotPoll: "{name} balotis"
|
||||
youGotMessagingMessageFromUser: "{name} sentis mesaĝon al vi."
|
||||
youGotMessagingMessageFromGroup: "Retbabilan mesaĝon oni sendis al la grupo {name}"
|
||||
youWereFollowed: "eksekvis vin"
|
||||
youReceivedFollowRequest: "Vi ricevis peton de sekvado"
|
||||
yourFollowRequestAccepted: "Via peto por sekvado estis akceptita."
|
||||
yourFollowRequestAccepted: "Via peto de sekvado estis akceptita."
|
||||
_types:
|
||||
follow: "Sekvatoj"
|
||||
all: "Ĉio"
|
||||
follow: "Nova sekvatoj"
|
||||
mention: "Mencioj"
|
||||
reply: "Respondoj"
|
||||
renote: "Notoj plusenditaj"
|
||||
quote: "Citi"
|
||||
reaction: "Reagoj"
|
||||
@@ -882,4 +891,4 @@ _deck:
|
||||
antenna: "Antenoj"
|
||||
list: "Listoj"
|
||||
mentions: "Al vi"
|
||||
direct: "Notoj rektaj"
|
||||
direct: "Rekte"
|
||||
|
@@ -7,6 +7,7 @@ search: "Buscar"
|
||||
notifications: "Notificaciones"
|
||||
username: "Nombre de usuario"
|
||||
password: "Contraseña"
|
||||
forgotPassword: "Olvidé mi Contraseña"
|
||||
fetchingAsApObject: "Buscando en el fediverso"
|
||||
ok: "OK"
|
||||
gotIt: "Entendido"
|
||||
@@ -279,6 +280,7 @@ emptyDrive: "El drive está vacío"
|
||||
emptyFolder: "La carpeta está vacía"
|
||||
unableToDelete: "No se puede borrar"
|
||||
inputNewFileName: "Ingrese un nuevo nombre de archivo"
|
||||
inputNewDescription: "Ingrese nueva descripción"
|
||||
inputNewFolderName: "Ingrese un nuevo nombre de la carpeta"
|
||||
circularReferenceFolder: "La carpeta de destino es una sub-carpeta de la carpeta que quieres mover."
|
||||
hasChildFilesOrFolders: "No se puede borrar esta carpeta. No está vacía."
|
||||
@@ -310,6 +312,8 @@ monthX: "Mes {month}"
|
||||
yearX: "Año {year}"
|
||||
pages: "Páginas"
|
||||
integration: "Integración"
|
||||
connectService: "Conectar"
|
||||
disconnectService: "Desconectar"
|
||||
enableLocalTimeline: "Habilitar linea de tiempo local"
|
||||
enableGlobalTimeline: "Habilitar linea de tiempo global"
|
||||
disablingTimelinesInfo: "Aunque se desactiven estas lineas de tiempo, por conveniencia el administrador y los moderadores pueden seguir usándolos"
|
||||
@@ -323,6 +327,7 @@ driveCapacityPerRemoteAccount: "Capacidad del drive por usuario remoto"
|
||||
inMb: "En megabytes"
|
||||
iconUrl: "URL de la imagen del avatar"
|
||||
bannerUrl: "URL de la imagen del banner"
|
||||
backgroundImageUrl: "URL de la imagen de fondo"
|
||||
basicInfo: "Información básica"
|
||||
pinnedUsers: "Usuarios fijados"
|
||||
pinnedUsersDescription: "Describir los usuarios que quiere fijar en la página \"Descubrir\" separados por una linea nueva"
|
||||
@@ -524,6 +529,9 @@ removeAllFollowing: "Retener todos los siguientes"
|
||||
removeAllFollowingDescription: "Cancelar todos los siguientes del servidor {host}. Ejecutar en caso de que esta instancia haya dejado de existir"
|
||||
userSuspended: "Este usuario ha sido suspendido."
|
||||
userSilenced: "Este usuario ha sido silenciado."
|
||||
yourAccountSuspendedTitle: "Esta cuenta ha sido suspendida"
|
||||
yourAccountSuspendedDescription: "Esta cuenta ha sido suspendida debido a violaciones de los términos de servicio del servidor y otras razones. Para más información, póngase en contacto con el administrador. Por favor, no cree una nueva cuenta."
|
||||
menu: "Menú"
|
||||
divider: "Divisor"
|
||||
addItem: "Agregar elemento"
|
||||
rooms: "Cuartos"
|
||||
@@ -543,6 +551,8 @@ disablePlayer: "Cerrar reproductor"
|
||||
expandTweet: "Expandir tweet"
|
||||
themeEditor: "Editor de temas"
|
||||
description: "Descripción"
|
||||
describeFile: "Añade una descripción"
|
||||
enterFileDescription: "Introducir un título"
|
||||
author: "Autor"
|
||||
leaveConfirm: "Hay modificaciones sin guardar. ¿Desea descartarlas?"
|
||||
manage: "Administrar"
|
||||
@@ -645,29 +655,90 @@ driveFilesCount: "Cantidad de archivos en el drive"
|
||||
driveUsage: "Uso del drive"
|
||||
noCrawle: "Rechazar indexación del crawler"
|
||||
noCrawleDescription: "Pedir a los motores de búsqueda que no indexen tu perfil, notas, páginas, etc."
|
||||
lockedAccountInfo: "A menos que configures la visibilidad de tus notas como \"Sólo seguidores\", tus notas serán visibles para cualquiera, incluso si requieres que los seguidores sean aprobados manualmente."
|
||||
alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por defecto"
|
||||
loadRawImages: "Cargar las imágenes originales en lugar de mostrar las miniaturas"
|
||||
disableShowingAnimatedImages: "No reproducir imágenes animadas"
|
||||
verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por favor, acceda al enlace proporcionado en el correo electrónico para completar la configuración."
|
||||
notSet: "Sin especificar"
|
||||
emailVerified: "Su dirección de correo electrónico ha sido verificada."
|
||||
noteFavoritesCount: "Número de notas favoritas"
|
||||
pageLikesCount: "Número de favoritos en la página"
|
||||
pageLikedCount: "Número de favoritos de su página"
|
||||
reversiCount: "Numero de partidas Reversi"
|
||||
contact: "Contacto"
|
||||
useSystemFont: "Utilizar la tipografía por defecto del sistema"
|
||||
clips: "Clip"
|
||||
experimentalFeatures: "Características experimentales"
|
||||
developer: "Desarrolladores"
|
||||
makeExplorable: "Hacer visible la cuenta en \"Explorar\""
|
||||
makeExplorableDescription: "Si desactiva esta opción, su cuenta no aparecerá en la sección \"Explorar\"."
|
||||
showGapBetweenNotesInTimeline: "Mostrar un intervalo entre notas en la línea de tiempo"
|
||||
duplicate: "Duplicar"
|
||||
left: "Izquierda"
|
||||
center: "Centrar"
|
||||
wide: "Ancho"
|
||||
narrow: "Estrecho"
|
||||
reloadToApplySetting: "Esta configuración sólo se aplicará después de recargar la página. ¿Recargar ahora?"
|
||||
showTitlebar: "Mostrar la barra de título"
|
||||
clearCache: "Limpiar caché"
|
||||
onlineUsersCount: "{n} usuarios en línea"
|
||||
nUsers: "{n} Usuarios"
|
||||
nNotes: "{n} Notas"
|
||||
sendErrorReports: "Envíar informe de errores"
|
||||
sendErrorReportsDescription: "Si habilita esta opción, ayudará a mejorar la calidad de Misskey compartiendo información detallada sobre los errores cuando se produzca un problema.\nEsto incluye información como la versión de su sistema operativo, el tipo de navegador que utiliza, su historial de actividad, etc."
|
||||
myTheme: "Mi Tema"
|
||||
backgroundColor: "Fondo"
|
||||
accentColor: "Acento"
|
||||
textColor: "Texto"
|
||||
saveAs: "Guardar como…"
|
||||
advanced: "Avanzado"
|
||||
value: "Valores"
|
||||
createdAt: "Fecha de creación"
|
||||
updatedAt: "Actualizado"
|
||||
saveConfirm: "¿Guardar cambios?"
|
||||
deleteConfirm: "¿Desea eliminarlo?"
|
||||
invalidValue: "Este no es un valor válido."
|
||||
registry: "Registro"
|
||||
closeAccount: "Cerrar cuenta"
|
||||
currentVersion: "Versión actual"
|
||||
latestVersion: "Última versión"
|
||||
youAreRunningUpToDateClient: "Está utilizando la versión más reciente de su cliente."
|
||||
newVersionOfClientAvailable: "Hay una versión más nueva de su cliente disponible."
|
||||
usageAmount: "Uso"
|
||||
capacity: "Capacidad"
|
||||
inUse: "Usado"
|
||||
editCode: "Editar código"
|
||||
goBack: "Deseleccionar"
|
||||
info: "Información"
|
||||
user: "Usuarios"
|
||||
administration: "Administrar"
|
||||
expiration: "Termina el"
|
||||
middle: "Mediano"
|
||||
customCssWarn: "Este ajuste sólo debe utilizarse si se sabe lo que hace. Introducir valores inadecuados puede hacer que el cliente deje de funcionar con normalidad."
|
||||
global: "Global"
|
||||
squareAvatars: "Mostrar iconos cuadrados"
|
||||
sent: "Enviar"
|
||||
received: "Recibido"
|
||||
searchResult: "Resultados de búsqueda"
|
||||
hashtags: "Hashtag"
|
||||
troubleshooting: "Solución de problemas"
|
||||
useBlurEffect: "Utilizar efecto de desenfoque en la interfaz de usuario"
|
||||
learnMore: "Ver más"
|
||||
misskeyUpdated: "¡Misskey ha sido actualizado!"
|
||||
whatIsNew: "Mostrar cambios"
|
||||
translate: "Traducir"
|
||||
translatedFrom: "Traducido de {x}"
|
||||
accountDeletionInProgress: "La eliminación de la cuenta está en curso"
|
||||
usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede utilizar el alfabeto (a~z, A~Z), dígitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente."
|
||||
aiChanMode: "Modo Ai"
|
||||
keepCw: "Mantener la advertencia de contenido"
|
||||
pubSub: "Cuentas Pub/Sub"
|
||||
lastCommunication: "Última comunicación"
|
||||
resolved: "Resuelto"
|
||||
unresolved: "Sin resolver"
|
||||
_accountDelete:
|
||||
accountDelete: "Eliminar Cuenta"
|
||||
_docs:
|
||||
admin: "Administrar"
|
||||
_ad:
|
||||
|
@@ -81,6 +81,8 @@ somethingHappened: "Une erreur est survenue"
|
||||
retry: "Réessayer"
|
||||
pageLoadError: "Le chargement de la page a échoué"
|
||||
pageLoadErrorDescription: "Cela est généralement causé par le cache du navigateur ou par un problème réseau. Veuillez vider votre cache ou attendre un peu et réessayer."
|
||||
serverIsDead: "Le serveur ne répond pas. Patientez quelques instants puis essayez à nouveau."
|
||||
youShouldUpgradeClient: "Si la page ne s'affiche pas correctement, rechargez-la pour mettre votre client à jour."
|
||||
enterListName: "Nom de la liste"
|
||||
privacy: "Confidentialité"
|
||||
makeFollowManuallyApprove: "Accepter manuellement les demandes d’abonnement"
|
||||
@@ -136,7 +138,7 @@ settingGuide: "Configuration proposée"
|
||||
cacheRemoteFiles: "Mise en cache des fichiers distants"
|
||||
cacheRemoteFilesDescription: "Lorsque cette option est désactivée, les fichiers distants sont chargés directement depuis l’instance distante. La désactiver diminuera certes l’utilisation de l’espace de stockage local mais augmentera le trafic réseau puisque les miniatures ne seront plus générées."
|
||||
flagAsBot: "Ce compte est un robot"
|
||||
flagAsBotDescription: "Si ce compte est géré de manière automatisée , définissez cette option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster les systèmes internes de Misskey pour traiter ce compte comme un robot."
|
||||
flagAsBotDescription: "Si ce compte est géré de manière automatisée, choisissez cette option. Si elle est activée, elle agira comme un marqueur pour les autres développeurs afin d'éviter des chaînes d'interaction sans fin avec d'autres robots et d'ajuster les systèmes internes de Misskey pour traiter ce compte comme un robot."
|
||||
flagAsCat: "Ce compte est un chat"
|
||||
flagAsCatDescription: "Activer l'option \" Je suis un chat \" pour ce compte."
|
||||
autoAcceptFollowed: "Accepter automatiquement les demandes d’abonnement venant d’utilisateur·rice·s que vous suivez"
|
||||
@@ -363,7 +365,7 @@ withFiles: "Avec fichiers joints"
|
||||
silence: "Mettre en sourdine"
|
||||
silenceConfirm: "Êtes-vous sûr·e de vouloir mettre l’utilisateur·rice en sourdine ?"
|
||||
unsilence: "Annuler la sourdine"
|
||||
unsilenceConfirm: "Êtes-vous sûr·e de vouloir annuler la mise en sourdine de cette utilisateur·rice ?"
|
||||
unsilenceConfirm: "Êtes-vous sûr·e de vouloir annuler la mise en sourdine de cet·te utilisateur·rice ?"
|
||||
popularUsers: "Utilisateur·rice·s populaires"
|
||||
recentlyUpdatedUsers: "Utilisateur·rice·s actif·ve·s récemment"
|
||||
recentlyRegisteredUsers: "Utilisateur·rice·s récemment inscrit·e·s"
|
||||
@@ -377,7 +379,7 @@ aboutMisskey: "À propos de Misskey"
|
||||
administrator: "Administrateur"
|
||||
token: "Jeton"
|
||||
twoStepAuthentication: "Authentification à deux facteurs"
|
||||
moderator: "Modérateurs"
|
||||
moderator: "Modérateur·rice·s"
|
||||
nUsersMentioned: "{n} utilisateur·rice·s mentionné·e·s"
|
||||
securityKey: "Clé de sécurité"
|
||||
securityKeyName: "Nom de la clé"
|
||||
@@ -495,7 +497,7 @@ objectStorageSetPublicRead: "Régler sur « public » lors de l'envoi"
|
||||
serverLogs: "Journal du serveur"
|
||||
deleteAll: "Supprimer tout"
|
||||
showFixedPostForm: "Afficher le formulaire de publication en haut du fil d'actualité"
|
||||
newNoteRecived: "Vous avez reçu une nouvelle note"
|
||||
newNoteRecived: "Voir les nouvelles notes"
|
||||
sounds: "Sons"
|
||||
listen: "Écouter"
|
||||
none: "Rien"
|
||||
@@ -530,6 +532,7 @@ removeAllFollowingDescription: "Se désabonner de tous les comptes de {host}. Ve
|
||||
userSuspended: "Cet·te utilisateur·rice a été suspendu·e."
|
||||
userSilenced: "Cette utilisateur·trice a été mis·e en sourdine."
|
||||
yourAccountSuspendedTitle: "Ce compte est suspendu"
|
||||
yourAccountSuspendedDescription: "Ce compte est suspendu car vous avez enfreint les conditions d'utilisation de l'instance, ou pour un motif similaire. Si vous souhaitez connaître en détail les raisons de cette suspension, renseignez-vous auprès de l'administrateur·rice de votre instance. Merci de ne pas créer de nouveau compte."
|
||||
menu: "Menu"
|
||||
divider: "Séparateur"
|
||||
addItem: "Ajouter un élément"
|
||||
@@ -763,6 +766,7 @@ middle: "Moyen"
|
||||
low: "Basse"
|
||||
emailNotConfiguredWarning: "Vous n'avez pas configuré d'adresse e-mail."
|
||||
ratio: "Ratio"
|
||||
previewNoteText: "Voir l'aperçu"
|
||||
customCss: "CSS personnalisé"
|
||||
customCssWarn: "Utilisez cette fonctionnalité uniquement si vous savez exactement ce que vous faites. Une configuration inadaptée peut empêcher le client de s'exécuter normalement."
|
||||
global: "Global"
|
||||
@@ -786,6 +790,17 @@ pubSub: "Comptes Pub/Sub"
|
||||
lastCommunication: "Dernière communication"
|
||||
resolved: "Résolu"
|
||||
unresolved: "En attente"
|
||||
itsOn: "Activé"
|
||||
itsOff: "Désactivé"
|
||||
emailRequiredForSignup: "Une adresse e-mail est nécessaire pour créer un compte"
|
||||
unread: "Non lu"
|
||||
filter: "Filtre"
|
||||
controllPanel: "Panneau de contrôle"
|
||||
manageAccounts: "Gérer les comptes"
|
||||
_signup:
|
||||
almostThere: "Bientôt fini"
|
||||
emailAddressInfo: "Insérez votre adresse e-mail."
|
||||
emailSent: "Un courriel de confirmation vient d'être envoyé à l'adresse que vous avez renseignée ({email}). Cliquez sur le lien contenu dans le message pour terminer la création de votre compte."
|
||||
_accountDelete:
|
||||
accountDelete: "Supprimer le compte"
|
||||
mayTakeTime: "La suppression de compte nécessitant beaucoup de ressources, l'exécution du processus peut prendre du temps, en fonction de la quantité de contenus que vous avez créés et du nombre de fichiers que vous avez téléversés."
|
||||
@@ -900,6 +915,8 @@ _mfm:
|
||||
fontDescription: "Il est possible de choisir la police."
|
||||
rainbow: "Arc-en-ciel"
|
||||
rainbowDescription: "Permet d'afficher le contenu en couleurs arc-en-ciel."
|
||||
sparkle: "Paillettes"
|
||||
sparkleDescription: "Ajoute un effet scintillant au contenu."
|
||||
_reversi:
|
||||
reversi: "Reversi"
|
||||
gameSettings: "Réglages de la partie"
|
||||
@@ -1243,7 +1260,7 @@ _charts:
|
||||
federationInstancesTotal: "Nombre total d'instances fédérées"
|
||||
usersIncDec: "Variation du nombre d'utilisateur·rice·s"
|
||||
usersTotal: "Nombre des utilisateur·rice·s au total"
|
||||
activeUsers: "Utilisateur·rice·s actif·ve·s"
|
||||
activeUsers: "Nombre d'utilisateurices actif·ve·s"
|
||||
notesIncDec: "Variation du nombre des notes"
|
||||
localNotesIncDec: "Variation du nombre de notes locales"
|
||||
remoteNotesIncDec: "Variation du nombre de notes distantes"
|
||||
|
@@ -780,6 +780,7 @@ translatedFrom: "Terjemahkan dari {x}"
|
||||
accountDeletionInProgress: "Penghapusan akun sedang dalam proses"
|
||||
usernameInfo: "Nama yang mengidentifikasikan akun kamu dari yang lain pada server ini. Kamu dapat menggunakan alfabet (a~z, A~Z), digit (0~9) atau garis bawah (_). Username tidak dapat diubah setelahnya."
|
||||
keepCw: "Biarkan Peringatan Konten"
|
||||
controllPanel: "Panel kontrol"
|
||||
_accountDelete:
|
||||
accountDelete: "Hapus akun"
|
||||
mayTakeTime: "Karena penghapusan akun merupakan proses yang berat dan intensif, kemungkinan dapat membutuhkan waktu untuk menyelesaikan tergantung daripada berapa banyak konten yang kamu buat dan berapa banyak berkas yang telah kamu unggah."
|
||||
|
@@ -482,7 +482,7 @@ objectStorageSetPublicRead: "Imposta \"visibilità pubblica\" al momento di cari
|
||||
serverLogs: "Log del server"
|
||||
deleteAll: "Cancella cronologia"
|
||||
showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline"
|
||||
newNoteRecived: "Nuova nota ricevuta"
|
||||
newNoteRecived: "Vedi le nuove note"
|
||||
sounds: "Impostazioni suoni"
|
||||
listen: "Ascolta"
|
||||
none: "Niente"
|
||||
|
@@ -81,6 +81,8 @@ somethingHappened: "問題が発生しました"
|
||||
retry: "再試行"
|
||||
pageLoadError: "ページの読み込みに失敗しました。"
|
||||
pageLoadErrorDescription: "これは通常、ネットワークまたはブラウザキャッシュが原因です。キャッシュをクリアするか、しばらく待ってから再度試してください。"
|
||||
serverIsDead: "サーバーの応答がありません。しばらく待ってから再度試してください。"
|
||||
youShouldUpgradeClient: "このページを表示するためには、リロードして新しいバージョンのクライアントをご利用ください。"
|
||||
enterListName: "リスト名を入力"
|
||||
privacy: "プライバシー"
|
||||
makeFollowManuallyApprove: "フォローを承認制にする"
|
||||
@@ -764,6 +766,7 @@ middle: "中"
|
||||
low: "低"
|
||||
emailNotConfiguredWarning: "メールアドレスの設定がされていません。"
|
||||
ratio: "比率"
|
||||
previewNoteText: "本文をプレビュー"
|
||||
customCss: "カスタムCSS"
|
||||
customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。"
|
||||
global: "グローバル"
|
||||
@@ -787,6 +790,20 @@ pubSub: "Pub/Subのアカウント"
|
||||
lastCommunication: "直近の通信"
|
||||
resolved: "解決済み"
|
||||
unresolved: "未解決"
|
||||
itsOn: "オンになっています"
|
||||
itsOff: "オフになっています"
|
||||
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
|
||||
unread: "未読"
|
||||
filter: "フィルタ"
|
||||
controllPanel: "コントロールパネル"
|
||||
manageAccounts: "アカウントを管理"
|
||||
makeReactionsPublic: "リアクション一覧を公開する"
|
||||
makeReactionsPublicDescription: "あなたがしたリアクション一覧を誰でも見れるようにします。"
|
||||
|
||||
_signup:
|
||||
almostThere: "ほとんど完了です"
|
||||
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。"
|
||||
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。"
|
||||
|
||||
_accountDelete:
|
||||
accountDelete: "アカウントの削除"
|
||||
|
@@ -787,6 +787,7 @@ pubSub: "Pub/Sub 계정"
|
||||
lastCommunication: "마지막 통신"
|
||||
resolved: "해결됨"
|
||||
unresolved: "해결되지 않음"
|
||||
controllPanel: "제어판"
|
||||
_accountDelete:
|
||||
accountDelete: "계정 삭제"
|
||||
mayTakeTime: "계정 삭제는 서버에 부하를 가하기 때문에, 작성한 콘텐츠나 업로드한 파일의 수가 많으면 완료까지 시간이 걸릴 수 있습니다."
|
||||
@@ -901,6 +902,8 @@ _mfm:
|
||||
fontDescription: "내용의 글꼴을 지정할 수 있습니다."
|
||||
rainbow: "무지개"
|
||||
rainbowDescription: "내용을 무지개로 표시합니다."
|
||||
sparkle: "반짝반짝"
|
||||
sparkleDescription: "반짝이는 파티클 효과를 추가합니다."
|
||||
_reversi:
|
||||
reversi: "리버시"
|
||||
gameSettings: "대국 설정"
|
||||
|
@@ -81,6 +81,8 @@ somethingHappened: "Что-то пошло не так"
|
||||
retry: "Повторить попытку"
|
||||
pageLoadError: "Не удалось загрузить страницу"
|
||||
pageLoadErrorDescription: "Обычно это случается из-за сбоев в сети или кэша браузера. Попробуйте очистить кэш, или подождать пару минут, а потом попытаться загрузить страницу снова."
|
||||
serverIsDead: "Ответа от сервера нет. Пожалуйста, подождите немного и повторите попытку."
|
||||
youShouldUpgradeClient: "Чтобы просмотреть эту страницу, пожалуйста, обновите ее."
|
||||
enterListName: "Название списка"
|
||||
privacy: "Конфиденциальность"
|
||||
makeFollowManuallyApprove: "Принимать подписчиков вручную"
|
||||
@@ -529,6 +531,8 @@ removeAllFollowing: "Удалить всех подписчиков"
|
||||
removeAllFollowingDescription: "Отменить все подписки с домена {host}? Пожалуйста, применяйте это действие, если инстанс больше не существует."
|
||||
userSuspended: "Эта учётная запись заморожена"
|
||||
userSilenced: "Этот пользователь был заглушен"
|
||||
yourAccountSuspendedTitle: "Эта учетная запись заблокирована"
|
||||
yourAccountSuspendedDescription: "Эта учетная запись была заблокирована из-за нарушения условий предоставления услуг сервера. Свяжитесь с администратором, если вы хотите узнать более подробную причину. Пожалуйста, не создавайте новую учетную запись."
|
||||
menu: "Меню"
|
||||
divider: "Линия-разделитель"
|
||||
addItem: "Добавить элемент"
|
||||
@@ -775,6 +779,13 @@ useBlurEffect: "Размытие в интерфейсе"
|
||||
learnMore: "Подробнее"
|
||||
misskeyUpdated: "Misskey обновился!"
|
||||
whatIsNew: "Что новенького?"
|
||||
translate: "Перевод"
|
||||
accountDeletionInProgress: "В настоящее время выполняется удаление учетной записи"
|
||||
usernameInfo: "Имя, которое отличает вашу учетную запись от других на этом сервере. Вы можете использовать алфавит (a~z, A~Z), цифры (0~9) или символы подчеркивания (_). Имена пользователей не могут быть изменены позже."
|
||||
aiChanMode: "ИИ режим"
|
||||
keepCw: "Сохраняйте Предупреждения о содержимом"
|
||||
controllPanel: "Панель управления"
|
||||
manageAccounts: "Управление аккаунтом"
|
||||
_docs:
|
||||
continueReading: "Читать подробнее"
|
||||
features: "Возможности"
|
||||
|
@@ -81,6 +81,8 @@ somethingHappened: "出现了一些问题!"
|
||||
retry: "重试"
|
||||
pageLoadError: "页面加载失败。"
|
||||
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
|
||||
serverIsDead: "服务器没有响应。 请稍等片刻,然后重试。"
|
||||
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
|
||||
enterListName: "输入列表名称"
|
||||
privacy: "隐私"
|
||||
makeFollowManuallyApprove: "关注者的关注请求需要批准"
|
||||
@@ -764,6 +766,7 @@ middle: "中"
|
||||
low: "低"
|
||||
emailNotConfiguredWarning: "电子邮件地址未设置。"
|
||||
ratio: "比率"
|
||||
previewNoteText: "预览文本"
|
||||
customCss: "自定义 CSS"
|
||||
customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用!"
|
||||
global: "全局"
|
||||
@@ -787,6 +790,17 @@ pubSub: "Pub/Sub账户"
|
||||
lastCommunication: "最近通信"
|
||||
resolved: "已解决"
|
||||
unresolved: "未解决"
|
||||
itsOn: "已开启"
|
||||
itsOff: "已关闭"
|
||||
emailRequiredForSignup: "注册账户需要电子邮件地址"
|
||||
unread: "未读"
|
||||
filter: "筛选"
|
||||
controllPanel: "控制面板"
|
||||
manageAccounts: "管理账户"
|
||||
_signup:
|
||||
almostThere: "即将完成"
|
||||
emailAddressInfo: "请输入您所使用的电子邮件地址"
|
||||
emailSent: "已将确认邮件发送至您输入的电子邮件地址 ({email})。请访问电子邮件中的链接以完成帐户创建。"
|
||||
_accountDelete:
|
||||
accountDelete: "删除帐户"
|
||||
mayTakeTime: "删除账号是一个性能损耗较大的处理,如果账号持有的内容数量和上传的文件数量较多的话,完成需要花费一段时间。"
|
||||
@@ -901,6 +915,8 @@ _mfm:
|
||||
fontDescription: "可以设置内容所使用的字体。"
|
||||
rainbow: "彩虹"
|
||||
rainbowDescription: "用彩虹色来显示内容。"
|
||||
sparkle: "闪光"
|
||||
sparkleDescription: "添加发光粒子效果。"
|
||||
_reversi:
|
||||
reversi: "黑白棋"
|
||||
gameSettings: "对局设置"
|
||||
|
14
migration/1633068642000-email-required-for-signup.ts
Normal file
14
migration/1633068642000-email-required-for-signup.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class emailRequiredForSignup1633068642000 implements MigrationInterface {
|
||||
name = 'emailRequiredForSignup1633068642000'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "emailRequiredForSignup" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "emailRequiredForSignup"`);
|
||||
}
|
||||
|
||||
}
|
16
migration/1633071909016-user-pending.ts
Normal file
16
migration/1633071909016-user-pending.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class userPending1633071909016 implements MigrationInterface {
|
||||
name = 'userPending1633071909016'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "user_pending" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "code" character varying(128) NOT NULL, "username" character varying(128) NOT NULL, "email" character varying(128) NOT NULL, "password" character varying(128) NOT NULL, CONSTRAINT "PK_d4c84e013c98ec02d19b8fbbafa" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4e5c4c99175638ec0761714ab0" ON "user_pending" ("code") `);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_4e5c4c99175638ec0761714ab0"`);
|
||||
await queryRunner.query(`DROP TABLE "user_pending"`);
|
||||
}
|
||||
|
||||
}
|
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> {
|
||||
}
|
||||
|
||||
}
|
91
package.json
91
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
||||
"version": "12.91.0",
|
||||
"version": "12.93.0",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@sinonjs/fake-timers": "7.1.2",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.15.4",
|
||||
"@types/bull": "3.15.5",
|
||||
"@types/cbor": "6.0.0",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/escape-regexp": "0.0.0",
|
||||
@@ -60,20 +60,20 @@
|
||||
"@types/jsonld": "1.5.6",
|
||||
"@types/katex": "0.11.1",
|
||||
"@types/koa": "2.13.4",
|
||||
"@types/koa-bodyparser": "4.3.3",
|
||||
"@types/koa-cors": "0.0.2",
|
||||
"@types/koa-favicon": "2.0.21",
|
||||
"@types/koa-logger": "3.1.1",
|
||||
"@types/koa-mount": "4.0.1",
|
||||
"@types/koa-send": "4.1.3",
|
||||
"@types/koa-views": "7.0.0",
|
||||
"@types/koa__cors": "3.0.3",
|
||||
"@types/koa__multer": "2.0.3",
|
||||
"@types/koa__router": "8.0.8",
|
||||
"@types/markdown-it": "12.2.1",
|
||||
"@types/koa-bodyparser": "4.3.3",
|
||||
"@types/koa-cors": "0.0.2",
|
||||
"@types/koa-favicon": "2.0.21",
|
||||
"@types/koa-logger": "3.1.2",
|
||||
"@types/koa-mount": "4.0.1",
|
||||
"@types/koa-send": "4.1.3",
|
||||
"@types/koa-views": "7.0.0",
|
||||
"@types/markdown-it": "12.2.3",
|
||||
"@types/matter-js": "0.17.5",
|
||||
"@types/mocha": "8.2.3",
|
||||
"@types/node": "16.9.6",
|
||||
"@types/node": "16.10.3",
|
||||
"@types/node-fetch": "2.5.12",
|
||||
"@types/nodemailer": "6.4.4",
|
||||
"@types/nprogress": "0.2.0",
|
||||
@@ -102,47 +102,49 @@
|
||||
"@types/webpack": "5.28.0",
|
||||
"@types/webpack-stream": "3.2.12",
|
||||
"@types/websocket": "1.0.4",
|
||||
"@types/ws": "7.4.7",
|
||||
"@typescript-eslint/parser": "4.31.2",
|
||||
"@vue/compiler-sfc": "3.2.13",
|
||||
"@types/ws": "8.2.0",
|
||||
"@typescript-eslint/parser": "5.0.0",
|
||||
"@vue/compiler-sfc": "3.2.20",
|
||||
"abort-controller": "3.0.0",
|
||||
"apexcharts": "3.28.3",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "4.0.4",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.992.0",
|
||||
"aws-sdk": "2.1003.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.4",
|
||||
"broadcast-channel": "4.2.0",
|
||||
"bull": "3.29.2",
|
||||
"cacheable-lookup": "6.0.1",
|
||||
"bull": "3.29.3",
|
||||
"cacheable-lookup": "6.0.3",
|
||||
"cafy": "15.2.1",
|
||||
"cbor": "8.0.0",
|
||||
"cbor": "8.0.2",
|
||||
"chalk": "4.1.2",
|
||||
"chart.js": "2.9.4",
|
||||
"chart.js": "3.5.1",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chartjs-plugin-zoom": "1.1.1",
|
||||
"cli-highlight": "2.1.11",
|
||||
"commander": "8.1.0",
|
||||
"compare-versions": "3.6.0",
|
||||
"concurrently": "6.2.1",
|
||||
"concurrently": "6.3.0",
|
||||
"content-disposition": "0.5.3",
|
||||
"crc-32": "1.2.0",
|
||||
"css-loader": "6.3.0",
|
||||
"css-loader": "6.4.0",
|
||||
"cssnano": "5.0.8",
|
||||
"date-fns": "2.25.0",
|
||||
"dateformat": "4.5.1",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-plugin-vue": "7.18.0",
|
||||
"eslint": "8.0.1",
|
||||
"eslint-plugin-vue": "7.19.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "16.5.3",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"glob": "7.1.7",
|
||||
"glob": "7.2.0",
|
||||
"got": "11.8.2",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-replace": "1.1.3",
|
||||
"gulp-terser": "2.0.1",
|
||||
"gulp-terser": "2.1.0",
|
||||
"gulp-tslint": "8.1.4",
|
||||
"hpagent": "0.1.2",
|
||||
"http-signature": "1.3.5",
|
||||
@@ -157,7 +159,7 @@
|
||||
"jsonld": "5.2.0",
|
||||
"jsrsasign": "8.0.20",
|
||||
"katex": "0.13.18",
|
||||
"koa": "2.13.1",
|
||||
"koa": "2.13.3",
|
||||
"koa-bodyparser": "4.3.0",
|
||||
"koa-favicon": "2.1.0",
|
||||
"koa-json-body": "5.3.0",
|
||||
@@ -170,22 +172,22 @@
|
||||
"markdown-it": "12.2.0",
|
||||
"markdown-it-anchor": "7.1.0",
|
||||
"matter-js": "0.17.1",
|
||||
"mfm-js": "0.19.0",
|
||||
"mfm-js": "0.20.0",
|
||||
"misskey-js": "0.0.6",
|
||||
"mocha": "8.4.0",
|
||||
"ms": "2.1.3",
|
||||
"multer": "1.4.3",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"nodemailer": "6.6.3",
|
||||
"nodemailer": "6.7.0",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "6.0.1",
|
||||
"pg": "8.7.1",
|
||||
"portscanner": "2.2.0",
|
||||
"postcss": "8.3.7",
|
||||
"postcss-loader": "6.1.1",
|
||||
"postcss": "8.3.9",
|
||||
"postcss-loader": "6.2.0",
|
||||
"prismjs": "1.25.0",
|
||||
"private-ip": "2.2.1",
|
||||
"private-ip": "2.3.0",
|
||||
"probe-image-size": "7.2.1",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
@@ -204,16 +206,17 @@
|
||||
"rimraf": "3.0.2",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.42.1",
|
||||
"sass-loader": "12.1.0",
|
||||
"sass": "1.43.2",
|
||||
"sass-loader": "12.2.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"sharp": "0.29.1",
|
||||
"speakeasy": "2.0.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"style-loader": "3.3.0",
|
||||
"summaly": "2.4.1",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "5.9.3",
|
||||
"systeminformation": "5.9.7",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.117.1",
|
||||
@@ -221,19 +224,19 @@
|
||||
"tinycolor2": "1.4.2",
|
||||
"tmp": "0.2.1",
|
||||
"ts-loader": "9.2.6",
|
||||
"ts-node": "10.2.1",
|
||||
"tsc-alias": "1.3.9",
|
||||
"ts-node": "10.3.0",
|
||||
"tsc-alias": "1.3.10",
|
||||
"tsconfig-paths": "3.11.0",
|
||||
"tslint": "6.1.3",
|
||||
"tslint-sonarts": "1.9.0",
|
||||
"twemoji-parser": "13.1.0",
|
||||
"typeorm": "0.2.37",
|
||||
"typescript": "4.4.3",
|
||||
"typeorm": "0.2.38",
|
||||
"typescript": "4.4.4",
|
||||
"ulid": "2.3.0",
|
||||
"uuid": "8.3.2",
|
||||
"v-debounce": "0.1.2",
|
||||
"vanilla-tilt": "1.7.2",
|
||||
"vue": "3.2.13",
|
||||
"vue": "3.2.20",
|
||||
"vue-loader": "16.7.0",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vue-router": "4.0.5",
|
||||
@@ -241,17 +244,17 @@
|
||||
"vue-svg-loader": "0.17.0-beta.2",
|
||||
"vuedraggable": "4.0.1",
|
||||
"web-push": "3.4.5",
|
||||
"webpack": "5.53.0",
|
||||
"webpack-cli": "4.8.0",
|
||||
"webpack": "5.58.2",
|
||||
"webpack-cli": "4.9.0",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.2.2",
|
||||
"ws": "8.2.3",
|
||||
"xev": "2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.54",
|
||||
"@types/fluent-ffmpeg": "2.1.17",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "8.4.1",
|
||||
"cypress": "8.5.0",
|
||||
"start-server-and-test": "1.14.0"
|
||||
}
|
||||
}
|
||||
|
23
src/argv.ts
23
src/argv.ts
@@ -1,23 +0,0 @@
|
||||
import { Command } from 'commander';
|
||||
import config from '@/config/index';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program.version(config.version);
|
||||
program.option('--no-daemons', 'Disable daemon processes (for debbuging)');
|
||||
program.option('--disable-clustering', 'Disable clustering');
|
||||
program.option('--only-server', 'Run server only (without job queue processing)');
|
||||
program.option('--only-queue', 'Pocessing job queue only (without server)');
|
||||
program.option('--quiet', 'Suppress all logs');
|
||||
program.option('--verbose', 'Enable all logs');
|
||||
program.option('--with-log-time', 'Include timestamp for each logs');
|
||||
program.option('--slow', 'Delay all requests (for debbuging)');
|
||||
program.option('--color', 'This option is a dummy for some external program\'s (e.g. forever) issue.');
|
||||
program.parse(process.argv);
|
||||
|
||||
if (process.env.MK_ONLY_QUEUE) program.onlyQueue = true;
|
||||
if (process.env.NODE_ENV === 'test') program.disableClustering = true;
|
||||
//if (process.env.NODE_ENV === 'test') program.quiet = true;
|
||||
if (process.env.NODE_ENV === 'test') program.noDaemons = true;
|
||||
|
||||
export { program };
|
@@ -3,7 +3,7 @@ import * as chalk from 'chalk';
|
||||
import Xev from 'xev';
|
||||
|
||||
import Logger from '@/services/logger';
|
||||
import { program } from '../argv';
|
||||
import { envOption } from '../env';
|
||||
|
||||
// for typeorm
|
||||
import 'reflect-metadata';
|
||||
@@ -20,7 +20,7 @@ const ev = new Xev();
|
||||
export default async function() {
|
||||
process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
|
||||
|
||||
if (cluster.isMaster || program.disableClustering) {
|
||||
if (cluster.isMaster || envOption.disableClustering) {
|
||||
await masterMain();
|
||||
|
||||
if (cluster.isMaster) {
|
||||
@@ -28,7 +28,7 @@ export default async function() {
|
||||
}
|
||||
}
|
||||
|
||||
if (cluster.isWorker || program.disableClustering) {
|
||||
if (cluster.isWorker || envOption.disableClustering) {
|
||||
await workerMain();
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ cluster.on('exit', worker => {
|
||||
});
|
||||
|
||||
// Display detail of unhandled promise rejection
|
||||
if (!program.quiet) {
|
||||
if (!envOption.quiet) {
|
||||
process.on('unhandledRejection', console.dir);
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,7 @@ import Logger from '@/services/logger';
|
||||
import loadConfig from '@/config/load';
|
||||
import { Config } from '@/config/types';
|
||||
import { lessThan } from '@/prelude/array';
|
||||
import { program } from '../argv';
|
||||
import { envOption } from '../env';
|
||||
import { showMachineInfo } from '@/misc/show-machine-info';
|
||||
import { initDb } from '../db/postgre';
|
||||
|
||||
@@ -25,7 +25,7 @@ const logger = new Logger('core', 'cyan');
|
||||
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
|
||||
|
||||
function greet() {
|
||||
if (!program.quiet) {
|
||||
if (!envOption.quiet) {
|
||||
//#region Misskey logo
|
||||
const v = `v${meta.version}`;
|
||||
console.log(' _____ _ _ ');
|
||||
@@ -73,13 +73,13 @@ export async function masterMain() {
|
||||
|
||||
bootLogger.succ('Misskey initialized');
|
||||
|
||||
if (!program.disableClustering) {
|
||||
if (!envOption.disableClustering) {
|
||||
await spawnWorkers(config.clusterLimit);
|
||||
}
|
||||
|
||||
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
|
||||
|
||||
if (!program.noDaemons) {
|
||||
if (!envOption.noDaemons) {
|
||||
require('../daemons/server-stats').default();
|
||||
require('../daemons/queue-stats').default();
|
||||
require('../daemons/janitor').default();
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { del, get, set } from '@client/scripts/idb-proxy';
|
||||
import { reactive } from 'vue';
|
||||
import { apiUrl } from '@client/config';
|
||||
import { waiting } from '@client/os';
|
||||
import { waiting, api, popup, popupMenu, success } from '@client/os';
|
||||
import { unisonReload, reloadChannel } from '@client/scripts/unison-reload';
|
||||
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
|
||||
import { i18n } from './i18n';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
@@ -129,6 +130,77 @@ export async function login(token: Account['token'], redirect?: string) {
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
export async function openAccountMenu(ev: MouseEvent) {
|
||||
function showSigninDialog() {
|
||||
popup(import('@client/components/signin-dialog.vue'), {}, {
|
||||
done: res => {
|
||||
addAccount(res.id, res.i);
|
||||
success();
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
function createAccount() {
|
||||
popup(import('@client/components/signup-dialog.vue'), {}, {
|
||||
done: res => {
|
||||
addAccount(res.id, res.i);
|
||||
switchAccountWithToken(res.i);
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
async function switchAccount(account: any) {
|
||||
const storedAccounts = await getAccounts();
|
||||
const token = storedAccounts.find(x => x.id === account.id).token;
|
||||
switchAccountWithToken(token);
|
||||
}
|
||||
|
||||
function switchAccountWithToken(token: string) {
|
||||
login(token);
|
||||
}
|
||||
|
||||
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
|
||||
const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) });
|
||||
|
||||
const accountItemPromises = storedAccounts.map(a => new Promise(res => {
|
||||
accountsPromise.then(accounts => {
|
||||
const account = accounts.find(x => x.id === a.id);
|
||||
if (account == null) return res(null);
|
||||
res({
|
||||
type: 'user',
|
||||
user: account,
|
||||
action: () => { switchAccount(account); }
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
popupMenu([...[{
|
||||
type: 'link',
|
||||
text: i18n.locale.profile,
|
||||
to: `/@${ $i.username }`,
|
||||
avatar: $i,
|
||||
}, null, ...accountItemPromises, {
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.locale.addAccount,
|
||||
action: () => {
|
||||
popupMenu([{
|
||||
text: i18n.locale.existingAccount,
|
||||
action: () => { showSigninDialog(); },
|
||||
}, {
|
||||
text: i18n.locale.createAccount,
|
||||
action: () => { createAccount(); },
|
||||
}], ev.currentTarget || ev.target);
|
||||
},
|
||||
}, {
|
||||
type: 'link',
|
||||
icon: 'fas fa-users',
|
||||
text: i18n.locale.manageAccounts,
|
||||
to: `/settings/accounts`,
|
||||
}]], ev.currentTarget || ev.target, {
|
||||
align: 'left'
|
||||
});
|
||||
}
|
||||
|
||||
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import XWindow from '@client/components/ui/window.vue';
|
||||
import MkTextarea from '@client/components/ui/textarea.vue';
|
||||
import MkTextarea from '@client/components/form/textarea.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import * as os from '@client/os';
|
||||
|
||||
|
@@ -10,12 +10,12 @@
|
||||
</li>
|
||||
<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $ts.selectUser }}</li>
|
||||
</ol>
|
||||
<ol class="hashtags" ref="suggests" v-if="hashtags.length > 0">
|
||||
<ol class="hashtags" ref="suggests" v-else-if="hashtags.length > 0">
|
||||
<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1">
|
||||
<span class="name">{{ hashtag }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
|
||||
<ol class="emojis" ref="suggests" v-else-if="emojis.length > 0">
|
||||
<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
|
||||
<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
|
||||
<span class="emoji" v-else-if="!$store.state.useOsNativeEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
|
||||
@@ -24,6 +24,11 @@
|
||||
<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
|
||||
</li>
|
||||
</ol>
|
||||
<ol class="mfmTags" ref="suggests" v-else-if="mfmTags.length > 0">
|
||||
<li v-for="tag in mfmTags" @click="complete(type, tag)" @keydown="onKeydown" tabindex="-1">
|
||||
<span class="tag">{{ tag }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -106,6 +111,8 @@ emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
|
||||
const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
|
||||
//#endregion
|
||||
|
||||
const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle'];
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
type: {
|
||||
@@ -137,11 +144,6 @@ export default defineComponent({
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
|
||||
showing: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
@@ -154,18 +156,11 @@ export default defineComponent({
|
||||
hashtags: [],
|
||||
emojis: [],
|
||||
items: [],
|
||||
mfmTags: [],
|
||||
select: -1,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
showing() {
|
||||
if (!this.showing) {
|
||||
this.$emit('closed');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.setPosition();
|
||||
this.items = (this.$refs.suggests as Element | undefined)?.children || [];
|
||||
@@ -236,7 +231,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
if (this.type == 'user') {
|
||||
if (this.type === 'user') {
|
||||
if (this.q == null) {
|
||||
this.users = [];
|
||||
this.fetching = false;
|
||||
@@ -262,7 +257,7 @@ export default defineComponent({
|
||||
sessionStorage.setItem(cacheKey, JSON.stringify(users));
|
||||
});
|
||||
}
|
||||
} else if (this.type == 'hashtag') {
|
||||
} else if (this.type === 'hashtag') {
|
||||
if (this.q == null || this.q == '') {
|
||||
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
|
||||
this.fetching = false;
|
||||
@@ -286,7 +281,7 @@ export default defineComponent({
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (this.type == 'emoji') {
|
||||
} else if (this.type === 'emoji') {
|
||||
if (this.q == null || this.q == '') {
|
||||
// 最近使った絵文字をサジェスト
|
||||
this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null);
|
||||
@@ -314,6 +309,13 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
this.emojis = matched;
|
||||
} else if (this.type === 'mfmTag') {
|
||||
if (this.q == null || this.q == '') {
|
||||
this.mfmTags = MFM_TAGS;
|
||||
return;
|
||||
}
|
||||
|
||||
this.mfmTags = MFM_TAGS.filter(tag => tag.startsWith(this.q));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -490,5 +492,11 @@ export default defineComponent({
|
||||
margin: 0 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .mfmTags > li {
|
||||
|
||||
.name {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -39,7 +39,7 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
@@ -116,7 +116,7 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
callback(response?: string) {
|
||||
this.$emit('update:value', typeof response == 'string' ? response : null);
|
||||
this.$emit('update:modelValue', typeof response == 'string' ? response : null);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@@ -91,7 +91,7 @@ export default defineComponent({
|
||||
width: 31px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
&:focus-visible {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
|
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>
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button class="nrvgflfu _button" @click="toggle">
|
||||
<b>{{ value ? $ts._cw.hide : $ts._cw.show }}</b>
|
||||
<span v-if="!value">{{ label }}</span>
|
||||
<b>{{ modelValue ? $ts._cw.hide : $ts._cw.show }}</b>
|
||||
<span v-if="!modelValue">{{ label }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { concat } from '../../prelude/array';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
@@ -36,7 +36,7 @@ export default defineComponent({
|
||||
length,
|
||||
|
||||
toggle() {
|
||||
this.$emit('update:value', !this.value);
|
||||
this.$emit('update:modelValue', !this.modelValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -21,39 +21,39 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
.rbusrurv {
|
||||
// 他のCSSからも参照されるので消さないように
|
||||
--formXPadding: 32px;
|
||||
--formYPadding: 32px;
|
||||
--debobigegoXPadding: 32px;
|
||||
--debobigegoYPadding: 32px;
|
||||
|
||||
--formContentHMargin: 16px;
|
||||
--debobigegoContentHMargin: 16px;
|
||||
|
||||
font-size: 95%;
|
||||
line-height: 1.3em;
|
||||
background: var(--bg);
|
||||
padding: var(--formYPadding) var(--formXPadding);
|
||||
padding: var(--debobigegoYPadding) var(--debobigegoXPadding);
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
|
||||
&:not(.wide).max-width_400px {
|
||||
--formXPadding: 0px;
|
||||
--debobigegoXPadding: 0px;
|
||||
|
||||
> ::v-deep(*) {
|
||||
._formPanel {
|
||||
._debobigegoPanel {
|
||||
border: solid 0.5px var(--divider);
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
._form_group {
|
||||
> *:not(._formNoConcat) {
|
||||
&:not(:last-child):not(._formNoConcatPrev) {
|
||||
&._formPanel, ._formPanel {
|
||||
._debobigego_group {
|
||||
> *:not(._debobigegoNoConcat) {
|
||||
&:not(:last-child):not(._debobigegoNoConcatPrev) {
|
||||
&._debobigegoPanel, ._debobigegoPanel {
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child):not(._formNoConcatNext) {
|
||||
&._formPanel, ._formPanel {
|
||||
&:not(:first-child):not(._debobigegoNoConcatNext) {
|
||||
&._debobigegoPanel, ._debobigegoPanel {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="yzpgjkxe _formItem">
|
||||
<div class="_formLabel"><slot name="label"></slot></div>
|
||||
<button class="main _button _formPanel _formClickable" :class="{ center, primary, danger }">
|
||||
<div class="yzpgjkxe _debobigegoItem">
|
||||
<div class="_debobigegoLabel"><slot name="label"></slot></div>
|
||||
<button class="main _button _debobigegoPanel _debobigegoClickable" :class="{ center, primary, danger }">
|
||||
<slot></slot>
|
||||
<div class="suffix">
|
||||
<slot name="suffix"></slot>
|
||||
@@ -10,13 +10,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="_formCaption"><slot name="desc"></slot></div>
|
||||
<div class="_debobigegoCaption"><slot name="desc"></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import './form.scss';
|
||||
import './debobigego.scss';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
@@ -1,9 +1,9 @@
|
||||
._formPanel {
|
||||
._debobigegoPanel {
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&._formClickable {
|
||||
&._debobigegoClickable {
|
||||
&:hover {
|
||||
//background: var(--panelHighlight);
|
||||
}
|
||||
@@ -15,8 +15,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
._formLabel,
|
||||
._formCaption {
|
||||
._debobigegoLabel,
|
||||
._debobigegoCaption {
|
||||
font-size: 80%;
|
||||
color: var(--fgTransparentWeak);
|
||||
|
||||
@@ -25,28 +25,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
._formLabel {
|
||||
._debobigegoLabel {
|
||||
position: sticky;
|
||||
top: var(--stickyTop, 0px);
|
||||
z-index: 2;
|
||||
margin: -8px calc(var(--formXPadding) * -1) 0 calc(var(--formXPadding) * -1);
|
||||
padding: 8px calc(var(--formContentHMargin) + var(--formXPadding)) 8px calc(var(--formContentHMargin) + var(--formXPadding));
|
||||
margin: -8px calc(var(--debobigegoXPadding) * -1) 0 calc(var(--debobigegoXPadding) * -1);
|
||||
padding: 8px calc(var(--debobigegoContentHMargin) + var(--debobigegoXPadding)) 8px calc(var(--debobigegoContentHMargin) + var(--debobigegoXPadding));
|
||||
background: var(--X17);
|
||||
-webkit-backdrop-filter: var(--blur, blur(10px));
|
||||
backdrop-filter: var(--blur, blur(10px));
|
||||
}
|
||||
|
||||
._themeChanging_ ._formLabel {
|
||||
._themeChanging_ ._debobigegoLabel {
|
||||
transition: none !important;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
._formCaption {
|
||||
padding: 8px var(--formContentHMargin) 0 var(--formContentHMargin);
|
||||
._debobigegoCaption {
|
||||
padding: 8px var(--debobigegoContentHMargin) 0 var(--debobigegoContentHMargin);
|
||||
}
|
||||
|
||||
._formItem {
|
||||
& + ._formItem {
|
||||
._debobigegoItem {
|
||||
& + ._debobigegoItem {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="vrtktovg _formItem _formNoConcat" v-size="{ max: [500] }" v-sticky-container>
|
||||
<div class="_formLabel"><slot name="label"></slot></div>
|
||||
<div class="main _form_group" ref="child">
|
||||
<div class="vrtktovg _debobigegoItem _debobigegoNoConcat" v-size="{ max: [500] }" v-sticky-container>
|
||||
<div class="_debobigegoLabel"><slot name="label"></slot></div>
|
||||
<div class="main _debobigego_group" ref="child">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="_formCaption"><slot name="caption"></slot></div>
|
||||
<div class="_debobigegoCaption"><slot name="caption"></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -20,9 +20,9 @@ export default defineComponent({
|
||||
const els = Array.from(child.value.children);
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
const el = els[i];
|
||||
if (el.classList.contains('_formNoConcat')) {
|
||||
if (els[i - 1]) els[i - 1].classList.add('_formNoConcatPrev');
|
||||
if (els[i + 1]) els[i + 1].classList.add('_formNoConcatNext');
|
||||
if (el.classList.contains('_debobigegoNoConcat')) {
|
||||
if (els[i - 1]) els[i - 1].classList.add('_debobigegoNoConcatPrev');
|
||||
if (els[i + 1]) els[i + 1].classList.add('_debobigegoNoConcatNext');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -52,21 +52,21 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
.vrtktovg {
|
||||
> .main {
|
||||
> ::v-deep(*):not(._formNoConcat) {
|
||||
&:not(._formNoConcatNext) {
|
||||
> ::v-deep(*):not(._debobigegoNoConcat) {
|
||||
&:not(._debobigegoNoConcatNext) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:not(:last-child):not(._formNoConcatPrev) {
|
||||
&._formPanel, ._formPanel {
|
||||
&:not(:last-child):not(._debobigegoNoConcatPrev) {
|
||||
&._debobigegoPanel, ._debobigegoPanel {
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child):not(._formNoConcatNext) {
|
||||
&._formPanel, ._formPanel {
|
||||
&:not(:first-child):not(._debobigegoNoConcatNext) {
|
||||
&._debobigegoPanel, ._debobigegoPanel {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="fzenkabp _formItem">
|
||||
<div class="_formPanel" :class="{ warn }">
|
||||
<div class="fzenkabp _debobigegoItem">
|
||||
<div class="_debobigegoPanel" :class="{ warn }">
|
||||
<i v-if="warn" class="fas fa-exclamation-triangle"></i>
|
||||
<i v-else class="fas fa-info-circle"></i>
|
||||
<slot></slot>
|
@@ -1,49 +1,53 @@
|
||||
<template>
|
||||
<div class="matxzzsk">
|
||||
<div class="label" @click="focus"><slot name="label"></slot></div>
|
||||
<div class="input" :class="{ inline, disabled, focused }">
|
||||
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
|
||||
<input ref="inputEl"
|
||||
:type="type"
|
||||
v-model="v"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
:placeholder="placeholder"
|
||||
:pattern="pattern"
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="spellcheck"
|
||||
:step="step"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="onKeydown($event)"
|
||||
@input="onInput"
|
||||
:list="id"
|
||||
>
|
||||
<datalist :id="id" v-if="datalist">
|
||||
<option v-for="data in datalist" :value="data"/>
|
||||
</datalist>
|
||||
<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
|
||||
<FormGroup class="_debobigegoItem">
|
||||
<template #label><slot></slot></template>
|
||||
<div class="ztzhwixg _debobigegoItem" :class="{ inline, disabled }">
|
||||
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
||||
<div class="input _debobigegoPanel">
|
||||
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
|
||||
<input ref="inputEl"
|
||||
:type="type"
|
||||
v-model="v"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
:placeholder="placeholder"
|
||||
:pattern="pattern"
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="spellcheck"
|
||||
:step="step"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="onKeydown($event)"
|
||||
@input="onInput"
|
||||
:list="id"
|
||||
>
|
||||
<datalist :id="id" v-if="datalist">
|
||||
<option v-for="data in datalist" :value="data"/>
|
||||
</datalist>
|
||||
<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption"><slot name="caption"></slot></div>
|
||||
<template #caption><slot name="desc"></slot></template>
|
||||
|
||||
<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
</div>
|
||||
<FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
</FormGroup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||
import MkButton from './button.vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import './debobigego.scss';
|
||||
import FormButton from './button.vue';
|
||||
import FormGroup from './group.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
FormGroup,
|
||||
FormButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
modelValue: {
|
||||
required: true
|
||||
required: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
@@ -92,20 +96,13 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
debounce: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
manualSave: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['change', 'keydown', 'enter', 'update:modelValue'],
|
||||
|
||||
setup(props, context) {
|
||||
const { modelValue, type, autofocus } = toRefs(props);
|
||||
const v = ref(modelValue.value);
|
||||
@@ -140,19 +137,13 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedUpdated = debounce(1000, updated);
|
||||
|
||||
watch(modelValue, newValue => {
|
||||
watch(modelValue.value, newValue => {
|
||||
v.value = newValue;
|
||||
});
|
||||
|
||||
watch(v, newValue => {
|
||||
if (!props.manualSave) {
|
||||
if (props.debounce) {
|
||||
debouncedUpdated();
|
||||
} else {
|
||||
updated();
|
||||
}
|
||||
updated();
|
||||
}
|
||||
|
||||
invalid.value = inputEl.value.validity.badInput;
|
||||
@@ -205,68 +196,59 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.matxzzsk {
|
||||
margin: 1.5em 0;
|
||||
.ztzhwixg {
|
||||
position: relative;
|
||||
|
||||
> .label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 12px;
|
||||
user-select: none;
|
||||
> .icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .caption {
|
||||
font-size: 0.8em;
|
||||
padding: 8px 0 0 12px;
|
||||
color: var(--fgTransparentWeak);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
&:not(:empty) + .input {
|
||||
margin-left: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
> .input {
|
||||
$height: 42px;
|
||||
$height: 48px;
|
||||
position: relative;
|
||||
|
||||
> input {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
height: $height;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
padding: 0 16px;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
color: var(--fg);
|
||||
background: var(--panel);
|
||||
border: solid 0.5px var(--inputBorder);
|
||||
border-radius: 6px;
|
||||
line-height: $height;
|
||||
color: var(--inputText);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.1s ease-out;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--inputBorderHover);
|
||||
&[type='file'] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .prefix,
|
||||
> .suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
padding: 0 12px;
|
||||
padding: 0 16px;
|
||||
font-size: 1em;
|
||||
height: $height;
|
||||
line-height: $height;
|
||||
color: var(--inputLabel);
|
||||
pointer-events: none;
|
||||
|
||||
&:empty {
|
||||
@@ -285,32 +267,25 @@ export default defineComponent({
|
||||
|
||||
> .prefix {
|
||||
left: 0;
|
||||
padding-right: 6px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
> .suffix {
|
||||
right: 0;
|
||||
padding-left: 6px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.inline {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
&.inline {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
> input {
|
||||
border-color: var(--accent);
|
||||
//box-shadow: 0 0 0 4px var(--focus);
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
|
||||
&, * {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
&, * {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="_formItem">
|
||||
<div class="_formPanel anocepby">
|
||||
<div class="_debobigegoItem">
|
||||
<div class="_debobigegoPanel anocepby">
|
||||
<span class="key"><slot name="key"></slot></span>
|
||||
<span class="value"><slot name="value"></slot></span>
|
||||
</div>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import './form.scss';
|
||||
import './debobigego.scss';
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
@@ -20,7 +20,7 @@ export default defineComponent({
|
||||
.anocepby {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px var(--formContentHMargin);
|
||||
padding: 14px var(--debobigegoContentHMargin);
|
||||
|
||||
> .key {
|
||||
margin-right: 12px;
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="qmfkfnzi _formItem">
|
||||
<a class="main _button _formPanel _formClickable" :href="to" target="_blank" v-if="external">
|
||||
<div class="qmfkfnzi _debobigegoItem">
|
||||
<a class="main _button _debobigegoPanel _debobigegoClickable" :href="to" target="_blank" v-if="external">
|
||||
<span class="icon"><slot name="icon"></slot></span>
|
||||
<span class="text"><slot></slot></span>
|
||||
<span class="right">
|
||||
@@ -8,7 +8,7 @@
|
||||
<i class="fas fa-external-link-alt icon"></i>
|
||||
</span>
|
||||
</a>
|
||||
<MkA class="main _button _formPanel _formClickable" :class="{ active }" :to="to" :behavior="behavior" v-else>
|
||||
<MkA class="main _button _debobigegoPanel _debobigegoClickable" :class="{ active }" :to="to" :behavior="behavior" v-else>
|
||||
<span class="icon"><slot name="icon"></slot></span>
|
||||
<span class="text"><slot></slot></span>
|
||||
<span class="right">
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import './form.scss';
|
||||
import './debobigego.scss';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<FormGroup class="_formItem">
|
||||
<FormGroup class="_debobigegoItem">
|
||||
<template #label><slot></slot></template>
|
||||
<div class="drooglns _formItem" :class="{ tall }">
|
||||
<div class="input _formPanel">
|
||||
<div class="drooglns _debobigegoItem" :class="{ tall }">
|
||||
<div class="input _debobigegoPanel">
|
||||
<textarea class="_monospace"
|
||||
v-model="v"
|
||||
readonly
|
||||
@@ -17,7 +17,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, toRefs, watch } from 'vue';
|
||||
import * as JSON5 from 'json5';
|
||||
import './form.scss';
|
||||
import './debobigego.scss';
|
||||
import FormGroup from './group.vue';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -75,7 +75,7 @@ export default defineComponent({
|
||||
max-width: 100%;
|
||||
min-height: 130px;
|
||||
margin: 0;
|
||||
padding: 16px var(--formContentHMargin);
|
||||
padding: 16px var(--debobigegoContentHMargin);
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<FormGroup class="uljviswt _formItem">
|
||||
<FormGroup class="uljviswt _debobigegoItem">
|
||||
<template #label><slot name="label"></slot></template>
|
||||
<slot :items="items"></slot>
|
||||
<div class="empty" v-if="empty" key="_empty_">
|
112
src/client/components/debobigego/radios.vue
Normal file
112
src/client/components/debobigego/radios.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, h } from 'vue';
|
||||
import MkRadio from '@client/components/form/radio.vue';
|
||||
import './debobigego.scss';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkRadio
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
required: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: this.modelValue,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue() {
|
||||
this.value = this.modelValue;
|
||||
},
|
||||
value() {
|
||||
this.$emit('update:modelValue', this.value);
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const label = this.$slots.desc();
|
||||
let options = this.$slots.default();
|
||||
|
||||
// なぜかFragmentになることがあるため
|
||||
if (options.length === 1 && options[0].props == null) options = options[0].children;
|
||||
|
||||
return h('div', {
|
||||
class: 'cnklmpwm _debobigegoItem'
|
||||
}, [
|
||||
h('div', {
|
||||
class: '_debobigegoLabel',
|
||||
}, label),
|
||||
...options.map(option => h('button', {
|
||||
class: '_button _debobigegoPanel _debobigegoClickable',
|
||||
key: option.key,
|
||||
onClick: () => this.value = option.props.value,
|
||||
}, [h('span', {
|
||||
class: ['check', { checked: this.value === option.props.value }],
|
||||
}), option.children]))
|
||||
]);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.cnklmpwm {
|
||||
> button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 14px 18px;
|
||||
text-align: left;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: none !important;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
> .check {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
background: none;
|
||||
border: 2px solid var(--inputBorder);
|
||||
border-radius: 100%;
|
||||
transition: inherit;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
border-radius: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition: .4s cubic-bezier(.25,.8,.25,1);
|
||||
}
|
||||
|
||||
&.checked {
|
||||
border-color: var(--accent);
|
||||
|
||||
&:after {
|
||||
background-color: var(--accent);
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
122
src/client/components/debobigego/range.vue
Normal file
122
src/client/components/debobigego/range.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="ifitouly _debobigegoItem" :class="{ focused, disabled }">
|
||||
<div class="_debobigegoLabel"><slot name="label"></slot></div>
|
||||
<div class="_debobigegoPanel main">
|
||||
<input
|
||||
type="range"
|
||||
ref="input"
|
||||
v-model="v"
|
||||
:disabled="disabled"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@input="$emit('update:value', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="_debobigegoCaption"><slot name="caption"></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 100
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 1
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
v: this.value,
|
||||
focused: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value(v) {
|
||||
this.v = parseFloat(v);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ifitouly {
|
||||
position: relative;
|
||||
|
||||
> .main {
|
||||
padding: 22px 16px;
|
||||
|
||||
> input {
|
||||
display: block;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--X10);
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
145
src/client/components/debobigego/select.vue
Normal file
145
src/client/components/debobigego/select.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="yrtfrpux _debobigegoItem" :class="{ disabled, inline }">
|
||||
<div class="_debobigegoLabel"><slot name="label"></slot></div>
|
||||
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
||||
<div class="input _debobigegoPanel _debobigegoClickable" @click="focus">
|
||||
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
||||
<select ref="input"
|
||||
v-model="v"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
>
|
||||
<slot></slot>
|
||||
</select>
|
||||
<div class="suffix">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_debobigegoCaption"><slot name="caption"></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import './debobigego.scss';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
required: false
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
v: {
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(v) {
|
||||
this.$emit('update:modelValue', v);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.yrtfrpux {
|
||||
position: relative;
|
||||
|
||||
> .icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
|
||||
&:not(:empty) + .input {
|
||||
margin-left: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
> .input {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
> select {
|
||||
display: block;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
height: 48px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
color: var(--fg);
|
||||
|
||||
option,
|
||||
optgroup {
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
}
|
||||
}
|
||||
|
||||
> .prefix,
|
||||
> .suffix {
|
||||
display: block;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
font-size: 1em;
|
||||
line-height: 32px;
|
||||
color: var(--inputLabel);
|
||||
pointer-events: none;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> * {
|
||||
display: block;
|
||||
min-width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
> .prefix {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
> .suffix {
|
||||
padding: 0 16px 0 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="_formItem" v-if="pending">
|
||||
<div class="_formPanel">
|
||||
<div class="_debobigegoItem" v-if="pending">
|
||||
<div class="_debobigegoPanel">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="resolved" class="_formItem">
|
||||
<div v-else-if="resolved" class="_debobigegoItem">
|
||||
<slot :result="result"></slot>
|
||||
</div>
|
||||
<div class="_formItem" v-else>
|
||||
<div class="_formPanel eiurkvay">
|
||||
<div class="_debobigegoItem" v-else>
|
||||
<div class="_debobigegoPanel eiurkvay">
|
||||
<div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div>
|
||||
<MkButton inline @click="retry" class="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton>
|
||||
</div>
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, watch } from 'vue';
|
||||
import './form.scss';
|
||||
import './debobigego.scss';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
|
||||
export default defineComponent({
|
132
src/client/components/debobigego/switch.vue
Normal file
132
src/client/components/debobigego/switch.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="ijnpvmgr _debobigegoItem">
|
||||
<div class="main _debobigegoPanel _debobigegoClickable"
|
||||
:class="{ disabled, checked }"
|
||||
:aria-checked="checked"
|
||||
:aria-disabled="disabled"
|
||||
@click.prevent="toggle"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
ref="input"
|
||||
:disabled="disabled"
|
||||
@keydown.enter="toggle"
|
||||
>
|
||||
<span class="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff">
|
||||
<span class="handle"></span>
|
||||
</span>
|
||||
<span class="label">
|
||||
<span><slot></slot></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="_debobigegoCaption"><slot name="desc"></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import './debobigego.scss';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checked(): boolean {
|
||||
return this.modelValue;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
if (this.disabled) return;
|
||||
this.$emit('update:modelValue', !this.checked);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ijnpvmgr {
|
||||
> .main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
|
||||
> * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> .button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
width: 34px;
|
||||
height: 22px;
|
||||
background: var(--switchBg);
|
||||
outline: none;
|
||||
border-radius: 999px;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
|
||||
> .handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 3px;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border-radius: 100%;
|
||||
transition: background-color 0.3s, transform 0.3s;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .label {
|
||||
margin-left: 12px;
|
||||
display: block;
|
||||
transition: inherit;
|
||||
color: var(--fg);
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
transition: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
> .button {
|
||||
background-color: var(--accent);
|
||||
|
||||
> .handle {
|
||||
transform: translateX(12px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
161
src/client/components/debobigego/textarea.vue
Normal file
161
src/client/components/debobigego/textarea.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<FormGroup class="_debobigegoItem">
|
||||
<template #label><slot></slot></template>
|
||||
<div class="rivhosbp _debobigegoItem" :class="{ tall, pre }">
|
||||
<div class="input _debobigegoPanel">
|
||||
<textarea ref="input" :class="{ code, _monospace: code }"
|
||||
v-model="v"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
:pattern="pattern"
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="!code"
|
||||
@input="onInput"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<template #caption><slot name="desc"></slot></template>
|
||||
|
||||
<FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
</FormGroup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, toRefs, watch } from 'vue';
|
||||
import './debobigego.scss';
|
||||
import FormButton from './button.vue';
|
||||
import FormGroup from './group.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormGroup,
|
||||
FormButton,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
required: false
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
pattern: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
code: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
tall: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
pre: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
manualSave: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { modelValue } = toRefs(props);
|
||||
const v = ref(modelValue.value);
|
||||
const changed = ref(false);
|
||||
const inputEl = ref(null);
|
||||
const focus = () => inputEl.value.focus();
|
||||
const onInput = (ev) => {
|
||||
changed.value = true;
|
||||
context.emit('change', ev);
|
||||
};
|
||||
|
||||
const updated = () => {
|
||||
changed.value = false;
|
||||
context.emit('update:modelValue', v.value);
|
||||
};
|
||||
|
||||
watch(modelValue.value, newValue => {
|
||||
v.value = newValue;
|
||||
});
|
||||
|
||||
watch(v, newValue => {
|
||||
if (!props.manualSave) {
|
||||
updated();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
v,
|
||||
updated,
|
||||
changed,
|
||||
focus,
|
||||
onInput,
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rivhosbp {
|
||||
position: relative;
|
||||
|
||||
> .input {
|
||||
position: relative;
|
||||
|
||||
> textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 130px;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
color: var(--fg);
|
||||
|
||||
&.code {
|
||||
tab-size: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tall {
|
||||
> .input {
|
||||
> textarea {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.pre {
|
||||
> .input {
|
||||
> textarea {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="wthhikgt _formItem" v-size="{ max: [500] }">
|
||||
<div class="wthhikgt _debobigegoItem" v-size="{ max: [500] }">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
@@ -40,8 +40,8 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import MkModal from '@client/components/ui/modal.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import MkInput from '@client/components/ui/input.vue';
|
||||
import MkSelect from '@client/components/ui/select.vue';
|
||||
import MkInput from '@client/components/form/input.vue';
|
||||
import MkSelect from '@client/components/form/select.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@@ -153,7 +153,7 @@ export default defineComponent({
|
||||
height: var(--eachSize);
|
||||
border-radius: 4px;
|
||||
|
||||
&:focus {
|
||||
&:focus-visible {
|
||||
outline: solid 2px var(--focus);
|
||||
z-index: 1;
|
||||
}
|
||||
|
@@ -465,7 +465,7 @@ export default defineComponent({
|
||||
height: var(--eachSize);
|
||||
border-radius: 4px;
|
||||
|
||||
&:focus {
|
||||
&:focus-visible {
|
||||
outline: solid 2px var(--focus);
|
||||
z-index: 1;
|
||||
}
|
||||
|
@@ -161,7 +161,7 @@ export default defineComponent({
|
||||
width: 31px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
&:focus-visible {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
|
@@ -35,7 +35,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import XModalWindow from '@client/components/ui/modal-window.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import MkInput from '@client/components/ui/input.vue';
|
||||
import MkInput from '@client/components/form/input.vue';
|
||||
import * as os from '@client/os';
|
||||
|
||||
export default defineComponent({
|
||||
|
@@ -14,23 +14,23 @@
|
||||
</template>
|
||||
<FormBase class="xkpnjxcv">
|
||||
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
|
||||
<FormInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1">
|
||||
<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
|
||||
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
|
||||
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text">
|
||||
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
|
||||
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
|
||||
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
|
||||
</FormInput>
|
||||
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]">
|
||||
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
|
||||
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
|
||||
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
|
||||
</FormTextarea>
|
||||
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">
|
||||
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
|
||||
<span v-text="form[item].label || item"></span>
|
||||
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
|
||||
</FormSwitch>
|
||||
<FormSelect v-else-if="form[item].type === 'enum'" v-model:value="values[item]">
|
||||
<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<option v-for="item in form[item].enum" :value="item.value" :key="item.value">{{ item.label }}</option>
|
||||
</FormSelect>
|
||||
@@ -38,7 +38,7 @@
|
||||
<template #desc><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<option v-for="item in form[item].options" :value="item.value" :key="item.value">{{ item.label }}</option>
|
||||
</FormRadios>
|
||||
<FormRange v-else-if="form[item].type === 'range'" v-model:value="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step">
|
||||
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
|
||||
</FormRange>
|
||||
@@ -53,14 +53,14 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import XModalWindow from '@client/components/ui/modal-window.vue';
|
||||
import FormBase from './form/base.vue';
|
||||
import FormInput from './form/input.vue';
|
||||
import FormTextarea from './form/textarea.vue';
|
||||
import FormSwitch from './form/switch.vue';
|
||||
import FormSelect from './form/select.vue';
|
||||
import FormRange from './form/range.vue';
|
||||
import FormButton from './form/button.vue';
|
||||
import FormRadios from './form/radios.vue';
|
||||
import FormBase from './debobigego/base.vue';
|
||||
import FormInput from './debobigego/input.vue';
|
||||
import FormTextarea from './debobigego/textarea.vue';
|
||||
import FormSwitch from './debobigego/switch.vue';
|
||||
import FormSelect from './debobigego/select.vue';
|
||||
import FormRange from './debobigego/range.vue';
|
||||
import FormButton from './debobigego/button.vue';
|
||||
import FormRadios from './debobigego/radios.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@@ -1,53 +1,49 @@
|
||||
<template>
|
||||
<FormGroup class="_formItem">
|
||||
<template #label><slot></slot></template>
|
||||
<div class="ztzhwixg _formItem" :class="{ inline, disabled }">
|
||||
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
||||
<div class="input _formPanel">
|
||||
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
|
||||
<input ref="inputEl"
|
||||
:type="type"
|
||||
v-model="v"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
:placeholder="placeholder"
|
||||
:pattern="pattern"
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="spellcheck"
|
||||
:step="step"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="onKeydown($event)"
|
||||
@input="onInput"
|
||||
:list="id"
|
||||
>
|
||||
<datalist :id="id" v-if="datalist">
|
||||
<option v-for="data in datalist" :value="data"/>
|
||||
</datalist>
|
||||
<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
|
||||
</div>
|
||||
<div class="matxzzsk">
|
||||
<div class="label" @click="focus"><slot name="label"></slot></div>
|
||||
<div class="input" :class="{ inline, disabled, focused }">
|
||||
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
|
||||
<input ref="inputEl"
|
||||
:type="type"
|
||||
v-model="v"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
:placeholder="placeholder"
|
||||
:pattern="pattern"
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="spellcheck"
|
||||
:step="step"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="onKeydown($event)"
|
||||
@input="onInput"
|
||||
:list="id"
|
||||
>
|
||||
<datalist :id="id" v-if="datalist">
|
||||
<option v-for="data in datalist" :value="data"/>
|
||||
</datalist>
|
||||
<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
|
||||
</div>
|
||||
<template #caption><slot name="desc"></slot></template>
|
||||
<div class="caption"><slot name="caption"></slot></div>
|
||||
|
||||
<FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
</FormGroup>
|
||||
<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||
import './form.scss';
|
||||
import FormButton from './button.vue';
|
||||
import FormGroup from './group.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormGroup,
|
||||
FormButton,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: false
|
||||
modelValue: {
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
@@ -96,16 +92,23 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
debounce: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
manualSave: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
emits: ['change', 'keydown', 'enter'],
|
||||
|
||||
emits: ['change', 'keydown', 'enter', 'update:modelValue'],
|
||||
|
||||
setup(props, context) {
|
||||
const { value, type, autofocus } = toRefs(props);
|
||||
const v = ref(value.value);
|
||||
const { modelValue, type, autofocus } = toRefs(props);
|
||||
const v = ref(modelValue.value);
|
||||
const id = Math.random().toString(); // TODO: uuid?
|
||||
const focused = ref(false);
|
||||
const changed = ref(false);
|
||||
@@ -131,19 +134,25 @@ export default defineComponent({
|
||||
const updated = () => {
|
||||
changed.value = false;
|
||||
if (type?.value === 'number') {
|
||||
context.emit('update:value', parseFloat(v.value));
|
||||
context.emit('update:modelValue', parseFloat(v.value));
|
||||
} else {
|
||||
context.emit('update:value', v.value);
|
||||
context.emit('update:modelValue', v.value);
|
||||
}
|
||||
};
|
||||
|
||||
watch(value, newValue => {
|
||||
const debouncedUpdated = debounce(1000, updated);
|
||||
|
||||
watch(modelValue, newValue => {
|
||||
v.value = newValue;
|
||||
});
|
||||
|
||||
watch(v, newValue => {
|
||||
if (!props.manualSave) {
|
||||
updated();
|
||||
if (props.debounce) {
|
||||
debouncedUpdated();
|
||||
} else {
|
||||
updated();
|
||||
}
|
||||
}
|
||||
|
||||
invalid.value = inputEl.value.validity.badInput;
|
||||
@@ -196,59 +205,66 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ztzhwixg {
|
||||
position: relative;
|
||||
.matxzzsk {
|
||||
> .label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 12px;
|
||||
user-select: none;
|
||||
|
||||
> .icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:empty) + .input {
|
||||
margin-left: 28px;
|
||||
> .caption {
|
||||
font-size: 0.8em;
|
||||
padding: 8px 0 0 12px;
|
||||
color: var(--fgTransparentWeak);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .input {
|
||||
$height: 48px;
|
||||
$height: 42px;
|
||||
position: relative;
|
||||
|
||||
> input {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
height: $height;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
line-height: $height;
|
||||
color: var(--inputText);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: var(--fg);
|
||||
background: var(--panel);
|
||||
border: solid 0.5px var(--inputBorder);
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.1s ease-out;
|
||||
|
||||
&[type='file'] {
|
||||
display: none;
|
||||
&:hover {
|
||||
border-color: var(--inputBorderHover);
|
||||
}
|
||||
}
|
||||
|
||||
> .prefix,
|
||||
> .suffix {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
font-size: 1em;
|
||||
line-height: $height;
|
||||
color: var(--inputLabel);
|
||||
height: $height;
|
||||
pointer-events: none;
|
||||
|
||||
&:empty {
|
||||
@@ -267,25 +283,32 @@ export default defineComponent({
|
||||
|
||||
> .prefix {
|
||||
left: 0;
|
||||
padding-right: 8px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
> .suffix {
|
||||
right: 0;
|
||||
padding-left: 8px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&.inline {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
&.inline {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
&.focused {
|
||||
> input {
|
||||
border-color: var(--accent);
|
||||
//box-shadow: 0 0 0 4px var(--focus);
|
||||
}
|
||||
}
|
||||
|
||||
&, * {
|
||||
cursor: not-allowed !important;
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
|
||||
&, * {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, h } from 'vue';
|
||||
import MkRadio from '@client/components/ui/radio.vue';
|
||||
import './form.scss';
|
||||
import MkRadio from './radio.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -18,95 +17,38 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue() {
|
||||
this.value = this.modelValue;
|
||||
},
|
||||
value() {
|
||||
this.$emit('update:modelValue', this.value);
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const label = this.$slots.desc();
|
||||
let options = this.$slots.default();
|
||||
|
||||
// なぜかFragmentになることがあるため
|
||||
if (options.length === 1 && options[0].props == null) options = options[0].children;
|
||||
|
||||
return h('div', {
|
||||
class: 'cnklmpwm _formItem'
|
||||
class: 'novjtcto'
|
||||
}, [
|
||||
h('div', {
|
||||
class: '_formLabel',
|
||||
}, label),
|
||||
...options.map(option => h('button', {
|
||||
class: '_button _formPanel _formClickable',
|
||||
...options.map(option => h(MkRadio, {
|
||||
key: option.key,
|
||||
onClick: () => this.value = option.props.value,
|
||||
}, [h('span', {
|
||||
class: ['check', { checked: this.value === option.props.value }],
|
||||
}), option.children]))
|
||||
value: option.props.value,
|
||||
modelValue: this.value,
|
||||
'onUpdate:modelValue': value => this.value = value,
|
||||
}, option.children))
|
||||
]);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.cnklmpwm {
|
||||
> button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 14px 18px;
|
||||
text-align: left;
|
||||
.novjtcto {
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: none !important;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
> .check {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
background: none;
|
||||
border: 2px solid var(--inputBorder);
|
||||
border-radius: 100%;
|
||||
transition: inherit;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
border-radius: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition: .4s cubic-bezier(.25,.8,.25,1);
|
||||
}
|
||||
|
||||
&.checked {
|
||||
border-color: var(--accent);
|
||||
|
||||
&:after {
|
||||
background-color: var(--accent);
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,21 +1,20 @@
|
||||
<template>
|
||||
<div class="ifitouly _formItem" :class="{ focused, disabled }">
|
||||
<div class="_formLabel"><slot name="label"></slot></div>
|
||||
<div class="_formPanel main">
|
||||
<input
|
||||
type="range"
|
||||
ref="input"
|
||||
v-model="v"
|
||||
:disabled="disabled"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@input="$emit('update:value', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="_formCaption"><slot name="caption"></slot></div>
|
||||
<div class="timctyfi" :class="{ focused, disabled }">
|
||||
<div class="icon"><slot name="icon"></slot></div>
|
||||
<span class="label"><slot name="label"></slot></span>
|
||||
<input
|
||||
type="range"
|
||||
ref="input"
|
||||
v-model="v"
|
||||
:disabled="disabled"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
:autofocus="autofocus"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@input="$emit('update:value', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -49,6 +48,10 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: 1
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -61,61 +64,75 @@ export default defineComponent({
|
||||
this.v = parseFloat(v);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.autofocus) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.input.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ifitouly {
|
||||
.timctyfi {
|
||||
position: relative;
|
||||
margin: 8px;
|
||||
|
||||
> .main {
|
||||
padding: 22px 16px;
|
||||
> .icon {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> input {
|
||||
display: block;
|
||||
> .title {
|
||||
pointer-events: none;
|
||||
font-size: 16px;
|
||||
color: var(--inputLabel);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> input {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--X10);
|
||||
height: 7px;
|
||||
margin: 0 8px;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--X10);
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
31
src/client/components/form/section.vue
Normal file
31
src/client/components/form/section.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="vrtktovh" v-size="{ max: [500] }" v-sticky-container>
|
||||
<div class="label"><slot name="label"></slot></div>
|
||||
<div class="main">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vrtktovh {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> .label {
|
||||
font-weight: bold;
|
||||
padding: 24px 0 16px 0;
|
||||
}
|
||||
|
||||
> .main {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,125 +1,265 @@
|
||||
<template>
|
||||
<div class="yrtfrpux _formItem" :class="{ disabled, inline }">
|
||||
<div class="_formLabel"><slot name="label"></slot></div>
|
||||
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
||||
<div class="input _formPanel _formClickable" @click="focus">
|
||||
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
||||
<select ref="input"
|
||||
<div class="vblkjoeq">
|
||||
<div class="label" @click="focus"><slot name="label"></slot></div>
|
||||
<div class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick" ref="container">
|
||||
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
|
||||
<select class="select" ref="inputEl"
|
||||
v-model="v"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
:placeholder="placeholder"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@input="onInput"
|
||||
>
|
||||
<slot></slot>
|
||||
</select>
|
||||
<div class="suffix">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="suffix" ref="suffixEl"><i class="fas fa-chevron-down"></i></div>
|
||||
</div>
|
||||
<div class="_formCaption"><slot name="caption"></slot></div>
|
||||
<div class="caption"><slot name="caption"></slot></div>
|
||||
|
||||
<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import './form.scss';
|
||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import * as os from '@client/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: false
|
||||
modelValue: {
|
||||
required: true
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
v: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(v) {
|
||||
this.$emit('update:value', v);
|
||||
}
|
||||
manualSave: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
emits: ['change', 'update:modelValue'],
|
||||
|
||||
setup(props, context) {
|
||||
const { modelValue, autofocus } = toRefs(props);
|
||||
const v = ref(modelValue.value);
|
||||
const focused = ref(false);
|
||||
const changed = ref(false);
|
||||
const invalid = ref(false);
|
||||
const filled = computed(() => v.value !== '' && v.value != null);
|
||||
const inputEl = ref(null);
|
||||
const prefixEl = ref(null);
|
||||
const suffixEl = ref(null);
|
||||
const container = ref(null);
|
||||
|
||||
const focus = () => inputEl.value.focus();
|
||||
const onInput = (ev) => {
|
||||
changed.value = true;
|
||||
context.emit('change', ev);
|
||||
};
|
||||
|
||||
const updated = () => {
|
||||
changed.value = false;
|
||||
context.emit('update:modelValue', v.value);
|
||||
};
|
||||
|
||||
watch(modelValue, newValue => {
|
||||
v.value = newValue;
|
||||
});
|
||||
|
||||
watch(v, newValue => {
|
||||
if (!props.manualSave) {
|
||||
updated();
|
||||
}
|
||||
|
||||
invalid.value = inputEl.value.validity.badInput;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (autofocus.value) {
|
||||
focus();
|
||||
}
|
||||
|
||||
// このコンポーネントが作成された時、非表示状態である場合がある
|
||||
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
|
||||
const clock = setInterval(() => {
|
||||
if (prefixEl.value) {
|
||||
if (prefixEl.value.offsetWidth) {
|
||||
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
|
||||
}
|
||||
}
|
||||
if (suffixEl.value) {
|
||||
if (suffixEl.value.offsetWidth) {
|
||||
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(clock);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const onClick = (ev: MouseEvent) => {
|
||||
focused.value = true;
|
||||
|
||||
const menu = [];
|
||||
let options = context.slots.default();
|
||||
|
||||
for (const optionOrOptgroup of options) {
|
||||
if (optionOrOptgroup.type === 'optgroup') {
|
||||
const optgroup = optionOrOptgroup;
|
||||
menu.push({
|
||||
type: 'label',
|
||||
text: optgroup.props.label,
|
||||
});
|
||||
for (const option of optgroup.children) {
|
||||
menu.push({
|
||||
text: option.children,
|
||||
active: v.value === option.props.value,
|
||||
action: () => {
|
||||
v.value = option.props.value;
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const option = optionOrOptgroup;
|
||||
menu.push({
|
||||
text: option.children,
|
||||
active: v.value === option.props.value,
|
||||
action: () => {
|
||||
v.value = option.props.value;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
os.popupMenu(menu, container.value, {
|
||||
width: container.value.offsetWidth,
|
||||
}).then(() => {
|
||||
focused.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
v,
|
||||
focused,
|
||||
invalid,
|
||||
changed,
|
||||
filled,
|
||||
inputEl,
|
||||
prefixEl,
|
||||
suffixEl,
|
||||
container,
|
||||
focus,
|
||||
onInput,
|
||||
onClick,
|
||||
updated,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.yrtfrpux {
|
||||
position: relative;
|
||||
.vblkjoeq {
|
||||
> .label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 12px;
|
||||
user-select: none;
|
||||
|
||||
> .icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:empty) + .input {
|
||||
margin-left: 28px;
|
||||
> .caption {
|
||||
font-size: 0.8em;
|
||||
padding: 8px 0 0 12px;
|
||||
color: var(--fgTransparentWeak);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .input {
|
||||
display: flex;
|
||||
$height: 42px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
> select {
|
||||
&:hover {
|
||||
> .select {
|
||||
border-color: var(--inputBorderHover);
|
||||
}
|
||||
}
|
||||
|
||||
> .select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
flex: 1;
|
||||
height: $height;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
height: 48px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: var(--fg);
|
||||
background: var(--panel);
|
||||
border: solid 1px var(--inputBorder);
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
color: var(--fg);
|
||||
|
||||
option,
|
||||
optgroup {
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
}
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.1s ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .prefix,
|
||||
> .suffix {
|
||||
display: block;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
padding: 0 12px;
|
||||
font-size: 1em;
|
||||
line-height: 32px;
|
||||
color: var(--inputLabel);
|
||||
height: $height;
|
||||
pointer-events: none;
|
||||
|
||||
&:empty {
|
||||
@@ -127,18 +267,42 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
> * {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
min-width: 16px;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
> .prefix {
|
||||
padding-right: 4px;
|
||||
left: 0;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
> .suffix {
|
||||
padding: 0 16px 0 0;
|
||||
right: 0;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
&.inline {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
> select {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
|
||||
&, * {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
50
src/client/components/form/slot.vue
Normal file
50
src/client/components/form/slot.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="adhpbeou">
|
||||
<div class="label" @click="focus"><slot name="label"></slot></div>
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="caption"><slot name="caption"></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.adhpbeou {
|
||||
margin: 1.5em 0;
|
||||
|
||||
> .label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 12px;
|
||||
user-select: none;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .caption {
|
||||
font-size: 0.8em;
|
||||
padding: 8px 0 0 12px;
|
||||
color: var(--fgTransparentWeak);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
position: relative;
|
||||
background: var(--panel);
|
||||
border: solid 0.5px var(--inputBorder);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,35 +1,34 @@
|
||||
<template>
|
||||
<div class="ijnpvmgr _formItem">
|
||||
<div class="main _formPanel _formClickable"
|
||||
:class="{ disabled, checked }"
|
||||
:aria-checked="checked"
|
||||
:aria-disabled="disabled"
|
||||
@click.prevent="toggle"
|
||||
<div
|
||||
class="ziffeoms"
|
||||
:class="{ disabled, checked }"
|
||||
role="switch"
|
||||
:aria-checked="checked"
|
||||
:aria-disabled="disabled"
|
||||
@click.prevent="toggle"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
ref="input"
|
||||
:disabled="disabled"
|
||||
@keydown.enter="toggle"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
ref="input"
|
||||
:disabled="disabled"
|
||||
@keydown.enter="toggle"
|
||||
>
|
||||
<span class="button">
|
||||
<span></span>
|
||||
</span>
|
||||
<span class="label">
|
||||
<span><slot></slot></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="_formCaption"><slot name="desc"></slot></div>
|
||||
<span class="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff">
|
||||
<span class="handle"></span>
|
||||
</span>
|
||||
<span class="label">
|
||||
<span><slot></slot></span>
|
||||
<p><slot name="caption"></slot></p>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import './form.scss';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
@@ -40,91 +39,110 @@ export default defineComponent({
|
||||
},
|
||||
computed: {
|
||||
checked(): boolean {
|
||||
return this.value;
|
||||
return this.modelValue;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
if (this.disabled) return;
|
||||
this.$emit('update:value', !this.checked);
|
||||
this.$emit('update:modelValue', !this.checked);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ijnpvmgr {
|
||||
> .main {
|
||||
.ziffeoms {
|
||||
position: relative;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> .button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
width: 36px;
|
||||
height: 26px;
|
||||
background: var(--switchBg);
|
||||
outline: none;
|
||||
border-radius: 999px;
|
||||
transition: inherit;
|
||||
|
||||
> * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
> .button {
|
||||
background-color: var(--X10);
|
||||
border-color: var(--X10);
|
||||
|
||||
> * {
|
||||
background-color: var(--accent);
|
||||
transform: translateX(14px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> input {
|
||||
> .handle {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 5px;
|
||||
margin: auto 0;
|
||||
border-radius: 100%;
|
||||
transition: background-color 0.3s, transform 0.3s;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
> .button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
margin: 3px 0 0 0;
|
||||
width: 34px;
|
||||
height: 14px;
|
||||
background: var(--X6);
|
||||
outline: none;
|
||||
border-radius: 14px;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
> .label {
|
||||
margin-left: 16px;
|
||||
margin-top: 2px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
transition: inherit;
|
||||
color: var(--fg);
|
||||
|
||||
> * {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: 0;
|
||||
border-radius: 100%;
|
||||
transition: background-color 0.3s, transform 0.3s;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
> .label {
|
||||
margin-left: 12px;
|
||||
> span {
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
transition: inherit;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
transition: inherit;
|
||||
> p {
|
||||
margin: 0;
|
||||
color: var(--fgTransparentWeak);
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> .button {
|
||||
background-color: var(--accentedBg);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
> .button {
|
||||
background-color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
|
||||
> .handle {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,40 +1,45 @@
|
||||
<template>
|
||||
<FormGroup class="_formItem">
|
||||
<template #label><slot></slot></template>
|
||||
<div class="rivhosbp _formItem" :class="{ tall, pre }">
|
||||
<div class="input _formPanel">
|
||||
<textarea ref="input" :class="{ code, _monospace: code }"
|
||||
v-model="v"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
:pattern="pattern"
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="!code"
|
||||
@input="onInput"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="adhpbeos">
|
||||
<div class="label" @click="focus"><slot name="label"></slot></div>
|
||||
<div class="input" :class="{ disabled, focused, tall, pre }">
|
||||
<textarea ref="inputEl"
|
||||
:class="{ code, _monospace: code }"
|
||||
v-model="v"
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:readonly="readonly"
|
||||
:placeholder="placeholder"
|
||||
:pattern="pattern"
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="spellcheck"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="onKeydown($event)"
|
||||
@input="onInput"
|
||||
></textarea>
|
||||
</div>
|
||||
<template #caption><slot name="desc"></slot></template>
|
||||
<div class="caption"><slot name="caption"></slot></div>
|
||||
|
||||
<FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
</FormGroup>
|
||||
<MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, toRefs, watch } from 'vue';
|
||||
import './form.scss';
|
||||
import FormButton from './button.vue';
|
||||
import FormGroup from './group.vue';
|
||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormGroup,
|
||||
FormButton,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
required: {
|
||||
@@ -45,14 +50,29 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
pattern: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
autocomplete: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
autocomplete: {
|
||||
required: false
|
||||
},
|
||||
spellcheck: {
|
||||
required: false
|
||||
},
|
||||
code: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
@@ -67,91 +87,162 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
debounce: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
manualSave: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['change', 'keydown', 'enter', 'update:modelValue'],
|
||||
|
||||
setup(props, context) {
|
||||
const { value } = toRefs(props);
|
||||
const v = ref(value.value);
|
||||
const { modelValue, autofocus } = toRefs(props);
|
||||
const v = ref(modelValue.value);
|
||||
const focused = ref(false);
|
||||
const changed = ref(false);
|
||||
const invalid = ref(false);
|
||||
const filled = computed(() => v.value !== '' && v.value != null);
|
||||
const inputEl = ref(null);
|
||||
|
||||
const focus = () => inputEl.value.focus();
|
||||
const onInput = (ev) => {
|
||||
changed.value = true;
|
||||
context.emit('change', ev);
|
||||
};
|
||||
const onKeydown = (ev: KeyboardEvent) => {
|
||||
context.emit('keydown', ev);
|
||||
|
||||
if (ev.code === 'Enter') {
|
||||
context.emit('enter');
|
||||
}
|
||||
};
|
||||
|
||||
const updated = () => {
|
||||
changed.value = false;
|
||||
context.emit('update:value', v.value);
|
||||
context.emit('update:modelValue', v.value);
|
||||
};
|
||||
|
||||
watch(value, newValue => {
|
||||
const debouncedUpdated = debounce(1000, updated);
|
||||
|
||||
watch(modelValue, newValue => {
|
||||
v.value = newValue;
|
||||
});
|
||||
|
||||
watch(v, newValue => {
|
||||
if (!props.manualSave) {
|
||||
updated();
|
||||
if (props.debounce) {
|
||||
debouncedUpdated();
|
||||
} else {
|
||||
updated();
|
||||
}
|
||||
}
|
||||
|
||||
invalid.value = inputEl.value.validity.badInput;
|
||||
});
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (autofocus.value) {
|
||||
focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
v,
|
||||
updated,
|
||||
focused,
|
||||
invalid,
|
||||
changed,
|
||||
filled,
|
||||
inputEl,
|
||||
focus,
|
||||
onInput,
|
||||
onKeydown,
|
||||
updated,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rivhosbp {
|
||||
position: relative;
|
||||
.adhpbeos {
|
||||
> .label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 12px;
|
||||
user-select: none;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .caption {
|
||||
font-size: 0.8em;
|
||||
padding: 8px 0 0 12px;
|
||||
color: var(--fgTransparentWeak);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .input {
|
||||
position: relative;
|
||||
|
||||
|
||||
> textarea {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 130px;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
font-size: 1em;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: var(--fg);
|
||||
background: var(--panel);
|
||||
border: solid 0.5px var(--inputBorder);
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
color: var(--fg);
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.1s ease-out;
|
||||
|
||||
&.code {
|
||||
tab-size: 2;
|
||||
&:hover {
|
||||
border-color: var(--inputBorderHover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tall {
|
||||
> .input {
|
||||
&.focused {
|
||||
> textarea {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
|
||||
&, * {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.tall {
|
||||
> textarea {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.pre {
|
||||
> .input {
|
||||
&.pre {
|
||||
> textarea {
|
||||
white-space: pre;
|
||||
}
|
||||
|
@@ -27,8 +27,7 @@ export default defineComponent({
|
||||
default: false
|
||||
},
|
||||
customEmojis: {
|
||||
required: false,
|
||||
default: () => []
|
||||
required: false
|
||||
},
|
||||
isReaction: {
|
||||
type: Boolean,
|
||||
@@ -58,10 +57,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
ce() {
|
||||
let ce = [];
|
||||
if (this.customEmojis) ce = ce.concat(this.customEmojis);
|
||||
if (this.$instance && this.$instance.emojis) ce = ce.concat(this.$instance.emojis);
|
||||
return ce;
|
||||
return this.customEmojis || this.$instance?.emojis || [];
|
||||
}
|
||||
},
|
||||
|
||||
|
365
src/client/components/global/header.vue
Normal file
365
src/client/components/global/header.vue
Normal file
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<div class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick" ref="el">
|
||||
<template v-if="info">
|
||||
<div class="titleContainer" @click="showTabsPopup" v-if="!hideTitle">
|
||||
<i v-if="info.icon" class="icon" :class="info.icon"></i>
|
||||
<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
|
||||
|
||||
<div class="title">
|
||||
<MkUserName v-if="info.userName" :user="info.userName" :nowrap="false" class="title"/>
|
||||
<div v-else-if="info.title" class="title">{{ info.title }}</div>
|
||||
<div class="subtitle" v-if="!narrow && info.subtitle">
|
||||
{{ info.subtitle }}
|
||||
</div>
|
||||
<div class="subtitle activeTab" v-if="narrow && hasTabs">
|
||||
{{ info.tabs.find(tab => tab.active)?.title }}
|
||||
<i class="chevron fas fa-chevron-down"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabs" v-if="!narrow || hideTitle">
|
||||
<button class="tab _button" v-for="tab in info.tabs" :class="{ active: tab.active }" @click="tab.onClick" v-tooltip="tab.title">
|
||||
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
|
||||
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="buttons right">
|
||||
<template v-if="info && info.actions && !narrow">
|
||||
<template v-for="action in info.actions">
|
||||
<MkButton class="fullButton" v-if="action.asFullButton" @click.stop="action.handler" primary><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
|
||||
<button v-else class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag" v-tooltip="action.text"><i :class="action.icon"></i></button>
|
||||
</template>
|
||||
</template>
|
||||
<button v-if="shouldShowMenu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag" v-tooltip="$ts.menu"><i class="fas fa-ellipsis-h"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, inject } from 'vue';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import { popupMenu } from '@client/os';
|
||||
import { url } from '@client/config';
|
||||
import { scrollToTop } from '@client/scripts/scroll';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import { i18n } from '@client/i18n';
|
||||
import { globalEvents } from '@client/events';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton
|
||||
},
|
||||
|
||||
props: {
|
||||
info: {
|
||||
type: Object as PropType<{
|
||||
actions?: {}[];
|
||||
tabs?: {}[];
|
||||
}>,
|
||||
required: true
|
||||
},
|
||||
menu: {
|
||||
required: false
|
||||
},
|
||||
thin: {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const el = ref<HTMLElement>(null);
|
||||
const bg = ref(null);
|
||||
const narrow = ref(false);
|
||||
const height = ref(0);
|
||||
const hasTabs = computed(() => {
|
||||
return props.info.tabs && props.info.tabs.length > 0;
|
||||
});
|
||||
const shouldShowMenu = computed(() => {
|
||||
if (props.info == null) return false;
|
||||
if (props.info.actions != null && narrow.value) return true;
|
||||
if (props.info.menu != null) return true;
|
||||
if (props.info.share != null) return true;
|
||||
if (props.menu != null) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
const share = () => {
|
||||
navigator.share({
|
||||
url: url + props.info.path,
|
||||
...props.info.share,
|
||||
});
|
||||
};
|
||||
|
||||
const showMenu = (ev: MouseEvent) => {
|
||||
let menu = props.info.menu ? props.info.menu() : [];
|
||||
if (narrow.value && props.info.actions) {
|
||||
menu = [...props.info.actions.map(x => ({
|
||||
text: x.text,
|
||||
icon: x.icon,
|
||||
action: x.handler
|
||||
})), menu.length > 0 ? null : undefined, ...menu];
|
||||
}
|
||||
if (props.info.share) {
|
||||
if (menu.length > 0) menu.push(null);
|
||||
menu.push({
|
||||
text: i18n.locale.share,
|
||||
icon: 'fas fa-share-alt',
|
||||
action: share
|
||||
});
|
||||
}
|
||||
if (props.menu) {
|
||||
if (menu.length > 0) menu.push(null);
|
||||
menu = menu.concat(props.menu);
|
||||
}
|
||||
popupMenu(menu, ev.currentTarget || ev.target);
|
||||
};
|
||||
|
||||
const showTabsPopup = (ev: MouseEvent) => {
|
||||
if (!hasTabs.value) return;
|
||||
if (!narrow.value) return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const menu = props.info.tabs.map(tab => ({
|
||||
text: tab.title,
|
||||
icon: tab.icon,
|
||||
action: tab.onClick,
|
||||
}));
|
||||
popupMenu(menu, ev.currentTarget || ev.target);
|
||||
};
|
||||
|
||||
const preventDrag = (ev: TouchEvent) => {
|
||||
ev.stopPropagation();
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
scrollToTop(el.value, { behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const calcBg = () => {
|
||||
const rawBg = props.info?.bg || 'var(--bg)';
|
||||
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||
tinyBg.setAlpha(0.85);
|
||||
bg.value = tinyBg.toRgbString();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
calcBg();
|
||||
globalEvents.on('themeChanged', calcBg);
|
||||
onUnmounted(() => {
|
||||
globalEvents.off('themeChanged', calcBg);
|
||||
});
|
||||
|
||||
if (el.value.parentElement) {
|
||||
narrow.value = el.value.parentElement.offsetWidth < 500;
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
if (el.value) {
|
||||
narrow.value = el.value.parentElement.offsetWidth < 500;
|
||||
}
|
||||
});
|
||||
ro.observe(el.value.parentElement);
|
||||
onUnmounted(() => {
|
||||
ro.disconnect();
|
||||
});
|
||||
setTimeout(() => {
|
||||
const currentStickyTop = getComputedStyle(el.value.parentElement).getPropertyValue('--stickyTop') || '0px';
|
||||
el.value.style.setProperty('--stickyTop', currentStickyTop);
|
||||
el.value.parentElement.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${el.value.offsetHeight}px)`);
|
||||
}, 100); // レンダリング順序の関係で親のstickyTopの設定が少し遅れることがあるため
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
el,
|
||||
bg,
|
||||
narrow,
|
||||
height,
|
||||
hasTabs,
|
||||
shouldShowMenu,
|
||||
share,
|
||||
showMenu,
|
||||
showTabsPopup,
|
||||
preventDrag,
|
||||
onClick,
|
||||
hideTitle: inject('shouldOmitHeaderTitle', false),
|
||||
thin_: props.thin || inject('shouldHeaderThin', false)
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fdidabkb {
|
||||
--height: 60px;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: var(--stickyTop, 0);
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
|
||||
&.thin {
|
||||
--height: 50px;
|
||||
|
||||
> .buttons {
|
||||
> .button {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.slim {
|
||||
text-align: center;
|
||||
|
||||
> .titleContainer {
|
||||
flex: 1;
|
||||
margin: 0 auto;
|
||||
margin-left: var(--height);
|
||||
|
||||
> *:first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
--margin: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--height);
|
||||
margin: 0 var(--margin);
|
||||
|
||||
&.right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
width: var(--height);
|
||||
}
|
||||
|
||||
> .button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(var(--height) - (var(--margin) * 2));
|
||||
width: calc(var(--height) - (var(--margin) * 2));
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
> .fullButton {
|
||||
& + .fullButton {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .titleContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
margin-left: 24px;
|
||||
|
||||
> .avatar {
|
||||
$size: 32px;
|
||||
display: inline-block;
|
||||
width: $size;
|
||||
height: $size;
|
||||
vertical-align: bottom;
|
||||
margin: 0 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
> .title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.1;
|
||||
|
||||
> .subtitle {
|
||||
opacity: 0.6;
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.activeTab {
|
||||
text-align: center;
|
||||
|
||||
> .chevron {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .tabs {
|
||||
margin-left: 16px;
|
||||
font-size: 0.8em;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
|
||||
> .tab {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
font-weight: normal;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
> .icon + .title {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
76
src/client/components/global/spacer.vue
Normal file
76
src/client/components/global/spacer.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div ref="root" :class="$style.root" :style="{ padding: margin + 'px' }">
|
||||
<div ref="content" :class="$style.content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
contentMax: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
let ro: ResizeObserver;
|
||||
const root = ref<HTMLElement>(null);
|
||||
const content = ref<HTMLElement>(null);
|
||||
const margin = ref(0);
|
||||
const adjust = (rect: { width: number; height: number; }) => {
|
||||
if (rect.width > (props.contentMax || 500)) {
|
||||
margin.value = 32;
|
||||
} else {
|
||||
margin.value = 12;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
ro = new ResizeObserver((entries) => {
|
||||
/* iOSが対応していない
|
||||
adjust({
|
||||
width: entries[0].borderBoxSize[0].inlineSize,
|
||||
height: entries[0].borderBoxSize[0].blockSize,
|
||||
});
|
||||
*/
|
||||
adjust({
|
||||
width: root.value.offsetWidth,
|
||||
height: root.value.offsetHeight,
|
||||
});
|
||||
});
|
||||
ro.observe(root.value);
|
||||
|
||||
if (props.contentMax) {
|
||||
content.value.style.maxWidth = `${props.contentMax}px`;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
ro.disconnect();
|
||||
});
|
||||
|
||||
return {
|
||||
root,
|
||||
content,
|
||||
margin,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
@@ -13,6 +13,8 @@ import i18n from './global/i18n';
|
||||
import loading from './global/loading.vue';
|
||||
import error from './global/error.vue';
|
||||
import ad from './global/ad.vue';
|
||||
import header from './global/header.vue';
|
||||
import spacer from './global/spacer.vue';
|
||||
|
||||
export default function(app: App) {
|
||||
app.component('I18n', i18n);
|
||||
@@ -28,4 +30,6 @@ export default function(app: App) {
|
||||
app.component('MkLoading', loading);
|
||||
app.component('MkError', error);
|
||||
app.component('MkAd', ad);
|
||||
app.component('MkHeader', header);
|
||||
app.component('MkSpacer', spacer);
|
||||
}
|
||||
|
@@ -24,35 +24,26 @@
|
||||
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
|
||||
</optgroup>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="chartSpan" style="margin: 0;">
|
||||
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
|
||||
<option value="hour">{{ $ts.perHour }}</option>
|
||||
<option value="day">{{ $ts.perDay }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<canvas ref="chart"></canvas>
|
||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import Chart from 'chart.js';
|
||||
import MkSelect from './ui/select.vue';
|
||||
import number from '@client/filters/number';
|
||||
|
||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||
const negate = arr => arr.map(x => -x);
|
||||
const alpha = (hex, a) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
||||
const r = parseInt(result[1], 16);
|
||||
const g = parseInt(result[2], 16);
|
||||
const b = parseInt(result[3], 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
};
|
||||
import { defineComponent, onMounted, ref, watch } from 'vue';
|
||||
import MkSelect from '@client/components/form/select.vue';
|
||||
import MkChart from '@client/components/chart.vue';
|
||||
import * as os from '@client/os';
|
||||
import { defaultStore } from '@client/store';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkSelect
|
||||
MkSelect,
|
||||
MkChart,
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -68,463 +59,15 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
setup() {
|
||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
||||
const chartSrc = ref('notes');
|
||||
|
||||
return {
|
||||
notesLocalWoW: 0,
|
||||
notesLocalDoD: 0,
|
||||
notesRemoteWoW: 0,
|
||||
notesRemoteDoD: 0,
|
||||
usersLocalWoW: 0,
|
||||
usersLocalDoD: 0,
|
||||
usersRemoteWoW: 0,
|
||||
usersRemoteDoD: 0,
|
||||
now: null,
|
||||
chart: null,
|
||||
chartInstance: null,
|
||||
chartSrc: 'notes',
|
||||
chartSpan: 'hour',
|
||||
}
|
||||
chartSrc,
|
||||
chartSpan,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
data(): any {
|
||||
if (this.chart == null) return null;
|
||||
switch (this.chartSrc) {
|
||||
case 'federation-instances': return this.federationInstancesChart(false);
|
||||
case 'federation-instances-total': return this.federationInstancesChart(true);
|
||||
case 'users': return this.usersChart(false);
|
||||
case 'users-total': return this.usersChart(true);
|
||||
case 'active-users': return this.activeUsersChart();
|
||||
case 'notes': return this.notesChart('combined');
|
||||
case 'local-notes': return this.notesChart('local');
|
||||
case 'remote-notes': return this.notesChart('remote');
|
||||
case 'notes-total': return this.notesTotalChart();
|
||||
case 'drive': return this.driveChart();
|
||||
case 'drive-total': return this.driveTotalChart();
|
||||
case 'drive-files': return this.driveFilesChart();
|
||||
case 'drive-files-total': return this.driveFilesTotalChart();
|
||||
}
|
||||
},
|
||||
|
||||
stats(): any[] {
|
||||
const stats =
|
||||
this.chartSpan == 'day' ? this.chart.perDay :
|
||||
this.chartSpan == 'hour' ? this.chart.perHour :
|
||||
null;
|
||||
|
||||
return stats;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
chartSrc() {
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
chartSpan() {
|
||||
this.renderChart();
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.now = new Date();
|
||||
|
||||
this.fetchChart();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchChart() {
|
||||
const [perHour, perDay] = await Promise.all([Promise.all([
|
||||
os.api('charts/federation', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/users', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/notes', { limit: this.chartLimit, span: 'hour' }),
|
||||
os.api('charts/drive', { limit: this.chartLimit, span: 'hour' }),
|
||||
]), Promise.all([
|
||||
os.api('charts/federation', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/users', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/active-users', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/notes', { limit: this.chartLimit, span: 'day' }),
|
||||
os.api('charts/drive', { limit: this.chartLimit, span: 'day' }),
|
||||
])]);
|
||||
|
||||
const chart = {
|
||||
perHour: {
|
||||
federation: perHour[0],
|
||||
users: perHour[1],
|
||||
activeUsers: perHour[2],
|
||||
notes: perHour[3],
|
||||
drive: perHour[4],
|
||||
},
|
||||
perDay: {
|
||||
federation: perDay[0],
|
||||
users: perDay[1],
|
||||
activeUsers: perDay[2],
|
||||
notes: perDay[3],
|
||||
drive: perDay[4],
|
||||
}
|
||||
};
|
||||
|
||||
this.chart = chart;
|
||||
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
renderChart() {
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.destroy();
|
||||
}
|
||||
|
||||
// TODO: var(--panel)の色が暗いか明るいかで判定する
|
||||
const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
this.chartInstance = markRaw(new Chart(this.$refs.chart, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
|
||||
datasets: this.data.series.map(x => ({
|
||||
label: x.name,
|
||||
data: x.data.slice().reverse(),
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: x.color,
|
||||
borderDash: x.borderDash || [],
|
||||
backgroundColor: alpha(x.color, 0.1),
|
||||
fill: x.fill == null ? true : x.fill,
|
||||
hidden: !!x.hidden
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 2.5,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: 8
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 16,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'time',
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: this.chartSpan == 'day' ? 'month' : 'day',
|
||||
},
|
||||
gridLines: {
|
||||
display: this.detailed,
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: this.detailed
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
position: 'left',
|
||||
gridLines: {
|
||||
color: gridColor,
|
||||
zeroLineColor: gridColor,
|
||||
},
|
||||
ticks: {
|
||||
display: this.detailed
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
getDate(ago: number) {
|
||||
const y = this.now.getFullYear();
|
||||
const m = this.now.getMonth();
|
||||
const d = this.now.getDate();
|
||||
const h = this.now.getHours();
|
||||
|
||||
return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
|
||||
},
|
||||
|
||||
format(arr) {
|
||||
const now = Date.now();
|
||||
return arr.map((v, i) => ({
|
||||
x: new Date(now - ((this.chartSpan == 'day' ? 86400000 :3600000 ) * i)),
|
||||
y: v
|
||||
}));
|
||||
},
|
||||
|
||||
federationInstancesChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Instances',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? this.stats.federation.instance.total
|
||||
: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
notesChart(type: string): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: this.format(type == 'combined'
|
||||
? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
|
||||
: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
|
||||
)
|
||||
}, {
|
||||
name: 'Renotes',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
data: this.format(type == 'combined'
|
||||
? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
|
||||
: this.stats.notes[type].diffs.renote
|
||||
)
|
||||
}, {
|
||||
name: 'Replies',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
data: this.format(type == 'combined'
|
||||
? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
|
||||
: this.stats.notes[type].diffs.reply
|
||||
)
|
||||
}, {
|
||||
name: 'Normal',
|
||||
type: 'area',
|
||||
color: '#FF4560',
|
||||
data: this.format(type == 'combined'
|
||||
? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
|
||||
: this.stats.notes[type].diffs.normal
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
notesTotalChart(): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.notes.local.total)
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.notes.remote.total)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
usersChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
data: this.format(total
|
||||
? sum(this.stats.users.local.total, this.stats.users.remote.total)
|
||||
: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
|
||||
)
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(total
|
||||
? this.stats.users.local.total
|
||||
: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
|
||||
)
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(total
|
||||
? this.stats.users.remote.total
|
||||
: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
activeUsersChart(): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count))
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.activeUsers.local.count)
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.activeUsers.remote.count)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveChart(): any {
|
||||
return {
|
||||
bytes: true,
|
||||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
color: '#09d8e2',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: this.format(
|
||||
sum(
|
||||
this.stats.drive.local.incSize,
|
||||
negate(this.stats.drive.local.decSize),
|
||||
this.stats.drive.remote.incSize,
|
||||
negate(this.stats.drive.remote.decSize)
|
||||
)
|
||||
)
|
||||
}, {
|
||||
name: 'Local +',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: this.format(this.stats.drive.local.incSize)
|
||||
}, {
|
||||
name: 'Local -',
|
||||
type: 'area',
|
||||
color: '#FF4560',
|
||||
data: this.format(negate(this.stats.drive.local.decSize))
|
||||
}, {
|
||||
name: 'Remote +',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
data: this.format(this.stats.drive.remote.incSize)
|
||||
}, {
|
||||
name: 'Remote -',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
data: this.format(negate(this.stats.drive.remote.decSize))
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveTotalChart(): any {
|
||||
return {
|
||||
bytes: true,
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.drive.local.totalSize)
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.drive.remote.totalSize)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveFilesChart(): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'All',
|
||||
type: 'line',
|
||||
color: '#09d8e2',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: this.format(
|
||||
sum(
|
||||
this.stats.drive.local.incCount,
|
||||
negate(this.stats.drive.local.decCount),
|
||||
this.stats.drive.remote.incCount,
|
||||
negate(this.stats.drive.remote.decCount)
|
||||
)
|
||||
)
|
||||
}, {
|
||||
name: 'Local +',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
data: this.format(this.stats.drive.local.incCount)
|
||||
}, {
|
||||
name: 'Local -',
|
||||
type: 'area',
|
||||
color: '#FF4560',
|
||||
data: this.format(negate(this.stats.drive.local.decCount))
|
||||
}, {
|
||||
name: 'Remote +',
|
||||
type: 'area',
|
||||
color: '#00E396',
|
||||
data: this.format(this.stats.drive.remote.incCount)
|
||||
}, {
|
||||
name: 'Remote -',
|
||||
type: 'area',
|
||||
color: '#FEB019',
|
||||
data: this.format(negate(this.stats.drive.remote.decCount))
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveFilesTotalChart(): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Combined',
|
||||
type: 'line',
|
||||
color: '#008FFB',
|
||||
data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
|
||||
}, {
|
||||
name: 'Local',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.drive.local.totalCount)
|
||||
}, {
|
||||
name: 'Remote',
|
||||
type: 'area',
|
||||
color: '#008FFB',
|
||||
hidden: true,
|
||||
data: this.format(this.stats.drive.remote.totalCount)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
number
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -3,10 +3,13 @@
|
||||
<div class="container">
|
||||
<div class="fullwidth top-caption">
|
||||
<div class="mk-dialog">
|
||||
<header v-if="title"><Mfm :text="title"/></header>
|
||||
<header>
|
||||
<Mfm v-if="title" class="title" :text="title"/>
|
||||
<span class="text-count" :class="{ over: remainingLength < 0 }">{{ remainingLength }}</span>
|
||||
</header>
|
||||
<textarea autofocus v-model="inputValue" :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea>
|
||||
<div class="buttons" v-if="(showOkButton || showCancelButton)">
|
||||
<MkButton inline @click="ok" primary>{{ $ts.ok }}</MkButton>
|
||||
<MkButton inline @click="ok" primary :disabled="remainingLength < 0">{{ $ts.ok }}</MkButton>
|
||||
<MkButton inline @click="cancel" >{{ $ts.cancel }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,10 +29,12 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { length } from 'stringz';
|
||||
import MkModal from '@client/components/ui/modal.vue';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import bytes from '@client/filters/bytes';
|
||||
import number from '@client/filters/number';
|
||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -79,6 +84,13 @@ export default defineComponent({
|
||||
document.removeEventListener('keydown', this.onKeydown);
|
||||
},
|
||||
|
||||
computed: {
|
||||
remainingLength(): number {
|
||||
if (typeof this.inputValue != "string") return DB_MAX_IMAGE_COMMENT_LENGTH;
|
||||
return DB_MAX_IMAGE_COMMENT_LENGTH - length(this.inputValue);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
bytes,
|
||||
number,
|
||||
@@ -156,8 +168,18 @@ export default defineComponent({
|
||||
|
||||
> header {
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
position: relative;
|
||||
|
||||
> .title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
> .text-count {
|
||||
opacity: 0.7;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons {
|
||||
@@ -184,7 +206,7 @@ export default defineComponent({
|
||||
min-width: 100%;
|
||||
min-height: 90px;
|
||||
|
||||
&:focus {
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
@@ -185,7 +185,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
if (style == null) {
|
||||
return h('span', {}, ['[', token.props.name, ...genEl(token.children), ']']);
|
||||
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']);
|
||||
} else {
|
||||
return h('span', {
|
||||
style: 'display: inline-block;' + style,
|
||||
|
@@ -2,11 +2,15 @@
|
||||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
||||
<div class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||
<div class="header" @contextmenu="onContextmenu">
|
||||
<span class="title">
|
||||
<XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="$refs.modal.close()"/>
|
||||
<button v-if="history.length > 0" class="_button" @click="back()" v-tooltip="$ts.goBack"><i class="fas fa-arrow-left"></i></button>
|
||||
<span v-else style="display: inline-block; width: 20px"></span>
|
||||
<span v-if="pageInfo" class="title">
|
||||
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i>
|
||||
<span>{{ pageInfo.title }}</span>
|
||||
</span>
|
||||
<button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="body _flat_">
|
||||
<div class="body _fitSide_">
|
||||
<keep-alive>
|
||||
<component :is="component" v-bind="props" :ref="changePage"/>
|
||||
</keep-alive>
|
||||
@@ -18,7 +22,6 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MkModal from '@client/components/ui/modal.vue';
|
||||
import XHeader from '@client/ui/_common_/header.vue';
|
||||
import { popout } from '@client/scripts/popout';
|
||||
import copyToClipboard from '@client/scripts/copy-to-clipboard';
|
||||
import { resolve } from '@client/router';
|
||||
@@ -29,7 +32,6 @@ import * as os from '@client/os';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkModal,
|
||||
XHeader,
|
||||
},
|
||||
|
||||
inject: {
|
||||
@@ -42,7 +44,8 @@ export default defineComponent({
|
||||
return {
|
||||
navHook: (path) => {
|
||||
this.navigate(path);
|
||||
}
|
||||
},
|
||||
shouldHeaderThin: true,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -172,19 +175,39 @@ export default defineComponent({
|
||||
$height-narrow: 42px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: $height;
|
||||
line-height: $height;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-shadow: 0px 1px var(--divider);
|
||||
|
||||
> button {
|
||||
height: $height;
|
||||
width: $height;
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
height: $height-narrow;
|
||||
line-height: $height-narrow;
|
||||
padding-left: 16px;
|
||||
|
||||
> button {
|
||||
height: $height-narrow;
|
||||
width: $height-narrow;
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
flex: 1;
|
||||
height: $height;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
height: $height-narrow;
|
||||
padding-left: 16px;
|
||||
> .icon {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -59,7 +59,7 @@
|
||||
<div class="body">
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<XCwButton v-model:value="showContent" :note="appearNote"/>
|
||||
<XCwButton v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div class="content" v-show="appearNote.cw == null || showContent">
|
||||
<div class="text">
|
||||
@@ -80,7 +80,7 @@
|
||||
</div>
|
||||
<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="true" class="url-preview"/>
|
||||
<div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div>
|
||||
<div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
|
||||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
@@ -132,7 +132,7 @@ import * as mfm from 'mfm-js';
|
||||
import { sum } from '../../prelude/array';
|
||||
import XSub from './note.sub.vue';
|
||||
import XNoteHeader from './note-header.vue';
|
||||
import XNotePreview from './note-preview.vue';
|
||||
import XNoteSimple from './note-simple.vue';
|
||||
import XReactionsViewer from './reactions-viewer.vue';
|
||||
import XMediaList from './media-list.vue';
|
||||
import XCwButton from './cw-button.vue';
|
||||
@@ -153,7 +153,7 @@ export default defineComponent({
|
||||
components: {
|
||||
XSub,
|
||||
XNoteHeader,
|
||||
XNotePreview,
|
||||
XNoteSimple,
|
||||
XReactionsViewer,
|
||||
XMediaList,
|
||||
XCwButton,
|
||||
|
@@ -1,15 +1,13 @@
|
||||
<template>
|
||||
<div class="yohlumlk" v-size="{ min: [350, 500] }">
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<div class="fefdfafb" v-size="{ min: [350, 500] }">
|
||||
<MkAvatar class="avatar" :user="$i"/>
|
||||
<div class="main">
|
||||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||
<div class="header">
|
||||
<MkUserName :user="$i"/>
|
||||
</div>
|
||||
<div class="body">
|
||||
<p v-if="note.cw != null" class="cw">
|
||||
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
|
||||
<XCwButton v-model:value="showContent" :note="note"/>
|
||||
</p>
|
||||
<div class="content" v-show="note.cw == null || showContent">
|
||||
<XSubNote-content class="text" :note="note"/>
|
||||
<div class="content">
|
||||
<Mfm :text="text" :author="$i" :i="$i"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,35 +16,22 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import XNoteHeader from './note-header.vue';
|
||||
import XSubNoteContent from './sub-note-content.vue';
|
||||
import XCwButton from './cw-button.vue';
|
||||
import * as os from '@client/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNoteHeader,
|
||||
XSubNoteContent,
|
||||
XCwButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
text: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showContent: false
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.yohlumlk {
|
||||
.fefdfafb {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
113
src/client/components/note-simple.vue
Normal file
113
src/client/components/note-simple.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="yohlumlk" v-size="{ min: [350, 500] }">
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<div class="main">
|
||||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||
<div class="body">
|
||||
<p v-if="note.cw != null" class="cw">
|
||||
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
|
||||
<XCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div class="content" v-show="note.cw == null || showContent">
|
||||
<XSubNote-content class="text" :note="note"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import XNoteHeader from './note-header.vue';
|
||||
import XSubNoteContent from './sub-note-content.vue';
|
||||
import XCwButton from './cw-button.vue';
|
||||
import * as os from '@client/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNoteHeader,
|
||||
XSubNoteContent,
|
||||
XCwButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
note: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showContent: false
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.yohlumlk {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: clip;
|
||||
font-size: 0.95em;
|
||||
|
||||
&.min-width_350px {
|
||||
> .avatar {
|
||||
margin: 0 10px 0 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
&.min-width_500px {
|
||||
> .avatar {
|
||||
margin: 0 12px 0 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
> .avatar {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
margin: 0 10px 0 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
> .main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
> .header {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
||||
> .cw {
|
||||
cursor: default;
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
> .text {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
> .text {
|
||||
cursor: default;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -7,7 +7,7 @@
|
||||
<div class="body">
|
||||
<p v-if="note.cw != null" class="cw">
|
||||
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" />
|
||||
<XCwButton v-model:value="showContent" :note="note"/>
|
||||
<XCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div class="content" v-show="note.cw == null || showContent">
|
||||
<XSubNote-content class="text" :note="note"/>
|
||||
|
@@ -43,7 +43,7 @@
|
||||
<div class="body">
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<XCwButton v-model:value="showContent" :note="appearNote"/>
|
||||
<XCwButton v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div class="content" :class="{ collapsed }" v-show="appearNote.cw == null || showContent">
|
||||
<div class="text">
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="false" class="url-preview"/>
|
||||
<div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div>
|
||||
<div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
|
||||
<button v-if="collapsed" class="fade _button" @click="collapsed = false">
|
||||
<span>{{ $ts.showMore }}</span>
|
||||
</button>
|
||||
@@ -114,7 +114,7 @@ import * as mfm from 'mfm-js';
|
||||
import { sum } from '../../prelude/array';
|
||||
import XSub from './note.sub.vue';
|
||||
import XNoteHeader from './note-header.vue';
|
||||
import XNotePreview from './note-preview.vue';
|
||||
import XNoteSimple from './note-simple.vue';
|
||||
import XReactionsViewer from './reactions-viewer.vue';
|
||||
import XMediaList from './media-list.vue';
|
||||
import XCwButton from './cw-button.vue';
|
||||
@@ -134,7 +134,7 @@ export default defineComponent({
|
||||
components: {
|
||||
XSub,
|
||||
XNoteHeader,
|
||||
XNotePreview,
|
||||
XNoteSimple,
|
||||
XReactionsViewer,
|
||||
XMediaList,
|
||||
XCwButton,
|
||||
@@ -888,7 +888,7 @@ export default defineComponent({
|
||||
//content-visibility: auto;
|
||||
//contain-intrinsic-size: 0 128px;
|
||||
|
||||
&:focus {
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
|
||||
&:after {
|
||||
|
@@ -29,10 +29,10 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import XModalWindow from '@client/components/ui/modal-window.vue';
|
||||
import MkSwitch from './ui/switch.vue';
|
||||
import MkSwitch from './form/switch.vue';
|
||||
import MkInfo from './ui/info.vue';
|
||||
import MkButton from './ui/button.vue';
|
||||
import { notificationTypes } from '../../types';
|
||||
import { notificationTypes } from '@/types';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@@ -26,7 +26,7 @@ import paging from '@client/scripts/paging';
|
||||
import XNotification from './notification.vue';
|
||||
import XList from './date-separated-list.vue';
|
||||
import XNote from './note.vue';
|
||||
import { notificationTypes } from '../../types';
|
||||
import { notificationTypes } from '@/types';
|
||||
import * as os from '@client/os';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
|
||||
@@ -48,6 +48,11 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
unreadOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -58,6 +63,7 @@ export default defineComponent({
|
||||
limit: 10,
|
||||
params: () => ({
|
||||
includeTypes: this.allIncludeTypes || undefined,
|
||||
unreadOnly: this.unreadOnly,
|
||||
})
|
||||
},
|
||||
};
|
||||
@@ -76,6 +82,11 @@ export default defineComponent({
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
unreadOnly: {
|
||||
handler() {
|
||||
this.reload();
|
||||
},
|
||||
},
|
||||
// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $i が更新されると、
|
||||
// mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す
|
||||
'$i.mutingNotificationTypes': {
|
||||
|
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>
|
@@ -3,14 +3,20 @@
|
||||
:initial-width="500"
|
||||
:initial-height="500"
|
||||
:can-resize="true"
|
||||
:close-button="false"
|
||||
:close-button="true"
|
||||
:contextmenu="contextmenu"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
<XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()" :title-only="true"/>
|
||||
<template v-if="pageInfo">
|
||||
<i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i>
|
||||
<span>{{ pageInfo.title }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<div class="yrolvcoq _flat_">
|
||||
<template #headerLeft>
|
||||
<button v-if="history.length > 0" class="_button" @click="back()" v-tooltip="$ts.goBack"><i class="fas fa-arrow-left"></i></button>
|
||||
</template>
|
||||
<div class="yrolvcoq _fitSide_">
|
||||
<component :is="component" v-bind="props" :ref="changePage"/>
|
||||
</div>
|
||||
</XWindow>
|
||||
@@ -19,7 +25,6 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import XWindow from '@client/components/ui/window.vue';
|
||||
import XHeader from '@client/ui/_common_/header.vue';
|
||||
import { popout } from '@client/scripts/popout';
|
||||
import copyToClipboard from '@client/scripts/copy-to-clipboard';
|
||||
import { resolve } from '@client/router';
|
||||
@@ -29,7 +34,6 @@ import * as symbols from '@client/symbols';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XWindow,
|
||||
XHeader,
|
||||
},
|
||||
|
||||
inject: {
|
||||
@@ -42,7 +46,8 @@ export default defineComponent({
|
||||
return {
|
||||
navHook: (path) => {
|
||||
this.navigate(path);
|
||||
}
|
||||
},
|
||||
shouldHeaderThin: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@@ -8,7 +8,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue';
|
||||
import MkInput from '../ui/input.vue';
|
||||
import MkInput from '../form/input.vue';
|
||||
import * as os from '@client/os';
|
||||
import { Hpml } from '@client/scripts/hpml/evaluator';
|
||||
import { NumberInputVarBlock } from '@client/scripts/hpml/block';
|
||||
|
@@ -10,7 +10,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import MkTextarea from '../ui/textarea.vue';
|
||||
import MkTextarea from '../form/textarea.vue';
|
||||
import MkButton from '../ui/button.vue';
|
||||
import { apiUrl } from '@client/config';
|
||||
import * as os from '@client/os';
|
||||
|
@@ -7,7 +7,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue';
|
||||
import MkRadio from '../ui/radio.vue';
|
||||
import MkRadio from '../form/radio.vue';
|
||||
import * as os from '@client/os';
|
||||
import { Hpml } from '@client/scripts/hpml/evaluator';
|
||||
import { RadioButtonVarBlock } from '@client/scripts/hpml/block';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user