Merge branch 'develop' into pr/ThatOneCalculator/8764
This commit is contained in:
commit
9cd1526073
249 changed files with 4798 additions and 6822 deletions
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -11,17 +11,31 @@ You should also include the user name that made the change.
|
||||||
|
|
||||||
## 12.x.x (unreleased)
|
## 12.x.x (unreleased)
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- ハイライトがみつけるに統合されました
|
||||||
|
- カスタム絵文字ページはインスタンス情報ページに統合されました
|
||||||
|
- 連合ページはインスタンス情報ページに統合されました
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
- Server: Allow GET method for some endpoints @syuilo
|
- Server: Allow GET method for some endpoints @syuilo
|
||||||
- Server: Add rate limit to i/notifications @tamaina
|
- Server: Add rate limit to i/notifications @tamaina
|
||||||
- Client: Improve control panel @syuilo
|
- Client: Improve control panel @syuilo
|
||||||
- Client: Show warning in control panel when there is an unresolved abuse report @syuilo
|
- Client: Show warning in control panel when there is an unresolved abuse report @syuilo
|
||||||
|
- Client: Add instance-cloud widget @syuilo
|
||||||
|
- Client: Add rss-ticker widget @syuilo
|
||||||
|
- Client: Removing entries from a clip @futchitwo
|
||||||
|
- Client: Poll highlights in explore page @syuilo
|
||||||
|
- Client: Improve deck UI @syuilo
|
||||||
|
- Client: Word mute also checks content warnings @Johann150
|
||||||
|
- ユーザーにモデレーションメモを残せる機能 @syuilo
|
||||||
- Make possible to delete an account by admin @syuilo
|
- Make possible to delete an account by admin @syuilo
|
||||||
- Improve player detection in URL preview @mei23
|
- Improve player detection in URL preview @mei23
|
||||||
- Add Badge Image to Push Notification #8012 @tamaina
|
- Add Badge Image to Push Notification #8012 @tamaina
|
||||||
- Client: Removing entries from a clip @futchitwo
|
- Server: Improve performance
|
||||||
- Server: Supports IPv6 on Redis transport. @mei23
|
- Server: Supports IPv6 on Redis transport. @mei23
|
||||||
IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`.
|
IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`.
|
||||||
|
- Server: Add possibility to log IP addresses of users @syuilo
|
||||||
|
- Add additional drive capacity change support @CyberRex0
|
||||||
- Migrate to Yarn Berry (v3.2.1) @ThatOneCalculator
|
- Migrate to Yarn Berry (v3.2.1) @ThatOneCalculator
|
||||||
- You may have to `yarn run clean-all` and `yarn set version berry` before running `yarn install` if you're still on yarn classic
|
- You may have to `yarn run clean-all` and `yarn set version berry` before running `yarn install` if you're still on yarn classic
|
||||||
|
|
||||||
|
@ -30,6 +44,10 @@ You should also include the user name that made the change.
|
||||||
- Server: Ensure temp directory cleanup @Johann150
|
- Server: Ensure temp directory cleanup @Johann150
|
||||||
- favicons of federated instances not showing @syuilo
|
- favicons of federated instances not showing @syuilo
|
||||||
- Admin: The checkbox for blocking an instance works again @Johann150
|
- Admin: The checkbox for blocking an instance works again @Johann150
|
||||||
|
- Client: Prevent access to user pages when not logged in @pixeldesu @Johann150
|
||||||
|
- Client: Disable some hotkeys (e.g. for creating a post) for not logged in users @pixeldesu
|
||||||
|
- Client: Ask users that are not logged in to log in when trying to vote in a poll @Johann150
|
||||||
|
- Instance mutes also apply in antennas etc. @Johann150
|
||||||
|
|
||||||
## 12.111.1 (2022/06/13)
|
## 12.111.1 (2022/06/13)
|
||||||
|
|
||||||
|
|
|
@ -203,6 +203,7 @@ done: "完了"
|
||||||
processing: "処理中"
|
processing: "処理中"
|
||||||
preview: "プレビュー"
|
preview: "プレビュー"
|
||||||
default: "デフォルト"
|
default: "デフォルト"
|
||||||
|
defaultValueIs: "デフォルト: {value}"
|
||||||
noCustomEmojis: "絵文字はありません"
|
noCustomEmojis: "絵文字はありません"
|
||||||
noJobs: "ジョブはありません"
|
noJobs: "ジョブはありません"
|
||||||
federating: "連合中"
|
federating: "連合中"
|
||||||
|
@ -381,6 +382,7 @@ administrator: "管理者"
|
||||||
token: "トークン"
|
token: "トークン"
|
||||||
twoStepAuthentication: "二段階認証"
|
twoStepAuthentication: "二段階認証"
|
||||||
moderator: "モデレーター"
|
moderator: "モデレーター"
|
||||||
|
moderation: "モデレーション"
|
||||||
nUsersMentioned: "{n}人が投稿"
|
nUsersMentioned: "{n}人が投稿"
|
||||||
securityKey: "セキュリティキー"
|
securityKey: "セキュリティキー"
|
||||||
securityKeyName: "キーの名前"
|
securityKeyName: "キーの名前"
|
||||||
|
@ -541,7 +543,7 @@ relays: "リレー"
|
||||||
addRelay: "リレーの追加"
|
addRelay: "リレーの追加"
|
||||||
inboxUrl: "inboxのURL"
|
inboxUrl: "inboxのURL"
|
||||||
addedRelays: "追加済みのリレー"
|
addedRelays: "追加済みのリレー"
|
||||||
serviceworkerInfo: "プッシュ通知を行うには有効する必要があります。"
|
serviceworkerInfo: "プッシュ通知を行うには有効にする必要があります。"
|
||||||
deletedNote: "削除された投稿"
|
deletedNote: "削除された投稿"
|
||||||
invisibleNote: "非公開の投稿"
|
invisibleNote: "非公開の投稿"
|
||||||
enableInfiniteScroll: "自動でもっと見る"
|
enableInfiniteScroll: "自動でもっと見る"
|
||||||
|
@ -854,9 +856,19 @@ noEmailServerWarning: "メールサーバーの設定がされていません。
|
||||||
thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。"
|
thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。"
|
||||||
recommended: "推奨"
|
recommended: "推奨"
|
||||||
check: "チェック"
|
check: "チェック"
|
||||||
|
driveCapOverrideLabel: "このユーザーのドライブ容量上限を変更"
|
||||||
|
driveCapOverrideCaption: "0以下を指定すると解除されます。"
|
||||||
|
requireAdminForView: "閲覧するには管理者アカウントでログインしている必要があります。"
|
||||||
isSystemAccount: "システムにより自動で作成・管理されているアカウントです。"
|
isSystemAccount: "システムにより自動で作成・管理されているアカウントです。"
|
||||||
typeToConfirm: "この操作を行うには {x} と入力してください"
|
typeToConfirm: "この操作を行うには {x} と入力してください"
|
||||||
deleteAccount: "アカウント削除"
|
deleteAccount: "アカウント削除"
|
||||||
|
document: "ドキュメント"
|
||||||
|
numberOfPageCache: "ページキャッシュ数"
|
||||||
|
numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
|
||||||
|
logoutConfirm: "ログアウトしますか?"
|
||||||
|
lastActiveDate: "最終利用日時"
|
||||||
|
statusbar: "ステータスバー"
|
||||||
|
pleaseSelect: "選択してください"
|
||||||
|
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "既に使用されています"
|
used: "既に使用されています"
|
||||||
|
@ -1242,10 +1254,12 @@ _widgets:
|
||||||
trends: "トレンド"
|
trends: "トレンド"
|
||||||
clock: "時計"
|
clock: "時計"
|
||||||
rss: "RSSリーダー"
|
rss: "RSSリーダー"
|
||||||
|
rssTicker: "RSSティッカー"
|
||||||
activity: "アクティビティ"
|
activity: "アクティビティ"
|
||||||
photos: "フォト"
|
photos: "フォト"
|
||||||
digitalClock: "デジタル時計"
|
digitalClock: "デジタル時計"
|
||||||
federation: "連合"
|
federation: "連合"
|
||||||
|
instanceCloud: "インスタンスクラウド"
|
||||||
postForm: "投稿フォーム"
|
postForm: "投稿フォーム"
|
||||||
slideshow: "スライドショー"
|
slideshow: "スライドショー"
|
||||||
button: "ボタン"
|
button: "ボタン"
|
||||||
|
@ -1710,8 +1724,6 @@ _notification:
|
||||||
_deck:
|
_deck:
|
||||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||||
columnAlign: "カラムの寄せ"
|
columnAlign: "カラムの寄せ"
|
||||||
columnMargin: "カラム間のマージン"
|
|
||||||
columnHeaderHeight: "カラムのヘッダー幅"
|
|
||||||
addColumn: "カラムを追加"
|
addColumn: "カラムを追加"
|
||||||
swapLeft: "左に移動"
|
swapLeft: "左に移動"
|
||||||
swapRight: "右に移動"
|
swapRight: "右に移動"
|
||||||
|
@ -1720,6 +1732,9 @@ _deck:
|
||||||
stackLeft: "左に重ねる"
|
stackLeft: "左に重ねる"
|
||||||
popRight: "右に出す"
|
popRight: "右に出す"
|
||||||
profile: "プロファイル"
|
profile: "プロファイル"
|
||||||
|
introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!"
|
||||||
|
introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。"
|
||||||
|
widgetsIntroduction: "カラムのメニューから、「ウィジェットの編集」を選択してウィジェットを追加してください"
|
||||||
|
|
||||||
_columns:
|
_columns:
|
||||||
main: "メイン"
|
main: "メイン"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "12.112.0-beta.7",
|
"version": "12.112.0-beta.16",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -48,13 +48,13 @@
|
||||||
"@types/gulp": "4.0.9",
|
"@types/gulp": "4.0.9",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "latest",
|
"@typescript-eslint/eslint-plugin": "latest",
|
||||||
"@typescript-eslint/parser": "5.27.1",
|
"@typescript-eslint/parser": "5.30.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "10.0.3",
|
"cypress": "10.3.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-vue": "latest",
|
"eslint-plugin-vue": "latest",
|
||||||
"start-server-and-test": "1.14.0",
|
"start-server-and-test": "1.14.0",
|
||||||
"typescript": "4.7.3",
|
"typescript": "4.7.4",
|
||||||
"vue-eslint-parser": "^9.0.2"
|
"vue-eslint-parser": "^9.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
export class driveCapacityOverrideMb1655813815729 {
|
||||||
|
name = 'driveCapacityOverrideMb1655813815729'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "driveCapacityOverrideMb" integer`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "driveCapacityOverrideMb"`);
|
||||||
|
}
|
||||||
|
}
|
17
packages/backend/migration/1655918165614-user-ip.js
Normal file
17
packages/backend/migration/1655918165614-user-ip.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export class userIp1655918165614 {
|
||||||
|
name = 'userIp1655918165614'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "user_ip" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "ip" character varying(128) NOT NULL, CONSTRAINT "PK_2c44ddfbf7c0464d028dcef325e" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_7f7f1c66f48e9a8e18a33bc515" ON "user_ip" ("userId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_361b500e06721013c124b7b6c5" ON "user_ip" ("userId", "ip") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_ip" ADD CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_ip" DROP CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_361b500e06721013c124b7b6c5"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_7f7f1c66f48e9a8e18a33bc515"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_ip"`);
|
||||||
|
}
|
||||||
|
}
|
13
packages/backend/migration/1656122560740-file-ip.js
Normal file
13
packages/backend/migration/1656122560740-file-ip.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export class fileIp1656122560740 {
|
||||||
|
name = 'fileIp1656122560740'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "drive_file" ADD "requestHeaders" jsonb DEFAULT '{}'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "drive_file" ADD "requestIp" character varying(128)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "requestIp"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "requestHeaders"`);
|
||||||
|
}
|
||||||
|
}
|
13
packages/backend/migration/1656328812281-ip-2.js
Normal file
13
packages/backend/migration/1656328812281-ip-2.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export class ip21656328812281 {
|
||||||
|
name = 'ip21656328812281'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_ip" DROP CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "enableIpLogging" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableIpLogging"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_ip" ADD CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class userModerationNote1656772790599 {
|
||||||
|
name = 'userModerationNote1656772790599'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "moderationNote"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,9 +18,9 @@
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^3.11.1",
|
"@bull-board/api": "4.0.0",
|
||||||
"@bull-board/koa": "3.10.4",
|
"@bull-board/koa": "4.0.0",
|
||||||
"@bull-board/ui": "^3.11.1",
|
"@bull-board/ui": "4.0.0",
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
"@elastic/elasticsearch": "7.17.0",
|
"@elastic/elasticsearch": "7.17.0",
|
||||||
"@koa/cors": "3.3.0",
|
"@koa/cors": "3.3.0",
|
||||||
|
@ -34,10 +34,10 @@
|
||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
"autobind-decorator": "2.4.0",
|
"autobind-decorator": "2.4.0",
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"aws-sdk": "2.1152.0",
|
"aws-sdk": "2.1165.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "1.1.5",
|
"blurhash": "1.1.5",
|
||||||
"bull": "4.8.3",
|
"bull": "4.8.4",
|
||||||
"cacheable-lookup": "6.0.4",
|
"cacheable-lookup": "6.0.4",
|
||||||
"cbor": "8.1.0",
|
"cbor": "8.1.0",
|
||||||
"chalk": "5.0.1",
|
"chalk": "5.0.1",
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
"ip-cidr": "3.0.10",
|
"ip-cidr": "3.0.10",
|
||||||
"is-svg": "4.3.2",
|
"is-svg": "4.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "19.0.0",
|
"jsdom": "20.0.0",
|
||||||
"json5": "2.2.1",
|
"json5": "2.2.1",
|
||||||
"json5-loader": "4.0.1",
|
"json5-loader": "4.0.1",
|
||||||
"jsonld": "6.0.0",
|
"jsonld": "6.0.0",
|
||||||
|
@ -79,26 +79,27 @@
|
||||||
"multer": "1.4.4",
|
"multer": "1.4.4",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.2.6",
|
"node-fetch": "3.2.6",
|
||||||
"nodemailer": "6.7.5",
|
"nodemailer": "6.7.6",
|
||||||
"oauth": "^0.9.15",
|
"oauth": "^0.9.15",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"parse5": "6.0.1",
|
"parse5": "7.0.0",
|
||||||
"pg": "8.7.3",
|
"pg": "8.7.3",
|
||||||
"private-ip": "2.3.3",
|
"private-ip": "2.3.3",
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.2",
|
||||||
"punycode": "2.1.1",
|
"punycode": "2.1.1",
|
||||||
"pureimage": "0.3.8",
|
"pureimage": "0.3.14",
|
||||||
"qrcode": "1.5.0",
|
"qrcode": "1.5.0",
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.17.4",
|
"re2": "1.17.7",
|
||||||
"redis-lock": "0.1.4",
|
"redis-lock": "0.1.4",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
"require-all": "3.0.0",
|
"require-all": "3.0.0",
|
||||||
"rndstr": "1.0.0",
|
"rndstr": "1.0.0",
|
||||||
|
"rss-parser": "3.12.0",
|
||||||
"s-age": "1.1.2",
|
"s-age": "1.1.2",
|
||||||
"sanitize-html": "2.7.0",
|
"sanitize-html": "2.7.0",
|
||||||
"semver": "7.3.7",
|
"semver": "7.3.7",
|
||||||
|
@ -109,15 +110,15 @@
|
||||||
"style-loader": "3.3.1",
|
"style-loader": "3.3.1",
|
||||||
"summaly": "2.6.0",
|
"summaly": "2.6.0",
|
||||||
"syslog-pro": "1.0.0",
|
"syslog-pro": "1.0.0",
|
||||||
"systeminformation": "5.11.16",
|
"systeminformation": "5.11.22",
|
||||||
"tinycolor2": "1.4.2",
|
"tinycolor2": "1.4.2",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"ts-loader": "9.3.0",
|
"ts-loader": "9.3.1",
|
||||||
"ts-node": "10.8.1",
|
"ts-node": "10.8.1",
|
||||||
"tsc-alias": "1.6.9",
|
"tsc-alias": "1.6.11",
|
||||||
"tsconfig-paths": "4.0.0",
|
"tsconfig-paths": "4.0.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typeorm": "0.3.6",
|
"typeorm": "0.3.7",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"unzipper": "0.10.11",
|
"unzipper": "0.10.11",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
|
@ -150,11 +151,10 @@
|
||||||
"@types/koa__multer": "2.0.4",
|
"@types/koa__multer": "2.0.4",
|
||||||
"@types/koa__router": "8.0.11",
|
"@types/koa__router": "8.0.11",
|
||||||
"@types/mocha": "9.1.1",
|
"@types/mocha": "9.1.1",
|
||||||
"@types/node": "17.0.41",
|
"@types/node": "18.0.0",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.4",
|
"@types/nodemailer": "6.4.4",
|
||||||
"@types/oauth": "0.9.1",
|
"@types/oauth": "0.9.1",
|
||||||
"@types/parse5": "6.0.3",
|
|
||||||
"@types/pug": "2.0.6",
|
"@types/pug": "2.0.6",
|
||||||
"@types/punycode": "2.1.0",
|
"@types/punycode": "2.1.0",
|
||||||
"@types/qrcode": "1.4.2",
|
"@types/qrcode": "1.4.2",
|
||||||
|
@ -163,8 +163,8 @@
|
||||||
"@types/redis": "4.0.11",
|
"@types/redis": "4.0.11",
|
||||||
"@types/rename": "1.0.4",
|
"@types/rename": "1.0.4",
|
||||||
"@types/sanitize-html": "2.6.2",
|
"@types/sanitize-html": "2.6.2",
|
||||||
"@types/semver": "7.3.9",
|
"@types/semver": "7.3.10",
|
||||||
"@types/sharp": "0.30.2",
|
"@types/sharp": "0.30.4",
|
||||||
"@types/sinonjs__fake-timers": "8.1.2",
|
"@types/sinonjs__fake-timers": "8.1.2",
|
||||||
"@types/speakeasy": "2.0.7",
|
"@types/speakeasy": "2.0.7",
|
||||||
"@types/tinycolor2": "1.4.3",
|
"@types/tinycolor2": "1.4.3",
|
||||||
|
@ -173,13 +173,13 @@
|
||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.3",
|
"@types/ws": "8.5.3",
|
||||||
"@typescript-eslint/eslint-plugin": "5.27.1",
|
"@typescript-eslint/eslint-plugin": "5.30.0",
|
||||||
"@typescript-eslint/parser": "5.27.1",
|
"@typescript-eslint/parser": "5.30.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.17.0",
|
"eslint": "8.18.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"execa": "6.1.0",
|
"execa": "6.1.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"typescript": "4.7.2"
|
"typescript": "4.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,9 +68,10 @@ import { RegistryItem } from '@/models/entities/registry-item.js';
|
||||||
import { Ad } from '@/models/entities/ad.js';
|
import { Ad } from '@/models/entities/ad.js';
|
||||||
import { PasswordResetRequest } from '@/models/entities/password-reset-request.js';
|
import { PasswordResetRequest } from '@/models/entities/password-reset-request.js';
|
||||||
import { UserPending } from '@/models/entities/user-pending.js';
|
import { UserPending } from '@/models/entities/user-pending.js';
|
||||||
|
import { Webhook } from '@/models/entities/webhook.js';
|
||||||
|
import { UserIp } from '@/models/entities/user-ip.js';
|
||||||
|
|
||||||
import { entities as charts } from '@/services/chart/entities.js';
|
import { entities as charts } from '@/services/chart/entities.js';
|
||||||
import { Webhook } from '@/models/entities/webhook.js';
|
|
||||||
import { envOption } from '../env.js';
|
import { envOption } from '../env.js';
|
||||||
import { dbLogger } from './logger.js';
|
import { dbLogger } from './logger.js';
|
||||||
import { redisClient } from './redis.js';
|
import { redisClient } from './redis.js';
|
||||||
|
@ -173,6 +174,7 @@ export const entities = [
|
||||||
PasswordResetRequest,
|
PasswordResetRequest,
|
||||||
UserPending,
|
UserPending,
|
||||||
Webhook,
|
Webhook,
|
||||||
|
UserIp,
|
||||||
...charts,
|
...charts,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import * as parse5 from 'parse5';
|
|
||||||
import treeAdapter from 'parse5/lib/tree-adapters/default.js';
|
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
|
import * as parse5 from 'parse5';
|
||||||
|
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
|
||||||
|
|
||||||
|
const treeAdapter = TreeAdapter.defaultTreeAdapter;
|
||||||
|
|
||||||
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
|
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
|
||||||
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
|
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
|
||||||
|
@ -19,7 +21,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
||||||
|
|
||||||
return text.trim();
|
return text.trim();
|
||||||
|
|
||||||
function getText(node: parse5.Node): string {
|
function getText(node: TreeAdapter.Node): string {
|
||||||
if (treeAdapter.isTextNode(node)) return node.value;
|
if (treeAdapter.isTextNode(node)) return node.value;
|
||||||
if (!treeAdapter.isElementNode(node)) return '';
|
if (!treeAdapter.isElementNode(node)) return '';
|
||||||
if (node.nodeName === 'br') return '\n';
|
if (node.nodeName === 'br') return '\n';
|
||||||
|
@ -31,7 +33,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendChildren(childNodes: parse5.ChildNode[]): void {
|
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
|
||||||
if (childNodes) {
|
if (childNodes) {
|
||||||
for (const n of childNodes) {
|
for (const n of childNodes) {
|
||||||
analyze(n);
|
analyze(n);
|
||||||
|
@ -39,7 +41,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function analyze(node: parse5.Node) {
|
function analyze(node: TreeAdapter.Node) {
|
||||||
if (treeAdapter.isTextNode(node)) {
|
if (treeAdapter.isTextNode(node)) {
|
||||||
text += node.value;
|
text += node.value;
|
||||||
return;
|
return;
|
||||||
|
@ -170,7 +172,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
||||||
const t = getText(node);
|
const t = getText(node);
|
||||||
if (t) {
|
if (t) {
|
||||||
text += '\n> ';
|
text += '\n> ';
|
||||||
text += t.split('\n').join(`\n> `);
|
text += t.split('\n').join('\n> ');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||||
|
import { id } from '../id.js';
|
||||||
import { User } from './user.js';
|
import { User } from './user.js';
|
||||||
import { DriveFolder } from './drive-folder.js';
|
import { DriveFolder } from './drive-folder.js';
|
||||||
import { id } from '../id.js';
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Index(['userId', 'folderId', 'id'])
|
@Index(['userId', 'folderId', 'id'])
|
||||||
|
@ -165,4 +165,15 @@ export class DriveFile {
|
||||||
comment: 'Whether the DriveFile is direct link to remote server.',
|
comment: 'Whether the DriveFile is direct link to remote server.',
|
||||||
})
|
})
|
||||||
public isLink: boolean;
|
public isLink: boolean;
|
||||||
|
|
||||||
|
@Column('jsonb', {
|
||||||
|
default: {},
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public requestHeaders: Record<string, string> | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128, nullable: true,
|
||||||
|
})
|
||||||
|
public requestIp: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
import { User } from './user.js';
|
|
||||||
import { id } from '../id.js';
|
import { id } from '../id.js';
|
||||||
|
import { User } from './user.js';
|
||||||
import { Clip } from './clip.js';
|
import { Clip } from './clip.js';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@ -427,4 +427,9 @@ export class Meta {
|
||||||
default: true,
|
default: true,
|
||||||
})
|
})
|
||||||
public objectStorageS3ForcePathStyle: boolean;
|
public objectStorageS3ForcePathStyle: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public enableIpLogging: boolean;
|
||||||
}
|
}
|
||||||
|
|
24
packages/backend/src/models/entities/user-ip.ts
Normal file
24
packages/backend/src/models/entities/user-ip.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { id } from '../id.js';
|
||||||
|
import { Note } from './note.js';
|
||||||
|
import { User } from './user.js';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['userId', 'ip'], { unique: true })
|
||||||
|
export class UserIp {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
})
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column(id())
|
||||||
|
public userId: User['id'];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128,
|
||||||
|
})
|
||||||
|
public ip: string;
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||||
|
import { ffVisibility, notificationTypes } from '@/types.js';
|
||||||
import { id } from '../id.js';
|
import { id } from '../id.js';
|
||||||
import { User } from './user.js';
|
import { User } from './user.js';
|
||||||
import { Page } from './page.js';
|
import { Page } from './page.js';
|
||||||
import { ffVisibility, notificationTypes } from '@/types.js';
|
|
||||||
|
|
||||||
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
|
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
|
||||||
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
|
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
|
||||||
|
@ -117,6 +117,11 @@ export class UserProfile {
|
||||||
})
|
})
|
||||||
public password: string | null;
|
public password: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 8192, default: '',
|
||||||
|
})
|
||||||
|
public moderationNote: string | null;
|
||||||
|
|
||||||
// TODO: そのうち消す
|
// TODO: そのうち消す
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: {},
|
default: {},
|
||||||
|
|
|
@ -218,6 +218,12 @@ export class User {
|
||||||
})
|
})
|
||||||
public token: string | null;
|
public token: string | null;
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
nullable: true,
|
||||||
|
comment: 'Overrides user drive capacity limit',
|
||||||
|
})
|
||||||
|
public driveCapacityOverrideMb: number | null;
|
||||||
|
|
||||||
constructor(data: Partial<User>) {
|
constructor(data: Partial<User>) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,7 @@ import { PasswordResetRequest } from './entities/password-reset-request.js';
|
||||||
import { UserPending } from './entities/user-pending.js';
|
import { UserPending } from './entities/user-pending.js';
|
||||||
import { InstanceRepository } from './repositories/instance.js';
|
import { InstanceRepository } from './repositories/instance.js';
|
||||||
import { Webhook } from './entities/webhook.js';
|
import { Webhook } from './entities/webhook.js';
|
||||||
|
import { UserIp } from './entities/user-ip.js';
|
||||||
|
|
||||||
export const Announcements = db.getRepository(Announcement);
|
export const Announcements = db.getRepository(Announcement);
|
||||||
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
||||||
|
@ -90,6 +91,7 @@ export const UserGroups = (UserGroupRepository);
|
||||||
export const UserGroupJoinings = db.getRepository(UserGroupJoining);
|
export const UserGroupJoinings = db.getRepository(UserGroupJoining);
|
||||||
export const UserGroupInvitations = (UserGroupInvitationRepository);
|
export const UserGroupInvitations = (UserGroupInvitationRepository);
|
||||||
export const UserNotePinings = db.getRepository(UserNotePining);
|
export const UserNotePinings = db.getRepository(UserNotePining);
|
||||||
|
export const UserIps = db.getRepository(UserIp);
|
||||||
export const UsedUsernames = db.getRepository(UsedUsername);
|
export const UsedUsernames = db.getRepository(UsedUsername);
|
||||||
export const Followings = (FollowingRepository);
|
export const Followings = (FollowingRepository);
|
||||||
export const FollowRequests = (FollowRequestRepository);
|
export const FollowRequests = (FollowRequestRepository);
|
||||||
|
|
|
@ -315,6 +315,7 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
} : undefined) : undefined,
|
} : undefined) : undefined,
|
||||||
emojis: populateEmojis(user.emojis, user.host),
|
emojis: populateEmojis(user.emojis, user.host),
|
||||||
onlineStatus: this.getOnlineStatus(user),
|
onlineStatus: this.getOnlineStatus(user),
|
||||||
|
driveCapacityOverrideMb: user.driveCapacityOverrideMb,
|
||||||
|
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
url: profile!.url,
|
url: profile!.url,
|
||||||
|
|
|
@ -2,6 +2,9 @@ import httpSignature from '@peertube/http-signature';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
|
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||||
|
import { IActivity } from '@/remote/activitypub/type.js';
|
||||||
|
import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js';
|
||||||
import { envOption } from '../env.js';
|
import { envOption } from '../env.js';
|
||||||
|
|
||||||
import processDeliver from './processors/deliver.js';
|
import processDeliver from './processors/deliver.js';
|
||||||
|
@ -12,18 +15,15 @@ import processSystemQueue from './processors/system/index.js';
|
||||||
import processWebhookDeliver from './processors/webhook-deliver.js';
|
import processWebhookDeliver from './processors/webhook-deliver.js';
|
||||||
import { endedPollNotification } from './processors/ended-poll-notification.js';
|
import { endedPollNotification } from './processors/ended-poll-notification.js';
|
||||||
import { queueLogger } from './logger.js';
|
import { queueLogger } from './logger.js';
|
||||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
|
||||||
import { getJobInfo } from './get-job-info.js';
|
import { getJobInfo } from './get-job-info.js';
|
||||||
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js';
|
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js';
|
||||||
import { ThinUser } from './types.js';
|
import { ThinUser } from './types.js';
|
||||||
import { IActivity } from '@/remote/activitypub/type.js';
|
|
||||||
import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js';
|
|
||||||
|
|
||||||
function renderError(e: Error): any {
|
function renderError(e: Error): any {
|
||||||
return {
|
return {
|
||||||
stack: e?.stack,
|
stack: e.stack,
|
||||||
message: e?.message,
|
message: e.message,
|
||||||
name: e?.name,
|
name: e.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,6 +314,12 @@ export default function() {
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
systemQueue.add('clean', {
|
||||||
|
}, {
|
||||||
|
repeat: { cron: '0 0 * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
|
|
||||||
systemQueue.add('checkExpiredMutings', {
|
systemQueue.add('checkExpiredMutings', {
|
||||||
}, {
|
}, {
|
||||||
repeat: { cron: '*/5 * * * *' },
|
repeat: { cron: '*/5 * * * *' },
|
||||||
|
|
18
packages/backend/src/queue/processors/system/clean.ts
Normal file
18
packages/backend/src/queue/processors/system/clean.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import Bull from 'bull';
|
||||||
|
import { LessThan } from 'typeorm';
|
||||||
|
import { UserIps } from '@/models/index.js';
|
||||||
|
|
||||||
|
import { queueLogger } from '../../logger.js';
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger('clean');
|
||||||
|
|
||||||
|
export async function clean(job: Bull.Job<Record<string, unknown>>, done: any): Promise<void> {
|
||||||
|
logger.info('Cleaning...');
|
||||||
|
|
||||||
|
UserIps.delete({
|
||||||
|
createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.succ('Cleaned.');
|
||||||
|
done();
|
||||||
|
}
|
|
@ -3,12 +3,14 @@ import { tickCharts } from './tick-charts.js';
|
||||||
import { resyncCharts } from './resync-charts.js';
|
import { resyncCharts } from './resync-charts.js';
|
||||||
import { cleanCharts } from './clean-charts.js';
|
import { cleanCharts } from './clean-charts.js';
|
||||||
import { checkExpiredMutings } from './check-expired-mutings.js';
|
import { checkExpiredMutings } from './check-expired-mutings.js';
|
||||||
|
import { clean } from './clean.js';
|
||||||
|
|
||||||
const jobs = {
|
const jobs = {
|
||||||
tickCharts,
|
tickCharts,
|
||||||
resyncCharts,
|
resyncCharts,
|
||||||
cleanCharts,
|
cleanCharts,
|
||||||
checkExpiredMutings,
|
checkExpiredMutings,
|
||||||
|
clean,
|
||||||
} as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>;
|
} as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>;
|
||||||
|
|
||||||
export default function(dbQueue: Bull.Queue<Record<string, unknown>>) {
|
export default function(dbQueue: Bull.Queue<Record<string, unknown>>) {
|
||||||
|
|
|
@ -1,10 +1,19 @@
|
||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
|
|
||||||
|
import { User } from '@/models/entities/user.js';
|
||||||
|
import { UserIps } from '@/models/index.js';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { IEndpoint } from './endpoints.js';
|
import { IEndpoint } from './endpoints.js';
|
||||||
import authenticate, { AuthenticationError } from './authenticate.js';
|
import authenticate, { AuthenticationError } from './authenticate.js';
|
||||||
import call from './call.js';
|
import call from './call.js';
|
||||||
import { ApiError } from './error.js';
|
import { ApiError } from './error.js';
|
||||||
|
|
||||||
|
const userIpHistories = new Map<User['id'], Set<string>>();
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
userIpHistories.clear();
|
||||||
|
}, 1000 * 60 * 60);
|
||||||
|
|
||||||
export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => {
|
export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => {
|
||||||
const body = ctx.is('multipart/form-data')
|
const body = ctx.is('multipart/form-data')
|
||||||
? (ctx.request as any).body
|
? (ctx.request as any).body
|
||||||
|
@ -44,6 +53,31 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res
|
||||||
}).catch((e: ApiError) => {
|
}).catch((e: ApiError) => {
|
||||||
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
|
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log IP
|
||||||
|
if (user) {
|
||||||
|
fetchMeta().then(meta => {
|
||||||
|
if (!meta.enableIpLogging) return;
|
||||||
|
const ip = ctx.ip;
|
||||||
|
const ips = userIpHistories.get(user.id);
|
||||||
|
if (ips == null || !ips.has(ip)) {
|
||||||
|
if (ips == null) {
|
||||||
|
userIpHistories.set(user.id, new Set([ip]));
|
||||||
|
} else {
|
||||||
|
ips.add(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
UserIps.insert({
|
||||||
|
createdAt: new Date(),
|
||||||
|
userId: user.id,
|
||||||
|
ip: ip,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
if (e instanceof AuthenticationError) {
|
if (e instanceof AuthenticationError) {
|
||||||
reply(403, new ApiError({
|
reply(403, new ApiError({
|
||||||
|
|
|
@ -116,7 +116,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
||||||
|
|
||||||
// API invoking
|
// API invoking
|
||||||
const before = performance.now();
|
const before = performance.now();
|
||||||
return await ep.exec(data, user, token, ctx?.file).catch((e: Error) => {
|
return await ep.exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers).catch((e: Error) => {
|
||||||
if (e instanceof ApiError) {
|
if (e instanceof ApiError) {
|
||||||
throw e;
|
throw e;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -51,26 +51,7 @@ export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: {
|
||||||
.select('muting.muteeId')
|
.select('muting.muteeId')
|
||||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile')
|
q.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
|
||||||
.select('user_profile.mutedInstances')
|
|
||||||
.where('user_profile.userId = :muterId', { muterId: me.id });
|
|
||||||
|
|
||||||
q
|
|
||||||
.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`)
|
|
||||||
// mute instances
|
|
||||||
.andWhere(new Brackets(qb => { qb
|
|
||||||
.andWhere('note.userHost IS NULL')
|
|
||||||
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
|
|
||||||
}))
|
|
||||||
.andWhere(new Brackets(qb => { qb
|
|
||||||
.where('note.replyUserHost IS NULL')
|
|
||||||
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
|
|
||||||
}))
|
|
||||||
.andWhere(new Brackets(qb => { qb
|
|
||||||
.where('note.renoteUserHost IS NULL')
|
|
||||||
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
|
|
||||||
}));
|
|
||||||
|
|
||||||
q.setParameters(mutingQuery.getParameters());
|
q.setParameters(mutingQuery.getParameters());
|
||||||
q.setParameters(mutingInstanceQuery.getParameters());
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import Ajv from 'ajv';
|
import Ajv from 'ajv';
|
||||||
import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js';
|
import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js';
|
||||||
import { IEndpointMeta } from './endpoints.js';
|
|
||||||
import { ApiError } from './error.js';
|
|
||||||
import { Schema, SchemaType } from '@/misc/schema.js';
|
import { Schema, SchemaType } from '@/misc/schema.js';
|
||||||
import { AccessToken } from '@/models/entities/access-token.js';
|
import { AccessToken } from '@/models/entities/access-token.js';
|
||||||
|
import { IEndpointMeta } from './endpoints.js';
|
||||||
|
import { ApiError } from './error.js';
|
||||||
|
|
||||||
export type Response = Record<string, any> | void;
|
export type Response = Record<string, any> | void;
|
||||||
|
|
||||||
// TODO: paramsの型をT['params']のスキーマ定義から推論する
|
// TODO: paramsの型をT['params']のスキーマ定義から推論する
|
||||||
type executor<T extends IEndpointMeta, Ps extends Schema> =
|
type executor<T extends IEndpointMeta, Ps extends Schema> =
|
||||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) =>
|
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
||||||
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
||||||
|
|
||||||
const ajv = new Ajv({
|
const ajv = new Ajv({
|
||||||
|
@ -20,23 +20,27 @@ const ajv = new Ajv({
|
||||||
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
||||||
|
|
||||||
export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>)
|
export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>)
|
||||||
: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
|
: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => Promise<any> {
|
||||||
const validate = ajv.compile(paramDef);
|
const validate = ajv.compile(paramDef);
|
||||||
|
|
||||||
return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => {
|
return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => {
|
||||||
function cleanup() {
|
let cleanup: undefined | (() => void) = undefined;
|
||||||
fs.unlink(file.path, () => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meta.requireFile && file == null) return Promise.reject(new ApiError({
|
if (meta.requireFile) {
|
||||||
|
cleanup = () => {
|
||||||
|
fs.unlink(file.path, () => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (file == null) return Promise.reject(new ApiError({
|
||||||
message: 'File required.',
|
message: 'File required.',
|
||||||
code: 'FILE_REQUIRED',
|
code: 'FILE_REQUIRED',
|
||||||
id: '4267801e-70d1-416a-b011-4ee502885d8b',
|
id: '4267801e-70d1-416a-b011-4ee502885d8b',
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const valid = validate(params);
|
const valid = validate(params);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
if (file) cleanup();
|
if (file) cleanup!();
|
||||||
|
|
||||||
const errors = validate.errors!;
|
const errors = validate.errors!;
|
||||||
const err = new ApiError({
|
const err = new ApiError({
|
||||||
|
@ -50,6 +54,6 @@ export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, pa
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(params as SchemaType<Ps>, user, token, file, cleanup);
|
return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/fed
|
||||||
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
|
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
|
||||||
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
|
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
|
||||||
|
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||||
import * as ep___admin_invite from './endpoints/admin/invite.js';
|
import * as ep___admin_invite from './endpoints/admin/invite.js';
|
||||||
import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js';
|
import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js';
|
||||||
import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js';
|
import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js';
|
||||||
|
@ -60,6 +61,7 @@ import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
||||||
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
||||||
import * as ep___admin_vacuum from './endpoints/admin/vacuum.js';
|
import * as ep___admin_vacuum from './endpoints/admin/vacuum.js';
|
||||||
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
|
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
|
||||||
|
import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
|
||||||
import * as ep___announcements from './endpoints/announcements.js';
|
import * as ep___announcements from './endpoints/announcements.js';
|
||||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||||
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
||||||
|
@ -311,6 +313,8 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by
|
||||||
import * as ep___users_search from './endpoints/users/search.js';
|
import * as ep___users_search from './endpoints/users/search.js';
|
||||||
import * as ep___users_show from './endpoints/users/show.js';
|
import * as ep___users_show from './endpoints/users/show.js';
|
||||||
import * as ep___users_stats from './endpoints/users/stats.js';
|
import * as ep___users_stats from './endpoints/users/stats.js';
|
||||||
|
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||||
|
import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js';
|
||||||
|
|
||||||
const eps = [
|
const eps = [
|
||||||
['admin/meta', ep___admin_meta],
|
['admin/meta', ep___admin_meta],
|
||||||
|
@ -348,6 +352,7 @@ const eps = [
|
||||||
['admin/federation/update-instance', ep___admin_federation_updateInstance],
|
['admin/federation/update-instance', ep___admin_federation_updateInstance],
|
||||||
['admin/get-index-stats', ep___admin_getIndexStats],
|
['admin/get-index-stats', ep___admin_getIndexStats],
|
||||||
['admin/get-table-stats', ep___admin_getTableStats],
|
['admin/get-table-stats', ep___admin_getTableStats],
|
||||||
|
['admin/get-user-ips', ep___admin_getUserIps],
|
||||||
['admin/invite', ep___admin_invite],
|
['admin/invite', ep___admin_invite],
|
||||||
['admin/moderators/add', ep___admin_moderators_add],
|
['admin/moderators/add', ep___admin_moderators_add],
|
||||||
['admin/moderators/remove', ep___admin_moderators_remove],
|
['admin/moderators/remove', ep___admin_moderators_remove],
|
||||||
|
@ -373,6 +378,7 @@ const eps = [
|
||||||
['admin/update-meta', ep___admin_updateMeta],
|
['admin/update-meta', ep___admin_updateMeta],
|
||||||
['admin/vacuum', ep___admin_vacuum],
|
['admin/vacuum', ep___admin_vacuum],
|
||||||
['admin/delete-account', ep___admin_deleteAccount],
|
['admin/delete-account', ep___admin_deleteAccount],
|
||||||
|
['admin/update-user-note', ep___admin_updateUserNote],
|
||||||
['announcements', ep___announcements],
|
['announcements', ep___announcements],
|
||||||
['antennas/create', ep___antennas_create],
|
['antennas/create', ep___antennas_create],
|
||||||
['antennas/delete', ep___antennas_delete],
|
['antennas/delete', ep___antennas_delete],
|
||||||
|
@ -624,6 +630,8 @@ const eps = [
|
||||||
['users/search', ep___users_search],
|
['users/search', ep___users_search],
|
||||||
['users/show', ep___users_show],
|
['users/show', ep___users_show],
|
||||||
['users/stats', ep___users_stats],
|
['users/stats', ep___users_stats],
|
||||||
|
['admin/drive-capacity-override', ep___admin_driveCapOverride],
|
||||||
|
['fetch-rss', ep___fetchRss],
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface IEndpointMeta {
|
export interface IEndpointMeta {
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import define from '../../define.js';
|
||||||
|
import { Users } from '@/models/index.js';
|
||||||
|
import { User } from '@/models/entities/user.js';
|
||||||
|
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
overrideMb: { type: 'number', nullable: true },
|
||||||
|
},
|
||||||
|
required: ['userId', 'overrideMb'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
|
const user = await Users.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new Error('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Users.isLocalUser(user)) {
|
||||||
|
throw new Error('user is not local user');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*if (user.isAdmin) {
|
||||||
|
throw new Error('cannot suspend admin');
|
||||||
|
}
|
||||||
|
if (user.isModerator) {
|
||||||
|
throw new Error('cannot suspend moderator');
|
||||||
|
}*/
|
||||||
|
|
||||||
|
await Users.update(user.id, {
|
||||||
|
driveCapacityOverrideMb: ps.overrideMb,
|
||||||
|
});
|
||||||
|
|
||||||
|
insertModerationLog(me, 'change-drive-capacity-override', {
|
||||||
|
targetId: user.id,
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { DriveFiles } from '@/models/index.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
import { DriveFiles } from '@/models/index.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -184,5 +184,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
throw new ApiError(meta.errors.noSuchFile);
|
throw new ApiError(meta.errors.noSuchFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!me.isAdmin) {
|
||||||
|
delete file.requestIp;
|
||||||
|
delete file.requestHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { UserIps } from '@/models/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireAdmin: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
|
const ips = await UserIps.find({
|
||||||
|
where: { userId: ps.userId },
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
take: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ips.map(x => ({
|
||||||
|
ip: x.ip,
|
||||||
|
createdAt: x.createdAt.toISOString(),
|
||||||
|
}));
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import define from '../../define.js';
|
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['meta'],
|
tags: ['meta'],
|
||||||
|
@ -304,6 +304,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
},
|
},
|
||||||
|
enableIpLogging: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -360,7 +364,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
pinnedPages: instance.pinnedPages,
|
pinnedPages: instance.pinnedPages,
|
||||||
pinnedClipId: instance.pinnedClipId,
|
pinnedClipId: instance.pinnedClipId,
|
||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
|
|
||||||
useStarForReactionFallback: instance.useStarForReactionFallback,
|
useStarForReactionFallback: instance.useStarForReactionFallback,
|
||||||
pinnedUsers: instance.pinnedUsers,
|
pinnedUsers: instance.pinnedUsers,
|
||||||
hiddenTags: instance.hiddenTags,
|
hiddenTags: instance.hiddenTags,
|
||||||
|
@ -397,5 +400,6 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
|
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
|
||||||
deeplAuthKey: instance.deeplAuthKey,
|
deeplAuthKey: instance.deeplAuthKey,
|
||||||
deeplIsPro: instance.deeplIsPro,
|
deeplIsPro: instance.deeplIsPro,
|
||||||
|
enableIpLogging: instance.enableIpLogging,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,7 +25,7 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
const [user, profile] = await Promise.all([
|
const [user, profile] = await Promise.all([
|
||||||
Users.findOneBy({ id: ps.userId }),
|
Users.findOneBy({ id: ps.userId }),
|
||||||
UserProfiles.findOneBy({ userId: ps.userId })
|
UserProfiles.findOneBy({ userId: ps.userId }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (user == null || profile == null) {
|
if (user == null || profile == null) {
|
||||||
|
@ -68,6 +68,8 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
isModerator: user.isModerator,
|
isModerator: user.isModerator,
|
||||||
isSilenced: user.isSilenced,
|
isSilenced: user.isSilenced,
|
||||||
isSuspended: user.isSuspended,
|
isSuspended: user.isSuspended,
|
||||||
|
lastActiveDate: user.lastActiveDate,
|
||||||
|
moderationNote: profile.moderationNote,
|
||||||
signins,
|
signins,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import define from '../../define.js';
|
|
||||||
import { Meta } from '@/models/entities/meta.js';
|
import { Meta } from '@/models/entities/meta.js';
|
||||||
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
||||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js';
|
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js';
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -96,6 +96,7 @@ export const paramDef = {
|
||||||
objectStorageUseProxy: { type: 'boolean' },
|
objectStorageUseProxy: { type: 'boolean' },
|
||||||
objectStorageSetPublicRead: { type: 'boolean' },
|
objectStorageSetPublicRead: { type: 'boolean' },
|
||||||
objectStorageS3ForcePathStyle: { type: 'boolean' },
|
objectStorageS3ForcePathStyle: { type: 'boolean' },
|
||||||
|
enableIpLogging: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -396,6 +397,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
set.deeplIsPro = ps.deeplIsPro;
|
set.deeplIsPro = ps.deeplIsPro;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.enableIpLogging !== undefined) {
|
||||||
|
set.enableIpLogging = ps.enableIpLogging;
|
||||||
|
}
|
||||||
|
|
||||||
await db.transaction(async transactionalEntityManager => {
|
await db.transaction(async transactionalEntityManager => {
|
||||||
const metas = await transactionalEntityManager.find(Meta, {
|
const metas = await transactionalEntityManager.find(Meta, {
|
||||||
order: {
|
order: {
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { UserProfiles, Users } from '@/models/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
text: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['userId', 'text'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
|
const user = await Users.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new Error('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserProfiles.update({ userId: user.id }, {
|
||||||
|
moderationNote: ps.text,
|
||||||
|
});
|
||||||
|
});
|
|
@ -39,7 +39,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
const usage = await DriveFiles.calcDriveUsageOf(user.id);
|
const usage = await DriveFiles.calcDriveUsageOf(user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
capacity: 1024 * 1024 * instance.localDriveCapacityMb,
|
capacity: 1024 * 1024 * (user.driveCapacityOverrideMb || instance.localDriveCapacityMb),
|
||||||
usage: usage,
|
usage: usage,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { addFile } from '@/services/drive/add-file.js';
|
import { addFile } from '@/services/drive/add-file.js';
|
||||||
|
import { DriveFiles } from '@/models/index.js';
|
||||||
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
import { apiLogger } from '../../../logger.js';
|
import { apiLogger } from '../../../logger.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
import { DriveFiles } from '@/models/index.js';
|
|
||||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['drive'],
|
tags: ['drive'],
|
||||||
|
@ -50,7 +51,7 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
|
export default define(meta, paramDef, async (ps, user, _, file, cleanup, ip, headers) => {
|
||||||
// Get 'name' parameter
|
// Get 'name' parameter
|
||||||
let name = ps.name || file.originalname;
|
let name = ps.name || file.originalname;
|
||||||
if (name !== undefined && name !== null) {
|
if (name !== undefined && name !== null) {
|
||||||
|
@ -66,9 +67,21 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
|
||||||
name = null;
|
name = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create file
|
// Create file
|
||||||
const driveFile = await addFile({ user, path: file.path, name, comment: ps.comment, folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive });
|
const driveFile = await addFile({
|
||||||
|
user,
|
||||||
|
path: file.path,
|
||||||
|
name,
|
||||||
|
comment: ps.comment,
|
||||||
|
folderId: ps.folderId,
|
||||||
|
force: ps.force,
|
||||||
|
sensitive: ps.isSensitive,
|
||||||
|
requestIp: meta.enableIpLogging ? ip : null,
|
||||||
|
requestHeaders: meta.enableIpLogging ? headers : null,
|
||||||
|
});
|
||||||
return await DriveFiles.pack(driveFile, { self: true });
|
return await DriveFiles.pack(driveFile, { self: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error || typeof e === 'string') {
|
if (e instanceof Error || typeof e === 'string') {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { uploadFromUrl } from '@/services/drive/upload-from-url.js';
|
import { uploadFromUrl } from '@/services/drive/upload-from-url.js';
|
||||||
import define from '../../../define.js';
|
|
||||||
import { DriveFiles } from '@/models/index.js';
|
import { DriveFiles } from '@/models/index.js';
|
||||||
import { publishMainStream } from '@/services/stream.js';
|
import { publishMainStream } from '@/services/stream.js';
|
||||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
|
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
|
||||||
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['drive'],
|
tags: ['drive'],
|
||||||
|
@ -34,8 +34,8 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => {
|
||||||
uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment }).then(file => {
|
uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => {
|
||||||
DriveFiles.pack(file, { self: true }).then(packedFile => {
|
DriveFiles.pack(file, { self: true }).then(packedFile => {
|
||||||
publishMainStream(user.id, 'urlUploadFinished', {
|
publishMainStream(user.id, 'urlUploadFinished', {
|
||||||
marker: ps.marker,
|
marker: ps.marker,
|
||||||
|
|
|
@ -15,6 +15,7 @@ export const meta = {
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -29,7 +30,7 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
order: {
|
order: {
|
||||||
followersCount: 'DESC',
|
followersCount: 'DESC',
|
||||||
},
|
},
|
||||||
take: 10,
|
take: ps.limit,
|
||||||
}),
|
}),
|
||||||
Instances.find({
|
Instances.find({
|
||||||
where: {
|
where: {
|
||||||
|
@ -38,7 +39,7 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
order: {
|
order: {
|
||||||
followingCount: 'DESC',
|
followingCount: 'DESC',
|
||||||
},
|
},
|
||||||
take: 10,
|
take: ps.limit,
|
||||||
}),
|
}),
|
||||||
Followings.count({
|
Followings.count({
|
||||||
where: {
|
where: {
|
||||||
|
@ -53,7 +54,7 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const gotSubCount = topSubInstances.map(x => x.followersCount).reduce((a, b) => a + b, 0);
|
const gotSubCount = topSubInstances.map(x => x.followersCount).reduce((a, b) => a + b, 0);
|
||||||
const gotPubCount = topSubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0);
|
const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
topSubInstances: Instances.packMany(topSubInstances),
|
topSubInstances: Instances.packMany(topSubInstances),
|
||||||
|
|
39
packages/backend/src/server/api/endpoints/fetch-rss.ts
Normal file
39
packages/backend/src/server/api/endpoints/fetch-rss.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import Parser from 'rss-parser';
|
||||||
|
import { getResponse } from '@/misc/fetch.js';
|
||||||
|
import config from '@/config/index.js';
|
||||||
|
import define from '../define.js';
|
||||||
|
|
||||||
|
const rssParser = new Parser();
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['meta'],
|
||||||
|
|
||||||
|
requireCredential: false,
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 3,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['url'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps) => {
|
||||||
|
const res = await getResponse({
|
||||||
|
url: ps.url,
|
||||||
|
method: 'GET',
|
||||||
|
headers: Object.assign({
|
||||||
|
'User-Agent': config.userAgent,
|
||||||
|
Accept: 'application/rss+xml, */*',
|
||||||
|
}),
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
|
||||||
|
return rssParser.parseString(text);
|
||||||
|
});
|
|
@ -60,12 +60,21 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
query.setParameters(mutingQuery.getParameters());
|
query.setParameters(mutingQuery.getParameters());
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const polls = await query.take(ps.limit).skip(ps.offset).getMany();
|
const polls = await query
|
||||||
|
.orderBy('poll.noteId', 'DESC')
|
||||||
|
.take(ps.limit)
|
||||||
|
.skip(ps.offset)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
if (polls.length === 0) return [];
|
if (polls.length === 0) return [];
|
||||||
|
|
||||||
const notes = await Notes.findBy({
|
const notes = await Notes.find({
|
||||||
|
where: {
|
||||||
id: In(polls.map(poll => poll.noteId)),
|
id: In(polls.map(poll => poll.noteId)),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
createdAt: 'DESC',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return await Notes.packMany(notes, user, {
|
return await Notes.packMany(notes, user, {
|
||||||
|
|
|
@ -27,6 +27,12 @@ export const paramDef = {
|
||||||
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
|
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
|
||||||
state: { type: 'string', enum: ['all', 'admin', 'moderator', 'adminOrModerator', 'alive'], default: 'all' },
|
state: { type: 'string', enum: ['all', 'admin', 'moderator', 'adminOrModerator', 'alive'], default: 'all' },
|
||||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
|
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
|
||||||
|
hostname: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
description: 'The local host is represented with `null`.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -48,6 +54,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
case 'remote': query.andWhere('user.host IS NOT NULL'); break;
|
case 'remote': query.andWhere('user.host IS NOT NULL'); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.hostname) {
|
||||||
|
query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() });
|
||||||
|
}
|
||||||
|
|
||||||
switch (ps.sort) {
|
switch (ps.sort) {
|
||||||
case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
|
case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
|
||||||
case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
|
case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default class extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (iUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
|
|
|
@ -2,26 +2,26 @@ import * as fs from 'node:fs';
|
||||||
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
import S3 from 'aws-sdk/clients/s3.js';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
import { publishMainStream, publishDriveStream } from '@/services/stream.js';
|
import { publishMainStream, publishDriveStream } from '@/services/stream.js';
|
||||||
import { deleteFile } from './delete-file.js';
|
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { GenerateVideoThumbnail } from './generate-video-thumbnail.js';
|
|
||||||
import { driveLogger } from './logger.js';
|
|
||||||
import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js';
|
|
||||||
import { contentDisposition } from '@/misc/content-disposition.js';
|
import { contentDisposition } from '@/misc/content-disposition.js';
|
||||||
import { getFileInfo } from '@/misc/get-file-info.js';
|
import { getFileInfo } from '@/misc/get-file-info.js';
|
||||||
import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '@/models/index.js';
|
import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '@/models/index.js';
|
||||||
import { InternalStorage } from './internal-storage.js';
|
|
||||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||||
import { IRemoteUser, User } from '@/models/entities/user.js';
|
import { IRemoteUser, User } from '@/models/entities/user.js';
|
||||||
import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index.js';
|
import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index.js';
|
||||||
import { genId } from '@/misc/gen-id.js';
|
import { genId } from '@/misc/gen-id.js';
|
||||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||||
import S3 from 'aws-sdk/clients/s3.js';
|
|
||||||
import { getS3 } from './s3.js';
|
|
||||||
import sharp from 'sharp';
|
|
||||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||||
import { IsNull } from 'typeorm';
|
import { getS3 } from './s3.js';
|
||||||
|
import { InternalStorage } from './internal-storage.js';
|
||||||
|
import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js';
|
||||||
|
import { driveLogger } from './logger.js';
|
||||||
|
import { GenerateVideoThumbnail } from './generate-video-thumbnail.js';
|
||||||
|
import { deleteFile } from './delete-file.js';
|
||||||
|
|
||||||
const logger = driveLogger.createSubLogger('register', 'yellow');
|
const logger = driveLogger.createSubLogger('register', 'yellow');
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'].includes(type)) {
|
if (!['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'].includes(type)) {
|
||||||
logger.debug(`web image and thumbnail not created (not an required file)`);
|
logger.debug('web image and thumbnail not created (not an required file)');
|
||||||
return {
|
return {
|
||||||
webpublic: null,
|
webpublic: null,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
|
@ -212,7 +212,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
|
||||||
let webpublic: IImage | null = null;
|
let webpublic: IImage | null = null;
|
||||||
|
|
||||||
if (generateWeb && !satisfyWebpublic) {
|
if (generateWeb && !satisfyWebpublic) {
|
||||||
logger.info(`creating web image`);
|
logger.info('creating web image');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (['image/jpeg', 'image/webp'].includes(type)) {
|
if (['image/jpeg', 'image/webp'].includes(type)) {
|
||||||
|
@ -222,14 +222,14 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
|
||||||
} else if (['image/svg+xml'].includes(type)) {
|
} else if (['image/svg+xml'].includes(type)) {
|
||||||
webpublic = await convertSharpToPng(img, 2048, 2048);
|
webpublic = await convertSharpToPng(img, 2048, 2048);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`web image not created (not an required image)`);
|
logger.debug('web image not created (not an required image)');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`web image not created (an error occured)`, err as Error);
|
logger.warn('web image not created (an error occured)', err as Error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (satisfyWebpublic) logger.info(`web image not created (original satisfies webpublic)`);
|
if (satisfyWebpublic) logger.info('web image not created (original satisfies webpublic)');
|
||||||
else logger.info(`web image not created (from remote)`);
|
else logger.info('web image not created (from remote)');
|
||||||
}
|
}
|
||||||
// #endregion webpublic
|
// #endregion webpublic
|
||||||
|
|
||||||
|
@ -240,10 +240,10 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
|
||||||
if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(type)) {
|
if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(type)) {
|
||||||
thumbnail = await convertSharpToWebp(img, 498, 280);
|
thumbnail = await convertSharpToWebp(img, 498, 280);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`thumbnail not created (not an required file)`);
|
logger.debug('thumbnail not created (not an required file)');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`thumbnail not created (an error occured)`, err as Error);
|
logger.warn('thumbnail not created (an error occured)', err as Error);
|
||||||
}
|
}
|
||||||
// #endregion thumbnail
|
// #endregion thumbnail
|
||||||
|
|
||||||
|
@ -276,7 +276,7 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string,
|
||||||
const s3 = getS3(meta);
|
const s3 = getS3(meta);
|
||||||
|
|
||||||
const upload = s3.upload(params, {
|
const upload = s3.upload(params, {
|
||||||
partSize: s3.endpoint?.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024,
|
partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await upload.promise();
|
const result = await upload.promise();
|
||||||
|
@ -307,7 +307,7 @@ async function deleteOldFile(user: IRemoteUser) {
|
||||||
|
|
||||||
type AddFileArgs = {
|
type AddFileArgs = {
|
||||||
/** User who wish to add file */
|
/** User who wish to add file */
|
||||||
user: { id: User['id']; host: User['host'] } | null;
|
user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null;
|
||||||
/** File path */
|
/** File path */
|
||||||
path: string;
|
path: string;
|
||||||
/** Name */
|
/** Name */
|
||||||
|
@ -326,6 +326,9 @@ type AddFileArgs = {
|
||||||
uri?: string | null;
|
uri?: string | null;
|
||||||
/** Mark file as sensitive */
|
/** Mark file as sensitive */
|
||||||
sensitive?: boolean | null;
|
sensitive?: boolean | null;
|
||||||
|
|
||||||
|
requestIp?: string | null;
|
||||||
|
requestHeaders?: Record<string, string> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -342,7 +345,9 @@ export async function addFile({
|
||||||
isLink = false,
|
isLink = false,
|
||||||
url = null,
|
url = null,
|
||||||
uri = null,
|
uri = null,
|
||||||
sensitive = null
|
sensitive = null,
|
||||||
|
requestIp = null,
|
||||||
|
requestHeaders = null,
|
||||||
}: AddFileArgs): Promise<DriveFile> {
|
}: AddFileArgs): Promise<DriveFile> {
|
||||||
const info = await getFileInfo(path);
|
const info = await getFileInfo(path);
|
||||||
logger.info(`${JSON.stringify(info)}`);
|
logger.info(`${JSON.stringify(info)}`);
|
||||||
|
@ -366,9 +371,16 @@ export async function addFile({
|
||||||
//#region Check drive usage
|
//#region Check drive usage
|
||||||
if (user && !isLink) {
|
if (user && !isLink) {
|
||||||
const usage = await DriveFiles.calcDriveUsageOf(user);
|
const usage = await DriveFiles.calcDriveUsageOf(user);
|
||||||
|
const u = await Users.findOneBy({ id: user.id });
|
||||||
|
|
||||||
const instance = await fetchMeta();
|
const instance = await fetchMeta();
|
||||||
const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
|
let driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
|
||||||
|
|
||||||
|
if (Users.isLocalUser(user) && u?.driveCapacityOverrideMb != null) {
|
||||||
|
driveCapacity = 1024 * 1024 * u.driveCapacityOverrideMb;
|
||||||
|
logger.debug('drive capacity override applied');
|
||||||
|
logger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
|
logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
|
||||||
|
|
||||||
|
@ -427,6 +439,8 @@ export async function addFile({
|
||||||
file.properties = properties;
|
file.properties = properties;
|
||||||
file.blurhash = info.blurhash || null;
|
file.blurhash = info.blurhash || null;
|
||||||
file.isLink = isLink;
|
file.isLink = isLink;
|
||||||
|
file.requestIp = requestIp;
|
||||||
|
file.requestHeaders = requestHeaders;
|
||||||
file.isSensitive = user
|
file.isSensitive = user
|
||||||
? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
|
? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
|
||||||
(sensitive !== null && sensitive !== undefined)
|
(sensitive !== null && sensitive !== undefined)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import { addFile } from './add-file.js';
|
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { driveLogger } from './logger.js';
|
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { downloadUrl } from '@/misc/download-url.js';
|
import { downloadUrl } from '@/misc/download-url.js';
|
||||||
import { DriveFolder } from '@/models/entities/drive-folder.js';
|
import { DriveFolder } from '@/models/entities/drive-folder.js';
|
||||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||||
import { DriveFiles } from '@/models/index.js';
|
import { DriveFiles } from '@/models/index.js';
|
||||||
|
import { driveLogger } from './logger.js';
|
||||||
|
import { addFile } from './add-file.js';
|
||||||
|
|
||||||
const logger = driveLogger.createSubLogger('downloader');
|
const logger = driveLogger.createSubLogger('downloader');
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ type Args = {
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
isLink?: boolean;
|
isLink?: boolean;
|
||||||
comment?: string | null;
|
comment?: string | null;
|
||||||
|
requestIp?: string | null;
|
||||||
|
requestHeaders?: Record<string, string> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function uploadFromUrl({
|
export async function uploadFromUrl({
|
||||||
|
@ -30,6 +32,8 @@ export async function uploadFromUrl({
|
||||||
force = false,
|
force = false,
|
||||||
isLink = false,
|
isLink = false,
|
||||||
comment = null,
|
comment = null,
|
||||||
|
requestIp = null,
|
||||||
|
requestHeaders = null,
|
||||||
}: Args): Promise<DriveFile> {
|
}: Args): Promise<DriveFile> {
|
||||||
let name = new URL(url).pathname.split('/').pop() || null;
|
let name = new URL(url).pathname.split('/').pop() || null;
|
||||||
if (name == null || !DriveFiles.validateFileName(name)) {
|
if (name == null || !DriveFiles.validateFileName(name)) {
|
||||||
|
@ -49,7 +53,7 @@ export async function uploadFromUrl({
|
||||||
// write content at URL to temp file
|
// write content at URL to temp file
|
||||||
await downloadUrl(url, path);
|
await downloadUrl(url, path);
|
||||||
|
|
||||||
const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive });
|
const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive, requestIp, requestHeaders });
|
||||||
logger.succ(`Got: ${driveFile.id}`);
|
logger.succ(`Got: ${driveFile.id}`);
|
||||||
return driveFile!;
|
return driveFile!;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -186,7 +186,7 @@ export function connectStream(user: any, channel: string, listener: (message: Re
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean) => {
|
export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: any) => {
|
||||||
return new Promise<boolean>(async (res, rej) => {
|
return new Promise<boolean>(async (res, rej) => {
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
@ -198,7 +198,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer);
|
||||||
res(true);
|
res(true);
|
||||||
}
|
}
|
||||||
});
|
}, params);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
rej(e);
|
rej(e);
|
||||||
}
|
}
|
||||||
|
@ -208,7 +208,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
ws.close();
|
ws.close();
|
||||||
res(false);
|
res(false);
|
||||||
}, 5000);
|
}, 3000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await trgr();
|
await trgr();
|
||||||
|
|
|
@ -22,9 +22,8 @@ module.exports = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
|
||||||
// data の禁止理由: 抽象的すぎるため
|
|
||||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||||
'id-denylist': ['error', 'window', 'data', 'e'],
|
'id-denylist': ['error', 'window', 'e'],
|
||||||
'no-shadow': ['warn'],
|
'no-shadow': ['warn'],
|
||||||
'vue/attributes-order': ['error', {
|
'vue/attributes-order': ['error', {
|
||||||
'alphabetical': false,
|
'alphabetical': false,
|
||||||
|
@ -69,6 +68,7 @@ module.exports = {
|
||||||
// Vue
|
// Vue
|
||||||
'$$': false,
|
'$$': false,
|
||||||
'$ref': false,
|
'$ref': false,
|
||||||
|
'$shallowRef': false,
|
||||||
'$computed': false,
|
'$computed': false,
|
||||||
|
|
||||||
// Misskey
|
// Misskey
|
||||||
|
|
21
packages/client/assets/tagcanvas.min.js
vendored
Normal file
21
packages/client/assets/tagcanvas.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -17,7 +17,7 @@
|
||||||
"@rollup/plugin-json": "4.1.0",
|
"@rollup/plugin-json": "4.1.0",
|
||||||
"@rollup/pluginutils": "^4.2.1",
|
"@rollup/pluginutils": "^4.2.1",
|
||||||
"@syuilo/aiscript": "0.11.1",
|
"@syuilo/aiscript": "0.11.1",
|
||||||
"@vitejs/plugin-vue": "2.3.3",
|
"@vitejs/plugin-vue": "3.0.0-beta.0",
|
||||||
"@vue/compiler-sfc": "3.2.37",
|
"@vue/compiler-sfc": "3.2.37",
|
||||||
"abort-controller": "3.0.0",
|
"abort-controller": "3.0.0",
|
||||||
"autobind-decorator": "2.4.0",
|
"autobind-decorator": "2.4.0",
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"eventemitter3": "4.0.7",
|
"eventemitter3": "4.0.7",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"idb-keyval": "6.1.0",
|
"idb-keyval": "6.2.0",
|
||||||
"insert-text-at-cursor": "0.3.0",
|
"insert-text-at-cursor": "0.3.0",
|
||||||
"json5": "2.2.1",
|
"json5": "2.2.1",
|
||||||
"katex": "0.15.6",
|
"katex": "0.15.6",
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
"mocha": "10.0.0",
|
"mocha": "10.0.0",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"photoswipe": "5.2.7",
|
"photoswipe": "5.2.8",
|
||||||
"prismjs": "1.28.0",
|
"prismjs": "1.28.0",
|
||||||
"private-ip": "2.3.3",
|
"private-ip": "2.3.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
|
@ -58,25 +58,25 @@
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rndstr": "1.0.0",
|
"rndstr": "1.0.0",
|
||||||
"rollup": "2.75.6",
|
"rollup": "2.75.7",
|
||||||
"s-age": "1.1.2",
|
"s-age": "1.1.2",
|
||||||
"sass": "1.52.3",
|
"sass": "1.53.0",
|
||||||
"seedrandom": "3.0.5",
|
"seedrandom": "3.0.5",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"syuilo-password-strength": "0.0.1",
|
"syuilo-password-strength": "0.0.1",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.141.0",
|
"three": "0.142.0",
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"tinycolor2": "1.4.2",
|
"tinycolor2": "1.4.2",
|
||||||
"tsc-alias": "1.6.9",
|
"tsc-alias": "1.6.11",
|
||||||
"tsconfig-paths": "4.0.0",
|
"tsconfig-paths": "4.0.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typescript": "4.7.3",
|
"typescript": "4.7.4",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"v-debounce": "0.1.2",
|
"v-debounce": "0.1.2",
|
||||||
"vanilla-tilt": "1.7.2",
|
"vanilla-tilt": "1.7.2",
|
||||||
"vite": "2.9.10",
|
"vite": "3.0.0-beta.6",
|
||||||
"vue": "3.2.37",
|
"vue": "3.2.37",
|
||||||
"vue-prism-editor": "2.0.0-alpha.2",
|
"vue-prism-editor": "2.0.0-alpha.2",
|
||||||
"vuedraggable": "4.0.1",
|
"vuedraggable": "4.0.1",
|
||||||
|
@ -102,13 +102,13 @@
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.3",
|
"@types/ws": "8.5.3",
|
||||||
"@typescript-eslint/eslint-plugin": "5.27.1",
|
"@typescript-eslint/eslint-plugin": "5.30.0",
|
||||||
"@typescript-eslint/parser": "5.27.1",
|
"@typescript-eslint/parser": "5.30.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "10.0.3",
|
"cypress": "10.3.0",
|
||||||
"eslint": "8.17.0",
|
"eslint": "8.18.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-vue": "9.1.0",
|
"eslint-plugin-vue": "9.1.1",
|
||||||
"start-server-and-test": "1.14.0"
|
"start-server-and-test": "1.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ const accountData = localStorage.getItem('account');
|
||||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||||
|
|
||||||
export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
|
export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
|
||||||
|
export const iAmAdmin = $i != null && $i.isAdmin;
|
||||||
|
|
||||||
export async function signout() {
|
export async function signout() {
|
||||||
waiting();
|
waiting();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
|
<XWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
|
||||||
<template #header>
|
<template #header>
|
||||||
<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i>
|
<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i>
|
||||||
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
|
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
|
||||||
|
@ -40,7 +40,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const window = ref<InstanceType<typeof XWindow>>();
|
const uiWindow = ref<InstanceType<typeof XWindow>>();
|
||||||
const comment = ref(props.initialComment || '');
|
const comment = ref(props.initialComment || '');
|
||||||
|
|
||||||
function send() {
|
function send() {
|
||||||
|
@ -52,7 +52,7 @@ function send() {
|
||||||
type: 'success',
|
type: 'success',
|
||||||
text: i18n.ts.abuseReported
|
text: i18n.ts.abuseReported
|
||||||
});
|
});
|
||||||
window.value?.close();
|
uiWindow.value?.close();
|
||||||
emit('closed');
|
emit('closed');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
|
<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
|
||||||
<span v-else-if="!defaultStore.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
|
<span v-else-if="!defaultStore.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
|
||||||
<span v-else class="emoji">{{ emoji.emoji }}</span>
|
<span v-else class="emoji">{{ emoji.emoji }}</span>
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
|
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
|
||||||
<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span>
|
<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -51,7 +51,7 @@ const variable = computed(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const loaded = computed(() => !!window[variable.value]);
|
const loaded = !!window[variable.value];
|
||||||
|
|
||||||
const src = computed(() => {
|
const src = computed(() => {
|
||||||
switch (props.provider) {
|
switch (props.provider) {
|
||||||
|
@ -62,7 +62,7 @@ const src = computed(() => {
|
||||||
|
|
||||||
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
|
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
|
||||||
|
|
||||||
if (loaded.value) {
|
if (loaded) {
|
||||||
available.value = true;
|
available.value = true;
|
||||||
} else {
|
} else {
|
||||||
(document.getElementById(props.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
|
(document.getElementById(props.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
|
||||||
|
@ -74,7 +74,7 @@ if (loaded.value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
if (captcha.value?.reset) captcha.value.reset();
|
if (captcha.value.reset) captcha.value.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestRender() {
|
function requestRender() {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<code v-if="inline" :class="`language-${prismLang}`" v-html="html"></code>
|
<code v-if="inline" :class="`language-${prismLang}`" v-html="html"></code>
|
||||||
<pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
|
<pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
|
||||||
|
@ -5,7 +6,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import 'prismjs';
|
import { Prism } from 'prismjs';
|
||||||
import 'prismjs/themes/prism-okaidia.css';
|
import 'prismjs/themes/prism-okaidia.css';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
|
@ -59,7 +59,7 @@ const isThumbnailAvailable = computed(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: clip;
|
overflow: hidden; overflow: clip;
|
||||||
|
|
||||||
> .icon-sub {
|
> .icon-sub {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -9,12 +9,12 @@
|
||||||
|
|
||||||
<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
|
<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
|
||||||
<div class="main _formRoot">
|
<div class="main _formRoot">
|
||||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
|
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
|
||||||
<template #label>{{ i18n.ts.username }}</template>
|
<template #label>{{ i18n.ts.username }}</template>
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
|
<MkInput v-model="email" class="_formBlock" type="email" :spellcheck="false" required>
|
||||||
<template #label>{{ i18n.ts.emailAddress }}</template>
|
<template #label>{{ i18n.ts.emailAddress }}</template>
|
||||||
<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
|
<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<XModalWindow ref="dialog"
|
<XModalWindow
|
||||||
|
ref="dialog"
|
||||||
:width="450"
|
:width="450"
|
||||||
:can-close="false"
|
:can-close="false"
|
||||||
:with-ok-button="true"
|
:with-ok-button="true"
|
||||||
|
@ -37,10 +38,10 @@
|
||||||
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
|
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||||
</FormSelect>
|
</FormSelect>
|
||||||
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock">
|
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock">
|
||||||
<template #caption><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||||
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
|
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||||
</FormRadios>
|
</FormRadios>
|
||||||
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
|
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||||
</FormRange>
|
</FormRange>
|
||||||
|
@ -55,7 +56,6 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
|
||||||
import FormInput from './form/input.vue';
|
import FormInput from './form/input.vue';
|
||||||
import FormTextarea from './form/textarea.vue';
|
import FormTextarea from './form/textarea.vue';
|
||||||
import FormSwitch from './form/switch.vue';
|
import FormSwitch from './form/switch.vue';
|
||||||
|
@ -63,6 +63,7 @@ import FormSelect from './form/select.vue';
|
||||||
import FormRange from './form/range.vue';
|
import FormRange from './form/range.vue';
|
||||||
import MkButton from './ui/button.vue';
|
import MkButton from './ui/button.vue';
|
||||||
import FormRadios from './form/radios.vue';
|
import FormRadios from './form/radios.vue';
|
||||||
|
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
@ -91,31 +92,31 @@ export default defineComponent({
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
values: {}
|
values: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
for (const item in this.form) {
|
for (const item in this.form) {
|
||||||
this.values[item] = this.form[item].hasOwnProperty('default') ? this.form[item].default : null;
|
this.values[item] = this.form[item].default ?? null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
ok() {
|
ok() {
|
||||||
this.$emit('done', {
|
this.$emit('done', {
|
||||||
result: this.values
|
result: this.values,
|
||||||
});
|
});
|
||||||
this.$refs.dialog.close();
|
this.$refs.dialog.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('done', {
|
this.$emit('done', {
|
||||||
canceled: true
|
canceled: true,
|
||||||
});
|
});
|
||||||
this.$refs.dialog.close();
|
this.$refs.dialog.close();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
<template>
|
|
||||||
<div v-sticky-container class="adfeebaf _formBlock">
|
|
||||||
<div class="label"><slot name="label"></slot></div>
|
|
||||||
<div class="main _formRoot">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.adfeebaf {
|
|
||||||
padding: 24px 24px;
|
|
||||||
border: solid 1px var(--divider);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
|
|
||||||
> .label {
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 0 0 16px 0;
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .main {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -4,65 +4,44 @@
|
||||||
<div v-adaptive-border class="body">
|
<div v-adaptive-border class="body">
|
||||||
<div ref="containerEl" class="container">
|
<div ref="containerEl" class="container">
|
||||||
<div class="track">
|
<div class="track">
|
||||||
<div class="highlight" :style="{ width: (steppedValue * 100) + '%' }"></div>
|
<div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="steps" class="ticks">
|
<div v-if="steps && showTicks" class="ticks">
|
||||||
<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
|
<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
|
<div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="caption"><slot name="caption"></slot></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = withDefaults(defineProps<{
|
||||||
props: {
|
modelValue: number;
|
||||||
modelValue: {
|
disabled?: boolean;
|
||||||
type: Number,
|
min: number;
|
||||||
required: false,
|
max: number;
|
||||||
default: 0,
|
step?: number;
|
||||||
},
|
textConverter?: (value: number) => string,
|
||||||
disabled: {
|
showTicks?: boolean;
|
||||||
type: Boolean,
|
}>(), {
|
||||||
required: false,
|
step: 1,
|
||||||
default: false,
|
textConverter: (v) => v.toString(),
|
||||||
},
|
});
|
||||||
min: {
|
|
||||||
type: Number,
|
const emit = defineEmits<{
|
||||||
required: false,
|
(ev: 'update:modelValue', value: number): void;
|
||||||
default: 0,
|
}>();
|
||||||
},
|
|
||||||
max: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: 100,
|
|
||||||
},
|
|
||||||
step: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
autofocus: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
textConverter: {
|
|
||||||
type: Function,
|
|
||||||
required: false,
|
|
||||||
default: (v) => v.toString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props, context) {
|
|
||||||
const containerEl = ref<HTMLElement>();
|
const containerEl = ref<HTMLElement>();
|
||||||
const thumbEl = ref<HTMLElement>();
|
const thumbEl = ref<HTMLElement>();
|
||||||
|
|
||||||
const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
|
const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
|
||||||
const steppedValue = computed(() => {
|
const steppedRawValue = computed(() => {
|
||||||
if (props.step) {
|
if (props.step) {
|
||||||
const step = props.step / (props.max - props.min);
|
const step = props.step / (props.max - props.min);
|
||||||
return (step * Math.round(rawValue.value / step));
|
return (step * Math.round(rawValue.value / step));
|
||||||
|
@ -71,10 +50,11 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const finalValue = computed(() => {
|
const finalValue = computed(() => {
|
||||||
return (steppedValue.value * (props.max - props.min)) + props.min;
|
if (Number.isInteger(props.step)) {
|
||||||
});
|
return Math.round((steppedRawValue.value * (props.max - props.min)) + props.min);
|
||||||
watch(finalValue, () => {
|
} else {
|
||||||
context.emit('update:modelValue', finalValue.value);
|
return (steppedRawValue.value * (props.max - props.min)) + props.min;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const thumbWidth = computed(() => {
|
const thumbWidth = computed(() => {
|
||||||
|
@ -86,10 +66,10 @@ export default defineComponent({
|
||||||
if (containerEl.value == null) {
|
if (containerEl.value == null) {
|
||||||
thumbPosition.value = 0;
|
thumbPosition.value = 0;
|
||||||
} else {
|
} else {
|
||||||
thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedValue.value;
|
thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedRawValue.value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
watch([steppedValue, containerEl], calcThumbPosition);
|
watch([steppedRawValue, containerEl], calcThumbPosition);
|
||||||
|
|
||||||
let ro: ResizeObserver | undefined;
|
let ro: ResizeObserver | undefined;
|
||||||
|
|
||||||
|
@ -136,6 +116,8 @@ export default defineComponent({
|
||||||
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value)));
|
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let beforeValue = finalValue.value;
|
||||||
|
|
||||||
const onMouseup = () => {
|
const onMouseup = () => {
|
||||||
document.head.removeChild(style);
|
document.head.removeChild(style);
|
||||||
tooltipShowing.value = false;
|
tooltipShowing.value = false;
|
||||||
|
@ -143,6 +125,11 @@ export default defineComponent({
|
||||||
window.removeEventListener('touchmove', onDrag);
|
window.removeEventListener('touchmove', onDrag);
|
||||||
window.removeEventListener('mouseup', onMouseup);
|
window.removeEventListener('mouseup', onMouseup);
|
||||||
window.removeEventListener('touchend', onMouseup);
|
window.removeEventListener('touchend', onMouseup);
|
||||||
|
|
||||||
|
// 値が変わってたら通知
|
||||||
|
if (beforeValue !== finalValue.value) {
|
||||||
|
emit('update:modelValue', finalValue.value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('mousemove', onDrag);
|
window.addEventListener('mousemove', onDrag);
|
||||||
|
@ -150,19 +137,6 @@ export default defineComponent({
|
||||||
window.addEventListener('mouseup', onMouseup, { once: true });
|
window.addEventListener('mouseup', onMouseup, { once: true });
|
||||||
window.addEventListener('touchend', onMouseup, { once: true });
|
window.addEventListener('touchend', onMouseup, { once: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
|
||||||
rawValue,
|
|
||||||
finalValue,
|
|
||||||
steppedValue,
|
|
||||||
onMousedown,
|
|
||||||
containerEl,
|
|
||||||
thumbEl,
|
|
||||||
thumbPosition,
|
|
||||||
steps,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -215,7 +189,7 @@ export default defineComponent({
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
overflow: clip;
|
overflow: hidden; overflow: clip;
|
||||||
|
|
||||||
> .highlight {
|
> .highlight {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -214,6 +214,7 @@ const onClick = (ev: MouseEvent) => {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.1s ease-out;
|
transition: border-color 0.1s ease-out;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .prefix,
|
> .prefix,
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
minWidth: number;
|
minWidth?: number;
|
||||||
}>(), {
|
}>(), {
|
||||||
minWidth: 210,
|
minWidth: 210,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<div v-if="block" v-html="compiledFormula"></div>
|
<div v-if="block" v-html="compiledFormula"></div>
|
||||||
<span v-else v-html="compiledFormula"></span>
|
<span v-else v-html="compiledFormula"></span>
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, defineAsyncComponent } from 'vue';import * as os from '@/os';
|
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue';
|
import { computed, onMounted, onUnmounted, ref, inject, watch, shallowReactive, nextTick, reactive } from 'vue';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { popupMenu } from '@/os';
|
import { popupMenu } from '@/os';
|
||||||
import { scrollToTop } from '@/scripts/scroll';
|
import { scrollToTop } from '@/scripts/scroll';
|
||||||
|
@ -137,7 +137,8 @@ onMounted(() => {
|
||||||
calcBg();
|
calcBg();
|
||||||
globalEvents.on('themeChanged', calcBg);
|
globalEvents.on('themeChanged', calcBg);
|
||||||
|
|
||||||
watch(() => props.tab, () => {
|
watch(() => [props.tab, props.tabs], () => {
|
||||||
|
nextTick(() => {
|
||||||
const tabEl = tabRefs[props.tab];
|
const tabEl = tabRefs[props.tab];
|
||||||
if (tabEl && tabHighlightEl) {
|
if (tabEl && tabHighlightEl) {
|
||||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||||
|
@ -147,6 +148,7 @@ onMounted(() => {
|
||||||
tabHighlightEl.style.width = rect.width + 'px';
|
tabHighlightEl.style.width = rect.width + 'px';
|
||||||
tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
|
tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
@ -170,11 +172,8 @@ onUnmounted(() => {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.fdidabkb {
|
.fdidabkb {
|
||||||
--height: 60px;
|
--height: 55px;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: sticky;
|
|
||||||
top: var(--stickyTop, 0);
|
|
||||||
z-index: 1000;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<KeepAlive max="5">
|
<KeepAlive :max="defaultStore.state.numberOfPageCache">
|
||||||
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
</template>
|
</template>
|
||||||
|
@ -7,21 +7,19 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, nextTick, onMounted, onUnmounted, watch } from 'vue';
|
import { inject, nextTick, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import { Router } from '@/nirax';
|
import { Router } from '@/nirax';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
router?: Router;
|
router?: Router;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const router = props.router ?? inject('router');
|
const router = props.router ?? inject('router');
|
||||||
|
|
||||||
if (router == null) {
|
if (router == null) {
|
||||||
throw new Error('no router provided');
|
throw new Error('no router provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentPageComponent = $ref(router.getCurrentComponent());
|
let currentPageComponent = $shallowRef(router.getCurrentComponent());
|
||||||
let currentPageProps = $ref(router.getCurrentProps());
|
let currentPageProps = $ref(router.getCurrentProps());
|
||||||
let key = $ref(router.getCurrentKey());
|
let key = $ref(router.getCurrentKey());
|
||||||
|
|
||||||
|
|
|
@ -1,46 +1,38 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="rootEl">
|
<div ref="rootEl">
|
||||||
|
<div ref="headerEl">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
|
</div>
|
||||||
<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
|
<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts">
|
||||||
import { onMounted, onUnmounted } from 'vue';
|
// なんか動かない
|
||||||
|
//const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP');
|
||||||
|
const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
|
||||||
|
</script>
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
<script lang="ts" setup>
|
||||||
autoSticky?: boolean;
|
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
|
||||||
}>(), {
|
|
||||||
autoSticky: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const rootEl = $ref<HTMLElement>();
|
const rootEl = $ref<HTMLElement>();
|
||||||
|
const headerEl = $ref<HTMLElement>();
|
||||||
const bodyEl = $ref<HTMLElement>();
|
const bodyEl = $ref<HTMLElement>();
|
||||||
|
|
||||||
let headerHeight = $ref<string | undefined>();
|
let headerHeight = $ref<string | undefined>();
|
||||||
|
let childStickyTop = $ref(0);
|
||||||
|
const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0));
|
||||||
|
provide(CURRENT_STICKY_TOP, $$(childStickyTop));
|
||||||
|
|
||||||
const calc = () => {
|
const calc = () => {
|
||||||
const currentStickyTop = getComputedStyle(rootEl).getPropertyValue('--stickyTop') || '0px';
|
childStickyTop = parentStickyTop.value + headerEl.offsetHeight;
|
||||||
|
headerHeight = headerEl.offsetHeight.toString();
|
||||||
const header = rootEl.children[0] as HTMLElement;
|
|
||||||
if (header === bodyEl) {
|
|
||||||
bodyEl.style.setProperty('--stickyTop', currentStickyTop);
|
|
||||||
} else {
|
|
||||||
bodyEl.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`);
|
|
||||||
headerHeight = header.offsetHeight.toString();
|
|
||||||
|
|
||||||
if (props.autoSticky) {
|
|
||||||
header.style.setProperty('--stickyTop', currentStickyTop);
|
|
||||||
header.style.position = 'sticky';
|
|
||||||
header.style.top = 'var(--stickyTop)';
|
|
||||||
header.style.zIndex = '1';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
calc();
|
calc();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
@ -49,11 +41,19 @@ const observer = new MutationObserver(() => {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
calc();
|
calc();
|
||||||
|
|
||||||
observer.observe(rootEl, {
|
watch(parentStickyTop, calc);
|
||||||
attributes: false,
|
|
||||||
childList: true,
|
watch($$(childStickyTop), () => {
|
||||||
subtree: false,
|
bodyEl.style.setProperty('--stickyTop', `${childStickyTop}px`);
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
headerEl.style.position = 'sticky';
|
||||||
|
headerEl.style.top = 'var(--stickyTop, 0)';
|
||||||
|
headerEl.style.zIndex = '1000';
|
||||||
|
|
||||||
|
observer.observe(headerEl);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="zbcjwnqg">
|
<div class="zbcjwnqg">
|
||||||
|
<div class="main">
|
||||||
|
<div class="body">
|
||||||
<div class="selects" style="display: flex;">
|
<div class="selects" style="display: flex;">
|
||||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||||
<optgroup :label="$ts.federation">
|
<optgroup :label="$ts.federation">
|
||||||
|
@ -31,51 +33,187 @@
|
||||||
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="subpub">
|
||||||
|
<div class="sub">
|
||||||
|
<div class="title">Sub</div>
|
||||||
|
<canvas ref="subDoughnutEl"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="pub">
|
||||||
|
<div class="title">Pub</div>
|
||||||
|
<canvas ref="pubDoughnutEl"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, ref } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
|
import {
|
||||||
|
Chart,
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
TimeScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle,
|
||||||
|
Filler,
|
||||||
|
DoughnutController,
|
||||||
|
} from 'chart.js';
|
||||||
import MkSelect from '@/components/form/select.vue';
|
import MkSelect from '@/components/form/select.vue';
|
||||||
import MkChart from '@/components/chart.vue';
|
import MkChart from '@/components/chart.vue';
|
||||||
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
export default defineComponent({
|
Chart.register(
|
||||||
components: {
|
ArcElement,
|
||||||
MkSelect,
|
LineElement,
|
||||||
MkChart,
|
BarElement,
|
||||||
},
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
DoughnutController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
TimeScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle,
|
||||||
|
Filler,
|
||||||
|
);
|
||||||
|
|
||||||
props: {
|
const props = withDefaults(defineProps<{
|
||||||
chartLimit: {
|
chartLimit?: number;
|
||||||
type: Number,
|
detailed?: boolean;
|
||||||
required: false,
|
}>(), {
|
||||||
default: 90
|
chartLimit: 90,
|
||||||
},
|
});
|
||||||
detailed: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
const chartSpan = $ref<'hour' | 'day'>('hour');
|
||||||
const chartSpan = ref<'hour' | 'day'>('hour');
|
const chartSrc = $ref('active-users');
|
||||||
const chartSrc = ref('active-users');
|
let subDoughnutEl = $ref<HTMLCanvasElement>();
|
||||||
|
let pubDoughnutEl = $ref<HTMLCanvasElement>();
|
||||||
|
|
||||||
return {
|
const { handler: externalTooltipHandler1 } = useChartTooltip();
|
||||||
chartSrc,
|
const { handler: externalTooltipHandler2 } = useChartTooltip();
|
||||||
chartSpan,
|
|
||||||
};
|
function createDoughnut(chartEl, tooltip, data) {
|
||||||
|
const chartInstance = new Chart(chartEl, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: data.map(x => x.name),
|
||||||
|
datasets: [{
|
||||||
|
backgroundColor: data.map(x => x.color),
|
||||||
|
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
|
||||||
|
borderWidth: 2,
|
||||||
|
hoverOffset: 0,
|
||||||
|
data: data.map(x => x.value),
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 16,
|
||||||
|
bottom: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onClick: (ev) => {
|
||||||
|
const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
|
||||||
|
if (hit && data[hit.index].onClick) {
|
||||||
|
data[hit.index].onClick();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
mode: 'index',
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
external: tooltip,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return chartInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
os.apiGet('federation/stats', { limit: 15 }).then(fedStats => {
|
||||||
|
createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
|
||||||
|
name: x.host,
|
||||||
|
color: x.themeColor,
|
||||||
|
value: x.followersCount,
|
||||||
|
onClick: () => {
|
||||||
|
os.pageWindow(`/instance-info/${x.host}`);
|
||||||
|
},
|
||||||
|
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
|
||||||
|
|
||||||
|
createDoughnut(pubDoughnutEl, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
|
||||||
|
name: x.host,
|
||||||
|
color: x.themeColor,
|
||||||
|
value: x.followingCount,
|
||||||
|
onClick: () => {
|
||||||
|
os.pageWindow(`/instance-info/${x.host}`);
|
||||||
|
},
|
||||||
|
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }]));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.zbcjwnqg {
|
.zbcjwnqg {
|
||||||
> .selects {
|
> .main {
|
||||||
}
|
background: var(--panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
> .body {
|
||||||
> .chart {
|
> .chart {
|
||||||
padding: 8px 0 0 0;
|
padding: 8px 0 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .subpub {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
> .sub, > .pub {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
background: var(--panel);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 24px;
|
||||||
|
max-height: 300px;
|
||||||
|
|
||||||
|
> .title {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
left: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -16,8 +16,8 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
copy: string | null;
|
copy?: string | null;
|
||||||
oneline: boolean;
|
oneline?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
copy: null,
|
copy: null,
|
||||||
oneline: false,
|
oneline: false,
|
||||||
|
|
|
@ -16,13 +16,13 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub">
|
<div class="sub">
|
||||||
<a v-click-anime href="https://misskey-hub.net/help.html" target="_blank" @click.passive="close()">
|
<button v-click-anime class="_button" @click="help">
|
||||||
<i class="fas fa-question-circle icon"></i>
|
<i class="fas fa-question-circle icon"></i>
|
||||||
<div class="text">{{ $ts.help }}</div>
|
<div class="text">{{ $ts.help }}</div>
|
||||||
</a>
|
</button>
|
||||||
<MkA v-click-anime to="/about" @click.passive="close()">
|
<MkA v-click-anime to="/about" @click.passive="close()">
|
||||||
<i class="fas fa-info-circle icon"></i>
|
<i class="fas fa-info-circle icon"></i>
|
||||||
<div class="text">{{ $t('aboutX', { x: instanceName }) }}</div>
|
<div class="text">{{ $ts.instanceInfo }}</div>
|
||||||
</MkA>
|
</MkA>
|
||||||
<MkA v-click-anime to="/about-misskey" @click.passive="close()">
|
<MkA v-click-anime to="/about-misskey" @click.passive="close()">
|
||||||
<img src="/static-assets/favicon.png" class="icon"/>
|
<img src="/static-assets/favicon.png" class="icon"/>
|
||||||
|
@ -41,6 +41,7 @@ import { instanceName } from '@/config';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { deviceKind } from '@/scripts/device-kind';
|
import { deviceKind } from '@/scripts/device-kind';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
src?: HTMLElement;
|
src?: HTMLElement;
|
||||||
|
@ -73,6 +74,28 @@ const items = Object.keys(menuDef).filter(k => !menu.includes(k)).map(k => menuD
|
||||||
function close() {
|
function close() {
|
||||||
modal.close();
|
modal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function help(ev: MouseEvent) {
|
||||||
|
os.popupMenu([{
|
||||||
|
type: 'link',
|
||||||
|
to: '/mfm-cheat-sheet',
|
||||||
|
text: i18n.ts._mfm.cheatSheet,
|
||||||
|
icon: 'fas fa-code',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
to: '/scratchpad',
|
||||||
|
text: i18n.ts.scratchpad,
|
||||||
|
icon: 'fas fa-terminal',
|
||||||
|
}, null, {
|
||||||
|
text: i18n.ts.document,
|
||||||
|
icon: 'fas fa-question-circle',
|
||||||
|
action: () => {
|
||||||
|
window.open('https://misskey-hub.net/help.html', '_blank');
|
||||||
|
},
|
||||||
|
}], ev.currentTarget ?? ev.target);
|
||||||
|
|
||||||
|
close();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
99
packages/client/src/components/marquee.vue
Normal file
99
packages/client/src/components/marquee.vue
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { h, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MarqueeText',
|
||||||
|
props: {
|
||||||
|
duration: {
|
||||||
|
type: Number,
|
||||||
|
default: 15,
|
||||||
|
},
|
||||||
|
repeat: {
|
||||||
|
type: Number,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
paused: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
reverse: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const contentEl = ref();
|
||||||
|
|
||||||
|
function calc() {
|
||||||
|
const eachLength = contentEl.value.offsetWidth / props.repeat;
|
||||||
|
const factor = 3000;
|
||||||
|
const duration = props.duration / ((1 / eachLength) * factor);
|
||||||
|
|
||||||
|
contentEl.value.style.animationDuration = `${duration}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.duration, calc);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
calc();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentEl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render({
|
||||||
|
$slots, $style, $props: {
|
||||||
|
duration, repeat, paused, reverse,
|
||||||
|
},
|
||||||
|
}) {
|
||||||
|
return h('div', { class: [$style.wrap] }, [
|
||||||
|
h('span', {
|
||||||
|
ref: 'contentEl',
|
||||||
|
class: [
|
||||||
|
paused
|
||||||
|
? $style.paused
|
||||||
|
: undefined,
|
||||||
|
$style.content,
|
||||||
|
],
|
||||||
|
}, Array(repeat).fill(
|
||||||
|
h('span', {
|
||||||
|
class: $style.text,
|
||||||
|
style: {
|
||||||
|
animationDirection: reverse
|
||||||
|
? 'reverse'
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
}, $slots.default()),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.wrap {
|
||||||
|
overflow: hidden; overflow: clip;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
display: inline-block;
|
||||||
|
animation-name: marquee;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-duration: inherit;
|
||||||
|
}
|
||||||
|
.paused .text {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
@keyframes marquee {
|
||||||
|
0% { transform:translateX(0); }
|
||||||
|
100% { transform:translateX(-100%); }
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -143,7 +143,6 @@ function onContextmenu(ev: MouseEvent) {
|
||||||
background: var(--windowHeader);
|
background: var(--windowHeader);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
box-shadow: 0px 1px var(--divider);
|
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
height: $height;
|
height: $height;
|
||||||
|
|
|
@ -27,7 +27,7 @@ const props = defineProps<{
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: clip;
|
overflow: hidden; overflow: clip;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
|
|
||||||
&.min-width_350px {
|
&.min-width_350px {
|
||||||
|
|
|
@ -36,7 +36,7 @@ const showContent = $ref(false);
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: clip;
|
overflow: hidden; overflow: clip;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
|
|
||||||
&.min-width_350px {
|
&.min-width_350px {
|
||||||
|
|
|
@ -297,7 +297,7 @@ function readPromo() {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: box-shadow 0.1s ease;
|
transition: box-shadow 0.1s ease;
|
||||||
font-size: 1.05em;
|
font-size: 1.05em;
|
||||||
overflow: clip;
|
overflow: hidden; overflow: clip;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
|
// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
|
||||||
|
|
|
@ -1,31 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="igpposuu _monospace">
|
<div class="igpposuu _monospace">
|
||||||
<div v-if="value === null" class="null">null</div>
|
<div v-if="value === null" class="null">null</div>
|
||||||
<div v-else-if="typeof value === 'boolean'" class="boolean">{{ value ? 'true' : 'false' }}</div>
|
<div v-else-if="typeof value === 'boolean'" class="boolean" :class="{ true: value, false: !value }">{{ value ? 'true' : 'false' }}</div>
|
||||||
<div v-else-if="typeof value === 'string'" class="string">"{{ value }}"</div>
|
<div v-else-if="typeof value === 'string'" class="string">"{{ value }}"</div>
|
||||||
<div v-else-if="typeof value === 'number'" class="number">{{ number(value) }}</div>
|
<div v-else-if="typeof value === 'number'" class="number">{{ number(value) }}</div>
|
||||||
<div v-else-if="Array.isArray(value)" class="array">
|
<div v-else-if="isArray(value) && isEmpty(value)" class="array empty">[]</div>
|
||||||
<button @click="collapsed_ = !collapsed_">[ {{ collapsed_ ? '+' : '-' }} ]</button>
|
<div v-else-if="isArray(value)" class="array">
|
||||||
<template v-if="!collapsed_">
|
|
||||||
<div v-for="i in value.length" class="element">
|
<div v-for="i in value.length" class="element">
|
||||||
{{ i }}: <XValue :value="value[i - 1]" collapsed/>
|
{{ i }}: <XValue :value="value[i - 1]" collapsed/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="typeof value === 'object'" class="object">
|
<div v-else-if="isObject(value) && isEmpty(value)" class="object empty">{}</div>
|
||||||
<button @click="collapsed_ = !collapsed_">{ {{ collapsed_ ? '+' : '-' }} }</button>
|
<div v-else-if="isObject(value)" class="object">
|
||||||
<template v-if="!collapsed_">
|
|
||||||
<div v-for="k in Object.keys(value)" class="kv">
|
<div v-for="k in Object.keys(value)" class="kv">
|
||||||
|
<button class="toggle _button" :class="{ visible: collapsable(value[k]) }" @click="collapsed[k] = !collapsed[k]">{{ collapsed[k] ? '+' : '-' }}</button>
|
||||||
<div class="k">{{ k }}:</div>
|
<div class="k">{{ k }}:</div>
|
||||||
<div class="v"><XValue :value="value[k]" collapsed/></div>
|
<div v-if="collapsed[k]" class="v">
|
||||||
|
<button class="_button" @click="collapsed[k] = !collapsed[k]">
|
||||||
|
<template v-if="typeof value[k] === 'string'">"..."</template>
|
||||||
|
<template v-else-if="isArray(value[k])">[...]</template>
|
||||||
|
<template v-else-if="isObject(value[k])">{...}</template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="v"><XValue :value="value[k]"/></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, ref } from 'vue';
|
import { computed, defineComponent, reactive, ref } from 'vue';
|
||||||
import number from '@/filters/number';
|
import number from '@/filters/number';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
@ -33,24 +37,44 @@ export default defineComponent({
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: Object,
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
collapsed: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const collapsed_ = ref(props.collapsed);
|
const collapsed = reactive({});
|
||||||
|
|
||||||
|
if (isObject(props.value)) {
|
||||||
|
for (const key in props.value) {
|
||||||
|
collapsed[key] = collapsable(props.value[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isObject(v): boolean {
|
||||||
|
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isArray(v): boolean {
|
||||||
|
return Array.isArray(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEmpty(v): boolean {
|
||||||
|
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapsable(v): boolean {
|
||||||
|
return (isObject(v) || isArray(v)) && !isEmpty(v);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
number,
|
number,
|
||||||
collapsed_,
|
collapsed,
|
||||||
|
isObject,
|
||||||
|
isArray,
|
||||||
|
isEmpty,
|
||||||
|
collapsable,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -66,6 +90,14 @@ export default defineComponent({
|
||||||
> .boolean {
|
> .boolean {
|
||||||
display: inline;
|
display: inline;
|
||||||
color: var(--codeBoolean);
|
color: var(--codeBoolean);
|
||||||
|
|
||||||
|
&.true {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.false {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .string {
|
> .string {
|
||||||
|
@ -78,7 +110,12 @@ export default defineComponent({
|
||||||
color: var(--codeNumber);
|
color: var(--codeNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .array {
|
> .array.empty {
|
||||||
|
display: inline;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .array:not(.empty) {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
|
||||||
> .element {
|
> .element {
|
||||||
|
@ -87,13 +124,28 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .object {
|
> .object.empty {
|
||||||
|
display: inline;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .object:not(.empty) {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
|
||||||
> .kv {
|
> .kv {
|
||||||
display: block;
|
display: block;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
|
|
||||||
|
> .toggle {
|
||||||
|
width: 16px;
|
||||||
|
color: var(--accent);
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .k {
|
> .k {
|
||||||
display: inline;
|
display: inline;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
|
|
@ -4,26 +4,13 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { computed, defineComponent } from 'vue';
|
import { } from 'vue';
|
||||||
import XValue from './object-view.value.vue';
|
import XValue from './object-view.value.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
value: Record<string, unknown>;
|
||||||
XValue
|
}>();
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -48,7 +48,10 @@ const router = new Router(routes, props.initialPath);
|
||||||
|
|
||||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||||
let windowEl = $ref<InstanceType<typeof XWindow>>();
|
let windowEl = $ref<InstanceType<typeof XWindow>>();
|
||||||
const history = $ref<string[]>([props.initialPath]);
|
const history = $ref<{ path: string; key: any; }[]>([{
|
||||||
|
path: router.getCurrentPath(),
|
||||||
|
key: router.getCurrentKey(),
|
||||||
|
}]);
|
||||||
const buttonsLeft = $computed(() => {
|
const buttonsLeft = $computed(() => {
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
|
|
||||||
|
@ -72,7 +75,7 @@ const buttonsRight = $computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.addListener('push', ctx => {
|
router.addListener('push', ctx => {
|
||||||
history.push(router.getCurrentPath());
|
history.push({ path: ctx.path, key: ctx.key });
|
||||||
});
|
});
|
||||||
|
|
||||||
provide('router', router);
|
provide('router', router);
|
||||||
|
@ -111,7 +114,7 @@ function menu(ev) {
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
history.pop();
|
history.pop();
|
||||||
router.change(history[history.length - 1]);
|
router.change(history[history.length - 1].path, history[history.length - 1].key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
@ -136,5 +139,6 @@ defineExpose({
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.yrolvcoq {
|
.yrolvcoq {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -24,7 +24,6 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, ctx) {
|
setup(props, ctx) {
|
||||||
|
|
||||||
const hpml = new Hpml(props.page, {
|
const hpml = new Hpml(props.page, {
|
||||||
randomSeed: Math.random(),
|
randomSeed: Math.random(),
|
||||||
visitor: $i,
|
visitor: $i,
|
||||||
|
|
|
@ -116,8 +116,11 @@ function get() {
|
||||||
let base = parseInt(after.value);
|
let base = parseInt(after.value);
|
||||||
switch (unit.value) {
|
switch (unit.value) {
|
||||||
case 'day': base *= 24;
|
case 'day': base *= 24;
|
||||||
|
// fallthrough
|
||||||
case 'hour': base *= 60;
|
case 'hour': base *= 60;
|
||||||
|
// fallthrough
|
||||||
case 'minute': base *= 60;
|
case 'minute': base *= 60;
|
||||||
|
// fallthrough
|
||||||
case 'second': return base *= 1000;
|
case 'second': return base *= 1000;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,39 +12,22 @@
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { computed, defineComponent, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
import XDetails from '@/components/reactions-viewer.details.vue';
|
import XDetails from '@/components/reactions-viewer.details.vue';
|
||||||
import XReactionIcon from '@/components/reaction-icon.vue';
|
import XReactionIcon from '@/components/reaction-icon.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
reaction: string;
|
||||||
XReactionIcon
|
count: number;
|
||||||
},
|
isInitial: boolean;
|
||||||
|
note: misskey.entities.Note;
|
||||||
|
}>();
|
||||||
|
|
||||||
props: {
|
|
||||||
reaction: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
count: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isInitial: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
note: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
|
||||||
const buttonRef = ref<HTMLElement>();
|
const buttonRef = ref<HTMLElement>();
|
||||||
|
|
||||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||||
|
@ -55,19 +38,19 @@ export default defineComponent({
|
||||||
const oldReaction = props.note.myReaction;
|
const oldReaction = props.note.myReaction;
|
||||||
if (oldReaction) {
|
if (oldReaction) {
|
||||||
os.api('notes/reactions/delete', {
|
os.api('notes/reactions/delete', {
|
||||||
noteId: props.note.id
|
noteId: props.note.id,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
if (oldReaction !== props.reaction) {
|
if (oldReaction !== props.reaction) {
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
noteId: props.note.id,
|
noteId: props.note.id,
|
||||||
reaction: props.reaction
|
reaction: props.reaction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
os.api('notes/reactions/create', {
|
os.api('notes/reactions/create', {
|
||||||
noteId: props.note.id,
|
noteId: props.note.id,
|
||||||
reaction: props.reaction
|
reaction: props.reaction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -90,7 +73,7 @@ export default defineComponent({
|
||||||
const reactions = await os.api('notes/reactions', {
|
const reactions = await os.api('notes/reactions', {
|
||||||
noteId: props.note.id,
|
noteId: props.note.id,
|
||||||
type: props.reaction,
|
type: props.reaction,
|
||||||
limit: 11
|
limit: 11,
|
||||||
});
|
});
|
||||||
|
|
||||||
const users = reactions.map(x => x.user);
|
const users = reactions.map(x => x.user);
|
||||||
|
@ -103,15 +86,7 @@ export default defineComponent({
|
||||||
count: props.count,
|
count: props.count,
|
||||||
targetElement: buttonRef.value,
|
targetElement: buttonRef.value,
|
||||||
}, {}, 'closed');
|
}, {}, 'closed');
|
||||||
});
|
}, 100);
|
||||||
|
|
||||||
return {
|
|
||||||
buttonRef,
|
|
||||||
canToggle,
|
|
||||||
toggleReaction,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</MkInfo>
|
</MkInfo>
|
||||||
<div v-if="!totpLogin" class="normal-signin">
|
<div v-if="!totpLogin" class="normal-signin">
|
||||||
<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
<template #suffix>@{{ host }}</template>
|
<template #suffix>@{{ host }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<template #label>{{ i18n.ts.password }}</template>
|
<template #label>{{ i18n.ts.password }}</template>
|
||||||
<template #prefix><i class="fas fa-lock"></i></template>
|
<template #prefix><i class="fas fa-lock"></i></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
|
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" :spellcheck="false" required>
|
||||||
<template #label>{{ i18n.ts.token }}</template>
|
<template #label>{{ i18n.ts.token }}</template>
|
||||||
<template #prefix><i class="fas fa-gavel"></i></template>
|
<template #prefix><i class="fas fa-gavel"></i></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
|
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||||
<template v-if="meta">
|
<template v-if="meta">
|
||||||
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" spellcheck="false" required>
|
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
|
||||||
<template #label>{{ $ts.invitationCode }}</template>
|
<template #label>{{ $ts.invitationCode }}</template>
|
||||||
<template #prefix><i class="fas fa-key"></i></template>
|
<template #prefix><i class="fas fa-key"></i></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
||||||
<template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
|
<template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
<template #suffix>@{{ host }}</template>
|
<template #suffix>@{{ host }}</template>
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
|
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
|
||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
|
<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
|
||||||
<template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
|
<template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
|
||||||
<template #prefix><i class="fas fa-envelope"></i></template>
|
<template #prefix><i class="fas fa-envelope"></i></template>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
|
|
88
packages/client/src/components/tag-cloud.vue
Normal file
88
packages/client/src/components/tag-cloud.vue
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<template>
|
||||||
|
<div ref="rootEl" class="meijqfqm">
|
||||||
|
<canvas :id="idForCanvas" ref="canvasEl" class="canvas" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas>
|
||||||
|
<div :id="idForTags" ref="tagsEl" class="tags">
|
||||||
|
<ul>
|
||||||
|
<slot></slot>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref, watch, PropType, onBeforeUnmount } from 'vue';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
|
const loaded = !!window.TagCanvas;
|
||||||
|
const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
|
const idForCanvas = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
||||||
|
const idForTags = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
||||||
|
let available = $ref(false);
|
||||||
|
let rootEl = $ref<HTMLElement | null>(null);
|
||||||
|
let canvasEl = $ref<HTMLCanvasElement | null>(null);
|
||||||
|
let tagsEl = $ref<HTMLElement | null>(null);
|
||||||
|
let width = $ref(300);
|
||||||
|
|
||||||
|
watch($$(available), () => {
|
||||||
|
window.TagCanvas.Start(idForCanvas, idForTags, {
|
||||||
|
textColour: '#ffffff',
|
||||||
|
outlineColour: tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(),
|
||||||
|
outlineRadius: 10,
|
||||||
|
initial: [-0.030, -0.010],
|
||||||
|
frontSelect: true,
|
||||||
|
imageRadius: 8,
|
||||||
|
//dragControl: true,
|
||||||
|
dragThreshold: 3,
|
||||||
|
wheelZoom: false,
|
||||||
|
reverse: true,
|
||||||
|
depth: 0.5,
|
||||||
|
maxSpeed: 0.2,
|
||||||
|
minSpeed: 0.003,
|
||||||
|
stretchX: 0.8,
|
||||||
|
stretchY: 0.8,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
width = rootEl.offsetWidth;
|
||||||
|
|
||||||
|
if (loaded) {
|
||||||
|
available = true;
|
||||||
|
} else {
|
||||||
|
document.head.appendChild(Object.assign(document.createElement('script'), {
|
||||||
|
async: true,
|
||||||
|
src: '/client-assets/tagcanvas.min.js',
|
||||||
|
})).addEventListener('load', () => available = true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.TagCanvas.Delete(idForCanvas);
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
update: () => {
|
||||||
|
window.TagCanvas.Update(idForCanvas);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.meijqfqm {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden; overflow: clip;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
|
||||||
|
> .canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tags {
|
||||||
|
position: absolute;
|
||||||
|
top: 999px;
|
||||||
|
left: 999px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -54,7 +54,7 @@ onMounted(() => {
|
||||||
width: min-content;
|
width: min-content;
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: clip;
|
overflow: hidden; overflow: clip;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
|
|
@ -148,7 +148,7 @@ export default defineComponent({
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: var(--buttonBg);
|
background: var(--buttonBg);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow: clip;
|
overflow: hidden; overflow: clip;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: background 0.1s ease;
|
transition: background 0.1s ease;
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,8 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<transition :name="$store.state.animation ? 'container-toggle' : ''"
|
<transition
|
||||||
|
:name="$store.state.animation ? 'container-toggle' : ''"
|
||||||
@enter="enter"
|
@enter="enter"
|
||||||
@after-enter="afterEnter"
|
@after-enter="afterEnter"
|
||||||
@leave="leave"
|
@leave="leave"
|
||||||
|
@ -34,37 +35,37 @@ export default defineComponent({
|
||||||
showHeader: {
|
showHeader: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
thin: {
|
thin: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
naked: {
|
naked: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
foldable: {
|
foldable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
expanded: {
|
expanded: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
scrollable: {
|
scrollable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
maxHeight: {
|
maxHeight: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -79,12 +80,12 @@ export default defineComponent({
|
||||||
const headerHeight = this.showHeader ? this.$refs.header.offsetHeight : 0;
|
const headerHeight = this.showHeader ? this.$refs.header.offsetHeight : 0;
|
||||||
this.$el.style.minHeight = `${headerHeight}px`;
|
this.$el.style.minHeight = `${headerHeight}px`;
|
||||||
if (showBody) {
|
if (showBody) {
|
||||||
this.$el.style.flexBasis = `auto`;
|
this.$el.style.flexBasis = 'auto';
|
||||||
} else {
|
} else {
|
||||||
this.$el.style.flexBasis = `${headerHeight}px`;
|
this.$el.style.flexBasis = `${headerHeight}px`;
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
immediate: true
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px');
|
this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px');
|
||||||
|
@ -124,7 +125,7 @@ export default defineComponent({
|
||||||
afterLeave(el) {
|
afterLeave(el) {
|
||||||
el.style.height = null;
|
el.style.height = null;
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -142,7 +143,7 @@ export default defineComponent({
|
||||||
|
|
||||||
.ukygtjoj {
|
.ukygtjoj {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: clip;
|
overflow: hidden; overflow: clip;
|
||||||
|
|
||||||
&.naked {
|
&.naked {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';import * as os from '@/os';
|
import { defineComponent } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
export default defineComponent({});
|
export default defineComponent({});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -136,11 +136,11 @@ function focusDown() {
|
||||||
> .item {
|
> .item {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 8px 18px;
|
padding: 6px 18px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 0.9em;
|
font-size: 0.85em;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -105,7 +105,6 @@ defineExpose({
|
||||||
background: var(--windowHeader);
|
background: var(--windowHeader);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
box-shadow: 0px 1px var(--divider);
|
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
height: $height;
|
height: $height;
|
||||||
|
|
|
@ -389,7 +389,7 @@ defineExpose({
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: clip;
|
overflow: hidden; overflow: clip;
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
@ -99,12 +99,12 @@ export default defineComponent({
|
||||||
buttonsLeft: {
|
buttonsLeft: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
default: [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
buttonsRight: {
|
buttonsRight: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
default: [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -410,6 +410,7 @@ export default defineComponent({
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
//border-bottom: solid 1px var(--divider);
|
//border-bottom: solid 1px var(--divider);
|
||||||
font-size: 95%;
|
font-size: 95%;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
> .left, > .right {
|
> .left, > .right {
|
||||||
> .button {
|
> .button {
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
<div class="customize-container">
|
<div class="customize-container">
|
||||||
<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button>
|
<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button>
|
||||||
<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button>
|
<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button>
|
||||||
<component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="handle" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
|
<div class="handle">
|
||||||
|
<component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</XDraggable>
|
</XDraggable>
|
||||||
|
@ -141,6 +143,12 @@ export default defineComponent({
|
||||||
> .remove {
|
> .remove {
|
||||||
right: 8px;
|
right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .handle {
|
||||||
|
> .widget {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -34,7 +34,6 @@ function calc(src: Element) {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mounted(src, binding, vn) {
|
mounted(src, binding, vn) {
|
||||||
|
|
||||||
const resize = new ResizeObserver((entries, observer) => {
|
const resize = new ResizeObserver((entries, observer) => {
|
||||||
calc(src);
|
calc(src);
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,11 +35,6 @@ export const menuDef = reactive({
|
||||||
indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
|
indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
|
||||||
to: '/my/follow-requests',
|
to: '/my/follow-requests',
|
||||||
},
|
},
|
||||||
featured: {
|
|
||||||
title: 'featured',
|
|
||||||
icon: 'fas fa-fire-alt',
|
|
||||||
to: '/featured',
|
|
||||||
},
|
|
||||||
explore: {
|
explore: {
|
||||||
title: 'explore',
|
title: 'explore',
|
||||||
icon: 'fas fa-hashtag',
|
icon: 'fas fa-hashtag',
|
||||||
|
@ -81,12 +76,14 @@ export const menuDef = reactive({
|
||||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
groups: {
|
groups: {
|
||||||
title: 'groups',
|
title: 'groups',
|
||||||
icon: 'fas fa-users',
|
icon: 'fas fa-users',
|
||||||
show: computed(() => $i != null),
|
show: computed(() => $i != null),
|
||||||
to: '/my/groups',
|
to: '/my/groups',
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
antennas: {
|
antennas: {
|
||||||
title: 'antennas',
|
title: 'antennas',
|
||||||
icon: 'fas fa-satellite',
|
icon: 'fas fa-satellite',
|
||||||
|
@ -112,20 +109,6 @@ export const menuDef = reactive({
|
||||||
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
os.popupMenu(items, ev.currentTarget ?? ev.target);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mentions: {
|
|
||||||
title: 'mentions',
|
|
||||||
icon: 'fas fa-at',
|
|
||||||
show: computed(() => $i != null),
|
|
||||||
indicated: computed(() => $i != null && $i.hasUnreadMentions),
|
|
||||||
to: '/my/mentions',
|
|
||||||
},
|
|
||||||
messages: {
|
|
||||||
title: 'directNotes',
|
|
||||||
icon: 'fas fa-envelope',
|
|
||||||
show: computed(() => $i != null),
|
|
||||||
indicated: computed(() => $i != null && $i.hasUnreadSpecifiedNotes),
|
|
||||||
to: '/my/messages',
|
|
||||||
},
|
|
||||||
favorites: {
|
favorites: {
|
||||||
title: 'favorites',
|
title: 'favorites',
|
||||||
icon: 'fas fa-star',
|
icon: 'fas fa-star',
|
||||||
|
@ -153,21 +136,6 @@ export const menuDef = reactive({
|
||||||
icon: 'fas fa-satellite-dish',
|
icon: 'fas fa-satellite-dish',
|
||||||
to: '/channels',
|
to: '/channels',
|
||||||
},
|
},
|
||||||
federation: {
|
|
||||||
title: 'federation',
|
|
||||||
icon: 'fas fa-globe',
|
|
||||||
to: '/federation',
|
|
||||||
},
|
|
||||||
emojis: {
|
|
||||||
title: 'emojis',
|
|
||||||
icon: 'fas fa-laugh',
|
|
||||||
to: '/emojis',
|
|
||||||
},
|
|
||||||
scratchpad: {
|
|
||||||
title: 'scratchpad',
|
|
||||||
icon: 'fas fa-terminal',
|
|
||||||
to: '/scratchpad',
|
|
||||||
},
|
|
||||||
ui: {
|
ui: {
|
||||||
title: 'switchUi',
|
title: 'switchUi',
|
||||||
icon: 'fas fa-columns',
|
icon: 'fas fa-columns',
|
||||||
|
|
|
@ -2,12 +2,15 @@
|
||||||
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue';
|
import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue';
|
||||||
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
|
|
||||||
type RouteDef = {
|
type RouteDef = {
|
||||||
path: string;
|
path: string;
|
||||||
component: Component;
|
component: Component;
|
||||||
query?: Record<string, string>;
|
query?: Record<string, string>;
|
||||||
|
loginRequired?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
hash?: string;
|
||||||
globalCacheKey?: string;
|
globalCacheKey?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -78,7 +81,12 @@ export class Router extends EventEmitter<{
|
||||||
|
|
||||||
public resolve(path: string): { route: RouteDef; props: Map<string, string>; } | null {
|
public resolve(path: string): { route: RouteDef; props: Map<string, string>; } | null {
|
||||||
let queryString: string | null = null;
|
let queryString: string | null = null;
|
||||||
|
let hash: string | null = null;
|
||||||
if (path[0] === '/') path = path.substring(1);
|
if (path[0] === '/') path = path.substring(1);
|
||||||
|
if (path.includes('#')) {
|
||||||
|
hash = path.substring(path.indexOf('#') + 1);
|
||||||
|
path = path.substring(0, path.indexOf('#'));
|
||||||
|
}
|
||||||
if (path.includes('?')) {
|
if (path.includes('?')) {
|
||||||
queryString = path.substring(path.indexOf('?') + 1);
|
queryString = path.substring(path.indexOf('?') + 1);
|
||||||
path = path.substring(0, path.indexOf('?'));
|
path = path.substring(0, path.indexOf('?'));
|
||||||
|
@ -127,6 +135,10 @@ export class Router extends EventEmitter<{
|
||||||
|
|
||||||
if (parts.length !== 0) continue forEachRouteLoop;
|
if (parts.length !== 0) continue forEachRouteLoop;
|
||||||
|
|
||||||
|
if (route.hash != null && hash != null) {
|
||||||
|
props.set(route.hash, hash);
|
||||||
|
}
|
||||||
|
|
||||||
if (route.query != null && queryString != null) {
|
if (route.query != null && queryString != null) {
|
||||||
const queryObject = [...new URLSearchParams(queryString).entries()]
|
const queryObject = [...new URLSearchParams(queryString).entries()]
|
||||||
.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
|
.reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {});
|
||||||
|
@ -138,6 +150,7 @@ export class Router extends EventEmitter<{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
route,
|
route,
|
||||||
props,
|
props,
|
||||||
|
@ -158,6 +171,10 @@ export class Router extends EventEmitter<{
|
||||||
throw new Error('no route found for: ' + path);
|
throw new Error('no route found for: ' + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (res.route.loginRequired) {
|
||||||
|
pleaseLogin('/');
|
||||||
|
}
|
||||||
|
|
||||||
const isSamePath = beforePath === path;
|
const isSamePath = beforePath === path;
|
||||||
if (isSamePath && key == null) key = this.currentKey;
|
if (isSamePath && key == null) key = this.currentKey;
|
||||||
this.currentComponent = res.route.component;
|
this.currentComponent = res.route.component;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<div style="overflow: clip;">
|
<div style="overflow: hidden; overflow: clip;">
|
||||||
<MkSpacer :content-max="600" :margin-min="20">
|
<MkSpacer :content-max="600" :margin-min="20">
|
||||||
<div class="_formRoot znqjceqz">
|
<div class="_formRoot znqjceqz">
|
||||||
<div id="debug"></div>
|
<div id="debug"></div>
|
||||||
|
@ -204,7 +204,6 @@ const headerTabs = $computed(() => []);
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.aboutMisskey,
|
title: i18n.ts.aboutMisskey,
|
||||||
icon: null,
|
icon: null,
|
||||||
bg: 'var(--bg)',
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
106
packages/client/src/pages/about.federation.vue
Normal file
106
packages/client/src/pages/about.federation.vue
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<div class="taeiyria">
|
||||||
|
<div class="query">
|
||||||
|
<MkInput v-model="host" :debounce="true" class="">
|
||||||
|
<template #prefix><i class="fas fa-search"></i></template>
|
||||||
|
<template #label>{{ $ts.host }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<FormSplit style="margin-top: var(--margin);">
|
||||||
|
<MkSelect v-model="state">
|
||||||
|
<template #label>{{ $ts.state }}</template>
|
||||||
|
<option value="all">{{ $ts.all }}</option>
|
||||||
|
<option value="federating">{{ $ts.federating }}</option>
|
||||||
|
<option value="subscribing">{{ $ts.subscribing }}</option>
|
||||||
|
<option value="publishing">{{ $ts.publishing }}</option>
|
||||||
|
<option value="suspended">{{ $ts.suspended }}</option>
|
||||||
|
<option value="blocked">{{ $ts.blocked }}</option>
|
||||||
|
<option value="notResponding">{{ $ts.notResponding }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
<MkSelect v-model="sort">
|
||||||
|
<template #label>{{ $ts.sort }}</template>
|
||||||
|
<option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
|
||||||
|
<option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
|
||||||
|
<option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
|
||||||
|
<option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
|
||||||
|
<option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
|
||||||
|
<option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
|
||||||
|
<option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
|
||||||
|
<option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
|
||||||
|
<option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
|
||||||
|
<option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
|
||||||
|
<option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
|
||||||
|
<option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
|
||||||
|
<option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
|
||||||
|
<option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
|
||||||
|
</MkSelect>
|
||||||
|
</FormSplit>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||||
|
<div class="dqokceoi">
|
||||||
|
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Last communicated: ${new Date(instance.lastCommunicatedAt).toLocaleString()}\nStatus: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`">
|
||||||
|
<MkInstanceCardMini :instance="instance"/>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
|
</MkPagination>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import MkButton from '@/components/ui/button.vue';
|
||||||
|
import MkInput from '@/components/form/input.vue';
|
||||||
|
import MkSelect from '@/components/form/select.vue';
|
||||||
|
import MkPagination from '@/components/ui/pagination.vue';
|
||||||
|
import MkInstanceCardMini from '@/components/instance-card-mini.vue';
|
||||||
|
import FormSplit from '@/components/form/split.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
let host = $ref('');
|
||||||
|
let state = $ref('federating');
|
||||||
|
let sort = $ref('+pubSub');
|
||||||
|
const pagination = {
|
||||||
|
endpoint: 'federation/instances' as const,
|
||||||
|
limit: 10,
|
||||||
|
offsetMode: true,
|
||||||
|
params: computed(() => ({
|
||||||
|
sort: sort,
|
||||||
|
host: host !== '' ? host : null,
|
||||||
|
...(
|
||||||
|
state === 'federating' ? { federating: true } :
|
||||||
|
state === 'subscribing' ? { subscribing: true } :
|
||||||
|
state === 'publishing' ? { publishing: true } :
|
||||||
|
state === 'suspended' ? { suspended: true } :
|
||||||
|
state === 'blocked' ? { blocked: true } :
|
||||||
|
state === 'notResponding' ? { notResponding: true } :
|
||||||
|
{}),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStatus(instance) {
|
||||||
|
if (instance.isSuspended) return 'Suspended';
|
||||||
|
if (instance.isBlocked) return 'Blocked';
|
||||||
|
if (instance.isNotResponding) return 'Error';
|
||||||
|
return 'Alive';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.taeiyria {
|
||||||
|
> .query {
|
||||||
|
background: var(--bg);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dqokceoi {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||||
|
grid-gap: 12px;
|
||||||
|
|
||||||
|
> .instance:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -67,7 +67,13 @@
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
<MkSpacer v-else-if="tab === 'charts'" :content-max="1200" :margin-min="20">
|
<MkSpacer v-else-if="tab === 'emojis'" :content-max="1000" :margin-min="20">
|
||||||
|
<XEmojis/>
|
||||||
|
</MkSpacer>
|
||||||
|
<MkSpacer v-else-if="tab === 'federation'" :content-max="1000" :margin-min="20">
|
||||||
|
<XFederation/>
|
||||||
|
</MkSpacer>
|
||||||
|
<MkSpacer v-else-if="tab === 'charts'" :content-max="1000" :margin-min="20">
|
||||||
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
|
@ -75,6 +81,8 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
import XEmojis from './about.emojis.vue';
|
||||||
|
import XFederation from './about.federation.vue';
|
||||||
import { version, instanceName , host } from '@/config';
|
import { version, instanceName , host } from '@/config';
|
||||||
import FormLink from '@/components/form/link.vue';
|
import FormLink from '@/components/form/link.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
|
@ -87,8 +95,14 @@ import number from '@/filters/number';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
initialTab?: string;
|
||||||
|
}>(), {
|
||||||
|
initialTab: 'overview',
|
||||||
|
});
|
||||||
|
|
||||||
let stats = $ref(null);
|
let stats = $ref(null);
|
||||||
let tab = $ref('overview');
|
let tab = $ref(props.initialTab);
|
||||||
|
|
||||||
const initStats = () => os.api('stats', {
|
const initStats = () => os.api('stats', {
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
|
@ -100,16 +114,23 @@ const headerActions = $computed(() => []);
|
||||||
const headerTabs = $computed(() => [{
|
const headerTabs = $computed(() => [{
|
||||||
key: 'overview',
|
key: 'overview',
|
||||||
title: i18n.ts.overview,
|
title: i18n.ts.overview,
|
||||||
|
}, {
|
||||||
|
key: 'emojis',
|
||||||
|
title: i18n.ts.customEmojis,
|
||||||
|
icon: 'fas fa-laugh',
|
||||||
|
}, {
|
||||||
|
key: 'federation',
|
||||||
|
title: i18n.ts.federation,
|
||||||
|
icon: 'fas fa-globe',
|
||||||
}, {
|
}, {
|
||||||
key: 'charts',
|
key: 'charts',
|
||||||
title: i18n.ts.charts,
|
title: i18n.ts.charts,
|
||||||
icon: 'fas fa-chart-bar',
|
icon: 'fas fa-chart-simple',
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
definePageMetadata(computed(() => ({
|
definePageMetadata(computed(() => ({
|
||||||
title: i18n.ts.instanceInfo,
|
title: i18n.ts.instanceInfo,
|
||||||
icon: 'fas fa-info-circle',
|
icon: 'fas fa-info-circle',
|
||||||
bg: 'var(--bg)',
|
|
||||||
})));
|
})));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -117,7 +138,7 @@ definePageMetadata(computed(() => ({
|
||||||
.fwhjspax {
|
.fwhjspax {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: clip;
|
overflow: hidden; overflow: clip;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer v-if="file" :content-max="500" :margin-min="16" :margin-max="32">
|
<MkSpacer v-if="file" :content-max="600" :margin-min="16" :margin-max="32">
|
||||||
<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot">
|
<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot">
|
||||||
<a class="_formBlock thumbnail" :href="file.url" target="_blank">
|
<a class="_formBlock thumbnail" :href="file.url" target="_blank">
|
||||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||||
|
@ -39,6 +39,20 @@
|
||||||
<MkButton danger @click="del"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</MkButton>
|
<MkButton danger @click="del"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="tab === 'ip' && info" class="_formRoot">
|
||||||
|
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
|
||||||
|
<MkKeyValue v-if="info.requestIp" class="_formBlock _monospace" :copy="info.requestIp" oneline>
|
||||||
|
<template #key>IP</template>
|
||||||
|
<template #value>{{ info.requestIp }}</template>
|
||||||
|
</MkKeyValue>
|
||||||
|
<FormSection v-if="info.requestHeaders">
|
||||||
|
<template #label>Headers</template>
|
||||||
|
<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_formBlock _monospace">
|
||||||
|
<template #key>{{ k }}</template>
|
||||||
|
<template #value>{{ v }}</template>
|
||||||
|
</MkKeyValue>
|
||||||
|
</FormSection>
|
||||||
|
</div>
|
||||||
<div v-else-if="tab === 'raw'" class="_formRoot">
|
<div v-else-if="tab === 'raw'" class="_formRoot">
|
||||||
<MkObjectView v-if="info" tall :value="info">
|
<MkObjectView v-if="info" tall :value="info">
|
||||||
</MkObjectView>
|
</MkObjectView>
|
||||||
|
@ -54,13 +68,15 @@ import MkSwitch from '@/components/form/switch.vue';
|
||||||
import MkObjectView from '@/components/object-view.vue';
|
import MkObjectView from '@/components/object-view.vue';
|
||||||
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
|
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
|
||||||
import MkKeyValue from '@/components/key-value.vue';
|
import MkKeyValue from '@/components/key-value.vue';
|
||||||
import FormLink from '@/components/form/link.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
import MkUserCardMini from '@/components/user-card-mini.vue';
|
import MkUserCardMini from '@/components/user-card-mini.vue';
|
||||||
|
import MkInfo from '@/components/ui/info.vue';
|
||||||
import bytes from '@/filters/bytes';
|
import bytes from '@/filters/bytes';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { acct } from '@/filters/user';
|
import { acct } from '@/filters/user';
|
||||||
|
import { iAmAdmin, iAmModerator } from '@/account';
|
||||||
|
|
||||||
let tab = $ref('overview');
|
let tab = $ref('overview');
|
||||||
let file: any = $ref(null);
|
let file: any = $ref(null);
|
||||||
|
@ -108,7 +124,11 @@ const headerTabs = $computed(() => [{
|
||||||
key: 'overview',
|
key: 'overview',
|
||||||
title: i18n.ts.overview,
|
title: i18n.ts.overview,
|
||||||
icon: 'fas fa-info-circle',
|
icon: 'fas fa-info-circle',
|
||||||
}, {
|
}, iAmModerator ? {
|
||||||
|
key: 'ip',
|
||||||
|
title: 'IP',
|
||||||
|
icon: 'fas fa-bars-staggered',
|
||||||
|
} : null, {
|
||||||
key: 'raw',
|
key: 'raw',
|
||||||
title: 'Raw data',
|
title: 'Raw data',
|
||||||
icon: 'fas fa-code',
|
icon: 'fas fa-code',
|
||||||
|
@ -117,7 +137,6 @@ const headerTabs = $computed(() => [{
|
||||||
definePageMetadata(computed(() => ({
|
definePageMetadata(computed(() => ({
|
||||||
title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file,
|
title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file,
|
||||||
icon: 'fas fa-file',
|
icon: 'fas fa-file',
|
||||||
bg: 'var(--bg)',
|
|
||||||
})));
|
})));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, onUnmounted, ref, inject, watch } from 'vue';
|
import { computed, onMounted, onUnmounted, ref, inject, watch, nextTick } from 'vue';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { popupMenu } from '@/os';
|
import { popupMenu } from '@/os';
|
||||||
import { url } from '@/config';
|
import { url } from '@/config';
|
||||||
|
@ -75,7 +75,6 @@ const hasTabs = computed(() => {
|
||||||
|
|
||||||
const showTabsPopup = (ev: MouseEvent) => {
|
const showTabsPopup = (ev: MouseEvent) => {
|
||||||
if (!hasTabs.value) return;
|
if (!hasTabs.value) return;
|
||||||
if (!narrow.value) return;
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const menu = props.tabs.map(tab => ({
|
const menu = props.tabs.map(tab => ({
|
||||||
|
@ -126,7 +125,8 @@ onMounted(() => {
|
||||||
calcBg();
|
calcBg();
|
||||||
globalEvents.on('themeChanged', calcBg);
|
globalEvents.on('themeChanged', calcBg);
|
||||||
|
|
||||||
watch(() => props.tab, () => {
|
watch(() => [props.tab, props.tabs], () => {
|
||||||
|
nextTick(() => {
|
||||||
const tabEl = tabRefs[props.tab];
|
const tabEl = tabRefs[props.tab];
|
||||||
if (tabEl && tabHighlightEl) {
|
if (tabEl && tabHighlightEl) {
|
||||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||||
|
@ -136,6 +136,7 @@ onMounted(() => {
|
||||||
tabHighlightEl.style.width = rect.width + 'px';
|
tabHighlightEl.style.width = rect.width + 'px';
|
||||||
tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
|
tabHighlightEl.style.left = (rect.left - parentRect.left) + 'px';
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
@ -150,9 +151,6 @@ onUnmounted(() => {
|
||||||
.fdidabkc {
|
.fdidabkc {
|
||||||
--height: 60px;
|
--height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: sticky;
|
|
||||||
top: var(--stickyTop, 0);
|
|
||||||
z-index: 1000;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue