Compare commits
48 Commits
Author | SHA1 | Date | |
---|---|---|---|
712802e682 | |||
abe99c3c73 | |||
d7a3b71028 | |||
10c434f24a | |||
fe46c53ea6 | |||
cdd123dfd3 | |||
a1a3ee44b5 | |||
a86c419f95 | |||
e3ec0ad97e | |||
75791981ce | |||
e813fe16b9 | |||
42ac7b954d | |||
c1bbf5dab6 | |||
e16dc2a910 | |||
e236c05d79 | |||
454c1e3faf | |||
43daf814df | |||
c40b630530 | |||
7fc0698ecf | |||
4f3c8b940e | |||
1855ab60f1 | |||
af4f1a7bd6 | |||
8646a9c49c | |||
8d7c033cf5 | |||
b8900e32de | |||
d48c25d2c9 | |||
a87c5899c5 | |||
147ad69864 | |||
c146006476 | |||
a0f10d7ca1 | |||
299b91edc4 | |||
95c89ca6db | |||
7fe0d71e7f | |||
fbbb506e86 | |||
ec80b06a45 | |||
41e1619f1f | |||
ba6a9c6a93 | |||
18571c52fb | |||
5d5dfeaa83 | |||
3669d8c0f3 | |||
69d72819c6 | |||
54dcc10250 | |||
1edfce8f73 | |||
675e573a8c | |||
1080fa63a9 | |||
8047086988 | |||
449b9f7fa0 | |||
b7a15bf6ca |
@ -1,6 +1,3 @@
|
|||||||
name: example-instance-name # Name of your instance
|
|
||||||
description: example-description # Description of your instance
|
|
||||||
|
|
||||||
maintainer:
|
maintainer:
|
||||||
name: example-maitainer-name # Your name
|
name: example-maitainer-name # Your name
|
||||||
url: http://example.com/ # Your contact (http or mailto)
|
url: http://example.com/ # Your contact (http or mailto)
|
||||||
@ -25,7 +22,7 @@ url: https://example.tld/
|
|||||||
# +------+ |+-------------+ +----------------+|
|
# +------+ |+-------------+ +----------------+|
|
||||||
# +---------------------------------------+
|
# +---------------------------------------+
|
||||||
#
|
#
|
||||||
# You need to setup reverse proxy. (eg. Nginx)
|
# You need to setup reverse proxy. (eg. nginx)
|
||||||
# You do not define 'https' section.
|
# You do not define 'https' section.
|
||||||
|
|
||||||
# Option 2: Standalone
|
# Option 2: Standalone
|
||||||
@ -148,6 +145,12 @@ drive:
|
|||||||
# consumer_key: example-twitter-consumer-key
|
# consumer_key: example-twitter-consumer-key
|
||||||
# consumer_secret: example-twitter-consumer-secret-key
|
# consumer_secret: example-twitter-consumer-secret-key
|
||||||
|
|
||||||
|
# GitHub integration
|
||||||
|
# You need to set the oauth callback url as : https://<your-misskey-instance>/api/gh/cb
|
||||||
|
#github:
|
||||||
|
# client_id: example-github-client-id
|
||||||
|
# client_secret: example-github-client-secret
|
||||||
|
|
||||||
# Ghost
|
# Ghost
|
||||||
# Ghost account is an account used for the purpose of delegating
|
# Ghost account is an account used for the purpose of delegating
|
||||||
# followers when putting users in the list.
|
# followers when putting users in the list.
|
||||||
@ -164,6 +167,3 @@ drive:
|
|||||||
# external: true
|
# external: true
|
||||||
# engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}
|
# engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}
|
||||||
# timeout: 300000
|
# timeout: 300000
|
||||||
|
|
||||||
# Max allowed note text length in charactors
|
|
||||||
maxNoteTextLength: 1000
|
|
||||||
|
41
.travis.yml
41
.travis.yml
@ -1,41 +0,0 @@
|
|||||||
# travis file
|
|
||||||
# https://docs.travis-ci.com/user/customizing-the-build
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
||||||
|
|
||||||
branches:
|
|
||||||
except:
|
|
||||||
- l10n_master
|
|
||||||
|
|
||||||
language: node_js
|
|
||||||
|
|
||||||
node_js:
|
|
||||||
- 11.0.0
|
|
||||||
|
|
||||||
env:
|
|
||||||
- CXX=g++-4.8 NODE_ENV=production
|
|
||||||
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
sources:
|
|
||||||
- ubuntu-toolchain-r-test
|
|
||||||
packages:
|
|
||||||
- g++-4.8
|
|
||||||
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- node_modules
|
|
||||||
|
|
||||||
services:
|
|
||||||
- mongodb
|
|
||||||
- redis-server
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- npm install
|
|
||||||
|
|
||||||
# 設定ファイルを配置
|
|
||||||
- cp ./.ci/default.yml ./.config
|
|
||||||
- cp ./.ci/test.yml ./.config
|
|
||||||
|
|
||||||
- travis_wait npm run build
|
|
@ -23,5 +23,5 @@ Please use [Crowdin](https://crowdin.com/project/misskey) for localization.
|
|||||||
* Test codes are located in `/test`.
|
* Test codes are located in `/test`.
|
||||||
|
|
||||||
## Continuous integration
|
## Continuous integration
|
||||||
Misskey uses Travis for automated test.
|
Misskey uses CircleCI for automated test.
|
||||||
Configuration files are located in `/.travis`.
|
Configuration files are located in `/.circleci`.
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
================================================================
|
================================================================
|
||||||
|
|
||||||
[](https://circleci.com/gh/syuilo/misskey)
|
[](https://circleci.com/gh/syuilo/misskey)
|
||||||
[![][travis-badge]][travis-link]
|
|
||||||
[![][dependencies-badge]][dependencies-link]
|
[![][dependencies-badge]][dependencies-link]
|
||||||
[](http://makeapullrequest.com)
|
[](http://makeapullrequest.com)
|
||||||
|
|
||||||
@ -44,7 +43,7 @@ Easiest way to tell your emotions. Misskey allows you to add various type of rea
|
|||||||
|
|
||||||
<h3 align="left">Interface</h3>
|
<h3 align="left">Interface</h3>
|
||||||
<p align="left">
|
<p align="left">
|
||||||
No UI fits for everyone. Therefore, Misskey has a highly customizable UI for your taste. You can edit layouts of your timeline, place selectable widgets you can easily move and create your unique home as this place will be your home.
|
Highly customizable UI for your taste. We understand no UI fits for everyone. You can edit layouts of your timeline, place selectable widgets you can easily move and create your unique home as this place will be your home.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -124,8 +123,6 @@ Misskey is an open-source software licensed under the [GNU AGPLv3](LICENSE).
|
|||||||
|
|
||||||
[agpl-3.0]: https://www.gnu.org/licenses/agpl-3.0.en.html
|
[agpl-3.0]: https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||||
[agpl-3.0-badge]: https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square
|
[agpl-3.0-badge]: https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square
|
||||||
[travis-link]: https://travis-ci.org/syuilo/misskey
|
|
||||||
[travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square
|
|
||||||
[dependencies-link]: https://david-dm.org/syuilo/misskey
|
[dependencies-link]: https://david-dm.org/syuilo/misskey
|
||||||
[dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square
|
[dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ adduser --disabled-password --disabled-login misskey
|
|||||||
Please install and setup these softwares:
|
Please install and setup these softwares:
|
||||||
|
|
||||||
#### Dependencies :package:
|
#### Dependencies :package:
|
||||||
* **[Node.js](https://nodejs.org/en/)**
|
* **[Node.js](https://nodejs.org/en/)** >= 10.0.0
|
||||||
* **[MongoDB](https://www.mongodb.com/)** >= 3.6
|
* **[MongoDB](https://www.mongodb.com/)** >= 3.6
|
||||||
|
|
||||||
##### Optional
|
##### Optional
|
||||||
|
@ -22,7 +22,7 @@ adduser --disabled-password --disabled-login misskey
|
|||||||
これらのソフトウェアをインストール・設定してください:
|
これらのソフトウェアをインストール・設定してください:
|
||||||
|
|
||||||
#### 依存関係 :package:
|
#### 依存関係 :package:
|
||||||
* **[Node.js](https://nodejs.org/en/)**
|
* **[Node.js](https://nodejs.org/en/)** (10.0.0以上)
|
||||||
* **[MongoDB](https://www.mongodb.com/)** (3.6以上)
|
* **[MongoDB](https://www.mongodb.com/)** (3.6以上)
|
||||||
|
|
||||||
##### オプション
|
##### オプション
|
||||||
|
@ -417,6 +417,7 @@ common/views/components/signin.vue:
|
|||||||
signin: "サインイン"
|
signin: "サインイン"
|
||||||
or: "または"
|
or: "または"
|
||||||
signin-with-twitter: "Twitterでログイン"
|
signin-with-twitter: "Twitterでログイン"
|
||||||
|
signin-with-github: "GitHubでログイン"
|
||||||
login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。"
|
login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。"
|
||||||
|
|
||||||
common/views/components/signup.vue:
|
common/views/components/signup.vue:
|
||||||
@ -460,6 +461,14 @@ common/views/components/twitter-setting.vue:
|
|||||||
connect: "Twitterと接続する"
|
connect: "Twitterと接続する"
|
||||||
disconnect: "切断する"
|
disconnect: "切断する"
|
||||||
|
|
||||||
|
common/views/components/github-setting.vue:
|
||||||
|
description: "お使いのGitHubアカウントをお使いのMisskeyアカウントに接続しておくと、プロフィールでGitHubアカウント情報が表示されるようになったり、GitHubを用いた便利なサインインを利用できるようになります。"
|
||||||
|
connected-to: "次のGitHubアカウントに接続されています"
|
||||||
|
detail: "詳細..."
|
||||||
|
reconnect: "再接続する"
|
||||||
|
connect: "GitHubと接続する"
|
||||||
|
disconnect: "切断する"
|
||||||
|
|
||||||
common/views/components/uploader.vue:
|
common/views/components/uploader.vue:
|
||||||
waiting: "待機中"
|
waiting: "待機中"
|
||||||
|
|
||||||
@ -599,32 +608,6 @@ desktop/views/components/calendar.vue:
|
|||||||
next: "次の月"
|
next: "次の月"
|
||||||
go: "クリックして時間遡行"
|
go: "クリックして時間遡行"
|
||||||
|
|
||||||
desktop/views/components/charts.vue:
|
|
||||||
title: "チャート"
|
|
||||||
per-day: "1日ごと"
|
|
||||||
per-hour: "1時間ごと"
|
|
||||||
federation: "フェデレーション"
|
|
||||||
notes: "投稿"
|
|
||||||
users: "ユーザー"
|
|
||||||
drive: "ドライブ"
|
|
||||||
network: "ネットワーク"
|
|
||||||
charts:
|
|
||||||
federation-instances: "インスタンスの増減"
|
|
||||||
federation-instances-total: "インスタンスの積算"
|
|
||||||
notes: "投稿の増減 (統合)"
|
|
||||||
local-notes: "投稿の増減 (ローカル)"
|
|
||||||
remote-notes: "投稿の増減 (リモート)"
|
|
||||||
notes-total: "投稿の積算"
|
|
||||||
users: "ユーザーの増減"
|
|
||||||
users-total: "ユーザーの積算"
|
|
||||||
drive: "ドライブ使用量の増減"
|
|
||||||
drive-total: "ドライブ使用量の積算"
|
|
||||||
drive-files: "ドライブのファイル数の増減"
|
|
||||||
drive-files-total: "ドライブのファイル数の積算"
|
|
||||||
network-requests: "リクエスト"
|
|
||||||
network-time: "応答時間"
|
|
||||||
network-usage: "通信量"
|
|
||||||
|
|
||||||
desktop/views/components/choose-file-from-drive-window.vue:
|
desktop/views/components/choose-file-from-drive-window.vue:
|
||||||
choose-file: "ファイル選択中"
|
choose-file: "ファイル選択中"
|
||||||
upload: "PCからドライブにファイルをアップロード"
|
upload: "PCからドライブにファイルをアップロード"
|
||||||
@ -1088,10 +1071,18 @@ admin/views/dashboard.vue:
|
|||||||
instances: "インスタンス"
|
instances: "インスタンス"
|
||||||
this-instance: "このインスタンス"
|
this-instance: "このインスタンス"
|
||||||
federated: "連合"
|
federated: "連合"
|
||||||
|
|
||||||
|
admin/views/instance.vue:
|
||||||
|
instance: "インスタンス"
|
||||||
|
instance-name: "インスタンス名"
|
||||||
|
instance-description: "インスタンスの紹介"
|
||||||
|
banner-url: "バナー画像URL"
|
||||||
|
max-note-text-length: "投稿の最大文字数"
|
||||||
|
disable-registration: "ユーザー登録の受付を停止する"
|
||||||
|
disable-local-timeline: "ローカルタイムラインを無効にする"
|
||||||
invite: "招待"
|
invite: "招待"
|
||||||
banner-url: "Banner URL"
|
save: "保存"
|
||||||
disableRegistration: "Disable new user registration"
|
saved: "保存しました"
|
||||||
disableLocalTimeline: "Disable the local timeline"
|
|
||||||
|
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
@ -1142,10 +1133,16 @@ admin/views/emoji.vue:
|
|||||||
aliases-desc: "スペースで区切って複数設定できます。"
|
aliases-desc: "スペースで区切って複数設定できます。"
|
||||||
url: "絵文字画像URL"
|
url: "絵文字画像URL"
|
||||||
add: "追加"
|
add: "追加"
|
||||||
|
info: "50KB以下のPNG画像をおすすめします。"
|
||||||
|
added: "絵文字を登録しました"
|
||||||
emojis:
|
emojis:
|
||||||
title: "絵文字一覧"
|
title: "絵文字一覧"
|
||||||
update: "更新"
|
update: "更新"
|
||||||
remove: "削除"
|
remove: "削除"
|
||||||
|
updated: "更新しました"
|
||||||
|
remove-emoji:
|
||||||
|
are-you-sure: "「$1」を削除しますか?"
|
||||||
|
removed: "削除しました"
|
||||||
|
|
||||||
admin/views/announcements.vue:
|
admin/views/announcements.vue:
|
||||||
announcements: "お知らせ"
|
announcements: "お知らせ"
|
||||||
@ -1154,6 +1151,10 @@ admin/views/announcements.vue:
|
|||||||
add: "追加"
|
add: "追加"
|
||||||
title: "タイトル"
|
title: "タイトル"
|
||||||
text: "内容"
|
text: "内容"
|
||||||
|
saved: "保存しました"
|
||||||
|
_remove:
|
||||||
|
are-you-sure: "「$1」を削除しますか?"
|
||||||
|
removed: "削除しました"
|
||||||
|
|
||||||
admin/views/hashtags.vue:
|
admin/views/hashtags.vue:
|
||||||
hided-tags: "Hidden Tags"
|
hided-tags: "Hidden Tags"
|
||||||
@ -1173,12 +1174,6 @@ desktop/views/pages/deck/deck.user-column.vue:
|
|||||||
pinned-notes: "ピン留めされた投稿"
|
pinned-notes: "ピン留めされた投稿"
|
||||||
push-to-a-list: "リストに追加"
|
push-to-a-list: "リストに追加"
|
||||||
|
|
||||||
desktop/views/pages/stats/stats.vue:
|
|
||||||
all-users: "全てのユーザー"
|
|
||||||
original-users: "このインスタンスのユーザー"
|
|
||||||
all-notes: "全ての投稿"
|
|
||||||
original-notes: "このインスタンスの投稿"
|
|
||||||
|
|
||||||
desktop/views/pages/welcome.vue:
|
desktop/views/pages/welcome.vue:
|
||||||
about: "詳しく..."
|
about: "詳しく..."
|
||||||
gotit: "わかった"
|
gotit: "わかった"
|
||||||
@ -1560,6 +1555,10 @@ mobile/views/pages/settings.vue:
|
|||||||
twitter-connect: "Twitterアカウントに接続する"
|
twitter-connect: "Twitterアカウントに接続する"
|
||||||
twitter-reconnect: "再接続する"
|
twitter-reconnect: "再接続する"
|
||||||
twitter-disconnect: "切断する"
|
twitter-disconnect: "切断する"
|
||||||
|
github: "GitHub連携"
|
||||||
|
github-connect: "GitHubアカウントに接続する"
|
||||||
|
github-reconnect: "再接続する"
|
||||||
|
github-disconnect: "切断する"
|
||||||
update: "Misskey Update"
|
update: "Misskey Update"
|
||||||
version: "バージョン:"
|
version: "バージョン:"
|
||||||
latest-version: "最新のバージョン:"
|
latest-version: "最新のバージョン:"
|
||||||
|
@ -186,7 +186,7 @@ common:
|
|||||||
stack-left: "左に重ねんで!"
|
stack-left: "左に重ねんで!"
|
||||||
pop-right: "右に出すで!"
|
pop-right: "右に出すで!"
|
||||||
dev: "アプリの作成あかんかったわ。もっぺんやってみて。"
|
dev: "アプリの作成あかんかったわ。もっぺんやってみて。"
|
||||||
ai-chan-kawaii: "藍ちゃかわいい"
|
ai-chan-kawaii: "藍ちゃめっさべっぴんさんや"
|
||||||
auth/views/form.vue:
|
auth/views/form.vue:
|
||||||
share-access: "<i>{{ app.name }}</i>があんさんのアカウントにアクセスすんのを<b>許可</b>してもええか?"
|
share-access: "<i>{{ app.name }}</i>があんさんのアカウントにアクセスすんのを<b>許可</b>してもええか?"
|
||||||
permission-ask: "このアプリは次の権限を要求してんで:"
|
permission-ask: "このアプリは次の権限を要求してんで:"
|
||||||
@ -744,7 +744,7 @@ desktop/views/components/settings.vue:
|
|||||||
apps: "アプリ"
|
apps: "アプリ"
|
||||||
mute-and-block: "ミュート/ブロック"
|
mute-and-block: "ミュート/ブロック"
|
||||||
blocking: "ブロック"
|
blocking: "ブロック"
|
||||||
security: "守護神セキュリティ"
|
security: "セキュリティ"
|
||||||
signin: "こんな感じでサインインしたらしいで"
|
signin: "こんな感じでサインインしたらしいで"
|
||||||
password: "パスワード"
|
password: "パスワード"
|
||||||
2fa: "二段階認証"
|
2fa: "二段階認証"
|
||||||
@ -873,15 +873,15 @@ common/views/components/mute-and-block.vue:
|
|||||||
mute-and-block: "ミュートとブロック"
|
mute-and-block: "ミュートとブロック"
|
||||||
mute: "ミュート"
|
mute: "ミュート"
|
||||||
block: "ブロック"
|
block: "ブロック"
|
||||||
no-muted-users: "ミュートしているユーザーはいません"
|
no-muted-users: "ミュートしとるユーザーはおらんで"
|
||||||
no-blocked-users: "ブロックしているユーザーはいません"
|
no-blocked-users: "ブロックしとるユーザーはおらんで"
|
||||||
common/views/components/password-settings.vue:
|
common/views/components/password-settings.vue:
|
||||||
reset: "パスワードを変更する"
|
reset: "パスワード変える"
|
||||||
enter-current-password: "現在のパスワードを入力してください"
|
enter-current-password: "今のパスワードを入れてや"
|
||||||
enter-new-password: "新しいパスワードを入力してください"
|
enter-new-password: "こんどのパスワード入れてや"
|
||||||
enter-new-password-again: "もう一度新しいパスワードを入力してください"
|
enter-new-password-again: "もっぺん入れてや"
|
||||||
not-match: "新しいパスワードが一致しません"
|
not-match: "パスワードがおうとらん"
|
||||||
changed: "パスワードを変更しました"
|
changed: "パスワード変えたわ"
|
||||||
desktop/views/components/sub-note-content.vue:
|
desktop/views/components/sub-note-content.vue:
|
||||||
private: "この投稿は見せられへんわ"
|
private: "この投稿は見せられへんわ"
|
||||||
deleted: "この投稿なんか無くなってもうたわ"
|
deleted: "この投稿なんか無くなってもうたわ"
|
||||||
@ -953,7 +953,7 @@ admin/views/index.vue:
|
|||||||
emoji: "カスタム絵文字"
|
emoji: "カスタム絵文字"
|
||||||
users: "ユーザー"
|
users: "ユーザー"
|
||||||
update: "更新"
|
update: "更新"
|
||||||
announcements: "お知らせ"
|
announcements: "知っといてや"
|
||||||
hashtags: "ハッシュタグ"
|
hashtags: "ハッシュタグ"
|
||||||
back-to-misskey: "Misskeyに戻る"
|
back-to-misskey: "Misskeyに戻る"
|
||||||
admin/views/dashboard.vue:
|
admin/views/dashboard.vue:
|
||||||
@ -962,9 +962,9 @@ admin/views/dashboard.vue:
|
|||||||
notes: "投稿"
|
notes: "投稿"
|
||||||
drive: "ドライブ"
|
drive: "ドライブ"
|
||||||
instances: "インスタンス"
|
instances: "インスタンス"
|
||||||
this-instance: "このインスタンス"
|
this-instance: "ワイのインスタンス"
|
||||||
federated: "連合"
|
federated: "連合"
|
||||||
invite: "招待"
|
invite: "来てや"
|
||||||
banner-url: "Banner URL"
|
banner-url: "Banner URL"
|
||||||
disableRegistration: "Disable new user registration"
|
disableRegistration: "Disable new user registration"
|
||||||
disableLocalTimeline: "Disable the local timeline"
|
disableLocalTimeline: "Disable the local timeline"
|
||||||
@ -980,7 +980,7 @@ admin/views/charts.vue:
|
|||||||
charts:
|
charts:
|
||||||
federation-instances: "インスタンスの増減"
|
federation-instances: "インスタンスの増減"
|
||||||
federation-instances-total: "インスタンスの積算"
|
federation-instances-total: "インスタンスの積算"
|
||||||
notes: "投稿の増減 (統合)"
|
notes: "投稿の増減(統合)"
|
||||||
local-notes: "投稿の増減 (ローカル)"
|
local-notes: "投稿の増減 (ローカル)"
|
||||||
remote-notes: "投稿の増減 (リモート)"
|
remote-notes: "投稿の増減 (リモート)"
|
||||||
notes-total: "投稿の積算"
|
notes-total: "投稿の積算"
|
||||||
@ -1387,7 +1387,7 @@ mobile/views/pages/user.vue:
|
|||||||
mute: "ミュート"
|
mute: "ミュート"
|
||||||
unmute: "ミュート解除"
|
unmute: "ミュート解除"
|
||||||
block: "ブロック"
|
block: "ブロック"
|
||||||
unblock: "ブロック解除"
|
unblock: "ブロックやめたる"
|
||||||
mobile/views/pages/user/home.vue:
|
mobile/views/pages/user/home.vue:
|
||||||
recent-notes: "最近儲かりまっか?"
|
recent-notes: "最近儲かりまっか?"
|
||||||
images: "画像"
|
images: "画像"
|
||||||
|
17362
package-lock.json
generated
17362
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "10.38.1",
|
"version": "10.38.7",
|
||||||
"clientVersion": "1.0.11482",
|
"clientVersion": "1.0.11530",
|
||||||
"codename": "nighthike",
|
"codename": "nighthike",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
@ -62,6 +62,7 @@
|
|||||||
"@types/mongodb": "3.1.12",
|
"@types/mongodb": "3.1.12",
|
||||||
"@types/ms": "0.7.30",
|
"@types/ms": "0.7.30",
|
||||||
"@types/node": "10.12.2",
|
"@types/node": "10.12.2",
|
||||||
|
"@types/oauth": "0.9.1",
|
||||||
"@types/portscanner": "2.1.0",
|
"@types/portscanner": "2.1.0",
|
||||||
"@types/pug": "2.0.4",
|
"@types/pug": "2.0.4",
|
||||||
"@types/qrcode": "1.3.0",
|
"@types/qrcode": "1.3.0",
|
||||||
@ -95,7 +96,6 @@
|
|||||||
"chai": "4.2.0",
|
"chai": "4.2.0",
|
||||||
"chai-http": "4.2.0",
|
"chai-http": "4.2.0",
|
||||||
"chalk": "2.4.1",
|
"chalk": "2.4.1",
|
||||||
"chart.js": "2.7.3",
|
|
||||||
"commander": "2.19.0",
|
"commander": "2.19.0",
|
||||||
"crc-32": "1.2.0",
|
"crc-32": "1.2.0",
|
||||||
"css-loader": "1.0.1",
|
"css-loader": "1.0.1",
|
||||||
@ -211,7 +211,6 @@
|
|||||||
"uuid": "3.3.2",
|
"uuid": "3.3.2",
|
||||||
"v-animate-css": "0.0.2",
|
"v-animate-css": "0.0.2",
|
||||||
"vue": "2.5.17",
|
"vue": "2.5.17",
|
||||||
"vue-chartjs": "3.4.0",
|
|
||||||
"vue-color": "2.7.0",
|
"vue-color": "2.7.0",
|
||||||
"vue-content-loading": "1.5.3",
|
"vue-content-loading": "1.5.3",
|
||||||
"vue-cropperjs": "2.2.2",
|
"vue-cropperjs": "2.2.2",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="cdeuzmsthagexbkpofbmatmugjuvogfb">
|
||||||
<ui-card>
|
<ui-card>
|
||||||
<div slot="title">%fa:broadcast-tower% %i18n:@announcements%</div>
|
<div slot="title">%fa:broadcast-tower% %i18n:@announcements%</div>
|
||||||
<section v-for="(announcement, i) in announcements" class="fit-top">
|
<section v-for="(announcement, i) in announcements" class="fit-top">
|
||||||
@ -9,10 +9,10 @@
|
|||||||
<ui-textarea v-model="announcement.text">
|
<ui-textarea v-model="announcement.text">
|
||||||
<span>%i18n:@text%</span>
|
<span>%i18n:@text%</span>
|
||||||
</ui-textarea>
|
</ui-textarea>
|
||||||
<ui-button-group>
|
<ui-horizon-group>
|
||||||
<ui-button inline @click="save">%fa:save R% %i18n:@save%</ui-button>
|
<ui-button @click="save()">%fa:save R% %i18n:@save%</ui-button>
|
||||||
<ui-button inline @click="remove(i)">%fa:trash-alt R% %i18n:@remove%</ui-button>
|
<ui-button @click="remove(i)">%fa:trash-alt R% %i18n:@remove%</ui-button>
|
||||||
</ui-button-group>
|
</ui-horizon-group>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<ui-button @click="add">%fa:plus% %i18n:@add%</ui-button>
|
<ui-button @click="add">%fa:plus% %i18n:@add%</ui-button>
|
||||||
@ -46,19 +46,45 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
remove(i) {
|
remove(i) {
|
||||||
this.announcements = this.announcements.filter((_, j) => j !== i);
|
this.$swal({
|
||||||
this.save();
|
type: 'warning',
|
||||||
|
text: '%i18n:@_remove.are-you-sure%'.replace('$1', this.announcements.find((_, j) => j == i).title),
|
||||||
|
showCancelButton: true
|
||||||
|
}).then(res => {
|
||||||
|
if (!res.value) return;
|
||||||
|
this.announcements = this.announcements.filter((_, j) => j !== i);
|
||||||
|
this.save(true);
|
||||||
|
this.$swal({
|
||||||
|
type: 'success',
|
||||||
|
text: '%i18n:@_remove.removed%'
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
save() {
|
save(silent) {
|
||||||
(this as any).api('admin/update-meta', {
|
(this as any).api('admin/update-meta', {
|
||||||
broadcasts: this.announcements
|
broadcasts: this.announcements
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
(this as any).os.apis.dialog({ text: `Saved` });
|
if (!silent) {
|
||||||
|
this.$swal({
|
||||||
|
type: 'success',
|
||||||
|
text: '%i18n:@saved%'
|
||||||
|
});
|
||||||
|
}
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
(this as any).os.apis.dialog({ text: `Failed ${e}` });
|
this.$swal({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.cdeuzmsthagexbkpofbmatmugjuvogfb
|
||||||
|
@media (min-width 500px)
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
</style>
|
||||||
|
@ -63,11 +63,11 @@ export default Vue.extend({
|
|||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.hyhctythnmwihguaaapnbrbszsjqxpio
|
.hyhctythnmwihguaaapnbrbszsjqxpio
|
||||||
display block
|
display block
|
||||||
padding 16px
|
padding 12px 16px 16px 16px
|
||||||
height 250px
|
height 250px
|
||||||
overflow auto
|
overflow hidden
|
||||||
box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
|
box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
|
||||||
background var(--face)
|
background var(--adminDashboardCardBg)
|
||||||
border-radius 8px
|
border-radius 8px
|
||||||
|
|
||||||
> table
|
> table
|
||||||
@ -76,10 +76,11 @@ export default Vue.extend({
|
|||||||
overflow auto
|
overflow auto
|
||||||
border-spacing 0
|
border-spacing 0
|
||||||
border-collapse collapse
|
border-collapse collapse
|
||||||
color #555
|
color var(--adminDashboardCardFg)
|
||||||
|
font-size 14px
|
||||||
|
|
||||||
thead
|
thead
|
||||||
border-bottom solid 2px #eee
|
border-bottom solid 1px var(--adminDashboardCardDivider)
|
||||||
|
|
||||||
tr
|
tr
|
||||||
th
|
th
|
||||||
@ -89,7 +90,7 @@ export default Vue.extend({
|
|||||||
tbody
|
tbody
|
||||||
tr
|
tr
|
||||||
&:nth-child(odd)
|
&:nth-child(odd)
|
||||||
background #fbfbfb
|
background rgba(0, 0, 0, 0.025)
|
||||||
|
|
||||||
th, td
|
th, td
|
||||||
padding 8px 16px
|
padding 8px 16px
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import * as tinycolor from 'tinycolor2';
|
||||||
import * as ApexCharts from 'apexcharts';
|
import * as ApexCharts from 'apexcharts';
|
||||||
|
|
||||||
const limit = 90;
|
const limit = 90;
|
||||||
@ -147,7 +148,7 @@ export default Vue.extend({
|
|||||||
this.chartInstance.destroy();
|
this.chartInstance.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.chartInstance = new ApexCharts(this.$refs.chart, Object.assign({
|
this.chartInstance = new ApexCharts(this.$refs.chart, {
|
||||||
chart: {
|
chart: {
|
||||||
type: 'area',
|
type: 'area',
|
||||||
height: 300,
|
height: 300,
|
||||||
@ -168,17 +169,41 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
clipMarkers: false,
|
clipMarkers: false,
|
||||||
|
borderColor: 'rgba(0, 0, 0, 0.1)'
|
||||||
},
|
},
|
||||||
stroke: {
|
stroke: {
|
||||||
curve: 'straight',
|
curve: 'straight',
|
||||||
width: 2
|
width: 2
|
||||||
},
|
},
|
||||||
|
legend: {
|
||||||
|
labels: {
|
||||||
|
color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
|
||||||
|
},
|
||||||
|
},
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: 'datetime'
|
type: 'datetime',
|
||||||
|
labels: {
|
||||||
|
style: {
|
||||||
|
colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisBorder: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.1)'
|
||||||
|
},
|
||||||
|
axisTicks: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.1)'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
}
|
labels: {
|
||||||
}, this.data));
|
formatter: this.data.bytes ? v => Vue.filter('bytes')(v, 0) : v => Vue.filter('number')(v),
|
||||||
|
style: {
|
||||||
|
color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: this.data.series
|
||||||
|
});
|
||||||
|
|
||||||
this.chartInstance.render();
|
this.chartInstance.render();
|
||||||
},
|
},
|
||||||
@ -286,6 +311,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
driveChart(): any {
|
driveChart(): any {
|
||||||
return {
|
return {
|
||||||
|
bytes: true,
|
||||||
series: [{
|
series: [{
|
||||||
name: 'All',
|
name: 'All',
|
||||||
data: this.format(
|
data: this.format(
|
||||||
@ -314,6 +340,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
driveTotalChart(): any {
|
driveTotalChart(): any {
|
||||||
return {
|
return {
|
||||||
|
bytes: true,
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Combined',
|
name: 'Combined',
|
||||||
data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
|
data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
|
||||||
@ -396,6 +423,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
networkUsageChart(): any {
|
networkUsageChart(): any {
|
||||||
return {
|
return {
|
||||||
|
bytes: true,
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Incoming',
|
name: 'Incoming',
|
||||||
data: this.format(this.stats.network.incomingBytes)
|
data: this.format(this.stats.network.incomingBytes)
|
||||||
@ -424,8 +452,8 @@ export default Vue.extend({
|
|||||||
margin 0 8px
|
margin 0 8px
|
||||||
padding 0 0 8px 0
|
padding 0 0 8px 0
|
||||||
font-size 1em
|
font-size 1em
|
||||||
color #555
|
color var(--adminDashboardCardFg)
|
||||||
border-bottom solid 1px #eee
|
border-bottom solid 1px var(--adminDashboardCardDivider)
|
||||||
|
|
||||||
> b
|
> b
|
||||||
margin-right 8px
|
margin-right 8px
|
||||||
|
@ -79,6 +79,7 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
clipMarkers: false,
|
clipMarkers: false,
|
||||||
|
borderColor: 'rgba(0, 0, 0, 0.1)'
|
||||||
},
|
},
|
||||||
stroke: {
|
stroke: {
|
||||||
curve: 'straight',
|
curve: 'straight',
|
||||||
@ -153,7 +154,7 @@ export default Vue.extend({
|
|||||||
display flex
|
display flex
|
||||||
padding 0 8px
|
padding 0 8px
|
||||||
margin-bottom -16px
|
margin-bottom -16px
|
||||||
color #555
|
color var(--adminDashboardCardFg)
|
||||||
font-size 14px
|
font-size 14px
|
||||||
|
|
||||||
> span
|
> span
|
||||||
@ -167,4 +168,13 @@ export default Vue.extend({
|
|||||||
> div
|
> div
|
||||||
margin-bottom -10px
|
margin-bottom -10px
|
||||||
|
|
||||||
|
@media (max-width 1000px)
|
||||||
|
display block
|
||||||
|
margin-bottom 26px
|
||||||
|
|
||||||
|
> div
|
||||||
|
&:first-child
|
||||||
|
margin-right 0
|
||||||
|
margin-bottom 26px
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -124,17 +124,28 @@ export default Vue.extend({
|
|||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.obdskegsannmntldydackcpzezagxqfy
|
.obdskegsannmntldydackcpzezagxqfy
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
@media (min-width 500px)
|
||||||
|
padding 32px
|
||||||
|
|
||||||
> header
|
> header
|
||||||
display flex
|
display flex
|
||||||
margin-bottom 16px
|
margin-bottom 16px
|
||||||
padding-bottom 16px
|
padding-bottom 16px
|
||||||
border-bottom solid 1px #ccc
|
border-bottom solid 1px var(--adminDashboardHeaderBorder)
|
||||||
color #777
|
color var(--adminDashboardHeaderFg)
|
||||||
font-size 14px
|
font-size 14px
|
||||||
|
white-space nowrap
|
||||||
|
|
||||||
|
@media (max-width 1000px)
|
||||||
|
display none
|
||||||
|
|
||||||
> p
|
> p
|
||||||
display inline
|
display block
|
||||||
margin 0 32px 0 0
|
margin 0 32px 0 0
|
||||||
|
overflow hidden
|
||||||
|
text-overflow ellipsis
|
||||||
|
|
||||||
> b
|
> b
|
||||||
&:after
|
&:after
|
||||||
@ -152,11 +163,10 @@ export default Vue.extend({
|
|||||||
|
|
||||||
> div
|
> div
|
||||||
flex 1
|
flex 1
|
||||||
max-width 300px
|
|
||||||
margin-right 16px
|
margin-right 16px
|
||||||
color var(--text)
|
color var(--adminDashboardCardFg)
|
||||||
box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
|
box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
|
||||||
background var(--face)
|
background var(--adminDashboardCardBg)
|
||||||
border-radius 8px
|
border-radius 8px
|
||||||
|
|
||||||
&:last-child
|
&:last-child
|
||||||
@ -192,7 +202,7 @@ export default Vue.extend({
|
|||||||
> div:last-child
|
> div:last-child
|
||||||
display flex
|
display flex
|
||||||
padding 6px 16px
|
padding 6px 16px
|
||||||
border-top solid 1px #eee
|
border-top solid 1px var(--adminDashboardCardDivider)
|
||||||
|
|
||||||
> span
|
> span
|
||||||
font-size 70%
|
font-size 70%
|
||||||
@ -202,6 +212,21 @@ export default Vue.extend({
|
|||||||
margin-left auto
|
margin-left auto
|
||||||
cursor pointer
|
cursor pointer
|
||||||
|
|
||||||
|
@media (max-width 900px)
|
||||||
|
display grid
|
||||||
|
grid-template-columns 1fr 1fr
|
||||||
|
grid-template-rows 1fr 1fr
|
||||||
|
gap 16px
|
||||||
|
|
||||||
|
> div
|
||||||
|
margin-right 0
|
||||||
|
|
||||||
|
@media (max-width 500px)
|
||||||
|
display block
|
||||||
|
|
||||||
|
> div:not(:last-child)
|
||||||
|
margin-bottom 16px
|
||||||
|
|
||||||
> .charts
|
> .charts
|
||||||
margin-bottom 16px
|
margin-bottom 16px
|
||||||
|
|
||||||
|
@ -1,19 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="tumhkfkmgtvzljezfvmgkeurkfncshbe">
|
||||||
<ui-card>
|
<ui-card>
|
||||||
<div slot="title">%fa:plus% %i18n:@add-emoji.title%</div>
|
<div slot="title">%fa:plus% %i18n:@add-emoji.title%</div>
|
||||||
<section class="fit-top">
|
<section class="fit-top">
|
||||||
<ui-input v-model="name">
|
<ui-horizon-group inputs>
|
||||||
<span>%i18n:@add-emoji.name%</span>
|
<ui-input v-model="name">
|
||||||
<span slot="text">%i18n:@add-emoji.name-desc%</span>
|
<span>%i18n:@add-emoji.name%</span>
|
||||||
</ui-input>
|
<span slot="text">%i18n:@add-emoji.name-desc%</span>
|
||||||
<ui-input v-model="aliases">
|
</ui-input>
|
||||||
<span>%i18n:@add-emoji.aliases%</span>
|
<ui-input v-model="aliases">
|
||||||
<span slot="text">%i18n:@add-emoji.aliases-desc%</span>
|
<span>%i18n:@add-emoji.aliases%</span>
|
||||||
</ui-input>
|
<span slot="text">%i18n:@add-emoji.aliases-desc%</span>
|
||||||
|
</ui-input>
|
||||||
|
</ui-horizon-group>
|
||||||
<ui-input v-model="url">
|
<ui-input v-model="url">
|
||||||
<span>%i18n:@add-emoji.url%</span>
|
<span>%i18n:@add-emoji.url%</span>
|
||||||
</ui-input>
|
</ui-input>
|
||||||
|
<ui-info>%i18n:@add-emoji.info%</ui-info>
|
||||||
<ui-button @click="add">%i18n:@add-emoji.add%</ui-button>
|
<ui-button @click="add">%i18n:@add-emoji.add%</ui-button>
|
||||||
</section>
|
</section>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
@ -22,21 +25,21 @@
|
|||||||
<div slot="title">%fa:grin R% %i18n:@emojis.title%</div>
|
<div slot="title">%fa:grin R% %i18n:@emojis.title%</div>
|
||||||
<section v-for="emoji in emojis">
|
<section v-for="emoji in emojis">
|
||||||
<img :src="emoji.url" :alt="emoji.name" style="width: 64px;"/>
|
<img :src="emoji.url" :alt="emoji.name" style="width: 64px;"/>
|
||||||
<ui-input v-model="emoji.name">
|
<ui-horizon-group inputs>
|
||||||
<span>%i18n:@add-emoji.name%</span>
|
<ui-input v-model="emoji.name">
|
||||||
<span slot="text">%i18n:@add-emoji.name-desc%</span>
|
<span>%i18n:@add-emoji.name%</span>
|
||||||
</ui-input>
|
</ui-input>
|
||||||
<ui-input v-model="emoji.aliases">
|
<ui-input v-model="emoji.aliases">
|
||||||
<span>%i18n:@add-emoji.aliases%</span>
|
<span>%i18n:@add-emoji.aliases%</span>
|
||||||
<span slot="text">%i18n:@add-emoji.aliases-desc%</span>
|
</ui-input>
|
||||||
</ui-input>
|
</ui-horizon-group>
|
||||||
<ui-input v-model="emoji.url">
|
<ui-input v-model="emoji.url">
|
||||||
<span>%i18n:@add-emoji.url%</span>
|
<span>%i18n:@add-emoji.url%</span>
|
||||||
</ui-input>
|
</ui-input>
|
||||||
<ui-button-group>
|
<ui-horizon-group>
|
||||||
<ui-button inline @click="updateEmoji(emoji)">%fa:save R% %i18n:@emojis.update%</ui-button>
|
<ui-button @click="updateEmoji(emoji)">%fa:save R% %i18n:@emojis.update%</ui-button>
|
||||||
<ui-button inline @click="removeEmoji(emoji)">%fa:trash-alt R% %i18n:@emojis.remove%</ui-button>
|
<ui-button @click="removeEmoji(emoji)">%fa:trash-alt R% %i18n:@emojis.remove%</ui-button>
|
||||||
</ui-button-group>
|
</ui-horizon-group>
|
||||||
</section>
|
</section>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
</div>
|
</div>
|
||||||
@ -66,15 +69,22 @@ export default Vue.extend({
|
|||||||
url: this.url,
|
url: this.url,
|
||||||
aliases: this.aliases.split(' ')
|
aliases: this.aliases.split(' ')
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
(this as any).os.apis.dialog({ text: `Added` });
|
this.$swal({
|
||||||
|
type: 'success',
|
||||||
|
text: '%i18n:@add-emoji.added%'
|
||||||
|
});
|
||||||
this.fetchEmojis();
|
this.fetchEmojis();
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
(this as any).os.apis.dialog({ text: `Failed ${e}` });
|
this.$swal({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchEmojis() {
|
fetchEmojis() {
|
||||||
(this as any).api('admin/emoji/list').then(emojis => {
|
(this as any).api('admin/emoji/list').then(emojis => {
|
||||||
|
emojis.reverse();
|
||||||
emojis.forEach(e => e.aliases = (e.aliases || []).join(' '));
|
emojis.forEach(e => e.aliases = (e.aliases || []).join(' '));
|
||||||
this.emojis = emojis;
|
this.emojis = emojis;
|
||||||
});
|
});
|
||||||
@ -87,22 +97,49 @@ export default Vue.extend({
|
|||||||
url: emoji.url,
|
url: emoji.url,
|
||||||
aliases: emoji.aliases.split(' ')
|
aliases: emoji.aliases.split(' ')
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
(this as any).os.apis.dialog({ text: `Updated` });
|
this.$swal({
|
||||||
|
type: 'success',
|
||||||
|
text: '%i18n:@updated%'
|
||||||
|
});
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
(this as any).os.apis.dialog({ text: `Failed ${e}` });
|
this.$swal({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
removeEmoji(emoji) {
|
removeEmoji(emoji) {
|
||||||
(this as any).api('admin/emoji/remove', {
|
this.$swal({
|
||||||
id: emoji.id
|
type: 'warning',
|
||||||
}).then(() => {
|
text: '%i18n:@remove-emoji.are-you-sure%'.replace('$1', emoji.name),
|
||||||
(this as any).os.apis.dialog({ text: `Removed` });
|
showCancelButton: true
|
||||||
this.fetchEmojis();
|
}).then(res => {
|
||||||
}).catch(e => {
|
if (!res.value) return;
|
||||||
(this as any).os.apis.dialog({ text: `Failed ${e}` });
|
|
||||||
|
(this as any).api('admin/emoji/remove', {
|
||||||
|
id: emoji.id
|
||||||
|
}).then(() => {
|
||||||
|
this.$swal({
|
||||||
|
type: 'success',
|
||||||
|
text: '%i18n:@remove-emoji.removed%'
|
||||||
|
});
|
||||||
|
this.fetchEmojis();
|
||||||
|
}).catch(e => {
|
||||||
|
this.$swal({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.tumhkfkmgtvzljezfvmgkeurkfncshbe
|
||||||
|
@media (min-width 500px)
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
</style>
|
||||||
|
@ -29,9 +29,9 @@ export default Vue.extend({
|
|||||||
(this as any).api('admin/update-meta', {
|
(this as any).api('admin/update-meta', {
|
||||||
hidedTags: this.hidedTags.split('\n')
|
hidedTags: this.hidedTags.split('\n')
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
(this as any).os.apis.dialog({ text: `Saved` });
|
//(this as any).os.apis.dialog({ text: `Saved` });
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
(this as any).os.apis.dialog({ text: `Failed ${e}` });
|
//(this as any).os.apis.dialog({ text: `Failed ${e}` });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-admin">
|
<div class="mk-admin" :class="{ isMobile }">
|
||||||
<nav>
|
<header v-show="isMobile">
|
||||||
|
<button class="nav" @click="navOpend = true">%fa:bars%</button>
|
||||||
|
<span>MisskeyMyAdmin</span>
|
||||||
|
</header>
|
||||||
|
<div class="nav-backdrop"
|
||||||
|
v-if="navOpend && isMobile"
|
||||||
|
@click="navOpend = false"
|
||||||
|
@touchstart="navOpend = false"
|
||||||
|
></div>
|
||||||
|
<nav v-show="navOpend">
|
||||||
<div class="mi">
|
<div class="mi">
|
||||||
<img svg-inline src="../assets/header-icon.svg"/>
|
<img svg-inline src="../assets/header-icon.svg"/>
|
||||||
</div>
|
</div>
|
||||||
@ -49,6 +58,10 @@ import XAnnouncements from "./announcements.vue";
|
|||||||
import XHashtags from "./hashtags.vue";
|
import XHashtags from "./hashtags.vue";
|
||||||
import XUsers from "./users.vue";
|
import XUsers from "./users.vue";
|
||||||
|
|
||||||
|
// Detect the user agent
|
||||||
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
const isMobile = /mobile|iphone|ipad|android/.test(ua);
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
XDashboard,
|
XDashboard,
|
||||||
@ -58,10 +71,15 @@ export default Vue.extend({
|
|||||||
XHashtags,
|
XHashtags,
|
||||||
XUsers
|
XUsers
|
||||||
},
|
},
|
||||||
|
provide: {
|
||||||
|
isMobile
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
page: 'dashboard',
|
page: 'dashboard',
|
||||||
version
|
version,
|
||||||
|
isMobile,
|
||||||
|
navOpend: !isMobile
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -74,12 +92,46 @@ export default Vue.extend({
|
|||||||
|
|
||||||
<style lang="stylus">
|
<style lang="stylus">
|
||||||
.mk-admin
|
.mk-admin
|
||||||
|
$headerHeight = 48px
|
||||||
|
|
||||||
display flex
|
display flex
|
||||||
height 100%
|
height 100%
|
||||||
|
|
||||||
|
> header
|
||||||
|
position fixed
|
||||||
|
top 0
|
||||||
|
z-index 10000
|
||||||
|
width 100%
|
||||||
|
color var(--mobileHeaderFg)
|
||||||
|
background-color var(--mobileHeaderBg)
|
||||||
|
box-shadow 0 1px 0 rgba(#000, 0.075)
|
||||||
|
|
||||||
|
&, *
|
||||||
|
user-select none
|
||||||
|
|
||||||
|
> span
|
||||||
|
display block
|
||||||
|
line-height $headerHeight
|
||||||
|
text-align center
|
||||||
|
|
||||||
|
> .nav
|
||||||
|
display block
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
z-index 10001
|
||||||
|
padding 0
|
||||||
|
width $headerHeight
|
||||||
|
font-size 1.4em
|
||||||
|
line-height $headerHeight
|
||||||
|
border-right solid 1px rgba(#000, 0.1)
|
||||||
|
|
||||||
|
> [data-fa]
|
||||||
|
transition all 0.2s ease
|
||||||
|
|
||||||
> nav
|
> nav
|
||||||
position fixed
|
position fixed
|
||||||
z-index 10000
|
z-index 20001
|
||||||
top 0
|
top 0
|
||||||
left 0
|
left 0
|
||||||
width 250px
|
width 250px
|
||||||
@ -187,9 +239,22 @@ export default Vue.extend({
|
|||||||
border-bottom solid 16px transparent
|
border-bottom solid 16px transparent
|
||||||
border-left solid 16px transparent
|
border-left solid 16px transparent
|
||||||
|
|
||||||
|
> .nav-backdrop
|
||||||
|
position fixed
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
z-index 20000
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
background var(--mobileNavBackdrop)
|
||||||
|
|
||||||
> main
|
> main
|
||||||
width 100%
|
width 100%
|
||||||
padding 32px 32px 32px calc(32px + 250px)
|
padding 0 0 0 250px
|
||||||
max-width 1300px
|
max-width 1300px
|
||||||
|
|
||||||
|
&.isMobile
|
||||||
|
> main
|
||||||
|
padding $headerHeight 0 0 0
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="axbwjelsbymowqjyywpirzhdlszoncqs">
|
||||||
<ui-card>
|
<ui-card>
|
||||||
<div slot="title">%i18n:@banner-url%</div>
|
<div slot="title">%fa:cog% %i18n:@instance%</div>
|
||||||
<section class="fit-top">
|
<section class="fit-top">
|
||||||
<ui-input v-model="bannerUrl"/>
|
<ui-input v-model="name">%i18n:@instance-name%</ui-input>
|
||||||
|
<ui-textarea v-model="description">%i18n:@instance-description%</ui-textarea>
|
||||||
|
<ui-input v-model="bannerUrl">%i18n:@banner-url%</ui-input>
|
||||||
|
<ui-input v-model="maxNoteTextLength">%i18n:@max-note-text-length%</ui-input>
|
||||||
<ui-button @click="updateMeta">%i18n:@save%</ui-button>
|
<ui-button @click="updateMeta">%i18n:@save%</ui-button>
|
||||||
</section>
|
</section>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
@ -35,28 +38,61 @@ export default Vue.extend({
|
|||||||
disableRegistration: false,
|
disableRegistration: false,
|
||||||
disableLocalTimeline: false,
|
disableLocalTimeline: false,
|
||||||
bannerUrl: null,
|
bannerUrl: null,
|
||||||
|
name: null,
|
||||||
|
description: null,
|
||||||
|
maxNoteTextLength: null,
|
||||||
inviteCode: null,
|
inviteCode: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
(this as any).os.getMeta().then(meta => {
|
||||||
|
this.bannerUrl = meta.bannerUrl;
|
||||||
|
this.name = meta.name;
|
||||||
|
this.description = meta.description;
|
||||||
|
this.maxNoteTextLength = meta.maxNoteTextLength;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
invite() {
|
invite() {
|
||||||
(this as any).api('admin/invite').then(x => {
|
(this as any).api('admin/invite').then(x => {
|
||||||
this.inviteCode = x.code;
|
this.inviteCode = x.code;
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
(this as any).os.apis.dialog({ text: `Failed ${e}` });
|
this.$swal({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateMeta() {
|
updateMeta() {
|
||||||
(this as any).api('admin/update-meta', {
|
(this as any).api('admin/update-meta', {
|
||||||
disableRegistration: this.disableRegistration,
|
disableRegistration: this.disableRegistration,
|
||||||
disableLocalTimeline: this.disableLocalTimeline,
|
disableLocalTimeline: this.disableLocalTimeline,
|
||||||
bannerUrl: this.bannerUrl
|
bannerUrl: this.bannerUrl,
|
||||||
|
name: this.name,
|
||||||
|
description: this.description,
|
||||||
|
maxNoteTextLength: parseInt(this.maxNoteTextLength, 10)
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
(this as any).os.apis.dialog({ text: `Saved` });
|
this.$swal({
|
||||||
|
type: 'success',
|
||||||
|
text: '%i18n:@saved%'
|
||||||
|
});
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
(this as any).os.apis.dialog({ text: `Failed ${e}` });
|
this.$swal({
|
||||||
|
type: 'error',
|
||||||
|
text: e
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.axbwjelsbymowqjyywpirzhdlszoncqs
|
||||||
|
@media (min-width 500px)
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
</style>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="ucnffhbtogqgscfmqcymwmmupoknpfsw">
|
||||||
<ui-card>
|
<ui-card>
|
||||||
<div slot="title">%i18n:@verify-user%</div>
|
<div slot="title">%i18n:@verify-user%</div>
|
||||||
<section class="fit-top">
|
<section class="fit-top">
|
||||||
@ -67,11 +67,11 @@ export default Vue.extend({
|
|||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await (this as any).os.api('users/show', parseAcct(this.verifyUsername));
|
const user = await (this as any).os.api('users/show', parseAcct(this.verifyUsername));
|
||||||
await (this as any).os.api('admin/verify-user', { userId: user.id });
|
await (this as any).os.api('admin/verify-user', { userId: user.id });
|
||||||
(this as any).os.apis.dialog({ text: '%i18n:@verified%' });
|
//(this as any).os.apis.dialog({ text: '%i18n:@verified%' });
|
||||||
};
|
};
|
||||||
|
|
||||||
await process().catch(e => {
|
await process().catch(e => {
|
||||||
(this as any).os.apis.dialog({ text: `Failed: ${e}` });
|
//(this as any).os.apis.dialog({ text: `Failed: ${e}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.verifying = false;
|
this.verifying = false;
|
||||||
@ -83,11 +83,11 @@ export default Vue.extend({
|
|||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await (this as any).os.api('users/show', parseAcct(this.unverifyUsername));
|
const user = await (this as any).os.api('users/show', parseAcct(this.unverifyUsername));
|
||||||
await (this as any).os.api('admin/unverify-user', { userId: user.id });
|
await (this as any).os.api('admin/unverify-user', { userId: user.id });
|
||||||
(this as any).os.apis.dialog({ text: '%i18n:@unverified%' });
|
//(this as any).os.apis.dialog({ text: '%i18n:@unverified%' });
|
||||||
};
|
};
|
||||||
|
|
||||||
await process().catch(e => {
|
await process().catch(e => {
|
||||||
(this as any).os.apis.dialog({ text: `Failed: ${e}` });
|
//(this as any).os.apis.dialog({ text: `Failed: ${e}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.unverifying = false;
|
this.unverifying = false;
|
||||||
@ -99,11 +99,11 @@ export default Vue.extend({
|
|||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await (this as any).os.api('users/show', parseAcct(this.suspendUsername));
|
const user = await (this as any).os.api('users/show', parseAcct(this.suspendUsername));
|
||||||
await (this as any).os.api('admin/suspend-user', { userId: user.id });
|
await (this as any).os.api('admin/suspend-user', { userId: user.id });
|
||||||
(this as any).os.apis.dialog({ text: '%i18n:@suspended%' });
|
//(this as any).os.apis.dialog({ text: '%i18n:@suspended%' });
|
||||||
};
|
};
|
||||||
|
|
||||||
await process().catch(e => {
|
await process().catch(e => {
|
||||||
(this as any).os.apis.dialog({ text: `Failed: ${e}` });
|
//(this as any).os.apis.dialog({ text: `Failed: ${e}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.suspending = false;
|
this.suspending = false;
|
||||||
@ -115,11 +115,11 @@ export default Vue.extend({
|
|||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await (this as any).os.api('users/show', parseAcct(this.unsuspendUsername));
|
const user = await (this as any).os.api('users/show', parseAcct(this.unsuspendUsername));
|
||||||
await (this as any).os.api('admin/unsuspend-user', { userId: user.id });
|
await (this as any).os.api('admin/unsuspend-user', { userId: user.id });
|
||||||
(this as any).os.apis.dialog({ text: '%i18n:@unsuspended%' });
|
//(this as any).os.apis.dialog({ text: '%i18n:@unsuspended%' });
|
||||||
};
|
};
|
||||||
|
|
||||||
await process().catch(e => {
|
await process().catch(e => {
|
||||||
(this as any).os.apis.dialog({ text: `Failed: ${e}` });
|
//(this as any).os.apis.dialog({ text: `Failed: ${e}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.unsuspending = false;
|
this.unsuspending = false;
|
||||||
@ -127,3 +127,10 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.ucnffhbtogqgscfmqcymwmmupoknpfsw
|
||||||
|
@media (min-width 500px)
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
</style>
|
||||||
|
63
src/client/app/common/views/components/github-setting.vue
Normal file
63
src/client/app/common/views/components/github-setting.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mk-github-setting">
|
||||||
|
<p>%i18n:@description%<a :href="`${docsUrl}/link-to-github`" target="_blank">%i18n:@detail%</a></p>
|
||||||
|
<p class="account" v-if="$store.state.i.github" :title="`GitHub ID: ${$store.state.i.github.id}`">%i18n:@connected-to%: <a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
|
||||||
|
<p>
|
||||||
|
<a :href="`${apiUrl}/connect/github`" target="_blank" @click.prevent="connect">{{ $store.state.i.github ? '%i18n:@reconnect%' : '%i18n:@connect%' }}</a>
|
||||||
|
<span v-if="$store.state.i.github"> or </span>
|
||||||
|
<a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github" @click.prevent="disconnect">%i18n:@disconnect%</a>
|
||||||
|
</p>
|
||||||
|
<p class="id" v-if="$store.state.i.github">GitHub ID: {{ $store.state.i.github.id }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { apiUrl, docsUrl } from '../../../config';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: null,
|
||||||
|
apiUrl,
|
||||||
|
docsUrl
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$watch('$store.state.i', () => {
|
||||||
|
if (this.$store.state.i.github && this.form)
|
||||||
|
this.form.close();
|
||||||
|
}, {
|
||||||
|
deep: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
connect() {
|
||||||
|
this.form = window.open(apiUrl + '/connect/github',
|
||||||
|
'github_connect_window',
|
||||||
|
'height=570, width=520');
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
window.open(apiUrl + '/disconnect/github',
|
||||||
|
'github_disconnect_window',
|
||||||
|
'height=570, width=520');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.mk-github-setting
|
||||||
|
.account
|
||||||
|
border solid 1px #e1e8ed
|
||||||
|
border-radius 4px
|
||||||
|
padding 16px
|
||||||
|
|
||||||
|
a
|
||||||
|
font-weight bold
|
||||||
|
color inherit
|
||||||
|
|
||||||
|
.id
|
||||||
|
color #8899a6
|
||||||
|
</style>
|
@ -37,12 +37,13 @@ import messaging from './messaging.vue';
|
|||||||
import messagingRoom from './messaging-room.vue';
|
import messagingRoom from './messaging-room.vue';
|
||||||
import urlPreview from './url-preview.vue';
|
import urlPreview from './url-preview.vue';
|
||||||
import twitterSetting from './twitter-setting.vue';
|
import twitterSetting from './twitter-setting.vue';
|
||||||
|
import githubSetting from './github-setting.vue';
|
||||||
import fileTypeIcon from './file-type-icon.vue';
|
import fileTypeIcon from './file-type-icon.vue';
|
||||||
import Reversi from './games/reversi/reversi.vue';
|
import Reversi from './games/reversi/reversi.vue';
|
||||||
import welcomeTimeline from './welcome-timeline.vue';
|
import welcomeTimeline from './welcome-timeline.vue';
|
||||||
import uiInput from './ui/input.vue';
|
import uiInput from './ui/input.vue';
|
||||||
import uiButton from './ui/button.vue';
|
import uiButton from './ui/button.vue';
|
||||||
import uiButtonGroup from './ui/button-group.vue';
|
import uiHorizonGroup from './ui/horizon-group.vue';
|
||||||
import uiCard from './ui/card.vue';
|
import uiCard from './ui/card.vue';
|
||||||
import uiForm from './ui/form.vue';
|
import uiForm from './ui/form.vue';
|
||||||
import uiTextarea from './ui/textarea.vue';
|
import uiTextarea from './ui/textarea.vue';
|
||||||
@ -90,12 +91,13 @@ Vue.component('mk-messaging', messaging);
|
|||||||
Vue.component('mk-messaging-room', messagingRoom);
|
Vue.component('mk-messaging-room', messagingRoom);
|
||||||
Vue.component('mk-url-preview', urlPreview);
|
Vue.component('mk-url-preview', urlPreview);
|
||||||
Vue.component('mk-twitter-setting', twitterSetting);
|
Vue.component('mk-twitter-setting', twitterSetting);
|
||||||
|
Vue.component('mk-github-setting', githubSetting);
|
||||||
Vue.component('mk-file-type-icon', fileTypeIcon);
|
Vue.component('mk-file-type-icon', fileTypeIcon);
|
||||||
Vue.component('mk-reversi', Reversi);
|
Vue.component('mk-reversi', Reversi);
|
||||||
Vue.component('mk-welcome-timeline', welcomeTimeline);
|
Vue.component('mk-welcome-timeline', welcomeTimeline);
|
||||||
Vue.component('ui-input', uiInput);
|
Vue.component('ui-input', uiInput);
|
||||||
Vue.component('ui-button', uiButton);
|
Vue.component('ui-button', uiButton);
|
||||||
Vue.component('ui-button-group', uiButtonGroup);
|
Vue.component('ui-horizon-group', uiHorizonGroup);
|
||||||
Vue.component('ui-card', uiCard);
|
Vue.component('ui-card', uiCard);
|
||||||
Vue.component('ui-form', uiForm);
|
Vue.component('ui-form', uiForm);
|
||||||
Vue.component('ui-textarea', uiTextarea);
|
Vue.component('ui-textarea', uiTextarea);
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
<span class="mk-nav">
|
<span class="mk-nav">
|
||||||
<a :href="aboutUrl">%i18n:@about%</a>
|
<a :href="aboutUrl">%i18n:@about%</a>
|
||||||
<i>・</i>
|
<i>・</i>
|
||||||
<a href="/stats">%i18n:@stats%</a>
|
|
||||||
<i>・</i>
|
|
||||||
<a :href="repositoryUrl">%i18n:@repository%</a>
|
<a :href="repositoryUrl">%i18n:@repository%</a>
|
||||||
<i>・</i>
|
<i>・</i>
|
||||||
<a :href="feedbackUrl" target="_blank">%i18n:@feedback%</a>
|
<a :href="feedbackUrl" target="_blank">%i18n:@feedback%</a>
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required styl="fill"/>
|
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required styl="fill"/>
|
||||||
<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
|
<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
|
||||||
<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p>
|
<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p>
|
||||||
|
<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/github`">%i18n:@signin-with-github%</a></p>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="pfzekjfwkwvadvlujpdnnxfggqgqjoze">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from 'vue';
|
|
||||||
export default Vue.extend({});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
.pfzekjfwkwvadvlujpdnnxfggqgqjoze
|
|
||||||
display flex
|
|
||||||
|
|
||||||
> *
|
|
||||||
flex 1
|
|
||||||
|
|
||||||
&:not(:last-child)
|
|
||||||
margin-right 16px
|
|
||||||
</style>
|
|
@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<component class="dmtdnykelhudezerjlfpbhgovrgnqqgr" :is="link ? 'a' : 'button'" :class="[styl, { inline, primary }]" :type="type" @click="$emit('click')">
|
<component class="dmtdnykelhudezerjlfpbhgovrgnqqgr"
|
||||||
|
:is="link ? 'a' : 'button'"
|
||||||
|
:class="[styl, { inline, primary }]"
|
||||||
|
:type="type"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
@ -7,6 +12,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
inject: {
|
||||||
|
horizonGrouped: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -20,7 +30,9 @@ export default Vue.extend({
|
|||||||
inline: {
|
inline: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default(): boolean {
|
||||||
|
return this.horizonGrouped;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
35
src/client/app/common/views/components/ui/horizon-group.vue
Normal file
35
src/client/app/common/views/components/ui/horizon-group.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pfzekjfwkwvadvlujpdnnxfggqgqjoze" :class="{ inputs }">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
provide: {
|
||||||
|
horizonGrouped: true
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
inputs: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.pfzekjfwkwvadvlujpdnnxfggqgqjoze
|
||||||
|
display flex
|
||||||
|
|
||||||
|
&.inputs
|
||||||
|
margin 32px 0
|
||||||
|
|
||||||
|
> *
|
||||||
|
flex 1
|
||||||
|
|
||||||
|
&:not(:last-child)
|
||||||
|
margin-right 16px
|
||||||
|
</style>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ui-input" :class="[{ focused, filled }, styl]">
|
<div class="ui-input" :class="[{ focused, filled, inline }, styl]">
|
||||||
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
|
<div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength">
|
||||||
@ -41,6 +41,11 @@ import Vue from 'vue';
|
|||||||
const getPasswordStrength = require('syuilo-password-strength');
|
const getPasswordStrength = require('syuilo-password-strength');
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
inject: {
|
||||||
|
horizonGrouped: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
required: false
|
required: false
|
||||||
@ -72,6 +77,13 @@ export default Vue.extend({
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
inline: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default(): boolean {
|
||||||
|
return this.horizonGrouped;
|
||||||
|
}
|
||||||
|
},
|
||||||
styl: {
|
styl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@ -337,4 +349,8 @@ root(fill)
|
|||||||
&:not(.fill)
|
&:not(.fill)
|
||||||
root(false)
|
root(false)
|
||||||
|
|
||||||
|
&.inline
|
||||||
|
display inline-block
|
||||||
|
margin 0
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -23,7 +23,6 @@ import updateBanner from './api/update-banner';
|
|||||||
import MkIndex from './views/pages/index.vue';
|
import MkIndex from './views/pages/index.vue';
|
||||||
import MkHome from './views/pages/home.vue';
|
import MkHome from './views/pages/home.vue';
|
||||||
import MkDeck from './views/pages/deck/deck.vue';
|
import MkDeck from './views/pages/deck/deck.vue';
|
||||||
import MkStats from './views/pages/stats/stats.vue';
|
|
||||||
import MkUser from './views/pages/user/user.vue';
|
import MkUser from './views/pages/user/user.vue';
|
||||||
import MkFavorites from './views/pages/favorites.vue';
|
import MkFavorites from './views/pages/favorites.vue';
|
||||||
import MkSelectDrive from './views/pages/selectdrive.vue';
|
import MkSelectDrive from './views/pages/selectdrive.vue';
|
||||||
@ -56,7 +55,6 @@ init(async (launch) => {
|
|||||||
{ path: '/', name: 'index', component: MkIndex },
|
{ path: '/', name: 'index', component: MkIndex },
|
||||||
{ path: '/home', name: 'home', component: MkHome },
|
{ path: '/home', name: 'home', component: MkHome },
|
||||||
{ path: '/deck', name: 'deck', component: MkDeck },
|
{ path: '/deck', name: 'deck', component: MkDeck },
|
||||||
{ path: '/stats', name: 'stats', component: MkStats },
|
|
||||||
{ path: '/i/customize-home', component: MkHomeCustomize },
|
{ path: '/i/customize-home', component: MkHomeCustomize },
|
||||||
{ path: '/i/favorites', component: MkFavorites },
|
{ path: '/i/favorites', component: MkFavorites },
|
||||||
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
import { Line } from 'vue-chartjs';
|
|
||||||
import * as mergeOptions from 'merge-options';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
extends: Line,
|
|
||||||
props: {
|
|
||||||
data: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
opts: {
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
data() {
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
render() {
|
|
||||||
this.renderChart(this.data, mergeOptions({
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
xAxes: [{
|
|
||||||
type: 'time',
|
|
||||||
distribution: 'series'
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
intersect: false,
|
|
||||||
mode: 'index',
|
|
||||||
position: 'nearest'
|
|
||||||
}
|
|
||||||
}, this.opts || {}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,723 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="gkgckalzgidaygcxnugepioremxvxvpt">
|
|
||||||
<header>
|
|
||||||
<b>%i18n:@title%:</b>
|
|
||||||
<select v-model="chartType">
|
|
||||||
<optgroup label="%i18n:@federation%">
|
|
||||||
<option value="federation-instances">%i18n:@charts.federation-instances%</option>
|
|
||||||
<option value="federation-instances-total">%i18n:@charts.federation-instances-total%</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="%i18n:@users%">
|
|
||||||
<option value="users">%i18n:@charts.users%</option>
|
|
||||||
<option value="users-total">%i18n:@charts.users-total%</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="%i18n:@notes%">
|
|
||||||
<option value="notes">%i18n:@charts.notes%</option>
|
|
||||||
<option value="local-notes">%i18n:@charts.local-notes%</option>
|
|
||||||
<option value="remote-notes">%i18n:@charts.remote-notes%</option>
|
|
||||||
<option value="notes-total">%i18n:@charts.notes-total%</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="%i18n:@drive%">
|
|
||||||
<option value="drive-files">%i18n:@charts.drive-files%</option>
|
|
||||||
<option value="drive-files-total">%i18n:@charts.drive-files-total%</option>
|
|
||||||
<option value="drive">%i18n:@charts.drive%</option>
|
|
||||||
<option value="drive-total">%i18n:@charts.drive-total%</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="%i18n:@network%">
|
|
||||||
<option value="network-requests">%i18n:@charts.network-requests%</option>
|
|
||||||
<option value="network-time">%i18n:@charts.network-time%</option>
|
|
||||||
<option value="network-usage">%i18n:@charts.network-usage%</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
<div>
|
|
||||||
<span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div>
|
|
||||||
<x-chart v-if="chart" :data="data[0]" :opts="data[1]"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from 'vue';
|
|
||||||
import XChart from './charts.chart.ts';
|
|
||||||
|
|
||||||
const colors = {
|
|
||||||
local: 'rgb(246, 88, 79)',
|
|
||||||
remote: 'rgb(65, 221, 222)',
|
|
||||||
|
|
||||||
localPlus: 'rgb(52, 178, 118)',
|
|
||||||
remotePlus: 'rgb(158, 255, 209)',
|
|
||||||
localMinus: 'rgb(255, 97, 74)',
|
|
||||||
remoteMinus: 'rgb(255, 149, 134)',
|
|
||||||
|
|
||||||
incoming: 'rgb(52, 178, 118)',
|
|
||||||
outgoing: 'rgb(255, 97, 74)',
|
|
||||||
};
|
|
||||||
|
|
||||||
const rgba = (color: string): string => {
|
|
||||||
return color.replace('rgb', 'rgba').replace(')', ', 0.1)');
|
|
||||||
};
|
|
||||||
|
|
||||||
const limit = 35;
|
|
||||||
|
|
||||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
|
||||||
const negate = arr => arr.map(x => -x);
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
components: {
|
|
||||||
XChart
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
now: null,
|
|
||||||
chart: null,
|
|
||||||
chartType: 'notes',
|
|
||||||
span: 'hour'
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
data(): any {
|
|
||||||
if (this.chart == null) return null;
|
|
||||||
switch (this.chartType) {
|
|
||||||
case 'federation-instances': return this.federationInstancesChart(false);
|
|
||||||
case 'federation-instances-total': return this.federationInstancesChart(true);
|
|
||||||
case 'users': return this.usersChart(false);
|
|
||||||
case 'users-total': return this.usersChart(true);
|
|
||||||
case 'notes': return this.notesChart('combined');
|
|
||||||
case 'local-notes': return this.notesChart('local');
|
|
||||||
case 'remote-notes': return this.notesChart('remote');
|
|
||||||
case 'notes-total': return this.notesTotalChart();
|
|
||||||
case 'drive': return this.driveChart();
|
|
||||||
case 'drive-total': return this.driveTotalChart();
|
|
||||||
case 'drive-files': return this.driveFilesChart();
|
|
||||||
case 'drive-files-total': return this.driveFilesTotalChart();
|
|
||||||
case 'network-requests': return this.networkRequestsChart();
|
|
||||||
case 'network-time': return this.networkTimeChart();
|
|
||||||
case 'network-usage': return this.networkUsageChart();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
stats(): any[] {
|
|
||||||
const stats =
|
|
||||||
this.span == 'day' ? this.chart.perDay :
|
|
||||||
this.span == 'hour' ? this.chart.perHour :
|
|
||||||
null;
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async created() {
|
|
||||||
this.now = new Date();
|
|
||||||
|
|
||||||
const [perHour, perDay] = await Promise.all([Promise.all([
|
|
||||||
(this as any).api('charts/federation', { limit: limit, span: 'hour' }),
|
|
||||||
(this as any).api('charts/users', { limit: limit, span: 'hour' }),
|
|
||||||
(this as any).api('charts/notes', { limit: limit, span: 'hour' }),
|
|
||||||
(this as any).api('charts/drive', { limit: limit, span: 'hour' }),
|
|
||||||
(this as any).api('charts/network', { limit: limit, span: 'hour' })
|
|
||||||
]), Promise.all([
|
|
||||||
(this as any).api('charts/federation', { limit: limit, span: 'day' }),
|
|
||||||
(this as any).api('charts/users', { limit: limit, span: 'day' }),
|
|
||||||
(this as any).api('charts/notes', { limit: limit, span: 'day' }),
|
|
||||||
(this as any).api('charts/drive', { limit: limit, span: 'day' }),
|
|
||||||
(this as any).api('charts/network', { limit: limit, span: 'day' })
|
|
||||||
])]);
|
|
||||||
|
|
||||||
const chart = {
|
|
||||||
perHour: {
|
|
||||||
federation: perHour[0],
|
|
||||||
users: perHour[1],
|
|
||||||
notes: perHour[2],
|
|
||||||
drive: perHour[3],
|
|
||||||
network: perHour[4]
|
|
||||||
},
|
|
||||||
perDay: {
|
|
||||||
federation: perDay[0],
|
|
||||||
users: perDay[1],
|
|
||||||
notes: perDay[2],
|
|
||||||
drive: perDay[3],
|
|
||||||
network: perDay[4]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.chart = chart;
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
getDate(i: number) {
|
|
||||||
const y = this.now.getFullYear();
|
|
||||||
const m = this.now.getMonth();
|
|
||||||
const d = this.now.getDate();
|
|
||||||
const h = this.now.getHours();
|
|
||||||
|
|
||||||
return (
|
|
||||||
this.span == 'day' ? new Date(y, m, d - i) :
|
|
||||||
this.span == 'hour' ? new Date(y, m, d, h - i) :
|
|
||||||
null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
format(arr) {
|
|
||||||
return arr.map((v, i) => ({ t: this.getDate(i).getTime(), y: v }));
|
|
||||||
},
|
|
||||||
|
|
||||||
federationInstancesChart(total: boolean): any {
|
|
||||||
return [{
|
|
||||||
datasets: [{
|
|
||||||
label: 'Instances',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.localPlus),
|
|
||||||
borderColor: colors.localPlus,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.federation.instance.total
|
|
||||||
: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec)))
|
|
||||||
}]
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
notesChart(type: string): any {
|
|
||||||
return [{
|
|
||||||
datasets: [{
|
|
||||||
label: 'All',
|
|
||||||
fill: false,
|
|
||||||
borderColor: '#555',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderDash: [4, 4],
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(type == 'combined'
|
|
||||||
? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
|
|
||||||
: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
label: 'Renotes',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: 'rgba(161, 222, 65, 0.1)',
|
|
||||||
borderColor: '#a1de41',
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(type == 'combined'
|
|
||||||
? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
|
|
||||||
: this.stats.notes[type].diffs.renote
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
label: 'Replies',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: 'rgba(247, 121, 108, 0.1)',
|
|
||||||
borderColor: '#f7796c',
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(type == 'combined'
|
|
||||||
? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
|
|
||||||
: this.stats.notes[type].diffs.reply
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
label: 'Normal',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: 'rgba(65, 221, 222, 0.1)',
|
|
||||||
borderColor: '#41ddde',
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(type == 'combined'
|
|
||||||
? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
|
|
||||||
: this.stats.notes[type].diffs.normal
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
scales: {
|
|
||||||
yAxes: [{
|
|
||||||
ticks: {
|
|
||||||
callback: value => {
|
|
||||||
return Vue.filter('number')(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
callbacks: {
|
|
||||||
label: (tooltipItem, data) => {
|
|
||||||
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
|
||||||
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
notesTotalChart(): any {
|
|
||||||
return [{
|
|
||||||
datasets: [{
|
|
||||||
label: 'Combined',
|
|
||||||
fill: false,
|
|
||||||
borderColor: '#555',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderDash: [4, 4],
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
|
|
||||||
}, {
|
|
||||||
label: 'Local',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.local),
|
|
||||||
borderColor: colors.local,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.notes.local.total)
|
|
||||||
}, {
|
|
||||||
label: 'Remote',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.remote),
|
|
||||||
borderColor: colors.remote,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.notes.remote.total)
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
scales: {
|
|
||||||
yAxes: [{
|
|
||||||
ticks: {
|
|
||||||
callback: value => {
|
|
||||||
return Vue.filter('number')(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
callbacks: {
|
|
||||||
label: (tooltipItem, data) => {
|
|
||||||
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
|
||||||
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
usersChart(total: boolean): any {
|
|
||||||
return [{
|
|
||||||
datasets: [{
|
|
||||||
label: 'Combined',
|
|
||||||
fill: false,
|
|
||||||
borderColor: '#555',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderDash: [4, 4],
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(total
|
|
||||||
? sum(this.stats.users.local.total, this.stats.users.remote.total)
|
|
||||||
: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
label: 'Local',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.local),
|
|
||||||
borderColor: colors.local,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.users.local.total
|
|
||||||
: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
label: 'Remote',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.remote),
|
|
||||||
borderColor: colors.remote,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.users.remote.total
|
|
||||||
: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
scales: {
|
|
||||||
yAxes: [{
|
|
||||||
ticks: {
|
|
||||||
callback: value => {
|
|
||||||
return Vue.filter('number')(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
callbacks: {
|
|
||||||
label: (tooltipItem, data) => {
|
|
||||||
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
|
||||||
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
driveChart(): any {
|
|
||||||
return [{
|
|
||||||
datasets: [{
|
|
||||||
label: 'All',
|
|
||||||
fill: false,
|
|
||||||
borderColor: '#555',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderDash: [4, 4],
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(sum(this.stats.drive.local.incSize, negate(this.stats.drive.local.decSize), this.stats.drive.remote.incSize, negate(this.stats.drive.remote.decSize)))
|
|
||||||
}, {
|
|
||||||
label: 'Local +',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.localPlus),
|
|
||||||
borderColor: colors.localPlus,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.drive.local.incSize)
|
|
||||||
}, {
|
|
||||||
label: 'Local -',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.localMinus),
|
|
||||||
borderColor: colors.localMinus,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(negate(this.stats.drive.local.decSize))
|
|
||||||
}, {
|
|
||||||
label: 'Remote +',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.remotePlus),
|
|
||||||
borderColor: colors.remotePlus,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.drive.remote.incSize)
|
|
||||||
}, {
|
|
||||||
label: 'Remote -',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.remoteMinus),
|
|
||||||
borderColor: colors.remoteMinus,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(negate(this.stats.drive.remote.decSize))
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
scales: {
|
|
||||||
yAxes: [{
|
|
||||||
ticks: {
|
|
||||||
callback: value => {
|
|
||||||
return Vue.filter('bytes')(value, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
callbacks: {
|
|
||||||
label: (tooltipItem, data) => {
|
|
||||||
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
|
||||||
return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
driveTotalChart(): any {
|
|
||||||
return [{
|
|
||||||
datasets: [{
|
|
||||||
label: 'Combined',
|
|
||||||
fill: false,
|
|
||||||
borderColor: '#555',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderDash: [4, 4],
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
|
|
||||||
}, {
|
|
||||||
label: 'Local',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.local),
|
|
||||||
borderColor: colors.local,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.drive.local.totalSize)
|
|
||||||
}, {
|
|
||||||
label: 'Remote',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.remote),
|
|
||||||
borderColor: colors.remote,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.drive.remote.totalSize)
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
scales: {
|
|
||||||
yAxes: [{
|
|
||||||
ticks: {
|
|
||||||
callback: value => {
|
|
||||||
return Vue.filter('bytes')(value, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
callbacks: {
|
|
||||||
label: (tooltipItem, data) => {
|
|
||||||
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
|
||||||
return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
driveFilesChart(): any {
|
|
||||||
return [{
|
|
||||||
datasets: [{
|
|
||||||
label: 'All',
|
|
||||||
fill: false,
|
|
||||||
borderColor: '#555',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderDash: [4, 4],
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(sum(this.stats.drive.local.incCount, negate(this.stats.drive.local.decCount), this.stats.drive.remote.incCount, negate(this.stats.drive.remote.decCount)))
|
|
||||||
}, {
|
|
||||||
label: 'Local +',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.localPlus),
|
|
||||||
borderColor: colors.localPlus,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.drive.local.incCount)
|
|
||||||
}, {
|
|
||||||
label: 'Local -',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.localMinus),
|
|
||||||
borderColor: colors.localMinus,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(negate(this.stats.drive.local.decCount))
|
|
||||||
}, {
|
|
||||||
label: 'Remote +',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.remotePlus),
|
|
||||||
borderColor: colors.remotePlus,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.drive.remote.incCount)
|
|
||||||
}, {
|
|
||||||
label: 'Remote -',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.remoteMinus),
|
|
||||||
borderColor: colors.remoteMinus,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(negate(this.stats.drive.remote.decCount))
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
scales: {
|
|
||||||
yAxes: [{
|
|
||||||
ticks: {
|
|
||||||
callback: value => {
|
|
||||||
return Vue.filter('number')(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
callbacks: {
|
|
||||||
label: (tooltipItem, data) => {
|
|
||||||
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
|
||||||
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
driveFilesTotalChart(): any {
|
|
||||||
return [{
|
|
||||||
datasets: [{
|
|
||||||
label: 'Combined',
|
|
||||||
fill: false,
|
|
||||||
borderColor: '#555',
|
|
||||||
borderWidth: 2,
|
|
||||||
borderDash: [4, 4],
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
|
|
||||||
}, {
|
|
||||||
label: 'Local',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.local),
|
|
||||||
borderColor: colors.local,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.drive.local.totalCount)
|
|
||||||
}, {
|
|
||||||
label: 'Remote',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.remote),
|
|
||||||
borderColor: colors.remote,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.drive.remote.totalCount)
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
scales: {
|
|
||||||
yAxes: [{
|
|
||||||
ticks: {
|
|
||||||
callback: value => {
|
|
||||||
return Vue.filter('number')(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
callbacks: {
|
|
||||||
label: (tooltipItem, data) => {
|
|
||||||
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
|
||||||
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
networkRequestsChart(): any {
|
|
||||||
return [{
|
|
||||||
datasets: [{
|
|
||||||
label: 'Incoming',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.localPlus),
|
|
||||||
borderColor: colors.localPlus,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.network.incomingRequests)
|
|
||||||
}]
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
networkTimeChart(): any {
|
|
||||||
const data = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < limit; i++) {
|
|
||||||
data.push(this.stats.network.incomingRequests[i] != 0 ? (this.stats.network.totalTime[i] / this.stats.network.incomingRequests[i]) : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [{
|
|
||||||
datasets: [{
|
|
||||||
label: 'Avg time (ms)',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.localPlus),
|
|
||||||
borderColor: colors.localPlus,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(data)
|
|
||||||
}]
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
networkUsageChart(): any {
|
|
||||||
return [{
|
|
||||||
datasets: [{
|
|
||||||
label: 'Incoming',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.incoming),
|
|
||||||
borderColor: colors.incoming,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.network.incomingBytes)
|
|
||||||
}, {
|
|
||||||
label: 'Outgoing',
|
|
||||||
fill: true,
|
|
||||||
backgroundColor: rgba(colors.outgoing),
|
|
||||||
borderColor: colors.outgoing,
|
|
||||||
borderWidth: 2,
|
|
||||||
pointBackgroundColor: '#fff',
|
|
||||||
lineTension: 0,
|
|
||||||
data: this.format(this.stats.network.outgoingBytes)
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
scales: {
|
|
||||||
yAxes: [{
|
|
||||||
ticks: {
|
|
||||||
callback: value => {
|
|
||||||
return Vue.filter('bytes')(value, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
callbacks: {
|
|
||||||
label: (tooltipItem, data) => {
|
|
||||||
const label = data.datasets[tooltipItem.datasetIndex].label || '';
|
|
||||||
return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
.gkgckalzgidaygcxnugepioremxvxvpt
|
|
||||||
padding 32px
|
|
||||||
background #fff
|
|
||||||
box-shadow 0 2px 8px rgba(#000, 0.1)
|
|
||||||
|
|
||||||
*
|
|
||||||
user-select none
|
|
||||||
|
|
||||||
> header
|
|
||||||
display flex
|
|
||||||
margin 0 0 1em 0
|
|
||||||
padding 0 0 8px 0
|
|
||||||
font-size 1em
|
|
||||||
color #555
|
|
||||||
border-bottom solid 1px #eee
|
|
||||||
|
|
||||||
> b
|
|
||||||
margin-right 8px
|
|
||||||
|
|
||||||
> *:last-child
|
|
||||||
margin-left auto
|
|
||||||
|
|
||||||
*
|
|
||||||
&:not(.active)
|
|
||||||
color var(--primary)
|
|
||||||
cursor pointer
|
|
||||||
|
|
||||||
> div
|
|
||||||
> *
|
|
||||||
display block
|
|
||||||
height 350px
|
|
||||||
|
|
||||||
</style>
|
|
@ -23,6 +23,13 @@
|
|||||||
<mk-twitter-setting/>
|
<mk-twitter-setting/>
|
||||||
</section>
|
</section>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
|
|
||||||
|
<ui-card>
|
||||||
|
<div slot="title">%fa:B github% %i18n:@github%</div>
|
||||||
|
<section>
|
||||||
|
<mk-github-setting/>
|
||||||
|
</section>
|
||||||
|
</ui-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ui-card class="theme" v-show="page == 'theme'">
|
<ui-card class="theme" v-show="page == 'theme'">
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p>
|
<p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="$store.state.i.isAdmin">
|
<li v-if="$store.state.i.isAdmin">
|
||||||
<router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link>
|
<a href="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="tcrwdhwpuxrwmcttxjcsehgpagpstqey">
|
|
||||||
<div v-if="stats" class="stats">
|
|
||||||
<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
|
|
||||||
<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
|
|
||||||
<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
|
|
||||||
<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<x-charts/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import Vue from "vue";
|
|
||||||
import XCharts from "../../components/charts.vue";
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
components: {
|
|
||||||
XCharts
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
stats: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
(this as any).api('stats').then(stats => {
|
|
||||||
this.stats = stats;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
|
|
||||||
|
|
||||||
.tcrwdhwpuxrwmcttxjcsehgpagpstqey
|
|
||||||
width 100%
|
|
||||||
padding 16px
|
|
||||||
|
|
||||||
> .stats
|
|
||||||
display flex
|
|
||||||
justify-content center
|
|
||||||
margin 0 auto 16px auto
|
|
||||||
padding 32px
|
|
||||||
background #fff
|
|
||||||
box-shadow 0 2px 8px rgba(#000, 0.1)
|
|
||||||
|
|
||||||
> div
|
|
||||||
flex 1
|
|
||||||
text-align center
|
|
||||||
|
|
||||||
> *:first-child
|
|
||||||
display block
|
|
||||||
color var(--primary)
|
|
||||||
|
|
||||||
> *:last-child
|
|
||||||
font-size 70%
|
|
||||||
|
|
||||||
> div
|
|
||||||
max-width 950px
|
|
||||||
margin 0 auto
|
|
||||||
</style>
|
|
26
src/client/app/desktop/views/pages/user/user.github.vue
Normal file
26
src/client/app/desktop/views/pages/user/user.github.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="aqooishiizumijmihokohinatamihoaz">
|
||||||
|
<span>%fa:B github%<a :href="`https://github.com/${user.github.login}`" target="_blank">@{{ user.github.login }}</a></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: ['user']
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.aqooishiizumijmihokohinatamihoaz
|
||||||
|
padding 32px
|
||||||
|
background #171515
|
||||||
|
border-radius 6px
|
||||||
|
color #fff
|
||||||
|
|
||||||
|
a
|
||||||
|
margin-left 8px
|
||||||
|
color #fff
|
||||||
|
|
||||||
|
</style>
|
@ -2,7 +2,7 @@
|
|||||||
<mk-ui>
|
<mk-ui>
|
||||||
<div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching">
|
<div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching">
|
||||||
<div class="is-suspended" v-if="user.isSuspended">%fa:exclamation-triangle% %i18n:@is-suspended%</div>
|
<div class="is-suspended" v-if="user.isSuspended">%fa:exclamation-triangle% %i18n:@is-suspended%</div>
|
||||||
<div class="is-remote" v-if="user.host != null">%fa:exclamation-triangle% %i18n:common.is-remote-user%<a :href="user.url || user.uri" target="_blank">%i18n:common.view-on-remote%</a></div>
|
<div class="is-remote" v-if="user.host">%fa:exclamation-triangle% %i18n:common.is-remote-user%<a :href="user.url || user.uri" target="_blank">%i18n:common.view-on-remote%</a></div>
|
||||||
<main>
|
<main>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<x-header :user="user"/>
|
<x-header :user="user"/>
|
||||||
@ -12,14 +12,15 @@
|
|||||||
<div class="side">
|
<div class="side">
|
||||||
<div class="instance" v-if="!$store.getters.isSignedIn"><mk-instance/></div>
|
<div class="instance" v-if="!$store.getters.isSignedIn"><mk-instance/></div>
|
||||||
<x-profile :user="user"/>
|
<x-profile :user="user"/>
|
||||||
<x-twitter :user="user" v-if="user.host === null && user.twitter"/>
|
<x-twitter :user="user" v-if="!user.host && user.twitter"/>
|
||||||
|
<x-github :user="user" v-if="!user.host && user.github"/>
|
||||||
<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
|
<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
|
||||||
<mk-activity :user="user"/>
|
<mk-activity :user="user"/>
|
||||||
<x-photos :user="user"/>
|
<x-photos :user="user"/>
|
||||||
<x-friends :user="user"/>
|
<x-friends :user="user"/>
|
||||||
<x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
|
<x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
|
||||||
<div class="nav"><mk-nav/></div>
|
<div class="nav"><mk-nav/></div>
|
||||||
<p v-if="user.host === null">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
|
<p v-if="!user.host">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@ -37,6 +38,7 @@ import XPhotos from './user.photos.vue';
|
|||||||
import XFollowersYouKnow from './user.followers-you-know.vue';
|
import XFollowersYouKnow from './user.followers-you-know.vue';
|
||||||
import XFriends from './user.friends.vue';
|
import XFriends from './user.friends.vue';
|
||||||
import XTwitter from './user.twitter.vue';
|
import XTwitter from './user.twitter.vue';
|
||||||
|
import XGithub from './user.github.vue'; // ?MEM: Don't fix the intentional typo. (XGitHub -> `<x-git-hub>`)
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -46,7 +48,8 @@ export default Vue.extend({
|
|||||||
XPhotos,
|
XPhotos,
|
||||||
XFollowersYouKnow,
|
XFollowersYouKnow,
|
||||||
XFriends,
|
XFriends,
|
||||||
XTwitter
|
XTwitter,
|
||||||
|
XGithub // ?MEM: Don't fix the intentional typo. (see L41)
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li>
|
<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li>
|
||||||
<li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
|
<li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
|
||||||
<li v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link></li>
|
<li v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><a href="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</a></li>
|
||||||
<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li>
|
<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -125,6 +125,19 @@
|
|||||||
</section>
|
</section>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
|
|
||||||
|
<ui-card>
|
||||||
|
<div slot="title">%fa:B github% %i18n:@github%</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p class="account" v-if="$store.state.i.github"><a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
|
||||||
|
<p>
|
||||||
|
<a :href="`${apiUrl}/connect/github`" target="_blank">{{ $store.state.i.github ? '%i18n:@github-reconnect%' : '%i18n:@github-connect%' }}</a>
|
||||||
|
<span v-if="$store.state.i.github"> or </span>
|
||||||
|
<a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github">%i18n:@github-disconnect%</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</ui-card>
|
||||||
|
|
||||||
<mk-api-settings />
|
<mk-api-settings />
|
||||||
|
|
||||||
<ui-card>
|
<ui-card>
|
||||||
|
@ -215,5 +215,11 @@
|
|||||||
reversiGameEmptyCell: ':lighten<2<$secondary',
|
reversiGameEmptyCell: ':lighten<2<$secondary',
|
||||||
reversiGameEmptyCellMyTurn: ':lighten<5<$secondary',
|
reversiGameEmptyCellMyTurn: ':lighten<5<$secondary',
|
||||||
reversiGameEmptyCellCanPut: ':lighten<4<$secondary',
|
reversiGameEmptyCellCanPut: ':lighten<4<$secondary',
|
||||||
|
|
||||||
|
adminDashboardHeaderFg: ':alpha<0.9<$text',
|
||||||
|
adminDashboardHeaderBorder: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
adminDashboardCardBg: '$secondary',
|
||||||
|
adminDashboardCardFg: '$text',
|
||||||
|
adminDashboardCardDivider: 'rgba(0, 0, 0, 0.3)',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -215,5 +215,11 @@
|
|||||||
reversiGameEmptyCell: 'rgba(0, 0, 0, 0.06)',
|
reversiGameEmptyCell: 'rgba(0, 0, 0, 0.06)',
|
||||||
reversiGameEmptyCellMyTurn: 'rgba(0, 0, 0, 0.12)',
|
reversiGameEmptyCellMyTurn: 'rgba(0, 0, 0, 0.12)',
|
||||||
reversiGameEmptyCellCanPut: 'rgba(0, 0, 0, 0.9)',
|
reversiGameEmptyCellCanPut: 'rgba(0, 0, 0, 0.9)',
|
||||||
|
|
||||||
|
adminDashboardHeaderFg: ':alpha<0.9<$text',
|
||||||
|
adminDashboardHeaderBorder: 'rgba(0, 0, 0, 0.1)',
|
||||||
|
adminDashboardCardBg: '$secondary',
|
||||||
|
adminDashboardCardFg: '$text',
|
||||||
|
adminDashboardCardDivider: 'rgba(0, 0, 0, 0.082)',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -49,10 +49,6 @@ export default function load() {
|
|||||||
if (config.localDriveCapacityMb == null) config.localDriveCapacityMb = 256;
|
if (config.localDriveCapacityMb == null) config.localDriveCapacityMb = 256;
|
||||||
if (config.remoteDriveCapacityMb == null) config.remoteDriveCapacityMb = 8;
|
if (config.remoteDriveCapacityMb == null) config.remoteDriveCapacityMb = 8;
|
||||||
|
|
||||||
if (config.maxNoteTextLength == null) config.maxNoteTextLength = 1000;
|
|
||||||
|
|
||||||
if (config.name == null) config.name = 'Misskey';
|
|
||||||
|
|
||||||
return Object.assign(config, mixin);
|
return Object.assign(config, mixin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,8 +18,6 @@ export type Source = {
|
|||||||
repository_url?: string;
|
repository_url?: string;
|
||||||
feedback_url?: string;
|
feedback_url?: string;
|
||||||
};
|
};
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
languages?: string[];
|
languages?: string[];
|
||||||
welcome_bg_url?: string;
|
welcome_bg_url?: string;
|
||||||
url: string;
|
url: string;
|
||||||
@ -74,6 +72,10 @@ export type Source = {
|
|||||||
consumer_key: string;
|
consumer_key: string;
|
||||||
consumer_secret: string;
|
consumer_secret: string;
|
||||||
};
|
};
|
||||||
|
github?: {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
};
|
||||||
github_bot?: {
|
github_bot?: {
|
||||||
hook_secret: string;
|
hook_secret: string;
|
||||||
username: string;
|
username: string;
|
||||||
@ -105,8 +107,6 @@ export type Source = {
|
|||||||
engine: string;
|
engine: string;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
maxNoteTextLength?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,7 +9,7 @@ export type TextElementEmoji = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function(text: string) {
|
export default function(text: string) {
|
||||||
const match = text.match(/^:([a-zA-Z0-9+-_]+?):/);
|
const match = text.match(/^:([a-zA-Z0-9+_-]+):/);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
const emoji = match[0];
|
const emoji = match[0];
|
||||||
return {
|
return {
|
||||||
|
@ -1,9 +1,37 @@
|
|||||||
import db from '../db/mongodb';
|
import db from '../db/mongodb';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
const Meta = db.get<IMeta>('meta');
|
const Meta = db.get<IMeta>('meta');
|
||||||
export default Meta;
|
export default Meta;
|
||||||
|
|
||||||
|
// 後方互換性のため。
|
||||||
|
// 過去のMisskeyではインスタンス名や紹介を設定ファイルに記述していたのでそれを移行
|
||||||
|
if ((config as any).name) {
|
||||||
|
Meta.findOne({}).then(m => {
|
||||||
|
if (m != null && m.name == null) {
|
||||||
|
Meta.update({}, {
|
||||||
|
$set: {
|
||||||
|
name: (config as any).name
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ((config as any).description) {
|
||||||
|
Meta.findOne({}).then(m => {
|
||||||
|
if (m != null && m.description == null) {
|
||||||
|
Meta.update({}, {
|
||||||
|
$set: {
|
||||||
|
description: (config as any).description
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export type IMeta = {
|
export type IMeta = {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
broadcasts?: any[];
|
broadcasts?: any[];
|
||||||
stats?: {
|
stats?: {
|
||||||
notesCount: number;
|
notesCount: number;
|
||||||
@ -15,4 +43,9 @@ export type IMeta = {
|
|||||||
disableLocalTimeline?: boolean;
|
disableLocalTimeline?: boolean;
|
||||||
hidedTags?: string[];
|
hidedTags?: string[];
|
||||||
bannerUrl?: string;
|
bannerUrl?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max allowed note text length in charactors
|
||||||
|
*/
|
||||||
|
maxNoteTextLength?: number;
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,6 @@ import Reaction from './note-reaction';
|
|||||||
import { packMany as packFileMany, IDriveFile } from './drive-file';
|
import { packMany as packFileMany, IDriveFile } from './drive-file';
|
||||||
import Favorite from './favorite';
|
import Favorite from './favorite';
|
||||||
import Following from './following';
|
import Following from './following';
|
||||||
import config from '../config';
|
|
||||||
import Emoji from './emoji';
|
import Emoji from './emoji';
|
||||||
|
|
||||||
const Note = db.get<INote>('notes');
|
const Note = db.get<INote>('notes');
|
||||||
@ -27,10 +26,6 @@ Note.createIndex({ createdAt: -1 });
|
|||||||
Note.createIndex({ score: -1 }, { sparse: true });
|
Note.createIndex({ score: -1 }, { sparse: true });
|
||||||
export default Note;
|
export default Note;
|
||||||
|
|
||||||
export function isValidText(text: string): boolean {
|
|
||||||
return length(text.trim()) <= config.maxNoteTextLength && text.trim() != '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidCw(text: string): boolean {
|
export function isValidCw(text: string): boolean {
|
||||||
return length(text.trim()) <= 100;
|
return length(text.trim()) <= 100;
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,11 @@ export interface ILocalUser extends IUserBase {
|
|||||||
userId: string;
|
userId: string;
|
||||||
screenName: string;
|
screenName: string;
|
||||||
};
|
};
|
||||||
|
github: {
|
||||||
|
accessToken: string;
|
||||||
|
id: string;
|
||||||
|
login: string;
|
||||||
|
};
|
||||||
line: {
|
line: {
|
||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
@ -280,6 +285,9 @@ export const pack = (
|
|||||||
delete _user.twitter.accessToken;
|
delete _user.twitter.accessToken;
|
||||||
delete _user.twitter.accessTokenSecret;
|
delete _user.twitter.accessTokenSecret;
|
||||||
}
|
}
|
||||||
|
if (_user.github) {
|
||||||
|
delete _user.github.accessToken;
|
||||||
|
}
|
||||||
delete _user.line;
|
delete _user.line;
|
||||||
|
|
||||||
// Visible via only the official client
|
// Visible via only the official client
|
||||||
|
@ -4,7 +4,7 @@ import parse from '../../../mfm/parse';
|
|||||||
|
|
||||||
export default function(note: INote) {
|
export default function(note: INote) {
|
||||||
let html = toHtml(parse(note.text), note.mentionedRemoteUsers);
|
let html = toHtml(parse(note.text), note.mentionedRemoteUsers);
|
||||||
if (html == null) html = '';
|
if (html == null) html = '<p>.</p>';
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,8 @@ export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, Error] {
|
function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, Error] {
|
||||||
|
if (defs.params == null) return [params, null];
|
||||||
|
|
||||||
const x: any = {};
|
const x: any = {};
|
||||||
let err: Error = null;
|
let err: Error = null;
|
||||||
Object.entries(defs.params).some(([k, def]) => {
|
Object.entries(defs.params).some(([k, def]) => {
|
||||||
@ -38,7 +40,7 @@ function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, E
|
|||||||
(err as any).param = k;
|
(err as any).param = k;
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (v === undefined && def.default) {
|
if (v === undefined && def.hasOwnProperty('default')) {
|
||||||
x[k] = def.default;
|
x[k] = def.default;
|
||||||
} else {
|
} else {
|
||||||
x[k] = v;
|
x[k] = v;
|
||||||
|
@ -12,27 +12,30 @@ export const meta = {
|
|||||||
|
|
||||||
params: {
|
params: {
|
||||||
name: {
|
name: {
|
||||||
validator: $.str
|
validator: $.str.min(1)
|
||||||
},
|
},
|
||||||
|
|
||||||
url: {
|
url: {
|
||||||
validator: $.str
|
validator: $.str.min(1)
|
||||||
},
|
},
|
||||||
|
|
||||||
aliases: {
|
aliases: {
|
||||||
validator: $.arr($.str).optional,
|
validator: $.arr($.str.min(1)).optional,
|
||||||
default: [] as string[]
|
default: [] as string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default define(meta, (ps) => new Promise(async (res, rej) => {
|
export default define(meta, (ps) => new Promise(async (res, rej) => {
|
||||||
await Emoji.insert({
|
const emoji = await Emoji.insert({
|
||||||
|
updatedAt: new Date(),
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
host: null,
|
host: null,
|
||||||
aliases: ps.aliases,
|
aliases: ps.aliases,
|
||||||
url: ps.url
|
url: ps.url
|
||||||
});
|
});
|
||||||
|
|
||||||
res();
|
res({
|
||||||
|
id: emoji._id
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
|
@ -39,6 +39,7 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
|
|||||||
|
|
||||||
await Emoji.update({ _id: emoji._id }, {
|
await Emoji.update({ _id: emoji._id }, {
|
||||||
$set: {
|
$set: {
|
||||||
|
updatedAt: new Date(),
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
aliases: ps.aliases,
|
aliases: ps.aliases,
|
||||||
url: ps.url
|
url: ps.url
|
||||||
|
@ -45,6 +45,27 @@ export const meta = {
|
|||||||
'ja-JP': 'インスタンスのバナー画像URL'
|
'ja-JP': 'インスタンスのバナー画像URL'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
name: {
|
||||||
|
validator: $.str.optional.nullable,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': 'インスタンス名'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
description: {
|
||||||
|
validator: $.str.optional.nullable,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': 'インスタンスの紹介文'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
maxNoteTextLength: {
|
||||||
|
validator: $.num.optional.min(1),
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '投稿の最大文字数'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,6 +92,18 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
|
|||||||
set.bannerUrl = ps.bannerUrl;
|
set.bannerUrl = ps.bannerUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.name !== undefined) {
|
||||||
|
set.name = ps.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.description !== undefined) {
|
||||||
|
set.description = ps.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.maxNoteTextLength) {
|
||||||
|
set.maxNoteTextLength = ps.maxNoteTextLength;
|
||||||
|
}
|
||||||
|
|
||||||
await Meta.update({}, {
|
await Meta.update({}, {
|
||||||
$set: set
|
$set: set
|
||||||
}, { upsert: true });
|
}, { upsert: true });
|
||||||
|
@ -26,7 +26,7 @@ export const meta = {
|
|||||||
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
||||||
const folders = await DriveFolder
|
const folders = await DriveFolder
|
||||||
.find({
|
.find({
|
||||||
name: name,
|
name: ps.name,
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
parentId: ps.parentId
|
parentId: ps.parentId
|
||||||
});
|
});
|
||||||
|
@ -41,8 +41,8 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
clientVersion: client.version,
|
clientVersion: client.version,
|
||||||
|
|
||||||
name: config.name || 'Misskey',
|
name: met.name || 'Misskey',
|
||||||
description: config.description,
|
description: met.description,
|
||||||
|
|
||||||
secure: config.https != null,
|
secure: config.https != null,
|
||||||
machine: os.hostname(),
|
machine: os.hostname(),
|
||||||
@ -62,7 +62,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
swPublickey: config.sw ? config.sw.public_key : null,
|
swPublickey: config.sw ? config.sw.public_key : null,
|
||||||
hidedTags: (me && me.isAdmin) ? met.hidedTags : undefined,
|
hidedTags: (me && me.isAdmin) ? met.hidedTags : undefined,
|
||||||
bannerUrl: met.bannerUrl,
|
bannerUrl: met.bannerUrl,
|
||||||
maxNoteTextLength: config.maxNoteTextLength,
|
maxNoteTextLength: met.maxNoteTextLength || 1000,
|
||||||
|
|
||||||
emojis: emojis,
|
emojis: emojis,
|
||||||
|
|
||||||
@ -73,6 +73,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
recaptcha: config.recaptcha ? true : false,
|
recaptcha: config.recaptcha ? true : false,
|
||||||
objectStorage: config.drive && config.drive.storage === 'minio',
|
objectStorage: config.drive && config.drive.storage === 'minio',
|
||||||
twitter: config.twitter ? true : false,
|
twitter: config.twitter ? true : false,
|
||||||
|
github: config.github ? true : false,
|
||||||
serviceWorker: config.sw ? true : false,
|
serviceWorker: config.sw ? true : false,
|
||||||
userRecommendation: config.user_recommendation ? config.user_recommendation : {}
|
userRecommendation: config.user_recommendation ? config.user_recommendation : {}
|
||||||
} : undefined
|
} : undefined
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
import $ from 'cafy'; import ID, { transform, transformMany } from '../../../../misc/cafy-id';
|
import $ from 'cafy'; import ID, { transform, transformMany } from '../../../../misc/cafy-id';
|
||||||
const ms = require('ms');
|
const ms = require('ms');
|
||||||
import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note';
|
import { length } from 'stringz';
|
||||||
|
import Note, { INote, isValidCw, pack } from '../../../../models/note';
|
||||||
import User, { IUser } from '../../../../models/user';
|
import User, { IUser } from '../../../../models/user';
|
||||||
import DriveFile, { IDriveFile } from '../../../../models/drive-file';
|
import DriveFile, { IDriveFile } from '../../../../models/drive-file';
|
||||||
import create from '../../../../services/note/create';
|
import create from '../../../../services/note/create';
|
||||||
import define from '../../define';
|
import define from '../../define';
|
||||||
|
import Meta from '../../../../models/meta';
|
||||||
|
|
||||||
|
let maxNoteTextLength = 1000;
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
Meta.findOne({}).then(m => {
|
||||||
|
if (m.maxNoteTextLength) maxNoteTextLength = m.maxNoteTextLength;
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
stability: 'stable',
|
stability: 'stable',
|
||||||
@ -40,7 +50,9 @@ export const meta = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
text: {
|
text: {
|
||||||
validator: $.str.optional.nullable.pipe(isValidText),
|
validator: $.str.optional.nullable.pipe(text =>
|
||||||
|
length(text.trim()) <= maxNoteTextLength && text.trim() != ''
|
||||||
|
),
|
||||||
default: null as any,
|
default: null as any,
|
||||||
desc: {
|
desc: {
|
||||||
'ja-JP': '投稿内容'
|
'ja-JP': '投稿内容'
|
||||||
|
@ -4,12 +4,18 @@ import { toASCII } from 'punycode';
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import Meta from '../../models/meta';
|
import Meta from '../../models/meta';
|
||||||
import { ObjectID } from 'bson';
|
import { ObjectID } from 'bson';
|
||||||
|
import Emoji from '../../models/emoji';
|
||||||
const pkg = require('../../../package.json');
|
const pkg = require('../../../package.json');
|
||||||
|
|
||||||
// Init router
|
// Init router
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
router.get('/v1/custom_emojis', async ctx => ctx.body = {});
|
router.get('/v1/custom_emojis', async ctx => ctx.body =
|
||||||
|
await Emoji.find({ host: null }, {
|
||||||
|
fields: {
|
||||||
|
_id: false
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
router.get('/v1/instance', async ctx => { // TODO: This is a temporary implementation. Consider creating helper methods!
|
router.get('/v1/instance', async ctx => { // TODO: This is a temporary implementation. Consider creating helper methods!
|
||||||
const meta = await Meta.findOne() || {};
|
const meta = await Meta.findOne() || {};
|
||||||
@ -37,8 +43,8 @@ router.get('/v1/instance', async ctx => { // TODO: This is a temporary implement
|
|||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
uri: config.hostname,
|
uri: config.hostname,
|
||||||
title: config.name || 'Misskey',
|
title: meta.name || 'Misskey',
|
||||||
description: config.description || '',
|
description: meta.description || '',
|
||||||
email: config.maintainer.email || config.maintainer.url.startsWith('mailto:') ? config.maintainer.url.slice(7) : '',
|
email: config.maintainer.email || config.maintainer.url.startsWith('mailto:') ? config.maintainer.url.slice(7) : '',
|
||||||
version: `0.0.0:compatible:misskey:${pkg.version}`, // TODO: How to tell about that this is an api for compatibility?
|
version: `0.0.0:compatible:misskey:${pkg.version}`, // TODO: How to tell about that this is an api for compatibility?
|
||||||
thumbnail: meta.bannerUrl,
|
thumbnail: meta.bannerUrl,
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import * as EventEmitter from 'events';
|
import * as EventEmitter from 'events';
|
||||||
|
import * as Koa from 'koa';
|
||||||
import * as Router from 'koa-router';
|
import * as Router from 'koa-router';
|
||||||
import * as request from 'request';
|
import * as request from 'request';
|
||||||
const crypto = require('crypto');
|
import { OAuth2 } from 'oauth';
|
||||||
|
import User, { IUser, pack, ILocalUser } from '../../../models/user';
|
||||||
import User, { IUser } from '../../../models/user';
|
|
||||||
import createNote from '../../../services/note/create';
|
import createNote from '../../../services/note/create';
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
|
import { publishMainStream } from '../../../stream';
|
||||||
|
import redis from '../../../db/redis';
|
||||||
|
import uuid = require('uuid');
|
||||||
|
import signin from '../common/signin';
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
const handler = new EventEmitter();
|
const handler = new EventEmitter();
|
||||||
|
|
||||||
@ -28,10 +33,264 @@ const post = async (text: string, home = true) => {
|
|||||||
createNote(bot, { text, visibility: home ? 'home' : 'public' });
|
createNote(bot, { text, visibility: home ? 'home' : 'public' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getUserToken(ctx: Koa.Context) {
|
||||||
|
return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareOrigin(ctx: Koa.Context) {
|
||||||
|
function normalizeUrl(url: string) {
|
||||||
|
return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = ctx.headers['referer'];
|
||||||
|
|
||||||
|
return (normalizeUrl(referer) == normalizeUrl(config.url));
|
||||||
|
}
|
||||||
|
|
||||||
// Init router
|
// Init router
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
if (config.github_bot != null) {
|
router.get('/disconnect/github', async ctx => {
|
||||||
|
if (!compareOrigin(ctx)) {
|
||||||
|
ctx.throw(400, 'invalid origin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userToken = getUserToken(ctx);
|
||||||
|
if (!userToken) {
|
||||||
|
ctx.throw(400, 'signin required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findOneAndUpdate({
|
||||||
|
host: null,
|
||||||
|
'token': userToken
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
'github': null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = `GitHubの連携を解除しました :v:`;
|
||||||
|
|
||||||
|
// Publish i updated event
|
||||||
|
publishMainStream(user._id, 'meUpdated', await pack(user, user, {
|
||||||
|
detail: true,
|
||||||
|
includeSecrets: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config.github || !redis) {
|
||||||
|
router.get('/connect/github', ctx => {
|
||||||
|
ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)';
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/signin/github', ctx => {
|
||||||
|
ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const oauth2 = new OAuth2(
|
||||||
|
config.github.client_id,
|
||||||
|
config.github.client_secret,
|
||||||
|
'https://github.com/',
|
||||||
|
'login/oauth/authorize',
|
||||||
|
'login/oauth/access_token');
|
||||||
|
|
||||||
|
router.get('/connect/github', async ctx => {
|
||||||
|
if (!compareOrigin(ctx)) {
|
||||||
|
ctx.throw(400, 'invalid origin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userToken = getUserToken(ctx);
|
||||||
|
if (!userToken) {
|
||||||
|
ctx.throw(400, 'signin required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
redirect_uri: `${config.url}/api/gh/cb`,
|
||||||
|
scope: ['read:user'],
|
||||||
|
state: uuid()
|
||||||
|
};
|
||||||
|
|
||||||
|
redis.set(userToken, JSON.stringify(params));
|
||||||
|
ctx.redirect(oauth2.getAuthorizeUrl(params));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/signin/github', async ctx => {
|
||||||
|
const sessid = uuid();
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
redirect_uri: `${config.url}/api/gh/cb`,
|
||||||
|
scope: ['read:user'],
|
||||||
|
state: uuid()
|
||||||
|
};
|
||||||
|
|
||||||
|
const expires = 1000 * 60 * 60; // 1h
|
||||||
|
ctx.cookies.set('signin_with_github_session_id', sessid, {
|
||||||
|
path: '/',
|
||||||
|
domain: config.host,
|
||||||
|
secure: config.url.startsWith('https'),
|
||||||
|
httpOnly: true,
|
||||||
|
expires: new Date(Date.now() + expires),
|
||||||
|
maxAge: expires
|
||||||
|
});
|
||||||
|
|
||||||
|
redis.set(sessid, JSON.stringify(params));
|
||||||
|
ctx.redirect(oauth2.getAuthorizeUrl(params));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/gh/cb', async ctx => {
|
||||||
|
const userToken = getUserToken(ctx);
|
||||||
|
|
||||||
|
if (!userToken) {
|
||||||
|
const sessid = ctx.cookies.get('signin_with_github_session_id');
|
||||||
|
|
||||||
|
if (!sessid) {
|
||||||
|
ctx.throw(400, 'invalid session');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = ctx.query.code;
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
ctx.throw(400, 'invalid session');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||||
|
redis.get(sessid, async (_, state) => {
|
||||||
|
res(JSON.parse(state));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ctx.query.state !== state) {
|
||||||
|
ctx.throw(400, 'invalid session');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken } = await new Promise<any>((res, rej) =>
|
||||||
|
oauth2.getOAuthAccessToken(
|
||||||
|
code,
|
||||||
|
{ redirect_uri },
|
||||||
|
(err, accessToken, refresh, result) => {
|
||||||
|
if (err)
|
||||||
|
rej(err);
|
||||||
|
else if (result.error)
|
||||||
|
rej(result.error);
|
||||||
|
else
|
||||||
|
res({ accessToken });
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { login, id } = await new Promise<any>((res, rej) =>
|
||||||
|
request({
|
||||||
|
url: 'https://api.github.com/user',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/vnd.github.v3+json',
|
||||||
|
'Authorization': `bearer ${accessToken}`,
|
||||||
|
'User-Agent': config.user_agent
|
||||||
|
}
|
||||||
|
}, (err, response, body) => {
|
||||||
|
if (err)
|
||||||
|
rej(err);
|
||||||
|
else
|
||||||
|
res(JSON.parse(body));
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!login || !id) {
|
||||||
|
ctx.throw(400, 'invalid session');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
host: null,
|
||||||
|
'github.id': id
|
||||||
|
}) as ILocalUser;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
signin(ctx, user, true);
|
||||||
|
} else {
|
||||||
|
const code = ctx.query.code;
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
ctx.throw(400, 'invalid session');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { redirect_uri, state } = await new Promise<any>((res, rej) => {
|
||||||
|
redis.get(userToken, async (_, state) => {
|
||||||
|
res(JSON.parse(state));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ctx.query.state !== state) {
|
||||||
|
ctx.throw(400, 'invalid session');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken } = await new Promise<any>((res, rej) =>
|
||||||
|
oauth2.getOAuthAccessToken(
|
||||||
|
code,
|
||||||
|
{ redirect_uri },
|
||||||
|
(err, accessToken, refresh, result) => {
|
||||||
|
if (err)
|
||||||
|
rej(err);
|
||||||
|
else if (result.error)
|
||||||
|
rej(result.error);
|
||||||
|
else
|
||||||
|
res({ accessToken });
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { login, id } = await new Promise<any>((res, rej) =>
|
||||||
|
request({
|
||||||
|
url: 'https://api.github.com/user',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/vnd.github.v3+json',
|
||||||
|
'Authorization': `bearer ${accessToken}`,
|
||||||
|
'User-Agent': config.user_agent
|
||||||
|
}
|
||||||
|
}, (err, response, body) => {
|
||||||
|
if (err)
|
||||||
|
rej(err);
|
||||||
|
else
|
||||||
|
res(JSON.parse(body));
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!login || !id) {
|
||||||
|
ctx.throw(400, 'invalid session');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findOneAndUpdate({
|
||||||
|
host: null,
|
||||||
|
token: userToken
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
github: {
|
||||||
|
accessToken,
|
||||||
|
id,
|
||||||
|
login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
|
||||||
|
|
||||||
|
// Publish i updated event
|
||||||
|
publishMainStream(user._id, 'meUpdated', await pack(user, user, {
|
||||||
|
detail: true,
|
||||||
|
includeSecrets: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.github_bot) {
|
||||||
const secret = config.github_bot.hook_secret;
|
const secret = config.github_bot.hook_secret;
|
||||||
|
|
||||||
router.post('/hooks/github', ctx => {
|
router.post('/hooks/github', ctx => {
|
||||||
|
Reference in New Issue
Block a user