Compare commits
60 Commits
Author | SHA1 | Date | |
---|---|---|---|
9406079cb7 | |||
cd2de7f893 | |||
342061803e | |||
05b8111c19 | |||
747a0b1791 | |||
c05586b53a | |||
0cfca4a618 | |||
2b8187f7ab | |||
291e7e7943 | |||
535d10f469 | |||
4cb58c0892 | |||
6721d27e3f | |||
da9dd7c423 | |||
867eb41618 | |||
6fdff13480 | |||
e581ead1ed | |||
7495206db2 | |||
fe87d16d46 | |||
0db54386cd | |||
772258b0b8 | |||
3ef002e14d | |||
b90d473ae5 | |||
f5091d524b | |||
ee5720df2c | |||
afe2037985 | |||
878cd18144 | |||
e8c8f67a09 | |||
7b29e7cf7e | |||
723d3e6871 | |||
f063dee12a | |||
db4b315d3d | |||
64d00b08a3 | |||
74736a941e | |||
c40366f20a | |||
d73e52ac2a | |||
9fac8a611f | |||
9c774a50f8 | |||
4f20ee1909 | |||
1440c5bc65 | |||
5829b2a7c1 | |||
e3c4bc18d0 | |||
8555ec2f72 | |||
8d7cefbbbe | |||
5aa5896b22 | |||
8a55bdd89d | |||
6444ef6444 | |||
701ab6cc68 | |||
3c6ce7a943 | |||
1e54afd217 | |||
4e90fd3a06 | |||
0463c6bb0f | |||
f31f986d66 | |||
c203c8302b | |||
dea4a7b389 | |||
0caf9b0706 | |||
5f655c9fd7 | |||
44aed53998 | |||
f9ff621d4a | |||
23a5a75778 | |||
a78475844a |
@ -1,5 +1,4 @@
|
||||
# db settings
|
||||
POSTGRES_PASSWORD="example-misskey-pass"
|
||||
POSTGRES_USER="example-misskey-user"
|
||||
POSTGRES_DB="misskey"
|
||||
|
||||
POSTGRES_PASSWORD=example-misskey-pass
|
||||
POSTGRES_USER=example-misskey-user
|
||||
POSTGRES_DB=misskey
|
||||
|
@ -132,6 +132,9 @@ drive:
|
||||
# ulid ... Millisecond accuracy
|
||||
# objectid ... This is left for backward compatibility
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# ID SETTINGS AFTER THAT!
|
||||
|
||||
id: 'aid'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
|
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@ -1,7 +1,7 @@
|
||||
# PATH OWNERS
|
||||
/.autogen/ @acid-chicken
|
||||
/.circleci/ @syuilo @acid-chicken
|
||||
/.config/ @syuilo @AyaMorisawa @mei23 @acid-chicken
|
||||
/.config/ @syuilo @AyaMorisawa @mei23 @acid-chicken @rinsuki
|
||||
# /.config/mongo_initdb_example.js @khws4v1
|
||||
/.github/ @syuilo @AyaMorisawa @acid-chicken
|
||||
/.vscode/ @acid-chicken
|
||||
@ -12,7 +12,7 @@
|
||||
# /docs/*.fr.md @BoFFire
|
||||
# /docs/docker.*.md @khws4v1
|
||||
/locales/ @syuilo
|
||||
/src/ @syuilo @AyaMorisawa @mei23 @acid-chicken
|
||||
/src/ @syuilo @AyaMorisawa @mei23 @acid-chicken @rinsuki
|
||||
# /src/crypto_key.cc @akihikodaki
|
||||
# /src/crypto_key.d.ts @akihikodaki
|
||||
/.dockerignore @syuilo # @khws4v1
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,3 +20,4 @@ api-docs.json
|
||||
yarn.lock
|
||||
.DS_Store
|
||||
/files
|
||||
ormconfig.json
|
||||
|
101
CHANGELOG.md
101
CHANGELOG.md
@ -7,6 +7,13 @@ If you encounter any problems with updating, please try the following:
|
||||
|
||||
How to migrate to v11 from v10
|
||||
------------------------------
|
||||
### 移行の注意点
|
||||
**以下のデータは引き継がれません**
|
||||
* 通知
|
||||
* リモートの投稿
|
||||
* リバーシの対局
|
||||
|
||||
### 手順
|
||||
1. v11をインストールしたい場所に syuilo/misskey をクローン
|
||||
2. config を設定する
|
||||
* PostgreSQL(`db`)の設定とは別に、v10からMongoDBの設定をコピペしてくる(例は下にあります)
|
||||
@ -35,6 +42,100 @@ mongodb:
|
||||
8. master ブランチに戻す
|
||||
9. enjoy
|
||||
|
||||
11.5.0 (2019/04/29)
|
||||
-------------------
|
||||
### 注意
|
||||
このアップデートを適用した後、プロセスを起動(もしくは再起動)する前にまず以下の手順を実行してください
|
||||
|
||||
#### 1
|
||||
`ormconfig.json`という名前で、Misskeyのインストール場所(package.jsonとかがあるディレクトリ)に新たなファイルを作る。中身は次のようにします:
|
||||
``` json
|
||||
{
|
||||
"type": "postgres",
|
||||
"host": "PostgreSQLのホスト",
|
||||
"port": 5432,
|
||||
"username": "PostgreSQLのユーザー名",
|
||||
"password": "PostgreSQLのパスワード",
|
||||
"database": "PostgreSQLのデータベース名",
|
||||
"entities": ["src/models/entities/*.ts"],
|
||||
"migrations": ["migration/*.ts"],
|
||||
"cli": {
|
||||
"migrationsDir": "migration"
|
||||
}
|
||||
}
|
||||
```
|
||||
上記の各種PostgreSQLの設定(ポートも)は、設定ファイルに書いてあるものをコピーしてください。
|
||||
|
||||
#### 2
|
||||
```
|
||||
npm i -g ts-node
|
||||
```
|
||||
|
||||
#### 3
|
||||
```
|
||||
ts-node ./node_modules/typeorm/cli.js migration:run
|
||||
```
|
||||
|
||||
### New features
|
||||
#### MisskeyPages
|
||||
ページ(記事)を作成できるように。
|
||||
|
||||
* 後から何度でも編集できる
|
||||
* アイキャッチを設定できる
|
||||
* フォントを設定できる
|
||||
* 画像を好きな位置に挿入できる
|
||||
* URLを決められる
|
||||
* タイトルを設定できる
|
||||
* 見出しを設定できる
|
||||
* ページの要約を設定できる(URLプレビュー時などに便利)
|
||||
* 変数や式(aka AiScript)を使用して動的なページも作れる
|
||||
* 目次自動生成(coming soon)
|
||||
|
||||
ページを気に入ったら「いいね」しよう (coming soon)
|
||||
|
||||
### Improvements
|
||||
* APIコンソールでパラメータテンプレートを表示するように
|
||||
|
||||
### Fixes
|
||||
* おすすめユーザーに自分自身が含まれる問題を修正
|
||||
* ユーザーサジェストで表示名が変わらない問題を修正
|
||||
|
||||
11.4.0 (2019/04/25)
|
||||
-------------------
|
||||
### Improvements
|
||||
* 検索でローカルの投稿のみに絞れるように
|
||||
* 検索で特定のインスタンスの投稿のみに絞れるように
|
||||
* 検索で特定のユーザーの投稿のみに絞れるように
|
||||
|
||||
### Fixes
|
||||
* 投稿が増殖する問題を修正
|
||||
* ストリームで過去の投稿が流れてくる問題を修正
|
||||
* モバイル版のユーザーページで遷移してもユーザー名が変わらない問題を修正
|
||||
* お知らせを切り替えても内容が変わらない問題を修正
|
||||
|
||||
11.3.1 (2019/04/24)
|
||||
-------------------
|
||||
### Fixes
|
||||
* Webからファイルがアップロードできない問題を修正
|
||||
|
||||
11.3.0 (2019/04/24)
|
||||
-------------------
|
||||
### Improvements
|
||||
* お知らせにMFMを使えるように
|
||||
* お知らせに画像を添付できるように
|
||||
|
||||
### Fixes
|
||||
* 投稿のタグ検索APIで大文字小文字が区別されていたのを修正
|
||||
* 公開範囲がホームの投稿がグローバルTLに流れる問題を修正
|
||||
* モバイルビューの投稿詳細にて acct が長いとアイコンが圧迫面接される問題を修正
|
||||
|
||||
11.2.2 (2019/04/22)
|
||||
-------------------
|
||||
### Fixes
|
||||
* 2段階認証を有効にするとログインできない問題を修正
|
||||
* リモートユーザーの修復処理が自動的に実行されない問題を修正
|
||||
* リモートユーザー情報が更新されない問題を修正
|
||||
|
||||
11.2.1 (2019/04/21)
|
||||
-------------------
|
||||
### Fixes
|
||||
|
16
README.md
16
README.md
@ -1,6 +1,6 @@
|
||||
<a href="https://xn--931a.moe/"><img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a>
|
||||
|
||||
[](https://misskey.xyz/)
|
||||
[](https://misskey.io/)
|
||||
================================================================
|
||||
|
||||
[](https://circleci.com/gh/syuilo/misskey)
|
||||
@ -10,7 +10,7 @@
|
||||
**A forever evolving, sophisticated microblogging platform.**
|
||||
|
||||
<p align="justify">
|
||||
<a href="https://misskey.xyz">Misskey</a> is a decentralized microblogging platform born on Earth.
|
||||
<a href="https://misskey.io">Misskey</a> is a decentralized microblogging platform born on Earth.
|
||||
Since it exists within the Fediverse (a universe where various social media platforms are organized),
|
||||
it is mutually linked with other social media platforms.
|
||||
Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? <a href="https://joinmisskey.github.io/">Find an instance!</a>
|
||||
@ -88,12 +88,14 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
<td><img src="https://avatars0.githubusercontent.com/u/10798641?s=460&v=4" alt="AyaMorisawa" width="100"></td>
|
||||
<td><img src="https://avatars1.githubusercontent.com/u/30769358?s=460&v=4" alt="mei23" width="100"></td>
|
||||
<td><img src="https://avatars2.githubusercontent.com/u/20679825?s=460&v=4" alt="acid-chicken" width="100"></td>
|
||||
<td><img src="https://avatars2.githubusercontent.com/u/6533808?s=460&v=4" alt="rinsuki" width="100"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/syuilo">@syuilo</a></td>
|
||||
<td align="center"><a href="https://github.com/AyaMorisawa">@AyaMorisawa</a></td>
|
||||
<td align="center"><a href="https://github.com/mei23">@mei23</a></td>
|
||||
<td align="center"><a href="https://github.com/acid-chicken">@acid-chicken</a></td>
|
||||
<td align="center"><a href="https://github.com/rinsuki">@rinsuki</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@ -102,14 +104,14 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
<!-- PATREON_START -->
|
||||
<table><tr>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5888816/36da0f7c15954df0ab13f9abdf227f66/1.jpeg?token-time=2145916800&token-hash=at8QpJXJ8C0zINY_NmoMKv-MhXVoUK-YzTgaJPJzJYU%3D" alt="Hiroshi Seki" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weep" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weepjp" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13099460/43cecdbaa63a40d79bf50a96b9910b9d/1.jpe?token-time=2145916800&token-hash=bqwLTk0Wo0hUJJ8J5y7ii05bLzz-_CDA7Bo0Mp4RFU0%3D" alt="ne_moni" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/4.jpe?token-time=2145916800&token-hash=zEyJqVM7u9d8Ri-65fJYSJcWF1jBH1nJ5a3taRzrTmw%3D" alt="Melilot" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1.jpg?token-time=2145916800&token-hash=mPLM9CA-riFHx-myr3bLZJuH2xBRHA9se5VbHhLIOuA%3D" alt="osapon" width="100"></td>
|
||||
<td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://www.patreon.com/rane_hs">Hiroshi Seki</a></td>
|
||||
<td><a href="https://www.patreon.com/weepjp">weep</a></td>
|
||||
<td><a href="https://www.patreon.com/weepjp">weepjp</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=13099460">ne_moni</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td>
|
||||
<td><a href="https://www.patreon.com/osapon">osapon</a></td>
|
||||
@ -124,7 +126,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
<td><img src="https://c8.patreon.com/2/200/17866454" alt="sikyosyounin" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3.png?token-time=2145916800&token-hash=KjfQL8nf3AIf6WqzLshBYAyX44piAqOAZiYXgZS_H6A%3D" alt="YUKIMOCHI" width="100"></td>
|
||||
<td><img src="https://c8.patreon.com/2/200/17463605" alt="Sampot" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/1.jpeg?token-time=2145916800&token-hash=3aMtpAjhwf01G3Uf8iIKYL8FUXXgxV7NvoQLne7lAKE%3D" alt="Ryosuke Yamamoto" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/2.jpeg?token-time=2145916800&token-hash=_sTj3dUBOhn9qwiJ7F19Qd-yWWfUqJC_0jG1h0agEqQ%3D" alt="sheeta.s" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td>
|
||||
</tr><tr>
|
||||
<td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td>
|
||||
@ -135,7 +137,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
<td><a href="https://www.patreon.com/user?u=17866454">sikyosyounin</a></td>
|
||||
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=17463605">Sampot</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=19356899">Ryosuke Yamamoto</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=19356899">sheeta.s</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td>
|
||||
</tr></table>
|
||||
<table><tr>
|
||||
@ -165,7 +167,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
|
||||
</tr></table>
|
||||
|
||||
**Last updated:** Thu, 18 Apr 2019 23:38:06 UTC
|
||||
**Last updated:** Wed, 24 Apr 2019 05:56:07 UTC
|
||||
<!-- PATREON_END -->
|
||||
|
||||
:four_leaf_clover: Copyright
|
||||
|
@ -77,6 +77,9 @@ common:
|
||||
"read:messaging": "Prohlížet konverzaci"
|
||||
"write:messaging": "Pracovat s konverzaci"
|
||||
"read:mutes": "Prohlížet ztlumené"
|
||||
"read:notifications": "Prohlížet oznámení"
|
||||
"write:notifications": "Pracovat s oznámeními"
|
||||
"read:reactions": "Prohlížet reakce"
|
||||
"write:votes": "Hlasovat"
|
||||
empty-timeline-info:
|
||||
follow-users-to-make-your-timeline: "Poznámky sledujících se zobrazí ve vaší časové ose"
|
||||
@ -285,6 +288,7 @@ common:
|
||||
nav: "Navigace"
|
||||
tips: "Tipy"
|
||||
hashtags: "Hashtagy"
|
||||
queue: "Ve frontě"
|
||||
dev: "Nepodařilo se vytvořit aplikace. Prosím zkuste to znovu."
|
||||
ai-chan-kawaii: "Ai-chan kawaii!"
|
||||
you: "Vy"
|
||||
@ -593,6 +597,8 @@ common/views/components/user-list-editor.vue:
|
||||
remove-user: "Odebrat z tohoto seznamu"
|
||||
delete-are-you-sure: "Smazat seznam \"$1\"?"
|
||||
deleted: "Smazáno"
|
||||
common/views/components/user-lists.vue:
|
||||
list-name: "Název seznamu"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "Načítám"
|
||||
next: "Další"
|
||||
@ -832,8 +838,6 @@ desktop/views/components/received-follow-requests-window.vue:
|
||||
reject: "Odmítnout"
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "Seznamy uživatelů"
|
||||
create-list: "Vytvořit seznam"
|
||||
list-name: "Název seznamu"
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "Příspěvky"
|
||||
desktop/views/components/users-list.vue:
|
||||
@ -1078,7 +1082,6 @@ mobile/views/components/drive.vue:
|
||||
used: "využito"
|
||||
file-count: "Soubor(ů)"
|
||||
folder-is-empty: "Tato složka je prázdná"
|
||||
deletion-alert: "Omlouváme se, ale mazání složek ještě nebylo implementováno."
|
||||
folder-name: "Název složky"
|
||||
url-prompt: "URL adresa souboru, který chcete nahrát"
|
||||
uploading: "Byl zahájen upload. Může chvilku trvat než bude dokončen."
|
||||
@ -1139,6 +1142,10 @@ mobile/views/components/ui.nav.vue:
|
||||
game: "Hry"
|
||||
admin: "Administrace"
|
||||
about: "O Misskey"
|
||||
mobile/views/pages/drive.vue:
|
||||
contextmenu:
|
||||
upload: "Nahrát soubor"
|
||||
create-folder: "Vytvořit složku"
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "Seznamy"
|
||||
mobile/views/pages/signup.vue:
|
||||
|
@ -676,6 +676,10 @@ mobile/views/components/ui.nav.vue:
|
||||
user-lists: "Listen"
|
||||
game: "Spielen"
|
||||
about: "Über"
|
||||
mobile/views/pages/drive.vue:
|
||||
contextmenu:
|
||||
upload: "Eine Datei hochladen"
|
||||
create-folder: "Ein Verzeichnis erstellen"
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "Listen"
|
||||
mobile/views/pages/home.vue:
|
||||
|
@ -84,6 +84,11 @@ common:
|
||||
"write:messaging": "Work with Messaging"
|
||||
"read:mutes": "View Muted"
|
||||
"write:mutes": "Work with Muted"
|
||||
"write:notes": "Create and delete posts"
|
||||
"read:notifications": "View notifications"
|
||||
"write:notifications": "Work with notifications"
|
||||
"read:reactions": "View reactions"
|
||||
"write:reactions": "Work with reactions"
|
||||
"write:votes": "Vote"
|
||||
empty-timeline-info:
|
||||
follow-users-to-make-your-timeline: "Following users will show their posts in your timeline."
|
||||
@ -484,8 +489,12 @@ common/views/components/user-menu.vue:
|
||||
mention: "Mention"
|
||||
mute: "Mute"
|
||||
unmute: "Unmute"
|
||||
mute-confirm: "Are you sure you want to mute this user?"
|
||||
unmute-confirm: "Are you certain that you want to unmute this user?"
|
||||
block: "Block"
|
||||
unblock: "Unblock"
|
||||
block-confirm: "Are you sure you want to block this user?"
|
||||
unblock-confirm: "Are you certain that you want to unblock this user?"
|
||||
push-to-list: "Add to list"
|
||||
select-list: "Select a list"
|
||||
report-abuse: "Report abuse"
|
||||
@ -493,8 +502,12 @@ common/views/components/user-menu.vue:
|
||||
report-abuse-reported: "The issue has been reported to the administrator. Your cooperation is much appreciated."
|
||||
silence: "Silence"
|
||||
unsilence: "Unsilence"
|
||||
silence-confirm: "Are you sure that you want to silence this user?"
|
||||
unsilence-confirm: "Are you sure that you want to stop silencing this user?"
|
||||
suspend: "Suspend"
|
||||
unsuspend: "Unsuspend"
|
||||
suspend-confirm: "Are you sure that you want to suspend this user?"
|
||||
unsuspend-confirm: "Are you sure that you want to unsuspend this user?"
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "Vote for '{}'"
|
||||
vote-count: "{} votes"
|
||||
@ -677,6 +690,9 @@ common/views/components/user-list-editor.vue:
|
||||
remove-user: "Remove from this list"
|
||||
delete-are-you-sure: "Delete list \"$1\"?"
|
||||
deleted: "Deleted successfully"
|
||||
common/views/components/user-lists.vue:
|
||||
create-list: "Create a list"
|
||||
list-name: "List name"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "Checking"
|
||||
no-broadcasts: "No announcements"
|
||||
@ -1025,8 +1041,6 @@ desktop/views/components/received-follow-requests-window.vue:
|
||||
reject: "Reject"
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "User lists"
|
||||
create-list: "Create a list"
|
||||
list-name: "List name"
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "Posts"
|
||||
following: "Following"
|
||||
@ -1204,7 +1218,9 @@ admin/views/users.vue:
|
||||
unsuspend-confirm: "Are you sure you want to unsuspend this account?"
|
||||
unsuspended: "The user has successfully unsuspended."
|
||||
make-silence: "Silence"
|
||||
silence-confirm: "Silence user?"
|
||||
unmake-silence: "Unsilence"
|
||||
unsilence-confirm: "Are you certain that you want to stop silencing this user?"
|
||||
verify: "Verify account"
|
||||
verify-confirm: "Do you want this to be a verified account?"
|
||||
verified: "The account is now being verified"
|
||||
@ -1398,6 +1414,7 @@ desktop/views/widgets/polls.vue:
|
||||
desktop/views/widgets/post-form.vue:
|
||||
title: "Post"
|
||||
note: "Post"
|
||||
something-happened: "Could not be posted in this circumstance."
|
||||
desktop/views/widgets/profile.vue:
|
||||
update-banner: "Click to edit your banner"
|
||||
update-avatar: "Click to edit your avatar"
|
||||
@ -1416,12 +1433,11 @@ mobile/views/components/drive.vue:
|
||||
file-count: "File(s)"
|
||||
nothing-in-drive: "There's nothing stored."
|
||||
folder-is-empty: "This folder is empty"
|
||||
prompt: "What do you want to do? (Please enter a number): <1 → Upload a file | 2 → Upload a file from a URL | 3 → Create a folder | 4 → Change this folder's name | 5 → Move this folder | 6 → Delete this folder>"
|
||||
deletion-alert: "Sorry! Deleting a folder is not yet implemented."
|
||||
folder-name: "Folder name"
|
||||
here-is-root: "Currently, you are on the root, not inside of any folder."
|
||||
url-prompt: "URL of the file you want to upload"
|
||||
uploading: "Upload requested. It may take a while for the upload to finish."
|
||||
folder-name-cannot-empty: "Folder name cannot be blank."
|
||||
mobile/views/components/drive-file-chooser.vue:
|
||||
select-file: "Choose files"
|
||||
mobile/views/components/drive-folder-chooser.vue:
|
||||
@ -1495,9 +1511,16 @@ mobile/views/components/ui.nav.vue:
|
||||
game: "Games"
|
||||
admin: "Admin"
|
||||
about: "About Misskey"
|
||||
mobile/views/pages/drive.vue:
|
||||
contextmenu:
|
||||
upload: "Upload a file"
|
||||
url-upload: "Upload file from a URL"
|
||||
create-folder: "Create a folder"
|
||||
rename-folder: "Rename folder"
|
||||
move-folder: "Move this folder"
|
||||
delete-folder: "Delete this folder"
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "Lists"
|
||||
enter-list-name: "Enter a name of the list to make"
|
||||
mobile/views/pages/signup.vue:
|
||||
lets-start: "Your account is now ready! 📦"
|
||||
mobile/views/pages/followers.vue:
|
||||
@ -1624,10 +1647,12 @@ dev/views/apps.vue:
|
||||
create-app: "Create app"
|
||||
app-missing: "No apps"
|
||||
dev/views/new-app.vue:
|
||||
new-app: "New Application"
|
||||
create-app: "Creating application"
|
||||
app-name: "Application name"
|
||||
app-name-desc: "The name of your app"
|
||||
app-overview: "Application summary"
|
||||
app-overview-desc: "A brief description, or an introduction of your app."
|
||||
callback-url: "The callback URL (optional)"
|
||||
callback-url-desc: "The URL to redirect to after the user is authenticated via the authentication form."
|
||||
authority: "Permissions"
|
||||
|
@ -466,6 +466,8 @@ common/views/components/profile-editor.vue:
|
||||
enter-password: "Escribe una contraseña"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "Usuarios"
|
||||
common/views/components/user-lists.vue:
|
||||
list-name: "Nombre de lista"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "Recuperando"
|
||||
no-broadcasts: "Sin emisión"
|
||||
@ -773,8 +775,6 @@ desktop/views/components/received-follow-requests-window.vue:
|
||||
reject: "Rechazar"
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "Listas de usuario"
|
||||
create-list: "Crear lista"
|
||||
list-name: "Nombre de lista"
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "Publicaciones"
|
||||
following: "Sigue"
|
||||
@ -931,6 +931,10 @@ mobile/views/components/ui.nav.vue:
|
||||
game: "Juegos"
|
||||
admin: "Admin"
|
||||
about: "Sobre"
|
||||
mobile/views/pages/drive.vue:
|
||||
contextmenu:
|
||||
upload: "Subir fichero"
|
||||
create-folder: "Crear una carpeta"
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "Listas"
|
||||
mobile/views/pages/home.vue:
|
||||
|
@ -13,7 +13,7 @@ common:
|
||||
rich-contents-desc: "Partagez vos idées, les événements et les sujets qui vous tiennent à cœur ainsi que tout autre chose que vous souhaitez partager avec les autres. Si vous le désirez, vous pouvez décorer vos messages en utilisant une syntaxe différente ou en y joignant des sondages et des fichiers, tels que les photos ou les vidéos que vous aimez."
|
||||
reaction: "Réactions"
|
||||
reaction-desc: "Une manière simple d'exprimer vos émotions. Misskey peut attacher diverses réactions aux publications des autres utilisateurs. Si vous essayez les réactions sur Misskey, vous ne pourrez plus retourner sur une autre plateforme de réseaux sociaux n'offrant que des « J'aime »."
|
||||
ui: "Interface utilisateur"
|
||||
ui: "Interface"
|
||||
ui-desc: "Aucune interface graphique ne peut plaire à tout le monde. Par conséquent, Misskey possède une interface utilisateur hautement personnalisable selon vos goûts. Vous pouvez rendre votre page d'accueil originale en modifiant la mise en page de votre fil et en déplaçant les widgets que vous pouvez facilement ajuster pour vous approprier cet espace."
|
||||
drive: "Drive"
|
||||
drive-desc: "Vous voulez poster une photo que vous avez déjà transférée ? Vous souhaitez organiser, nommer et créer un dossier pour vos fichiers téléversés ? Misskey Drive est la meilleure solution pour vous. Très facile de partager vos fichiers en ligne."
|
||||
@ -33,6 +33,7 @@ common:
|
||||
signin: "Se connecter"
|
||||
signup: "S'enregistrer"
|
||||
signout: "Se déconnecter"
|
||||
reload-to-apply-the-setting: "Le rechargement de la page est nécessaire pour appliquer ces paramètres. Désirez-vous la recharger maintenant ?"
|
||||
got-it: "J’ai compris !"
|
||||
customization-tips:
|
||||
title: "Conseils de personnalisation"
|
||||
@ -72,6 +73,10 @@ common:
|
||||
"write:account": "Mettre à jour les informations de votre compte"
|
||||
"read:drive": "Parcourir le Drive"
|
||||
"write:drive": "Écrire sur le Drive"
|
||||
"read:favorites": "Afficher les favoris"
|
||||
"write:notes": "Créer ou supprimer des publications"
|
||||
"read:notifications": "Afficher les notifications"
|
||||
"read:reactions": "Lire les réactions"
|
||||
"write:votes": "Vote"
|
||||
empty-timeline-info:
|
||||
follow-users-to-make-your-timeline: "Les utilisateurs suivants afficheront leurs publications sur votre fil."
|
||||
@ -136,6 +141,7 @@ common:
|
||||
appearance: "Apparence"
|
||||
behavior: "Comportement"
|
||||
fetch-on-scroll: "Chargement automatique lors du défilement"
|
||||
fetch-on-scroll-desc: "Chargement automatique du contenu lors du défilement de la page."
|
||||
note-visibility: "Visibilité de la publication"
|
||||
default-note-visibility: "Visibilité par défaut"
|
||||
remember-note-visibility: "Se souvenir du mode de visibilité de la publication"
|
||||
@ -206,16 +212,16 @@ common:
|
||||
debug-mode: "Activer le mode débogage"
|
||||
debug-mode-desc: "Ce paramètre est stocké dans le navigateur."
|
||||
navbar-position: "Position de la barre de navigation"
|
||||
navbar-position-top: "en haut"
|
||||
navbar-position-top: "En haut"
|
||||
navbar-position-left: "À gauche"
|
||||
navbar-position-right: "à droite"
|
||||
navbar-position-right: "À droite"
|
||||
i-am-under-limited-internet: "J'ai un accès Internet limité"
|
||||
post-style: "Style d'affichage des notes"
|
||||
post-style-standard: "Standard"
|
||||
post-style-smart: "Intelligent"
|
||||
notification-position: "Afficher les notifications"
|
||||
notification-position-bottom: "en bas"
|
||||
notification-position-top: "en haut"
|
||||
notification-position-top: "En haut"
|
||||
disable-via-mobile: "Enlever la mention publié via 'mobile'"
|
||||
load-raw-images: "Afficher les photos jointes dans leur qualité originale"
|
||||
load-remote-media: "Afficher les médias depuis le serveur distant"
|
||||
@ -304,6 +310,7 @@ common/views/pages/explore.vue:
|
||||
explore: "Explorer {host}"
|
||||
users-info: "Actuellement, {users} utilisateurs se sont inscrit ici"
|
||||
common/views/components/url-preview.vue:
|
||||
enable-player: "Activer la lecture"
|
||||
disable-player: "Fermer le lecteur"
|
||||
common/views/components/user-list.vue:
|
||||
no-users: "Il n'y a aucun utilisateur"
|
||||
@ -644,6 +651,7 @@ common/views/components/profile-editor.vue:
|
||||
blocking-list: "Liste des comptes bloqués"
|
||||
user-lists: "Listes"
|
||||
export-requested: "Vous avez demandé une exportation. Cela peut prendre un certain temps. Une fois l'exportation terminée, le fichier résultant sera ajouté dans le Drive."
|
||||
import-requested: "Vous avez initié un import. Ceci peut prendre un peu de temps."
|
||||
enter-password: "Veuillez saisir votre mot de passe"
|
||||
danger-zone: "Zone de danger"
|
||||
delete-account: "Supprimer le compte"
|
||||
@ -655,6 +663,9 @@ common/views/components/user-list-editor.vue:
|
||||
remove-user: "Retirer de cette liste"
|
||||
delete-are-you-sure: "Voulez-vous vraiment supprimer la liste « $1 » ?"
|
||||
deleted: "Supprimé"
|
||||
common/views/components/user-lists.vue:
|
||||
create-list: "Créer une liste"
|
||||
list-name: "Nom de la liste"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "Récupération"
|
||||
no-broadcasts: "Aucune annonce"
|
||||
@ -955,6 +966,7 @@ common/views/components/password-settings.vue:
|
||||
changed: "Mot de passe modifié avec succès"
|
||||
failed: "Échec lors de la modification du mot de passe"
|
||||
common/views/components/post-form-attaches.vue:
|
||||
attach-cancel: "Enlever le fichier attaché"
|
||||
mark-as-sensitive: "Marquer comme sensible"
|
||||
unmark-as-sensitive: "Ne pas marquer comme sensible"
|
||||
desktop/views/components/sub-note-content.vue:
|
||||
@ -1001,8 +1013,6 @@ desktop/views/components/received-follow-requests-window.vue:
|
||||
reject: "Refuser"
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "Listes de l'utilisateur"
|
||||
create-list: "Créer une liste"
|
||||
list-name: "Nom de la liste"
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "Publications"
|
||||
following: "Abonné à"
|
||||
@ -1379,8 +1389,6 @@ mobile/views/components/drive.vue:
|
||||
file-count: "Fichier·s"
|
||||
nothing-in-drive: "Rien"
|
||||
folder-is-empty: "Ce dossier est vide"
|
||||
prompt: "Que veux-tu faire ? (Entrez un nombre): <1 → Télécharger le fichier | 2 → Télécharger le fichier avec l'URL | 3 → Créer le dossier | 4 → Modifier le nom du dossier | 5 → Déplacer ce dossier | 6 → Supprimer ce dossier >"
|
||||
deletion-alert: "Désolé ! La suppression d’un dossier n’est pas encore implémentée."
|
||||
folder-name: "Nom du dossier"
|
||||
url-prompt: "URL du fichier que vous souhaitez téléverser"
|
||||
uploading: "Envoi demandé. Le téléversement pourrait prendre un certain temps avant de s'achever."
|
||||
@ -1457,9 +1465,12 @@ mobile/views/components/ui.nav.vue:
|
||||
game: "Jeux"
|
||||
admin: "Admin"
|
||||
about: "À propos de Misskey"
|
||||
mobile/views/pages/drive.vue:
|
||||
contextmenu:
|
||||
upload: "Téléverser un fichier"
|
||||
create-folder: "Créer un dossier"
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "Listes"
|
||||
enter-list-name: "Nom de la liste"
|
||||
mobile/views/pages/signup.vue:
|
||||
lets-start: "Votre compte est prêt ! 📦"
|
||||
mobile/views/pages/followers.vue:
|
||||
|
@ -65,6 +65,7 @@ common:
|
||||
|
||||
trash: "ゴミ箱"
|
||||
drive: "ドライブ"
|
||||
pages: "ページ"
|
||||
messaging: "トーク"
|
||||
home: "ホーム"
|
||||
deck: "デッキ"
|
||||
@ -1813,26 +1814,6 @@ docs:
|
||||
edit-this-page-on-github: "間違いや改善点を見つけましたか?"
|
||||
edit-this-page-on-github-link: "このページをGitHubで編集"
|
||||
|
||||
api:
|
||||
entities:
|
||||
properties: "プロパティ"
|
||||
endpoints:
|
||||
params: "パラメータ"
|
||||
no-params: "パラメータはありません"
|
||||
res: "レスポンス"
|
||||
require-credential: "このエンドポイントは認証情報が必須です。"
|
||||
require-permission: "このエンドポイントは{permission}の権限を必要とします。"
|
||||
has-limit: "レートリミットがあります。"
|
||||
duration-limit: "直近{duration}ミリ秒の間のこのエンドポイントへのリクエスト数の合計が{max}を超える場合はリクエストできません。"
|
||||
min-interval-limit: "前回のリクエストから{interval}ミリ秒経っていない場合はリクエストできません。"
|
||||
show-src: "このエンドポイントのソースコードも閲覧できます。"
|
||||
show-src-link: "コードをGitHubで見る"
|
||||
generated: "このドキュメントはAPI定義に基づき自動生成されています。"
|
||||
props:
|
||||
name: "名前"
|
||||
type: "型"
|
||||
description: "説明"
|
||||
|
||||
dev/views/index.vue:
|
||||
manage-apps: "アプリの管理"
|
||||
|
||||
@ -1857,3 +1838,165 @@ dev/views/new-app.vue:
|
||||
authority: "権限"
|
||||
authority-desc: "ここで要求した機能だけがAPIからアクセスできます。"
|
||||
authority-warning: "アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。"
|
||||
|
||||
pages:
|
||||
new-page: "ページの作成"
|
||||
edit-page: "ページの編集"
|
||||
page-created: "ページを作成しました"
|
||||
page-updated: "ページを更新しました"
|
||||
are-you-sure-delete: "このページを削除しますか?"
|
||||
page-deleted: "ページを削除しました"
|
||||
edit-this-page: "このページを編集"
|
||||
variables: "変数"
|
||||
variables-info: "変数を使うことで動的なページを作成できます。テキスト内で <b>{ 変数名 }</b> と書くとそこに変数の値を埋め込めます。例えば <b>Hello { thing } world!</b> というテキストで、変数(thing)の値が <b>ai</b> だった場合、テキストは <b>Hello ai world!</b> になります。"
|
||||
variables-info2: "変数の評価(値を算出すること)は上から下に行われるので、ある変数の中で自分より下の変数を参照することはできません。例えば上から <b>A、B、C</b> と3つの変数を定義したとき、<b>C</b>の中で<b>A</b>や<b>B</b>を参照することはできますが、<b>A</b>の中で<b>B</b>や<b>C</b>を参照することはできません。"
|
||||
variables-info3: "ユーザーからの入力を受け取るには、ページに「ユーザー入力」ブロックを設置し、「変数名」に入力を格納したい変数名を設定します(変数は自動で作成されます)。その変数を使ってユーザー入力に応じた動作を行えます。"
|
||||
more-details: "詳しい説明"
|
||||
title: "タイトル"
|
||||
url: "ページURL"
|
||||
summary: "ページの要約"
|
||||
align-center: "中央寄せ"
|
||||
font: "フォント"
|
||||
fontSerif: "セリフ"
|
||||
fontSansSerif: "サンセリフ"
|
||||
set-eye-catchig-image: "アイキャッチ画像を設定"
|
||||
remove-eye-catchig-image: "アイキャッチ画像を削除"
|
||||
choose-block: "ブロックを追加"
|
||||
select-type: "種類を選択"
|
||||
enter-variable-name: "変数名を決めてください"
|
||||
the-variable-name-is-already-used: "その変数名は既に使われています"
|
||||
blocks:
|
||||
text: "テキスト"
|
||||
section: "セクション"
|
||||
image: "画像"
|
||||
button: "ボタン"
|
||||
input: "ユーザー入力"
|
||||
_input:
|
||||
name: "変数名"
|
||||
text: "タイトル"
|
||||
default: "デフォルト値"
|
||||
inputType: "入力の種類"
|
||||
_inputType:
|
||||
string: "テキスト"
|
||||
number: "数値"
|
||||
switch: "スイッチ"
|
||||
_switch:
|
||||
name: "変数名"
|
||||
text: "タイトル"
|
||||
default: "デフォルト値"
|
||||
_button:
|
||||
text: "タイトル"
|
||||
action: "ボタンを押したときの動作"
|
||||
_action:
|
||||
dialog: "ダイアログを表示する"
|
||||
_dialog:
|
||||
content: "内容"
|
||||
resetRandom: "乱数をリセット"
|
||||
script:
|
||||
categories:
|
||||
flow: "制御"
|
||||
logical: "論理演算"
|
||||
operation: "計算"
|
||||
comparison: "比較"
|
||||
random: "ランダム"
|
||||
value: "値"
|
||||
fn: "関数"
|
||||
blocks:
|
||||
text: "テキスト"
|
||||
multiLineText: "テキスト(複数行)"
|
||||
textList: "テキストのリスト"
|
||||
add: "+ 足す"
|
||||
_add:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
subtract: "- 引く"
|
||||
_subtract:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
multiply: "× 掛ける"
|
||||
_multiply:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
divide: "÷ 割る"
|
||||
_divide:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
eq: "AとBが同じ"
|
||||
_eq:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
notEq: "AとBが異なる"
|
||||
_notEq:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
and: "AかつB"
|
||||
_and:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
or: "AまたはB"
|
||||
_or:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
lt: "< AがBより小さい"
|
||||
_lt:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
gt: "> AがBより大きい"
|
||||
_gt:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
ltEq: "<= AがBと同じか小さい"
|
||||
_ltEq:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
gtEq: ">= AがBと同じか大きい"
|
||||
_gtEq:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
if: "分岐"
|
||||
_if:
|
||||
arg1: "もし"
|
||||
arg2: "なら"
|
||||
arg3: "そうでなければ"
|
||||
not: "否定"
|
||||
_not:
|
||||
arg1: "否定"
|
||||
random: "ランダム"
|
||||
_random:
|
||||
arg1: "確率"
|
||||
rannum: "乱数"
|
||||
_rannum:
|
||||
arg1: "最小"
|
||||
arg2: "最大"
|
||||
randomPick: "リストからランダムに選択"
|
||||
_randomPick:
|
||||
arg1: "リスト"
|
||||
dailyRandom: "ランダム (ユーザーごとに日替わり)"
|
||||
_dailyRandom:
|
||||
arg1: "確率"
|
||||
dailyRannum: "乱数 (ユーザーごとに日替わり)"
|
||||
_dailyRannum:
|
||||
arg1: "最小"
|
||||
arg2: "最大"
|
||||
dailyRandomPick: "リストからランダムに選択 (ユーザーごとに日替わり)"
|
||||
_dailyRandomPick:
|
||||
arg1: "リスト"
|
||||
number: "数"
|
||||
ref: "変数"
|
||||
in: "入力"
|
||||
_in:
|
||||
arg1: "スロット番号"
|
||||
fn: "関数"
|
||||
_fn:
|
||||
arg1: "出力"
|
||||
typeError: "スロット{slot}は\"{expect}\"を受け付けますが、\"{actual}\"が入れられています!"
|
||||
thereIsEmptySlot: "スロット{slot}が空です!"
|
||||
types:
|
||||
string: "テキスト"
|
||||
number: "数値"
|
||||
boolean: "フラグ"
|
||||
array: "リスト"
|
||||
stringArray: "テキストのリスト"
|
||||
emptySlot: "空のスロット"
|
||||
enviromentVariables: "環境変数"
|
||||
pageVariables: "ページ要素"
|
||||
|
@ -478,6 +478,8 @@ common/views/components/profile-editor.vue:
|
||||
enter-password: "パスワードを入れてや"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "ユーザー"
|
||||
common/views/components/user-lists.vue:
|
||||
list-name: "リスト名"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "見てみるわ…"
|
||||
no-broadcasts: "お知らせはあらへんで"
|
||||
@ -815,8 +817,6 @@ desktop/views/components/received-follow-requests-window.vue:
|
||||
reject: "許さん"
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "リスト"
|
||||
create-list: "新しいリストを作成"
|
||||
list-name: "リスト名"
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "投稿"
|
||||
following: "フォロー"
|
||||
@ -1071,8 +1071,6 @@ mobile/views/components/drive.vue:
|
||||
file-count: "ファイル"
|
||||
nothing-in-drive: "ドライブには何もあらへんで。"
|
||||
folder-is-empty: "このフォルダ何もないわ"
|
||||
prompt: "何すんの?(数字を入れてや): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
|
||||
deletion-alert: "フォルダの削除は未実装やねん...。堪忍な!"
|
||||
folder-name: "フォルダー名"
|
||||
url-prompt: "このURLのファイルをアップロードしたいねん"
|
||||
uploading: "アップロードをリクエストしたで。アップロードが完了するまで時間がかかるかも分からん、知らんけど。"
|
||||
@ -1149,9 +1147,12 @@ mobile/views/components/ui.nav.vue:
|
||||
game: "ゲーム"
|
||||
admin: "管理"
|
||||
about: "Misskeyってなんや?"
|
||||
mobile/views/pages/drive.vue:
|
||||
contextmenu:
|
||||
upload: "ファイル上げる"
|
||||
create-folder: "フォルダー作る"
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "リスト"
|
||||
enter-list-name: "リスト名を入力してや"
|
||||
mobile/views/pages/signup.vue:
|
||||
lets-start: "📦 始めようや"
|
||||
mobile/views/pages/followers.vue:
|
||||
|
@ -667,6 +667,8 @@ common/views/components/user-list-editor.vue:
|
||||
remove-user: "이 리스트에서 제거"
|
||||
delete-are-you-sure: "리스트 \"$1\"을 삭제하시겠습니까?"
|
||||
deleted: "삭제하였습니다"
|
||||
common/views/components/user-lists.vue:
|
||||
list-name: "리스트 이름"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "확인중"
|
||||
no-broadcasts: "공지사항이 없습니다"
|
||||
@ -1015,8 +1017,6 @@ desktop/views/components/received-follow-requests-window.vue:
|
||||
reject: "거부"
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "리스트"
|
||||
create-list: "리스트 만들기"
|
||||
list-name: "리스트 이름"
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "글"
|
||||
following: "팔로잉"
|
||||
@ -1406,8 +1406,6 @@ mobile/views/components/drive.vue:
|
||||
file-count: "파일"
|
||||
nothing-in-drive: "드라이브에 아무것도 없습니다"
|
||||
folder-is-empty: "폴더가 비어있습니다"
|
||||
prompt: "무엇을 하시겠습니까? (숫자를 입력하여 주십시오): <1 → 파일 업로드 | 2 → 파일을 URL에서 업로드 | 3 → 폴더 만들기 | 4 → 이 폴더의 이름을 변경 | 5 → 현재 폴더 이동| 6 → 현재 폴더 삭제>"
|
||||
deletion-alert: "죄송합니다! 폴더 삭제는 아직 구현되지 않았습니다..."
|
||||
folder-name: "폴더 이름"
|
||||
here-is-root: "현재 경로는 루트 경로로 폴더가 아닙니다."
|
||||
url-prompt: "업로드 하려는 파일의 URL"
|
||||
@ -1485,9 +1483,12 @@ mobile/views/components/ui.nav.vue:
|
||||
game: "게임"
|
||||
admin: "관리"
|
||||
about: "Misskey에 대하여"
|
||||
mobile/views/pages/drive.vue:
|
||||
contextmenu:
|
||||
upload: "파일 업로드"
|
||||
create-folder: "폴더 만들기"
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "리스트"
|
||||
enter-list-name: "리스트명을 입력하십시오"
|
||||
mobile/views/pages/signup.vue:
|
||||
lets-start: "📦 이제 시작해도 됩니다"
|
||||
mobile/views/pages/followers.vue:
|
||||
|
@ -403,7 +403,6 @@ desktop/views/components/ui.header.search.vue:
|
||||
placeholder: "Zoeken"
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "Lijsten"
|
||||
create-list: "Lijst creëren"
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "Berichten"
|
||||
following: "Volgend"
|
||||
@ -557,6 +556,10 @@ mobile/views/components/ui.nav.vue:
|
||||
user-lists: "Lijsten"
|
||||
game: "Othello spelen"
|
||||
about: "Over Misskey"
|
||||
mobile/views/pages/drive.vue:
|
||||
contextmenu:
|
||||
upload: "Bestand uploaden"
|
||||
create-folder: "Map creëren"
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "Lijsten"
|
||||
mobile/views/pages/home.vue:
|
||||
|
@ -192,6 +192,8 @@ common/views/components/profile-editor.vue:
|
||||
user-lists: "Lister"
|
||||
common/views/components/user-list-editor.vue:
|
||||
users: "Bruker"
|
||||
common/views/components/user-lists.vue:
|
||||
list-name: "Liste navn"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "Henter"
|
||||
next: "Neste"
|
||||
@ -310,7 +312,6 @@ desktop/views/components/received-follow-requests-window.vue:
|
||||
reject: "Avslå"
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "Brukerlister"
|
||||
list-name: "Liste navn"
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "Innlegg"
|
||||
following: "Følger"
|
||||
|
@ -512,6 +512,8 @@ common/views/components/user-list-editor.vue:
|
||||
remove-user: "Usuń z tej listy"
|
||||
delete-are-you-sure: "Usunąć listę \"$1\"?"
|
||||
deleted: "Usunięto"
|
||||
common/views/components/user-lists.vue:
|
||||
list-name: "Nazwa listy"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "Sprawdzanie"
|
||||
no-broadcasts: "Brak transmisji"
|
||||
@ -834,8 +836,6 @@ desktop/views/components/received-follow-requests-window.vue:
|
||||
reject: "Odmów"
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "Listy"
|
||||
create-list: "Utwórz listę"
|
||||
list-name: "Nazwa listy"
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "Wpisy"
|
||||
following: "Śledzeni"
|
||||
@ -1026,8 +1026,6 @@ mobile/views/components/drive.vue:
|
||||
file-count: "Plik(i)"
|
||||
nothing-in-drive: "Pusto"
|
||||
folder-is-empty: "Ten katalog jest pusty"
|
||||
prompt: "Co chcesz zrobić? (wprowadź odpowiednią cyfrę): <1 → Wysłać plik | 2 → Wysłać plik z adresu URL | 3 → Utworzyć katalog | 4 → Zmienić nazwę tego katalogu | 5 → Przenieść ten katalog | 6 → Usunąć ten katalog>"
|
||||
deletion-alert: "Przepraszamy. Usuwanie katalogów nie zostało jeszcze zaimplementowane."
|
||||
folder-name: "Nazwa katalogu"
|
||||
url-prompt: "Adres URL pliku, który chcesz wysłać"
|
||||
uploading: "Rozpoczęto wysyłanie. Może to trochę potrwać."
|
||||
@ -1103,9 +1101,12 @@ mobile/views/components/ui.nav.vue:
|
||||
game: "Gry"
|
||||
admin: "Admin"
|
||||
about: "O Misskey"
|
||||
mobile/views/pages/drive.vue:
|
||||
contextmenu:
|
||||
upload: "Wyślij plik"
|
||||
create-folder: "Utwórz katalog"
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "Listy"
|
||||
enter-list-name: "Wprowadź nazwę listy"
|
||||
mobile/views/pages/signup.vue:
|
||||
lets-start: "Rozpocznijmy! 📦"
|
||||
mobile/views/pages/home.vue:
|
||||
|
@ -34,6 +34,7 @@ common:
|
||||
signup: "注册"
|
||||
signout: "退出"
|
||||
reload-to-apply-the-setting: "必须重新加载页面以应用此设置。 确实要立即重新加载吗?"
|
||||
fetching-as-ap-object: "联合查询"
|
||||
got-it: "知道了"
|
||||
customization-tips:
|
||||
title: "自定义提示"
|
||||
@ -162,7 +163,7 @@ common:
|
||||
keep-cw: "保留内容警告"
|
||||
keep-cw-desc: "在回复帖子时,如果原帖设置了内容警告,默认情况下回帖也会设置相同的内容警告。"
|
||||
i-like-sushi: "相比于布丁来说, 我更喜欢寿司。"
|
||||
show-reversi-board-labels: "在 Reversi 中显示行和列表签"
|
||||
show-reversi-board-labels: "在黑白棋中显示行和列表签"
|
||||
use-avatar-reversi-stones: "用头像作为黑白棋的棋子"
|
||||
disable-animated-mfm: "在帖子中禁用动画文本"
|
||||
disable-showing-animated-images: "不播放动画"
|
||||
@ -205,9 +206,9 @@ common:
|
||||
show-clock-on-header: "在右上角显示时钟"
|
||||
show-reply-target: "显示回复目标"
|
||||
timeline: "时间线"
|
||||
show-my-renotes: "在时间线上显示我的Renote"
|
||||
show-renoted-my-notes: "在时间线上显示我的帖子的Renote"
|
||||
show-local-renotes: "在时间线上显示本地帖子的Renote"
|
||||
show-my-renotes: "在时间线上显示我的转推"
|
||||
show-renoted-my-notes: "在时间线上显示我的帖子的转推"
|
||||
show-local-renotes: "在时间线上显示本地帖子的转推"
|
||||
remain-deleted-note: "继续显示已删除的帖子"
|
||||
sound: "声音"
|
||||
enable-sounds: "开启声音"
|
||||
@ -257,7 +258,7 @@ common:
|
||||
is-remote-user: "此用户信息可能不准确。"
|
||||
is-remote-post: "该投稿已被复制."
|
||||
view-on-remote: "查看准确的信息"
|
||||
renoted-by: "由 {user} Renote"
|
||||
renoted-by: "由 {user} 转推"
|
||||
no-notes: "没有帖子"
|
||||
turn-on-darkmode: "切换暗色主题"
|
||||
turn-off-darkmode: "切换亮色主题"
|
||||
@ -338,8 +339,8 @@ common/views/components/games/reversi/reversi.game.vue:
|
||||
surrender: "认输"
|
||||
surrendered: "已认输"
|
||||
is-llotheo: "棋子较少一方获胜(LLoTheO规则)"
|
||||
looped-map: "环形棋盘"
|
||||
can-put-everywhere: "可以下在任意放置"
|
||||
looped-map: "循环棋盘"
|
||||
can-put-everywhere: "可以下在任意位置"
|
||||
common/views/components/games/reversi/reversi.index.vue:
|
||||
title: "Misskey 黑白棋"
|
||||
sub-title: "和其他人一起来玩Misskey黑白棋"
|
||||
@ -363,8 +364,8 @@ common/views/components/games/reversi/reversi.room.vue:
|
||||
black-is: "{}是黑"
|
||||
rules: "规则"
|
||||
is-llotheo: "棋子较少一方获胜(LLoTheO规则)"
|
||||
looped-map: "环形棋盘"
|
||||
can-put-everywhere: "可以下在任意放置"
|
||||
looped-map: "循环棋盘"
|
||||
can-put-everywhere: "可以下在任意位置"
|
||||
settings-of-the-bot: "机器人设定"
|
||||
this-game-is-started-soon: "游戏即将在数秒后开始"
|
||||
waiting-for-other: "等待对手准备"
|
||||
@ -480,8 +481,8 @@ common/views/components/note-menu.vue:
|
||||
unfavorite: "取消收藏"
|
||||
watch: "关注"
|
||||
unwatch: "取消关注"
|
||||
pin: "固定个人资料"
|
||||
unpin: "解除固定"
|
||||
pin: "置顶"
|
||||
unpin: "取消置顶"
|
||||
delete: "删除"
|
||||
delete-confirm: "确定删除这个投稿吗?"
|
||||
remote: "显示原始投稿"
|
||||
@ -489,8 +490,12 @@ common/views/components/user-menu.vue:
|
||||
mention: "提到"
|
||||
mute: "屏蔽"
|
||||
unmute: "解除屏蔽"
|
||||
mute-confirm: "屏蔽此用户?"
|
||||
unmute-confirm: "取消屏蔽用户?"
|
||||
block: "拉黑"
|
||||
unblock: "取消拉黑"
|
||||
block-confirm: "确定拉黑此用户?"
|
||||
unblock-confirm: "取消拉黑此用户?"
|
||||
push-to-list: "添加至列表"
|
||||
select-list: "请选择一个列表"
|
||||
report-abuse: "举报骚扰"
|
||||
@ -498,8 +503,12 @@ common/views/components/user-menu.vue:
|
||||
report-abuse-reported: "已报告给管理员。 非常感谢你的合作。"
|
||||
silence: "禁言"
|
||||
unsilence: "解除禁言"
|
||||
silence-confirm: "确认屏蔽此用户?"
|
||||
unsilence-confirm: "取消屏蔽此用户?"
|
||||
suspend: "冻结"
|
||||
unsuspend: "解除冻结"
|
||||
suspend-confirm: "确认冻结此用户?"
|
||||
unsuspend-confirm: "确认解冻此用户?"
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "为\"{}\"投票"
|
||||
vote-count: "{}票"
|
||||
@ -682,6 +691,9 @@ common/views/components/user-list-editor.vue:
|
||||
remove-user: "从此列表中删除"
|
||||
delete-are-you-sure: "删除列表“$1”?"
|
||||
deleted: "已删除"
|
||||
common/views/components/user-lists.vue:
|
||||
create-list: "创建列表"
|
||||
list-name: "列表名称"
|
||||
common/views/widgets/broadcast.vue:
|
||||
fetching: "确认中"
|
||||
no-broadcasts: "没有公告"
|
||||
@ -724,7 +736,7 @@ common/views/widgets/tips.vue:
|
||||
tips-line8: "可以从设置中定制主页。"
|
||||
tips-line9: "Misskey 根据 AGPLv3 获得许可。"
|
||||
tips-line10: "使用Time Machine(时光机)小部件可以轻松追溯到过去的时间轴。"
|
||||
tips-line11: "您可以点击“...”将帖子固定到用户页面"
|
||||
tips-line11: "您可以点击“...”将帖子置顶到用户页面"
|
||||
tips-line13: "附在帖子上的所有文件都会保存到网盘中。"
|
||||
tips-line14: "在自定义首页布局时,您可以右键单击窗口小部件以更改其设计。"
|
||||
tips-line17: "用“**”围绕文本将突出显示它。"
|
||||
@ -759,7 +771,7 @@ desktop/views/components/activity.chart.vue:
|
||||
total: "黑 ... 总计"
|
||||
notes: "蓝 ... 投稿"
|
||||
replies: "红 ... 回复"
|
||||
renotes: "绿 ... 转发"
|
||||
renotes: "绿 ... 转推"
|
||||
desktop/views/components/activity.vue:
|
||||
title: "活动"
|
||||
toggle: "切换显示"
|
||||
@ -856,12 +868,12 @@ desktop/views/components/note-detail.vue:
|
||||
private: "私密投稿"
|
||||
deleted: "投稿已删除"
|
||||
location: "位置信息"
|
||||
renote: "转发"
|
||||
renote: "转推"
|
||||
add-reaction: "回应"
|
||||
undo-reaction: "取消回应"
|
||||
desktop/views/components/note.vue:
|
||||
reply: "回复"
|
||||
renote: "Renote"
|
||||
renote: "转推"
|
||||
add-reaction: "回应"
|
||||
undo-reaction: "取消回应"
|
||||
detail: "详细信息"
|
||||
@ -880,13 +892,13 @@ desktop/views/components/post-form.vue:
|
||||
quote-placeholder: "引用这个帖子…"
|
||||
submit: "投稿"
|
||||
reply: "回复"
|
||||
renote: "转发"
|
||||
renote: "转推"
|
||||
posted: "已发送投稿!"
|
||||
replied: "已回复!"
|
||||
reposted: "已转发!"
|
||||
reposted: "已转推!"
|
||||
note-failed: "发帖失败"
|
||||
reply-failed: "回复失败"
|
||||
renote-failed: "转发失败"
|
||||
renote-failed: "转推失败"
|
||||
posting: "发送中"
|
||||
attach-media-from-local: "从设备中添加媒体文件"
|
||||
attach-media-from-drive: "从网盘中添加媒体文件"
|
||||
@ -911,13 +923,13 @@ desktop/views/components/progress-dialog.vue:
|
||||
desktop/views/components/renote-form.vue:
|
||||
quote: "引用..."
|
||||
cancel: "取消"
|
||||
renote: "重新发送"
|
||||
renote-home: "重新发送(首页)"
|
||||
renote: "转推"
|
||||
renote-home: "转推(首页)"
|
||||
reposting: "重新发送中..."
|
||||
success: "已重新发送!"
|
||||
failure: "重新发送失败"
|
||||
success: "已转推!"
|
||||
failure: "转推失败"
|
||||
desktop/views/components/renote-form-window.vue:
|
||||
title: "您是否要重新发送?"
|
||||
title: "您是否要转推?"
|
||||
desktop/views/pages/user-following-or-followers.vue:
|
||||
following: "{user}的正在关注"
|
||||
followers: "{user}的关注者"
|
||||
@ -967,7 +979,7 @@ common/views/components/drive-settings.vue:
|
||||
common/views/components/mute-and-block.vue:
|
||||
mute-and-block: "屏蔽/拉黑"
|
||||
mute: "屏蔽"
|
||||
block: "拉黑中"
|
||||
block: "拉黑"
|
||||
no-muted-users: "无屏蔽用户"
|
||||
no-blocked-users: "无拉黑的用户"
|
||||
word-mute: "文字屏蔽"
|
||||
@ -1030,8 +1042,6 @@ desktop/views/components/received-follow-requests-window.vue:
|
||||
reject: "拒绝"
|
||||
desktop/views/components/user-lists-window.vue:
|
||||
title: "用户列表"
|
||||
create-list: "创建列表"
|
||||
list-name: "列表名称"
|
||||
desktop/views/components/user-preview.vue:
|
||||
notes: "帖子"
|
||||
following: "关注中"
|
||||
@ -1209,7 +1219,9 @@ admin/views/users.vue:
|
||||
unsuspend-confirm: "是否解除冻结?"
|
||||
unsuspended: "已成功解除用户冻结"
|
||||
make-silence: "禁言"
|
||||
silence-confirm: "确认屏蔽?"
|
||||
unmake-silence: "解除禁言"
|
||||
unsilence-confirm: "解除屏蔽?"
|
||||
verify: "认证用户"
|
||||
verify-confirm: "是否官方账号?"
|
||||
verified: "此账户已被认证"
|
||||
@ -1422,12 +1434,11 @@ mobile/views/components/drive.vue:
|
||||
file-count: "文件"
|
||||
nothing-in-drive: "网盘为空"
|
||||
folder-is-empty: "这文件夹是空的"
|
||||
prompt: "您想要干什么呢?(请输入数字):<1 → 上传文件 | 2 → 从URL上传文件 | 3 → 创建新文件夹 | 4 → 更改这个文件夹的名称 | 5 → 移动这个文件夹 | 6 → 删除这个文件夹>"
|
||||
deletion-alert: "抱歉! 删除文件夹功能尚未实现。"
|
||||
folder-name: "文件夹名称"
|
||||
here-is-root: "当前位置为根目录。"
|
||||
url-prompt: "要上传的文件的URL"
|
||||
uploading: "已请求上传。 上传完成可能需要一段时间。"
|
||||
folder-name-cannot-empty: "文件夹名不能为空。"
|
||||
mobile/views/components/drive-file-chooser.vue:
|
||||
select-file: "选择文件"
|
||||
mobile/views/components/drive-folder-chooser.vue:
|
||||
@ -1476,7 +1487,7 @@ mobile/views/components/post-form.vue:
|
||||
add-visible-user: "添加用户"
|
||||
submit: "帖子"
|
||||
reply: "回复"
|
||||
renote: "Renote"
|
||||
renote: "转推"
|
||||
quote-placeholder: "引用这个帖子t... (可选)"
|
||||
reply-placeholder: "回复这个帖子"
|
||||
cw-placeholder: "评论帖子(可选)"
|
||||
@ -1501,9 +1512,16 @@ mobile/views/components/ui.nav.vue:
|
||||
game: "游戏"
|
||||
admin: "管理"
|
||||
about: "关于 Misskey"
|
||||
mobile/views/pages/drive.vue:
|
||||
contextmenu:
|
||||
upload: "上传文件"
|
||||
url-upload: "从URL上传文件"
|
||||
create-folder: "创建文件夹"
|
||||
rename-folder: "重命名文件夹"
|
||||
move-folder: "移动此文件夹"
|
||||
delete-folder: "删除此文件夹"
|
||||
mobile/views/pages/user-lists.vue:
|
||||
title: "列表"
|
||||
enter-list-name: "输入要列出的列表的名称"
|
||||
mobile/views/pages/signup.vue:
|
||||
lets-start: "您的账户现已准备就绪! 📦"
|
||||
mobile/views/pages/followers.vue:
|
||||
@ -1600,7 +1618,7 @@ deck/deck.user-column.vue:
|
||||
images: "图片"
|
||||
activity: "活动"
|
||||
timeline: "时间线"
|
||||
pinned-notes: "标记投稿"
|
||||
pinned-notes: "置顶帖"
|
||||
docs:
|
||||
edit-this-page-on-github: "发现错误或想要为文档做出贡献?"
|
||||
edit-this-page-on-github-link: "在GitHub上编辑这个页面。"
|
||||
|
31
migration/1556348509290-Pages.ts
Normal file
31
migration/1556348509290-Pages.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class Pages1556348509290 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`CREATE TYPE "page_visibility_enum" AS ENUM('public', 'followers', 'specified')`);
|
||||
await queryRunner.query(`CREATE TABLE "page" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "name" character varying(256) NOT NULL, "summary" character varying(256), "alignCenter" boolean NOT NULL, "font" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "eyeCatchingImageId" character varying(32), "content" jsonb NOT NULL DEFAULT '[]', "variables" jsonb NOT NULL DEFAULT '[]', "visibility" "page_visibility_enum" NOT NULL, "visibleUserIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_742f4117e065c5b6ad21b37ba1f" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_fbb4297c927a9b85e9cefa2eb1" ON "page" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_af639b066dfbca78b01a920f8a" ON "page" ("updatedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_b82c19c08afb292de4600d99e4" ON "page" ("name") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_ae1d917992dd0c9d9bbdad06c4" ON "page" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_90148bbc2bf0854428786bfc15" ON "page" ("visibleUserIds") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2133ef8317e4bdb839c0dcbf13" ON "page" ("userId", "name") `);
|
||||
await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" FOREIGN KEY ("eyeCatchingImageId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10"`);
|
||||
await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_2133ef8317e4bdb839c0dcbf13"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_90148bbc2bf0854428786bfc15"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_ae1d917992dd0c9d9bbdad06c4"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_b82c19c08afb292de4600d99e4"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_af639b066dfbca78b01a920f8a"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_fbb4297c927a9b85e9cefa2eb1"`);
|
||||
await queryRunner.query(`DROP TABLE "page"`);
|
||||
await queryRunner.query(`DROP TYPE "page_visibility_enum"`);
|
||||
}
|
||||
|
||||
}
|
33
package.json
33
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "11.2.1",
|
||||
"version": "11.5.0",
|
||||
"codename": "daybreak",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -23,20 +23,19 @@
|
||||
"format": "gulp format"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "7.0.0-rc.2",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.15",
|
||||
"@fortawesome/free-brands-svg-icons": "5.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "5.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.7.2",
|
||||
"@fortawesome/vue-fontawesome": "0.1.5",
|
||||
"@koa/cors": "2.2.3",
|
||||
"@prezzemolo/rap": "0.1.2",
|
||||
"@prezzemolo/zip": "0.0.3",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.5.11",
|
||||
"@types/dateformat": "3.0.0",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
"@types/double-ended-queue": "2.1.0",
|
||||
"@types/elasticsearch": "5.0.32",
|
||||
"@types/file-type": "10.9.1",
|
||||
"@types/gulp": "4.0.6",
|
||||
"@types/gulp-mocha": "0.0.32",
|
||||
@ -94,13 +93,13 @@
|
||||
"@types/websocket": "0.0.40",
|
||||
"@types/ws": "6.0.1",
|
||||
"animejs": "3.0.1",
|
||||
"apexcharts": "3.6.7",
|
||||
"apexcharts": "3.6.9",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "4.0.2",
|
||||
"autwh": "0.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bootstrap-vue": "2.0.0-rc.13",
|
||||
"bull": "3.7.0",
|
||||
"bull": "3.8.1",
|
||||
"cafy": "15.1.1",
|
||||
"chai": "4.2.0",
|
||||
"chalk": "2.4.2",
|
||||
@ -112,9 +111,8 @@
|
||||
"cssnano": "4.1.10",
|
||||
"dateformat": "3.0.3",
|
||||
"deep-equal": "1.0.1",
|
||||
"diskusage": "1.0.0",
|
||||
"diskusage": "1.1.0",
|
||||
"double-ended-queue": "2.1.0-0",
|
||||
"elasticsearch": "15.4.1",
|
||||
"emojilib": "2.4.0",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-plugin-vue": "5.2.2",
|
||||
@ -122,7 +120,7 @@
|
||||
"feed": "2.0.4",
|
||||
"file-type": "10.11.0",
|
||||
"fuckadblock": "3.2.1",
|
||||
"gulp": "4.0.0",
|
||||
"gulp": "4.0.1",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-imagemin": "5.0.3",
|
||||
"gulp-mocha": "6.0.0",
|
||||
@ -138,10 +136,10 @@
|
||||
"html-minifier": "4.0.0",
|
||||
"http-signature": "1.2.0",
|
||||
"insert-text-at-cursor": "0.2.0",
|
||||
"is-root": "2.0.0",
|
||||
"is-root": "2.1.0",
|
||||
"is-svg": "4.1.0",
|
||||
"js-yaml": "3.13.1",
|
||||
"jsdom": "14.0.0",
|
||||
"jsdom": "15.0.0",
|
||||
"json5": "2.1.0",
|
||||
"json5-loader": "2.0.0",
|
||||
"katex": "0.10.1",
|
||||
@ -161,20 +159,20 @@
|
||||
"loader-utils": "1.2.3",
|
||||
"lolex": "3.1.0",
|
||||
"lookup-dns-cache": "2.1.0",
|
||||
"minio": "7.0.5",
|
||||
"minio": "7.0.7",
|
||||
"mocha": "6.1.3",
|
||||
"moji": "0.5.1",
|
||||
"moment": "2.24.0",
|
||||
"ms": "2.1.1",
|
||||
"nested-property": "0.0.7",
|
||||
"node-fetch": "2.3.0",
|
||||
"nodemailer": "6.1.0",
|
||||
"nodemailer": "6.1.1",
|
||||
"nprogress": "0.2.0",
|
||||
"object-assign-deep": "0.4.0",
|
||||
"os-utils": "0.0.14",
|
||||
"parse5": "5.1.0",
|
||||
"parsimmon": "1.12.0",
|
||||
"pg": "7.9.0",
|
||||
"pg": "7.10.0",
|
||||
"portscanner": "2.2.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"prismjs": "1.16.0",
|
||||
@ -201,7 +199,8 @@
|
||||
"rimraf": "2.6.3",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sharp": "0.22.0",
|
||||
"seedrandom": "3.0.1",
|
||||
"sharp": "0.22.1",
|
||||
"showdown": "1.9.0",
|
||||
"showdown-highlightjs-extension": "0.1.2",
|
||||
"speakeasy": "2.0.0",
|
||||
@ -210,7 +209,7 @@
|
||||
"stylus": "0.54.5",
|
||||
"stylus-loader": "3.0.2",
|
||||
"summaly": "2.2.0",
|
||||
"systeminformation": "4.1.4",
|
||||
"systeminformation": "4.1.6",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"terser-webpack-plugin": "1.2.3",
|
||||
"textarea-caret": "3.1.0",
|
||||
@ -234,13 +233,13 @@
|
||||
"vue-color": "2.7.0",
|
||||
"vue-content-loading": "1.6.0",
|
||||
"vue-cropperjs": "3.0.0",
|
||||
"vue-i18n": "8.10.0",
|
||||
"vue-i18n": "8.11.1",
|
||||
"vue-js-modal": "1.3.28",
|
||||
"vue-json-pretty": "1.6.0",
|
||||
"vue-loader": "15.7.0",
|
||||
"vue-marquee-text-component": "1.1.1",
|
||||
"vue-prism-component": "1.1.1",
|
||||
"vue-router": "3.0.4",
|
||||
"vue-router": "3.0.6",
|
||||
"vue-sequential-entrance": "1.1.3",
|
||||
"vue-style-loader": "4.1.2",
|
||||
"vue-svg-inline-loader": "1.2.15",
|
||||
|
@ -27,7 +27,7 @@ function greet() {
|
||||
console.log(' ' + chalk.gray(v) + (' |___|\n'.substr(v.length)));
|
||||
//#endregion
|
||||
|
||||
console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.');
|
||||
console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, @acid-chicken, and @rinsuki.');
|
||||
console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo'));
|
||||
|
||||
console.log('');
|
||||
|
@ -9,6 +9,9 @@
|
||||
<ui-textarea v-model="announcement.text">
|
||||
<span>{{ $t('text') }}</span>
|
||||
</ui-textarea>
|
||||
<ui-input v-model="announcement.image">
|
||||
<span>{{ $t('image-url') }}</span>
|
||||
</ui-input>
|
||||
<ui-horizon-group class="fit-bottom">
|
||||
<ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button>
|
||||
<ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button>
|
||||
@ -43,7 +46,8 @@ export default Vue.extend({
|
||||
add() {
|
||||
this.announcements.unshift({
|
||||
title: '',
|
||||
text: ''
|
||||
text: '',
|
||||
image: null
|
||||
});
|
||||
},
|
||||
|
||||
|
470
src/client/app/common/scripts/aiscript.ts
Normal file
470
src/client/app/common/scripts/aiscript.ts
Normal file
@ -0,0 +1,470 @@
|
||||
/**
|
||||
* AiScript
|
||||
* evaluator & type checker
|
||||
*/
|
||||
|
||||
import autobind from 'autobind-decorator';
|
||||
import * as seedrandom from 'seedrandom';
|
||||
|
||||
import {
|
||||
faSuperscript,
|
||||
faAlignLeft,
|
||||
faShareAlt,
|
||||
faSquareRootAlt,
|
||||
faPlus,
|
||||
faMinus,
|
||||
faTimes,
|
||||
faDivide,
|
||||
faList,
|
||||
faQuoteRight,
|
||||
faEquals,
|
||||
faGreaterThan,
|
||||
faLessThan,
|
||||
faGreaterThanEqual,
|
||||
faLessThanEqual,
|
||||
faExclamation,
|
||||
faNotEqual,
|
||||
faDice,
|
||||
faSortNumericUp,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faFlag } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
import { version } from '../../config';
|
||||
|
||||
export type Block = {
|
||||
id: string;
|
||||
type: string;
|
||||
args: Block[];
|
||||
value: any;
|
||||
};
|
||||
|
||||
export type Variable = Block & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
type Type = 'string' | 'number' | 'boolean' | 'stringArray';
|
||||
|
||||
type TypeError = {
|
||||
arg: number;
|
||||
expect: Type;
|
||||
actual: Type;
|
||||
};
|
||||
|
||||
const funcDefs = {
|
||||
if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: faShareAlt, },
|
||||
not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
|
||||
or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
|
||||
and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: faFlag, },
|
||||
add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faPlus, },
|
||||
subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faMinus, },
|
||||
multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faTimes, },
|
||||
divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: faDivide, },
|
||||
eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faEquals, },
|
||||
notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: faNotEqual, },
|
||||
gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThan, },
|
||||
lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faLessThan, },
|
||||
gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faGreaterThanEqual, },
|
||||
ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: faLessThanEqual, },
|
||||
rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, },
|
||||
random: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, },
|
||||
randomPick: { in: [0], out: 0, category: 'random', icon: faDice, },
|
||||
dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: faDice, },
|
||||
dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: faDice, },
|
||||
dailyRandomPick: { in: [0], out: 0, category: 'random', icon: faDice, },
|
||||
};
|
||||
|
||||
const blockDefs = [
|
||||
{ type: 'text', out: 'string', category: 'value', icon: faQuoteRight, },
|
||||
{ type: 'multiLineText', out: 'string', category: 'value', icon: faAlignLeft, },
|
||||
{ type: 'textList', out: 'stringArray', category: 'value', icon: faList, },
|
||||
{ type: 'number', out: 'number', category: 'value', icon: faSortNumericUp, },
|
||||
{ type: 'ref', out: null, category: 'value', icon: faSuperscript, },
|
||||
{ type: 'in', out: null, category: 'value', icon: faSuperscript, },
|
||||
{ type: 'fn', out: 'function', category: 'value', icon: faSuperscript, },
|
||||
...Object.entries(funcDefs).map(([k, v]) => ({
|
||||
type: k, out: v.out || null, category: v.category, icon: v.icon
|
||||
}))
|
||||
];
|
||||
|
||||
type PageVar = { name: string; value: any; type: Type; };
|
||||
|
||||
const envVarsDef = {
|
||||
AI: 'string',
|
||||
VERSION: 'string',
|
||||
LOGIN: 'boolean',
|
||||
NAME: 'string',
|
||||
USERNAME: 'string',
|
||||
USERID: 'string',
|
||||
NOTES_COUNT: 'number',
|
||||
FOLLOWERS_COUNT: 'number',
|
||||
FOLLOWING_COUNT: 'number',
|
||||
IS_CAT: 'boolean',
|
||||
MY_NOTES_COUNT: 'number',
|
||||
MY_FOLLOWERS_COUNT: 'number',
|
||||
MY_FOLLOWING_COUNT: 'number',
|
||||
};
|
||||
|
||||
export class AiScript {
|
||||
private variables: Variable[];
|
||||
private pageVars: PageVar[];
|
||||
private envVars: Record<keyof typeof envVarsDef, any>;
|
||||
|
||||
public static envVarsDef = envVarsDef;
|
||||
public static blockDefs = blockDefs;
|
||||
public static funcDefs = funcDefs;
|
||||
private opts: {
|
||||
randomSeed?: string; user?: any; visitor?: any;
|
||||
};
|
||||
|
||||
constructor(variables: Variable[] = [], pageVars: PageVar[] = [], opts: AiScript['opts'] = {}) {
|
||||
this.variables = variables;
|
||||
this.pageVars = pageVars;
|
||||
this.opts = opts;
|
||||
|
||||
this.envVars = {
|
||||
AI: 'kawaii',
|
||||
VERSION: version,
|
||||
LOGIN: opts.visitor != null,
|
||||
NAME: opts.visitor ? opts.visitor.name : '',
|
||||
USERNAME: opts.visitor ? opts.visitor.username : '',
|
||||
USERID: opts.visitor ? opts.visitor.id : '',
|
||||
NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0,
|
||||
FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0,
|
||||
FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0,
|
||||
IS_CAT: opts.visitor ? opts.visitor.isCat : false,
|
||||
MY_NOTES_COUNT: opts.user ? opts.user.notesCount : 0,
|
||||
MY_FOLLOWERS_COUNT: opts.user ? opts.user.followersCount : 0,
|
||||
MY_FOLLOWING_COUNT: opts.user ? opts.user.followingCount : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
public injectVars(vars: Variable[]) {
|
||||
this.variables = vars;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public injectPageVars(pageVars: PageVar[]) {
|
||||
this.pageVars = pageVars;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public updatePageVar(name: string, value: any) {
|
||||
this.pageVars.find(v => v.name === name).value = value;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public updateRandomSeed(seed: string) {
|
||||
this.opts.randomSeed = seed;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public static isLiteralBlock(v: Block) {
|
||||
if (v.type === null) return true;
|
||||
if (v.type === 'text') return true;
|
||||
if (v.type === 'multiLineText') return true;
|
||||
if (v.type === 'textList') return true;
|
||||
if (v.type === 'number') return true;
|
||||
if (v.type === 'ref') return true;
|
||||
if (v.type === 'fn') return true;
|
||||
if (v.type === 'in') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public typeCheck(v: Block): TypeError | null {
|
||||
if (AiScript.isLiteralBlock(v)) return null;
|
||||
|
||||
const def = AiScript.funcDefs[v.type];
|
||||
if (def == null) {
|
||||
throw new Error('Unknown type: ' + v.type);
|
||||
}
|
||||
|
||||
const generic: Type[] = [];
|
||||
|
||||
for (let i = 0; i < def.in.length; i++) {
|
||||
const arg = def.in[i];
|
||||
const type = this.typeInference(v.args[i]);
|
||||
if (type === null) continue;
|
||||
|
||||
if (typeof arg === 'number') {
|
||||
if (generic[arg] === undefined) {
|
||||
generic[arg] = type;
|
||||
} else if (type !== generic[arg]) {
|
||||
return {
|
||||
arg: i,
|
||||
expect: generic[arg],
|
||||
actual: type
|
||||
};
|
||||
}
|
||||
} else if (type !== arg) {
|
||||
return {
|
||||
arg: i,
|
||||
expect: arg,
|
||||
actual: type
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getExpectedType(v: Block, slot: number): Type | null {
|
||||
const def = AiScript.funcDefs[v.type];
|
||||
if (def == null) {
|
||||
throw new Error('Unknown type: ' + v.type);
|
||||
}
|
||||
|
||||
const generic: Type[] = [];
|
||||
|
||||
for (let i = 0; i < def.in.length; i++) {
|
||||
const arg = def.in[i];
|
||||
const type = this.typeInference(v.args[i]);
|
||||
if (type === null) continue;
|
||||
|
||||
if (typeof arg === 'number') {
|
||||
if (generic[arg] === undefined) {
|
||||
generic[arg] = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof def.in[slot] === 'number') {
|
||||
return generic[def.in[slot]] || null;
|
||||
} else {
|
||||
return def.in[slot];
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public typeInference(v: Block): Type | null {
|
||||
if (v.type === null) return null;
|
||||
if (v.type === 'text') return 'string';
|
||||
if (v.type === 'multiLineText') return 'string';
|
||||
if (v.type === 'textList') return 'stringArray';
|
||||
if (v.type === 'number') return 'number';
|
||||
if (v.type === 'ref') {
|
||||
const variable = this.variables.find(va => va.name === v.value);
|
||||
if (variable) {
|
||||
return this.typeInference(variable);
|
||||
}
|
||||
|
||||
const pageVar = this.pageVars.find(va => va.name === v.value);
|
||||
if (pageVar) {
|
||||
return pageVar.type;
|
||||
}
|
||||
|
||||
const envVar = AiScript.envVarsDef[v.value];
|
||||
if (envVar) {
|
||||
return envVar;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
if (v.type === 'fn') return null; // todo
|
||||
if (v.type === 'in') return null; // todo
|
||||
|
||||
const generic: Type[] = [];
|
||||
|
||||
const def = AiScript.funcDefs[v.type];
|
||||
|
||||
for (let i = 0; i < def.in.length; i++) {
|
||||
const arg = def.in[i];
|
||||
if (typeof arg === 'number') {
|
||||
const type = this.typeInference(v.args[i]);
|
||||
|
||||
if (generic[arg] === undefined) {
|
||||
generic[arg] = type;
|
||||
} else {
|
||||
if (type !== generic[arg]) {
|
||||
generic[arg] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof def.out === 'number') {
|
||||
return generic[def.out];
|
||||
} else {
|
||||
return def.out;
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getVarsByType(type: Type | null): Variable[] {
|
||||
if (type == null) return this.variables;
|
||||
return this.variables.filter(x => (this.typeInference(x) === null) || (this.typeInference(x) === type));
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getVarByName(name: string): Variable {
|
||||
return this.variables.find(x => x.name === name);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getEnvVarsByType(type: Type | null): string[] {
|
||||
if (type == null) return Object.keys(AiScript.envVarsDef);
|
||||
return Object.entries(AiScript.envVarsDef).filter(([k, v]) => type === v).map(([k, v]) => k);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public getPageVarsByType(type: Type | null): string[] {
|
||||
if (type == null) return this.pageVars.map(v => v.name);
|
||||
return this.pageVars.filter(v => type === v.type).map(v => v.name);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private interpolate(str: string, values: { name: string, value: any }[]) {
|
||||
return str.replace(/\{(.+?)\}/g, match =>
|
||||
(this.getVariableValue(match.slice(1, -1).trim(), values) || '').toString());
|
||||
}
|
||||
|
||||
@autobind
|
||||
public evaluateVars() {
|
||||
const values: { name: string, value: any }[] = [];
|
||||
|
||||
for (const v of this.variables) {
|
||||
values.push({
|
||||
name: v.name,
|
||||
value: this.evaluate(v, values)
|
||||
});
|
||||
}
|
||||
|
||||
for (const v of this.pageVars) {
|
||||
values.push({
|
||||
name: v.name,
|
||||
value: v.value
|
||||
});
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(this.envVars)) {
|
||||
values.push({
|
||||
name: k,
|
||||
value: v
|
||||
});
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
@autobind
|
||||
private evaluate(block: Block, values: { name: string, value: any }[], slotArg: Record<string, any> = {}): any {
|
||||
if (block.type === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (block.type === 'number') {
|
||||
return parseInt(block.value, 10);
|
||||
}
|
||||
|
||||
if (block.type === 'text' || block.type === 'multiLineText') {
|
||||
return this.interpolate(block.value, values);
|
||||
}
|
||||
|
||||
if (block.type === 'textList') {
|
||||
return block.value.trim().split('\n');
|
||||
}
|
||||
|
||||
if (block.type === 'ref') {
|
||||
return this.getVariableValue(block.value, values);
|
||||
}
|
||||
|
||||
if (block.type === 'in') {
|
||||
return slotArg[block.value];
|
||||
}
|
||||
|
||||
if (block.type === 'fn') { // ユーザー関数定義
|
||||
return {
|
||||
slots: block.value.slots,
|
||||
exec: slotArg => this.evaluate(block.value.expression, values, slotArg)
|
||||
};
|
||||
}
|
||||
|
||||
if (block.type.startsWith('fn:')) { // ユーザー関数呼び出し
|
||||
const fnName = block.type.split(':')[1];
|
||||
const fn = this.getVariableValue(fnName, values);
|
||||
for (let i = 0; i < fn.slots.length; i++) {
|
||||
const name = fn.slots[i];
|
||||
slotArg[name] = this.evaluate(block.args[i], values);
|
||||
}
|
||||
return fn.exec(slotArg);
|
||||
}
|
||||
|
||||
if (block.args === undefined) return null;
|
||||
|
||||
const date = new Date();
|
||||
const day = `${this.opts.visitor ? this.opts.visitor.id : ''} ${date.getFullYear()}/${date.getMonth()}/${date.getDate()}`;
|
||||
|
||||
const funcs: { [p in keyof typeof funcDefs]: any } = {
|
||||
not: (a) => !a,
|
||||
eq: (a, b) => a === b,
|
||||
notEq: (a, b) => a !== b,
|
||||
gt: (a, b) => a > b,
|
||||
lt: (a, b) => a < b,
|
||||
gtEq: (a, b) => a >= b,
|
||||
ltEq: (a, b) => a <= b,
|
||||
or: (a, b) => a || b,
|
||||
and: (a, b) => a && b,
|
||||
if: (bool, a, b) => bool ? a : b,
|
||||
add: (a, b) => a + b,
|
||||
subtract: (a, b) => a - b,
|
||||
multiply: (a, b) => a * b,
|
||||
divide: (a, b) => a / b,
|
||||
random: (probability) => Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * 100) < probability,
|
||||
rannum: (min, max) => min + Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * (max - min + 1)),
|
||||
randomPick: (list) => list[Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * list.length)],
|
||||
dailyRandom: (probability) => Math.floor(seedrandom(`${day}:${block.id}`)() * 100) < probability,
|
||||
dailyRannum: (min, max) => min + Math.floor(seedrandom(`${day}:${block.id}`)() * (max - min + 1)),
|
||||
dailyRandomPick: (list) => list[Math.floor(seedrandom(`${day}:${block.id}`)() * list.length)],
|
||||
};
|
||||
|
||||
const fnName = block.type;
|
||||
|
||||
const fn = funcs[fnName];
|
||||
if (fn == null) {
|
||||
console.error('Unknown function: ' + fnName);
|
||||
throw new Error('Unknown function: ' + fnName);
|
||||
}
|
||||
|
||||
const args = block.args.map(x => this.evaluate(x, values, slotArg));
|
||||
|
||||
return fn(...args);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private getVariableValue(name: string, values: { name: string, value: any }[]): any {
|
||||
const v = values.find(v => v.name === name);
|
||||
if (v) {
|
||||
return v.value;
|
||||
}
|
||||
|
||||
const pageVar = this.pageVars.find(v => v.name === name);
|
||||
if (pageVar) {
|
||||
return pageVar.value;
|
||||
}
|
||||
|
||||
if (AiScript.envVarsDef[name]) {
|
||||
return this.envVars[name].value;
|
||||
}
|
||||
|
||||
throw new Error(`Script: No such variable '${name}'`);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public isUsedName(name: string) {
|
||||
if (this.variables.some(v => v.name === name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.pageVars.some(v => v.name === name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (AiScript.envVarsDef[name]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
24
src/client/app/common/scripts/collect-page-vars.ts
Normal file
24
src/client/app/common/scripts/collect-page-vars.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export function collectPageVars(content) {
|
||||
const pageVars = [];
|
||||
const collect = (xs: any[]) => {
|
||||
for (const x of xs) {
|
||||
if (x.type === 'input') {
|
||||
pageVars.push({
|
||||
name: x.name,
|
||||
type: x.inputType,
|
||||
value: x.default
|
||||
});
|
||||
} else if (x.type === 'switch') {
|
||||
pageVars.push({
|
||||
name: x.name,
|
||||
type: 'boolean',
|
||||
value: x.default
|
||||
});
|
||||
} else if (x.children) {
|
||||
collect(x.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
collect(content);
|
||||
return pageVars;
|
||||
}
|
31
src/client/app/common/scripts/gen-search-query.ts
Normal file
31
src/client/app/common/scripts/gen-search-query.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import parseAcct from '../../../../misc/acct/parse';
|
||||
import { host as localHost } from '../../config';
|
||||
|
||||
export async function genSearchQuery(v: any, q: string) {
|
||||
let host: string;
|
||||
let userId: string;
|
||||
if (q.split(' ').some(x => x.startsWith('@'))) {
|
||||
for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) {
|
||||
if (at.includes('.')) {
|
||||
if (at === localHost || at === '.') {
|
||||
host = null;
|
||||
} else {
|
||||
host = at;
|
||||
}
|
||||
} else {
|
||||
const user = await v.$root.api('users/show', parseAcct(at)).catch(x => null);
|
||||
if (user) {
|
||||
userId = user.id;
|
||||
} else {
|
||||
// todo: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return {
|
||||
query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '),
|
||||
host: host,
|
||||
userId: userId
|
||||
};
|
||||
}
|
@ -3,7 +3,7 @@ import { faHistory } from '@fortawesome/free-solid-svg-icons';
|
||||
export async function search(v: any, q: string) {
|
||||
q = q.trim();
|
||||
|
||||
if (q.startsWith('@')) {
|
||||
if (q.startsWith('@') && !q.includes(' ')) {
|
||||
v.$router.push(`/${q}`);
|
||||
return;
|
||||
}
|
||||
|
@ -78,6 +78,7 @@ export default Vue.extend({
|
||||
.mk-avatar
|
||||
display inline-block
|
||||
vertical-align bottom
|
||||
flex-shrink 0
|
||||
|
||||
&:not(.cat)
|
||||
overflow hidden
|
||||
|
@ -22,7 +22,14 @@
|
||||
<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input>
|
||||
<ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></ui-input>
|
||||
<ui-select v-if="select" v-model="selectedValue" autofocus>
|
||||
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
||||
<template v-if="select.items">
|
||||
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
|
||||
</template>
|
||||
<template v-else>
|
||||
<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
|
||||
<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
|
||||
</optgroup>
|
||||
</template>
|
||||
</ui-select>
|
||||
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)">
|
||||
<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
|
||||
@ -230,7 +237,7 @@ export default Vue.extend({
|
||||
font-size 32px
|
||||
|
||||
&.success
|
||||
color #37ec92
|
||||
color #85da5a
|
||||
|
||||
&.error
|
||||
color #ec4137
|
||||
|
@ -36,7 +36,7 @@ export default Vue.extend({
|
||||
return {
|
||||
hide: true
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
style(): any {
|
||||
let url = `url(${
|
||||
|
@ -13,8 +13,8 @@
|
||||
@click="navigate(user)"
|
||||
tabindex="-1"
|
||||
>
|
||||
<mk-avatar class="avatar" :user="user"/>
|
||||
<span class="name"><mk-user-name :user="user"/></span>
|
||||
<mk-avatar class="avatar" :user="user" :key="user.id"/>
|
||||
<span class="name"><mk-user-name :user="user" :key="user.id"/></span>
|
||||
<span class="username">@{{ user | acct }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<component :is="'x-' + value.type" :value="value" @input="v => updateItem(v)" @remove="() => $emit('remove', value)" :key="value.id"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XSection from './page-editor.section.vue';
|
||||
import XText from './page-editor.text.vue';
|
||||
import XImage from './page-editor.image.vue';
|
||||
import XButton from './page-editor.button.vue';
|
||||
import XInput from './page-editor.input.vue';
|
||||
import XSwitch from './page-editor.switch.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XSection, XText, XImage, XButton, XInput, XSwitch
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<x-container @remove="() => $emit('remove')">
|
||||
<template #header><fa :icon="faBolt"/> {{ $t('blocks.button') }}</template>
|
||||
|
||||
<section class="xfhsjczc">
|
||||
<ui-input v-model="value.text"><span>{{ $t('blocks._button.text') }}</span></ui-input>
|
||||
<ui-select v-model="value.action">
|
||||
<template #label>{{ $t('blocks._button.action') }}</template>
|
||||
<option value="dialog">{{ $t('blocks._button._action.dialog') }}</option>
|
||||
<option value="resetRandom">{{ $t('blocks._button._action.resetRandom') }}</option>
|
||||
</ui-select>
|
||||
<ui-input v-if="value.action === 'dialog'" v-model="value.content"><span>{{ $t('blocks._button._action._dialog.content') }}</span></ui-input>
|
||||
</section>
|
||||
</x-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { faBolt } from '@fortawesome/free-solid-svg-icons';
|
||||
import XContainer from './page-editor.container.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('pages'),
|
||||
|
||||
components: {
|
||||
XContainer
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faBolt
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.value.text == null) Vue.set(this.value, 'text', '');
|
||||
if (this.value.action == null) Vue.set(this.value, 'action', 'dialog');
|
||||
if (this.value.content == null) Vue.set(this.value, 'content', null);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.xfhsjczc
|
||||
padding 0 16px 0 16px
|
||||
|
||||
</style>
|
@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }">
|
||||
<header>
|
||||
<div class="title"><slot name="header"></slot></div>
|
||||
<div class="buttons">
|
||||
<slot name="func"></slot>
|
||||
<button v-if="removable" @click="remove()">
|
||||
<fa :icon="faTrashAlt"/>
|
||||
</button>
|
||||
<button @click="toggleContent(!showBody)">
|
||||
<template v-if="showBody"><fa icon="angle-up"/></template>
|
||||
<template v-else><fa icon="angle-down"/></template>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<p v-show="showBody" class="error" v-if="error != null">{{ $t('script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p>
|
||||
<p v-show="showBody" class="warn" v-if="warn != null">{{ $t('script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
|
||||
<div v-show="showBody">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
import i18n from '../../../../i18n';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('pages'),
|
||||
|
||||
props: {
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
removable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
error: {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
warn: {
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showBody: this.expanded,
|
||||
faTrashAlt
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleContent(show: boolean) {
|
||||
this.showBody = show;
|
||||
this.$emit('toggle', show);
|
||||
},
|
||||
remove() {
|
||||
this.$emit('remove');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.cpjygsrt
|
||||
overflow hidden
|
||||
background var(--face)
|
||||
border solid 2px var(--pageBlockBorder)
|
||||
border-radius 6px
|
||||
|
||||
&:hover
|
||||
border solid 2px var(--pageBlockBorderHover)
|
||||
|
||||
&.warn
|
||||
border solid 2px #dec44c
|
||||
|
||||
&.error
|
||||
border solid 2px #f00
|
||||
|
||||
& + .cpjygsrt
|
||||
margin-top 16px
|
||||
|
||||
> header
|
||||
> .title
|
||||
z-index 1
|
||||
margin 0
|
||||
padding 0 16px
|
||||
line-height 42px
|
||||
font-size 0.9em
|
||||
font-weight bold
|
||||
color var(--faceHeaderText)
|
||||
box-shadow 0 1px rgba(#000, 0.07)
|
||||
|
||||
> [data-icon]
|
||||
margin-right 6px
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> .buttons
|
||||
position absolute
|
||||
z-index 2
|
||||
top 0
|
||||
right 0
|
||||
|
||||
> button
|
||||
padding 0
|
||||
width 42px
|
||||
font-size 0.9em
|
||||
line-height 42px
|
||||
color var(--faceTextButton)
|
||||
|
||||
&:hover
|
||||
color var(--faceTextButtonHover)
|
||||
|
||||
&:active
|
||||
color var(--faceTextButtonActive)
|
||||
|
||||
> .warn
|
||||
color #b19e49
|
||||
margin 0
|
||||
padding 16px 16px 0 16px
|
||||
font-size 14px
|
||||
|
||||
> .error
|
||||
color #f00
|
||||
margin 0
|
||||
padding 16px 16px 0 16px
|
||||
font-size 14px
|
||||
|
||||
</style>
|
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<x-container @remove="() => $emit('remove')">
|
||||
<template #header><fa :icon="faImage"/> {{ $t('blocks.image') }}</template>
|
||||
<template #func>
|
||||
<button @click="choose()">
|
||||
<fa :icon="faFolderOpen"/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<section class="oyyftmcf">
|
||||
<x-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/>
|
||||
</section>
|
||||
</x-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faImage, faFolderOpen } from '@fortawesome/free-regular-svg-icons';
|
||||
import XContainer from './page-editor.container.vue';
|
||||
import XFileThumbnail from '../drive-file-thumbnail.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('pages'),
|
||||
|
||||
components: {
|
||||
XContainer, XFileThumbnail
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
file: null,
|
||||
faPencilAlt, faImage, faFolderOpen
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.value.fileId === undefined) Vue.set(this.value, 'fileId', null);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.value.fileId == null) {
|
||||
this.choose();
|
||||
} else {
|
||||
this.$root.api('drive/files/show', {
|
||||
fileId: this.value.fileId
|
||||
}).then(file => {
|
||||
this.file = file;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async choose() {
|
||||
this.$chooseDriveFile({
|
||||
multiple: false
|
||||
}).then(file => {
|
||||
this.file = file;
|
||||
this.value.fileId = file.id;
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.oyyftmcf
|
||||
> .preview
|
||||
height 150px
|
||||
|
||||
</style>
|
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<x-container @remove="() => $emit('remove')">
|
||||
<template #header><fa :icon="faBolt"/> {{ $t('blocks.input') }}</template>
|
||||
|
||||
<section class="dnvasjon">
|
||||
<ui-input v-model="value.name"><template #prefix><fa :icon="faSquareRootAlt"/></template><span>{{ $t('blocks._input.name') }}</span></ui-input>
|
||||
<ui-input v-model="value.text"><span>{{ $t('blocks._input.text') }}</span></ui-input>
|
||||
<ui-select v-model="value.inputType">
|
||||
<template #label>{{ $t('blocks._input.inputType') }}</template>
|
||||
<option value="string">{{ $t('blocks._input._inputType.string') }}</option>
|
||||
<option value="number">{{ $t('blocks._input._inputType.number') }}</option>
|
||||
</ui-select>
|
||||
<ui-input v-model="value.default" :type="value.inputType"><span>{{ $t('blocks._input.default') }}</span></ui-input>
|
||||
</section>
|
||||
</x-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { faBolt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import XContainer from './page-editor.container.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('pages'),
|
||||
|
||||
components: {
|
||||
XContainer
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faBolt, faSquareRootAlt
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.value.name == null) Vue.set(this.value, 'name', '');
|
||||
if (this.value.inputType == null) Vue.set(this.value, 'inputType', 'string');
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.dnvasjon
|
||||
padding 0 16px 0 16px
|
||||
|
||||
</style>
|
@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<x-container :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn">
|
||||
<template #header><fa v-if="icon" :icon="icon"/> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template>
|
||||
<template #func>
|
||||
<button @click="changeType()">
|
||||
<fa :icon="faPencilAlt"/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<section v-if="value.type === null" class="pbglfege" @click="changeType()">
|
||||
{{ $t('script.emptySlot') }}
|
||||
</section>
|
||||
<section v-else-if="value.type === 'text'" class="tbwccoaw">
|
||||
<input v-model="value.value"/>
|
||||
</section>
|
||||
<section v-else-if="value.type === 'multiLineText'" class="tbwccoaw">
|
||||
<textarea v-model="value.value"></textarea>
|
||||
</section>
|
||||
<section v-else-if="value.type === 'textList'" class="frvuzvoi">
|
||||
<ui-textarea v-model="value.value"></ui-textarea>
|
||||
</section>
|
||||
<section v-else-if="value.type === 'number'" class="tbwccoaw">
|
||||
<input v-model="value.value" type="number"/>
|
||||
</section>
|
||||
<section v-else-if="value.type === 'ref'" class="hpdwcrvs">
|
||||
<select v-model="value.value">
|
||||
<option v-for="v in aiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option>
|
||||
<optgroup :label="$t('script.pageVariables')">
|
||||
<option v-for="v in aiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$t('script.enviromentVariables')">
|
||||
<option v-for="v in aiScript.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</section>
|
||||
<section v-else-if="value.type === 'in'" class="hpdwcrvs">
|
||||
<select v-model="value.value">
|
||||
<option v-for="v in fnSlots" :value="v">{{ v }}</option>
|
||||
</select>
|
||||
</section>
|
||||
<section v-else-if="value.type === 'fn'" class="" style="padding:16px;">
|
||||
<ui-textarea v-model="slots"></ui-textarea>
|
||||
<x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/>
|
||||
</section>
|
||||
<section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;">
|
||||
<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aiScript.getVarByName(value.type.split(':')[1]).value.slots[i]" :get-expected-type="() => null" :ai-script="aiScript" :name="name" :key="i"/>
|
||||
</section>
|
||||
<section v-else class="" style="padding:16px;">
|
||||
<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/>
|
||||
</section>
|
||||
</x-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import XContainer from './page-editor.container.vue';
|
||||
import { faSuperscript, faPencilAlt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { AiScript } from '../../../scripts/aiscript';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('pages'),
|
||||
|
||||
components: {
|
||||
XContainer
|
||||
},
|
||||
|
||||
inject: ['getScriptBlockList'],
|
||||
|
||||
props: {
|
||||
getExpectedType: {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
required: false
|
||||
},
|
||||
removable: {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
aiScript: {
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
},
|
||||
fnSlots: {
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
AiScript,
|
||||
error: null,
|
||||
warn: null,
|
||||
slots: '',
|
||||
faSuperscript, faPencilAlt, faSquareRootAlt
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
icon(): any {
|
||||
if (this.value.type === null) return null;
|
||||
if (this.value.type.startsWith('fn:')) return null;
|
||||
return AiScript.blockDefs.find(x => x.type === this.value.type).icon;
|
||||
},
|
||||
typeText(): any {
|
||||
if (this.value.type === null) return null;
|
||||
return this.$t(`script.blocks.${this.value.type}`);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
slots() {
|
||||
this.value.value.slots = this.slots.split('\n');
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate() {
|
||||
this.$options.components.XV = require('./page-editor.script-block.vue').default;
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.value.value == null) Vue.set(this.value, 'value', null);
|
||||
|
||||
if (this.value.value && this.value.value.slots) this.slots = this.value.value.slots.join('\n');
|
||||
|
||||
this.$watch('value.type', (t) => {
|
||||
this.warn = null;
|
||||
|
||||
if (this.value.type === 'fn') {
|
||||
const id = uuid.v4();
|
||||
this.value.value = {};
|
||||
Vue.set(this.value.value, 'slots', []);
|
||||
Vue.set(this.value.value, 'expression', { id, type: null });
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.value.type && this.value.type.startsWith('fn:')) {
|
||||
const fnName = this.value.type.split(':')[1];
|
||||
const fn = this.aiScript.getVarByName(fnName);
|
||||
|
||||
const empties = [];
|
||||
for (let i = 0; i < fn.value.slots.length; i++) {
|
||||
const id = uuid.v4();
|
||||
empties.push({ id, type: null });
|
||||
}
|
||||
Vue.set(this.value, 'args', empties);
|
||||
return;
|
||||
}
|
||||
|
||||
if (AiScript.isLiteralBlock(this.value)) return;
|
||||
|
||||
const empties = [];
|
||||
for (let i = 0; i < AiScript.funcDefs[this.value.type].in.length; i++) {
|
||||
const id = uuid.v4();
|
||||
empties.push({ id, type: null });
|
||||
}
|
||||
Vue.set(this.value, 'args', empties);
|
||||
|
||||
for (let i = 0; i < AiScript.funcDefs[this.value.type].in.length; i++) {
|
||||
const inType = AiScript.funcDefs[this.value.type].in[i];
|
||||
if (typeof inType !== 'number') {
|
||||
if (inType === 'number') this.value.args[i].type = 'number';
|
||||
if (inType === 'string') this.value.args[i].type = 'text';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$watch('value.args', (args) => {
|
||||
if (args == null) {
|
||||
this.warn = null;
|
||||
return;
|
||||
}
|
||||
const emptySlotIndex = args.findIndex(x => x.type === null);
|
||||
if (emptySlotIndex !== -1 && emptySlotIndex < args.length) {
|
||||
this.warn = {
|
||||
slot: emptySlotIndex
|
||||
};
|
||||
} else {
|
||||
this.warn = null;
|
||||
}
|
||||
}, {
|
||||
deep: true
|
||||
});
|
||||
|
||||
this.$watch('aiScript.variables', () => {
|
||||
if (this.type != null && this.value) {
|
||||
this.error = this.aiScript.typeCheck(this.value);
|
||||
}
|
||||
}, {
|
||||
deep: true
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
async changeType() {
|
||||
const { canceled, result: type } = await this.$root.dialog({
|
||||
type: null,
|
||||
title: this.$t('select-type'),
|
||||
select: {
|
||||
groupedItems: this.getScriptBlockList(this.getExpectedType ? this.getExpectedType() : null)
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
this.value.type = type;
|
||||
},
|
||||
|
||||
_getExpectedType(slot: number) {
|
||||
return this.aiScript.getExpectedType(this.value, slot);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.turmquns
|
||||
opacity 0.7
|
||||
|
||||
.pbglfege
|
||||
opacity 0.5
|
||||
padding 16px
|
||||
text-align center
|
||||
cursor pointer
|
||||
color var(--text)
|
||||
|
||||
.tbwccoaw
|
||||
> input
|
||||
> textarea
|
||||
display block
|
||||
-webkit-appearance none
|
||||
-moz-appearance none
|
||||
appearance none
|
||||
width 100%
|
||||
max-width 100%
|
||||
min-width 100%
|
||||
border none
|
||||
box-shadow none
|
||||
padding 16px
|
||||
font-size 16px
|
||||
background transparent
|
||||
color var(--text)
|
||||
|
||||
> textarea
|
||||
min-height 100px
|
||||
|
||||
.hpdwcrvs
|
||||
padding 16px
|
||||
|
||||
> select
|
||||
display block
|
||||
padding 4px
|
||||
font-size 16px
|
||||
width 100%
|
||||
|
||||
</style>
|
@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<x-container @remove="() => $emit('remove')">
|
||||
<template #header><fa :icon="faStickyNote"/> {{ value.title }}</template>
|
||||
<template #func>
|
||||
<button @click="rename()">
|
||||
<fa :icon="faPencilAlt"/>
|
||||
</button>
|
||||
<button @click="add()">
|
||||
<fa :icon="faPlus"/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<section class="ilrvjyvi">
|
||||
<div class="children">
|
||||
<x-block v-for="child in value.children" :value="child" @input="v => updateItem(v)" @remove="() => remove(child)" :key="child.id"/>
|
||||
</div>
|
||||
</section>
|
||||
</x-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { faPlus, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||
import XContainer from './page-editor.container.vue';
|
||||
import * as uuid from 'uuid';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('pages'),
|
||||
|
||||
components: {
|
||||
XContainer
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faStickyNote, faPlus, faPencilAlt
|
||||
};
|
||||
},
|
||||
|
||||
beforeCreate() {
|
||||
this.$options.components.XBlock = require('./page-editor.block.vue').default
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.value.title == null) Vue.set(this.value, 'title', null);
|
||||
if (this.value.children == null) Vue.set(this.value, 'children', []);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.value.title == null) {
|
||||
this.rename();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async rename() {
|
||||
const { canceled, result: title } = await this.$root.dialog({
|
||||
title: 'Enter title',
|
||||
input: {
|
||||
type: 'text',
|
||||
default: this.value.title
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
this.value.title = title;
|
||||
},
|
||||
|
||||
async add() {
|
||||
const { canceled, result: type } = await this.$root.dialog({
|
||||
type: null,
|
||||
title: this.$t('choose-block'),
|
||||
select: {
|
||||
items: [{
|
||||
value: 'section', text: this.$t('blocks.section')
|
||||
}, {
|
||||
value: 'text', text: this.$t('blocks.text')
|
||||
}, {
|
||||
value: 'image', text: this.$t('blocks.image')
|
||||
}, {
|
||||
value: 'button', text: this.$t('blocks.button')
|
||||
}, {
|
||||
value: 'input', text: this.$t('blocks.input')
|
||||
}, {
|
||||
value: 'switch', text: this.$t('blocks.switch')
|
||||
}]
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const id = uuid.v4();
|
||||
this.value.children.push({ id, type });
|
||||
},
|
||||
|
||||
updateItem(v) {
|
||||
const i = this.value.children.findIndex(x => x.id === v.id);
|
||||
const newValue = [
|
||||
...this.value.children.slice(0, i),
|
||||
v,
|
||||
...this.value.children.slice(i + 1)
|
||||
];
|
||||
this.value.children = newValue;
|
||||
this.$emit('input', this.value);
|
||||
},
|
||||
|
||||
remove(el) {
|
||||
const i = this.value.children.findIndex(x => x.id === el.id);
|
||||
const newValue = [
|
||||
...this.value.children.slice(0, i),
|
||||
...this.value.children.slice(i + 1)
|
||||
];
|
||||
this.value.children = newValue;
|
||||
this.$emit('input', this.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.ilrvjyvi
|
||||
> .children
|
||||
padding 16px
|
||||
|
||||
</style>
|
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<x-container @remove="() => $emit('remove')">
|
||||
<template #header><fa :icon="faBolt"/> {{ $t('blocks.switch') }}</template>
|
||||
|
||||
<section class="kjuadyyj">
|
||||
<ui-input v-model="value.name"><template #prefix><fa :icon="faSquareRootAlt"/></template><span>{{ $t('blocks._switch.name') }}</span></ui-input>
|
||||
<ui-input v-model="value.text"><span>{{ $t('blocks._switch.text') }}</span></ui-input>
|
||||
<ui-switch v-model="value.default"><span>{{ $t('blocks._switch.default') }}</span></ui-switch>
|
||||
</section>
|
||||
</x-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { faBolt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import XContainer from './page-editor.container.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('pages'),
|
||||
|
||||
components: {
|
||||
XContainer
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faBolt, faSquareRootAlt
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.value.name == null) Vue.set(this.value, 'name', '');
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.kjuadyyj
|
||||
padding 0 16px 16px 16px
|
||||
|
||||
</style>
|
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<x-container @remove="() => $emit('remove')">
|
||||
<template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.text') }}</template>
|
||||
|
||||
<section class="ihymsbbe">
|
||||
<textarea v-model="value.text"></textarea>
|
||||
</section>
|
||||
</x-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { faAlignLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
import XContainer from './page-editor.container.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('pages'),
|
||||
|
||||
components: {
|
||||
XContainer
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faAlignLeft,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.value.text == null) Vue.set(this.value, 'text', '');
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.ihymsbbe
|
||||
> textarea
|
||||
display block
|
||||
-webkit-appearance none
|
||||
-moz-appearance none
|
||||
appearance none
|
||||
width 100%
|
||||
min-width 100%
|
||||
min-height 150px
|
||||
border none
|
||||
box-shadow none
|
||||
padding 16px
|
||||
background transparent
|
||||
color var(--text)
|
||||
</style>
|
@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="gwbmwxkm" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
|
||||
<header>
|
||||
<div class="title"><fa :icon="faStickyNote"/> {{ pageId ? $t('edit-page') : $t('new-page') }}</div>
|
||||
<div class="buttons">
|
||||
<button @click="del()"><fa :icon="faTrashAlt"/></button>
|
||||
<button @click="() => showOptions = !showOptions"><fa :icon="faCog"/></button>
|
||||
<button @click="save()"><fa :icon="faSave"/></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<ui-input v-model="title">
|
||||
<span>{{ $t('title') }}</span>
|
||||
</ui-input>
|
||||
|
||||
<template v-if="showOptions">
|
||||
<ui-input v-model="summary">
|
||||
<span>{{ $t('summary') }}</span>
|
||||
</ui-input>
|
||||
|
||||
<ui-input v-model="name">
|
||||
<template #prefix>{{ url }}/@{{ $store.state.i.username }}/pages/</template>
|
||||
<span>{{ $t('url') }}</span>
|
||||
</ui-input>
|
||||
|
||||
<ui-switch v-model="alignCenter">{{ $t('align-center') }}</ui-switch>
|
||||
|
||||
<ui-select v-model="font">
|
||||
<template #label>{{ $t('font') }}</template>
|
||||
<option value="serif">{{ $t('fontSerif') }}</option>
|
||||
<option value="sans-serif">{{ $t('fontSansSerif') }}</option>
|
||||
</ui-select>
|
||||
|
||||
<div class="eyeCatch">
|
||||
<ui-button v-if="eyeCatchingImageId == null" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catchig-image') }}</ui-button>
|
||||
<div v-else-if="eyeCatchingImage">
|
||||
<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name"/>
|
||||
<ui-button @click="removeEyeCatchingImage()"><fa :icon="faTrashAlt"/> {{ $t('remove-eye-catchig-image') }}</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="content" v-for="child in content">
|
||||
<x-block :value="child" @input="v => updateItem(v)" @remove="() => remove(child)" :key="child.id"/>
|
||||
</div>
|
||||
|
||||
<ui-button @click="add()"><fa :icon="faPlus"/></ui-button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<ui-container :body-togglable="true">
|
||||
<template #header><fa :icon="faSquareRootAlt"/> {{ $t('variables') }}</template>
|
||||
<div class="qmuvgica">
|
||||
<div class="variables" v-show="variables.length > 0">
|
||||
<template v-for="variable in variables">
|
||||
<x-variable
|
||||
:value="variable"
|
||||
:removable="true"
|
||||
@input="v => updateVariable(v)"
|
||||
@remove="() => removeVariable(variable)"
|
||||
:key="variable.name"
|
||||
:ai-script="aiScript"
|
||||
:name="variable.name"
|
||||
:title="variable.name"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<ui-button @click="addVariable()" class="add"><fa :icon="faPlus"/></ui-button>
|
||||
|
||||
<ui-info><span v-html="$t('variables-info')"></span><a @click="() => moreDetails = true" style="display:block;">{{ $t('more-details') }}</a></ui-info>
|
||||
|
||||
<template v-if="moreDetails">
|
||||
<ui-info><span v-html="$t('variables-info2')"></span></ui-info>
|
||||
<ui-info><span v-html="$t('variables-info3')"></span></ui-info>
|
||||
</template>
|
||||
</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { faICursor, faPlus, faSquareRootAlt, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
import XVariable from './page-editor.script-block.vue';
|
||||
import XBlock from './page-editor.block.vue';
|
||||
import * as uuid from 'uuid';
|
||||
import { AiScript } from '../../../scripts/aiscript';
|
||||
import { url } from '../../../../config';
|
||||
import { collectPageVars } from '../../../scripts/collect-page-vars';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('pages'),
|
||||
|
||||
components: {
|
||||
XVariable, XBlock
|
||||
},
|
||||
|
||||
props: {
|
||||
page: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
pageId: null,
|
||||
title: '',
|
||||
summary: null,
|
||||
name: Date.now().toString(),
|
||||
eyeCatchingImage: null,
|
||||
eyeCatchingImageId: null,
|
||||
font: 'sans-serif',
|
||||
content: [],
|
||||
alignCenter: false,
|
||||
variables: [],
|
||||
aiScript: null,
|
||||
showOptions: false,
|
||||
moreDetails: false,
|
||||
url,
|
||||
faPlus, faICursor, faSave, faStickyNote, faSquareRootAlt, faCog, faTrashAlt
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
async eyeCatchingImageId() {
|
||||
if (this.eyeCatchingImageId == null) {
|
||||
this.eyeCatchingImage = null;
|
||||
} else {
|
||||
this.eyeCatchingImage = await this.$root.api('drive/files/show', {
|
||||
fileId: this.eyeCatchingImageId,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.aiScript = new AiScript();
|
||||
|
||||
this.$watch('variables', () => {
|
||||
this.aiScript.injectVars(this.variables);
|
||||
}, { deep: true });
|
||||
|
||||
this.$watch('content', () => {
|
||||
this.aiScript.injectPageVars(collectPageVars(this.content));
|
||||
}, { deep: true });
|
||||
|
||||
if (this.page) {
|
||||
this.$root.api('pages/show', {
|
||||
pageId: this.page,
|
||||
}).then(page => {
|
||||
this.pageId = page.id;
|
||||
this.title = page.title;
|
||||
this.name = page.name;
|
||||
this.summary = page.summary;
|
||||
this.font = page.font;
|
||||
this.alignCenter = page.alignCenter;
|
||||
this.content = page.content;
|
||||
this.variables = page.variables;
|
||||
this.eyeCatchingImageId = page.eyeCatchingImageId;
|
||||
});
|
||||
} else {
|
||||
const id = uuid.v4();
|
||||
this.content = [{
|
||||
id,
|
||||
type: 'text',
|
||||
text: 'Hello World!'
|
||||
}];
|
||||
}
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
getScriptBlockList: this.getScriptBlockList
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
save() {
|
||||
if (this.pageId) {
|
||||
this.$root.api('pages/update', {
|
||||
pageId: this.pageId,
|
||||
title: this.title.trim(),
|
||||
name: this.name.trim(),
|
||||
summary: this.summary,
|
||||
font: this.font,
|
||||
alignCenter: this.alignCenter,
|
||||
content: this.content,
|
||||
variables: this.variables,
|
||||
eyeCatchingImageId: this.eyeCatchingImageId,
|
||||
}).then(page => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('page-updated')
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.$root.api('pages/create', {
|
||||
title: this.title.trim(),
|
||||
name: this.name.trim(),
|
||||
summary: this.summary,
|
||||
font: this.font,
|
||||
alignCenter: this.alignCenter,
|
||||
content: this.content,
|
||||
variables: this.variables,
|
||||
eyeCatchingImageId: this.eyeCatchingImageId,
|
||||
}).then(page => {
|
||||
this.pageId = page.id;
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('page-created')
|
||||
});
|
||||
this.$router.push(`/i/pages/edit/${this.pageId}`);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
del() {
|
||||
this.$root.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('are-you-sure-delete'),
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('pages/delete', {
|
||||
pageId: this.pageId,
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('page-deleted')
|
||||
});
|
||||
this.$router.push(`/i/pages`);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async add() {
|
||||
const { canceled, result: type } = await this.$root.dialog({
|
||||
type: null,
|
||||
title: this.$t('choose-block'),
|
||||
select: {
|
||||
items: [{
|
||||
value: 'section', text: this.$t('blocks.section')
|
||||
}, {
|
||||
value: 'text', text: this.$t('blocks.text')
|
||||
}, {
|
||||
value: 'image', text: this.$t('blocks.image')
|
||||
}, {
|
||||
value: 'button', text: this.$t('blocks.button')
|
||||
}, {
|
||||
value: 'input', text: this.$t('blocks.input')
|
||||
}, {
|
||||
value: 'switch', text: this.$t('blocks.switch')
|
||||
}]
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const id = uuid.v4();
|
||||
this.content.push({ id, type });
|
||||
},
|
||||
|
||||
async addVariable() {
|
||||
let { canceled, result: name } = await this.$root.dialog({
|
||||
title: this.$t('enter-variable-name'),
|
||||
input: {
|
||||
type: 'text',
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
name = name.trim();
|
||||
|
||||
if (this.aiScript.isUsedName(name)) {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('the-variable-name-is-already-used')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const id = uuid.v4();
|
||||
this.variables.push({ id, name, type: null });
|
||||
},
|
||||
|
||||
updateItem(v) {
|
||||
const i = this.content.findIndex(x => x.id === v.id);
|
||||
const newValue = [
|
||||
...this.content.slice(0, i),
|
||||
v,
|
||||
...this.content.slice(i + 1)
|
||||
];
|
||||
this.content = newValue;
|
||||
},
|
||||
|
||||
remove(el) {
|
||||
const i = this.content.findIndex(x => x.id === el.id);
|
||||
const newValue = [
|
||||
...this.content.slice(0, i),
|
||||
...this.content.slice(i + 1)
|
||||
];
|
||||
this.content = newValue;
|
||||
},
|
||||
|
||||
removeVariable(v) {
|
||||
const i = this.variables.findIndex(x => x.name === v.name);
|
||||
const newValue = [
|
||||
...this.variables.slice(0, i),
|
||||
...this.variables.slice(i + 1)
|
||||
];
|
||||
this.variables = newValue;
|
||||
},
|
||||
|
||||
getScriptBlockList(type: string = null) {
|
||||
const list = [];
|
||||
|
||||
const blocks = AiScript.blockDefs.filter(block => type === null || block.out === null || block.out === type);
|
||||
|
||||
for (const block of blocks) {
|
||||
const category = list.find(x => x.category === block.category);
|
||||
if (category) {
|
||||
category.items.push({
|
||||
value: block.type,
|
||||
text: this.$t(`script.blocks.${block.type}`)
|
||||
});
|
||||
} else {
|
||||
list.push({
|
||||
category: block.category,
|
||||
label: this.$t(`script.categories.${block.category}`),
|
||||
items: [{
|
||||
value: block.type,
|
||||
text: this.$t(`script.blocks.${block.type}`)
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const userFns = this.variables.filter(x => x.type === 'fn');
|
||||
if (userFns.length > 0) {
|
||||
list.unshift({
|
||||
label: this.$t(`script.categories.fn`),
|
||||
items: userFns.map(v => ({
|
||||
value: 'fn:' + v.name,
|
||||
text: v.name
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
},
|
||||
|
||||
setEyeCatchingImage() {
|
||||
this.$chooseDriveFile({
|
||||
multiple: false
|
||||
}).then(file => {
|
||||
this.eyeCatchingImageId = file.id;
|
||||
});
|
||||
},
|
||||
|
||||
removeEyeCatchingImage() {
|
||||
this.eyeCatchingImageId = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.gwbmwxkm
|
||||
overflow hidden
|
||||
background var(--face)
|
||||
margin-bottom 16px
|
||||
|
||||
&.round
|
||||
border-radius 6px
|
||||
|
||||
&.shadow
|
||||
box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> header
|
||||
background var(--faceHeader)
|
||||
|
||||
> .title
|
||||
z-index 1
|
||||
margin 0
|
||||
padding 0 16px
|
||||
line-height 42px
|
||||
font-size 0.9em
|
||||
font-weight bold
|
||||
color var(--faceHeaderText)
|
||||
box-shadow 0 var(--lineWidth) rgba(#000, 0.07)
|
||||
|
||||
> [data-icon]
|
||||
margin-right 6px
|
||||
|
||||
&:empty
|
||||
display none
|
||||
|
||||
> .buttons
|
||||
position absolute
|
||||
z-index 2
|
||||
top 0
|
||||
right 0
|
||||
|
||||
> button
|
||||
padding 0
|
||||
width 42px
|
||||
font-size 0.9em
|
||||
line-height 42px
|
||||
color var(--faceTextButton)
|
||||
|
||||
&:hover
|
||||
color var(--faceTextButtonHover)
|
||||
|
||||
&:active
|
||||
color var(--faceTextButtonActive)
|
||||
|
||||
> section
|
||||
padding 0 32px 32px 32px
|
||||
|
||||
@media (max-width 500px)
|
||||
padding 0 16px 16px 16px
|
||||
|
||||
> .content
|
||||
margin-bottom 16px
|
||||
|
||||
> .eyeCatch
|
||||
margin-bottom 16px
|
||||
|
||||
> div
|
||||
> img
|
||||
max-width 100%
|
||||
|
||||
.qmuvgica
|
||||
padding 32px
|
||||
|
||||
@media (max-width 500px)
|
||||
padding 16px
|
||||
|
||||
> .variables
|
||||
margin-bottom 16px
|
||||
|
||||
> .add
|
||||
margin-bottom 16px
|
||||
|
||||
</style>
|
141
src/client/app/common/views/components/page-preview.vue
Normal file
141
src/client/app/common/views/components/page-preview.vue
Normal file
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
|
||||
<div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
|
||||
<article>
|
||||
<header>
|
||||
<h1 :title="page.title">{{ page.title }}</h1>
|
||||
</header>
|
||||
<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
|
||||
<footer>
|
||||
<img class="icon" :src="page.user.avatarUrl"/>
|
||||
<p>{{ page.user | userName }}</p>
|
||||
</footer>
|
||||
</article>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
page: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.vhpxefrj
|
||||
display block
|
||||
overflow hidden
|
||||
width 100%
|
||||
background var(--face)
|
||||
|
||||
&.round
|
||||
border-radius 8px
|
||||
|
||||
&.shadow
|
||||
box-shadow 0 4px 16px rgba(#000, 0.1)
|
||||
|
||||
@media (min-width 500px)
|
||||
box-shadow 0 8px 32px rgba(#000, 0.1)
|
||||
|
||||
> .thumbnail
|
||||
position absolute
|
||||
width 100px
|
||||
height 100%
|
||||
background-position center
|
||||
background-size cover
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
> button
|
||||
font-size 3.5em
|
||||
opacity: 0.7
|
||||
|
||||
&:hover
|
||||
font-size 4em
|
||||
opacity 0.9
|
||||
|
||||
& + article
|
||||
left 100px
|
||||
width calc(100% - 100px)
|
||||
|
||||
> article
|
||||
padding 16px
|
||||
|
||||
> header
|
||||
margin-bottom 8px
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
font-size 1em
|
||||
color var(--urlPreviewTitle)
|
||||
|
||||
> p
|
||||
margin 0
|
||||
color var(--urlPreviewText)
|
||||
font-size 0.8em
|
||||
|
||||
> footer
|
||||
margin-top 8px
|
||||
height 16px
|
||||
|
||||
> img
|
||||
display inline-block
|
||||
width 16px
|
||||
height 16px
|
||||
margin-right 4px
|
||||
vertical-align top
|
||||
|
||||
> p
|
||||
display inline-block
|
||||
margin 0
|
||||
color var(--urlPreviewInfo)
|
||||
font-size 0.8em
|
||||
line-height 16px
|
||||
vertical-align top
|
||||
|
||||
@media (max-width 700px)
|
||||
> .thumbnail
|
||||
position relative
|
||||
width 100%
|
||||
height 100px
|
||||
|
||||
& + article
|
||||
left 0
|
||||
width 100%
|
||||
|
||||
@media (max-width 550px)
|
||||
font-size 12px
|
||||
|
||||
> .thumbnail
|
||||
height 80px
|
||||
|
||||
> article
|
||||
padding 12px
|
||||
|
||||
@media (max-width 500px)
|
||||
font-size 10px
|
||||
|
||||
> .thumbnail
|
||||
height 70px
|
||||
|
||||
> article
|
||||
padding 8px
|
||||
|
||||
> header
|
||||
margin-bottom 4px
|
||||
|
||||
> footer
|
||||
margin-top 4px
|
||||
|
||||
> img
|
||||
width 12px
|
||||
height 12px
|
||||
|
||||
</style>
|
@ -14,7 +14,7 @@
|
||||
|
||||
<section>
|
||||
<header><fa icon="terminal"/> {{ $t('console.title') }}</header>
|
||||
<ui-input v-model="endpoint" :datalist="endpoints">
|
||||
<ui-input v-model="endpoint" :datalist="endpoints" @change="onEndpointChange()">
|
||||
<span>{{ $t('console.endpoint') }}</span>
|
||||
</ui-input>
|
||||
<ui-textarea v-model="body">
|
||||
@ -80,6 +80,22 @@ export default Vue.extend({
|
||||
this.sending = false;
|
||||
this.res = JSON5.stringify(err, null, 2);
|
||||
});
|
||||
},
|
||||
|
||||
onEndpointChange() {
|
||||
this.$root.api('endpoint', { endpoint: this.endpoint }).then(endpoint => {
|
||||
const body = {};
|
||||
for (const p of endpoint.params) {
|
||||
body[p.name] =
|
||||
p.type === 'String' ? '' :
|
||||
p.type === 'Number' ? 0 :
|
||||
p.type === 'Boolean' ? false :
|
||||
p.type === 'Array' ? [] :
|
||||
p.type === 'Object' ? {} :
|
||||
null;
|
||||
}
|
||||
this.body = JSON5.stringify(body, null, 2);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -23,6 +23,7 @@
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="$emit('keydown', $event)"
|
||||
@change="$emit('change', $event)"
|
||||
:list="id"
|
||||
>
|
||||
<input v-else ref="input"
|
||||
@ -38,6 +39,7 @@
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@keydown="$emit('keydown', $event)"
|
||||
@change="$emit('change', $event)"
|
||||
:list="id"
|
||||
>
|
||||
<datalist :id="id" v-if="datalist">
|
||||
@ -60,7 +62,7 @@
|
||||
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
|
||||
</div>
|
||||
<div class="toggle" v-if="withPasswordToggle">
|
||||
<a @click='togglePassword'>
|
||||
<a @click="togglePassword">
|
||||
<span v-if="type == 'password'"><fa :icon="['fa', 'eye']"/> {{ $t('@.show-password') }}</span>
|
||||
<span v-if="type != 'password'"><fa :icon="['far', 'eye-slash']"/> {{ $t('@.hide-password') }}</span>
|
||||
</a>
|
||||
|
@ -38,10 +38,10 @@ export default Vue.extend({
|
||||
const data = new FormData();
|
||||
data.append('md5', getMD5(fileData));
|
||||
|
||||
this.$root.api('drive/files/check_existence', {
|
||||
this.$root.api('drive/files/find-by-hash', {
|
||||
md5: getMD5(fileData)
|
||||
}).then(resp => {
|
||||
resolve(resp.file);
|
||||
resolve(resp.length > 0 ? resp[0] : null);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -77,11 +77,11 @@ export default Vue.extend({
|
||||
input: {
|
||||
default: this.list.name
|
||||
}
|
||||
}).then(({ canceled, result: title }) => {
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('users/lists/update', {
|
||||
listId: this.list.id,
|
||||
title: title
|
||||
name: name
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -60,9 +60,9 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
async init() {
|
||||
this.fetching = true;
|
||||
this.makePromise().then(x => {
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.us = x;
|
||||
} else {
|
||||
@ -76,9 +76,9 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
fetchMoreUsers() {
|
||||
async fetchMoreUsers() {
|
||||
this.fetchingMoreUsers = true;
|
||||
this.makePromise(this.cursor).then(x => {
|
||||
await (this.makePromise(this.cursor)).then(x => {
|
||||
this.us = this.us.concat(x.users);
|
||||
this.cursor = x.cursor;
|
||||
this.fetchingMoreUsers = false;
|
||||
|
@ -28,10 +28,10 @@ export default Vue.extend({
|
||||
this.$root.dialog({
|
||||
title: this.$t('list-name'),
|
||||
input: true
|
||||
}).then(async ({ canceled, result: title }) => {
|
||||
}).then(async ({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
const list = await this.$root.api('users/lists/create', {
|
||||
title
|
||||
name
|
||||
});
|
||||
|
||||
this.lists.push(list)
|
||||
|
@ -110,11 +110,11 @@ export default Vue.extend({
|
||||
this.init();
|
||||
},
|
||||
|
||||
init() {
|
||||
async init() {
|
||||
this.queue = [];
|
||||
this.notes = [];
|
||||
this.fetching = true;
|
||||
this.makePromise().then(x => {
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.notes = x;
|
||||
} else {
|
||||
@ -129,10 +129,10 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
fetchMore() {
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
await (this.makePromise(this.notes[this.notes.length - 1].id)).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
this.more = x.more;
|
||||
this.moreFetching = false;
|
||||
|
@ -14,6 +14,7 @@
|
||||
import Vue from 'vue';
|
||||
import XColumn from './deck.column.vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
@ -25,10 +26,10 @@ export default Vue.extend({
|
||||
|
||||
data() {
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('notes/search', {
|
||||
makePromise: async cursor => this.$root.api('notes/search', {
|
||||
limit: limit + 1,
|
||||
offset: cursor ? cursor : undefined,
|
||||
query: this.q
|
||||
...(await genSearchQuery(this, this.q))
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
|
34
src/client/app/common/views/pages/page/page.block.vue
Normal file
34
src/client/app/common/views/pages/page/page.block.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<component :is="'x-' + value.type" :value="value" :page="page" :script="script" :key="value.id" :h="h"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import XText from './page.text.vue';
|
||||
import XSection from './page.section.vue';
|
||||
import XImage from './page.image.vue';
|
||||
import XButton from './page.button.vue';
|
||||
import XInput from './page.input.vue';
|
||||
import XSwitch from './page.switch.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XText, XSection, XImage, XButton, XInput, XSwitch
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
script: {
|
||||
required: true
|
||||
},
|
||||
page: {
|
||||
required: true
|
||||
},
|
||||
h: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
42
src/client/app/common/views/pages/page/page.button.vue
Normal file
42
src/client/app/common/views/pages/page/page.button.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<ui-button class="kudkigyw" @click="click()">{{ value.text }}</ui-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
script: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
click() {
|
||||
if (this.value.action === 'dialog') {
|
||||
this.script.reEval();
|
||||
this.$root.dialog({
|
||||
text: this.script.interpolate(this.value.content)
|
||||
});
|
||||
} else if (this.value.action === 'resetRandom') {
|
||||
this.script.aiScript.updateRandomSeed(Math.random());
|
||||
this.script.reEval();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.kudkigyw
|
||||
display inline-block
|
||||
min-width 300px
|
||||
max-width 450px
|
||||
margin 8px 0
|
||||
</style>
|
36
src/client/app/common/views/pages/page/page.image.vue
Normal file
36
src/client/app/common/views/pages/page/page.image.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="lzyxtsnt">
|
||||
<img v-if="image" :src="image.url"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
page: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.image = this.page.attachedFiles.find(x => x.id === this.value.fileId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.lzyxtsnt
|
||||
> img
|
||||
max-width 100%
|
||||
</style>
|
43
src/client/app/common/views/pages/page/page.input.vue
Normal file
43
src/client/app/common/views/pages/page/page.input.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<ui-input class="kudkigyw" v-model="v" :type="value.inputType">{{ value.text }}</ui-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
script: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
v: this.value.default,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
v() {
|
||||
let v = this.v;
|
||||
if (this.value.inputType === 'number') v = parseInt(v, 10);
|
||||
this.script.aiScript.updatePageVar(this.value.name, v);
|
||||
this.script.reEval();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.kudkigyw
|
||||
display inline-block
|
||||
min-width 300px
|
||||
max-width 450px
|
||||
margin 8px 0
|
||||
</style>
|
55
src/client/app/common/views/pages/page/page.section.vue
Normal file
55
src/client/app/common/views/pages/page/page.section.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<section class="sdgxphyu">
|
||||
<component :is="'h' + h">{{ value.title }}</component>
|
||||
|
||||
<div class="children">
|
||||
<x-block v-for="child in value.children" :value="child" :page="page" :script="script" :key="child.id" :h="h + 1"/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
script: {
|
||||
required: true
|
||||
},
|
||||
page: {
|
||||
required: true
|
||||
},
|
||||
h: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate() {
|
||||
this.$options.components.XBlock = require('./page.block.vue').default
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.sdgxphyu
|
||||
margin 1.5em 0
|
||||
|
||||
> h2
|
||||
font-size 1.35em
|
||||
margin 0 0 0.5em 0
|
||||
|
||||
> h3
|
||||
font-size 1em
|
||||
margin 0 0 0.5em 0
|
||||
|
||||
> h4
|
||||
font-size 1em
|
||||
margin 0 0 0.5em 0
|
||||
|
||||
> .children
|
||||
//padding 16px
|
||||
|
||||
</style>
|
33
src/client/app/common/views/pages/page/page.switch.vue
Normal file
33
src/client/app/common/views/pages/page/page.switch.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<ui-switch v-model="v">{{ value.text }}</ui-switch>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
script: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
v: this.value.default,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
v() {
|
||||
this.script.aiScript.updatePageVar(this.value.name, this.v);
|
||||
this.script.reEval();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
35
src/client/app/common/views/pages/page/page.text.vue
Normal file
35
src/client/app/common/views/pages/page/page.text.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
value: {
|
||||
required: true
|
||||
},
|
||||
script: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
text: this.script.interpolate(this.value.text),
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$watch('script.vars', () => {
|
||||
this.text = this.script.interpolate(this.value.text);
|
||||
}, { deep: true });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
</style>
|
143
src/client/app/common/views/pages/page/page.vue
Normal file
143
src/client/app/common/views/pages/page/page.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div v-if="page" class="iroscrza" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners, center: page.alignCenter }" :style="{ fontFamily: page.font }">
|
||||
<header>
|
||||
<div class="title">{{ page.title }}</div>
|
||||
</header>
|
||||
|
||||
<div v-if="script">
|
||||
<x-block v-for="child in page.content" :value="child" @input="v => updateBlock(v)" :page="page" :script="script" :key="child.id" :h="2"/>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<small>@{{ page.user.username }}</small>
|
||||
<router-link v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId" :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { faICursor, faPlus, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faSave, faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||
import XBlock from './page.block.vue';
|
||||
import { AiScript } from '../../../scripts/aiscript';
|
||||
import { collectPageVars } from '../../../scripts/collect-page-vars';
|
||||
|
||||
class Script {
|
||||
public aiScript: AiScript;
|
||||
public vars: any;
|
||||
|
||||
constructor(aiScript) {
|
||||
this.aiScript = aiScript;
|
||||
this.vars = this.aiScript.evaluateVars();
|
||||
}
|
||||
|
||||
public reEval() {
|
||||
this.vars = this.aiScript.evaluateVars();
|
||||
}
|
||||
|
||||
public interpolate(str: string) {
|
||||
return str.replace(/\{(.+?)\}/g, match =>
|
||||
(this.vars.find(x => x.name === match.slice(1, -1).trim()).value || '').toString());
|
||||
}
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('pages'),
|
||||
|
||||
components: {
|
||||
XBlock
|
||||
},
|
||||
|
||||
props: {
|
||||
pageName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
page: null,
|
||||
script: null,
|
||||
faPlus, faICursor, faSave, faStickyNote, faSquareRootAlt
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$root.api('pages/show', {
|
||||
name: this.pageName,
|
||||
username: this.username,
|
||||
}).then(page => {
|
||||
this.page = page;
|
||||
const pageVars = this.getPageVars();
|
||||
this.script = new Script(new AiScript(this.page.variables, pageVars, {
|
||||
randomSeed: Math.random(),
|
||||
user: page.user,
|
||||
visitor: this.$store.state.i
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
getPageVars() {
|
||||
return collectPageVars(this.page.content);
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.iroscrza
|
||||
overflow hidden
|
||||
background var(--face)
|
||||
|
||||
&.center
|
||||
text-align center
|
||||
|
||||
&.round
|
||||
border-radius 6px
|
||||
|
||||
&.shadow
|
||||
box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
|
||||
|
||||
> header
|
||||
> .title
|
||||
z-index 1
|
||||
margin 0
|
||||
padding 32px 64px
|
||||
font-size 24px
|
||||
font-weight bold
|
||||
color var(--text)
|
||||
box-shadow 0 var(--lineWidth) rgba(#000, 0.07)
|
||||
|
||||
@media (max-width 600px)
|
||||
padding 16px 32px
|
||||
font-size 20px
|
||||
|
||||
> div
|
||||
color var(--text)
|
||||
padding 48px 64px
|
||||
font-size 18px
|
||||
|
||||
@media (max-width 600px)
|
||||
padding 24px 32px
|
||||
font-size 16px
|
||||
|
||||
> footer
|
||||
color var(--text)
|
||||
padding 0 64px 38px 64px
|
||||
|
||||
@media (max-width 600px)
|
||||
padding 0 32px 28px 32px
|
||||
|
||||
> small
|
||||
display block
|
||||
opacity 0.5
|
||||
|
||||
</style>
|
@ -18,7 +18,8 @@
|
||||
<p class="fetching" v-if="fetching">{{ $t('fetching') }}<mk-ellipsis/></p>
|
||||
<h1 v-if="!fetching">{{ announcements.length == 0 ? $t('no-broadcasts') : announcements[i].title }}</h1>
|
||||
<p v-if="!fetching">
|
||||
<span v-if="announcements.length != 0" v-html="announcements[i].text"></span>
|
||||
<mfm v-if="announcements.length != 0" :text="announcements[i].text" :key="i"/>
|
||||
<img v-if="announcements.length != 0 && announcements[i].image" :src="announcements[i].image" alt="" style="display: block; max-height: 130px; max-width: 100%;"/>
|
||||
<template v-if="announcements.length == 0">{{ $t('have-a-nice-day') }}</template>
|
||||
</p>
|
||||
<a v-if="announcements.length > 1" @click="next">{{ $t('next') }} >></a>
|
||||
|
@ -156,7 +156,11 @@ init(async (launch, os) => {
|
||||
{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
|
||||
{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
|
||||
{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) },
|
||||
{ path: '/i/pages', component: () => import('./views/home/pages.vue').then(m => m.default) },
|
||||
]},
|
||||
{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) },
|
||||
{ path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/i/pages/edit/:page', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
||||
{ path: '/i/drive', component: MkDrive },
|
||||
{ path: '/i/drive/folder/:folder', component: MkDrive },
|
||||
|
@ -105,9 +105,9 @@ export default Vue.extend({
|
||||
this.init();
|
||||
},
|
||||
|
||||
init() {
|
||||
async init() {
|
||||
this.fetching = true;
|
||||
this.makePromise().then(x => {
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.notes = x;
|
||||
} else {
|
||||
@ -122,7 +122,7 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
fetchMore() {
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
|
@ -228,7 +228,7 @@ export default Vue.extend({
|
||||
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftId];
|
||||
if (draft) {
|
||||
this.text = draft.data.text;
|
||||
this.files = draft.data.files;
|
||||
this.files = (draft.data.files || []).filter(e => e);
|
||||
if (draft.data.poll) {
|
||||
this.poll = true;
|
||||
this.$nextTick(() => {
|
||||
|
@ -9,35 +9,42 @@
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="`/@${ $store.state.i.username }`">
|
||||
<i><fa icon="user"/></i>
|
||||
<i><fa icon="user" fixed-width/></i>
|
||||
<span>{{ $t('profile') }}</span>
|
||||
<i><fa icon="angle-right"/></i>
|
||||
</router-link>
|
||||
</li>
|
||||
<li @click="drive">
|
||||
<p>
|
||||
<i><fa icon="cloud"/></i>
|
||||
<i><fa icon="cloud" fixed-width/></i>
|
||||
<span>{{ $t('@.drive') }}</span>
|
||||
<i><fa icon="angle-right"/></i>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/i/favorites">
|
||||
<i><fa icon="star"/></i>
|
||||
<i><fa icon="star" fixed-width/></i>
|
||||
<span>{{ $t('@.favorites') }}</span>
|
||||
<i><fa icon="angle-right"/></i>
|
||||
</router-link>
|
||||
</li>
|
||||
<li @click="list">
|
||||
<p>
|
||||
<i><fa icon="list"/></i>
|
||||
<i><fa icon="list" fixed-width/></i>
|
||||
<span>{{ $t('lists') }}</span>
|
||||
<i><fa icon="angle-right"/></i>
|
||||
</p>
|
||||
</li>
|
||||
<li @click="page">
|
||||
<router-link to="/i/pages">
|
||||
<i><fa :icon="faStickyNote" fixed-width/></i>
|
||||
<span>{{ $t('@.pages') }}</span>
|
||||
<i><fa icon="angle-right"/></i>
|
||||
</router-link>
|
||||
</li>
|
||||
<li @click="followRequests" v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
|
||||
<p>
|
||||
<i><fa :icon="['far', 'envelope']"/></i>
|
||||
<i><fa :icon="['far', 'envelope']" fixed-width/></i>
|
||||
<span>{{ $t('follow-requests') }}<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>
|
||||
<i><fa icon="angle-right"/></i>
|
||||
</p>
|
||||
@ -46,14 +53,14 @@
|
||||
<ul>
|
||||
<li>
|
||||
<router-link to="/i/settings">
|
||||
<i><fa icon="cog"/></i>
|
||||
<i><fa icon="cog" fixed-width/></i>
|
||||
<span>{{ $t('@.settings') }}</span>
|
||||
<i><fa icon="angle-right"/></i>
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="$store.state.i.isAdmin || $store.state.i.isModerator">
|
||||
<a href="/admin">
|
||||
<i><fa icon="terminal"/></i>
|
||||
<i><fa icon="terminal" fixed-width/></i>
|
||||
<span>{{ $t('admin') }}</span>
|
||||
<i><fa icon="angle-right"/></i>
|
||||
</a>
|
||||
@ -76,7 +83,7 @@
|
||||
<ul>
|
||||
<li @click="signout">
|
||||
<p class="signout">
|
||||
<i><fa icon="power-off"/></i>
|
||||
<i><fa icon="power-off" fixed-width/></i>
|
||||
<span>{{ $t('@.signout') }}</span>
|
||||
</p>
|
||||
</li>
|
||||
@ -95,14 +102,14 @@ import MkFollowRequestsWindow from './received-follow-requests-window.vue';
|
||||
import MkDriveWindow from './drive-window.vue';
|
||||
import contains from '../../../common/scripts/contains';
|
||||
import { faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/components/ui.header.account.vue'),
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
faHome, faColumns, faMoon, faSun
|
||||
faHome, faColumns, faMoon, faSun, faStickyNote
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
92
src/client/app/desktop/views/home/pages.vue
Normal file
92
src/client/app/desktop/views/home/pages.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="rknalgpo" v-if="!fetching">
|
||||
<ui-button @click="create()"><fa :icon="faPlus"/></ui-button>
|
||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||
<template v-for="page in pages">
|
||||
<x-page-preview class="page" :page="page" :key="page.id"/>
|
||||
</template>
|
||||
</sequential-entrance>
|
||||
<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||
import XPagePreview from '../../../common/views/components/page-preview.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
components: {
|
||||
XPagePreview
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
pages: [],
|
||||
existMore: false,
|
||||
moreFetching: false,
|
||||
faStickyNote, faPlus
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
Progress.start();
|
||||
this.fetching = true;
|
||||
|
||||
this.$root.api('i/pages', {
|
||||
limit: 11
|
||||
}).then(pages => {
|
||||
if (pages.length == 11) {
|
||||
this.existMore = true;
|
||||
pages.pop();
|
||||
}
|
||||
|
||||
this.pages = pages;
|
||||
this.fetching = false;
|
||||
|
||||
Progress.done();
|
||||
});
|
||||
},
|
||||
fetchMore() {
|
||||
this.moreFetching = true;
|
||||
this.$root.api('i/pages', {
|
||||
limit: 11,
|
||||
untilId: this.pages[this.pages.length - 1].id
|
||||
}).then(pages => {
|
||||
if (pages.length == 11) {
|
||||
this.existMore = true;
|
||||
pages.pop();
|
||||
} else {
|
||||
this.existMore = false;
|
||||
}
|
||||
|
||||
this.pages = this.pages.concat(pages);
|
||||
this.moreFetching = false;
|
||||
});
|
||||
},
|
||||
create() {
|
||||
this.$router.push(`/i/pages/new`);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.rknalgpo
|
||||
margin 0 auto
|
||||
|
||||
> * > .page
|
||||
margin-bottom 8px
|
||||
|
||||
@media (min-width 500px)
|
||||
> * > .page
|
||||
margin-bottom 16px
|
||||
|
||||
</style>
|
@ -14,6 +14,7 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
@ -21,10 +22,10 @@ export default Vue.extend({
|
||||
i18n: i18n('desktop/views/pages/search.vue'),
|
||||
data() {
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('notes/search', {
|
||||
makePromise: async cursor => this.$root.api('notes/search', {
|
||||
limit: limit + 1,
|
||||
offset: cursor ? cursor : undefined,
|
||||
query: this.q
|
||||
...(await genSearchQuery(this, this.q))
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
|
@ -123,10 +123,10 @@ export default Vue.extend({
|
||||
this.$root.dialog({
|
||||
title: this.$t('list-name'),
|
||||
input: true
|
||||
}).then(async ({ canceled, result: title }) => {
|
||||
}).then(async ({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
const list = await this.$root.api('users/lists/create', {
|
||||
title
|
||||
name
|
||||
});
|
||||
|
||||
this.list = list;
|
||||
|
32
src/client/app/desktop/views/pages/page-editor.vue
Normal file
32
src/client/app/desktop/views/pages/page-editor.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<mk-ui>
|
||||
<main>
|
||||
<x-page-editor :page="page"/>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XPageEditor: () => import('../../../common/views/components/page-editor/page-editor.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
props: {
|
||||
page: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
margin 0 auto
|
||||
padding 16px
|
||||
max-width 900px
|
||||
|
||||
</style>
|
36
src/client/app/desktop/views/pages/page.vue
Normal file
36
src/client/app/desktop/views/pages/page.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<mk-ui>
|
||||
<main>
|
||||
<x-page :page-name="page" :username="user"/>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XPage: () => import('../../../common/views/pages/page/page.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
props: {
|
||||
page: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
margin 0 auto
|
||||
padding 16px
|
||||
max-width 950px
|
||||
|
||||
</style>
|
@ -44,7 +44,8 @@
|
||||
<div v-if="announcements && announcements.length > 0">
|
||||
<div v-for="announcement in announcements">
|
||||
<h1 v-html="announcement.title"></h1>
|
||||
<div v-html="announcement.text"></div>
|
||||
<mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 130px; max-width: 100%;"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -135,6 +135,7 @@ init((launch, os) => {
|
||||
{ path: '/signup', name: 'signup', component: MkSignup },
|
||||
{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) },
|
||||
{ path: '/i/favorites', name: 'favorites', component: MkFavorites },
|
||||
{ path: '/i/pages', name: 'pages', component: () => import('./views/pages/pages.vue').then(m => m.default) },
|
||||
{ path: '/i/lists', name: 'user-lists', component: MkUserLists },
|
||||
{ path: '/i/lists/:list', name: 'user-list', component: MkUserList },
|
||||
{ path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests },
|
||||
@ -144,6 +145,8 @@ init((launch, os) => {
|
||||
{ path: '/i/drive', name: 'drive', component: MkDrive },
|
||||
{ path: '/i/drive/folder/:folder', component: MkDrive },
|
||||
{ path: '/i/drive/file/:file', component: MkDrive },
|
||||
{ path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/i/pages/edit/:page', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/selectdrive', component: MkSelectDrive },
|
||||
{ path: '/search', component: MkSearch },
|
||||
{ path: '/tags/:tag', component: MkTag },
|
||||
@ -156,6 +159,7 @@ init((launch, os) => {
|
||||
{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
|
||||
{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
|
||||
]},
|
||||
{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) },
|
||||
{ path: '/notes/:note', component: MkNote },
|
||||
{ path: '/authorize-follow', component: MkFollow },
|
||||
{ path: '*', component: MkNotFound }
|
||||
|
@ -215,11 +215,6 @@ export default Vue.extend({
|
||||
@media (min-width 500px)
|
||||
padding 28px 32px 18px 32px
|
||||
|
||||
&:after
|
||||
content ""
|
||||
display block
|
||||
clear both
|
||||
|
||||
> header
|
||||
display flex
|
||||
line-height 1.1em
|
||||
@ -236,6 +231,7 @@ export default Vue.extend({
|
||||
height 60px
|
||||
|
||||
> div
|
||||
min-width 0
|
||||
|
||||
> .name
|
||||
display inline-block
|
||||
|
@ -106,9 +106,9 @@ export default Vue.extend({
|
||||
this.init();
|
||||
},
|
||||
|
||||
init() {
|
||||
async init() {
|
||||
this.fetching = true;
|
||||
this.makePromise().then(x => {
|
||||
await (this.makePromise()).then(x => {
|
||||
if (Array.isArray(x)) {
|
||||
this.notes = x;
|
||||
} else {
|
||||
@ -123,10 +123,10 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
fetchMore() {
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
await (this.makePromise(this.notes[this.notes.length - 1].id)).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
this.more = x.more;
|
||||
this.moreFetching = false;
|
||||
|
@ -29,6 +29,7 @@
|
||||
<li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('@.favorites') }}<i><fa icon="angle-right"/></i></router-link></li>
|
||||
<li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li>
|
||||
<li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li>
|
||||
<li><router-link to="/i/pages" :data-active="$route.name == 'pages'"><i><fa :icon="faStickyNote" fixed-width/></i>{{ $t('@.pages') }}<i><fa icon="angle-right"/></i></router-link></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li>
|
||||
@ -43,7 +44,8 @@
|
||||
<div class="announcements" v-if="announcements && announcements.length > 0">
|
||||
<article v-for="announcement in announcements">
|
||||
<span v-html="announcement.title" class="title"></span>
|
||||
<div v-html="announcement.text"></div>
|
||||
<div><mfm :text="announcement.text"/></div>
|
||||
<img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/>
|
||||
</article>
|
||||
</div>
|
||||
<a :href="aboutUrl"><p class="about">{{ $t('about') }}</p></a>
|
||||
@ -65,7 +67,7 @@ import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import { lang } from '../../../config';
|
||||
import { faNewspaper, faHashtag, faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||
import { search } from '../../../common/scripts/search';
|
||||
|
||||
export default Vue.extend({
|
||||
@ -85,7 +87,7 @@ export default Vue.extend({
|
||||
announcements: [],
|
||||
searching: false,
|
||||
showNotifications: false,
|
||||
faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns
|
||||
faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns, faStickyNote
|
||||
};
|
||||
},
|
||||
|
||||
|
32
src/client/app/mobile/views/pages/page-editor.vue
Normal file
32
src/client/app/mobile/views/pages/page-editor.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<mk-ui>
|
||||
<main>
|
||||
<x-page-editor :page="page"/>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XPageEditor: () => import('../../../common/views/components/page-editor/page-editor.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
props: {
|
||||
page: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
margin 0 auto
|
||||
padding 16px
|
||||
max-width 1000px
|
||||
|
||||
</style>
|
36
src/client/app/mobile/views/pages/page.vue
Normal file
36
src/client/app/mobile/views/pages/page.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<mk-ui>
|
||||
<main>
|
||||
<x-page :page-name="page" :username="user"/>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XPage: () => import('../../../common/views/pages/page/page.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
props: {
|
||||
page: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
margin 0 auto
|
||||
padding 16px
|
||||
max-width 1000px
|
||||
|
||||
</style>
|
94
src/client/app/mobile/views/pages/pages.vue
Normal file
94
src/client/app/mobile/views/pages/pages.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<mk-ui>
|
||||
<template #header><span style="margin-right:4px;"><fa :icon="faStickyNote"/></span>{{ $t('@.pages') }}</template>
|
||||
|
||||
<main>
|
||||
<ui-button @click="create()"><fa :icon="faPlus"/></ui-button>
|
||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||
<template v-for="page in pages">
|
||||
<x-page-preview class="page" :page="page" :key="page.id"/>
|
||||
</template>
|
||||
</sequential-entrance>
|
||||
<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||
import XPagePreview from '../../../common/views/components/page-preview.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n(),
|
||||
components: {
|
||||
XPagePreview
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
pages: [],
|
||||
existMore: false,
|
||||
moreFetching: false,
|
||||
faStickyNote, faPlus
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
Progress.start();
|
||||
this.fetching = true;
|
||||
|
||||
this.$root.api('i/pages', {
|
||||
limit: 11
|
||||
}).then(pages => {
|
||||
if (pages.length == 11) {
|
||||
this.existMore = true;
|
||||
pages.pop();
|
||||
}
|
||||
|
||||
this.pages = pages;
|
||||
this.fetching = false;
|
||||
|
||||
Progress.done();
|
||||
});
|
||||
},
|
||||
fetchMore() {
|
||||
this.moreFetching = true;
|
||||
this.$root.api('i/pages', {
|
||||
limit: 11,
|
||||
untilId: this.pages[this.pages.length - 1].id
|
||||
}).then(pages => {
|
||||
if (pages.length == 11) {
|
||||
this.existMore = true;
|
||||
pages.pop();
|
||||
} else {
|
||||
this.existMore = false;
|
||||
}
|
||||
|
||||
this.pages = this.pages.concat(pages);
|
||||
this.moreFetching = false;
|
||||
});
|
||||
},
|
||||
create() {
|
||||
this.$router.push(`/i/pages/new`);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
> * > .page
|
||||
margin-bottom 8px
|
||||
|
||||
@media (min-width 500px)
|
||||
> * > .page
|
||||
margin-bottom 16px
|
||||
|
||||
</style>
|
@ -12,6 +12,7 @@
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||
|
||||
const limit = 20;
|
||||
|
||||
@ -19,10 +20,10 @@ export default Vue.extend({
|
||||
i18n: i18n('mobile/views/pages/search.vue'),
|
||||
data() {
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('notes/search', {
|
||||
makePromise: async cursor => this.$root.api('notes/search', {
|
||||
limit: limit + 1,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
query: this.q
|
||||
...(await genSearchQuery(this, this.q))
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
|
@ -18,7 +18,7 @@
|
||||
</div>
|
||||
<div class="title">
|
||||
<h1><mk-user-name :user="user" :key="user.id"/></h1>
|
||||
<span class="username"><mk-acct :user="user" :detail="true" /></span>
|
||||
<span class="username"><mk-acct :user="user" :detail="true" :key="user.id"/></span>
|
||||
<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
|
@ -29,7 +29,8 @@
|
||||
<div class="announcements" v-if="announcements && announcements.length > 0">
|
||||
<article v-for="announcement in announcements">
|
||||
<span class="title" v-html="announcement.title"></span>
|
||||
<div v-html="announcement.text"></div>
|
||||
<mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/>
|
||||
</article>
|
||||
</div>
|
||||
<article class="about-misskey">
|
||||
|
@ -19,7 +19,7 @@ export const cafeTheme: Theme = require('../themes/cafe.json5');
|
||||
export const japaneseSushiSetTheme: Theme = require('../themes/japanese-sushi-set.json5');
|
||||
export const gruvboxDarkTheme: Theme = require('../themes/gruvbox-dark.json5');
|
||||
export const monokaiTheme: Theme = require('../themes/monokai.json5');
|
||||
export const colorfulTheme: Theme = require('../themes/colorful.json5');
|
||||
export const vividTheme: Theme = require('../themes/vivid.json5');
|
||||
export const rainyTheme: Theme = require('../themes/rainy.json5');
|
||||
export const mauveTheme: Theme = require('../themes/mauve.json5');
|
||||
export const grayTheme: Theme = require('../themes/gray.json5');
|
||||
@ -35,7 +35,7 @@ export const builtinThemes = [
|
||||
japaneseSushiSetTheme,
|
||||
gruvboxDarkTheme,
|
||||
monokaiTheme,
|
||||
colorfulTheme,
|
||||
vividTheme,
|
||||
rainyTheme,
|
||||
mauveTheme,
|
||||
grayTheme,
|
||||
|
@ -232,5 +232,8 @@
|
||||
adminDashboardCardBg: '$secondary',
|
||||
adminDashboardCardFg: '$text',
|
||||
adminDashboardCardDivider: 'rgba(0, 0, 0, 0.3)',
|
||||
|
||||
pageBlockBorder: 'rgba(255, 255, 255, 0.1)',
|
||||
pageBlockBorderHover: 'rgba(255, 255, 255, 0.15)',
|
||||
},
|
||||
}
|
||||
|
@ -232,5 +232,8 @@
|
||||
adminDashboardCardBg: '$secondary',
|
||||
adminDashboardCardFg: '$text',
|
||||
adminDashboardCardDivider: 'rgba(0, 0, 0, 0.082)',
|
||||
|
||||
pageBlockBorder: 'rgba(0, 0, 0, 0.1)',
|
||||
pageBlockBorderHover: 'rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
id: '2d066d6e-bd39-4f23-bd48-686d5c1c6ae8',
|
||||
|
||||
name: 'Colorful',
|
||||
name: 'Vivid',
|
||||
author: 'syuilo',
|
||||
|
||||
base: 'light',
|
@ -1,41 +1,30 @@
|
||||
import * as elasticsearch from 'elasticsearch';
|
||||
import * as elasticsearch from '@elastic/elasticsearch';
|
||||
import config from '../config';
|
||||
import Logger from '../services/logger';
|
||||
|
||||
const esLogger = new Logger('es');
|
||||
|
||||
const index = {
|
||||
settings: {
|
||||
analysis: {
|
||||
normalizer: {
|
||||
lowercase_normalizer: {
|
||||
type: 'custom',
|
||||
filter: ['lowercase']
|
||||
}
|
||||
},
|
||||
analyzer: {
|
||||
bigram: {
|
||||
tokenizer: 'bigram_tokenizer'
|
||||
}
|
||||
},
|
||||
tokenizer: {
|
||||
bigram_tokenizer: {
|
||||
type: 'nGram',
|
||||
min_gram: 2,
|
||||
max_gram: 2
|
||||
ngram: {
|
||||
tokenizer: 'ngram'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mappings: {
|
||||
note: {
|
||||
properties: {
|
||||
text: {
|
||||
type: 'text',
|
||||
index: true,
|
||||
analyzer: 'bigram',
|
||||
normalizer: 'lowercase_normalizer'
|
||||
}
|
||||
properties: {
|
||||
text: {
|
||||
type: 'text',
|
||||
index: true,
|
||||
analyzer: 'ngram',
|
||||
},
|
||||
userId: {
|
||||
type: 'keyword',
|
||||
index: true,
|
||||
},
|
||||
userHost: {
|
||||
type: 'keyword',
|
||||
index: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,31 +32,20 @@ const index = {
|
||||
|
||||
// Init ElasticSearch connection
|
||||
const client = config.elasticsearch ? new elasticsearch.Client({
|
||||
host: `${config.elasticsearch.host}:${config.elasticsearch.port}`
|
||||
node: `http://${config.elasticsearch.host}:${config.elasticsearch.port}`,
|
||||
pingTimeout: 30000
|
||||
}) : null;
|
||||
|
||||
if (client) {
|
||||
// Send a HEAD request
|
||||
client.ping({
|
||||
// Ping usually has a 3000ms timeout
|
||||
requestTimeout: 30000
|
||||
}, error => {
|
||||
if (error) {
|
||||
esLogger.error('elasticsearch is down!');
|
||||
} else {
|
||||
esLogger.succ('elasticsearch is available!');
|
||||
}
|
||||
});
|
||||
|
||||
client.indices.exists({
|
||||
index: 'misskey'
|
||||
index: 'misskey_note'
|
||||
}).then(exist => {
|
||||
if (exist) return;
|
||||
|
||||
client.indices.create({
|
||||
index: 'misskey',
|
||||
body: index
|
||||
});
|
||||
if (!exist.body) {
|
||||
client.indices.create({
|
||||
index: 'misskey_note',
|
||||
body: index
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,7 @@ import { Poll } from '../models/entities/poll';
|
||||
import { UserKeypair } from '../models/entities/user-keypair';
|
||||
import { UserPublickey } from '../models/entities/user-publickey';
|
||||
import { UserProfile } from '../models/entities/user-profile';
|
||||
import { Page } from '../models/entities/page';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||
|
||||
@ -114,6 +115,7 @@ export function initDb(justBorrow = false, sync = false, log = false) {
|
||||
NoteReaction,
|
||||
NoteWatching,
|
||||
NoteUnread,
|
||||
Page,
|
||||
Log,
|
||||
DriveFile,
|
||||
DriveFolder,
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { Meta } from '../models/entities/meta';
|
||||
import { getConnection } from 'typeorm';
|
||||
|
||||
export default async function(): Promise<Meta> {
|
||||
let cache: Meta;
|
||||
|
||||
export async function fetchMeta(noCache = false): Promise<Meta> {
|
||||
if (!noCache && cache) return cache;
|
||||
|
||||
return await getConnection().transaction(async transactionalEntityManager => {
|
||||
// バグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
||||
const meta = await transactionalEntityManager.findOne(Meta, {
|
||||
@ -11,11 +15,21 @@ export default async function(): Promise<Meta> {
|
||||
});
|
||||
|
||||
if (meta) {
|
||||
cache = meta;
|
||||
return meta;
|
||||
} else {
|
||||
return await transactionalEntityManager.save(Meta, {
|
||||
const saved = await transactionalEntityManager.save(Meta, {
|
||||
id: 'x'
|
||||
}) as Meta;
|
||||
|
||||
cache = saved;
|
||||
return saved;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
fetchMeta(true).then(meta => {
|
||||
cache = meta;
|
||||
});
|
||||
}, 5000);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import fetchMeta from './fetch-meta';
|
||||
import { fetchMeta } from './fetch-meta';
|
||||
import { ILocalUser } from '../models/entities/user';
|
||||
import { Users } from '../models';
|
||||
import { ensure } from '../prelude/ensure';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { emojiRegex } from './emoji-regex';
|
||||
import fetchMeta from './fetch-meta';
|
||||
import { fetchMeta } from './fetch-meta';
|
||||
import { Emojis } from '../models';
|
||||
|
||||
const basic10: Record<string, string> = {
|
||||
|
@ -1,14 +1,46 @@
|
||||
export const types = {
|
||||
boolean: 'boolean' as 'boolean',
|
||||
string: 'string' as 'string',
|
||||
number: 'number' as 'number',
|
||||
array: 'array' as 'array',
|
||||
object: 'object' as 'object',
|
||||
any: 'any' as 'any',
|
||||
};
|
||||
|
||||
export const bool = {
|
||||
true: true as true,
|
||||
false: false as false,
|
||||
};
|
||||
|
||||
export type Schema = {
|
||||
type: 'number' | 'string' | 'array' | 'object' | any;
|
||||
optional?: boolean;
|
||||
type: 'boolean' | 'number' | 'string' | 'array' | 'object' | 'any';
|
||||
nullable: boolean;
|
||||
optional: boolean;
|
||||
items?: Schema;
|
||||
properties?: Obj;
|
||||
description?: string;
|
||||
example?: any;
|
||||
format?: string;
|
||||
ref?: string;
|
||||
enum?: string[];
|
||||
};
|
||||
|
||||
type NonUndefinedPropertyNames<T extends Obj> = {
|
||||
[K in keyof T]: T[K]['optional'] extends true ? never : K
|
||||
}[keyof T];
|
||||
|
||||
type UndefinedPropertyNames<T extends Obj> = {
|
||||
[K in keyof T]: T[K]['optional'] extends true ? K : never
|
||||
}[keyof T];
|
||||
|
||||
type OnlyRequired<T extends Obj> = Pick<T, NonUndefinedPropertyNames<T>>;
|
||||
type OnlyOptional<T extends Obj> = Pick<T, UndefinedPropertyNames<T>>;
|
||||
|
||||
export type Obj = { [key: string]: Schema };
|
||||
|
||||
export type ObjType<s extends Obj> = { [P in keyof s]: SchemaType<s[P]> };
|
||||
export type ObjType<s extends Obj> =
|
||||
{ [P in keyof OnlyOptional<s>]?: SchemaType<s[P]> } &
|
||||
{ [P in keyof OnlyRequired<s>]: SchemaType<s[P]> };
|
||||
|
||||
// https://qiita.com/hrsh7th@github/items/84e8968c3601009cdcf2
|
||||
type MyType<T extends Schema> = {
|
||||
@ -16,26 +48,20 @@ type MyType<T extends Schema> = {
|
||||
1: SchemaType<T>;
|
||||
}[T extends Schema ? 1 : 0];
|
||||
|
||||
export type SchemaType<p extends Schema> =
|
||||
p['type'] extends 'number' ? number :
|
||||
p['type'] extends 'string' ? string :
|
||||
p['type'] extends 'array' ? MyType<NonNullable<p['items']>>[] :
|
||||
p['type'] extends 'object' ? ObjType<NonNullable<p['properties']>> :
|
||||
any;
|
||||
type NullOrUndefined<p extends Schema, T> =
|
||||
p['nullable'] extends true
|
||||
? p['optional'] extends true
|
||||
? (T | null | undefined)
|
||||
: (T | null)
|
||||
: p['optional'] extends true
|
||||
? (T | undefined)
|
||||
: T;
|
||||
|
||||
export function convertOpenApiSchema(schema: Schema) {
|
||||
const x = JSON.parse(JSON.stringify(schema)); // copy
|
||||
if (!['string', 'number', 'boolean', 'array', 'object'].includes(x.type)) {
|
||||
x['$ref'] = `#/components/schemas/${x.type}`;
|
||||
}
|
||||
if (x.type === 'array' && x.items) {
|
||||
x.items = convertOpenApiSchema(x.items);
|
||||
}
|
||||
if (x.type === 'object' && x.properties) {
|
||||
x.required = Object.entries(x.properties).filter(([k, v]: any) => !v.isOptional).map(([k, v]: any) => k);
|
||||
for (const k of Object.keys(x.properties)) {
|
||||
x.properties[k] = convertOpenApiSchema(x.properties[k]);
|
||||
}
|
||||
}
|
||||
return x;
|
||||
}
|
||||
export type SchemaType<p extends Schema> =
|
||||
p['type'] extends 'number' ? NullOrUndefined<p, number> :
|
||||
p['type'] extends 'string' ? NullOrUndefined<p, string> :
|
||||
p['type'] extends 'boolean' ? NullOrUndefined<p, boolean> :
|
||||
p['type'] extends 'array' ? NullOrUndefined<p, MyType<NonNullable<p['items']>>[]> :
|
||||
p['type'] extends 'object' ? NullOrUndefined<p, ObjType<NonNullable<p['properties']>>> :
|
||||
p['type'] extends 'any' ? NullOrUndefined<p, any> :
|
||||
any;
|
||||
|
@ -36,7 +36,7 @@ export class NoteReaction {
|
||||
public note: Note | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32
|
||||
length: 128
|
||||
})
|
||||
public reaction: string;
|
||||
}
|
||||
|
105
src/models/entities/page.ts
Normal file
105
src/models/entities/page.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { User } from './user';
|
||||
import { id } from '../id';
|
||||
import { DriveFile } from './drive-file';
|
||||
|
||||
@Entity()
|
||||
@Index(['userId', 'name'], { unique: true })
|
||||
export class Page {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the Page.'
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The updated date of the Page.'
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public title: string;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, nullable: true
|
||||
})
|
||||
public summary: string | null;
|
||||
|
||||
@Column('boolean')
|
||||
public alignCenter: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32,
|
||||
})
|
||||
public font: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The ID of author.'
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public eyeCatchingImageId: DriveFile['id'] | null;
|
||||
|
||||
@ManyToOne(type => DriveFile, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public eyeCatchingImage: DriveFile | null;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: []
|
||||
})
|
||||
public content: Record<string, any>[];
|
||||
|
||||
@Column('jsonb', {
|
||||
default: []
|
||||
})
|
||||
public variables: Record<string, any>[];
|
||||
|
||||
/**
|
||||
* public ... 公開
|
||||
* followers ... フォロワーのみ
|
||||
* specified ... visibleUserIds で指定したユーザーのみ
|
||||
*/
|
||||
@Column('enum', { enum: ['public', 'followers', 'specified'] })
|
||||
public visibility: 'public' | 'followers' | 'specified';
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
array: true, default: '{}'
|
||||
})
|
||||
public visibleUserIds: User['id'][];
|
||||
|
||||
constructor(data: Partial<Page>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,6 @@ import { Meta } from './entities/meta';
|
||||
import { SwSubscription } from './entities/sw-subscription';
|
||||
import { NoteWatching } from './entities/note-watching';
|
||||
import { UserListJoining } from './entities/user-list-joining';
|
||||
import { Hashtag } from './entities/hashtag';
|
||||
import { NoteUnread } from './entities/note-unread';
|
||||
import { RegistrationTicket } from './entities/registration-tickets';
|
||||
import { UserRepository } from './repositories/user';
|
||||
@ -35,6 +34,8 @@ import { FollowingRepository } from './repositories/following';
|
||||
import { AbuseUserReportRepository } from './repositories/abuse-user-report';
|
||||
import { AuthSessionRepository } from './repositories/auth-session';
|
||||
import { UserProfile } from './entities/user-profile';
|
||||
import { HashtagRepository } from './repositories/hashtag';
|
||||
import { PageRepository } from './repositories/page';
|
||||
|
||||
export const Apps = getCustomRepository(AppRepository);
|
||||
export const Notes = getCustomRepository(NoteRepository);
|
||||
@ -62,7 +63,7 @@ export const Metas = getRepository(Meta);
|
||||
export const Mutings = getCustomRepository(MutingRepository);
|
||||
export const Blockings = getCustomRepository(BlockingRepository);
|
||||
export const SwSubscriptions = getRepository(SwSubscription);
|
||||
export const Hashtags = getRepository(Hashtag);
|
||||
export const Hashtags = getCustomRepository(HashtagRepository);
|
||||
export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository);
|
||||
export const RegistrationTickets = getRepository(RegistrationTicket);
|
||||
export const AuthSessions = getCustomRepository(AuthSessionRepository);
|
||||
@ -72,3 +73,4 @@ export const MessagingMessages = getCustomRepository(MessagingMessageRepository)
|
||||
export const ReversiGames = getCustomRepository(ReversiGameRepository);
|
||||
export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
|
||||
export const Logs = getRepository(Log);
|
||||
export const Pages = getCustomRepository(PageRepository);
|
||||
|
@ -1,23 +1,17 @@
|
||||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { Users } from '..';
|
||||
import rap from '@prezzemolo/rap';
|
||||
import { AbuseUserReport } from '../entities/abuse-user-report';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
|
||||
@EntityRepository(AbuseUserReport)
|
||||
export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
|
||||
public packMany(
|
||||
reports: any[],
|
||||
) {
|
||||
return Promise.all(reports.map(x => this.pack(x)));
|
||||
}
|
||||
|
||||
public async pack(
|
||||
src: AbuseUserReport['id'] | AbuseUserReport,
|
||||
) {
|
||||
const report = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
|
||||
return await rap({
|
||||
return await awaitAll({
|
||||
id: report.id,
|
||||
createdAt: report.createdAt,
|
||||
reporterId: report.reporterId,
|
||||
@ -30,4 +24,10 @@ export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
public packMany(
|
||||
reports: any[],
|
||||
) {
|
||||
return Promise.all(reports.map(x => this.pack(x)));
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ import { EntityRepository, Repository } from 'typeorm';
|
||||
import { App } from '../entities/app';
|
||||
import { AccessTokens } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { types, bool, SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedApp = SchemaType<typeof packedAppSchema>;
|
||||
|
||||
@EntityRepository(App)
|
||||
export class AppRepository extends Repository<App> {
|
||||
@ -13,7 +16,7 @@ export class AppRepository extends Repository<App> {
|
||||
includeSecret?: boolean,
|
||||
includeProfileImageIds?: boolean
|
||||
}
|
||||
) {
|
||||
): Promise<PackedApp> {
|
||||
const opts = Object.assign({
|
||||
detail: false,
|
||||
includeSecret: false,
|
||||
@ -37,3 +40,40 @@ export class AppRepository extends Repository<App> {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const packedAppSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this Note.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
name: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'アプリケーションの名前'
|
||||
},
|
||||
callbackUrl: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
description: 'コールバックするURL'
|
||||
},
|
||||
permission: {
|
||||
type: types.array,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
items: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
}
|
||||
},
|
||||
secret: {
|
||||
type: types.string,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
description: 'アプリケーションのシークレットキー'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { Apps } from '..';
|
||||
import rap from '@prezzemolo/rap';
|
||||
import { AuthSession } from '../entities/auth-session';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
|
||||
@EntityRepository(AuthSession)
|
||||
export class AuthSessionRepository extends Repository<AuthSession> {
|
||||
@ -12,7 +12,7 @@ export class AuthSessionRepository extends Repository<AuthSession> {
|
||||
) {
|
||||
const session = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
|
||||
return await rap({
|
||||
return await awaitAll({
|
||||
id: session.id,
|
||||
app: Apps.pack(session.appId, me),
|
||||
token: session.token
|
||||
|
@ -1,29 +1,65 @@
|
||||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { Users } from '..';
|
||||
import rap from '@prezzemolo/rap';
|
||||
import { Blocking } from '../entities/blocking';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { SchemaType, types, bool } from '../../misc/schema';
|
||||
|
||||
export type PackedBlocking = SchemaType<typeof packedBlockingSchema>;
|
||||
|
||||
@EntityRepository(Blocking)
|
||||
export class BlockingRepository extends Repository<Blocking> {
|
||||
public async pack(
|
||||
src: Blocking['id'] | Blocking,
|
||||
me?: any
|
||||
): Promise<PackedBlocking> {
|
||||
const blocking = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
|
||||
return await awaitAll({
|
||||
id: blocking.id,
|
||||
createdAt: blocking.createdAt.toISOString(),
|
||||
blockeeId: blocking.blockeeId,
|
||||
blockee: Users.pack(blocking.blockeeId, me, {
|
||||
detail: true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public packMany(
|
||||
blockings: any[],
|
||||
me: any
|
||||
) {
|
||||
return Promise.all(blockings.map(x => this.pack(x, me)));
|
||||
}
|
||||
|
||||
public async pack(
|
||||
src: Blocking['id'] | Blocking,
|
||||
me?: any
|
||||
) {
|
||||
const blocking = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
|
||||
return await rap({
|
||||
id: blocking.id,
|
||||
blockee: Users.pack(blocking.blockeeId, me, {
|
||||
detail: true
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const packedBlockingSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this blocking.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'date-time',
|
||||
description: 'The date that the blocking was created.'
|
||||
},
|
||||
blockeeId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'id',
|
||||
},
|
||||
blockee: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
ref: 'User',
|
||||
description: 'The blockee.'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { DriveFile } from '../entities/drive-file';
|
||||
import { Users, DriveFolders } from '..';
|
||||
import rap from '@prezzemolo/rap';
|
||||
import { User } from '../entities/user';
|
||||
import { toPuny } from '../../misc/convert-host';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { types, bool, SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedDriveFile = SchemaType<typeof packedDriveFileSchema>;
|
||||
|
||||
@EntityRepository(DriveFile)
|
||||
export class DriveFileRepository extends Repository<DriveFile> {
|
||||
@ -64,17 +67,6 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
||||
return parseInt(sum, 10) || 0;
|
||||
}
|
||||
|
||||
public packMany(
|
||||
files: any[],
|
||||
options?: {
|
||||
detail?: boolean
|
||||
self?: boolean,
|
||||
withUser?: boolean,
|
||||
}
|
||||
) {
|
||||
return Promise.all(files.map(f => this.pack(f, options)));
|
||||
}
|
||||
|
||||
public async pack(
|
||||
src: DriveFile['id'] | DriveFile,
|
||||
options?: {
|
||||
@ -82,7 +74,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
||||
self?: boolean,
|
||||
withUser?: boolean,
|
||||
}
|
||||
) {
|
||||
): Promise<PackedDriveFile> {
|
||||
const opts = Object.assign({
|
||||
detail: false,
|
||||
self: false
|
||||
@ -90,9 +82,9 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
||||
|
||||
const file = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
|
||||
return await rap({
|
||||
return await awaitAll({
|
||||
id: file.id,
|
||||
createdAt: file.createdAt,
|
||||
createdAt: file.createdAt.toISOString(),
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
md5: file.md5,
|
||||
@ -108,4 +100,78 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
||||
user: opts.withUser ? Users.pack(file.userId!) : null
|
||||
});
|
||||
}
|
||||
|
||||
public packMany(
|
||||
files: any[],
|
||||
options?: {
|
||||
detail?: boolean
|
||||
self?: boolean,
|
||||
withUser?: boolean,
|
||||
}
|
||||
) {
|
||||
return Promise.all(files.map(f => this.pack(f, options)));
|
||||
}
|
||||
}
|
||||
|
||||
export const packedDriveFileSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this Drive file.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'date-time',
|
||||
description: 'The date that the Drive file was created on Misskey.'
|
||||
},
|
||||
name: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'The file name with extension.',
|
||||
example: 'lenna.jpg'
|
||||
},
|
||||
type: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'The MIME type of this Drive file.',
|
||||
example: 'image/jpeg'
|
||||
},
|
||||
md5: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'md5',
|
||||
description: 'The MD5 hash of this Drive file.',
|
||||
example: '15eca7fba0480996e2245f5185bf39f2'
|
||||
},
|
||||
size: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'The size of this Drive file. (bytes)',
|
||||
example: 51469
|
||||
},
|
||||
url: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
format: 'url',
|
||||
description: 'The URL of this Drive file.',
|
||||
},
|
||||
folderId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
format: 'id',
|
||||
description: 'The parent folder ID of this Drive file.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
isSensitive: {
|
||||
type: types.boolean,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'Whether this Drive file is sensitive.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { DriveFolders, DriveFiles } from '..';
|
||||
import rap from '@prezzemolo/rap';
|
||||
import { DriveFolder } from '../entities/drive-folder';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { SchemaType, types, bool } from '../../misc/schema';
|
||||
|
||||
export type PackedDriveFolder = SchemaType<typeof packedDriveFolderSchema>;
|
||||
|
||||
@EntityRepository(DriveFolder)
|
||||
export class DriveFolderRepository extends Repository<DriveFolder> {
|
||||
@ -18,16 +21,16 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
|
||||
options?: {
|
||||
detail: boolean
|
||||
}
|
||||
): Promise<Record<string, any>> {
|
||||
): Promise<PackedDriveFolder> {
|
||||
const opts = Object.assign({
|
||||
detail: false
|
||||
}, options);
|
||||
|
||||
const folder = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
|
||||
return await rap({
|
||||
return await awaitAll({
|
||||
id: folder.id,
|
||||
createdAt: folder.createdAt,
|
||||
createdAt: folder.createdAt.toISOString(),
|
||||
name: folder.name,
|
||||
parentId: folder.parentId,
|
||||
|
||||
@ -48,3 +51,50 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const packedDriveFolderSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this Drive folder.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'date-time',
|
||||
description: 'The date that the Drive folder was created.'
|
||||
},
|
||||
name: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'The folder name.',
|
||||
},
|
||||
foldersCount: {
|
||||
type: types.number,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
description: 'The count of child folders.',
|
||||
},
|
||||
filesCount: {
|
||||
type: types.number,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
description: 'The count of child files.',
|
||||
},
|
||||
parentId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
format: 'id',
|
||||
description: 'The parent folder ID of this folder.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
parent: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
ref: 'DriveFolder'
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { Users } from '..';
|
||||
import rap from '@prezzemolo/rap';
|
||||
import { Following } from '../entities/following';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { SchemaType, types, bool } from '../../misc/schema';
|
||||
|
||||
type LocalFollowerFollowing = Following & {
|
||||
followerHost: null;
|
||||
@ -28,6 +29,8 @@ type RemoteFolloweeFollowing = Following & {
|
||||
followeeSharedInbox: string;
|
||||
};
|
||||
|
||||
export type PackedFollowing = SchemaType<typeof packedFollowingSchema>;
|
||||
|
||||
@EntityRepository(Following)
|
||||
export class FollowingRepository extends Repository<Following> {
|
||||
public isLocalFollower(following: Following): following is LocalFollowerFollowing {
|
||||
@ -46,6 +49,32 @@ export class FollowingRepository extends Repository<Following> {
|
||||
return following.followeeHost != null;
|
||||
}
|
||||
|
||||
public async pack(
|
||||
src: Following['id'] | Following,
|
||||
me?: any,
|
||||
opts?: {
|
||||
populateFollowee?: boolean;
|
||||
populateFollower?: boolean;
|
||||
}
|
||||
): Promise<PackedFollowing> {
|
||||
const following = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
|
||||
if (opts == null) opts = {};
|
||||
|
||||
return await awaitAll({
|
||||
id: following.id,
|
||||
createdAt: following.createdAt.toISOString(),
|
||||
followeeId: following.followeeId,
|
||||
followerId: following.followerId,
|
||||
followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, {
|
||||
detail: true
|
||||
}) : undefined,
|
||||
follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, {
|
||||
detail: true
|
||||
}) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
public packMany(
|
||||
followings: any[],
|
||||
me?: any,
|
||||
@ -56,30 +85,46 @@ export class FollowingRepository extends Repository<Following> {
|
||||
) {
|
||||
return Promise.all(followings.map(x => this.pack(x, me, opts)));
|
||||
}
|
||||
|
||||
public async pack(
|
||||
src: Following['id'] | Following,
|
||||
me?: any,
|
||||
opts?: {
|
||||
populateFollowee?: boolean;
|
||||
populateFollower?: boolean;
|
||||
}
|
||||
) {
|
||||
const following = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
|
||||
if (opts == null) opts = {};
|
||||
|
||||
return await rap({
|
||||
id: following.id,
|
||||
createdAt: following.createdAt,
|
||||
followeeId: following.followeeId,
|
||||
followerId: following.followerId,
|
||||
followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, {
|
||||
detail: true
|
||||
}) : null,
|
||||
follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, {
|
||||
detail: true
|
||||
}) : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const packedFollowingSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this following.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'date-time',
|
||||
description: 'The date that the following was created.'
|
||||
},
|
||||
followeeId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'id',
|
||||
},
|
||||
followee: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
ref: 'User',
|
||||
description: 'The followee.'
|
||||
},
|
||||
followerId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
format: 'id',
|
||||
},
|
||||
follower: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
ref: 'User',
|
||||
description: 'The follower.'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import rap from '@prezzemolo/rap';
|
||||
import { ReversiMatching } from '../../../entities/games/reversi/matching';
|
||||
import { Users } from '../../..';
|
||||
import { ensure } from '../../../../prelude/ensure';
|
||||
import { awaitAll } from '../../../../prelude/await-all';
|
||||
|
||||
@EntityRepository(ReversiMatching)
|
||||
export class ReversiMatchingRepository extends Repository<ReversiMatching> {
|
||||
@ -12,7 +12,7 @@ export class ReversiMatchingRepository extends Repository<ReversiMatching> {
|
||||
) {
|
||||
const matching = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
|
||||
|
||||
return await rap({
|
||||
return await awaitAll({
|
||||
id: matching.id,
|
||||
createdAt: matching.createdAt,
|
||||
parentId: matching.parentId,
|
||||
|
71
src/models/repositories/hashtag.ts
Normal file
71
src/models/repositories/hashtag.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { Hashtag } from '../entities/hashtag';
|
||||
import { SchemaType, types, bool } from '../../misc/schema';
|
||||
|
||||
export type PackedHashtag = SchemaType<typeof packedHashtagSchema>;
|
||||
|
||||
@EntityRepository(Hashtag)
|
||||
export class HashtagRepository extends Repository<Hashtag> {
|
||||
public async pack(
|
||||
src: Hashtag,
|
||||
): Promise<PackedHashtag> {
|
||||
return {
|
||||
tag: src.name,
|
||||
mentionedUsersCount: src.mentionedUsersCount,
|
||||
mentionedLocalUsersCount: src.mentionedLocalUsersCount,
|
||||
mentionedRemoteUsersCount: src.mentionedRemoteUsersCount,
|
||||
attachedUsersCount: src.attachedUsersCount,
|
||||
attachedLocalUsersCount: src.attachedLocalUsersCount,
|
||||
attachedRemoteUsersCount: src.attachedRemoteUsersCount,
|
||||
};
|
||||
}
|
||||
|
||||
public packMany(
|
||||
hashtags: Hashtag[],
|
||||
) {
|
||||
return Promise.all(hashtags.map(x => this.pack(x)));
|
||||
}
|
||||
}
|
||||
|
||||
export const packedHashtagSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
properties: {
|
||||
tag: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'The hashtag name. No # prefixed.',
|
||||
example: 'misskey',
|
||||
},
|
||||
mentionedUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'Number of all users using this hashtag.'
|
||||
},
|
||||
mentionedLocalUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'Number of local users using this hashtag.'
|
||||
},
|
||||
mentionedRemoteUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'Number of remote users using this hashtag.'
|
||||
},
|
||||
attachedUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'Number of all users who attached this hashtag to profile.'
|
||||
},
|
||||
attachedLocalUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'Number of local users who attached this hashtag to profile.'
|
||||
},
|
||||
attachedRemoteUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
description: 'Number of remote users who attached this hashtag to profile.'
|
||||
},
|
||||
}
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user