Compare commits

..
Author SHA1 Message Date
syuilo 7f9180e045 Merge branch 'develop' into lowpowermode 2025-08-01 15:20:01 +09:00
syuilo e8b5aa5c2f wip 2025-07-30 13:52:52 +09:00
110 changed files with 3640 additions and 4885 deletions
-12
View File
@@ -12,20 +12,8 @@
- 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。
- 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。
- サーバーの初期設定が完了するまでは連合がオンにならないようになりました
- 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました
- mfm.jsをアップデートしました
- Enhance: Unicode 15.1 および 16.0 に収録されている絵文字に対応
- Enhance: acctに `.` が入っているユーザーのメンションに対応
- Fix: Unicode絵文字に隣接する異体字セレクタ(`U+FE0F`)が絵文字として認識される問題を修正
### Client
- Feat: セーフモード
- プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます
- 以下の方法でセーフモードを起動できます
- `g` キーを連打する
- URLに`?safemode=true`を付ける
- PWAのショートカットで Safemode を選択して起動する
- Enhance: コントロールパネルを検索できるように
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
- Fix: テーマエディタが動作しない問題を修正
+15 -27
View File
@@ -315,11 +315,11 @@ export interface Locale extends ILocale {
*/
"mention": string;
/**
* メンション
* あなた宛て
*/
"mentions": string;
/**
* 指名
* ダイレクト投稿
*/
"directNotes": string;
/**
@@ -5493,6 +5493,10 @@ export interface Locale extends ILocale {
* 低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。
*/
"defaultImageCompressionLevel_description": string;
/**
* 低電力モード
*/
"lowPowerMode": string;
/**
* 分
*/
@@ -5501,22 +5505,6 @@ export interface Locale extends ILocale {
* 日
*/
"inDays": string;
/**
* セーフモードが有効です
*/
"safeModeEnabled": string;
/**
* セーフモードが有効なため、プラグインはすべて無効化されています。
*/
"pluginsAreDisabledBecauseSafeMode": string;
/**
* セーフモードが有効なため、カスタムCSSは適用されていません。
*/
"customCssIsDisabledBecauseSafeMode": string;
/**
* セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。
*/
"themeIsDefaultBecauseSafeMode": string;
"_order": {
/**
* 新しい順
@@ -5823,6 +5811,10 @@ export interface Locale extends ILocale {
* UIのアニメーション
*/
"uiAnimations": string;
/**
* アニメーション画像を再生
*/
"playAnimatedImages": string;
/**
* ナビゲーションバーに副ボタンを表示
*/
@@ -6353,7 +6345,7 @@ export interface Locale extends ILocale {
*/
"followers": string;
/**
* 指定したユーザーにのみ公開され、また相手に通知が入ります。
* 指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。
*/
"direct": string;
/**
@@ -6361,7 +6353,7 @@ export interface Locale extends ILocale {
*/
"doNotSendConfidencialOnDirect1": string;
/**
* 送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーが含まれる限定公開のノートを作成する際は、機密情報の扱いに注意が必要です。
* 送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。
*/
"doNotSendConfidencialOnDirect2": string;
/**
@@ -9641,7 +9633,7 @@ export interface Locale extends ILocale {
*/
"followersDescription": string;
/**
* 指名
* ダイレクト
*/
"specified": string;
/**
@@ -10530,11 +10522,11 @@ export interface Locale extends ILocale {
*/
"channel": string;
/**
* メンション
* あなた宛て
*/
"mentions": string;
/**
* 指名
* ダイレクト
*/
"direct": string;
/**
@@ -11855,10 +11847,6 @@ export interface Locale extends ILocale {
* 修復ツールを起動
*/
"otherOption3": string;
/**
* Misskeyをセーフモードで起動
*/
"otherOption4": string;
};
"_search": {
/**
+9 -12
View File
@@ -74,8 +74,8 @@ youGotNewFollower: "フォローされました"
receiveFollowRequest: "フォローリクエストされました"
followRequestAccepted: "フォローが承認されました"
mention: "メンション"
mentions: "メンション"
directNotes: "指名"
mentions: "あなた宛て"
directNotes: "ダイレクト投稿"
importAndExport: "インポートとエクスポート"
import: "インポート"
export: "エクスポート"
@@ -1368,12 +1368,9 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示"
hideAllTips: "全ての「ヒントとコツ」を非表示"
defaultImageCompressionLevel: "デフォルトの画像圧縮度"
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
lowPowerMode: "低電力モード"
inMinutes: "分"
inDays: "日"
safeModeEnabled: "セーフモードが有効です"
pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プラグインはすべて無効化されています。"
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
_order:
newest: "新しい順"
@@ -1457,6 +1454,7 @@ _settings:
useStickyIcons: "アイコンをスクロールに追従させる"
enableHighQualityImagePlaceholders: "高品質な画像のプレースホルダを表示"
uiAnimations: "UIのアニメーション"
playAnimatedImages: "アニメーション画像を再生"
showNavbarSubButtons: "ナビゲーションバーに副ボタンを表示"
ifOn: "オンのとき"
ifOff: "オフのとき"
@@ -1609,9 +1607,9 @@ _initialTutorial:
public: "すべてのユーザーに公開。"
home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。"
followers: "フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。"
direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。"
direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。"
doNotSendConfidencialOnDirect1: "機密情報は送信する際は注意してください。"
doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーが含まれる限定公開のノートを作成する際は、機密情報の扱いに注意が必要です。"
doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。"
localOnly: "他のサーバーに投稿を連合しません。上記の公開範囲に関わらず、他のサーバーのユーザーは、この設定がついたノートを直接閲覧することができなくなります。"
_cw:
title: "内容を隠す(CW)"
@@ -2536,7 +2534,7 @@ _visibility:
homeDescription: "ホームタイムラインのみに公開"
followers: "フォロワー"
followersDescription: "自分のフォロワーのみに公開"
specified: "指名"
specified: "ダイレクト"
specifiedDescription: "指定したユーザーのみに公開"
disableFederation: "連合なし"
disableFederationDescription: "他サーバーへの配信を行いません"
@@ -2782,8 +2780,8 @@ _deck:
antenna: "アンテナ"
list: "リスト"
channel: "チャンネル"
mentions: "メンション"
direct: "指名"
mentions: "あなた宛て"
direct: "ダイレクト"
roleTimeline: "ロールタイムライン"
chat: "チャット"
@@ -3168,7 +3166,6 @@ _bootErrors:
otherOption1: "クライアント設定とキャッシュを削除"
otherOption2: "簡易クライアントを起動"
otherOption3: "修復ツールを起動"
otherOption4: "Misskeyをセーフモードで起動"
_search:
searchScopeAll: "全て"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.8.0-alpha.4",
"version": "2025.8.0-alpha.1",
"codename": "nasubi",
"repository": {
"type": "git",
@@ -7,7 +7,7 @@ export class RemoteNotesCleaning1753863104203 {
name = 'RemoteNotesCleaning1753863104203'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableRemoteNotesCleaning" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "meta" ADD "enableRemoteNotesCleaning" boolean NOT NULL DEFAULT true`);
await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningMaxProcessingDurationInMinutes" integer NOT NULL DEFAULT \'60\'');
await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningExpiryDaysForEachNotes" integer NOT NULL DEFAULT \'90\'');
}
@@ -8,9 +8,11 @@ export class TweakDefaultFederationSettings1754019326356 {
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'none'`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT true`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'all'`);
}
}
+3 -3
View File
@@ -70,7 +70,7 @@
"dependencies": {
"@aws-sdk/client-s3": "3.826.0",
"@aws-sdk/lib-storage": "3.826.0",
"@discordapp/twemoji": "16.0.1",
"@discordapp/twemoji": "15.1.0",
"@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2",
"@fastify/cors": "10.1.0",
@@ -93,7 +93,7 @@
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.7.7",
"@swc/core": "1.12.0",
"@twemoji/parser": "16.0.0",
"@twemoji/parser": "15.1.1",
"@types/redis-info": "3.0.3",
"accepts": "1.3.8",
"ajv": "8.17.1",
@@ -135,7 +135,7 @@
"jsrsasign": "11.1.0",
"juice": "11.0.1",
"meilisearch": "0.51.0",
"mfm-js": "0.25.0",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.3",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
-12
View File
@@ -16,13 +16,11 @@ import {
RelationshipJobData,
UserWebhookDeliverJobData,
SystemWebhookDeliverJobData,
DeleteUserMutingsJobData,
} from '../queue/types.js';
import type { Provider } from '@nestjs/common';
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
export type DeleteUserMutingQueue = Bull.Queue<DeleteUserMutingsJobData>;
export type DeliverQueue = Bull.Queue<DeliverJobData>;
export type InboxQueue = Bull.Queue<InboxJobData>;
export type DbQueue = Bull.Queue;
@@ -43,12 +41,6 @@ const $endedPollNotification: Provider = {
inject: [DI.config],
};
const $deleteUserMuting: Provider = {
provide: 'queue:deleteUserMuting',
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELETE_USER_MUTING, baseQueueOptions(config, QUEUE.DELETE_USER_MUTING)),
inject: [DI.config],
};
const $deliver: Provider = {
provide: 'queue:deliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
@@ -97,7 +89,6 @@ const $systemWebhookDeliver: Provider = {
providers: [
$system,
$endedPollNotification,
$deleteUserMuting,
$deliver,
$inbox,
$db,
@@ -109,7 +100,6 @@ const $systemWebhookDeliver: Provider = {
exports: [
$system,
$endedPollNotification,
$deleteUserMuting,
$deliver,
$inbox,
$db,
@@ -123,7 +113,6 @@ export class QueueModule implements OnApplicationShutdown {
constructor(
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:deleteUserMuting') public deleteUserMutingQueue: DeleteUserMutingQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@@ -140,7 +129,6 @@ export class QueueModule implements OnApplicationShutdown {
await Promise.all([
this.systemQueue.close(),
this.endedPollNotificationQueue.close(),
this.deleteUserMutingQueue.close(),
this.deliverQueue.close(),
this.inboxQueue.close(),
this.dbQueue.close(),
+5 -7
View File
@@ -31,7 +31,6 @@ import type {
DbQueue,
DeliverQueue,
EndedPollNotificationQueue,
DeleteUserMutingQueue,
InboxQueue,
ObjectStorageQueue,
RelationshipQueue,
@@ -45,7 +44,6 @@ import type * as Bull from 'bullmq';
export const QUEUE_TYPES = [
'system',
'endedPollNotification',
'deleteUserMuting',
'deliver',
'inbox',
'db',
@@ -72,16 +70,18 @@ const REPEATABLE_SYSTEM_JOB_DEF = [{
pattern: '0 0 * * *',
}, {
name: 'checkExpiredMutings',
pattern: '0 */3 * * *', // 3時間ごと
pattern: '*/5 * * * *',
}, {
name: 'bakeBufferedReactions',
pattern: '0 0 * * *',
}, {
name: 'checkModeratorsActivity',
pattern: '30 * * * *', // 毎時30分に起動
// 毎時30分に起動
pattern: '30 * * * *',
}, {
name: 'cleanRemoteNotes',
pattern: '0 4 * * *', // 毎日午前4時に起動(最も人の少ない時間帯)
// 毎日午前4時に起動(最も人の少ない時間帯)
pattern: '0 4 * * *',
}];
@Injectable()
@@ -92,7 +92,6 @@ export class QueueService {
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:deleteUserMutingQueue') public deleteUserMutingQueue: DeleteUserMutingQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@@ -717,7 +716,6 @@ export class QueueService {
switch (type) {
case 'system': return this.systemQueue;
case 'endedPollNotification': return this.endedPollNotificationQueue;
case 'deleteUserMuting': return this.deleteUserMutingQueue;
case 'deliver': return this.deliverQueue;
case 'inbox': return this.inboxQueue;
case 'db': return this.dbQueue;
+2 -2
View File
@@ -227,9 +227,9 @@ export class SearchService {
if (opts.host) {
if (opts.host === '.') {
query.andWhere('note.userHost IS NULL');
query.andWhere('user.host IS NULL');
} else {
query.andWhere('note.userHost = :host', { host: opts.host });
query.andWhere('user.host = :host', { host: opts.host });
}
}
+1 -41
View File
@@ -11,7 +11,6 @@ import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { QueueService } from '@/core/QueueService.js';
@Injectable()
export class UserMutingService {
@@ -21,13 +20,12 @@ export class UserMutingService {
private idService: IdService,
private cacheService: CacheService,
private queueService: QueueService,
) {
}
@bindThis
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
const inserted = await this.mutingsRepository.insertOne({
await this.mutingsRepository.insert({
id: this.idService.gen(),
expiresAt: expiresAt ?? null,
muterId: user.id,
@@ -35,23 +33,6 @@ export class UserMutingService {
});
this.cacheService.userMutingsCache.refresh(user.id);
if (expiresAt != null) {
const delay = expiresAt.getTime() - Date.now();
this.queueService.deleteUserMutingQueue.add(inserted.id, {
mutingId: inserted.id,
}, {
delay,
removeOnComplete: {
age: 3600 * 24 * 7, // keep up to 7 days
count: 30,
},
removeOnFail: {
age: 3600 * 24 * 7, // keep up to 7 days
count: 100,
},
});
}
}
@bindThis
@@ -67,25 +48,4 @@ export class UserMutingService {
this.cacheService.userMutingsCache.refresh(muterId);
}
}
@bindThis
public async deleteExpired(): Promise<void> {
const expired = await this.mutingsRepository.createQueryBuilder('muting')
.where('muting.expiresAt IS NOT NULL')
.andWhere('muting.expiresAt < :now', { now: new Date() })
.innerJoinAndSelect('muting.mutee', 'mutee')
.getMany();
if (expired.length > 0) {
await this.unmute(expired);
}
}
@bindThis
public async unmuteById(mutingId: MiMuting['id']): Promise<void> {
const muting = await this.mutingsRepository.findOneBy({ id: mutingId });
if (muting == null) return;
await this.unmute([muting]);
}
}
File diff suppressed because one or more lines are too long
@@ -14,7 +14,6 @@ import { CheckModeratorsActivityProcessorService } from '@/queue/processors/Chec
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { DeleteUserMutingsProcessorService } from './processors/DeleteUserMutingsProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
@@ -86,7 +85,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
private relationshipQueueWorker: Bull.Worker;
private objectStorageQueueWorker: Bull.Worker;
private endedPollNotificationQueueWorker: Bull.Worker;
private deleteUserMutingQueueWorker: Bull.Worker;
constructor(
@Inject(DI.config)
@@ -96,7 +94,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
private deleteUserMutingsProcessorService: DeleteUserMutingsProcessorService,
private deliverProcessorService: DeliverProcessorService,
private inboxProcessorService: InboxProcessorService,
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
@@ -523,21 +520,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
});
}
//#endregion
//#region delete user muting
{
this.deleteUserMutingQueueWorker = new Bull.Worker(QUEUE.DELETE_USER_MUTING, (job) => {
if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: DeleteUserMutings' }, () => this.deleteUserMutingsProcessorService.process(job));
} else {
return this.deleteUserMutingsProcessorService.process(job);
}
}, {
...baseWorkerOptions(this.config, QUEUE.DELETE_USER_MUTING),
autorun: false,
});
}
//#endregion
}
@bindThis
@@ -552,7 +534,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker.run(),
this.objectStorageQueueWorker.run(),
this.endedPollNotificationQueueWorker.run(),
this.deleteUserMutingQueueWorker.run(),
]);
}
@@ -568,7 +549,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker.close(),
this.objectStorageQueueWorker.close(),
this.endedPollNotificationQueueWorker.close(),
this.deleteUserMutingQueueWorker.close(),
]);
}
-1
View File
@@ -12,7 +12,6 @@ export const QUEUE = {
INBOX: 'inbox',
SYSTEM: 'system',
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
DELETE_USER_MUTING: 'deleteUserMuting',
DB: 'db',
RELATIONSHIP: 'relationship',
OBJECT_STORAGE: 'objectStorage',
@@ -3,17 +3,24 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MutingsRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@Injectable()
export class CheckExpiredMutingsProcessorService {
private logger: Logger;
constructor(
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private userMutingService: UserMutingService,
private queueLoggerService: QueueLoggerService,
) {
@@ -24,7 +31,15 @@ export class CheckExpiredMutingsProcessorService {
public async process(): Promise<void> {
this.logger.info('Checking expired mutings...');
await this.userMutingService.deleteExpired();
const expired = await this.mutingsRepository.createQueryBuilder('muting')
.where('muting.expiresAt IS NOT NULL')
.andWhere('muting.expiresAt < :now', { now: new Date() })
.innerJoinAndSelect('muting.mutee', 'mutee')
.getMany();
if (expired.length > 0) {
await this.userMutingService.unmute(expired);
}
this.logger.succ('All expired mutings checked.');
}
@@ -1,29 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DeleteUserMutingsJobData } from '../types.js';
@Injectable()
export class DeleteUserMutingsProcessorService {
private logger: Logger;
constructor(
private userMutingService: UserMutingService,
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('delete-user-mutings');
}
@bindThis
public async process(job: Bull.Job<DeleteUserMutingsJobData>): Promise<void> {
await this.userMutingService.unmuteById(job.data.mutingId);
}
}
-4
View File
@@ -109,10 +109,6 @@ export type EndedPollNotificationJobData = {
noteId: MiNote['id'];
};
export type DeleteUserMutingsJobData = {
mutingId: string;
};
export type SystemWebhookDeliverJobData<T extends SystemWebhookEventType = SystemWebhookEventType> = {
type: T;
content: SystemWebhookPayload<T>;
@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, DeleteUserMutingQueue } from '@/core/QueueModule.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
export const meta = {
tags: ['admin'],
@@ -49,7 +49,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:deleteUserMuting') public deleteUserMutingQueue: DeleteUserMutingQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@@ -20,6 +20,17 @@ import type { Config } from '@/config.js';
import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import type {
DbQueue,
DeliverQueue,
EndedPollNotificationQueue,
InboxQueue,
ObjectStorageQueue,
RelationshipQueue,
SystemQueue,
UserWebhookDeliverQueue,
SystemWebhookDeliverQueue,
} from '@/core/QueueModule.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
@@ -118,6 +129,16 @@ export class ClientServerService {
private feedService: FeedService,
private roleService: RoleService,
private clientLoggerService: ClientLoggerService,
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
//this.createServer = this.createServer.bind(this);
}
@@ -167,10 +188,6 @@ export class ClientServerService {
'url': 'url',
},
},
'shortcuts': [{
'name': 'Safemode',
'url': '/?safemode=true',
}],
};
manifest = {
+15 -41
View File
@@ -94,37 +94,23 @@
}
//#endregion
let isSafeMode = (localStorage.getItem('isSafeMode') === 'true');
if (!isSafeMode) {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('safemode') && urlParams.get('safemode') === 'true') {
localStorage.setItem('isSafeMode', 'true');
isSafeMode = true;
}
}
//#region Theme
if (!isSafeMode) {
const theme = localStorage.getItem('theme');
if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) {
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
const theme = localStorage.getItem('theme');
if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) {
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
// HTMLの theme-color 適用
if (k === 'htmlThemeColor') {
for (const tag of document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', v);
break;
}
// HTMLの theme-color 適用
if (k === 'htmlThemeColor') {
for (const tag of document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', v);
break;
}
}
}
}
}
const colorScheme = localStorage.getItem('colorScheme');
if (colorScheme) {
document.documentElement.style.setProperty('color-scheme', colorScheme);
@@ -141,13 +127,11 @@
document.documentElement.classList.add('useSystemFont');
}
if (!isSafeMode) {
const customCss = localStorage.getItem('customCss');
if (customCss && customCss.length > 0) {
const style = document.createElement('style');
style.innerHTML = customCss;
document.head.appendChild(style);
}
const customCss = localStorage.getItem('customCss');
if (customCss && customCss.length > 0) {
const style = document.createElement('style');
style.innerHTML = customCss;
document.head.appendChild(style);
}
async function addStyle(styleText) {
@@ -175,13 +159,9 @@
otherOption1: 'Clear preferences and cache',
otherOption2: 'Start the simple client',
otherOption3: 'Start the repair tool',
otherOption4: 'Start Misskey in safe mode',
}, locale?._bootErrors || {});
const reload = locale?.reload || 'Reload';
const safeModeUrl = new URL(window.location.href);
safeModeUrl.searchParams.set('safemode', 'true');
let errorsElement = document.getElementById('errors');
if (!errorsElement) {
@@ -202,12 +182,6 @@
<p>${messages.solution4}</p>
<details style="color: #86b300;">
<summary>${messages.otherOption}</summary>
<a href="${safeModeUrl}">
<button class="button-small">
<span class="button-label-small">${messages.otherOption4}</span>
</button>
</a>
<br>
<a href="/flush">
<button class="button-small">
<span class="button-label-small">${messages.otherOption1}</span>
@@ -34,11 +34,5 @@
"text": "text",
"url": "url"
}
},
"shortcuts": [
{
"name": "Safemode",
"url": "/?safemode=true"
}
]
}
}
@@ -79,9 +79,6 @@ async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse
rateLimitFactor: 0 as never,
},
}, res.token);
await client.request('admin/update-meta', {
federation: 'all',
}, res.token);
return res;
}).catch(err => {
if (err.info.e.message === 'access denied') return undefined;
-1
View File
@@ -24,7 +24,6 @@ describe('Endpoints', () => {
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
dave = await signup({ username: 'dave' });
await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse);
}, 1000 * 60 * 2);
describe('signup', () => {
+1 -2
View File
@@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { api, channel, clip, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js';
import { channel, clip, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js';
import type { SimpleGetResponse } from '../utils.js';
import type * as misskey from 'misskey-js';
@@ -78,7 +78,6 @@ describe('Webリソース', () => {
beforeAll(async () => {
alice = await signup({ username: 'alice' });
await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse);
aliceUploadedFile = (await uploadFile(alice)).body;
alicesPost = await post(alice, {
text: 'test',
@@ -16,7 +16,6 @@ describe('FF visibility', () => {
beforeAll(async () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse);
}, 1000 * 60 * 2);
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
+1 -2
View File
@@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { api, host, origin, relativeFetch, signup } from '../utils.js';
import { host, origin, relativeFetch, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('.well-known', () => {
@@ -14,7 +14,6 @@ describe('.well-known', () => {
beforeAll(async () => {
alice = await signup({ username: 'alice' });
await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse);
}, 1000 * 60 * 2);
test('nodeinfo', async () => {
+21 -21
View File
@@ -10,53 +10,53 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"@discordapp/twemoji": "16.0.1",
"@discordapp/twemoji": "15.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.2.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.18",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.4",
"@vue/compiler-sfc": "3.5.17",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
"icons-subsetter": "workspace:*",
"frontend-shared": "workspace:*",
"json5": "2.2.3",
"mfm-js": "0.25.0",
"mfm-js": "0.24.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.46.2",
"rollup": "4.45.1",
"sass": "1.89.2",
"shiki": "3.9.1",
"shiki": "3.8.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"typescript": "5.8.3",
"uuid": "11.1.0",
"vite": "7.0.6",
"vue": "3.5.18"
"vite": "6.3.5",
"vue": "3.5.17"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.3",
"@tabler/icons-webfont": "3.34.1",
"@misskey-dev/summaly": "5.2.2",
"@tabler/icons-webfont": "3.34.0",
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.17.0",
"@types/node": "22.16.4",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/runtime-core": "3.5.18",
"@vue/runtime-core": "3.5.17",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.4.0",
"eslint-plugin-vue": "10.3.0",
"fast-glob": "3.3.3",
"happy-dom": "18.0.1",
"happy-dom": "17.6.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.10.4",
@@ -64,8 +64,8 @@
"prettier": "3.6.2",
"start-server-and-test": "2.0.12",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.0.5",
"vue-component-type-helpers": "2.2.12",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.5"
"vue-tsc": "2.2.12"
}
}
-1
View File
@@ -23,7 +23,6 @@ export const version = _VERSION_;
export const instanceName = (siteName === 'Misskey' || siteName == null) ? host : siteName;
export const ui = localStorage.getItem('ui');
export const debug = localStorage.getItem('debug') === 'true';
export const isSafeMode = localStorage.getItem('isSafeMode') === 'true';
export function updateLocale(newLocale: Locale): void {
locale = newLocale;
+32 -136
View File
@@ -44,8 +44,6 @@
["😑", "expressionless", 0],
["😒", "unamused", 0],
["🙄", "roll_eyes", 0],
["🙂‍↔️", "head_shaking_horizontally", 0],
["🙂‍↕️", "head_shaking_vertically", 0],
["🤔", "thinking", 0],
["🤥", "lying_face", 0],
["🤭", "hand_over_mouth", 0],
@@ -78,7 +76,6 @@
["😥", "disappointed_relieved", 0],
["🤤", "drooling_face", 0],
["😪", "sleepy", 0],
["🫩", "face_with_bags_under_eyes", 0],
["😓", "sweat", 0],
["🥵", "hot", 0],
["🥶", "cold", 0],
@@ -95,7 +92,6 @@
["🥴", "woozy", 0],
["🥱", "yawning", 0],
["😴", "sleeping", 0],
["👁️‍🗨️", "eye_in_speech_bubble", 0],
["💤", "zzz", 0],
["😶‍🌫️", "face_in_clouds", 0],
["😮‍💨", "face_exhaling", 0],
@@ -189,7 +185,6 @@
["👤", "bust_in_silhouette", 1],
["👥", "busts_in_silhouette", 1],
["🗣", "speaking_head", 1],
["🫆", "fingerprint", 1],
["👶", "baby", 1],
["🧒", "child", 1],
["👦", "boy", 1],
@@ -203,9 +198,8 @@
["🧑‍🦰", "red_hair", 1],
["👩‍🦰", "red_hair_woman", 1],
["👨‍🦰", "red_hair_man", 1],
["👱", "blonde_person", 1],
["👱‍♀️", "blonde_woman", 1],
["👱‍♂️", "blonde_man", 1],
["👱", "blonde_man", 1],
["🧑‍🦳", "white_hair", 1],
["👩‍🦳", "white_hair_woman", 1],
["👨‍🦳", "white_hair_man", 1],
@@ -213,28 +207,21 @@
["👩‍🦲", "bald_woman", 1],
["👨‍🦲", "bald_man", 1],
["🧔", "bearded_person", 1],
["🧔‍♀️", "beard_woman", 1],
["🧔‍♂️", "beard_man", 1],
["🧓", "older_adult", 1],
["👴", "older_man", 1],
["👵", "older_woman", 1],
["👲", "man_with_gua_pi_mao", 1],
["🧕", "woman_with_headscarf", 1],
["👳", "person_with_turban", 1],
["👳‍♀️", "woman_with_turban", 1],
["👳‍♂️", "man_with_turban", 1],
["👮", "police_officer", 1],
["👳", "man_with_turban", 1],
["👮‍♀️", "policewoman", 1],
["👮‍♂️", "policeman", 1],
["👷", "construction_worker", 1],
["👮", "policeman", 1],
["👷‍♀️", "construction_worker_woman", 1],
["👷‍♂️", "construction_worker_man", 1],
["💂", "guard", 1],
["👷", "construction_worker_man", 1],
["💂‍♀️", "guardswoman", 1],
["💂‍♂️", "guardsman", 1],
["🕵", "detective", 1],
["💂", "guardsman", 1],
["🕵️‍♀️", "female_detective", 1],
["🕵️‍♂️", "male_detective", 1],
["🕵", "male_detective", 1],
["🧑‍⚕️", "health_worker", 1],
["👩‍⚕️", "woman_health_worker", 1],
["👨‍⚕️", "man_health_worker", 1],
@@ -283,35 +270,26 @@
["🧑‍⚖️", "judge", 1],
["👩‍⚖️", "woman_judge", 1],
["👨‍⚖️", "man_judge", 1],
["🦸", "superhero", 1],
["🦸‍♀️", "woman_superhero", 1],
["🦸‍♂️", "man_superhero", 1],
["🦹", "supervillain", 1],
["🦹‍♀️", "woman_supervillain", 1],
["🦹‍♂️", "man_supervillain", 1],
["🤶", "mrs_claus", 1],
["🧑‍🎄", "mx_claus", 1],
["🎅", "santa", 1],
["🥷", "ninja", 1],
["🧙", "mage", 1],
["🧙‍♀️", "sorceress", 1],
["🧙‍♂️", "wizard", 1],
["🧝", "elf", 1],
["🧝‍♀️", "woman_elf", 1],
["🧝‍♂️", "man_elf", 1],
["🧛", "vampire", 1],
["🧛‍♀️", "woman_vampire", 1],
["🧛‍♂️", "man_vampire", 1],
["🧟", "zombie", 1],
["🧟‍♀️", "woman_zombie", 1],
["🧟‍♂️", "man_zombie", 1],
["🧞", "genie", 1],
["🧞‍♀️", "woman_genie", 1],
["🧞‍♂️", "man_genie", 1],
["🧜", "merperson", 1],
["🧜‍♀️", "mermaid", 1],
["🧜‍♂️", "merman", 1],
["🧚", "fairy", 1],
["🧚‍♀️", "woman_fairy", 1],
["🧚‍♂️", "man_fairy", 1],
["👼", "angel", 1],
@@ -327,108 +305,68 @@
["👸", "princess", 1],
["🤴", "prince", 1],
["👰", "person_with_veil", 1],
["👰‍♀️", "bride_with_veil", 1],
["👰‍♂️", "man_with_veil", 1],
["👰", "bride_with_veil", 1],
["🤵", "person_in_tuxedo", 1],
["🤵‍♀️", "woman_in_tuxedo", 1],
["🤵‍♂️", "man_in_tuxedo", 1],
["🏃", "running_person", 1],
["🏃‍➡️", "running_person_facing_right", 1],
["🤵", "man_in_tuxedo", 1],
["🏃‍♀️", "running_woman", 1],
["🏃‍♀️‍➡️", "running_woman_facing_right", 1],
["🏃‍♂️", "running_man", 1],
["🏃‍♂️‍➡️", "running_man_facing_right", 1],
["🚶", "walking_person", 1],
["🚶‍➡️", "walking_person_facing_right", 1],
["🏃", "running_man", 1],
["🚶‍♀️", "walking_woman", 1],
["🚶‍♀️‍➡️", "walking_woman_facing_right", 1],
["🚶‍♂️", "walking_man", 1],
["🚶‍♂️‍➡️", "walking_man_facing_right", 1],
["🚶", "walking_man", 1],
["💃", "dancer", 1],
["🕺", "man_dancing", 1],
["👯", "dancing_people", 1],
["👯‍♀️", "dancing_women", 1],
["👯", "dancing_women", 1],
["👯‍♂️", "dancing_men", 1],
["👫", "couple", 1],
["🧑‍🤝‍🧑", "people_holding_hands", 1],
["👬", "two_men_holding_hands", 1],
["👭", "two_women_holding_hands", 1],
["🫂", "people_hugging", 1],
["🙇", "bowing_person", 1],
["🙇‍♀️", "bowing_woman", 1],
["🙇‍♂️", "bowing_man", 1],
["🤦", "person_facepalming", 1],
["🙇", "bowing_man", 1],
["🤦‍♂️", "man_facepalming", 1],
["🤦‍♀️", "woman_facepalming", 1],
["🤷", "person_shrugging", 1],
["🤷‍♀️", "woman_shrugging", 1],
["🤷", "woman_shrugging", 1],
["🤷‍♂️", "man_shrugging", 1],
["💁", "tipping_hand_person", 1],
["💁‍♀️", "tipping_hand_woman", 1],
["💁", "tipping_hand_woman", 1],
["💁‍♂️", "tipping_hand_man", 1],
["🙅", "no_good_person", 1],
["🙅‍♀️", "no_good_woman", 1],
["🙅", "no_good_woman", 1],
["🙅‍♂️", "no_good_man", 1],
["🙆", "ok_person", 1],
["🙆‍♀️", "ok_woman", 1],
["🙆", "ok_woman", 1],
["🙆‍♂️", "ok_man", 1],
["🙋", "raising_hand_person", 1],
["🙋‍♀️", "raising_hand_woman", 1],
["🙋", "raising_hand_woman", 1],
["🙋‍♂️", "raising_hand_man", 1],
["🙎", "pouting_person", 1],
["🙎‍♀️", "pouting_woman", 1],
["🙎", "pouting_woman", 1],
["🙎‍♂️", "pouting_man", 1],
["🙍", "frowning_person", 1],
["🙍‍♀️", "frowning_woman", 1],
["🙍", "frowning_woman", 1],
["🙍‍♂️", "frowning_man", 1],
["💇", "haircut_person", 1],
["💇‍♀️", "haircut_woman", 1],
["💇", "haircut_woman", 1],
["💇‍♂️", "haircut_man", 1],
["💆", "massage_person", 1],
["💆‍♀️", "massage_woman", 1],
["💆", "massage_woman", 1],
["💆‍♂️", "massage_man", 1],
["🧖", "person_in_steamy_room", 1],
["🧖‍♀️", "woman_in_steamy_room", 1],
["🧖‍♂️", "man_in_steamy_room", 1],
["🧏", "person_deaf", 1],
["🧏‍♀️", "woman_deaf", 1],
["🧏‍♂️", "man_deaf", 1],
["🧍", "person_standing", 1],
["🧍‍♀️", "woman_standing", 1],
["🧍‍♂️", "man_standing", 1],
["🧎", "person_kneeling", 1],
["🧎‍➡️", "person_kneeling_facing_right", 1],
["🧎‍♀️", "woman_kneeling", 1],
["🧎‍♀️‍➡️", "woman_kneeling_facing_right", 1],
["🧎‍♂️", "man_kneeling", 1],
["🧎‍♂️‍➡️", "man_kneeling_facing_right", 1],
["🧑‍🦯", "person_with_probing_cane", 1],
["🧑‍🦯‍➡️", "person_with_probing_cane_facing_right", 1],
["👩‍🦯", "woman_with_probing_cane", 1],
["👩‍🦯‍➡️", "woman_with_probing_cane_facing_right", 1],
["👨‍🦯", "man_with_probing_cane", 1],
["👨‍🦯‍➡️", "man_with_probing_cane_facing_right", 1],
["🧑‍🦼", "person_in_motorized_wheelchair", 1],
["🧑‍🦼‍➡️", "person_in_motorized_wheelchair_facing_right", 1],
["👩‍🦼", "woman_in_motorized_wheelchair", 1],
["👩‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right", 1],
["👨‍🦼", "man_in_motorized_wheelchair", 1],
["👨‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right", 1],
["🧑‍🦽", "person_in_manual_wheelchair", 1],
["🧑‍🦽‍➡️", "person_in_manual_wheelchair_facing_right", 1],
["👩‍🦽", "woman_in_manual_wheelchair", 1],
["👩‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right", 1],
["👨‍🦽", "man_in_manual_wheelchair", 1],
["👨‍🦽‍➡️", "man_in_manual_wheelchair_facing_right", 1],
["💑", "couple_with_heart", 1],
["👩‍❤️‍👨", "couple_with_heart_woman_man", 1],
["💑", "couple_with_heart_woman_man", 1],
["👩‍❤️‍👩", "couple_with_heart_woman_woman", 1],
["👨‍❤️‍👨", "couple_with_heart_man_man", 1],
["💏", "couplekiss", 1],
["👩‍❤️‍💋‍👨", "couplekiss_woman_man", 1],
["💏", "couplekiss_man_woman", 1],
["👩‍❤️‍💋‍👩", "couplekiss_woman_woman", 1],
["👨‍❤️‍💋‍👨", "couplekiss_man_man", 1],
["👨‍👩‍👦", "family_man_woman_boy", 1],
["👪", "family_man_woman_boy", 1],
["👨‍👩‍👧", "family_man_woman_girl", 1],
["👨‍👩‍👧‍👦", "family_man_woman_girl_boy", 1],
["👨‍👩‍👦‍👦", "family_man_woman_boy_boy", 1],
@@ -453,11 +391,6 @@
["👨‍👧‍👦", "family_man_girl_boy", 1],
["👨‍👦‍👦", "family_man_boy_boy", 1],
["👨‍👧‍👧", "family_man_girl_girl", 1],
["👪", "family", 1],
["🧑‍🧑‍🧒", "family_adult_adult_child", 1],
["🧑‍🧑‍🧒‍🧒", "family_adult_adult_child_child", 1],
["🧑‍🧒", "family_adult_child", 1],
["🧑‍🧒‍🧒", "family_adult_child_child", 1],
["🧶", "yarn", 1],
["🧵", "thread", 1],
["🧥", "coat", 1],
@@ -542,7 +475,6 @@
["🐦‍⬛", "black_bird", 2],
["🦅", "eagle", 2],
["🦉", "owl", 2],
["🐦‍🔥", "phoenix", 2],
["🦇", "bat", 2],
["🐺", "wolf", 2],
["🐗", "boar", 2],
@@ -643,7 +575,6 @@
["🌿", "herb", 2],
["☘", "shamrock", 2],
["🍀", "four_leaf_clover", 2],
["🪾", "leafless_tree", 2],
["🎍", "bamboo", 2],
["🎋", "tanabata_tree", 2],
["🍃", "leaves", 2],
@@ -717,7 +648,6 @@
["🪸", "coral", 2],
["🪹", "empty_nest", 2],
["🪺", "nest_with_eggs", 2],
["🍋‍🟩", "lime", 3],
["🍏", "green_apple", 3],
["🍎", "apple", 3],
["🍐", "pear", 3],
@@ -737,8 +667,6 @@
["🥑", "avocado", 3],
["🫛", "pea_pod", 3],
["🥦", "broccoli", 3],
["🍄‍🟫", "brown_mushroom", 3],
["🫜", "root_vegetable", 3],
["🍅", "tomato", 3],
["🍆", "eggplant", 3],
["🥒", "cucumber", 3],
@@ -858,9 +786,8 @@
["🥏", "flying_disc", 4],
["🎱", "8ball", 4],
["⛳", "golf", 4],
["🏌", "golfing_person", 4],
["🏌️‍♀️", "golfing_woman", 4],
["🏌️‍♂️", "golfing_man", 4],
["🏌", "golfing_man", 4],
["🏓", "ping_pong", 4],
["🏸", "badminton", 4],
["🥅", "goal_net", 4],
@@ -872,13 +799,10 @@
["⛷", "skier", 4],
["🏂", "snowboarder", 4],
["🤺", "person_fencing", 4],
["🤼", "people_wrestling", 4],
["🤼‍♀️", "women_wrestling", 4],
["🤼‍♂️", "men_wrestling", 4],
["🤸", "person_cartwheeling", 4],
["🤸‍♀️", "woman_cartwheeling", 4],
["🤸‍♂️", "man_cartwheeling", 4],
["🤾", "person_playing_handball", 4],
["🤾‍♀️", "woman_playing_handball", 4],
["🤾‍♂️", "man_playing_handball", 4],
["⛸", "ice_skate", 4],
@@ -889,37 +813,27 @@
["🎣", "fishing_pole_and_fish", 4],
["🥊", "boxing_glove", 4],
["🥋", "martial_arts_uniform", 4],
["🚣", "rowing_person", 4],
["🚣‍♀️", "rowing_woman", 4],
["🚣‍♂️", "rowing_man", 4],
["🧗", "climbing_person", 4],
["🚣", "rowing_man", 4],
["🧗‍♀️", "climbing_woman", 4],
["🧗‍♂️", "climbing_man", 4],
["🏊", "swimming_person", 4],
["🏊‍♀️", "swimming_woman", 4],
["🏊‍♂️", "swimming_man", 4],
["🤽", "person_playing_water_polo", 4],
["🏊", "swimming_man", 4],
["🤽‍♀️", "woman_playing_water_polo", 4],
["🤽‍♂️", "man_playing_water_polo", 4],
["🧘", "person_in_lotus_position", 4],
["🧘‍♀️", "woman_in_lotus_position", 4],
["🧘‍♂️", "man_in_lotus_position", 4],
["🏄", "surfing_person", 4],
["🏄‍♀️", "surfing_woman", 4],
["🏄‍♂️", "surfing_man", 4],
["🏄", "surfing_man", 4],
["🛀", "bath", 4],
["⛹", "basketball_person", 4],
["⛹️‍♀️", "basketball_woman", 4],
["⛹️‍♂️", "basketball_man", 4],
["🏋", "weight_lifting_person", 4],
["⛹", "basketball_man", 4],
["🏋️‍♀️", "weight_lifting_woman", 4],
["🏋️‍♂️", "weight_lifting_man", 4],
["🚴", "biking_person", 4],
["🏋", "weight_lifting_man", 4],
["🚴‍♀️", "biking_woman", 4],
["🚴‍♂️", "biking_man", 4],
["🚵", "mountain_biking_person", 4],
["🚴", "biking_man", 4],
["🚵‍♀️", "mountain_biking_woman", 4],
["🚵‍♂️", "mountain_biking_man", 4],
["🚵", "mountain_biking_man", 4],
["🏇", "horse_racing", 4],
["🤿", "diving_mask", 4],
["🪀", "yo_yo", 4],
@@ -942,7 +856,6 @@
["🎭", "performing_arts", 4],
["🎨", "art", 4],
["🎪", "circus_tent", 4],
["🤹", "person_juggling", 4],
["🤹‍♀️", "woman_juggling", 4],
["🤹‍♂️", "man_juggling", 4],
["🎤", "microphone", 4],
@@ -959,7 +872,6 @@
["🪕", "banjo", 4],
["🪗", "accordion", 4],
["🪘", "long_drum", 4],
["🪉", "harp", 4],
["🎬", "clapper", 4],
["🎮", "video_game", 4],
["👾", "space_invader", 4],
@@ -1164,10 +1076,8 @@
["🪙", "coin", 6],
["💳", "credit_card", 6],
["🪪", "identification_card", 6],
["🥾", "hiking_boot", 6],
["💎", "gem", 6],
["⚖", "balance_scale", 6],
["⛓️‍💥", "broken_chain", 6],
["🧰", "toolbox", 6],
["🔧", "wrench", 6],
["🔨", "hammer", 6],
@@ -1183,7 +1093,6 @@
["🪛", "screwdriver", 6],
["🪝", "hook", 6],
["🪜", "ladder", 6],
["🪏", "shovel", 6],
["🧱", "brick", 6],
["⛓", "chains", 6],
["🧲", "magnet", 6],
@@ -1395,8 +1304,6 @@
["♓", "pisces", 7],
["🆔", "id", 7],
["⚛", "atom_symbol", 7],
["♀️", "female_sign", 7],
["♂️", "male_sign", 7],
["⚧️", "transgender_symbol", 7],
["🈳", "u7a7a", 7],
["🈹", "u5272", 7],
@@ -1556,11 +1463,9 @@
["♾", "infinity", 7],
["💲", "heavy_dollar_sign", 7],
["💱", "currency_exchange", 7],
["⚕️", "medical_symbol", 7],
["©️", "copyright", 7],
["®️", "registered", 7],
["™️", "tm", 7],
["🫟", "splatter", 7],
["🔚", "end", 7],
["🔙", "back", 7],
["🔛", "on", 7],
@@ -1671,7 +1576,6 @@
["🇧🇲", "bermuda", 8],
["🇧🇹", "bhutan", 8],
["🇧🇴", "bolivia", 8],
["🇧🇻", "bouvet_island", 8],
["🇧🇶", "caribbean_netherlands", 8],
["🇧🇦", "bosnia_herzegovina", 8],
["🇧🇼", "botswana", 8],
@@ -1689,12 +1593,10 @@
["🇮🇨", "canary_islands", 8],
["🇰🇾", "cayman_islands", 8],
["🇨🇫", "central_african_republic", 8],
["🇪🇦", "ceuta_melilla", 8],
["🇹🇩", "chad", 8],
["🇨🇱", "chile", 8],
["🇨🇳", "cn", 8],
["🇨🇽", "christmas_island", 8],
["🇨🇵", "clipperton_island", 8],
["🇨🇨", "cocos_islands", 8],
["🇨🇴", "colombia", 8],
["🇰🇲", "comoros", 8],
@@ -1708,7 +1610,6 @@
["🇨🇾", "cyprus", 8],
["🇨🇿", "czech_republic", 8],
["🇩🇰", "denmark", 8],
["🇩🇬", "diego_garcia", 8],
["🇩🇯", "djibouti", 8],
["🇩🇲", "dominica", 8],
["🇩🇴", "dominican_republic", 8],
@@ -1745,7 +1646,6 @@
["🇬🇼", "guinea_bissau", 8],
["🇬🇾", "guyana", 8],
["🇭🇹", "haiti", 8],
["🇭🇲", "heard_mcdonald_islands", 8],
["🇭🇳", "honduras", 8],
["🇭🇰", "hong_kong", 8],
["🇭🇺", "hungary", 8],
@@ -1833,12 +1733,10 @@
["🇷🇴", "romania", 8],
["🇷🇺", "ru", 8],
["🇷🇼", "rwanda", 8],
["🇨🇶", "sark", 8],
["🇧🇱", "st_barthelemy", 8],
["🇸🇭", "st_helena", 8],
["🇰🇳", "st_kitts_nevis", 8],
["🇱🇨", "st_lucia", 8],
["🇲🇫", "st_martin", 8],
["🇵🇲", "st_pierre_miquelon", 8],
["🇻🇨", "st_vincent_grenadines", 8],
["🇼🇸", "samoa", 8],
@@ -1864,7 +1762,6 @@
["🇸🇩", "sudan", 8],
["🇸🇷", "suriname", 8],
["🇸🇿", "swaziland", 8],
["🇸🇯", "svalbard_jan_mayen", 8],
["🇸🇪", "sweden", 8],
["🇨🇭", "switzerland", 8],
["🇸🇾", "syria", 8],
@@ -1891,7 +1788,6 @@
["🏴󠁧󠁢󠁳󠁣󠁴󠁿", "scotland", 8],
["🏴󠁧󠁢󠁷󠁬󠁳󠁿", "wales", 8],
["🇺🇸", "us", 8],
["🇺🇲", "us_outlying_islands", 8],
["🇻🇮", "us_virgin_islands", 8],
["🇺🇾", "uruguay", 8],
["🇺🇿", "uzbekistan", 8],
+7 -7
View File
@@ -21,13 +21,13 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.17.0",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"esbuild": "0.25.8",
"eslint-plugin-vue": "10.4.0",
"@types/node": "22.16.4",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"esbuild": "0.25.6",
"eslint-plugin-vue": "10.3.0",
"nodemon": "3.1.10",
"typescript": "5.9.2",
"typescript": "5.8.3",
"vue-eslint-parser": "10.2.0"
},
"files": [
@@ -35,6 +35,6 @@
],
"dependencies": {
"misskey-js": "workspace:*",
"vue": "3.5.18"
"vue": "3.5.17"
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ export const commonHandlers = [
}),
http.get('/twemoji/:codepoints.svg', async ({ params }) => {
const { codepoints } = params;
const value = await fetch(`https://unpkg.com/@discordapp/twemoji@16.0.1/dist/svg/${codepoints}.svg`).then((response) => response.blob());
const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob());
return new HttpResponse(value, {
headers: {
'Content-Type': 'image/svg+xml',
@@ -39,7 +39,6 @@ export interface SearchIndexItem {
path?: string;
label: string;
keywords: string[];
texts: string[];
icon?: string;
inlining?: string[];
}
@@ -228,14 +227,14 @@ function extractElementText2Inner(node: TemplateChildNode, processingNodeName: s
// region extractUsageInfoFromTemplateAst
/**
* SearchLabel/SearchText/SearchIconを探して抽出する関数
* SearchLabel/SearchKeyword/SearchIconを探して抽出する関数
*/
function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: string | null; texts: string[]; icon: string | null; } {
function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: string | null, keywords: string[], icon: string | null } {
let label: string | null | undefined = undefined;
let icon: string | null | undefined = undefined;
const texts: string[] = [];
const keywords: string[] = [];
logger.info(`Extracting labels and texts from ${nodes.length} nodes`);
logger.info(`Extracting labels and keywords from ${nodes.length} nodes`);
walkVueElements(nodes, null, (node) => {
switch (node.tag) {
@@ -249,10 +248,10 @@ function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: stri
label = extractElementText(node, id);
return;
case 'SearchText':
case 'SearchKeyword':
const content = extractElementText(node, id);
if (content) {
texts.push(content);
keywords.push(content);
}
return;
case 'SearchIcon':
@@ -279,8 +278,8 @@ function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: stri
});
// デバッグ情報
logger.info(`Extraction completed: label=${label}, text=[${texts.join(', ')}, icon=${icon}]`);
return { label: label ?? null, texts, icon: icon ?? null };
logger.info(`Extraction completed: label=${label}, keywords=[${keywords.join(', ')}, icon=${icon}]`);
return { label: label ?? null, keywords, icon: icon ?? null };
}
function getStringProp(attr: AttributeNode | DirectiveNode | null, id: string): string | null {
@@ -352,36 +351,33 @@ function extractUsageInfoFromTemplateAst(
parentId: parentId ?? undefined,
label: '', // デフォルト値
keywords: [],
texts: [],
};
// バインドプロパティを取得
const path = getStringProp(findAttribute(node.props, 'path'), id);
const icon = getStringProp(findAttribute(node.props, 'icon'), id);
const label = getStringProp(findAttribute(node.props, 'label'), id);
const inlining = getStringArrayProp(findAttribute(node.props, 'inlining'), id);
const keywords = getStringArrayProp(findAttribute(node.props, 'keywords'), id);
const texts = getStringArrayProp(findAttribute(node.props, 'texts'), id);
const path = getStringProp(findAttribute(node.props, 'path'), id)
const icon = getStringProp(findAttribute(node.props, 'icon'), id)
const label = getStringProp(findAttribute(node.props, 'label'), id)
const inlining = getStringArrayProp(findAttribute(node.props, 'inlining'), id)
const keywords = getStringArrayProp(findAttribute(node.props, 'keywords'), id)
if (path) markerInfo.path = path;
if (icon) markerInfo.icon = icon;
if (label) markerInfo.label = label;
if (inlining) markerInfo.inlining = inlining;
if (keywords) markerInfo.keywords = keywords;
if (texts) markerInfo.texts = texts;
// pathがない場合はファイルパスを設定
//pathがない場合はファイルパスを設定
if (markerInfo.path == null && parentId == null) {
markerInfo.path = id.match(/.*(\/(admin|settings)\/[^\/]+)\.vue$/)?.[1];
}
// SearchLabelとSearchTextを抽出 (AST全体を探索)
// SearchLabelとSearchKeywordを抽出 (AST全体を探索)
{
const extracted = extractSugarTags(node.children, id);
if (extracted.label && markerInfo.label) logger.warn(`Duplicate label found for ${markerId} at ${id}:${node.loc.start.line}`);
if (extracted.icon && markerInfo.icon) logger.warn(`Duplicate icon found for ${markerId} at ${id}:${node.loc.start.line}`);
markerInfo.label = extracted.label ?? markerInfo.label ?? '';
markerInfo.texts = [...extracted.texts, ...markerInfo.texts];
markerInfo.keywords = [...extracted.keywords, ...markerInfo.keywords];
markerInfo.icon = extracted.icon ?? markerInfo.icon ?? undefined;
}
+37 -37
View File
@@ -17,18 +17,18 @@
},
"dependencies": {
"@analytics/google-analytics": "1.1.0",
"@discordapp/twemoji": "16.0.1",
"@discordapp/twemoji": "15.1.0",
"@github/webauthn-json": "2.1.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.2.0",
"@sentry/vue": "10.0.0",
"@sentry/vue": "9.39.0",
"@syuilo/aiscript": "0.19.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.18",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.4",
"@vue/compiler-sfc": "3.5.17",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.16",
"astring": "1.9.0",
@@ -37,12 +37,12 @@
"canvas-confetti": "1.9.3",
"chart.js": "4.5.0",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "3.0.0",
"chartjs-chart-matrix": "2.1.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "13.1.3",
"chromatic": "11.29.0",
"compare-versions": "6.1.1",
"cropperjs": "2.0.1",
"cropperjs": "2.0.0",
"date-fns": "4.1.0",
"estree-walker": "3.0.3",
"eventemitter3": "5.0.1",
@@ -54,36 +54,36 @@
"json5": "2.2.3",
"magic-string": "0.30.17",
"matter-js": "0.20.0",
"mfm-js": "0.25.0",
"mfm-js": "0.24.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode.js": "2.3.1",
"rollup": "4.46.2",
"rollup": "4.45.1",
"sanitize-html": "2.17.0",
"sass": "1.89.2",
"shiki": "3.9.1",
"shiki": "3.8.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.179.1",
"three": "0.178.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"typescript": "5.8.3",
"v-code-diff": "1.13.1",
"vite": "7.0.6",
"vue": "3.5.18",
"vite": "6.3.5",
"vue": "3.5.17",
"vuedraggable": "next",
"wanakana": "5.3.1"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.3",
"@storybook/addon-actions": "9.0.8",
"@misskey-dev/summaly": "5.2.2",
"@storybook/addon-actions": "8.6.14",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
"@storybook/addon-links": "9.1.0",
"@storybook/addon-links": "8.6.14",
"@storybook/addon-mdx-gfm": "8.6.14",
"@storybook/addon-storysource": "8.6.14",
"@storybook/blocks": "8.6.14",
@@ -91,38 +91,38 @@
"@storybook/core-events": "8.6.14",
"@storybook/manager-api": "8.6.14",
"@storybook/preview-api": "8.6.14",
"@storybook/react": "9.1.0",
"@storybook/react-vite": "9.1.0",
"@storybook/react": "8.6.14",
"@storybook/react-vite": "8.6.14",
"@storybook/test": "8.6.14",
"@storybook/theming": "8.6.14",
"@storybook/types": "8.6.14",
"@storybook/vue3": "9.1.0",
"@storybook/vue3-vite": "9.1.0",
"@tabler/icons-webfont": "3.34.1",
"@storybook/vue3": "8.6.14",
"@storybook/vue3-vite": "8.6.14",
"@tabler/icons-webfont": "3.34.0",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8",
"@types/matter-js": "0.19.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.17.0",
"@types/node": "22.16.4",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/compiler-core": "3.5.18",
"@vue/runtime-core": "3.5.18",
"@vue/compiler-core": "3.5.17",
"@vue/runtime-core": "3.5.17",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"cypress": "14.5.3",
"cross-env": "7.0.3",
"cypress": "14.5.2",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.4.0",
"eslint-plugin-vue": "10.3.0",
"fast-glob": "3.3.3",
"happy-dom": "18.0.1",
"happy-dom": "17.6.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.0.3",
@@ -130,17 +130,17 @@
"msw-storybook-addon": "2.0.5",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"seedrandom": "3.0.5",
"start-server-and-test": "2.0.12",
"storybook": "9.1.0",
"storybook": "8.6.14",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.0.5",
"vue-component-type-helpers": "2.2.12",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.5"
"vue-tsc": "2.2.12"
}
}
+28 -37
View File
@@ -5,7 +5,7 @@
import { computed, watch, version as vueVersion } from 'vue';
import { compareVersions } from 'compare-versions';
import { version, lang, updateLocale, locale, apiUrl, isSafeMode } from '@@/js/config.js';
import { version, lang, updateLocale, locale, apiUrl } from '@@/js/config.js';
import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import type { App } from 'vue';
@@ -168,35 +168,28 @@ export async function common(createVue: () => Promise<App<Element>>) {
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
watch(store.r.darkMode, (darkMode) => {
const theme = (() => {
if (darkMode) {
return isSafeMode ? defaultDarkTheme : (prefer.s.darkTheme ?? defaultDarkTheme);
} else {
return isSafeMode ? defaultLightTheme : (prefer.s.lightTheme ?? defaultLightTheme);
}
})();
applyTheme(theme);
}, { immediate: isSafeMode || miLocalStorage.getItem('theme') == null });
applyTheme(darkMode
? (prefer.s.darkTheme ?? defaultDarkTheme)
: (prefer.s.lightTheme ?? defaultLightTheme),
);
}, { immediate: miLocalStorage.getItem('theme') == null });
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
if (!isSafeMode) {
const darkTheme = prefer.model('darkTheme');
const lightTheme = prefer.model('lightTheme');
const darkTheme = prefer.model('darkTheme');
const lightTheme = prefer.model('lightTheme');
watch(darkTheme, (theme) => {
if (store.s.darkMode) {
applyTheme(theme ?? defaultDarkTheme);
}
});
watch(darkTheme, (theme) => {
if (store.s.darkMode) {
applyTheme(theme ?? defaultDarkTheme);
}
});
watch(lightTheme, (theme) => {
if (!store.s.darkMode) {
applyTheme(theme ?? defaultLightTheme);
}
});
}
watch(lightTheme, (theme) => {
if (!store.s.darkMode) {
applyTheme(theme ?? defaultLightTheme);
}
});
//#region Sync dark mode
if (prefer.s.syncDeviceDarkMode) {
@@ -210,20 +203,18 @@ export async function common(createVue: () => Promise<App<Element>>) {
});
//#endregion
if (!isSafeMode) {
if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
} else if (prefer.s.lightTheme && !store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
}
fetchInstanceMetaPromise.then(() => {
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
});
if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
} else if (prefer.s.lightTheme && !store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
}
fetchInstanceMetaPromise.then(() => {
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
});
watch(prefer.r.overridedDeviceKind, (kind) => {
updateDeviceKind(kind);
}, { immediate: true });
+1 -21
View File
@@ -28,8 +28,8 @@ import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom
import { prefer } from '@/preferences.js';
import { launchPlugins } from '@/plugin.js';
import { updateCurrentAccountPartial } from '@/accounts.js';
import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js';
import { unisonReload } from '@/utility/unison-reload.js';
export async function mainBoot() {
const { isClientUpdated, lastVersion } = await common(async () => {
@@ -391,8 +391,6 @@ export async function mainBoot() {
}
// shortcut
let safemodeRequestCount = 0;
let safemodeRequestTimer: number | null = null;
const keymap = {
'p|n': () => {
if ($i == null) return;
@@ -404,24 +402,6 @@ export async function mainBoot() {
's': () => {
mainRouter.push('/search');
},
'g': {
callback: () => {
// mを5回押すとセーフモードに入る
safemodeRequestCount++;
if (safemodeRequestCount >= 5) {
miLocalStorage.setItem('isSafeMode', 'true');
unisonReload();
} else {
if (safemodeRequestTimer != null) {
window.clearTimeout(safemodeRequestTimer);
}
safemodeRequestTimer = window.setTimeout(() => {
safemodeRequestCount = 0;
}, 300);
}
},
allowRepeat: true,
}
} as const satisfies Keymap;
window.document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.stop
@keydown.stop
>
<button v-if="hide" :class="$style.hidden" @click="reveal">
<button v-if="hide" :class="$style.hidden" @click="show">
<div :class="$style.hiddenTextWrapper">
<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
<b v-else style="display: block;"><i class="ti ti-music"></i> {{ prefer.s.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
@@ -157,7 +157,7 @@ const audioEl = useTemplateRef('audioEl');
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore'));
async function reveal() {
async function show() {
if (props.audio.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({
type: 'question',
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root">
<MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/>
<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="reveal">
<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="show">
<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
<b>{{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
@@ -37,7 +37,7 @@ const props = defineProps<{
const hide = ref(true);
async function reveal() {
async function show() {
if (props.media.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({
type: 'question',
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="reveal">
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="onclick">
<component
:is="disableImageLink ? 'div' : 'a'"
v-bind="disableImageLink ? {
@@ -96,10 +96,10 @@ const url = computed(() => (props.raw || prefer.s.loadRawImages)
? props.image.url
: prefer.s.disableShowingAnimatedImages
? getStaticImageUrl(props.image.url)
: props.image.thumbnailUrl!,
: props.image.thumbnailUrl,
);
async function reveal(ev: MouseEvent) {
async function onclick(ev: MouseEvent) {
if (!props.controls) {
return;
}
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.stop
@keydown.stop
>
<button v-if="hide" :class="$style.hidden" @click="reveal">
<button v-if="hide" :class="$style.hidden" @click="show">
<div :class="$style.hiddenTextWrapper">
<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ prefer.s.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
@@ -178,7 +178,7 @@ function hasFocus() {
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore'));
async function reveal() {
async function show() {
if (props.video.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({
type: 'question',
@@ -52,9 +52,9 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ item.label }}
</template>
<template v-else>
<span style="opacity: 0.7; font-size: 90%; word-break: break-word;">{{ item.parentLabels.join(' > ') }}</span>
<span style="opacity: 0.7; font-size: 90%;">{{ item.parentLabels.join(' > ') }}</span>
<br>
<span style="word-break: break-word;">{{ item.label }}</span>
<span>{{ item.label }}</span>
</template>
</span>
</MkA>
@@ -95,7 +95,7 @@ export type SuperMenuDef = {
<script lang="ts" setup>
import { useTemplateRef, ref, watch, nextTick, computed } from 'vue';
import { getScrollContainer } from '@@/js/scroll.js';
import type { SearchIndexItem } from '@/utility/inapp-search.js';
import type { SearchIndexItem } from '@/utility/settings-search-index.js';
import MkInput from '@/components/MkInput.vue';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
@@ -165,28 +165,12 @@ watch(rawSearchQuery, (value) => {
});
};
// label, keywords, texts の順に優先して表示
let items = Array.from(searchIndexItemById.values());
for (const item of items) {
if (compareStringIncludes(item.label, value)) {
for (const item of searchIndexItemById.values()) {
if (
compareStringIncludes(item.label, value) ||
item.keywords.some((x) => compareStringIncludes(x, value))
) {
addSearchResult(item);
items = items.filter(i => i.id !== item.id);
}
}
for (const item of items) {
if (item.keywords.some((x) => compareStringIncludes(x, value))) {
addSearchResult(item);
items = items.filter(i => i.id !== item.id);
}
}
for (const item of items) {
if (item.texts.some((x) => compareStringIncludes(x, value))) {
addSearchResult(item);
items = items.filter(i => i.id !== item.id);
}
}
}
@@ -1,14 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<slot></slot>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" module>
</style>
+3 -3
View File
@@ -31,7 +31,7 @@ import PageWithHeader from './global/PageWithHeader.vue';
import PageWithAnimBg from './global/PageWithAnimBg.vue';
import SearchMarker from './global/SearchMarker.vue';
import SearchLabel from './global/SearchLabel.vue';
import SearchText from './global/SearchText.vue';
import SearchKeyword from './global/SearchKeyword.vue';
import SearchIcon from './global/SearchIcon.vue';
import type { App } from 'vue';
@@ -71,7 +71,7 @@ export const components = {
PageWithAnimBg: PageWithAnimBg,
SearchMarker: SearchMarker,
SearchLabel: SearchLabel,
SearchText: SearchText,
SearchKeyword: SearchKeyword,
SearchIcon: SearchIcon,
};
@@ -105,7 +105,7 @@ declare module '@vue/runtime-core' {
PageWithAnimBg: typeof PageWithAnimBg;
SearchMarker: typeof SearchMarker;
SearchLabel: typeof SearchLabel;
SearchText: typeof SearchText;
SearchKeyword: typeof SearchKeyword;
SearchIcon: typeof SearchIcon;
}
}
-2
View File
@@ -165,8 +165,6 @@ function buildFullPath(args: {
const replaceRegex = new RegExp(`:${key}(\\?)?`, 'g');
fullPath = fullPath.replace(replaceRegex, value ? encodeURIComponent(value) : '');
}
// remove any optional parameters that are not provided
fullPath = fullPath.replace(/\/:\w+\?(?=\/|$)/g, '');
}
if (args.query) {
-1
View File
@@ -33,7 +33,6 @@ export type Keys = (
'preferences' |
'latestPreferencesUpdate' |
'hidePreferencesRestoreSuggestion' |
'isSafeMode' |
`miux:${string}` |
`ui:folder:${string}` |
`themes:${string}` | // DEPRECATED
@@ -4,161 +4,158 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<SearchMarker markerId="botProtection" :keywords="['bot', 'protection', 'captcha', 'hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-shield"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.botProtection }}</SearchLabel></template>
<template v-if="botProtectionForm.savedState.provider === 'hcaptcha'" #suffix>hCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
<template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
<template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
<template #footer>
<MkFormFooter :canSaving="canSaving" :form="botProtectionForm"/>
<MkFolder>
<template #icon><i class="ti ti-shield"></i></template>
<template #label>{{ i18n.ts.botProtection }}</template>
<template v-if="botProtectionForm.savedState.provider === 'hcaptcha'" #suffix>hCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
<template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
<template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
<template #footer>
<MkFormFooter :canSaving="canSaving" :form="botProtectionForm"/>
</template>
<div class="_gaps_m">
<MkRadios v-model="botProtectionForm.state.provider">
<option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
<option value="hcaptcha">hCaptcha</option>
<option value="mcaptcha">mCaptcha</option>
<option value="recaptcha">reCAPTCHA</option>
<option value="turnstile">Turnstile</option>
<option value="testcaptcha">testCaptcha</option>
</MkRadios>
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
<MkInput v-model="botProtectionForm.state.hcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.hcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="hcaptcha"
:sitekey="botProtectionForm.state.hcaptchaSiteKey"
:secretKey="botProtectionForm.state.hcaptchaSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
<div>
<span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a>
</div>
</div>
</MkInfo>
</template>
<div class="_gaps_m">
<MkRadios v-model="botProtectionForm.state.provider">
<option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
<option value="hcaptcha">hCaptcha</option>
<option value="mcaptcha">mCaptcha</option>
<option value="recaptcha">reCAPTCHA</option>
<option value="turnstile">Turnstile</option>
<option value="testcaptcha">testCaptcha</option>
</MkRadios>
<template v-else-if="botProtectionForm.state.provider === 'mcaptcha'">
<MkInput v-model="botProtectionForm.state.mcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl" debounce>
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="mcaptcha"
:sitekey="botProtectionForm.state.mcaptchaSiteKey"
:secretKey="botProtectionForm.state.mcaptchaSecretKey"
:instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"
/>
</FormSlot>
</template>
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
<MkInput v-model="botProtectionForm.state.hcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.hcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="hcaptcha"
:sitekey="botProtectionForm.state.hcaptchaSiteKey"
:secretKey="botProtectionForm.state.hcaptchaSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
<div>
<span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a>
</div>
<template v-else-if="botProtectionForm.state.provider === 'recaptcha'">
<MkInput v-model="botProtectionForm.state.recaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.recaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="recaptcha"
:sitekey="botProtectionForm.state.recaptchaSiteKey"
:secretKey="botProtectionForm.state.recaptchaSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
<div>
<span>ref: </span>
<a
href="https://developers.google.com/recaptcha/docs/faq?hl=ja#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do"
target="_blank"
>reCAPTCHA FAQ</a>
</div>
</MkInfo>
</template>
</div>
</MkInfo>
</template>
<template v-else-if="botProtectionForm.state.provider === 'mcaptcha'">
<MkInput v-model="botProtectionForm.state.mcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl" debounce>
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="mcaptcha"
:sitekey="botProtectionForm.state.mcaptchaSiteKey"
:secretKey="botProtectionForm.state.mcaptchaSecretKey"
:instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"
/>
</FormSlot>
</template>
<template v-else-if="botProtectionForm.state.provider === 'recaptcha'">
<MkInput v-model="botProtectionForm.state.recaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.recaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="recaptcha"
:sitekey="botProtectionForm.state.recaptchaSiteKey"
:secretKey="botProtectionForm.state.recaptchaSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
<div>
<span>ref: </span>
<a
href="https://developers.google.com/recaptcha/docs/faq?hl=ja#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do"
target="_blank"
>reCAPTCHA FAQ</a>
</div>
<template v-else-if="botProtectionForm.state.provider === 'turnstile'">
<MkInput v-model="botProtectionForm.state.turnstileSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.turnstileSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.turnstileSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="turnstile"
:sitekey="botProtectionForm.state.turnstileSiteKey"
:secretKey="botProtectionForm.state.turnstileSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>
{{ i18n.ts._captcha.testSiteKeyMessage }}
</div>
</MkInfo>
</template>
<template v-else-if="botProtectionForm.state.provider === 'turnstile'">
<MkInput v-model="botProtectionForm.state.turnstileSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.turnstileSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.turnstileSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="turnstile"
:sitekey="botProtectionForm.state.turnstileSiteKey"
:secretKey="botProtectionForm.state.turnstileSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>
{{ i18n.ts._captcha.testSiteKeyMessage }}
</div>
<div>
<span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a>
</div>
<div>
<span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a>
</div>
</MkInfo>
</template>
</div>
</MkInfo>
</template>
<template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
<FormSlot>
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/>
</FormSlot>
</template>
</div>
</MkFolder>
</SearchMarker>
<template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
<FormSlot>
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/>
</FormSlot>
</template>
</div>
</MkFolder>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import type { ApiWithDialogCustomErrors } from '@/os.js';
import MkRadios from '@/components/MkRadios.vue';
import MkInput from '@/components/MkInput.vue';
import FormSlot from '@/components/form/slot.vue';
@@ -170,6 +167,7 @@ import { useForm } from '@/composables/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
import type { ApiWithDialogCustomErrors } from '@/os.js';
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
+100 -111
View File
@@ -6,117 +6,89 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker path="/admin/branding" :label="i18n.ts.branding" :keywords="['branding']" icon="ti ti-paint">
<FormSuspense :p="init">
<div class="_gaps_m">
<SearchMarker :keywords="['icon', 'image']">
<MkInput v-model="iconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="iconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
</MkInput>
<SearchMarker :keywords="['icon', 'image']">
<MkInput v-model="app192IconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</SearchLabel></template>
<template #caption>
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '192x192px' }) }}</strong></div>
</template>
</MkInput>
</SearchMarker>
<MkInput v-model="app192IconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
<template #caption>
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '192x192px' }) }}</strong></div>
</template>
</MkInput>
<SearchMarker :keywords="['icon', 'image']">
<MkInput v-model="app512IconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</SearchLabel></template>
<template #caption>
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '512x512px' }) }}</strong></div>
</template>
</MkInput>
</SearchMarker>
<MkInput v-model="app512IconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
<template #caption>
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '512x512px' }) }}</strong></div>
</template>
</MkInput>
<SearchMarker :keywords="['banner', 'image']">
<MkInput v-model="bannerUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.bannerUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="bannerUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.bannerUrl }}</template>
</MkInput>
<SearchMarker :keywords="['background', 'image']">
<MkInput v-model="backgroundImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.backgroundImageUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="backgroundImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
</MkInput>
<SearchMarker :keywords="['image']">
<MkInput v-model="notFoundImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.notFoundDescription }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="notFoundImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.notFoundDescription }}</template>
</MkInput>
<SearchMarker :keywords="['image']">
<MkInput v-model="infoImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.nothing }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="infoImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.nothing }}</template>
</MkInput>
<SearchMarker :keywords="['image']">
<MkInput v-model="serverErrorImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.somethingHappened }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="serverErrorImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.somethingHappened }}</template>
</MkInput>
<SearchMarker :keywords="['theme', 'color']">
<MkColorInput v-model="themeColor">
<template #label><SearchLabel>{{ i18n.ts.themeColor }}</SearchLabel></template>
</MkColorInput>
</SearchMarker>
<MkColorInput v-model="themeColor">
<template #label>{{ i18n.ts.themeColor }}</template>
</MkColorInput>
<SearchMarker :keywords="['theme', 'default', 'light']">
<MkTextarea v-model="defaultLightTheme">
<template #label><SearchLabel>{{ i18n.ts.instanceDefaultLightTheme }}</SearchLabel></template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
</SearchMarker>
<MkTextarea v-model="defaultLightTheme">
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
<SearchMarker :keywords="['theme', 'default', 'dark']">
<MkTextarea v-model="defaultDarkTheme">
<template #label><SearchLabel>{{ i18n.ts.instanceDefaultDarkTheme }}</SearchLabel></template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
</SearchMarker>
<MkTextarea v-model="defaultDarkTheme">
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
<SearchMarker>
<MkInput v-model="repositoryUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.repositoryUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="repositoryUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.repositoryUrl }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="feedbackUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.feedbackUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="feedbackUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.feedbackUrl }}</template>
</MkInput>
<SearchMarker>
<MkTextarea v-model="manifestJsonOverride">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.manifestJsonOverride }}</SearchLabel></template>
</MkTextarea>
</SearchMarker>
<MkTextarea v-model="manifestJsonOverride">
<template #label>{{ i18n.ts._serverSettings.manifestJsonOverride }}</template>
</MkTextarea>
</div>
</SearchMarker>
</FormSuspense>
</div>
<template #footer>
<div :class="$style.footer">
@@ -134,6 +106,7 @@ import JSON5 from 'json5';
import { host } from '@@/js/config.js';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { instance, fetchInstance } from '@/instance.js';
@@ -142,22 +115,38 @@ import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import MkColorInput from '@/components/MkColorInput.vue';
const meta = await misskeyApi('admin/meta');
const iconUrl = ref<string | null>(null);
const app192IconUrl = ref<string | null>(null);
const app512IconUrl = ref<string | null>(null);
const bannerUrl = ref<string | null>(null);
const backgroundImageUrl = ref<string | null>(null);
const themeColor = ref<string | null>(null);
const defaultLightTheme = ref<string | null>(null);
const defaultDarkTheme = ref<string | null>(null);
const serverErrorImageUrl = ref<string | null>(null);
const infoImageUrl = ref<string | null>(null);
const notFoundImageUrl = ref<string | null>(null);
const repositoryUrl = ref<string | null>(null);
const feedbackUrl = ref<string | null>(null);
const manifestJsonOverride = ref<string>('{}');
const iconUrl = ref(meta.iconUrl);
const app192IconUrl = ref(meta.app192IconUrl);
const app512IconUrl = ref(meta.app512IconUrl);
const bannerUrl = ref(meta.bannerUrl);
const backgroundImageUrl = ref(meta.backgroundImageUrl);
const themeColor = ref(meta.themeColor);
const defaultLightTheme = ref(meta.defaultLightTheme);
const defaultDarkTheme = ref(meta.defaultDarkTheme);
const serverErrorImageUrl = ref(meta.serverErrorImageUrl);
const infoImageUrl = ref(meta.infoImageUrl);
const notFoundImageUrl = ref(meta.notFoundImageUrl);
const repositoryUrl = ref(meta.repositoryUrl);
const feedbackUrl = ref(meta.feedbackUrl);
const manifestJsonOverride = ref(meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t'));
async function init() {
const meta = await misskeyApi('admin/meta');
iconUrl.value = meta.iconUrl;
app192IconUrl.value = meta.app192IconUrl;
app512IconUrl.value = meta.app512IconUrl;
bannerUrl.value = meta.bannerUrl;
backgroundImageUrl.value = meta.backgroundImageUrl;
themeColor.value = meta.themeColor;
defaultLightTheme.value = meta.defaultLightTheme;
defaultDarkTheme.value = meta.defaultDarkTheme;
serverErrorImageUrl.value = meta.serverErrorImageUrl;
infoImageUrl.value = meta.infoImageUrl;
notFoundImageUrl.value = meta.notFoundImageUrl;
repositoryUrl.value = meta.repositoryUrl;
feedbackUrl.value = meta.feedbackUrl;
manifestJsonOverride.value = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t');
}
function save() {
os.apiWithDialog('admin/update-meta', {
@@ -6,67 +6,48 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker path="/admin/email-settings" :label="i18n.ts.emailServer" :keywords="['email']" icon="ti ti-mail">
<FormSuspense :p="init">
<div class="_gaps_m">
<SearchMarker>
<MkSwitch v-model="enableEmail">
<template #label><SearchLabel>{{ i18n.ts.enableEmail }}</SearchLabel> ({{ i18n.ts.recommended }})</template>
<template #caption><SearchText>{{ i18n.ts.emailConfigInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="enableEmail">
<template #label>{{ i18n.ts.enableEmail }} ({{ i18n.ts.recommended }})</template>
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
</MkSwitch>
<template v-if="enableEmail">
<SearchMarker>
<MkInput v-model="email" type="email">
<template #label><SearchLabel>{{ i18n.ts.emailAddress }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="email" type="email">
<template #label>{{ i18n.ts.emailAddress }}</template>
</MkInput>
<SearchMarker>
<FormSection>
<template #label><SearchLabel>{{ i18n.ts.smtpConfig }}</SearchLabel></template>
<FormSection>
<template #label>{{ i18n.ts.smtpConfig }}</template>
<div class="_gaps_m">
<FormSplit :minWidth="280">
<SearchMarker>
<MkInput v-model="smtpHost">
<template #label><SearchLabel>{{ i18n.ts.smtpHost }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="smtpPort" type="number">
<template #label><SearchLabel>{{ i18n.ts.smtpPort }}</SearchLabel></template>
</MkInput>
</SearchMarker>
</FormSplit>
<FormSplit :minWidth="280">
<SearchMarker>
<MkInput v-model="smtpUser">
<template #label><SearchLabel>{{ i18n.ts.smtpUser }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="smtpPass" type="password">
<template #label><SearchLabel>{{ i18n.ts.smtpPass }}</SearchLabel></template>
</MkInput>
</SearchMarker>
</FormSplit>
<FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
<SearchMarker>
<MkSwitch v-model="smtpSecure">
<template #label><SearchLabel>{{ i18n.ts.smtpSecure }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.smtpSecureInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>
</FormSection>
</SearchMarker>
<div class="_gaps_m">
<FormSplit :minWidth="280">
<MkInput v-model="smtpHost">
<template #label>{{ i18n.ts.smtpHost }}</template>
</MkInput>
<MkInput v-model="smtpPort" type="number">
<template #label>{{ i18n.ts.smtpPort }}</template>
</MkInput>
</FormSplit>
<FormSplit :minWidth="280">
<MkInput v-model="smtpUser">
<template #label>{{ i18n.ts.smtpUser }}</template>
</MkInput>
<MkInput v-model="smtpPass" type="password">
<template #label>{{ i18n.ts.smtpPass }}</template>
</MkInput>
</FormSplit>
<FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
<MkSwitch v-model="smtpSecure">
<template #label>{{ i18n.ts.smtpSecure }}</template>
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
</MkSwitch>
</div>
</FormSection>
</template>
</div>
</SearchMarker>
</FormSuspense>
</div>
<template #footer>
<div :class="$style.footer">
@@ -86,6 +67,7 @@ import { ref, computed } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import FormInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
@@ -95,15 +77,24 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
const meta = await misskeyApi('admin/meta');
const enableEmail = ref<boolean>(false);
const email = ref<string | null>(null);
const smtpSecure = ref<boolean>(false);
const smtpHost = ref<string>('');
const smtpPort = ref<number>(0);
const smtpUser = ref<string>('');
const smtpPass = ref<string>('');
const enableEmail = ref(meta.enableEmail);
const email = ref(meta.email);
const smtpSecure = ref(meta.smtpSecure);
const smtpHost = ref(meta.smtpHost);
const smtpPort = ref(meta.smtpPort);
const smtpUser = ref(meta.smtpUser);
const smtpPass = ref(meta.smtpPass);
async function init() {
const meta = await misskeyApi('admin/meta');
enableEmail.value = meta.enableEmail;
email.value = meta.email;
smtpSecure.value = meta.smtpSecure;
smtpHost.value = meta.smtpHost;
smtpPort.value = meta.smtpPort;
smtpUser.value = meta.smtpUser;
smtpPass.value = meta.smtpPass;
}
async function testEmail() {
const { canceled, result: destination } = await os.inputText({
@@ -6,49 +6,36 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker path="/admin/external-services" :label="i18n.ts.externalServices" :keywords="['external', 'services', 'thirdparty']" icon="ti ti-link">
<FormSuspense :p="init">
<div class="_gaps_m">
<SearchMarker v-slot="slotProps">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Google Analytics</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<MkFolder>
<template #label>Google Analytics<span class="_beta">{{ i18n.ts.beta }}</span></template>
<div class="_gaps_m">
<SearchMarker>
<MkInput v-model="googleAnalyticsMeasurementId">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>Measurement ID</SearchLabel></template>
</MkInput>
</SearchMarker>
<div class="_gaps_m">
<MkInput v-model="googleAnalyticsMeasurementId">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Measurement ID</template>
</MkInput>
<MkButton primary @click="save_googleAnalytics">Save</MkButton>
</div>
</MkFolder>
<MkButton primary @click="save_googleAnalytics">Save</MkButton>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #label>DeepL Translation</template>
<SearchMarker v-slot="slotProps">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>DeepL Translation</SearchLabel></template>
<div class="_gaps_m">
<SearchMarker>
<MkInput v-model="deeplAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>Auth Key</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkSwitch v-model="deeplIsPro">
<template #label><SearchLabel>Pro account</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<MkButton primary @click="save_deepl">Save</MkButton>
</div>
</MkFolder>
</SearchMarker>
<div class="_gaps_m">
<MkInput v-model="deeplAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>DeepL Auth Key</template>
</MkInput>
<MkSwitch v-model="deeplIsPro">
<template #label>Pro account</template>
</MkSwitch>
<MkButton primary @click="save_deepl">Save</MkButton>
</div>
</MkFolder>
</div>
</SearchMarker>
</FormSuspense>
</div>
</PageWithHeader>
</template>
@@ -58,6 +45,7 @@ import { ref, computed } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { fetchInstance } from '@/instance.js';
@@ -65,11 +53,17 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkFolder from '@/components/MkFolder.vue';
const meta = await misskeyApi('admin/meta');
const deeplAuthKey = ref<string>('');
const deeplIsPro = ref<boolean>(false);
const deeplAuthKey = ref(meta.deeplAuthKey ?? '');
const deeplIsPro = ref(meta.deeplIsPro);
const googleAnalyticsMeasurementId = ref(meta.googleAnalyticsMeasurementId ?? '');
const googleAnalyticsMeasurementId = ref<string>('');
async function init() {
const meta = await misskeyApi('admin/meta');
deeplAuthKey.value = meta.deeplAuthKey ?? '';
deeplIsPro.value = meta.deeplIsPro;
googleAnalyticsMeasurementId.value = meta.googleAnalyticsMeasurementId ?? '';
}
function save_deepl() {
os.apiWithDialog('admin/update-meta', {
+7 -4
View File
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="noEmailServer" warn>{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
</div>
<MkSuperMenu :def="menuDef" :searchIndex="searchIndex" :grid="narrow"></MkSuperMenu>
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
</div>
</div>
</div>
@@ -44,9 +44,6 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { lookupUser, lookupUserByEmail, lookupFile } from '@/utility/admin-lookup.js';
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { useRouter } from '@/router.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
const searchIndex = await import('search-index:admin').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
const isEmpty = (x: string | null) => x == null || x === '';
@@ -327,6 +324,12 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePage(() => INFO.value);
defineExpose({
header: {
title: i18n.ts.controlPanel,
},
});
</script>
<style lang="scss" scoped>
+129 -138
View File
@@ -6,162 +6,140 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker path="/admin/moderation" :label="i18n.ts.moderation" :keywords="['moderation']" icon="ti ti-shield" :inlining="['serverRules']">
<FormSuspense :p="init">
<div class="_gaps_m">
<SearchMarker :keywords="['open', 'registration']">
<MkSwitch :modelValue="enableRegistration" @update:modelValue="onChange_enableRegistration">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.openRegistration }}</SearchLabel></template>
<template #caption>
<div><SearchText>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</SearchText></div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> <SearchText>{{ i18n.ts._serverSettings.openRegistrationWarning }}</SearchText></div>
</template>
</MkSwitch>
</SearchMarker>
<MkSwitch :modelValue="enableRegistration" @update:modelValue="onChange_enableRegistration">
<template #label>{{ i18n.ts._serverSettings.openRegistration }}</template>
<template #caption>
<div>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.openRegistrationWarning }}</div>
</template>
</MkSwitch>
<SearchMarker :keywords="['email', 'required', 'signup']">
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
<template #label><SearchLabel>{{ i18n.ts.emailRequiredForSignup }}</SearchLabel> ({{ i18n.ts.recommended }})</template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
<template #label>{{ i18n.ts.emailRequiredForSignup }} ({{ i18n.ts.recommended }})</template>
</MkSwitch>
<SearchMarker :keywords="['ugc', 'content', 'visibility', 'visitor', 'guest']">
<MkSelect v-model="ugcVisibilityForVisitor" @update:modelValue="onChange_ugcVisibilityForVisitor">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</SearchLabel></template>
<option value="all">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all }}</option>
<option value="local">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly }} ({{ i18n.ts.recommended }})</option>
<option value="none">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none }}</option>
<template #caption>
<div><SearchText>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</SearchText></div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> <SearchText>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description2 }}</SearchText></div>
</template>
</MkSelect>
</SearchMarker>
<MkSelect v-model="ugcVisibilityForVisitor" @update:modelValue="onChange_ugcVisibilityForVisitor">
<template #label>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</template>
<option value="all">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all }}</option>
<option value="local">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly }} ({{ i18n.ts.recommended }})</option>
<option value="none">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none }}</option>
<template #caption>
<div>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description2 }}</div>
</template>
</MkSelect>
<XServerRules/>
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
<SearchMarker :keywords="['preserved', 'usernames']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-lock-star"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.preservedUsernames }}</SearchLabel></template>
<MkFolder>
<template #icon><i class="ti ti-lock-star"></i></template>
<template #label>{{ i18n.ts.preservedUsernames }}</template>
<div class="_gaps">
<MkTextarea v-model="preservedUsernames">
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_preservedUsernames">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<div class="_gaps">
<MkTextarea v-model="preservedUsernames">
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_preservedUsernames">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<SearchMarker :keywords="['sensitive', 'words']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-message-exclamation"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.sensitiveWords }}</SearchLabel></template>
<MkFolder>
<template #icon><i class="ti ti-message-exclamation"></i></template>
<template #label>{{ i18n.ts.sensitiveWords }}</template>
<div class="_gaps">
<MkTextarea v-model="sensitiveWords">
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<div class="_gaps">
<MkTextarea v-model="sensitiveWords">
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<SearchMarker :keywords="['prohibited', 'words']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-message-x"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.prohibitedWords }}</SearchLabel></template>
<MkFolder>
<template #icon><i class="ti ti-message-x"></i></template>
<template #label>{{ i18n.ts.prohibitedWords }}</template>
<div class="_gaps">
<MkTextarea v-model="prohibitedWords">
<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<div class="_gaps">
<MkTextarea v-model="prohibitedWords">
<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<SearchMarker :keywords="['prohibited', 'name', 'user']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-user-x"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.prohibitedWordsForNameOfUser }}</SearchLabel></template>
<MkFolder>
<template #icon><i class="ti ti-user-x"></i></template>
<template #label>{{ i18n.ts.prohibitedWordsForNameOfUser }}</template>
<div class="_gaps">
<MkTextarea v-model="prohibitedWordsForNameOfUser">
<template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<div class="_gaps">
<MkTextarea v-model="prohibitedWordsForNameOfUser">
<template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<SearchMarker :keywords="['hidden', 'tags', 'hashtags']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.hiddenTags }}</SearchLabel></template>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.hiddenTags }}</template>
<div class="_gaps">
<MkTextarea v-model="hiddenTags">
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_hiddenTags">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<div class="_gaps">
<MkTextarea v-model="hiddenTags">
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_hiddenTags">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<SearchMarker :keywords="['silenced', 'servers', 'hosts']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.silencedInstances }}</SearchLabel></template>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.silencedInstances }}</template>
<div class="_gaps">
<MkTextarea v-model="silencedHosts">
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_silencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<div class="_gaps">
<MkTextarea v-model="silencedHosts">
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_silencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<SearchMarker :keywords="['media', 'silenced', 'servers', 'hosts']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.mediaSilencedInstances }}</SearchLabel></template>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.mediaSilencedInstances }}</template>
<div class="_gaps">
<MkTextarea v-model="mediaSilencedHosts">
<template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_mediaSilencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<div class="_gaps">
<MkTextarea v-model="mediaSilencedHosts">
<template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_mediaSilencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<SearchMarker :keywords="['blocked', 'servers', 'hosts']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-ban"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.blockedInstances }}</SearchLabel></template>
<MkFolder>
<template #icon><i class="ti ti-ban"></i></template>
<template #label>{{ i18n.ts.blockedInstances }}</template>
<div class="_gaps">
<MkTextarea v-model="blockedHosts">
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_blockedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<div class="_gaps">
<MkTextarea v-model="blockedHosts">
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_blockedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</div>
</SearchMarker>
</FormSuspense>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import XServerRules from './server-rules.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { fetchInstance } from '@/instance.js';
@@ -172,19 +150,32 @@ import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
const meta = await misskeyApi('admin/meta');
const enableRegistration = ref<boolean>(false);
const emailRequiredForSignup = ref<boolean>(false);
const ugcVisibilityForVisitor = ref<string>('all');
const sensitiveWords = ref<string>('');
const prohibitedWords = ref<string>('');
const prohibitedWordsForNameOfUser = ref<string>('');
const hiddenTags = ref<string>('');
const preservedUsernames = ref<string>('');
const blockedHosts = ref<string>('');
const silencedHosts = ref<string>('');
const mediaSilencedHosts = ref<string>('');
const enableRegistration = ref(!meta.disableRegistration);
const emailRequiredForSignup = ref(meta.emailRequiredForSignup);
const ugcVisibilityForVisitor = ref(meta.ugcVisibilityForVisitor);
const sensitiveWords = ref(meta.sensitiveWords.join('\n'));
const prohibitedWords = ref(meta.prohibitedWords.join('\n'));
const prohibitedWordsForNameOfUser = ref(meta.prohibitedWordsForNameOfUser.join('\n'));
const hiddenTags = ref(meta.hiddenTags.join('\n'));
const preservedUsernames = ref(meta.preservedUsernames.join('\n'));
const blockedHosts = ref(meta.blockedHosts.join('\n'));
const silencedHosts = ref(meta.silencedHosts?.join('\n') ?? '');
const mediaSilencedHosts = ref(meta.mediaSilencedHosts.join('\n'));
async function init() {
const meta = await misskeyApi('admin/meta');
enableRegistration.value = !meta.disableRegistration;
emailRequiredForSignup.value = meta.emailRequiredForSignup;
ugcVisibilityForVisitor.value = meta.ugcVisibilityForVisitor;
sensitiveWords.value = meta.sensitiveWords.join('\n');
prohibitedWords.value = meta.prohibitedWords.join('\n');
prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
hiddenTags.value = meta.hiddenTags.join('\n');
preservedUsernames.value = meta.preservedUsernames.join('\n');
blockedHosts.value = meta.blockedHosts.join('\n');
silencedHosts.value = meta.silencedHosts?.join('\n') ?? '';
mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
}
async function onChange_enableRegistration(value: boolean) {
if (value) {
@@ -6,94 +6,70 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker path="/admin/object-storage" :label="i18n.ts.objectStorage" :keywords="['objectStorage']" icon="ti ti-cloud">
<FormSuspense :p="init">
<div class="_gaps_m">
<SearchMarker>
<MkSwitch v-model="useObjectStorage"><SearchLabel>{{ i18n.ts.useObjectStorage }}</SearchLabel></MkSwitch>
</SearchMarker>
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
<template v-if="useObjectStorage">
<SearchMarker>
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'" type="url">
<template #label><SearchLabel>{{ i18n.ts.objectStorageBaseUrl }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageBaseUrlDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'" type="url">
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStorageBucket">
<template #label><SearchLabel>{{ i18n.ts.objectStorageBucket }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageBucketDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStorageBucket">
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStoragePrefix">
<template #label><SearchLabel>{{ i18n.ts.objectStoragePrefix }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStoragePrefixDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStoragePrefix">
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStorageEndpoint" :placeholder="'example.com'">
<template #label><SearchLabel>{{ i18n.ts.objectStorageEndpoint }}</SearchLabel></template>
<template #prefix>https://</template>
<template #caption><SearchText>{{ i18n.ts.objectStorageEndpointDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStorageEndpoint" :placeholder="'example.com'">
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
<template #prefix>https://</template>
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStorageRegion">
<template #label><SearchLabel>{{ i18n.ts.objectStorageRegion }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageRegionDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStorageRegion">
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
</MkInput>
<FormSplit :minWidth="280">
<SearchMarker>
<MkInput v-model="objectStorageAccessKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>Access key</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStorageAccessKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Access key</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStorageSecretKey" type="password">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>Secret key</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStorageSecretKey" type="password">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Secret key</template>
</MkInput>
</FormSplit>
<SearchMarker>
<MkSwitch v-model="objectStorageUseSSL">
<template #label><SearchLabel>{{ i18n.ts.objectStorageUseSSL }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageUseSSLDesc }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="objectStorageUseSSL">
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
</MkSwitch>
<SearchMarker>
<MkSwitch v-model="objectStorageUseProxy">
<template #label><SearchLabel>{{ i18n.ts.objectStorageUseProxy }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageUseProxyDesc }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="objectStorageUseProxy">
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
</MkSwitch>
<SearchMarker>
<MkSwitch v-model="objectStorageSetPublicRead">
<template #label><SearchLabel>{{ i18n.ts.objectStorageSetPublicRead }}</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="objectStorageSetPublicRead">
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
</MkSwitch>
<SearchMarker>
<MkSwitch v-model="objectStorageS3ForcePathStyle">
<template #label><SearchLabel>s3ForcePathStyle</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.s3ForcePathStyleDesc }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="objectStorageS3ForcePathStyle">
<template #label>s3ForcePathStyle</template>
<template #caption>{{ i18n.ts.s3ForcePathStyleDesc }}</template>
</MkSwitch>
</template>
</div>
</SearchMarker>
</FormSuspense>
</div>
<template #footer>
<div :class="$style.footer">
@@ -118,21 +94,36 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
const meta = await misskeyApi('admin/meta');
const useObjectStorage = ref<boolean>(false);
const objectStorageBaseUrl = ref<string | null>(null);
const objectStorageBucket = ref<string | null>(null);
const objectStoragePrefix = ref<string | null>(null);
const objectStorageEndpoint = ref<string | null>(null);
const objectStorageRegion = ref<string | null>(null);
const objectStoragePort = ref<number | null>(null);
const objectStorageAccessKey = ref<string | null>(null);
const objectStorageSecretKey = ref<string | null>(null);
const objectStorageUseSSL = ref<boolean>(false);
const objectStorageUseProxy = ref<boolean>(false);
const objectStorageSetPublicRead = ref<boolean>(false);
const objectStorageS3ForcePathStyle = ref<boolean>(true);
const useObjectStorage = ref(meta.useObjectStorage);
const objectStorageBaseUrl = ref(meta.objectStorageBaseUrl);
const objectStorageBucket = ref(meta.objectStorageBucket);
const objectStoragePrefix = ref(meta.objectStoragePrefix);
const objectStorageEndpoint = ref(meta.objectStorageEndpoint);
const objectStorageRegion = ref(meta.objectStorageRegion);
const objectStoragePort = ref(meta.objectStoragePort);
const objectStorageAccessKey = ref(meta.objectStorageAccessKey);
const objectStorageSecretKey = ref(meta.objectStorageSecretKey);
const objectStorageUseSSL = ref(meta.objectStorageUseSSL);
const objectStorageUseProxy = ref(meta.objectStorageUseProxy);
const objectStorageSetPublicRead = ref(meta.objectStorageSetPublicRead);
const objectStorageS3ForcePathStyle = ref(meta.objectStorageS3ForcePathStyle);
async function init() {
const meta = await misskeyApi('admin/meta');
useObjectStorage.value = meta.useObjectStorage;
objectStorageBaseUrl.value = meta.objectStorageBaseUrl;
objectStorageBucket.value = meta.objectStorageBucket;
objectStoragePrefix.value = meta.objectStoragePrefix;
objectStorageEndpoint.value = meta.objectStorageEndpoint;
objectStorageRegion.value = meta.objectStorageRegion;
objectStoragePort.value = meta.objectStoragePort;
objectStorageAccessKey.value = meta.objectStorageAccessKey;
objectStorageSecretKey.value = meta.objectStorageSecretKey;
objectStorageUseSSL.value = meta.objectStorageUseSSL;
objectStorageUseProxy.value = meta.objectStorageUseProxy;
objectStorageSetPublicRead.value = meta.objectStorageSetPublicRead;
objectStorageS3ForcePathStyle.value = meta.objectStorageS3ForcePathStyle;
}
function save() {
os.apiWithDialog('admin/update-meta', {
+126 -158
View File
@@ -6,163 +6,131 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker path="/admin/performance" :label="i18n.ts.performance" :keywords="['performance']" icon="ti ti-bolt">
<div class="_gaps">
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableServerMachineStats" @change="onChange_enableServerMachineStats">
<template #label><SearchLabel>{{ i18n.ts.enableServerMachineStats }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableIdenticonGeneration" @change="onChange_enableIdenticonGeneration">
<template #label><SearchLabel>{{ i18n.ts.enableIdenticonGeneration }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForRemoteUser" @change="onChange_enableChartsForRemoteUser">
<template #label><SearchLabel>{{ i18n.ts.enableChartsForRemoteUser }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableStatsForFederatedInstances" @change="onChange_enableStatsForFederatedInstances">
<template #label><SearchLabel>{{ i18n.ts.enableStatsForFederatedInstances }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances">
<template #label><SearchLabel>{{ i18n.ts.enableChartsForFederatedInstances }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template>
<template #label><SearchLabel>Misskey® Fan-out Timeline Technology™ (FTT)</SearchLabel></template>
<template v-if="fttForm.savedState.enableFanoutTimeline" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="fttForm.modified.value" #footer>
<MkFormFooter :form="fttForm"/>
</template>
<div class="_gaps">
<SearchMarker>
<MkSwitch v-model="fttForm.state.enableFanoutTimeline">
<template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div><SearchText>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</SearchText></div>
<div><MkLink target="_blank" url="https://misskey-hub.net/docs/for-admin/features/ftt/">{{ i18n.ts.details }}</MkLink></div>
</template>
</MkSwitch>
</SearchMarker>
<template v-if="fttForm.state.enableFanoutTimeline">
<SearchMarker :keywords="['db', 'database', 'fallback']">
<MkSwitch v-model="fttForm.state.enableFanoutTimelineDbFallback">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}</SearchLabel><span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker>
<MkInput v-model="fttForm.state.perLocalUserUserTimelineCacheMax" type="number">
<template #label><SearchLabel>perLocalUserUserTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="fttForm.state.perRemoteUserUserTimelineCacheMax" type="number">
<template #label><SearchLabel>perRemoteUserUserTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="fttForm.state.perUserHomeTimelineCacheMax" type="number">
<template #label><SearchLabel>perUserHomeTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="fttForm.state.perUserListTimelineCacheMax" type="number">
<template #label><SearchLabel>perUserListTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
</template>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template>
<template #label><SearchLabel>Misskey® Reactions Boost Technology™ (RBT)</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="rbtForm.modified.value" #footer>
<MkFormFooter :form="rbtForm"/>
</template>
<div class="_gaps_m">
<SearchMarker>
<MkSwitch v-model="rbtForm.state.enableReactionsBuffering">
<template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><SearchIcon><i class="ti ti-recycle"></i></SearchIcon></template>
<template #label><SearchLabel>Remote Notes Cleaning (仮)</SearchLabel></template>
<template v-if="remoteNotesCleaningForm.savedState.enableRemoteNotesCleaning" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="remoteNotesCleaningForm.modified.value" #footer>
<MkFormFooter :form="remoteNotesCleaningForm"/>
</template>
<div class="_gaps_m">
<MkSwitch v-model="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
<template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="remoteNotesCleaningForm.modifiedStates.enableRemoteNotesCleaning" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.remoteNotesCleaning_description }}</SearchText></template>
</MkSwitch>
<template v-if="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningExpiryDaysForEachNotes" type="number">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.remoteNotesCleaningExpiryDaysForEachNotes }}</SearchLabel> ({{ i18n.ts.inDays }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningExpiryDaysForEachNotes" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #suffix>{{ i18n.ts._time.day }}</template>
</MkInput>
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningMaxProcessingDurationInMinutes" type="number">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.remoteNotesCleaningMaxProcessingDuration }}</SearchLabel> ({{ i18n.ts.inMinutes }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningMaxProcessingDurationInMinutes" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #suffix>{{ i18n.ts._time.minute }}</template>
</MkInput>
</template>
</div>
</MkFolder>
</SearchMarker>
<div class="_gaps">
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableServerMachineStats" @change="onChange_enableServerMachineStats">
<template #label>{{ i18n.ts.enableServerMachineStats }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableIdenticonGeneration" @change="onChange_enableIdenticonGeneration">
<template #label>{{ i18n.ts.enableIdenticonGeneration }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForRemoteUser" @change="onChange_enableChartsForRemoteUser">
<template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableStatsForFederatedInstances" @change="onChange_enableStatsForFederatedInstances">
<template #label>{{ i18n.ts.enableStatsForFederatedInstances }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances">
<template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-bolt"></i></template>
<template #label>Misskey® Fan-out Timeline Technology™ (FTT)</template>
<template v-if="fttForm.savedState.enableFanoutTimeline" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="fttForm.modified.value" #footer>
<MkFormFooter :form="fttForm"/>
</template>
<div class="_gaps">
<MkSwitch v-model="fttForm.state.enableFanoutTimeline">
<template #label>{{ i18n.ts.enable }}<span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</div>
<div><MkLink target="_blank" url="https://misskey-hub.net/docs/for-admin/features/ftt/">{{ i18n.ts.details }}</MkLink></div>
</template>
</MkSwitch>
<template v-if="fttForm.state.enableFanoutTimeline">
<MkSwitch v-model="fttForm.state.enableFanoutTimelineDbFallback">
<template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}<span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template>
</MkSwitch>
<MkInput v-model="fttForm.state.perLocalUserUserTimelineCacheMax" type="number">
<template #label>perLocalUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<MkInput v-model="fttForm.state.perRemoteUserUserTimelineCacheMax" type="number">
<template #label>perRemoteUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<MkInput v-model="fttForm.state.perUserHomeTimelineCacheMax" type="number">
<template #label>perUserHomeTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<MkInput v-model="fttForm.state.perUserListTimelineCacheMax" type="number">
<template #label>perUserListTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</template>
</div>
</MkFolder>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-bolt"></i></template>
<template #label>Misskey® Reactions Boost Technology™ (RBT)<span class="_beta">{{ i18n.ts.beta }}</span></template>
<template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="rbtForm.modified.value" #footer>
<MkFormFooter :form="rbtForm"/>
</template>
<div class="_gaps_m">
<MkSwitch v-model="rbtForm.state.enableReactionsBuffering">
<template #label>{{ i18n.ts.enable }}<span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</template>
</MkSwitch>
</div>
</MkFolder>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-recycle"></i></template>
<template #label>Remote Notes Cleaning (仮)</template>
<template v-if="remoteNotesCleaningForm.savedState.enableRemoteNotesCleaning" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="remoteNotesCleaningForm.modified.value" #footer>
<MkFormFooter :form="remoteNotesCleaningForm"/>
</template>
<div class="_gaps_m">
<MkSwitch v-model="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
<template #label>{{ i18n.ts.enable }}<span v-if="remoteNotesCleaningForm.modifiedStates.enableRemoteNotesCleaning" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.remoteNotesCleaning_description }}</template>
</MkSwitch>
<template v-if="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningExpiryDaysForEachNotes" type="number">
<template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningExpiryDaysForEachNotes }} ({{ i18n.ts.inDays }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningExpiryDaysForEachNotes" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #suffix>{{ i18n.ts._time.day }}</template>
</MkInput>
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningMaxProcessingDurationInMinutes" type="number">
<template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningMaxProcessingDuration }} ({{ i18n.ts.inMinutes }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningMaxProcessingDurationInMinutes" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #suffix>{{ i18n.ts._time.minute }}</template>
</MkInput>
</template>
</div>
</MkFolder>
</div>
</div>
</PageWithHeader>
</template>
@@ -275,7 +243,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePage(() => ({
title: i18n.ts.performance,
icon: 'ti ti-bolt',
title: i18n.ts.other,
icon: 'ti ti-adjustments',
}));
</script>
+10 -12
View File
@@ -6,20 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 800px;">
<SearchMarker path="/admin/relays" :label="i18n.ts.relays" :keywords="['relays']" icon="ti ti-planet">
<div class="_gaps">
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;">
<div>{{ relay.inbox }}</div>
<div style="margin: 8px 0;">
<i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--MI_THEME-success);"></i>
<i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--MI_THEME-error);"></i>
<i v-else class="ti ti-clock" :class="$style.icon"></i>
<span>{{ i18n.ts._relayStatus[relay.status] }}</span>
</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
<div class="_gaps">
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;">
<div>{{ relay.inbox }}</div>
<div style="margin: 8px 0;">
<i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--MI_THEME-success);"></i>
<i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--MI_THEME-error);"></i>
<i v-else class="ti ti-clock" :class="$style.icon"></i>
<span>{{ i18n.ts._relayStatus[relay.status] }}</span>
</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</SearchMarker>
</div>
</div>
</PageWithHeader>
</template>
+92 -130
View File
@@ -6,153 +6,115 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker path="/admin/security" :label="i18n.ts.security" :keywords="['security']" icon="ti ti-lock" :inlining="['botProtection']">
<div class="_gaps_m">
<XBotProtection/>
<div class="_gaps_m">
<XBotProtection/>
<SearchMarker v-slot="slotProps" :keywords="['sensitive', 'media', 'detection']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.sensitiveMediaDetection }}</SearchLabel></template>
<template v-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
<template v-else #suffix>{{ i18n.ts.none }}</template>
<template v-if="sensitiveMediaDetectionForm.modified.value" #footer>
<MkFormFooter :form="sensitiveMediaDetectionForm"/>
</template>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
<template v-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
<template v-else #suffix>{{ i18n.ts.none }}</template>
<template v-if="sensitiveMediaDetectionForm.modified.value" #footer>
<MkFormFooter :form="sensitiveMediaDetectionForm"/>
</template>
<div class="_gaps_m">
<div><SearchText>{{ i18n.ts._sensitiveMediaDetection.description }}</SearchText></div>
<div class="_gaps_m">
<span>{{ i18n.ts._sensitiveMediaDetection.description }}</span>
<MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
<option value="none">{{ i18n.ts.none }}</option>
<option value="all">{{ i18n.ts.all }}</option>
<option value="local">{{ i18n.ts.localOnly }}</option>
<option value="remote">{{ i18n.ts.remoteOnly }}</option>
</MkRadios>
<MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
<option value="none">{{ i18n.ts.none }}</option>
<option value="all">{{ i18n.ts.all }}</option>
<option value="local">{{ i18n.ts.localOnly }}</option>
<option value="remote">{{ i18n.ts.remoteOnly }}</option>
</MkRadios>
<SearchMarker :keywords="['sensitivity']">
<MkRange v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :textConverter="(v) => `${v + 1}`">
<template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</SearchText></template>
</MkRange>
</SearchMarker>
<MkRange v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :textConverter="(v) => `${v + 1}`">
<template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template>
</MkRange>
<SearchMarker :keywords="['video', 'analyze']">
<MkSwitch v-model="sensitiveMediaDetectionForm.state.enableSensitiveMediaDetectionForVideos">
<template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="sensitiveMediaDetectionForm.state.enableSensitiveMediaDetectionForVideos">
<template #label>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</template>
</MkSwitch>
<SearchMarker :keywords="['flag', 'automatically']">
<MkSwitch v-model="sensitiveMediaDetectionForm.state.setSensitiveFlagAutomatically">
<template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }}</SearchLabel> ({{ i18n.ts.notRecommended }})</template>
<template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="sensitiveMediaDetectionForm.state.setSensitiveFlagAutomatically">
<template #label>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }} ({{ i18n.ts.notRecommended }})</template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</template>
</MkSwitch>
<!-- 現状 false positive が多すぎて実用に耐えない
<!-- 現状 false positive が多すぎて実用に耐えない
<MkSwitch v-model="disallowUploadWhenPredictedAsPorn">
<template #label>{{ i18n.ts._sensitiveMediaDetection.disallowUploadWhenPredictedAsPorn }}</template>
</MkSwitch>
-->
</div>
</MkFolder>
</SearchMarker>
</div>
</MkFolder>
<SearchMarker v-slot="slotProps" :keywords="['email', 'validation']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Active Email Validation</SearchLabel></template>
<template v-if="emailValidationForm.savedState.enableActiveEmailValidation" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="emailValidationForm.modified.value" #footer>
<MkFormFooter :form="emailValidationForm"/>
</template>
<MkFolder>
<template #label>Active Email Validation</template>
<template v-if="emailValidationForm.savedState.enableActiveEmailValidation" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="emailValidationForm.modified.value" #footer>
<MkFormFooter :form="emailValidationForm"/>
</template>
<div class="_gaps_m">
<div><SearchText>{{ i18n.ts.activeEmailValidationDescription }}</SearchText></div>
<div class="_gaps_m">
<span>{{ i18n.ts.activeEmailValidationDescription }}</span>
<MkSwitch v-model="emailValidationForm.state.enableActiveEmailValidation">
<template #label>Enable</template>
</MkSwitch>
<MkSwitch v-model="emailValidationForm.state.enableVerifymailApi">
<template #label>Use Verifymail.io API</template>
</MkSwitch>
<MkInput v-model="emailValidationForm.state.verifymailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Verifymail.io API Auth Key</template>
</MkInput>
<MkSwitch v-model="emailValidationForm.state.enableTruemailApi">
<template #label>Use TrueMail API</template>
</MkSwitch>
<MkInput v-model="emailValidationForm.state.truemailInstance">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>TrueMail API Instance</template>
</MkInput>
<MkInput v-model="emailValidationForm.state.truemailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>TrueMail API Auth Key</template>
</MkInput>
</div>
</MkFolder>
<SearchMarker>
<MkSwitch v-model="emailValidationForm.state.enableActiveEmailValidation">
<template #label><SearchLabel>Enable</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<MkFolder>
<template #label>Banned Email Domains</template>
<template v-if="bannedEmailDomainsForm.modified.value" #footer>
<MkFormFooter :form="bannedEmailDomainsForm"/>
</template>
<SearchMarker>
<MkSwitch v-model="emailValidationForm.state.enableVerifymailApi">
<template #label><SearchLabel>Use Verifymail.io API</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<div class="_gaps_m">
<MkTextarea v-model="bannedEmailDomainsForm.state.bannedEmailDomains">
<template #label>Banned Email Domains List</template>
</MkTextarea>
</div>
</MkFolder>
<SearchMarker>
<MkInput v-model="emailValidationForm.state.verifymailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>Verifymail.io API Auth Key</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkFolder>
<template #label>Log IP address</template>
<template v-if="ipLoggingForm.savedState.enableIpLogging" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="ipLoggingForm.modified.value" #footer>
<MkFormFooter :form="ipLoggingForm"/>
</template>
<SearchMarker>
<MkSwitch v-model="emailValidationForm.state.enableTruemailApi">
<template #label><SearchLabel>Use TrueMail API</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<SearchMarker>
<MkInput v-model="emailValidationForm.state.truemailInstance">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>TrueMail API Instance</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="emailValidationForm.state.truemailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>TrueMail API Auth Key</SearchLabel></template>
</MkInput>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['banned', 'email', 'domains', 'blacklist']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Banned Email Domains</SearchLabel></template>
<template v-if="bannedEmailDomainsForm.modified.value" #footer>
<MkFormFooter :form="bannedEmailDomainsForm"/>
</template>
<div class="_gaps_m">
<SearchMarker>
<MkTextarea v-model="bannedEmailDomainsForm.state.bannedEmailDomains">
<template #label><SearchLabel>Banned Email Domains List</SearchLabel></template>
</MkTextarea>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['log', 'ipAddress']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Log IP address</SearchLabel></template>
<template v-if="ipLoggingForm.savedState.enableIpLogging" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="ipLoggingForm.modified.value" #footer>
<MkFormFooter :form="ipLoggingForm"/>
</template>
<div class="_gaps_m">
<SearchMarker>
<MkSwitch v-model="ipLoggingForm.state.enableIpLogging">
<template #label><SearchLabel>Enable</SearchLabel></template>
</MkSwitch>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
</div>
</SearchMarker>
<div class="_gaps_m">
<MkSwitch v-model="ipLoggingForm.state.enableIpLogging">
<template #label>Enable</template>
</MkSwitch>
</div>
</MkFolder>
</div>
</div>
</PageWithHeader>
</template>
@@ -4,14 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<SearchMarker markerId="serverRules" :keywords="['rules']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-checkbox"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.serverRules }}</SearchLabel></template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<div class="_gaps_m">
<div><SearchText>{{ i18n.ts._serverRules.description }}</SearchText></div>
<div>{{ i18n.ts._serverRules.description }}</div>
<Sortable
v-model="serverRules"
class="_gaps_m"
@@ -37,8 +33,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
</MkFolder>
</SearchMarker>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
@@ -46,9 +42,9 @@ import { defineAsyncComponent, ref, computed } from 'vue';
import * as os from '@/os.js';
import { fetchInstance, instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkFolder from '@/components/MkFolder.vue';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -64,6 +60,13 @@ const save = async () => {
const remove = (index: number): void => {
serverRules.value.splice(index, 1);
};
const headerTabs = computed(() => []);
definePage(() => ({
title: i18n.ts.serverRules,
icon: 'ti ti-checkbox',
}));
</script>
<style lang="scss" module>
+260 -337
View File
@@ -6,369 +6,292 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker path="/admin/settings" :label="i18n.ts.general" :keywords="['general', 'settings']" icon="ti ti-settings">
<div class="_gaps_m">
<SearchMarker v-slot="slotProps" :keywords="['information', 'meta']">
<MkFolder :defaultOpen="true">
<template #icon><SearchIcon><i class="ti ti-info-circle"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.info }}</SearchLabel></template>
<template v-if="infoForm.modified.value" #footer>
<MkFormFooter :form="infoForm"/>
</template>
<div class="_gaps_m">
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-info-circle"></i></template>
<template #label>{{ i18n.ts.info }}</template>
<template v-if="infoForm.modified.value" #footer>
<MkFormFooter :form="infoForm"/>
</template>
<div class="_gaps">
<SearchMarker :keywords="['name']">
<MkInput v-model="infoForm.state.name">
<template #label><SearchLabel>{{ i18n.ts.instanceName }}</SearchLabel><span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<div class="_gaps">
<MkInput v-model="infoForm.state.name">
<template #label>{{ i18n.ts.instanceName }}<span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<SearchMarker :keywords="['shortName']">
<MkInput v-model="infoForm.state.shortName">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.shortName }}</SearchLabel> ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.shortNameDescription }}</SearchText></template>
</MkInput>
</SearchMarker>
<MkInput v-model="infoForm.state.shortName">
<template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template>
</MkInput>
<SearchMarker :keywords="['description']">
<MkTextarea v-model="infoForm.state.description">
<template #label><SearchLabel>{{ i18n.ts.instanceDescription }}</SearchLabel><span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkTextarea>
</SearchMarker>
<MkTextarea v-model="infoForm.state.description">
<template #label>{{ i18n.ts.instanceDescription }}<span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkTextarea>
<FormSplit :minWidth="300">
<SearchMarker :keywords="['maintainer', 'name']">
<MkInput v-model="infoForm.state.maintainerName">
<template #label><SearchLabel>{{ i18n.ts.maintainerName }}</SearchLabel><span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<FormSplit :minWidth="300">
<MkInput v-model="infoForm.state.maintainerName">
<template #label>{{ i18n.ts.maintainerName }}<span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<SearchMarker :keywords="['maintainer', 'email', 'contact']">
<MkInput v-model="infoForm.state.maintainerEmail" type="email">
<template #label><SearchLabel>{{ i18n.ts.maintainerEmail }}</SearchLabel><span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-mail"></i></template>
</MkInput>
</SearchMarker>
</FormSplit>
<MkInput v-model="infoForm.state.maintainerEmail" type="email">
<template #label>{{ i18n.ts.maintainerEmail }}<span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-mail"></i></template>
</MkInput>
</FormSplit>
<SearchMarker :keywords="['tos', 'termsOfService']">
<MkInput v-model="infoForm.state.tosUrl" type="url">
<template #label><SearchLabel>{{ i18n.ts.tosUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<MkInput v-model="infoForm.state.tosUrl" type="url">
<template #label>{{ i18n.ts.tosUrl }}<span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<SearchMarker :keywords="['privacyPolicy']">
<MkInput v-model="infoForm.state.privacyPolicyUrl" type="url">
<template #label><SearchLabel>{{ i18n.ts.privacyPolicyUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<MkInput v-model="infoForm.state.privacyPolicyUrl" type="url">
<template #label>{{ i18n.ts.privacyPolicyUrl }}<span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<SearchMarker :keywords="['inquiry', 'contact']">
<MkInput v-model="infoForm.state.inquiryUrl" type="url">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.inquiryUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</SearchText></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<MkInput v-model="infoForm.state.inquiryUrl" type="url">
<template #label>{{ i18n.ts._serverSettings.inquiryUrl }}<span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<SearchMarker :keywords="['repository', 'url']">
<MkInput v-model="infoForm.state.repositoryUrl" type="url">
<template #label><SearchLabel>{{ i18n.ts.repositoryUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.repositoryUrlDescription }}</SearchText></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<MkInput v-model="infoForm.state.repositoryUrl" type="url">
<template #label>{{ i18n.ts.repositoryUrl }}<span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<MkInfo v-if="!instance.providesTarball && !infoForm.state.repositoryUrl" warn>
{{ i18n.ts.repositoryUrlOrTarballRequired }}
</MkInfo>
<MkInfo v-if="!instance.providesTarball && !infoForm.state.repositoryUrl" warn>
{{ i18n.ts.repositoryUrlOrTarballRequired }}
</MkInfo>
<SearchMarker :keywords="['impressum', 'legalNotice']">
<MkInput v-model="infoForm.state.impressumUrl" type="url">
<template #label><SearchLabel>{{ i18n.ts.impressumUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.impressumDescription }}</SearchText></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<MkInput v-model="infoForm.state.impressumUrl" type="url">
<template #label>{{ i18n.ts.impressumUrl }}<span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.impressumDescription }}</template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-user-star"></i></template>
<template #label>{{ i18n.ts.pinnedUsers }}</template>
<template v-if="pinnedUsersForm.modified.value" #footer>
<MkFormFooter :form="pinnedUsersForm"/>
</template>
<MkTextarea v-model="pinnedUsersForm.state.pinnedUsers">
<template #label>{{ i18n.ts.pinnedUsers }}<span v-if="pinnedUsersForm.modifiedStates.pinnedUsers" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
</MkTextarea>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-world-cog"></i></template>
<template #label>ServiceWorker</template>
<template v-if="serviceWorkerForm.modified.value" #footer>
<MkFormFooter :form="serviceWorkerForm"/>
</template>
<div class="_gaps">
<MkSwitch v-model="serviceWorkerForm.state.enableServiceWorker">
<template #label>{{ i18n.ts.enableServiceworker }}<span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
</MkSwitch>
<template v-if="serviceWorkerForm.state.enableServiceWorker">
<MkInput v-model="serviceWorkerForm.state.swPublicKey">
<template #label>Public key<span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
<MkInput v-model="serviceWorkerForm.state.swPrivateKey">
<template #label>Private key<span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
</template>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-ad"></i></template>
<template #label>{{ i18n.ts._ad.adsSettings }}</template>
<template v-if="adForm.modified.value" #footer>
<MkFormFooter :form="adForm"/>
</template>
<div class="_gaps">
<div class="_gaps_s">
<MkInput v-model="adForm.state.notesPerOneAd" :min="0" type="number">
<template #label>{{ i18n.ts._ad.notesPerOneAd }}<span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
</MkInput>
<MkInfo v-if="adForm.state.notesPerOneAd > 0 && adForm.state.notesPerOneAd < 20" :warn="true">
{{ i18n.ts._ad.adsTooClose }}
</MkInfo>
</div>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-world-search"></i></template>
<template #label>{{ i18n.ts._urlPreviewSetting.title }}</template>
<template v-if="urlPreviewForm.modified.value" #footer>
<MkFormFooter :form="urlPreviewForm"/>
</template>
<div class="_gaps">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewEnabled">
<template #label>{{ i18n.ts._urlPreviewSetting.enable }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkSwitch>
<template v-if="urlPreviewForm.state.urlPreviewEnabled">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewAllowRedirect">
<template #label>{{ i18n.ts._urlPreviewSetting.allowRedirect }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewAllowRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.allowRedirectDescription }}</template>
</MkSwitch>
<MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength">
<template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
</MkSwitch>
<MkInput v-model="urlPreviewForm.state.urlPreviewMaximumContentLength" type="number">
<template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
</MkInput>
<MkInput v-model="urlPreviewForm.state.urlPreviewTimeout" type="number">
<template #label>{{ i18n.ts._urlPreviewSetting.timeout }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
</MkInput>
<MkInput v-model="urlPreviewForm.state.urlPreviewUserAgent" type="text">
<template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
</MkInput>
<div>
<MkInput v-model="urlPreviewForm.state.urlPreviewSummaryProxyUrl" type="text">
<template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
</MkInput>
<div :class="$style.subCaption">
{{ i18n.ts._urlPreviewSetting.summaryProxyDescription2 }}
<ul style="padding-left: 20px; margin: 4px 0">
<li>{{ i18n.ts._urlPreviewSetting.timeout }} / key:timeout</li>
<li>{{ i18n.ts._urlPreviewSetting.maximumContentLength }} / key:contentLengthLimit</li>
<li>{{ i18n.ts._urlPreviewSetting.requireContentLength }} / key:contentLengthRequired</li>
<li>{{ i18n.ts._urlPreviewSetting.userAgent }} / key:userAgent</li>
</ul>
</div>
</div>
</MkFolder>
</SearchMarker>
</template>
</div>
</MkFolder>
<SearchMarker v-slot="slotProps" :keywords="['pinned', 'users']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-user-star"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.pinnedUsers }}</SearchLabel></template>
<template v-if="pinnedUsersForm.modified.value" #footer>
<MkFormFooter :form="pinnedUsersForm"/>
<MkFolder>
<template #icon><i class="ti ti-planet"></i></template>
<template #label>{{ i18n.ts.federation }}</template>
<template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template>
<template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template>
<template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template>
<template v-if="federationForm.modified.value" #footer>
<MkFormFooter :form="federationForm"/>
</template>
<div class="_gaps">
<MkRadios v-model="federationForm.state.federation">
<template #label>{{ i18n.ts.behavior }}<span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
<option value="all">{{ i18n.ts.all }}</option>
<option value="specified">{{ i18n.ts.specifyHost }}</option>
<option value="none">{{ i18n.ts.none }}</option>
</MkRadios>
<MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts">
<template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
</MkTextarea>
<MkFolder>
<template #icon><i class="ti ti-list"></i></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.deliverSuspendedSoftware }}</SearchLabel></template>
<template #footer>
<div class="_buttons">
<MkButton @click="federationForm.state.deliverSuspendedSoftware.push({software: '', versionRange: ''})"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
</div>
</template>
<MkTextarea v-model="pinnedUsersForm.state.pinnedUsers">
<template #label>{{ i18n.ts.pinnedUsers }}<span v-if="pinnedUsersForm.modifiedStates.pinnedUsers" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.pinnedUsersDescription }}</SearchText></template>
</MkTextarea>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['serviceWorker']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-world-cog"></i></SearchIcon></template>
<template #label><SearchLabel>ServiceWorker</SearchLabel></template>
<template v-if="serviceWorkerForm.modified.value" #footer>
<MkFormFooter :form="serviceWorkerForm"/>
</template>
<div class="_gaps">
<SearchMarker>
<MkSwitch v-model="serviceWorkerForm.state.enableServiceWorker">
<template #label><SearchLabel>{{ i18n.ts.enableServiceworker }}</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.serviceworkerInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<template v-if="serviceWorkerForm.state.enableServiceWorker">
<SearchMarker>
<MkInput v-model="serviceWorkerForm.state.swPublicKey">
<template #label><SearchLabel>Public key</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="serviceWorkerForm.state.swPrivateKey">
<template #label><SearchLabel>Private key</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
</SearchMarker>
</template>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['ads']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-ad"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts._ad.adsSettings }}</SearchLabel></template>
<template v-if="adForm.modified.value" #footer>
<MkFormFooter :form="adForm"/>
</template>
<div class="_gaps">
<div class="_gaps_s">
<SearchMarker>
<MkInput v-model="adForm.state.notesPerOneAd" :min="0" type="number">
<template #label><SearchLabel>{{ i18n.ts._ad.notesPerOneAd }}</SearchLabel><span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
</MkInput>
</SearchMarker>
<MkInfo v-if="adForm.state.notesPerOneAd > 0 && adForm.state.notesPerOneAd < 20" :warn="true">
{{ i18n.ts._ad.adsTooClose }}
</MkInfo>
<div :class="$style.metadataRoot" class="_gaps_s">
<MkInfo>{{ i18n.ts._serverSettings.deliverSuspendedSoftwareDescription }}</MkInfo>
<div v-for="(element, index) in federationForm.state.deliverSuspendedSoftware" :key="index" v-panel :class="$style.fieldDragItem">
<button class="_button" :class="$style.dragItemRemove" @click="federationForm.state.deliverSuspendedSoftware.splice(index, 1)"><i class="ti ti-x"></i></button>
<div :class="$style.dragItemForm">
<FormSplit :minWidth="200">
<MkInput v-model="element.software" small :placeholder="i18n.ts.softwareName">
</MkInput>
<MkInput v-model="element.versionRange" small :placeholder="i18n.ts.version">
</MkInput>
</FormSplit>
</div>
</div>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['url', 'preview']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-world-search"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.title }}</SearchLabel></template>
<template v-if="urlPreviewForm.modified.value" #footer>
<MkFormFooter :form="urlPreviewForm"/>
<MkSwitch v-model="federationForm.state.signToActivityPubGet">
<template #label>{{ i18n.ts._serverSettings.signToActivityPubGet }}<span v-if="federationForm.modifiedStates.signToActivityPubGet" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.signToActivityPubGet_description }}</template>
</MkSwitch>
<MkSwitch v-model="federationForm.state.proxyRemoteFiles">
<template #label>{{ i18n.ts._serverSettings.proxyRemoteFiles }}<span v-if="federationForm.modifiedStates.proxyRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.proxyRemoteFiles_description }}</template>
</MkSwitch>
<MkSwitch v-model="federationForm.state.allowExternalApRedirect">
<template #label>{{ i18n.ts._serverSettings.allowExternalApRedirect }}<span v-if="federationForm.modifiedStates.allowExternalApRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div>{{ i18n.ts._serverSettings.allowExternalApRedirect_description }}</div>
<div>{{ i18n.ts.needToRestartServerToApply }}</div>
</template>
</MkSwitch>
<div class="_gaps">
<SearchMarker>
<MkSwitch v-model="urlPreviewForm.state.urlPreviewEnabled">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.enable }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="federationForm.state.cacheRemoteFiles">
<template #label>{{ i18n.ts.cacheRemoteFiles }}<span v-if="federationForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
</MkSwitch>
<template v-if="urlPreviewForm.state.urlPreviewEnabled">
<SearchMarker :keywords="['allow', 'redirect']">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewAllowRedirect">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.allowRedirect }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewAllowRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.allowRedirectDescription }}</template>
</MkSwitch>
</SearchMarker>
<template v-if="federationForm.state.cacheRemoteFiles">
<MkSwitch v-model="federationForm.state.cacheRemoteSensitiveFiles">
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}<span v-if="federationForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
</MkSwitch>
</template>
</div>
</MkFolder>
<SearchMarker :keywords="['contentLength']">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.requireContentLength }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
</MkSwitch>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-ghost"></i></template>
<template #label>{{ i18n.ts.proxyAccount }}</template>
<template v-if="proxyAccountForm.modified.value" #footer>
<MkFormFooter :form="proxyAccountForm"/>
</template>
<SearchMarker :keywords="['contentLength']">
<MkInput v-model="urlPreviewForm.state.urlPreviewMaximumContentLength" type="number">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
</MkInput>
</SearchMarker>
<div class="_gaps">
<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<SearchMarker :keywords="['timeout']">
<MkInput v-model="urlPreviewForm.state.urlPreviewTimeout" type="number">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.timeout }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
</MkInput>
</SearchMarker>
<MkTextarea v-model="proxyAccountForm.state.description" :max="500" tall mfmAutocomplete :mfmPreview="true">
<template #label>{{ i18n.ts._profile.description }}</template>
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
</MkTextarea>
</div>
</MkFolder>
<SearchMarker :keywords="['userAgent']">
<MkInput v-model="urlPreviewForm.state.urlPreviewUserAgent" type="text">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.userAgent }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
</MkInput>
</SearchMarker>
<div>
<SearchMarker :keywords="['proxy']">
<MkInput v-model="urlPreviewForm.state.urlPreviewSummaryProxyUrl" type="text">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.summaryProxy }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
</MkInput>
</SearchMarker>
<div :class="$style.subCaption">
{{ i18n.ts._urlPreviewSetting.summaryProxyDescription2 }}
<ul style="padding-left: 20px; margin: 4px 0">
<li>{{ i18n.ts._urlPreviewSetting.timeout }} / key:timeout</li>
<li>{{ i18n.ts._urlPreviewSetting.maximumContentLength }} / key:contentLengthLimit</li>
<li>{{ i18n.ts._urlPreviewSetting.requireContentLength }} / key:contentLengthRequired</li>
<li>{{ i18n.ts._urlPreviewSetting.userAgent }} / key:userAgent</li>
</ul>
</div>
</div>
</template>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['federation']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-planet"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.federation }}</SearchLabel></template>
<template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template>
<template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template>
<template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template>
<template v-if="federationForm.modified.value" #footer>
<MkFormFooter :form="federationForm"/>
</template>
<div class="_gaps">
<SearchMarker>
<MkRadios v-model="federationForm.state.federation">
<template #label><SearchLabel>{{ i18n.ts.behavior }}</SearchLabel><span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
<option value="all">{{ i18n.ts.all }}</option>
<option value="specified">{{ i18n.ts.specifyHost }}</option>
<option value="none">{{ i18n.ts.none }}</option>
</MkRadios>
</SearchMarker>
<SearchMarker :keywords="['hosts']">
<MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts">
<template #label><SearchLabel>{{ i18n.ts.federationAllowedHosts }}</SearchLabel><span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
</MkTextarea>
</SearchMarker>
<SearchMarker :keywords="['suspended', 'software']">
<MkFolder>
<template #icon><i class="ti ti-list"></i></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.deliverSuspendedSoftware }}</SearchLabel></template>
<template #footer>
<div class="_buttons">
<MkButton @click="federationForm.state.deliverSuspendedSoftware.push({software: '', versionRange: ''})"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
</div>
</template>
<div :class="$style.metadataRoot" class="_gaps_s">
<MkInfo>{{ i18n.ts._serverSettings.deliverSuspendedSoftwareDescription }}</MkInfo>
<div v-for="(element, index) in federationForm.state.deliverSuspendedSoftware" :key="index" v-panel :class="$style.fieldDragItem">
<button class="_button" :class="$style.dragItemRemove" @click="federationForm.state.deliverSuspendedSoftware.splice(index, 1)"><i class="ti ti-x"></i></button>
<div :class="$style.dragItemForm">
<FormSplit :minWidth="200">
<MkInput v-model="element.software" small :placeholder="i18n.ts.softwareName">
</MkInput>
<MkInput v-model="element.versionRange" small :placeholder="i18n.ts.version">
</MkInput>
</FormSplit>
</div>
</div>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['sign', 'get']">
<MkSwitch v-model="federationForm.state.signToActivityPubGet">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.signToActivityPubGet }}</SearchLabel><span v-if="federationForm.modifiedStates.signToActivityPubGet" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.signToActivityPubGet_description }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['proxy', 'remote', 'files']">
<MkSwitch v-model="federationForm.state.proxyRemoteFiles">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.proxyRemoteFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.proxyRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.proxyRemoteFiles_description }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['allow', 'external', 'redirect']">
<MkSwitch v-model="federationForm.state.allowExternalApRedirect">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.allowExternalApRedirect }}</SearchLabel><span v-if="federationForm.modifiedStates.allowExternalApRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div><SearchText>{{ i18n.ts._serverSettings.allowExternalApRedirect_description }}</SearchText></div>
<div>{{ i18n.ts.needToRestartServerToApply }}</div>
</template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['cache', 'remote', 'files']">
<MkSwitch v-model="federationForm.state.cacheRemoteFiles">
<template #label><SearchLabel>{{ i18n.ts.cacheRemoteFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.cacheRemoteFilesDescription }}</SearchText>{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
</MkSwitch>
</SearchMarker>
<template v-if="federationForm.state.cacheRemoteFiles">
<SearchMarker :keywords="['cache', 'remote', 'sensitive', 'files']">
<MkSwitch v-model="federationForm.state.cacheRemoteSensitiveFiles">
<template #label><SearchLabel>{{ i18n.ts.cacheRemoteSensitiveFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</template>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['proxy', 'account']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-ghost"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.proxyAccount }}</SearchLabel></template>
<template v-if="proxyAccountForm.modified.value" #footer>
<MkFormFooter :form="proxyAccountForm"/>
</template>
<div class="_gaps">
<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<SearchMarker :keywords="['description']">
<MkTextarea v-model="proxyAccountForm.state.description" :max="500" tall mfmAutocomplete :mfmPreview="true">
<template #label><SearchLabel>{{ i18n.ts._profile.description }}</SearchLabel></template>
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
</MkTextarea>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<MkButton primary @click="openSetupWizard">
Open setup wizard
</MkButton>
</div>
</SearchMarker>
<MkButton primary @click="openSetupWizard">
Open setup wizard
</MkButton>
</div>
</div>
</PageWithHeader>
</template>
@@ -6,21 +6,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 900px;">
<SearchMarker path="/admin/system-webhook" label="SystemWebhook" :keywords="['webhook']" icon="ti ti-webhook">
<div class="_gaps_m">
<SearchMarker>
<MkButton primary @click="onCreateWebhookClicked">
<i class="ti ti-plus"></i> <SearchLabel>{{ i18n.ts._webhookSettings.createWebhook }}</SearchLabel>
</MkButton>
</SearchMarker>
<div class="_gaps_m">
<MkButton primary @click="onCreateWebhookClicked">
<i class="ti ti-plus"></i> {{ i18n.ts._webhookSettings.createWebhook }}
</MkButton>
<FormSection>
<div class="_gaps">
<XItem v-for="item in webhooks" :key="item.id" :entity="item" @edit="onEditButtonClicked" @delete="onDeleteButtonClicked"/>
</div>
</FormSection>
</div>
</SearchMarker>
<FormSection>
<div class="_gaps">
<XItem v-for="item in webhooks" :key="item.id" :entity="item" @edit="onEditButtonClicked" @delete="onDeleteButtonClicked"/>
</div>
</FormSection>
</div>
</div>
</PageWithHeader>
</template>
+2 -2
View File
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-shield-lock"></i></template>
<template #label><SearchLabel>{{ i18n.ts.totp }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.totpDescription }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts.totpDescription }}</SearchKeyword></template>
<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['password', 'less', 'key', 'passkey', 'login', 'signin']">
<MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)">
<template #label><SearchLabel>{{ i18n.ts.passwordLessLogin }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.passwordLessLoginDescription }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts.passwordLessLoginDescription }}</SearchKeyword></template>
</MkSwitch>
</SearchMarker>
</div>
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/account-data" :label="i18n.ts._settings.accountData" :keywords="['import', 'export', 'data', 'archive']" icon="ti ti-package">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/package_3d.png" color="#ff9100">
<SearchText>{{ i18n.ts._settings.accountDataBanner }}</SearchText>
<SearchKeyword>{{ i18n.ts._settings.accountDataBanner }}</SearchKeyword>
</MkFeatureBanner>
<div class="_gaps_s">
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/connect" :label="i18n.ts._settings.serviceConnection" :keywords="['app', 'service', 'connect', 'webhook', 'api', 'token']" icon="ti ti-link">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/link_3d.png" color="#ff0088">
<SearchText>{{ i18n.ts._settings.serviceConnectionBanner }}</SearchText>
<SearchKeyword>{{ i18n.ts._settings.serviceConnectionBanner }}</SearchKeyword>
</MkFeatureBanner>
<SearchMarker :keywords="['api', 'app', 'token', 'accessToken']">
@@ -7,8 +7,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts.customCssWarn }}</FormInfo>
<FormInfo v-if="isSafeMode" warn>{{ i18n.ts.customCssIsDisabledBecauseSafeMode }}</FormInfo>
<MkCodeEditor v-model="localCustomCss" manualSave lang="css">
<template #label>CSS</template>
</MkCodeEditor>
@@ -19,7 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, watch, computed } from 'vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import FormInfo from '@/components/MkInfo.vue';
import { isSafeMode } from '@@/js/config.js';
import * as os from '@/os.js';
import { unisonReload } from '@/utility/unison-reload.js';
import { i18n } from '@/i18n.js';
@@ -97,8 +97,8 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { prefer } from '@/preferences.js';
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
import { reloadAsk } from '@/utility/reload-ask.js';
import { selectFile } from '@/utility/drive.js';
import { suggestReload } from '@/utility/reload-suggest.js';
const navWindow = prefer.model('deck.navWindow');
const useSimpleUiForNonRootPages = prefer.model('deck.useSimpleUiForNonRootPages');
@@ -109,8 +109,8 @@ const menuPosition = prefer.model('deck.menuPosition');
const navbarPosition = prefer.model('deck.navbarPosition');
const wallpaper = prefer.model('deck.wallpaper');
watch(wallpaper, () => {
suggestReload();
watch(wallpaper, async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
function setWallpaper(ev: MouseEvent) {
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/drive" :label="i18n.ts.drive" :keywords="['drive']" icon="ti ti-cloud">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/cloud_3d.png" color="#0059ff">
<SearchText>{{ i18n.ts._settings.driveBanner }}</SearchText>
<SearchKeyword>{{ i18n.ts._settings.driveBanner }}</SearchKeyword>
</MkFeatureBanner>
<SearchMarker :keywords="['capacity', 'usage']">
@@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="keepOriginalFilename">
<MkSwitch v-model="keepOriginalFilename">
<template #label><SearchLabel>{{ i18n.ts.keepOriginalFilename }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['auto', 'nsfw', 'sensitive', 'media', 'file']">
<MkSwitch v-model="autoSensitive" @update:modelValue="saveProfile()">
<template #label><SearchLabel>{{ i18n.ts.enableAutoSensitive }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption><SearchText>{{ i18n.ts.enableAutoSensitiveDescription }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts.enableAutoSensitiveDescription }}</SearchKeyword></template>
</MkSwitch>
</SearchMarker>
</div>
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>{{ i18n.ts._preferencesBackup.autoPreferencesBackupIsNotEnabledForThisDevice }}</div>
<div><button class="_textButton" @click="enableAutoBackup">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipAutoBackup">{{ i18n.ts.skip }}</button></div>
</MkInfo>
<MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="searchIndex"></MkSuperMenu>
<MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="SETTING_INDEX"></MkSuperMenu>
</div>
</div>
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
@@ -42,12 +42,12 @@ import { instance } from '@/instance.js';
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import * as os from '@/os.js';
import { useRouter } from '@/router.js';
import { searchIndexes } from '@/utility/settings-search-index.js';
import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js';
import { store } from '@/store.js';
import { signout } from '@/signout.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
const searchIndex = await import('search-index:settings').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
const SETTING_INDEX = searchIndexes; // TODO: lazy load
const indexInfo = {
title: i18n.ts.settings,
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/mute-block" :label="i18n.ts.muteAndBlock" icon="ti ti-ban" :keywords="['mute', 'block']">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/prohibited_3d.png" color="#ff2600">
<SearchText>{{ i18n.ts._settings.muteAndBlockBanner }}</SearchText>
<SearchKeyword>{{ i18n.ts._settings.muteAndBlockBanner }}</SearchKeyword>
</MkFeatureBanner>
<div class="_gaps_s">
@@ -189,10 +189,10 @@ import { ensureSignin } from '@/i.js';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { reloadAsk } from '@/utility/reload-ask.js';
import { prefer } from '@/preferences.js';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import { Paginator } from '@/utility/paginator.js';
import { suggestReload } from '@/utility/reload-suggest.js';
const $i = ensureSignin();
@@ -216,8 +216,8 @@ const showSoftWordMutedWord = prefer.model('showSoftWordMutedWord');
watch([
showSoftWordMutedWord,
], () => {
suggestReload();
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
async function unrenoteMute(user, ev) {
@@ -64,6 +64,7 @@ import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { store } from '@/store.js';
import { reloadAsk } from '@/utility/reload-ask.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { prefer } from '@/preferences.js';
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/notifications" :label="i18n.ts.notifications" :keywords="['notifications']" icon="ti ti-bell">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/bell_3d.png" color="#ffff00">
<SearchText>{{ i18n.ts._settings.notificationsBanner }}</SearchText>
<SearchKeyword>{{ i18n.ts._settings.notificationsBanner }}</SearchKeyword>
</MkFeatureBanner>
<FormSection first>
@@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
<FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount"><SearchText>{{ i18n.ts._accountDelete.requestAccountDelete }}</SearchText></MkButton>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount"><SearchKeyword>{{ i18n.ts._accountDelete.requestAccountDelete }}</SearchKeyword></MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
</div>
</MkFolder>
@@ -155,13 +155,13 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { ensureSignin } from '@/i.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { reloadAsk } from '@/utility/reload-ask.js';
import FormSection from '@/components/form/section.vue';
import { prefer } from '@/preferences.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js';
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
import { suggestReload } from '@/utility/reload-suggest.js';
const $i = ensureSignin();
@@ -172,8 +172,8 @@ const devMode = prefer.model('devMode');
const stackingRouterView = prefer.model('experimental.stackingRouterView');
const enableFolderPageView = prefer.model('experimental.enableFolderPageView');
watch(skipNoteRender, () => {
suggestReload();
watch(skipNoteRender, async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
async function deleteAccount() {
@@ -7,12 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/plugin" :label="i18n.ts.plugins" :keywords="['plugin', 'addon', 'extension']" icon="ti ti-plug">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/electric_plug_3d.png" color="#ffbb00">
<SearchText>{{ i18n.ts._settings.pluginBanner }}</SearchText>
<SearchKeyword>{{ i18n.ts._settings.pluginBanner }}</SearchKeyword>
</MkFeatureBanner>
<MkInfo v-if="isSafeMode" warn>{{ i18n.ts.pluginsAreDisabledBecauseSafeMode }}</MkInfo>
<FormLink v-else to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
<FormLink to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
<FormSection>
<template #label>{{ i18n.ts.manage }}</template>
@@ -105,12 +103,10 @@ import MkCode from '@/components/MkCode.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin, reloadPlugin } from '@/plugin.js';
import { prefer } from '@/preferences.js';
import { isSafeMode } from '@@/js/config.js';
import * as os from '@/os.js';
const plugins = prefer.r.plugins;
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/preferences" :label="i18n.ts.preferences" :keywords="['general', 'preferences']" icon="ti ti-adjustments">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/gear_3d.png" color="#00ff9d">
<SearchText>{{ i18n.ts._settings.preferencesBanner }}</SearchText>
<SearchKeyword>{{ i18n.ts._settings.preferencesBanner }}</SearchKeyword>
</MkFeatureBanner>
<div class="_gaps_s">
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['realtimemode']">
<MkSwitch v-model="realtimeMode">
<template #label><i class="ti ti-bolt"></i> <SearchLabel>{{ i18n.ts.realtimeMode }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts._settings.realtimeMode_description }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts._settings.realtimeMode_description }}</SearchKeyword></template>
</MkSwitch>
</SearchMarker>
@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="pollingInterval">
<MkRange v-model="pollingInterval" :min="1" :max="3" :step="1" easing :showTicks="true" :textConverter="(v) => v === 1 ? i18n.ts.low : v === 2 ? i18n.ts.middle : v === 3 ? i18n.ts.high : ''">
<template #label><SearchLabel>{{ i18n.ts._settings.contentsUpdateFrequency }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts._settings.contentsUpdateFrequency_description }}</SearchText><br><SearchText>{{ i18n.ts._settings.contentsUpdateFrequency_description2 }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description }}</SearchKeyword><br><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description2 }}</SearchKeyword></template>
<template #prefix><i class="ti ti-player-play"></i></template>
<template #suffix><i class="ti ti-player-track-next"></i></template>
</MkRange>
@@ -165,7 +165,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="collapseRenotes">
<MkSwitch v-model="collapseRenotes">
<template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.collapseRenotesDescription }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -449,7 +449,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/mens_room_3d.png" color="#0011ff">
<SearchText>{{ i18n.ts._settings.accessibilityBanner }}</SearchText>
<SearchKeyword>{{ i18n.ts._settings.accessibilityBanner }}</SearchKeyword>
</MkFeatureBanner>
<div class="_gaps_s">
@@ -489,7 +489,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="enablePullToRefresh">
<MkSwitch v-model="enablePullToRefresh">
<template #label><SearchLabel>{{ i18n.ts._settings.enablePullToRefresh }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts._settings.enablePullToRefresh_description }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts._settings.enablePullToRefresh_description }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -567,50 +567,73 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><SearchIcon><i class="ti ti-battery-vertical-eco"></i></SearchIcon></template>
<div class="_gaps_s">
<SearchMarker :keywords="['animation', 'motion', 'reduce']">
<MkPreferenceContainer k="animation">
<MkSwitch :modelValue="!reduceAnimation" @update:modelValue="v => reduceAnimation = !v">
<template #label><SearchLabel>{{ i18n.ts._settings.uiAnimations }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
<SearchMarker :keywords="['lowpowermode', 'battery', 'eco', 'save']">
<MkPreferenceContainer k="lowPowerMode">
<MkSwitch v-model="lowPowerMode">
<template #label><SearchLabel>{{ i18n.ts.lowPowerMode }}</SearchLabel></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blur']">
<MkPreferenceContainer k="useBlurEffect">
<MkSwitch v-model="useBlurEffect">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<hr>
<SearchMarker :keywords="['blur', 'modal']">
<MkPreferenceContainer k="useBlurEffectForModal">
<MkSwitch v-model="useBlurEffectForModal">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<MkDisableSection :disabled="lowPowerMode">
<div class="_gaps_s">
<SearchMarker :keywords="['animation', 'image', 'gif']">
<MkPreferenceContainer k="disableShowingAnimatedImages">
<MkSwitch :modelValue="!disableShowingAnimatedImages" @update:modelValue="v => disableShowingAnimatedImages = !v">
<template #label><SearchLabel>{{ i18n.ts._settings.playAnimatedImages }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blurhash', 'image', 'photo', 'picture', 'thumbnail', 'placeholder']">
<MkPreferenceContainer k="enableHighQualityImagePlaceholders">
<MkSwitch v-model="enableHighQualityImagePlaceholders">
<template #label><SearchLabel>{{ i18n.ts._settings.enableHighQualityImagePlaceholders }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['animation', 'motion', 'reduce']">
<MkPreferenceContainer k="animation">
<MkSwitch :modelValue="!reduceAnimation" @update:modelValue="v => reduceAnimation = !v">
<template #label><SearchLabel>{{ i18n.ts._settings.uiAnimations }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['sticky']">
<MkPreferenceContainer k="useStickyIcons">
<MkSwitch v-model="useStickyIcons">
<template #label><SearchLabel>{{ i18n.ts._settings.useStickyIcons }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blur']">
<MkPreferenceContainer k="useBlurEffect">
<MkSwitch v-model="useBlurEffect">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blur', 'modal']">
<MkPreferenceContainer k="useBlurEffectForModal">
<MkSwitch v-model="useBlurEffectForModal">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blurhash', 'image', 'photo', 'picture', 'thumbnail', 'placeholder']">
<MkPreferenceContainer k="enableHighQualityImagePlaceholders">
<MkSwitch v-model="enableHighQualityImagePlaceholders">
<template #label><SearchLabel>{{ i18n.ts._settings.enableHighQualityImagePlaceholders }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['sticky']">
<MkPreferenceContainer k="useStickyIcons">
<MkSwitch v-model="useStickyIcons">
<template #label><SearchLabel>{{ i18n.ts._settings.useStickyIcons }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
</div>
</MkDisableSection>
<MkInfo>
<div class="_gaps_s">
@@ -795,6 +818,7 @@ import MkInfo from '@/components/MkInfo.vue';
import { store } from '@/store.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { reloadAsk } from '@/utility/reload-ask.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { miLocalStorage } from '@/local-storage.js';
@@ -806,7 +830,6 @@ import { claimAchievement } from '@/utility/achievements.js';
import { instance } from '@/instance.js';
import { ensureSignin } from '@/i.js';
import { genId } from '@/utility/id.js';
import { suggestReload } from '@/utility/reload-suggest.js';
const $i = ensureSignin();
@@ -871,6 +894,7 @@ const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPla
const contextMenu = prefer.model('contextMenu');
const menuStyle = prefer.model('menuStyle');
const makeEveryTextElementsSelectable = prefer.model('makeEveryTextElementsSelectable');
const lowPowerMode = prefer.model('lowPowerMode');
const fontSize = ref(miLocalStorage.getItem('fontSize'));
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
@@ -928,8 +952,9 @@ watch([
enablePullToRefresh,
reduceAnimation,
showAvailableReactionsFirstInNote,
], () => {
suggestReload();
lowPowerMode,
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const;
@@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/privacy" :label="i18n.ts.privacy" :keywords="['privacy']" icon="ti ti-lock-open">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/unlocked_3d.png" color="#aeff00">
<SearchText>{{ i18n.ts._settings.privacyBanner }}</SearchText>
<SearchKeyword>{{ i18n.ts._settings.privacyBanner }}</SearchKeyword>
</MkFeatureBanner>
<SearchMarker :keywords="['follow', 'lock']">
<MkSwitch v-model="isLocked" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.makeFollowManuallyApprove }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.lockedAccountInfo }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts.lockedAccountInfo }}</SearchKeyword></template>
</MkSwitch>
</SearchMarker>
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['reaction', 'public']">
<MkSwitch v-model="publicReactions" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.makeReactionsPublic }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.makeReactionsPublicDescription }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts.makeReactionsPublicDescription }}</SearchKeyword></template>
</MkSwitch>
</SearchMarker>
@@ -53,28 +53,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['online', 'status']">
<MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.hideOnlineStatus }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.hideOnlineStatusDescription }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts.hideOnlineStatusDescription }}</SearchKeyword></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['crawle', 'index', 'search']">
<MkSwitch v-model="noCrawle" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.noCrawle }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.noCrawleDescription }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts.noCrawleDescription }}</SearchKeyword></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['crawle', 'ai']">
<MkSwitch v-model="preventAiLearning" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.preventAiLearning }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.preventAiLearningDescription }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts.preventAiLearningDescription }}</SearchKeyword></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['explore']">
<MkSwitch v-model="isExplorable" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.makeExplorable }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.makeExplorableDescription }}</SearchText></template>
<template #caption><SearchKeyword>{{ i18n.ts.makeExplorableDescription }}</SearchKeyword></template>
</MkSwitch>
</SearchMarker>
@@ -146,7 +146,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<template #caption>
<div><SearchText>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</SearchText></div>
<div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</SearchKeyword></div>
</template>
</FormSlot>
</SearchMarker>
@@ -183,7 +183,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<template #caption>
<div><SearchText>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</SearchText></div>
<div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</SearchKeyword></div>
</template>
</FormSlot>
</SearchMarker>
@@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false">
<template #label><SearchLabel>{{ i18n.ts._profile.followedMessage }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption>
<div><SearchText>{{ i18n.ts._profile.followedMessageDescription }}</SearchText></div>
<div><SearchKeyword>{{ i18n.ts._profile.followedMessageDescription }}</SearchKeyword></div>
<div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
</template>
</MkInput>
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/security" :label="i18n.ts.security" :keywords="['security']" icon="ti ti-lock" :inlining="['2fa']">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/locked_with_key_3d.png" color="#ffbf00">
<SearchText>{{ i18n.ts._settings.securityBanner }}</SearchText>
<SearchKeyword>{{ i18n.ts._settings.securityBanner }}</SearchKeyword>
</MkFeatureBanner>
<SearchMarker :keywords="['password']">
@@ -24,34 +24,30 @@ SPDX-License-Identifier: AGPL-3.0-only
<X2fa/>
<SearchMarker :keywords="['signin', 'login', 'history', 'log']">
<FormSection>
<template #label><SearchLabel>{{ i18n.ts.signinHistory }}</SearchLabel></template>
<MkPagination :paginator="paginator" withControl>
<template #default="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
<header>
<i v-if="item.success" class="ti ti-check icon succ"></i>
<i v-else class="ti ti-circle-x icon fail"></i>
<code class="ip _monospace">{{ item.ip }}</code>
<MkTime :time="item.createdAt" class="time"/>
</header>
</div>
<FormSection>
<template #label>{{ i18n.ts.signinHistory }}</template>
<MkPagination :paginator="paginator" withControl>
<template #default="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
<header>
<i v-if="item.success" class="ti ti-check icon succ"></i>
<i v-else class="ti ti-circle-x icon fail"></i>
<code class="ip _monospace">{{ item.ip }}</code>
<MkTime :time="item.createdAt" class="time"/>
</header>
</div>
</template>
</MkPagination>
</FormSection>
</SearchMarker>
</div>
</template>
</MkPagination>
</FormSection>
<SearchMarker :keywords="['regenerate', 'refresh', 'reset', 'token']">
<FormSection>
<FormSlot>
<MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> <SearchLabel>{{ i18n.ts.regenerateLoginToken }}</SearchLabel></MkButton>
<template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
</FormSlot>
</FormSection>
</SearchMarker>
<FormSection>
<FormSlot>
<MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton>
<template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
</FormSlot>
</FormSection>
</div>
</SearchMarker>
</template>
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/sounds" :label="i18n.ts.sounds" :keywords="['sounds']" icon="ti ti-music">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/speaker_high_volume_3d.png" color="#ff006f">
<SearchText>{{ i18n.ts._settings.soundsBanner }}</SearchText>
<SearchKeyword>{{ i18n.ts._settings.soundsBanner }}</SearchKeyword>
</MkFeatureBanner>
<SearchMarker :keywords="['mute']">
@@ -35,9 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<MkInfo v-if="isSafeMode" warn>{{ i18n.ts.themeIsDefaultBecauseSafeMode }}</MkInfo>
<div v-else class="_gaps">
<div class="_gaps">
<template v-if="!store.r.darkMode.value">
<SearchMarker :keywords="['light', 'theme']">
<MkFolder :defaultOpen="true" :max-height="500">
@@ -206,14 +204,12 @@ import JSON5 from 'json5';
import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import type { Theme } from '@/theme.js';
import { isSafeMode } from '@@/js/config.js';
import * as os from '@/os.js';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkThemePreview from '@/components/MkThemePreview.vue';
import MkInfo from '@/components/MkInfo.vue';
import { getBuiltinThemesRef, getThemesRef, removeTheme } from '@/theme.js';
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
import { store } from '@/store.js';
-2
View File
@@ -6,7 +6,6 @@
import { ref, defineAsyncComponent } from 'vue';
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
import { compareVersions } from 'compare-versions';
import { isSafeMode } from '@@/js/config.js';
import { genId } from '@/utility/id.js';
import * as Misskey from 'misskey-js';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
@@ -233,7 +232,6 @@ export function launchPlugins() {
}
async function launchPlugin(id: Plugin['installId']): Promise<void> {
if (isSafeMode) return;
const plugin = prefer.s.plugins.find(x => x.installId === id);
if (!plugin) return;
+455 -447
View File
@@ -35,464 +35,472 @@ export type SoundStore = {
// NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
export const PREF_DEF = definePreferences({
accounts: {
default: [] as [host: string, user: {
id: string;
username: string;
}][],
},
pinnedUserLists: {
accountDependent: true,
default: [] as Misskey.entities.UserList[],
},
uploadFolder: {
accountDependent: true,
default: null as string | null,
},
widgets: {
accountDependent: true,
default: () => [{
name: 'calendar',
id: genId(), place: 'right', data: {},
}, {
name: 'notifications',
id: genId(), place: 'right', data: {},
}, {
name: 'trends',
id: genId(), place: 'right', data: {},
}] as {
name: string;
id: string;
place: string | null;
data: Record<string, any>;
}[],
},
'deck.profile': {
accountDependent: true,
default: null as string | null,
},
'deck.profiles': {
accountDependent: true,
default: [] as DeckProfile[],
},
emojiPalettes: {
serverDependent: true,
default: () => [{
id: genId(),
name: '',
emojis: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
}] as {
id: string;
name: string;
emojis: string[];
}[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
states: {
accounts: {
default: [] as [host: string, user: {
id: string;
username: string;
}][],
},
},
emojiPaletteForReaction: {
serverDependent: true,
default: null as string | null,
},
emojiPaletteForMain: {
serverDependent: true,
default: null as string | null,
},
overridedDeviceKind: {
default: null as DeviceKind | null,
},
themes: {
default: [] as Theme[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
pinnedUserLists: {
accountDependent: true,
default: [] as Misskey.entities.UserList[],
},
},
lightTheme: {
default: null as Theme | null,
},
darkTheme: {
default: null as Theme | null,
},
syncDeviceDarkMode: {
default: true,
},
defaultNoteVisibility: {
default: 'public' as (typeof Misskey.noteVisibilities)[number],
},
defaultNoteLocalOnly: {
default: false,
},
keepCw: {
default: true,
},
rememberNoteVisibility: {
default: false,
},
reportError: {
default: false,
},
collapseRenotes: {
default: true,
},
menu: {
default: [
'notifications',
'clips',
'drive',
'followRequests',
'chat',
'-',
'explore',
'announcements',
'channels',
'search',
'-',
'ui',
],
},
statusbars: {
default: [] as {
name: string;
id: string;
type: string;
size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
black: boolean;
props: Record<string, any>;
}[],
},
serverDisconnectedBehavior: {
default: 'quiet' as 'quiet' | 'reload' | 'dialog',
},
nsfw: {
default: 'respect' as 'respect' | 'force' | 'ignore',
},
highlightSensitiveMedia: {
default: false,
},
animation: {
default: !window.matchMedia('(prefers-reduced-motion)').matches,
},
animatedMfm: {
default: !window.matchMedia('(prefers-reduced-motion)').matches,
},
advancedMfm: {
default: true,
},
showReactionsCount: {
default: false,
},
enableQuickAddMfmFunction: {
default: false,
},
loadRawImages: {
default: false,
},
imageNewTab: {
default: false,
},
disableShowingAnimatedImages: {
default: window.matchMedia('(prefers-reduced-motion)').matches,
},
emojiStyle: {
default: 'twemoji', // twemoji / fluentEmoji / native
},
menuStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer',
},
useBlurEffectForModal: {
default: true,
},
useBlurEffect: {
default: true,
},
useStickyIcons: {
default: true,
},
enableHighQualityImagePlaceholders: {
default: true,
},
showFixedPostForm: {
default: false,
},
showFixedPostFormInChannel: {
default: false,
},
enableInfiniteScroll: {
default: true,
},
useReactionPickerForContextMenu: {
default: false,
},
instanceTicker: {
default: 'remote' as 'none' | 'remote' | 'always',
},
emojiPickerScale: {
default: 2,
},
emojiPickerWidth: {
default: 2,
},
emojiPickerHeight: {
default: 3,
},
emojiPickerStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer',
},
squareAvatars: {
default: false,
},
showAvatarDecorations: {
default: true,
},
numberOfPageCache: {
default: 3,
},
pollingInterval: {
uploadFolder: {
accountDependent: true,
default: null as string | null,
},
widgets: {
accountDependent: true,
default: () => [{
name: 'calendar',
id: genId(), place: 'right', data: {},
}, {
name: 'notifications',
id: genId(), place: 'right', data: {},
}, {
name: 'trends',
id: genId(), place: 'right', data: {},
}] as {
name: string;
id: string;
place: string | null;
data: Record<string, any>;
}[],
},
'deck.profile': {
accountDependent: true,
default: null as string | null,
},
'deck.profiles': {
accountDependent: true,
default: [] as DeckProfile[],
},
emojiPalettes: {
serverDependent: true,
default: () => [{
id: genId(),
name: '',
emojis: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
}] as {
id: string;
name: string;
emojis: string[];
}[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
},
},
emojiPaletteForReaction: {
serverDependent: true,
default: null as string | null,
},
emojiPaletteForMain: {
serverDependent: true,
default: null as string | null,
},
overridedDeviceKind: {
default: null as DeviceKind | null,
},
themes: {
default: [] as Theme[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
},
},
lightTheme: {
default: null as Theme | null,
},
darkTheme: {
default: null as Theme | null,
},
syncDeviceDarkMode: {
default: true,
},
defaultNoteVisibility: {
default: 'public' as (typeof Misskey.noteVisibilities)[number],
},
defaultNoteLocalOnly: {
default: false,
},
keepCw: {
default: true,
},
rememberNoteVisibility: {
default: false,
},
reportError: {
default: false,
},
collapseRenotes: {
default: true,
},
menu: {
default: [
'notifications',
'clips',
'drive',
'followRequests',
'chat',
'-',
'explore',
'announcements',
'channels',
'search',
'-',
'ui',
],
},
statusbars: {
default: [] as {
name: string;
id: string;
type: string;
size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
black: boolean;
props: Record<string, any>;
}[],
},
serverDisconnectedBehavior: {
default: 'quiet' as 'quiet' | 'reload' | 'dialog',
},
nsfw: {
default: 'respect' as 'respect' | 'force' | 'ignore',
},
highlightSensitiveMedia: {
default: false,
},
animation: {
default: !window.matchMedia('(prefers-reduced-motion)').matches,
},
animatedMfm: {
default: !window.matchMedia('(prefers-reduced-motion)').matches,
},
advancedMfm: {
default: true,
},
showReactionsCount: {
default: false,
},
enableQuickAddMfmFunction: {
default: false,
},
loadRawImages: {
default: false,
},
imageNewTab: {
default: false,
},
disableShowingAnimatedImages: {
default: window.matchMedia('(prefers-reduced-motion)').matches,
},
emojiStyle: {
default: 'twemoji', // twemoji / fluentEmoji / native
},
menuStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer',
},
useBlurEffectForModal: {
default: true,
},
useBlurEffect: {
default: true,
},
useStickyIcons: {
default: true,
},
enableHighQualityImagePlaceholders: {
default: true,
},
showFixedPostForm: {
default: false,
},
showFixedPostFormInChannel: {
default: false,
},
enableInfiniteScroll: {
default: true,
},
useReactionPickerForContextMenu: {
default: false,
},
instanceTicker: {
default: 'remote' as 'none' | 'remote' | 'always',
},
emojiPickerScale: {
default: 2,
},
emojiPickerWidth: {
default: 2,
},
emojiPickerHeight: {
default: 3,
},
emojiPickerStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer',
},
squareAvatars: {
default: false,
},
showAvatarDecorations: {
default: true,
},
numberOfPageCache: {
default: 3,
},
pollingInterval: {
// 1 ... 低
// 2 ... 中
// 3 ... 高
default: 2,
},
showNoteActionsOnlyHover: {
default: false,
},
showClipButtonInNoteFooter: {
default: false,
},
reactionsDisplaySize: {
default: 'medium' as 'small' | 'medium' | 'large',
},
limitWidthOfReaction: {
default: true,
},
forceShowAds: {
default: false,
},
aiChanMode: {
default: false,
},
devMode: {
default: false,
},
mediaListWithOneImageAppearance: {
default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3',
},
notificationPosition: {
default: 'rightBottom' as 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom',
},
notificationStackAxis: {
default: 'horizontal' as 'vertical' | 'horizontal',
},
enableCondensedLine: {
default: true,
},
keepScreenOn: {
default: false,
},
useGroupedNotifications: {
default: true,
},
dataSaver: {
default: {
media: false,
avatar: false,
urlPreviewThumbnail: false,
disableUrlPreview: false,
code: false,
} satisfies Record<string, boolean>,
},
hemisphere: {
default: hemisphere as 'N' | 'S',
},
enableSeasonalScreenEffect: {
default: false,
},
enableHorizontalSwipe: {
default: false,
},
enablePullToRefresh: {
default: true,
},
useNativeUiForVideoAudioPlayer: {
default: false,
},
keepOriginalFilename: {
default: true,
},
alwaysConfirmFollow: {
default: true,
},
confirmWhenRevealingSensitiveMedia: {
default: false,
},
contextMenu: {
default: 'app' as 'app' | 'appWithShift' | 'native',
},
skipNoteRender: {
default: true,
},
showSoftWordMutedWord: {
default: false,
},
confirmOnReact: {
default: false,
},
defaultFollowWithReplies: {
default: false,
},
makeEveryTextElementsSelectable: {
default: DEFAULT_DEVICE_KIND === 'desktop',
},
showNavbarSubButtons: {
default: true,
},
showTitlebar: {
default: false,
},
showAvailableReactionsFirstInNote: {
default: false,
},
plugins: {
default: [] as Plugin[],
mergeStrategy: (a, b) => {
const sameIdExists = a.some(x => b.some(y => x.installId === y.installId));
if (sameIdExists) throw new Error();
const sameNameExists = a.some(x => b.some(y => x.name === y.name));
if (sameNameExists) throw new Error();
return a.concat(b);
default: 2,
},
},
mutingEmojis: {
default: [] as string[],
mergeStrategy: (a, b) => {
return [...new Set(a.concat(b))];
showNoteActionsOnlyHover: {
default: false,
},
},
watermarkPresets: {
accountDependent: true,
default: [] as WatermarkPreset[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
showClipButtonInNoteFooter: {
default: false,
},
reactionsDisplaySize: {
default: 'medium' as 'small' | 'medium' | 'large',
},
limitWidthOfReaction: {
default: true,
},
forceShowAds: {
default: false,
},
aiChanMode: {
default: false,
},
devMode: {
default: false,
},
mediaListWithOneImageAppearance: {
default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3',
},
notificationPosition: {
default: 'rightBottom' as 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom',
},
notificationStackAxis: {
default: 'horizontal' as 'vertical' | 'horizontal',
},
enableCondensedLine: {
default: true,
},
keepScreenOn: {
default: false,
},
useGroupedNotifications: {
default: true,
},
dataSaver: {
default: {
media: false,
avatar: false,
urlPreviewThumbnail: false,
disableUrlPreview: false,
code: false,
} satisfies Record<string, boolean>,
},
hemisphere: {
default: hemisphere as 'N' | 'S',
},
enableSeasonalScreenEffect: {
default: false,
},
enableHorizontalSwipe: {
default: false,
},
enablePullToRefresh: {
default: true,
},
useNativeUiForVideoAudioPlayer: {
default: false,
},
keepOriginalFilename: {
default: true,
},
alwaysConfirmFollow: {
default: true,
},
confirmWhenRevealingSensitiveMedia: {
default: false,
},
contextMenu: {
default: 'app' as 'app' | 'appWithShift' | 'native',
},
skipNoteRender: {
default: true,
},
showSoftWordMutedWord: {
default: false,
},
confirmOnReact: {
default: false,
},
defaultFollowWithReplies: {
default: false,
},
makeEveryTextElementsSelectable: {
default: DEFAULT_DEVICE_KIND === 'desktop',
},
showNavbarSubButtons: {
default: true,
},
showTitlebar: {
default: false,
},
showAvailableReactionsFirstInNote: {
default: false,
},
plugins: {
default: [] as Plugin[],
mergeStrategy: (a, b) => {
const sameIdExists = a.some(x => b.some(y => x.installId === y.installId));
if (sameIdExists) throw new Error();
const sameNameExists = a.some(x => b.some(y => x.name === y.name));
if (sameNameExists) throw new Error();
return a.concat(b);
},
},
mutingEmojis: {
default: [] as string[],
mergeStrategy: (a, b) => {
return [...new Set(a.concat(b))];
},
},
watermarkPresets: {
accountDependent: true,
default: [] as WatermarkPreset[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
return mergedItems;
},
},
defaultWatermarkPresetId: {
accountDependent: true,
default: null as WatermarkPreset['id'] | null,
},
defaultImageCompressionLevel: {
default: 2 as 0 | 1 | 2 | 3,
},
lowPowerMode: {
default: false,
},
'sound.masterVolume': {
default: 0.5,
},
'sound.notUseSound': {
default: false,
},
'sound.useSoundOnlyWhenActive': {
default: false,
},
'sound.on.note': {
default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore,
},
'sound.on.noteMy': {
default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
},
'sound.on.notification': {
default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
},
'sound.on.reaction': {
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
},
'sound.on.chatMessage': {
default: { type: 'syuilo/waon', volume: 1 } as SoundStore,
},
'deck.alwaysShowMainColumn': {
default: true,
},
'deck.navWindow': {
default: true,
},
'deck.useSimpleUiForNonRootPages': {
default: true,
},
'deck.columnAlign': {
default: 'center' as 'left' | 'right' | 'center',
},
'deck.columnGap': {
default: 6,
},
'deck.menuPosition': {
default: 'bottom' as 'right' | 'bottom',
},
'deck.navbarPosition': {
default: 'left' as 'left' | 'top' | 'bottom',
},
'deck.wallpaper': {
default: null as string | null,
},
'chat.showSenderName': {
default: false,
},
'chat.sendOnEnter': {
default: false,
},
'game.dropAndFusion': {
default: {
bgmVolume: 0.25,
sfxVolume: 1,
},
},
'experimental.stackingRouterView': {
default: false,
},
'experimental.enableFolderPageView': {
default: false,
},
},
defaultWatermarkPresetId: {
accountDependent: true,
default: null as WatermarkPreset['id'] | null,
},
defaultImageCompressionLevel: {
default: 2 as 0 | 1 | 2 | 3,
},
'sound.masterVolume': {
default: 0.5,
},
'sound.notUseSound': {
default: false,
},
'sound.useSoundOnlyWhenActive': {
default: false,
},
'sound.on.note': {
default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore,
},
'sound.on.noteMy': {
default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
},
'sound.on.notification': {
default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
},
'sound.on.reaction': {
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
},
'sound.on.chatMessage': {
default: { type: 'syuilo/waon', volume: 1 } as SoundStore,
},
'deck.alwaysShowMainColumn': {
default: true,
},
'deck.navWindow': {
default: true,
},
'deck.useSimpleUiForNonRootPages': {
default: true,
},
'deck.columnAlign': {
default: 'center' as 'left' | 'right' | 'center',
},
'deck.columnGap': {
default: 6,
},
'deck.menuPosition': {
default: 'bottom' as 'right' | 'bottom',
},
'deck.navbarPosition': {
default: 'left' as 'left' | 'top' | 'bottom',
},
'deck.wallpaper': {
default: null as string | null,
},
'chat.showSenderName': {
default: false,
},
'chat.sendOnEnter': {
default: false,
},
'game.dropAndFusion': {
default: {
bgmVolume: 0.25,
sfxVolume: 1,
},
},
'experimental.stackingRouterView': {
default: false,
},
'experimental.enableFolderPageView': {
default: false,
computed: {
disableShowingAnimatedImages: (s) => s.disableShowingAnimatedImages || s.lowPowerMode,
},
});
+12 -2
View File
@@ -101,9 +101,19 @@ type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) =>
export type PreferencesDefinition = Record<string, PreferencesDefinitionRecord<any>>;
export function definePreferences<T extends Record<string, unknown>>(x: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>
states: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>;
};
computed: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>;
};
}): {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>
states: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>;
};
computed: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>;
};
} {
return x;
}
@@ -491,6 +491,10 @@ export const ROUTE_DEF = [{
path: '/performance',
name: 'performance',
component: page(() => import('@/pages/admin/performance.vue')),
}, {
path: '/server-rules',
name: 'server-rules',
component: page(() => import('@/pages/admin/server-rules.vue')),
}, {
path: '/invites',
name: 'invites',
+5 -4
View File
@@ -37,6 +37,11 @@ html {
color: var(--MI_THEME-fg);
accent-color: var(--MI_THEME-accent);
&, * {
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
scrollbar-width: thin;
}
&.f-1 {
font-size: 15px;
}
@@ -86,11 +91,7 @@ html::selection {
100% {
opacity: 0;
}
}
html, body, main, div {
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
scrollbar-width: thin;
}
html,
@@ -48,6 +48,10 @@ function skip() {
.title {
padding: 0 10px;
font-weight: bold;
&:empty {
display: none;
}
}
.body {
@@ -1,71 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<span :class="$style.icon">
<i class="ti ti-info-circle"></i>
</span>
<span :class="$style.title">{{ i18n.ts.reloadRequiredToApplySettings }}</span>
<span :class="$style.body"><button class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" @click="reload">{{ i18n.ts.reload }}</button> | <button class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" @click="skip">{{ i18n.ts.skip }}</button></span>
</div>
</template>
<script lang="ts" setup>
import { i18n } from '@/i18n.js';
import { shouldSuggestReload } from '@/utility/reload-suggest.js';
import { unisonReload } from '@/utility/unison-reload.js';
function reload() {
unisonReload();
}
function skip() {
shouldSuggestReload.value = false;
}
</script>
<style lang="scss" module>
.root {
--height: 24px;
font-size: 0.85em;
display: flex;
vertical-align: bottom;
width: 100%;
line-height: var(--height);
height: var(--height);
overflow: clip;
contain: strict;
background: var(--MI_THEME-accent);
color: var(--MI_THEME-fgOnAccent);
}
.icon {
margin-left: 10px;
animation: blink 2s infinite;
}
.title {
padding: 0 10px;
font-weight: bold;
animation: blink 2s infinite;
}
.body {
min-width: 0;
flex: 1;
overflow: clip;
white-space: nowrap;
text-overflow: ellipsis;
}
@keyframes blink {
0% { opacity: 1; }
10% { opacity: 1; }
50% { opacity: 0; }
90% { opacity: 1; }
100% { opacity: 1; }
}
</style>
+1 -21
View File
@@ -94,11 +94,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="dev" id="devTicker"><span style="animation: dev-ticker-blink 2s infinite;">DEV BUILD</span></div>
<div v-if="$i && $i.isBot" id="botWarn"><span style="animation: dev-ticker-blink 2s infinite;">{{ i18n.ts.loggedInAsBot }}</span></div>
<div v-if="isSafeMode" id="safemodeWarn">
<span style="animation: dev-ticker-blink 2s infinite;">{{ i18n.ts.safeModeEnabled }}</span>&nbsp;
<button class="_textButton" style="pointer-events: all;" @click="exitSafeMode">{{ i18n.ts.turnItOff }}</button>
</div>
</template>
<script lang="ts" setup>
@@ -106,10 +101,7 @@ import { defineAsyncComponent, ref, TransitionGroup } from 'vue';
import * as Misskey from 'misskey-js';
import { swInject } from './sw-inject.js';
import XNotification from './notification.vue';
import { isSafeMode } from '@@/js/config.js';
import { popups } from '@/os.js';
import { unisonReload } from '@/utility/unison-reload.js';
import { miLocalStorage } from '@/local-storage.js';
import { pendingApiRequestsCount } from '@/utility/misskey-api.js';
import * as sound from '@/utility/sound.js';
import { $i } from '@/i.js';
@@ -152,13 +144,6 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
sound.playMisskeySfx('notification');
}
function exitSafeMode() {
miLocalStorage.removeItem('isSafeMode');
const url = new URL(window.location.href);
url.searchParams.delete('safemode');
unisonReload(url.toString());
}
if ($i) {
if (store.s.realtimeMode) {
const connection = useStream().useChannel('main');
@@ -411,7 +396,7 @@ if ($i) {
width: 100%;
height: max-content;
text-align: center;
z-index: 2147483646;
z-index: 2147483647;
color: #ff0;
background: rgba(0, 0, 0, 0.5);
padding: 4px 7px;
@@ -420,11 +405,6 @@ if ($i) {
user-select: none;
}
#safemodeWarn {
@extend #botWarn;
z-index: 2147483647;
}
#devTicker {
position: fixed;
bottom: 0;
+1 -7
View File
@@ -13,8 +13,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.main, { [$style.withWallpaper]: withWallpaper, [$style.withSidebarAndTitlebar]: !isMobile && prefer.r['deck.navbarPosition'].value === 'left' && prefer.r.showTitlebar.value }]" :style="{ backgroundImage: prefer.s['deck.wallpaper'] != null ? `url(${ prefer.s['deck.wallpaper'] })` : null }">
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'"/>
<XReloadSuggest v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XAnnouncements v-if="$i"/>
<XStatusBars/>
<div :class="$style.columnsWrapper">
@@ -83,14 +81,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, ref, useTemplateRef } from 'vue';
import XCommon from './_common_/common.vue';
import { genId } from '@/utility/id.js';
import XCommon from './_common_/common.vue';
import XSidebar from '@/ui/_common_/navbar.vue';
import XNavbarH from '@/ui/_common_/navbar-h.vue';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XReloadSuggest from '@/ui/_common_/ReloadSuggest.vue';
import * as os from '@/os.js';
import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
@@ -109,8 +105,6 @@ import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
import XChatColumn from '@/ui/deck/chat-column.vue';
import { mainRouter } from '@/router.js';
import { columns, layout, columnTypes, switchProfileMenu, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js';
import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
import { shouldSuggestReload } from '@/utility/reload-suggest.js';
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
-3
View File
@@ -12,7 +12,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.contents, !isMobile && prefer.r.showTitlebar.value ? $style.withSidebarAndTitlebar : null]" @contextmenu.stop="onContextmenu">
<div>
<XReloadSuggest v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XAnnouncements v-if="$i"/>
<XStatusBars :class="$style.statusbars"/>
@@ -39,7 +38,6 @@ import XCommon from './_common_/common.vue';
import type { PageMetadata } from '@/page.js';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XReloadSuggest from '@/ui/_common_/ReloadSuggest.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue';
import XSidebar from '@/ui/_common_/navbar.vue';
import * as os from '@/os.js';
@@ -52,7 +50,6 @@ import { mainRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
import { DI } from '@/di.js';
import { shouldSuggestReload } from '@/utility/reload-suggest.js';
const XWidgets = defineAsyncComponent(() => import('./_common_/widgets.vue'));
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
@@ -44,8 +44,6 @@
"😑": ["face", "indifferent", "-_-", "meh", "deadpan"],
"😒": ["indifference", "bored", "straight face", "serious", "sarcasm", "unimpressed", "skeptical", "dubious", "side_eye"],
"🙄": ["face", "eyeroll", "frustrated"],
"🙂‍↔️": ["face", "head", "horizontally", "no", "shake", "shaking"],
"🙂‍↕️": ["face", "head", "nod", "shaking", "vertically", "yes"],
"🤔": ["face", "hmmm", "think", "consider"],
"🤥": ["face", "lie", "pinocchio"],
"🤭": ["face", "whoops", "shock", "surprise"],
@@ -78,7 +76,6 @@
"😥": ["face", "phew", "sweat", "nervous"],
"🤤": ["face"],
"😪": ["face", "tired", "rest", "nap"],
"🫩": ["face", "bags", "bored", "exhausted", "eyes", "fatigued", "late", "sleepy", "tired", "weary"],
"😓": ["face", "hot", "sad", "tired", "exercise"],
"🥵": ["face", "feverish", "heat", "red", "sweating"],
"🥶": ["face", "blue", "freezing", "frozen", "frostbite", "icicles"],
@@ -95,11 +92,10 @@
"🥴": ["face", "dizzy", "intoxicated", "tipsy", "wavy"],
"🥱": ["face", "tired", "yawning"],
"😴": ["face", "tired", "sleepy", "night", "zzz"],
"👁️‍🗨️": ["balloon", "bubble", "eye", "speech", "witness"],
"💤": ["sleepy", "tired", "dream"],
"😶‍🌫️": ["face", "absentminded", "clouds", "fog", "head"],
"😮‍💨": ["face", "blow", "blowing", "exhale", "exhaling", "exhausted", "gasp", "groan", "relief", "sigh", "smiley", "smoke", "whisper", "whistle"],
"😵‍💫": ["face", "confused", "dizzy", "eyes", "hypnotized", "omg", "smiley", "spiral", "trouble", "whoa", "woah", "woozy"],
"😶‍🌫️": [],
"😮‍💨": [],
"😵‍💫": [],
"🫠": ["disappear", "dissolve", "liquid", "melt", "toketa"],
"🫢": ["amazement", "awe", "disbelief", "embarrass", "scared", "surprise", "ohoho"],
"🫣": ["captivated", "peep", "stare", "chunibyo"],
@@ -163,19 +159,19 @@
"🤞": ["good", "lucky"],
"🖖": ["hand", "fingers", "spock", "star trek"],
"✍": ["lower_left_ballpoint_pen", "stationery", "write", "compose"],
"🫰": ["<3", "crossed", "expensive", "finger", "hand", "heart", "index", "love", "money", "snap", "thumb"],
"🫱": ["hand", "handshake", "hold", "reach", "right", "rightward", "rightwards", "shake"],
"🫲": ["hand", "handshake", "hold", "left", "leftward", "leftwards", "reach", "shake"],
"🫳": ["dismiss", "down", "drop", "dropped", "hand", "palm", "pick", "shoo", "up"],
"🫴": ["beckon", "catch", "come", "hand", "hold", "know", "lift", "me", "offer", "palm", "tell"],
"🫵": ["at", "finger", "hand", "index", "pointing", "poke", "viewer", "you"],
"🫰": [],
"🫱": [],
"🫲": [],
"🫳": [],
"🫴": [],
"🫵": [],
"🫶": ["moemoekyun"],
"🤏": ["hand", "fingers"],
"🤌": ["hand", "fingers"],
"🤳": ["camera", "phone"],
"💅": ["beauty", "manicure", "finger", "fashion", "nail"],
"👄": ["mouth", "kiss"],
"🫦": ["anxious", "bite", "biting", "fear", "flirt", "flirting", "kiss", "lip", "lipstick", "nervous", "sexy", "uncomfortable", "worried", "worry"],
"🫦": [],
"🦷": ["teeth", "dentist"],
"👅": ["mouth", "playful"],
"👂": ["face", "hear", "sound", "listen"],
@@ -184,12 +180,11 @@
"👁": ["face", "look", "see", "watch", "stare"],
"👀": ["look", "watch", "stalk", "peek", "see"],
"🧠": ["smart", "intelligent"],
"🫀": ["anatomical", "beat", "cardiology", "heart", "heartbeat", "organ", "pulse", "real", "red"],
"🫁": ["breath", "breathe", "exhalation", "inhalation", "lung", "lungs", "organ", "respiration"],
"🫀": [],
"🫁": [],
"👤": ["user", "person", "human"],
"👥": ["user", "person", "human", "group", "team"],
"🗣": ["user", "person", "human", "sing", "say", "talk"],
"🫆": ["clue", "crime", "detective", "fingerprint", "forensics", "identity", "mystery", "print", "safety", "trace"],
"👶": ["child", "boy", "girl", "toddler"],
"🧒": ["gender-neutral", "young"],
"👦": ["man", "male", "guy", "teenager"],
@@ -204,7 +199,6 @@
"👩‍🦰": ["woman", "female", "girl", "ginger", "redhead"],
"👨‍🦰": ["man", "male", "boy", "guy", "ginger", "redhead"],
"👱‍♀️": ["woman", "female", "girl", "blonde", "person"],
"👱‍♂️": ["blond", "blond-haired", "hair", "man"],
"👱": ["man", "male", "boy", "blonde", "guy", "person"],
"🧑‍🦳": ["gray", "old", "white"],
"👩‍🦳": ["woman", "female", "girl", "gray", "old", "white"],
@@ -213,27 +207,20 @@
"👩‍🦲": ["woman", "female", "girl", "bald", "chemotherapy", "hairless", "shaven"],
"👨‍🦲": ["man", "male", "boy", "guy", "bald", "chemotherapy", "hairless", "shaven"],
"🧔": ["person", "bewhiskered"],
"🧔‍♀️": ["beard", "bearded", "whiskers", "woman"],
"🧔‍♂️": ["beard", "bearded", "man", "whiskers"],
"🧓": ["human", "elder", "senior", "gender-neutral"],
"👴": ["human", "male", "men", "old", "elder", "senior"],
"👵": ["human", "female", "women", "lady", "old", "elder", "senior"],
"👲": ["male", "boy", "chinese"],
"🧕": ["female", "hijab", "mantilla", "tichel"],
"👳‍♀️": ["female", "indian", "hinduism", "arabs", "woman"],
"👳‍♂️": ["man", "turban", "wearing"],
"👳": ["male", "indian", "hinduism", "arabs"],
"👮‍♀️": ["woman", "police", "law", "legal", "enforcement", "arrest", "911", "female"],
"👮‍♂️": ["apprehend", "arrest", "citation", "cop", "law", "man", "officer", "over", "police", "pulled", "undercover"],
"👮": ["man", "police", "law", "legal", "enforcement", "arrest", "911"],
"👷‍♀️": ["female", "human", "wip", "build", "construction", "worker", "labor", "woman"],
"👷‍♂️": ["build", "construction", "fix", "hardhat", "hat", "man", "rebuild", "remodel", "repair", "work", "worker"],
"👷": ["male", "human", "wip", "guy", "build", "construction", "worker", "labor"],
"💂‍♀️": ["uk", "gb", "british", "female", "royal", "woman"],
"💂‍♂️": ["buckingham", "guard", "helmet", "london", "man", "palace"],
"💂": ["uk", "gb", "british", "male", "guy", "royal"],
"🕵️‍♀️": ["human", "spy", "detective", "female", "woman"],
"🕵️‍♂️": ["detective", "man", "sleuth", "spy"],
"🕵": ["human", "spy", "detective"],
"🧑‍⚕️": ["doctor", "nurse", "therapist", "healthcare", "human"],
"👩‍⚕️": ["doctor", "nurse", "therapist", "healthcare", "woman", "human"],
@@ -283,152 +270,102 @@
"🧑‍⚖️": ["justice", "court", "human"],
"👩‍⚖️": ["justice", "court", "woman", "human"],
"👨‍⚖️": ["justice", "court", "man", "human"],
"🦸": ["good", "hero", "superhero", "superpower"],
"🦸‍♀️": ["woman", "female", "good", "heroine", "superpowers"],
"🦸‍♂️": ["man", "male", "good", "hero", "superpowers"],
"🦹": ["bad", "criminal", "evil", "superpower", "supervillain", "villain"],
"🦹‍♀️": ["woman", "female", "evil", "bad", "criminal", "heroine", "superpowers"],
"🦹‍♂️": ["man", "male", "evil", "bad", "criminal", "hero", "superpowers"],
"🤶": ["woman", "female", "xmas", "mother christmas"],
"🧑‍🎄": ["xmas", "christmas"],
"🎅": ["festival", "man", "male", "xmas", "father christmas"],
"🥷": ["assassin", "fight", "fighter", "hidden", "ninja", "person", "secret", "skills", "sly", "soldier", "stealth", "war"],
"🧙": ["fantasy", "mage", "magic", "play", "sorcerer", "sorceress", "sorcery", "spell", "summon", "witch", "wizard"],
"🥷": [],
"🧙‍♀️": ["woman", "female", "mage", "witch"],
"🧙‍♂️": ["man", "male", "mage", "sorcerer"],
"🧝": ["elf", "elves", "enchantment", "fantasy", "folklore", "magic", "magical", "myth"],
"🧝‍♀️": ["woman", "female"],
"🧝‍♂️": ["man", "male"],
"🧛": ["blood", "dracula", "fangs", "halloween", "scary", "supernatural", "teeth", "undead", "vampire"],
"🧛‍♀️": ["woman", "female"],
"🧛‍♂️": ["man", "male", "dracula"],
"🧟": ["apocalypse", "dead", "halloween", "horror", "scary", "undead", "walking", "zombie"],
"🧟‍♀️": ["woman", "female", "undead", "walking dead"],
"🧟‍♂️": ["man", "male", "dracula", "undead", "walking dead"],
"🧞": ["djinn", "fantasy", "genie", "jinn", "lamp", "myth", "rub", "wishes"],
"🧞‍♀️": ["woman", "female"],
"🧞‍♂️": ["man", "male"],
"🧜": ["creature", "fairytale", "folklore", "merperson", "ocean", "sea", "siren", "trident"],
"🧜‍♀️": ["woman", "female", "merwoman", "ariel"],
"🧜‍♂️": ["man", "male", "triton"],
"🧚": ["fairy", "fairytale", "fantasy", "myth", "person", "pixie", "tale", "wings"],
"🧚‍♀️": ["woman", "female"],
"🧚‍♂️": ["man", "male"],
"👼": ["heaven", "wings", "halo"],
"🧌": ["fairy", "fantasy", "monster", "tale", "troll", "trolling"],
"🧌": [],
"🤰": ["baby"],
"🫃": ["belly", "bloated", "full", "man", "overeat", "pregnant"],
"🫄": ["belly", "bloated", "full", "overeat", "person", "pregnant", "stuffed"],
"🫅": ["crown", "monarch", "noble", "person", "regal", "royal", "royalty"],
"🫃": [],
"🫄": [],
"🫅": [],
"🤱": ["nursing", "baby"],
"👩‍🍼": ["baby", "feed", "feeding", "mom", "mother", "nanny", "newborn", "nursing", "woman"],
"👨‍🍼": ["baby", "dad", "father", "feed", "feeding", "man", "nanny", "newborn", "nursing"],
"🧑‍🍼": ["baby", "feed", "feeding", "nanny", "newborn", "nursing", "parent"],
"👩‍🍼": [],
"👨‍🍼": [],
"🧑‍🍼": [],
"👸": ["girl", "woman", "female", "blond", "crown", "royal", "queen"],
"🤴": ["boy", "man", "male", "crown", "royal", "king"],
"👰": ["couple", "marriage", "wedding", "woman", "bride"],
"👰‍♀️": ["bride", "veil", "wedding", "woman"],
"👰‍♂️": ["man", "veil", "wedding"],
"👰": ["couple", "marriage", "wedding", "woman", "bride"],
"🤵": ["couple", "marriage", "wedding", "groom"],
"🤵": ["couple", "marriage", "wedding", "groom"],
"🤵‍♀️": ["formal", "tuxedo", "wedding", "woman"],
"🤵‍♂️": ["formal", "groom", "man", "tuxedo", "wedding"],
"🏃‍➡️": ["facing", "fast", "hurry", "marathon", "move", "person", "quick", "race", "racing", "right", "run", "rush", "speed"],
"🏃‍♀️": ["woman", "walking", "exercise", "race", "running", "female"],
"🏃": ["man", "walking", "exercise", "race", "running"],
"🏃‍♀️‍➡️": ["facing", "fast", "hurry", "marathon", "move", "quick", "race", "racing", "right", "run", "rush", "speed", "woman"],
"🏃‍♂️": ["fast", "hurry", "man", "marathon", "move", "quick", "race", "racing", "run", "rush", "speed"],
"🏃‍♂️‍➡️": ["facing", "fast", "hurry", "man", "marathon", "move", "quick", "race", "racing", "right", "run", "rush", "speed"],
"🚶‍➡️": ["amble", "facing", "gait", "hike", "man", "pace", "pedestrian", "person", "right", "stride", "stroll", "walk", "walking"],
"🚶‍♀️": ["human", "feet", "steps", "woman", "female"],
"🚶": ["human", "feet", "steps"],
"🚶‍♀️‍➡️": ["amble", "facing", "gait", "hike", "man", "pace", "pedestrian", "right", "stride", "stroll", "walk", "walking", "woman"],
"🚶‍♂️": ["amble", "gait", "hike", "man", "pace", "pedestrian", "stride", "stroll", "walk", "walking"],
"🚶‍♂️‍➡️": ["amble", "facing", "gait", "hike", "man", "pace", "pedestrian", "right", "stride", "stroll", "walk", "walking"],
"💃": ["female", "girl", "woman", "fun"],
"🕺": ["male", "boy", "fun", "dancer"],
"👯": ["female", "bunny", "women", "girls"],
"👯‍♀️": ["bestie", "bff", "bunny", "counterpart", "dancer", "double", "ear", "identical", "pair", "party", "partying", "people", "soulmate", "twin", "twinsies", "women"],
"👯‍♂️": ["male", "bunny", "men", "boys"],
"👫": ["pair", "people", "human", "love", "date", "dating", "like", "affection", "valentines", "marriage"],
"🧑‍🤝‍🧑": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "human"],
"👬": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "man", "human"],
"👭": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "female", "human"],
"🫂": ["comfort", "embrace", "farewell", "friendship", "goodbye", "hello", "hug", "hugging", "love", "people", "thanks"],
"🫂": [],
"🙇‍♀️": ["woman", "female", "girl"],
"🙇": ["man", "male", "boy"],
"🙇‍♂️": ["apology", "ask", "beg", "bow", "bowing", "favor", "forgive", "gesture", "man", "meditate", "meditation", "pity", "regret", "sorry"],
"🤦": ["again", "bewilder", "disbelief", "exasperation", "facepalm", "no", "not", "oh", "omg", "person", "shock", "smh"],
"🤦‍♂️": ["man", "male", "boy", "disbelief"],
"🤦‍♀️": ["woman", "female", "girl", "disbelief"],
"🤷": ["woman", "female", "girl", "confused", "indifferent", "doubt"],
"🤷‍♀️": ["doubt", "dunno", "guess", "idk", "ignorance", "indifference", "knows", "maybe", "shrug", "shrugging", "whatever", "who", "woman"],
"🤷‍♂️": ["man", "male", "boy", "confused", "indifferent", "doubt"],
"💁": ["female", "girl", "woman", "human", "information"],
"💁‍♀️": ["fetch", "flick", "flip", "gossip", "hand", "sarcasm", "sarcastic", "sassy", "seriously", "tipping", "whatever", "woman"],
"💁‍♂️": ["male", "boy", "man", "human", "information"],
"🙅": ["female", "girl", "woman", "nope"],
"🙅‍♀️": ["forbidden", "gesture", "hand", "no", "not", "prohibit", "woman"],
"🙅‍♂️": ["male", "boy", "man", "nope"],
"🙆": ["women", "girl", "female", "pink", "human", "woman"],
"🙆‍♀️": ["exercise", "gesture", "gesturing", "hand", "ok", "omg", "woman"],
"🙆‍♂️": ["men", "boy", "male", "blue", "human", "man"],
"🙋": ["female", "girl", "woman"],
"🙋‍♀️": ["gesture", "hand", "here", "know", "me", "pick", "question", "raise", "raising", "woman"],
"🙋‍♂️": ["male", "boy", "man"],
"🙎": ["female", "girl", "woman"],
"🙎‍♀️": ["disappointed", "downtrodden", "frown", "grimace", "pouting", "scowl", "sulk", "upset", "whine", "woman"],
"🙎‍♂️": ["male", "boy", "man"],
"🙍": ["female", "girl", "woman", "sad", "depressed", "discouraged", "unhappy"],
"🙍‍♀️": ["annoyed", "disappointed", "disgruntled", "disturbed", "frown", "frowning", "frustrated", "gesture", "irritated", "upset", "woman"],
"🙍‍♂️": ["male", "boy", "man", "sad", "depressed", "discouraged", "unhappy"],
"💇": ["female", "girl", "woman"],
"💇‍♀️": ["barber", "beauty", "chop", "cosmetology", "cut", "groom", "hair", "haircut", "parlor", "person", "shears", "style", "woman"],
"💇‍♂️": ["male", "boy", "man"],
"💆": ["female", "girl", "woman", "head"],
"💆‍♀️": ["face", "getting", "headache", "massage", "relax", "relaxing", "salon", "soothe", "spa", "tension", "therapy", "treatment", "woman"],
"💆‍♂️": ["male", "boy", "man", "head"],
"🧖": ["day", "luxurious", "pamper", "person", "relax", "room", "sauna", "spa", "steam", "steambath", "unwind"],
"🧖‍♀️": ["female", "woman", "spa", "steamroom", "sauna"],
"🧖‍♂️": ["male", "man", "spa", "steamroom", "sauna"],
"🧏": ["accessibility", "deaf", "ear", "gesture", "hear", "person"],
"🧏‍♀️": ["woman", "female"],
"🧏‍♂️": ["man", "male"],
"🧍": ["person", "stand", "standing"],
"🧍‍♀️": ["woman", "female"],
"🧍‍♂️": ["man", "male"],
"🧎": ["kneel", "kneeling", "knees", "person"],
"🧎‍➡️": ["facing", "kneel", "kneeling", "knees", "person", "right"],
"🧎‍♀️": ["woman", "female"],
"🧎‍♀️‍➡️": ["facing", "kneel", "kneeling", "knees", "right", "woman"],
"🧎‍♂️": ["man", "male"],
"🧎‍♂️‍➡️": ["facing", "kneel", "kneeling", "knees", "man", "right"],
"🧑‍🦯": ["accessibility", "blind"],
"🧑‍🦯‍➡️": ["accessibility", "blind", "cane", "facing", "person", "probing", "right", "white"],
"👩‍🦯": ["woman", "female", "accessibility", "blind"],
"👩‍🦯‍➡️": ["accessibility", "blind", "cane", "facing", "probing", "right", "white", "woman"],
"👨‍🦯": ["man", "male", "accessibility", "blind"],
"👨‍🦯‍➡️": ["accessibility", "blind", "cane", "facing", "man", "probing", "right", "white"],
"🧑‍🦼": ["accessibility"],
"🧑‍🦼‍➡️": ["accessibility", "facing", "motorized", "person", "right", "wheelchair"],
"👩‍🦼": ["woman", "female", "accessibility"],
"👩‍🦼‍➡️": ["accessibility", "facing", "motorized", "right", "wheelchair", "woman"],
"👨‍🦼": ["man", "male", "accessibility"],
"👨‍🦼‍➡️": ["accessibility", "facing", "man", "motorized", "right", "wheelchair"],
"🧑‍🦽": ["accessibility"],
"🧑‍🦽‍➡️": ["accessibility", "facing", "manual", "person", "right", "wheelchair"],
"👩‍🦽": ["woman", "female", "accessibility"],
"👩‍🦽‍➡️": ["accessibility", "facing", "manual", "right", "wheelchair", "woman"],
"👨‍🦽": ["man", "male", "accessibility"],
"👨‍🦽‍➡️": ["accessibility", "facing", "man", "manual", "right", "wheelchair"],
"💑": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"],
"👩‍❤️‍👨": ["anniversary", "babe", "bae", "couple", "dating", "heart", "kiss", "love", "man", "person", "relationship", "romance", "together", "woman", "you"],
"👩‍❤️‍👩": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"],
"👨‍❤️‍👨": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"],
"💏": ["pair", "valentines", "love", "like", "dating", "marriage"],
"👩‍❤️‍💋‍👨": ["anniversary", "babe", "bae", "couple", "date", "dating", "heart", "kiss", "love", "man", "mwah", "person", "romance", "together", "woman", "xoxo"],
"👩‍❤️‍💋‍👩": ["pair", "valentines", "love", "like", "dating", "marriage"],
"👨‍❤️‍💋‍👨": ["pair", "valentines", "love", "like", "dating", "marriage"],
"👨‍👩‍👦": ["boy", "child", "family", "man", "woman"],
"👪": ["home", "parents", "child", "mom", "dad", "father", "mother", "people", "human"],
"👨‍👩‍👧": ["home", "parents", "people", "human", "child"],
"👨‍👩‍👧‍👦": ["home", "parents", "people", "human", "children"],
@@ -454,10 +391,6 @@
"👨‍👧‍👦": ["home", "parent", "people", "human", "children"],
"👨‍👦‍👦": ["home", "parent", "people", "human", "children"],
"👨‍👧‍👧": ["home", "parent", "people", "human", "children"],
"🧑‍🧑‍🧒": ["adult", "child", "family"],
"🧑‍🧑‍🧒‍🧒": ["adult", "child", "family"],
"🧑‍🧒": ["adult", "child", "family"],
"🧑‍🧒‍🧒": ["adult", "child", "family"],
"🧶": ["ball", "crochet", "knit"],
"🧵": ["needle", "sewing", "spool", "string"],
"🧥": ["jacket"],
@@ -482,7 +415,7 @@
"👢": ["shoes", "fashion"],
"👞": ["fashion", "male"],
"👟": ["shoes", "sports", "sneakers"],
"🩴": ["beach", "flip", "flop", "sandal", "sandals", "shoe", "thong", "thongs", "zōri"],
"🩴": [],
"🩰": ["shoes", "sports"],
"🧦": ["stockings", "clothes"],
"🧤": ["hands", "winter", "clothes"],
@@ -491,7 +424,7 @@
"🎩": ["magic", "gentleman", "classy", "circus"],
"🧢": ["cap", "baseball"],
"⛑": ["construction", "build"],
"🪖": ["army", "helmet", "military", "soldier", "war", "warrior"],
"🪖": [],
"🎓": ["school", "college", "degree", "university", "graduation", "cap", "hat", "legal", "learn", "education"],
"👑": ["king", "kod", "leader", "royalty", "lord"],
"🎒": ["student", "education", "bag", "backpack"],
@@ -542,7 +475,6 @@
"🐦‍⬛": ["animal", "nature", "bird", "black", "crow", "raven", "rook"],
"🦅": ["animal", "nature", "bird"],
"🦉": ["animal", "nature", "bird", "hoot"],
"🐦‍🔥": ["animal", "nature", "ascend", "ascension", "emerge", "fantasy", "firebird", "glory", "immortal", "phoenix", "rebirth", "reincarnation", "reinvent", "renewal", "revival", "revive", "rise", "transform"],
"🦇": ["animal", "nature", "blind", "vampire"],
"🐺": ["animal", "nature", "wild"],
"🐗": ["animal", "nature"],
@@ -643,7 +575,6 @@
"🌿": ["vegetable", "plant", "medicine", "weed", "grass", "lawn"],
"☘": ["vegetable", "plant", "nature", "irish", "clover"],
"🍀": ["vegetable", "plant", "nature", "lucky", "irish"],
"🪾": ["plant", "nature", "bare", "barren", "branches", "dead", "drought", "leafless", "tree", "trunk", "winter", "wood"],
"🎍": ["plant", "nature", "vegetable", "panda", "pine_decoration"],
"🎋": ["plant", "nature", "branch", "summer"],
"🍃": ["nature", "plant", "tree", "vegetable", "grass", "lawn", "spring"],
@@ -713,11 +644,10 @@
"💧": ["water", "drip", "faucet", "spring"],
"💦": ["water", "drip", "oops"],
"🌊": ["sea", "water", "wave", "nature", "tsunami", "disaster"],
"🪷": ["plant", "nature", "beauty", "buddhism", "calm", "flower", "hinduism", "lotus", "peace", "purity", "serenity"],
"🪸": ["animal", "nature", "change", "climate", "coral", "ocean", "reef", "sea"],
"🪹": ["plant", "nature", "branch", "empty", "home", "nest", "nesting"],
"🪺": ["plant", "nature", "bird", "branch", "egg", "eggs", "nest", "nesting"],
"🍋‍🟩": ["acidity", "citrus", "cocktail", "fruit", "garnish", "key", "lime", "margarita", "mojito", "refreshing", "salsa", "sour", "tangy", "tequila", "tropical", "zest"],
"🪷": [],
"🪸": [],
"🪹": [],
"🪺": [],
"🍏": ["fruit", "nature"],
"🍎": ["fruit", "mac", "school"],
"🍐": ["fruit", "nature", "food"],
@@ -737,8 +667,6 @@
"🥑": ["fruit", "food"],
"🫛": ["beans", "edamame", "legume", "pea", "pod", "vegetable", "food"],
"🥦": ["fruit", "food", "vegetable"],
"🍄‍🟫": ["food", "fungi", "fungus", "mushroom", "nature", "pizza", "portobello", "shiitake", "shroom", "spore", "sprout", "toppings", "truffle", "vegetable", "vegetarian", "veggie"],
"🫜": ["beet", "food", "garden", "radish", "root", "salad", "turnip", "vegetable", "vegetarian"],
"🍅": ["fruit", "vegetable", "nature", "food"],
"🍆": ["vegetable", "nature", "food", "aubergine"],
"🥒": ["fruit", "food", "pickle"],
@@ -831,7 +759,7 @@
"🍵": ["drink", "bowl", "breakfast", "green", "british"],
"🥤": ["drink", "soda"],
"☕": ["beverage", "caffeine", "latte", "espresso"],
"🫖": ["brew", "drink", "food", "pot", "tea", "teapot"],
"🫖": [],
"🧋": ["tapioca"],
"🍼": ["food", "container", "milk"],
"🧃": ["food", "drink"],
@@ -844,9 +772,9 @@
"🥣": ["food", "breakfast", "cereal", "oatmeal", "porridge"],
"🥡": ["food", "leftovers"],
"🥢": ["food"],
"🫗": ["accident", "drink", "empty", "glass", "liquid", "oops", "pour", "pouring", "spill", "water"],
"🫘": ["beans", "food", "kidney", "legume", "small"],
"🫙": ["condiment", "container", "empty", "jar", "nothing", "sauce", "store"],
"🫗": [],
"🫘": [],
"🫙": [],
"⚽": ["sports", "football"],
"🏀": ["sports", "balls", "NBA"],
"🏈": ["sports", "balls", "NFL"],
@@ -860,7 +788,6 @@
"⛳": ["sports", "business", "flag", "hole", "summer"],
"🏌️‍♀️": ["sports", "business", "woman", "female"],
"🏌": ["sports", "business"],
"🏌️‍♂️": ["sport", "ball", "birdie", "caddy", "driving", "golf", "golfing", "green", "man", "pga", "putt", "range", "tee"],
"🏓": ["sports", "pingpong"],
"🏸": ["sports"],
"🥅": ["sports"],
@@ -872,13 +799,10 @@
"⛷": ["sports", "winter", "snow"],
"🏂": ["sports", "winter"],
"🤺": ["sports", "fencing", "sword"],
"🤼": ["sport", "combat", "duel", "grapple", "people", "ring", "tournament", "wrestle", "wrestling"],
"🤼‍♀️": ["sports", "wrestlers"],
"🤼‍♂️": ["sports", "wrestlers"],
"🤸": ["sport", "active", "cartwheel", "cartwheeling", "excited", "flip", "gymnastics", "happy", "person", "somersault"],
"🤸‍♀️": ["gymnastics"],
"🤸‍♂️": ["gymnastics"],
"🤾": ["sport", "athletics", "ball", "catch", "chuck", "handball", "hurl", "lob", "person", "pitch", "playing", "throw", "toss"],
"🤾‍♀️": ["sports"],
"🤾‍♂️": ["sports"],
"⛸": ["sports"],
@@ -891,42 +815,32 @@
"🥋": ["judo", "karate", "taekwondo"],
"🚣‍♀️": ["sports", "hobby", "water", "ship", "woman", "female"],
"🚣": ["sports", "hobby", "water", "ship"],
"🚣‍♂️": ["sport", "boat", "canoe", "cruise", "fishing", "lake", "man", "oar", "paddle", "raft", "river", "row", "rowboat", "rowing"],
"🧗": ["climb", "climber", "climbing", "mountain", "person", "rock", "scale", "up"],
"🧗‍♀️": ["sports", "hobby", "woman", "female", "rock"],
"🧗‍♂️": ["sports", "hobby", "man", "male", "rock"],
"🏊‍♀️": ["sports", "exercise", "human", "athlete", "water", "summer", "woman", "female"],
"🏊": ["sports", "exercise", "human", "athlete", "water", "summer"],
"🏊‍♂️": ["sport", "freestyle", "man", "swim", "swimmer", "swimming", "triathlon"],
"🤽": ["sport", "person", "playing", "polo", "swimming", "water", "waterpolo"],
"🤽‍♀️": ["sports", "pool"],
"🤽‍♂️": ["sports", "pool"],
"🧘": ["cross", "legged", "legs", "lotus", "meditation", "peace", "person", "position", "relax", "serenity", "yoga", "yogi", "zen"],
"🧘‍♀️": ["woman", "female", "meditation", "yoga", "serenity", "zen", "mindfulness"],
"🧘‍♂️": ["man", "male", "meditation", "yoga", "serenity", "zen", "mindfulness"],
"🏄‍♀️": ["sports", "ocean", "sea", "summer", "beach", "woman", "female"],
"🏄": ["sports", "ocean", "sea", "summer", "beach"],
"🏄‍♂️": ["sport", "beach", "man", "ocean", "surf", "surfer", "surfing", "swell", "waves"],
"🛀": ["clean", "shower", "bathroom"],
"⛹️‍♀️": ["sports", "human", "woman", "female"],
"⛹": ["sports", "human"],
"⛹️‍♂️": ["sport", "athletic", "ball", "basketball", "bouncing", "championship", "dribble", "man", "net", "player", "throw"],
"🏋️‍♀️": ["sports", "training", "exercise", "woman", "female"],
"🏋": ["sports", "training", "exercise"],
"🏋️‍♂️": ["sport", "barbell", "bodybuilder", "deadlift", "lifter", "lifting", "man", "powerlifting", "weight", "weightlifter", "weights", "workout"],
"🚴‍♀️": ["sports", "bike", "exercise", "hipster", "woman", "female"],
"🚴": ["sports", "bike", "exercise", "hipster"],
"🚴‍♂️": ["sport", "bicycle", "bicyclist", "bike", "biking", "cycle", "cyclist", "man", "riding"],
"🚵‍♀️": ["transportation", "sports", "human", "race", "bike", "woman", "female"],
"🚵": ["transportation", "sports", "human", "race", "bike"],
"🚵‍♂️": ["sport", "bicycle", "bicyclist", "bike", "biking", "cycle", "cyclist", "man", "mountain", "riding"],
"🏇": ["animal", "betting", "competition", "gambling", "luck"],
"🤿": ["sports"],
"🪀": ["sports"],
"🪁": ["sports"],
"🦺": ["sports"],
"🪡": ["embroidery", "needle", "sew", "sewing", "stitches", "sutures", "tailoring", "thread"],
"🪢": ["cord", "knot", "rope", "tangled", "tie", "twine", "twist"],
"🪡": [],
"🪢": [],
"🕴": ["suit", "business", "levitate", "hover", "jump"],
"🏆": ["win", "award", "contest", "place", "ftw", "ceremony"],
"🎽": ["play", "pageant"],
@@ -942,7 +856,6 @@
"🎭": ["acting", "theater", "drama"],
"🎨": ["design", "paint", "draw", "colors"],
"🎪": ["festival", "carnival", "party"],
"🤹": ["sport", "act", "balance", "balancing", "handle", "juggle", "juggling", "manage", "multitask", "person", "skill"],
"🤹‍♀️": ["juggle", "balance", "skill", "multitask"],
"🤹‍♂️": ["juggle", "balance", "skill", "multitask"],
"🎤": ["sound", "music", "PA", "sing", "talkshow"],
@@ -959,7 +872,6 @@
"🪕": ["music", "instrument"],
"🪗": ["music", "instrument"],
"🪘": ["music", "instrument"],
"🪉": ["cupid", "harp", "instrument", "love", "music", "orchestra"],
"🎬": ["movie", "film", "record"],
"🎮": ["play", "console", "PS4", "controller"],
"👾": ["game", "arcade", "play"],
@@ -969,11 +881,11 @@
"🎰": ["bet", "gamble", "vegas", "fruit machine", "luck", "casino"],
"🧩": ["interlocking", "puzzle", "piece"],
"🎳": ["sports", "fun", "play"],
"🪄": ["magic", "magician", "wand", "witch", "wizard"],
"🪅": ["candy", "celebrate", "celebration", "cinco", "de", "festive", "mayo", "party", "pinada", "pinata", "piñata"],
"🪆": ["babooshka", "baboushka", "babushka", "doll", "dolls", "matryoshka", "nesting", "russia"],
"🪬": ["amulet", "fatima", "fortune", "guide", "hamsa", "hand", "mary", "miriam", "palm", "protect", "protection"],
"🪩": ["ball", "dance", "disco", "glitter", "mirror", "party"],
"🪄": [],
"🪅": [],
"🪆": [],
"🪬": [],
"🪩": [],
"🚗": ["red", "transportation", "vehicle"],
"🚕": ["uber", "vehicle", "cars", "transportation"],
"🚙": ["transportation", "vehicle"],
@@ -1029,7 +941,7 @@
"🚀": ["launch", "ship", "staffmode", "NASA", "outer space", "outer_space", "fly"],
"🛰": ["communication", "gps", "orbit", "spaceflight", "NASA", "ISS"],
"🛻": ["car"],
"🛼": ["blades", "roller", "skate", "skates", "sport"],
"🛼": [],
"💺": ["sit", "airplane", "transport", "bus", "flight", "fly"],
"🛶": ["boat", "paddle", "water", "ship"],
"⚓": ["ship", "ferry", "sea", "boat"],
@@ -1101,12 +1013,12 @@
"🕋": ["mecca", "mosque", "islam"],
"⛩": ["temple", "japan", "kyoto"],
"🛕": ["temple"],
"🪨": ["boulder", "heavy", "rock", "solid", "stone", "tough"],
"🪵": ["log", "lumber", "timber", "wood"],
"🛖": ["home", "house", "hut", "roundhouse", "shelter", "yurt"],
"🛝": ["amusement", "park", "play", "playground", "playing", "slide", "sliding", "theme"],
"🛞": ["car", "circle", "tire", "turn", "vehicle", "wheel"],
"🛟": ["buoy", "float", "life", "lifesaver", "preserver", "rescue", "ring", "safety", "save", "saver", "swim"],
"🪨": [],
"🪵": [],
"🛖": [],
"🛝": [],
"🛞": [],
"🛟": [],
"⌚": ["time", "accessories"],
"📱": ["technology", "apple", "gadgets", "dial"],
"📲": ["iphone", "incoming"],
@@ -1147,7 +1059,7 @@
"⌛": ["time", "clock", "oldschool", "limit", "exam", "quiz", "test"],
"📡": ["communication", "future", "radio", "space"],
"🔋": ["power", "energy", "sustain"],
"🪫": ["battery", "drained", "electronic", "energy", "low", "power"],
"🪫": [],
"🔌": ["charger", "power"],
"💡": ["light", "electricity", "idea"],
"🔦": ["dark", "camping", "sight", "night"],
@@ -1163,11 +1075,9 @@
"💰": ["dollar", "payment", "coins", "sale"],
"🪙": ["dollar", "payment", "coins", "sale"],
"💳": ["money", "sales", "dollar", "bill", "payment", "shopping"],
"🪪": ["card", "credentials", "document", "id", "identification", "license", "security"],
"🥾": ["backpacking", "boot", "brown", "camping", "hiking", "outdoors", "shoe"],
"🪪": [],
"💎": ["blue", "ruby", "diamond", "jewelry"],
"⚖": ["law", "fairness", "weight"],
"⛓️‍💥": ["break", "breaking", "broken", "chain", "cuffs", "freedom"],
"🧰": ["tools", "diy", "fix", "maintainer", "mechanic"],
"🔧": ["tools", "diy", "ikea", "fix", "maintainer"],
"🔨": ["tools", "build", "create"],
@@ -1183,7 +1093,6 @@
"🪛": ["tool"],
"🪝": ["tool"],
"🪜": ["tool"],
"🪏": ["bury", "dig", "garden", "hole", "plant", "scoop", "shovel", "snow", "spade"],
"🧱": ["bricks"],
"⛓": ["lock", "arrest"],
"🧲": ["attraction", "magnetic"],
@@ -1214,8 +1123,8 @@
"🩺": ["health", "hospital", "medicine", "needle", "doctor", "nurse"],
"🪒": ["health"],
"🪮": ["afro", "comb", "hair", "pick"],
"🩻": ["bones", "doctor", "medical", "skeleton", "skull", "x-ray", "xray"],
"🩼": ["aid", "cane", "crutch", "disability", "help", "hurt", "injured", "mobility", "stick"],
"🩻": [],
"🩼": [],
"🧬": ["biologist", "genetics", "life"],
"🧫": ["bacteria", "biology", "culture", "lab"],
"🧪": ["chemistry", "experiment", "lab", "science"],
@@ -1250,7 +1159,7 @@
"🪤": ["household"],
"🪣": ["household"],
"🪥": ["household"],
"🫧": ["bubble", "bubbles", "burp", "clean", "floating", "pearl", "soap", "underwater"],
"🫧": [],
"⛱": ["weather", "summer"],
"🗿": ["rock", "easter island", "moai"],
"🛍": ["mall", "buy", "purchase"],
@@ -1340,8 +1249,8 @@
"🖌": ["drawing", "creativity", "art"],
"🔍": ["search", "zoom", "find", "detective"],
"🔎": ["search", "zoom", "find", "detective"],
"🪦": ["cemetery", "dead", "grave", "graveyard", "headstone", "memorial", "rip", "tomb", "tombstone"],
"🪧": ["card", "demonstration", "notice", "picket", "placard", "plaque", "protest", "sign"],
"🪦": [],
"🪧": [],
"💯": ["score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass", "hundred"],
"🔢": ["numbers", "blue-square"],
"🩷": ["love", "like", "affection", "valentines"],
@@ -1366,8 +1275,8 @@
"💘": ["love", "like", "heart", "affection", "valentines"],
"💝": ["love", "valentines"],
"💟": ["purple-square", "love", "like"],
"❤️‍🔥": ["burn", "fire", "heart", "love", "lust", "sacred"],
"❤️‍🩹": ["healthier", "heart", "improving", "mending", "recovering", "recuperating", "well"],
"❤️‍🔥": [],
"❤️‍🩹": [],
"☮": ["hippie"],
"✝": ["christianity"],
"☪": ["islam"],
@@ -1395,8 +1304,6 @@
"♓": ["purple-square", "sign", "zodiac", "astrology"],
"🆔": ["purple-square", "words"],
"⚛": ["science", "physics", "chemistry"],
"♀️": ["female", "sign", "woman", "pink-square"],
"♂️": ["male", "man", "sign", "blue-square"],
"⚧️": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"],
"🈳": ["kanji", "japanese", "chinese", "empty", "sky", "blue-square", "aki"],
"🈹": ["cut", "divide", "chinese", "kanji", "pink-square", "waribiki"],
@@ -1552,15 +1459,13 @@
"➖": ["math", "calculation", "subtract", "less"],
"➗": ["divide", "math", "calculation"],
"✖️": ["math", "calculation"],
"🟰": ["answer", "equal", "equality", "equals", "heavy", "math", "sign"],
"🟰": [],
"♾": ["forever"],
"💲": ["money", "sales", "payment", "currency", "buck"],
"💱": ["money", "sales", "dollar", "travel"],
"⚕️": ["aesculapius", "medical", "medicine", "staff", "symbol"],
"©️": ["ip", "license", "circle", "law", "legal"],
"®️": ["alphabet", "circle"],
"™️": ["trademark", "brand", "law", "legal"],
"🫟": ["drip", "holi", "ink", "liquid", "mess", "paint", "spill", "splatter", "stain"],
"🔚": ["words", "arrow"],
"🔙": ["arrow", "words", "return"],
"🔛": ["arrow", "words"],
@@ -1671,7 +1576,6 @@
"🇧🇲": ["bm", "bermuda", "flag", "nation", "country", "banner"],
"🇧🇹": ["bt", "bhutan", "flag", "nation", "country", "banner"],
"🇧🇴": ["bo", "bolivia", "flag", "nation", "country", "banner"],
"🇧🇻": ["bv", "bouvet", "island", "flag", "nation", "country", "banner"],
"🇧🇶": ["bq", "bonaire", "flag", "nation", "country", "banner"],
"🇧🇦": ["ba", "bosnia", "herzegovina", "flag", "nation", "country", "banner"],
"🇧🇼": ["bw", "botswana", "flag", "nation", "country", "banner"],
@@ -1689,12 +1593,10 @@
"🇮🇨": ["ic", "canary", "islands", "flag", "nation", "country", "banner"],
"🇰🇾": ["ky", "cayman", "islands", "flag", "nation", "country", "banner"],
"🇨🇫": ["cf", "central", "african", "republic", "flag", "nation", "country", "banner"],
"🇪🇦": ["ea", "ceuta", "melilla", "flag", "nation", "country", "banner"],
"🇹🇩": ["td", "chad", "flag", "nation", "country", "banner"],
"🇨🇱": ["cl", "chile", "flag", "nation", "country", "banner"],
"🇨🇳": ["cn", "china", "chinese", "prc", "flag", "country", "nation", "banner"],
"🇨🇽": ["cx", "christmas", "island", "flag", "nation", "country", "banner"],
"🇨🇵": ["cp", "clipperton", "island", "flag", "nation", "country", "banner"],
"🇨🇨": ["cc", "cocos", "keeling", "islands", "flag", "nation", "country", "banner"],
"🇨🇴": ["co", "colombia", "flag", "nation", "country", "banner"],
"🇰🇲": ["km", "comoros", "flag", "nation", "country", "banner"],
@@ -1708,7 +1610,6 @@
"🇨🇾": ["cy", "cyprus", "flag", "nation", "country", "banner"],
"🇨🇿": ["cz", "czech", "republic", "flag", "nation", "country", "banner"],
"🇩🇰": ["dk", "denmark", "flag", "nation", "country", "banner"],
"🇩🇬": ["dg", "diego", "garcia", "flag", "nation", "country", "banner"],
"🇩🇯": ["dj", "djibouti", "flag", "nation", "country", "banner"],
"🇩🇲": ["dm", "dominica", "flag", "nation", "country", "banner"],
"🇩🇴": ["do", "dominican", "republic", "flag", "nation", "country", "banner"],
@@ -1745,7 +1646,6 @@
"🇬🇼": ["gw", "guiana", "bissau", "flag", "nation", "country", "banner"],
"🇬🇾": ["gy", "guyana", "flag", "nation", "country", "banner"],
"🇭🇹": ["ht", "haiti", "flag", "nation", "country", "banner"],
"🇭🇲": ["hm", "heard", "mcdonald", "islands", "flag", "nation", "country", "banner"],
"🇭🇳": ["hn", "honduras", "flag", "nation", "country", "banner"],
"🇭🇰": ["hk", "hong", "kong", "flag", "nation", "country", "banner"],
"🇭🇺": ["hu", "hungary", "flag", "nation", "country", "banner"],
@@ -1833,12 +1733,10 @@
"🇷🇴": ["ro", "romania", "flag", "nation", "country", "banner"],
"🇷🇺": ["ru", "russian", "federation", "flag", "nation", "country", "banner"],
"🇷🇼": ["rw", "rwanda", "flag", "nation", "country", "banner"],
"🇨🇶": ["cq", "sark", "flag", "nation", "country", "banner"],
"🇧🇱": ["bl", "saint", "barthélemy", "flag", "nation", "country", "banner"],
"🇸🇭": ["sh", "saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"],
"🇰🇳": ["kn", "saint", "kitts", "nevis", "flag", "nation", "country", "banner"],
"🇱🇨": ["lc", "saint", "lucia", "flag", "nation", "country", "banner"],
"🇲🇫": ["mf", "st", "martin", "flag", "nation", "country", "banner"],
"🇵🇲": ["pm", "saint", "pierre", "miquelon", "flag", "nation", "country", "banner"],
"🇻🇨": ["vc", "saint", "vincent", "grenadines", "flag", "nation", "country", "banner"],
"🇼🇸": ["ws", "western", "samoa", "flag", "nation", "country", "banner"],
@@ -1864,7 +1762,6 @@
"🇸🇩": ["sd", "sudan", "flag", "nation", "country", "banner"],
"🇸🇷": ["sr", "suriname", "flag", "nation", "country", "banner"],
"🇸🇿": ["sz", "eswatini", "flag", "nation", "country", "banner"],
"🇸🇯": ["sj", "svalbard", "jan", "mayen", "flag", "nation", "country", "banner"],
"🇸🇪": ["se", "sweden", "flag", "nation", "country", "banner"],
"🇨🇭": ["ch", "switzerland", "confoederatio", "helvetica", "flag", "nation", "country", "banner"],
"🇸🇾": ["sy", "syrian", "arab", "republic", "flag", "nation", "country", "banner"],
@@ -1891,7 +1788,6 @@
"🏴󠁧󠁢󠁳󠁣󠁴󠁿": ["flag", "scottish"],
"🏴󠁧󠁢󠁷󠁬󠁳󠁿": ["flag", "welsh"],
"🇺🇸": ["us", "usa", "united", "states", "america", "flag", "nation", "country", "banner"],
"🇺🇲": ["um", "us", "outlying", "islands", "flag", "nation", "country", "banner"],
"🇻🇮": ["vi", "virgin", "islands", "us", "flag", "nation", "country", "banner"],
"🇺🇾": ["uy", "uruguay", "flag", "nation", "country", "banner"],
"🇺🇿": ["uz", "uzbekistan", "flag", "nation", "country", "banner"],
@@ -36,9 +36,6 @@
"🤡":["ピエロの顔","ピエロ","顔"],
"😏":["にやにやした顔","顔","にやにや"],
"😶":["口のない顔","顔","口","静かに","沈黙"],
"🙂‍↔️":["いいえ","不賛成","顔","首を振る","首を横に振る"],
"🙂‍↕️":["はい","頷く","顔","首を振る","首を縦に振る"],
"🫩":["クマ","徹夜","疲れた","眠い","顔"],
"🫥":["点線の顔","落ち込んだ","消える","隠れる","内向的","目に見えない"],
"😐":["普通の顔","無表情","顔","平静"],
"🫤":["口が斜めになった顔","がっかり","無関心","疑い深い","不安"],
@@ -114,38 +111,6 @@
"💩":["うんち","マンガ","漫画","フン","顔","モンスター"],
"👻":["お化け","妖怪","顔","おとぎ話","ファンタジー","幽霊","モンスター","ハロウィーン"],
"💀":["ドクロ","体","死","顔","おとぎ話","モンスター","骸骨","ハロウィーン"],
"🫆":["指紋","鑑識","生体認証","セキュリティ"],
"🏃‍➡️":["ジョギング","マラソン","ランナー","ランニング","右向き","急ぐ","走る","走る人","駆け足"],
"🏃‍♀️‍➡️":["ジョギング","マラソン","ランナー","ランニング","右向き","女性","急ぐ","走る","走る女","駆け足"],
"🏃‍♂️‍➡️":["ジョギング","マラソン","ランナー","ランニング","右向き","男性","走る男"],
"🚶‍➡️":["ウォーキング","ハイキング","ぶらつく","人","右向き","大股","散歩","歩く","歩行","歩行者"],
"🚶‍♀️‍➡️":["ウォーキング","ハイキング","右向き","女性","散歩","歩く","歩く女","歩行","歩行者"],
"🚶‍♂️‍➡️":["ウォーキング","右向き","歩く男","歩行","歩行者","男性"],
"🧎‍➡️":["ひざまずく","人","右向き","座る","正座する人"],
"🧎‍♀️‍➡️":["ひざまずく","右向き","女性","座る","正座する女性"],
"🧎‍♂️‍➡️":["ひざまずく","右向き","座る","正座する男性","男性"],
"🧑‍🦯‍➡️":["アクセシビリティ","右向き","杖をついた人","目","視覚","障がい"],
"👩‍🦯‍➡️":["アクセシビリティ","右向き","女性","杖をついた女性","白杖をついた女性","目","視覚","障がい"],
"👨‍🦯‍➡️":["アクセシビリティ","右向き","杖をついた男性","男性","白杖をついた男性","目","視覚","障がい"],
"🧑‍🦼‍➡️":["アクセシビリティ","右向き","車いす","障がい","電動車椅子の人"],
"👩‍🦼‍➡️":["アクセシビリティ","右向き","女性","車いす","障がい","電動車椅子の女性"],
"👨‍🦼‍➡️":["アクセシビリティ","右向き","男性","車いす","障がい","電動車椅子の男性"],
"🧑‍🦽‍➡️":["アクセシビリティ","右向き","手動式車椅子の人","車いす","障がい"],
"👩‍🦽‍➡️":["アクセシビリティ","右向き","女性","手動式車椅子の女性","車いす","障がい"],
"👨‍🦽‍➡️":["アクセシビリティ","右向き","手動式車椅子の男性","男性","車いす","障がい"],
"👨‍👩‍👦":["女性","子供","家族","男の子","男性","親子"],
"🧑‍🧑‍🧒":["大人二人","子供一人","家族","大人二人と子供一人","親子"],
"🧑‍🧑‍🧒‍🧒":["大人二人","子供二人","家族","大人二人と子供二人","親子"],
"🧑‍🧒":["大人一人","子供一人","家族","大人一人と子供一人","親子"],
"🧑‍🧒‍🧒":["大人一人","子供二人","家族","大人一人と子供二人","親子"],
"🐦‍🔥":["ファンタジー","フェニックス","不死鳥","再生","復活","火の鳥","生まれ変わり","神話","転生","輪廻"],
"🪾":["不毛","干ばつ","葉のない木","冬"],
"🍋‍🟩":["さわやか","トロピカル","フルーツ","マルガリータ","モヒート","ライム","果物","柑橘類","緑","酸っぱい"],
"🍄‍🟫":["きのこ","キノコ","しいたけ","トリュフ","ブラウンマッシュルーム","ポートベロー","マッシュルーム","椎茸","茸","菌類"],
"🫜":["ビーツ","庭","根","カブ","野菜"],
"🪉":["オーケストラ","キューピッド","ハープ","弦楽器","愛","楽器","音楽"],
"⛓️‍💥":["くさり","チェーン","壊れた","壊れた鎖"],
"🪏":["シャベル","ショベル","スコップ","掘る","穴","鋤"],
"☠":["ドクロマーク","体","交差した骨","死","顔","モンスター","骸骨","ハロウィーン"],
"👽":["宇宙人","怪獣","異星人","顔","おとぎ話","ファンタジー","モンスター","宇宙","UFO"],
"🤖":["ロボットの顔","顔","モンスター","ロボット"],
@@ -1553,7 +1518,6 @@
"©️":["コピーライトマーク","著作権"],
"®️":["登録商標マーク","登録済み","商標"],
"™️":["商標マーク","マーク","tm","商標"],
"🫟":["しぶき","ペンキ","飛沫","飛び散り","スプラッシュ"],
"🔚":["ENDと左矢印","矢印","端"],
"🔙":["BACKと左矢印","矢印","戻る"],
"🔛":["ON!と左右矢印","矢印","マーク","オン"],
@@ -1679,7 +1643,6 @@
"🇧🇷":["ブラジル国旗","ブラジル","国旗"],
"🇧🇸":["バハマ国旗","バハマ","国旗"],
"🇧🇹":["ブータン国旗","ブータン","国旗"],
"🇧🇻":["ブーベ島の旗","ブーベ島","国旗"],
"🇧🇼":["ボツワナ国旗","ボツワナ","国旗"],
"🇧🇾":["ベラルーシ国旗","ベラルーシ","国旗"],
"🇧🇿":["ベリーズ国旗","ベリーズ","国旗"],
@@ -1695,8 +1658,6 @@
"🇨🇲":["カメルーン国旗","カメルーン","国旗"],
"🇨🇳":["中国国旗","中国","国旗"],
"🇨🇴":["コロンビア国旗","コロンビア","国旗"],
"🇨🇵":["クリッパートン島の旗","クリッパートン島","国旗"],
"🇨🇶":["サーク島の旗", "サーク島", "国旗"],
"🇨🇷":["コスタリカ国旗","コスタリカ","国旗"],
"🇨🇺":["キューバ国旗","キューバ","国旗"],
"🇨🇻":["カーボベルデ国旗","カーボ","ケープ","国旗","ベルデ"],
@@ -1705,13 +1666,11 @@
"🇨🇾":["キプロス国旗","キプロス","国旗"],
"🇨🇿":["チェコ国旗","チェコ共和国","国旗"],
"🇩🇪":["ドイツ国旗","国旗","ドイツ"],
"🇩🇬":["ディエゴガルシア島の旗","ディエゴガルシア島","国旗"],
"🇩🇯":["ジブチ国旗","ジブチ","国旗"],
"🇩🇰":["デンマーク国旗","デンマーク","国旗"],
"🇩🇲":["ドミニカ国旗","ドミニカ","国旗"],
"🇩🇴":["ドミニカ共和国国旗","ドミニカ共和国","国旗"],
"🇩🇿":["アルジェリア国旗","アルジェリア","国旗"],
"🇪🇦":["セウタ・メリリャの旗","セウタ・メリリャ","国旗"],
"🇪🇨":["エクアドル国旗","エクアドル","国旗"],
"🏴󠁧󠁢󠁥󠁮󠁧󠁿":["イングランドの旗","イングランド","旗"],
"🇪🇪":["エストニア国旗","エストニア","国旗"],
@@ -1747,7 +1706,6 @@
"🇬🇼":["ギニアビサウ国旗","ビサウ","国旗","ギニア"],
"🇬🇾":["ガイアナ国旗","国旗","ガイアナ"],
"🇭🇰":["香港の旗","中国","国旗","香港"],
"🇭🇲":["ハード島・マクドナルド諸島の旗","ハード島・マクドナルド諸島","国旗"],
"🇭🇳":["ホンジュラス国旗","国旗","ホンジュラス"],
"🇭🇷":["クロアチア国旗","クロアチア","国旗"],
"🇭🇹":["ハイチ国旗","国旗","ハイチ"],
@@ -1793,7 +1751,6 @@
"🇲🇨":["モナコ国旗","国旗","モナコ"],
"🇲🇩":["モルドバ国旗","国旗","モルドバ"],
"🇲🇪":["モンテネグロ国旗","国旗","モンテネグロ"],
"🇲🇫":["サン・マルタンの旗","サン・マルタン","国旗"],
"🇲🇬":["マダガスカル国旗","国旗","マダガスカル"],
"🇲🇭":["マーシャル諸島国旗","国旗","諸島","マーシャル"],
"🇲🇰":["マケドニア国旗","国旗","マケドニア"],
@@ -1854,7 +1811,6 @@
"🇸🇬":["シンガポール国旗","国旗","シンガポール"],
"🇸🇭":["セントヘレナ島の旗","旗","ヘレナ","セント"],
"🇸🇮":["スロベニア国旗","国旗","スロベニア"],
"🇸🇯":["スバールバル諸島・ヤンマイエン島の旗","スバールバル諸島・ヤンマイエン島","国旗"],
"🇸🇰":["スロバキア国旗","国旗","スロバキア"],
"🇸🇱":["シエラレオネ国旗","国旗","シエラレオネ"],
"🇸🇲":["サンマリノ国旗","国旗","サンマリノ"],
@@ -1886,7 +1842,6 @@
"🇹🇿":["タンザニア国旗","国旗","タンザニア"],
"🇺🇦":["ウクライナ国旗","国旗","ウクライナ"],
"🇺🇬":["ウガンダ国旗","国旗","ウガンダ"],
"🇺🇲":["合衆国領有小離島の旗","合衆国領有小離島","国旗"],
"🇺🇳":["国連の旗","旗","国連","連合","国際"],
"🇺🇸":["アメリカ国旗","アメリカ","旗","合衆","合衆国","アメリカ合衆国","合衆国領有小離島"],
"🇺🇾":["ウルグアイ国旗","国旗","ウルグアイ"],
@@ -36,9 +36,6 @@
"🤡": ["ぴえろのかお","ぴえろ","かお"],
"😏": ["にやにやしたかお","かお","にやにや"],
"😶": ["くちのないかお","かお","くち","しずかに","ちんもく"],
"🙂‍↔️": ["いいえ","ふさんせい","かお","くびをふる","くびをよこにふる"],
"🙂‍↕️": ["はい","うなずく","かお","くびをふる","くびをたてにふる"],
"🫩": ["くま","てつや","つかれた","ねむい","かお"],
"🫥": ["てんせんのかお","おちこんだ","きえる","かくれる","ないこうてき","めにみえない"],
"😐": ["ふつうのかお","むひょうじょう","かお","へいせい"],
"🫤": ["くちがななめになったかお","がっかり","むかんしん","うたがいぶかい","ふあん"],
@@ -114,38 +111,6 @@
"💩": ["うんち","まんが","ふん","かお","もんすたー"],
"👻": ["おばけ","ようかい","かお","おとぎばなし","ふぁんたじー","ゆうれい","もんすたー","はろうぃーん"],
"💀": ["どくろ","からだ","し","かお","おとぎばなし","もんすたー","がいこつ","はろうぃーん"],
"🫆": ["しもん","かんしき","せいたいにんしょう","せきゅりてぃ"],
"🏃‍➡️": ["じょぎんぐ","まらそん","らんなー","らんにんぐ","みぎむき","いそぐ","はしる","はしるひと","かけあし"],
"🏃‍♀️‍➡️": ["じょぎんぐ","まらそん","らんなー","らんにんぐ","みぎむき","じょせい","いそぐ","はしる","はしるおんな","かけあし"],
"🏃‍♂️‍➡️": ["じょぎんぐ","まらそん","らんなー","らんにんぐ","みぎむき","だんせい","はしるおとこ"],
"🚶‍➡️": ["うぉーきんぐ","はいきんぐ","ぶらつく","ひと","みぎむき","おおまた","さんぽ","あるく","ほこう","ほこうしゃ"],
"🚶‍♀️‍➡️": ["うぉーきんぐ","はいきんぐ","みぎむき","じょせい","さんぽ","あるく","あるくおんな","ほこう","ほこうしゃ"],
"🚶‍♂️‍➡️": ["うぉーきんぐ","みぎむき","あるくおとこ","ほこう","ほこうしゃ","だんせい"],
"🧎‍➡️": ["ひざまずく","ひと","みぎむき","すわる","せいざするひと"],
"🧎‍♀️‍➡️": ["ひざまずく","みぎむき","じょせい","すわる","せいざするじょせい"],
"🧎‍♂️‍➡️": ["ひざまずく","みぎむき","すわる","せいざするだんせい","だんせい"],
"🧑‍🦯‍➡️": ["あくせしびりてぃ","みぎむき","つえをついたひと","め","しかく","しょうがい"],
"👩‍🦯‍➡️": ["あくせしびりてぃ","みぎむき","じょせい","つえをついたじょせい","はくじょうをついたじょせい","め","しかく","しょうがい"],
"👨‍🦯‍➡️": ["あくせしびりてぃ","みぎむき","つえをついただんせい","だんせい","はくじょうをついただんせい","め","しかく","しょうがい"],
"🧑‍🦼‍➡️": ["あくせしびりてぃ","みぎむき","くるまいす","しょうがい","でんどうくるまいすのひと"],
"👩‍🦼‍➡️": ["あくせしびりてぃ","みぎむき","じょせい","くるまいす","しょうがい","でんどうくるまいすのじょせい"],
"👨‍🦼‍➡️": ["あくせしびりてぃ","みぎむき","だんせい","くるまいす","しょうがい","でんどうくるまいすのだんせい"],
"🧑‍🦽‍➡️": ["あくせしびりてぃ","みぎむき","しゅどうしきくるまいすのひと","くるまいす","しょうがい"],
"👩‍🦽‍➡️": ["あくせしびりてぃ","みぎむき","じょせい","しゅどうしきくるまいすのじょせい","くるまいす","しょうがい"],
"👨‍🦽‍➡️": ["あくせしびりてぃ","みぎむき","しゅどうしきくるまいすのだんせい","だんせい","くるまいす","しょうがい"],
"👨‍👩‍👦": ["じょせい","こども","かぞく","おとこのこ","だんせい","おやこ"],
"🧑‍🧑‍🧒": ["おとなふたり","こどもひとり","かぞく","おとなふたりとこどもひとり","おやこ"],
"🧑‍🧑‍🧒‍🧒": ["おとなふたり","こどもふたり","かぞく","おとなふたりとこどもふたり","おやこ"],
"🧑‍🧒": ["おとなひとり","こどもひとり","かぞく","おとなひとりとこどもひとり","おやこ"],
"🧑‍🧒‍🧒": ["おとなひとり","こどもふたり","かぞく","おとなひとりとこどもふたり","おやこ"],
"🐦‍🔥": ["ふぁんたじー","ふぇにっくす","ふしちょう","さいせい","ふっかつ","ひのとり","うまれかわり","しんわ","てんせい","りんね"],
"🪾": ["ふもう","かんばつ","はのないき","ふゆ"],
"🍋‍🟩": ["さわやか","とろぴかる","ふるーつ","まるがりーた","もひーと","らいむ","くだもの","かんきつるい","みどり","すっぱい"],
"🍄‍🟫": ["きのこ","しいたけ","とりゅふ","ぶらうんまっしゅるーむ","ぽーとべろー","まっしゅるーむ","たけ","きんるい"],
"🫜": ["びーつ","にわ","ね","かぶ","やさい"],
"🪉": ["おーけすとら","きゅーぴっど","はーぷ","げんがっき","あい","がっき","おんがく"],
"⛓️‍💥": ["くさり","ちぇーん","こわれた","こわれたくさり"],
"🪏": ["しゃべる","しょべる","すこっぷ","ほる","あな","すき"],
"☠": ["どくろまーく","からだ","こうさしたほね","し","かお","もんすたー","がいこつ","はろうぃーん"],
"👽": ["うちゅうじん","かいじゅう","いせいじん","かお","おとぎばなし","ふぁんたじー","もんすたー","うちゅう","UFO"],
"🤖": ["ろぼっとのかお","かお","もんすたー","ろぼっと"],
@@ -417,9 +382,9 @@
"🚶‍♀️": ["あるくじょせい","はいきんぐ","ほこうしゃ","あるく","うぉーきんぐ","じょせい","おんな"],
"🚶": ["あるくひと","はいきんぐ","ほこうしゃ","あるく","うぉーきんぐ"],
"🚶‍♂️": ["あるくだんせい","はいきんぐ","ほこうしゃ","あるく","うぉーきんぐ","おとこ","だんせい"],
"👩‍🦯": ["はくじょうをもったじょせい","あくせしびりてぃ","めがふじゆう","じょせい","おんな"],
"🧑‍🦯": ["はくじょうをもったひと","あくせしびりてぃ","めがふじゆう"],
"👨‍🦯": ["はくじょうをもっただんせい","あくせしびりてぃ","めがふじゆう","だんせい","おとこ"],
"👩‍🦯": ["しろつえをもったじょせい","あくせしびりてぃ","めがふじゆう","じょせい","おんな"],
"🧑‍🦯": ["しろつえをもったひと","あくせしびりてぃ","めがふじゆう"],
"👨‍🦯": ["しろつえをもっただんせい","あくせしびりてぃ","めがふじゆう","だんせい","おとこ"],
"🏃‍♀️": ["はしるじょせい","まらそん","らんなー","らんにんぐ","じょせい","おんな"],
"🏃": ["はしるひと","まらそん","らんなー","らんにんぐ"],
"🏃‍♂️": ["はしるだんせい","まらそん","らんなー","らんにんぐ","おとこ","だんせい"],
@@ -1164,7 +1129,7 @@
"🧾": ["りょうしゅうしょ","かいけい","ぼき","しょうこ","しょうめい"],
"💎": ["ほうせき","だいあもんど","じゅえる","ろまんす"],
"⚖": ["はかり","てんびん","こうせい","てんびんざ","ものさし","どうぐ","じゅうりょう","せいざ"],
"🦯": ["はくじょう","あくせしびりてぃ","めがふじゆう"],
"🦯": ["しろつえ","あくせしびりてぃ","めがふじゆう"],
"🧰": ["どうぐばこ","むね","せいびし","こうぐ"],
"🔧": ["れんち","どうぐ"],
"🪛": ["どらいばー","ねじ","こうぐ"],
@@ -1553,7 +1518,6 @@
"©️": ["こぴーらいとまーく","ちょさくけん"],
"®️": ["とうろくしょうひょうまーく","とうろくずみ","しょうひょう"],
"™️": ["しょうひょうまーく","まーく","tm","しょうひょう"],
"🫟": ["しぶき","ぺんき","ひまつ","とびちり","すぷらっしゅ"],
"🔚": ["ENDとひだりやじるし","やじるし","はじ"],
"🔙": ["BACKとひだりやじるし","やじるし","もどる"],
"🔛": ["ON!とさゆうやじるし","やじるし","まーく","おん"],
@@ -1679,7 +1643,6 @@
"🇧🇷": ["ぶらじるこっき","ぶらじる","こっき"],
"🇧🇸": ["ばはまこっき","ばはま","こっき"],
"🇧🇹": ["ぶーたんこっき","ぶーたん","こっき"],
"🇧🇻": ["ぶーべとうのはた","ぶーべとう","こっき"],
"🇧🇼": ["ぼつわなこっき","ぼつわな","こっき"],
"🇧🇾": ["べらるーしこっき","べらるーし","こっき"],
"🇧🇿": ["べりーずこっき","べりーず","こっき"],
@@ -1695,8 +1658,6 @@
"🇨🇲": ["かめるーんこっき","かめるーん","こっき"],
"🇨🇳": ["ちゅうごくこっき","ちゅうごく","こっき"],
"🇨🇴": ["ころんびあこっき","ころんびあ","こっき"],
"🇨🇵": ["くりっぱーとんとうのはた","くりっぱーとんとう","こっき"],
"🇨🇶": ["さーくとうのはた", "さーくとう", "こっき"],
"🇨🇷": ["こすたりかこっき","こすたりか","こっき"],
"🇨🇺": ["きゅーばこっき","きゅーば","こっき"],
"🇨🇻": ["かーぼべるでこっき","かーぼ","けーぷ","こっき","べるで"],
@@ -1705,13 +1666,11 @@
"🇨🇾": ["きぷろすこっき","きぷろす","こっき"],
"🇨🇿": ["ちぇここっき","ちぇこきょうわこく","こっき"],
"🇩🇪": ["どいつこっき","こっき","どいつ"],
"🇩🇬": ["でぃえごがるしあとうのはた","でぃえごがるしあとう","こっき"],
"🇩🇯": ["じぶちこっき","じぶち","こっき"],
"🇩🇰": ["でんまーくこっき","でんまーく","こっき"],
"🇩🇲": ["どみにかこっき","どみにか","こっき"],
"🇩🇴": ["どみにかきょうわこくこっき","どみにかきょうわこく","こっき"],
"🇩🇿": ["あるじぇりあこっき","あるじぇりあ","こっき"],
"🇪🇦": ["せうた・めりりゃのはた","せうた・めりりゃ","こっき"],
"🇪🇨": ["えくあどるこっき","えくあどる","こっき"],
"🏴󠁧󠁢󠁥󠁮󠁧󠁿": ["いんぐらんどのはた","いんぐらんど","こっき"],
"🇪🇪": ["えすとにあこっき","えすとにあ","こっき"],
@@ -1747,7 +1706,6 @@
"🇬🇼": ["ぎにあびさうこっき","びさう","こっき","ぎにあ"],
"🇬🇾": ["がいあなこっき","こっき","がいあな"],
"🇭🇰": ["ほんこんのはた","ちゅうごく","こっき","ほんこん"],
"🇭🇲": ["はーどとう・まくどなるどしょとうのはた","はーどとう・まくどなるどしょとう","こっき"],
"🇭🇳": ["ほんじゅらすこっき","こっき","ほんじゅらす"],
"🇭🇷": ["くろあちあこっき","くろあちあ","こっき"],
"🇭🇹": ["はいちこっき","こっき","はいち"],
@@ -1793,7 +1751,6 @@
"🇲🇨": ["もなここっき","こっき","もなこ"],
"🇲🇩": ["もるどばこっき","こっき","もるどば"],
"🇲🇪": ["もんてねぐろこっき","こっき","もんてねぐろ"],
"🇲🇫": ["さん・まるたんのはた","さん・まるたん","こっき"],
"🇲🇬": ["まだがすかるこっき","こっき","まだがすかる"],
"🇲🇭": ["まーしゃるしょとうこっき","こっき","しょとう","まーしゃる"],
"🇲🇰": ["まけどにあこっき","こっき","まけどにあ"],
@@ -1854,7 +1811,6 @@
"🇸🇬": ["しんがぽーるこっき","こっき","しんがぽーる"],
"🇸🇭": ["せんとへれなとうのはた","はた","へれな","せんと"],
"🇸🇮": ["すろべにあこっき","こっき","すろべにあ"],
"🇸🇯": ["すばーるばるしょとう・やんまいえんとうのはた","すばーるばるしょとう・やんまいえんとう","こっき"],
"🇸🇰": ["すろばきあこっき","こっき","すろばきあ"],
"🇸🇱": ["しえられおねこっき","こっき","しえられおね"],
"🇸🇲": ["さんまりのこっき","こっき","さんまりの"],
@@ -1886,7 +1842,6 @@
"🇹🇿": ["たんざにあこっき","こっき","たんざにあ"],
"🇺🇦": ["うくらいなこっき","こっき","うくらいな"],
"🇺🇬": ["うがんだこっき","こっき","うがんだ"],
"🇺🇲": ["がっしゅうこくりょうゆうしょうりとうのはた","がっしゅうこくりょうゆうしょうりとう","こっき"],
"🇺🇳": ["こくれんのはた","はた","こくれん","れんごう","こくさい"],
"🇺🇸": ["あめりかこっき","あめりか","はた","ごうしゅう","がっしゅうこく","あめりかがっしゅうこく","がっしゅうこくりょうゆうしょうりとう"],
"🇺🇾": ["うるぐあいこっき","こっき","うるぐあい"],
@@ -1,37 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { GeneratedSearchIndexItem } from 'search-index';
export type SearchIndexItem = {
id: string;
parentId?: string;
path?: string;
label: string;
keywords: string[];
texts: string[];
icon?: string;
};
export function genSearchIndexes(generated: GeneratedSearchIndexItem[]): SearchIndexItem[] {
const rootMods = new Map(generated.map(item => [item.id, item]));
// link inlining here
for (const item of generated) {
if (item.inlining) {
for (const id of item.inlining) {
const inline = rootMods.get(id);
if (inline) {
inline.parentId = item.id;
inline.path = item.path;
} else {
console.log('[Settings Search Index] Failed to inline', id);
}
}
}
}
return generated;
}
@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { unisonReload } from '@/utility/unison-reload.js';
let isReloadConfirming = false;
export async function reloadAsk(opts: {
unison?: boolean;
reason?: string;
}) {
if (isReloadConfirming) {
return;
}
isReloadConfirming = true;
const { canceled } = await os.confirm(opts.reason == null ? {
type: 'info',
text: i18n.ts.reloadConfirm,
} : {
type: 'info',
title: i18n.ts.reloadConfirm,
text: opts.reason,
}).finally(() => {
isReloadConfirming = false;
});
if (canceled) return;
if (opts.unison) {
unisonReload();
} else {
window.location.reload();
}
}
@@ -1,12 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ref } from 'vue';
export const shouldSuggestReload = ref(false);
export function suggestReload() {
shouldSuggestReload.value = true;
}
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { searchIndexes as generated } from 'search-index:settings';
import type { GeneratedSearchIndexItem } from 'search-index:settings';
export type SearchIndexItem = {
id: string;
parentId?: string;
path?: string;
label: string;
keywords: string[];
icon?: string;
};
const rootMods = new Map(generated.map(item => [item.id, item]));
// link inlining here
for (const item of generated) {
if (item.inlining) {
for (const id of item.inlining) {
const inline = rootMods.get(id);
if (inline) {
inline.parentId = item.id;
inline.path = item.path;
} else {
console.log('[Settings Search Index] Failed to inline', id);
}
}
}
}
export const searchIndexes: SearchIndexItem[] = generated;
+10 -19
View File
@@ -3,25 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
type XGeneratedSearchIndexItem = {
id: string;
parentId?: string;
path?: string;
label: string;
keywords: string[];
texts: string[];
icon?: string;
inlining?: string[];
};
declare module 'search-index' {
export type GeneratedSearchIndexItem = XGeneratedSearchIndexItem;
}
declare module 'search-index:settings' {
export const searchIndexes: XGeneratedSearchIndexItem[];
}
export type GeneratedSearchIndexItem = {
id: string;
parentId?: string;
path?: string;
label: string;
keywords: string[];
icon?: string;
inlining?: string[];
};
declare module 'search-index:admin' {
export const searchIndexes: XGeneratedSearchIndexItem[];
export const searchIndexes: GeneratedSearchIndexItem[];
}
-5
View File
@@ -28,11 +28,6 @@ export const searchIndexes = [{
mainVirtualModule: 'search-index:settings',
modulesToHmrOnUpdate: ['src/pages/settings/index.vue'],
verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
}, {
targetFilePaths: ['src/pages/admin/*.vue'],
mainVirtualModule: 'search-index:admin',
modulesToHmrOnUpdate: ['src/pages/admin/index.vue'],
verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
}] satisfies SearchIndexOptions[];
/**
+6 -6
View File
@@ -11,17 +11,17 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.17.0",
"@types/node": "22.16.4",
"@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0"
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0"
},
"dependencies": {
"@tabler/icons-webfont": "3.34.1",
"harfbuzzjs": "0.4.8",
"@tabler/icons-webfont": "3.34.0",
"harfbuzzjs": "0.4.7",
"tiny-glob": "0.2.9",
"tsx": "4.20.3",
"typescript": "5.9.2",
"typescript": "5.8.3",
"wawoff2": "2.0.1"
},
"files": [
+5 -5
View File
@@ -24,13 +24,13 @@
"devDependencies": {
"@types/matter-js": "0.19.8",
"@types/seedrandom": "3.0.8",
"@types/node": "22.17.0",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"@types/node": "22.16.4",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"nodemon": "3.1.10",
"execa": "9.6.0",
"typescript": "5.9.2",
"esbuild": "0.25.8",
"typescript": "5.8.3",
"esbuild": "0.25.6",
"glob": "11.0.3"
},
"files": [
+23
View File
@@ -0,0 +1,23 @@
{
"$schema": "https://swc.rs/schema.json",
"jsc": {
"parser": {
"syntax": "typescript",
"dynamicImport": true,
"decorators": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"experimental": {
"keepImportAssertions": true
},
"baseUrl": "src",
"paths": {
"@/*": ["*"]
},
"target": "es2022"
},
"minify": false
}
+1 -1
View File
@@ -9,7 +9,7 @@ export default [
'**/node_modules',
'built',
'coverage',
'vitest.config.ts',
'jest.config.ts',
'test',
'test-d',
'generator',
+1 -1
View File
@@ -7,7 +7,7 @@
"generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix"
},
"devDependencies": {
"@readme/openapi-parser": "5.0.0",
"@readme/openapi-parser": "2.7.0",
"@types/node": "22.16.4",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
@@ -2,7 +2,7 @@ import assert from 'assert';
import { mkdir, readFile, writeFile } from 'fs/promises';
import type { OpenAPIV3_1 } from 'openapi-types';
import { toPascal } from 'ts-case-convert';
import { parse } from '@readme/openapi-parser';
import OpenAPIParser from '@readme/openapi-parser';
import openapiTS, { astToString } from 'openapi-typescript';
import type { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
import ts from 'typescript';
@@ -401,7 +401,7 @@ async function main() {
await mkdir(generatePath, { recursive: true });
const openApiJsonPath = './api.json';
const openApiDocs = await parse(openApiJsonPath) as OpenAPIV3_1.Document;
const openApiDocs = await OpenAPIParser.parse(openApiJsonPath) as OpenAPIV3_1.Document;
const typeFileName = './built/autogen/types.ts';
await generateBaseTypes(openApiDocs, openApiJsonPath, typeFileName);

Some files were not shown because too many files have changed in this diff Show More