Merge remote-tracking branch 'misskey/develop' into future-2024-04-10
This commit is contained in:
		
						commit
						a3b4ca782a
					
				
					 78 changed files with 3068 additions and 2243 deletions
				
			
		
							
								
								
									
										14
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										14
									
								
								CHANGELOG.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -18,6 +18,13 @@
 | 
			
		|||
  - 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました
 | 
			
		||||
- Enhance: ページのデザインを変更
 | 
			
		||||
- Enhance: 2要素認証(ワンタイムパスワード)の入力欄を改善
 | 
			
		||||
- Enhance: 「今日誕生日のフォロー中ユーザー」ウィジェットを手動でリロードできるように
 | 
			
		||||
- Enhance: 映像・音声の再生にブラウザのネイティブプレイヤーを使用できるように
 | 
			
		||||
- Enhance: 映像・音声の再生メニューに「再生速度」「ループ再生」「ピクチャインピクチャ」を追加
 | 
			
		||||
- Enhance: 映像・音声の再生にキーボードショートカットが使えるように
 | 
			
		||||
- Enhance: ノートについているリアクションの「もっと!」から、リアクションの一覧を表示できるように
 | 
			
		||||
- Enhance: リプライにて引用がある場合テキストが空でもノートできるように
 | 
			
		||||
  - 引用したいノートのURLをコピーしリプライ投稿画面にペーストして添付することで達成できます
 | 
			
		||||
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
 | 
			
		||||
- Fix: 周年の実績が閏年を考慮しない問題を修正
 | 
			
		||||
- Fix: ローカルURLのプレビューポップアップが左上に表示される
 | 
			
		||||
| 
						 | 
				
			
			@ -27,12 +34,19 @@
 | 
			
		|||
  (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/528)
 | 
			
		||||
- Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177
 | 
			
		||||
  - CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。
 | 
			
		||||
- Fix: タイムゾーンによっては、「今日誕生日のフォロー中ユーザー」ウィジェットが正しく動作しない問題を修正
 | 
			
		||||
- Fix: CWのみの引用リノートが詳細ページで純粋なリノートとして誤って扱われてしまう問題を修正
 | 
			
		||||
- Fix: ノート詳細ページにおいてCW付き引用リノートのCWボタンのラベルに「引用」が含まれていない問題を修正
 | 
			
		||||
- Fix: ダイアログの入力で字数制限に違反していてもEnterキーが押せてしまう問題を修正
 | 
			
		||||
 | 
			
		||||
### Server
 | 
			
		||||
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
 | 
			
		||||
- Enhance: misskey-dev/summaly@5.1.0の取り込み(プレビュー生成処理の効率化)
 | 
			
		||||
- Fix: フォローリクエストを作成する際に既存のものは削除するように  
 | 
			
		||||
  (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/440)
 | 
			
		||||
- Fix: エンドポイント`notes/translate`のエラーを改善
 | 
			
		||||
- Fix: CleanRemoteFilesProcessorService report progress from 100% (#13632)
 | 
			
		||||
- Fix: 一部の音声ファイルが映像ファイルとして扱われる問題を修正
 | 
			
		||||
 | 
			
		||||
## 2024.3.1
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										40
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										40
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -5125,6 +5125,14 @@ export interface Locale extends ILocale {
 | 
			
		|||
     * バックアップコードを使う
 | 
			
		||||
     */
 | 
			
		||||
    "useBackupCode": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * アプリを起動
 | 
			
		||||
     */
 | 
			
		||||
    "launchApp": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 動画・音声の再生にブラウザのUIを使用する
 | 
			
		||||
     */
 | 
			
		||||
    "useNativeUIForVideoAudioPlayer": string;
 | 
			
		||||
    "_bubbleGame": {
 | 
			
		||||
        /**
 | 
			
		||||
         * 遊び方
 | 
			
		||||
| 
						 | 
				
			
			@ -7767,13 +7775,9 @@ export interface Locale extends ILocale {
 | 
			
		|||
         */
 | 
			
		||||
        "step1": ParameterizedString<"a" | "b">;
 | 
			
		||||
        /**
 | 
			
		||||
         * 次に、表示されているQRコードをアプリでスキャンします。
 | 
			
		||||
         * 次に、表示されているQRコードをアプリでスキャンするか、ボタンをクリックして端末上でアプリを開きます。
 | 
			
		||||
         */
 | 
			
		||||
        "step2": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。
 | 
			
		||||
         */
 | 
			
		||||
        "step2Click": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * デスクトップアプリを使用する場合は次のURIを入力します
 | 
			
		||||
         */
 | 
			
		||||
| 
						 | 
				
			
			@ -7866,6 +7870,10 @@ export interface Locale extends ILocale {
 | 
			
		|||
         * バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。
 | 
			
		||||
         */
 | 
			
		||||
        "backupCodesExhaustedWarning": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 詳細なガイドはこちら
 | 
			
		||||
         */
 | 
			
		||||
        "moreDetailedGuideHere": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_permissions": {
 | 
			
		||||
        /**
 | 
			
		||||
| 
						 | 
				
			
			@ -9079,6 +9087,14 @@ export interface Locale extends ILocale {
 | 
			
		|||
             * ボタン
 | 
			
		||||
             */
 | 
			
		||||
            "button": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * 動的ブロック
 | 
			
		||||
             */
 | 
			
		||||
            "dynamic": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * このブロックは廃止されています。今後は{play}を利用してください。
 | 
			
		||||
             */
 | 
			
		||||
            "dynamicDescription": ParameterizedString<"play">;
 | 
			
		||||
            /**
 | 
			
		||||
             * ノート埋め込み
 | 
			
		||||
             */
 | 
			
		||||
| 
						 | 
				
			
			@ -10133,6 +10149,20 @@ export interface Locale extends ILocale {
 | 
			
		|||
         */
 | 
			
		||||
        "summaryProxyDescription2": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_mediaControls": {
 | 
			
		||||
        /**
 | 
			
		||||
         * ピクチャインピクチャ
 | 
			
		||||
         */
 | 
			
		||||
        "pip": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 再生速度
 | 
			
		||||
         */
 | 
			
		||||
        "playbackRate": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * ループ再生
 | 
			
		||||
         */
 | 
			
		||||
        "loop": string;
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
declare const locales: {
 | 
			
		||||
    [lang: string]: Locale;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1277,6 +1277,8 @@ gameRetry: "リトライ"
 | 
			
		|||
notUsePleaseLeaveBlank: "使用しない場合は空欄にしてください"
 | 
			
		||||
useTotp: "ワンタイムパスワードを使う"
 | 
			
		||||
useBackupCode: "バックアップコードを使う"
 | 
			
		||||
launchApp: "アプリを起動"
 | 
			
		||||
useNativeUIForVideoAudioPlayer: "動画・音声の再生にブラウザのUIを使用する"
 | 
			
		||||
 | 
			
		||||
_bubbleGame:
 | 
			
		||||
  howToPlay: "遊び方"
 | 
			
		||||
| 
						 | 
				
			
			@ -2039,8 +2041,7 @@ _2fa:
 | 
			
		|||
  alreadyRegistered: "既に設定は完了しています。"
 | 
			
		||||
  registerTOTP: "認証アプリの設定を開始"
 | 
			
		||||
  step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。"
 | 
			
		||||
  step2: "次に、表示されているQRコードをアプリでスキャンします。"
 | 
			
		||||
  step2Click: "QRコードをクリックすると、お使いの端末にインストールされている認証アプリやキーリングに登録できます。"
 | 
			
		||||
  step2: "次に、表示されているQRコードをアプリでスキャンするか、ボタンをクリックして端末上でアプリを開きます。"
 | 
			
		||||
  step2Uri: "デスクトップアプリを使用する場合は次のURIを入力します"
 | 
			
		||||
  step3Title: "確認コードを入力"
 | 
			
		||||
  step3: "アプリに表示されている確認コード(トークン)を入力します。"
 | 
			
		||||
| 
						 | 
				
			
			@ -2064,6 +2065,7 @@ _2fa:
 | 
			
		|||
  backupCodesDescription: "認証アプリが使用できなくなった場合、以下のバックアップコードを使ってアカウントにアクセスできます。これらのコードは必ず安全な場所に保管してください。各コードは一回だけ使用できます。"
 | 
			
		||||
  backupCodeUsedWarning: "バックアップコードが使用されました。認証アプリが使えなくなっている場合、なるべく早く認証アプリを再設定してください。"
 | 
			
		||||
  backupCodesExhaustedWarning: "バックアップコードが全て使用されました。認証アプリを利用できない場合、これ以上アカウントにアクセスできなくなります。認証アプリを再登録してください。"
 | 
			
		||||
  moreDetailedGuideHere: "詳細なガイドはこちら"
 | 
			
		||||
 | 
			
		||||
_permissions:
 | 
			
		||||
  "read:account": "アカウントの情報を見る"
 | 
			
		||||
| 
						 | 
				
			
			@ -2393,6 +2395,8 @@ _pages:
 | 
			
		|||
    section: "セクション"
 | 
			
		||||
    image: "画像"
 | 
			
		||||
    button: "ボタン"
 | 
			
		||||
    dynamic: "動的ブロック"
 | 
			
		||||
    dynamicDescription: "このブロックは廃止されています。今後は{play}を利用してください。"
 | 
			
		||||
 | 
			
		||||
    note: "ノート埋め込み"
 | 
			
		||||
    _note:
 | 
			
		||||
| 
						 | 
				
			
			@ -2697,3 +2701,8 @@ _urlPreviewSetting:
 | 
			
		|||
  summaryProxy: "プレビューを生成するプロキシのエンドポイント"
 | 
			
		||||
  summaryProxyDescription: "Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。"
 | 
			
		||||
  summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。"
 | 
			
		||||
 | 
			
		||||
_mediaControls:
 | 
			
		||||
  pip: "ピクチャインピクチャ"
 | 
			
		||||
  playbackRate: "再生速度"
 | 
			
		||||
  loop: "ループ再生"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,7 +56,9 @@
 | 
			
		|||
		"postcss": "8.4.35",
 | 
			
		||||
		"tar": "6.2.0",
 | 
			
		||||
		"terser": "5.28.1",
 | 
			
		||||
		"typescript": "5.3.3"
 | 
			
		||||
		"typescript": "5.3.3",
 | 
			
		||||
		"esbuild": "0.19.11",
 | 
			
		||||
		"glob": "10.3.10"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@types/node": "^20.11.28",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,5 +19,6 @@
 | 
			
		|||
		},
 | 
			
		||||
		"target": "es2022"
 | 
			
		||||
	},
 | 
			
		||||
	"minify": false
 | 
			
		||||
	"minify": false,
 | 
			
		||||
	"sourceMaps": "inline"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,15 +11,15 @@
 | 
			
		|||
		"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
 | 
			
		||||
		"migrate": "pnpm typeorm migration:run -d ormconfig.js",
 | 
			
		||||
		"revert": "pnpm typeorm migration:revert -d ormconfig.js",
 | 
			
		||||
		"check:connect": "node ./check_connect.js",
 | 
			
		||||
		"check:connect": "node ./scripts/check_connect.js",
 | 
			
		||||
		"build": "swc src -d built -D",
 | 
			
		||||
		"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc",
 | 
			
		||||
		"watch:swc": "swc src -d built -D -w",
 | 
			
		||||
		"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
 | 
			
		||||
		"watch": "node watch.mjs",
 | 
			
		||||
		"watch": "node ./scripts/watch.mjs",
 | 
			
		||||
		"restart": "pnpm build && pnpm start",
 | 
			
		||||
		"dev": "nodemon -w src -e ts,js,mjs,cjs,json --exec \"cross-env NODE_ENV=development pnpm run restart\"",
 | 
			
		||||
		"typecheck": "pnpm --filter megalodon build && tsc --noEmit",
 | 
			
		||||
		"dev": "node ./scripts/dev.mjs",
 | 
			
		||||
		"typecheck": "pnpm --filter megalodon build && tsc --noEmit && tsc -p test --noEmit",
 | 
			
		||||
		"eslint": "eslint --quiet \"src/**/*.ts\"",
 | 
			
		||||
		"lint": "pnpm typecheck && pnpm eslint",
 | 
			
		||||
		"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
 | 
			
		||||
| 
						 | 
				
			
			@ -31,7 +31,7 @@
 | 
			
		|||
		"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
 | 
			
		||||
		"test-and-coverage": "pnpm jest-and-coverage",
 | 
			
		||||
		"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
 | 
			
		||||
		"generate-api-json": "pnpm build && node ./generate_api_json.js"
 | 
			
		||||
		"generate-api-json": "pnpm build && node ./scripts/generate_api_json.js"
 | 
			
		||||
	},
 | 
			
		||||
	"optionalDependencies": {
 | 
			
		||||
		"@swc/core-android-arm64": "1.3.11",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
 */
 | 
			
		||||
 | 
			
		||||
import Redis from 'ioredis';
 | 
			
		||||
import { loadConfig } from './built/config.js';
 | 
			
		||||
import { loadConfig } from '../built/config.js';
 | 
			
		||||
 | 
			
		||||
const config = loadConfig();
 | 
			
		||||
const redis = new Redis(config.redis);
 | 
			
		||||
							
								
								
									
										61
									
								
								packages/backend/scripts/dev.mjs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								packages/backend/scripts/dev.mjs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { execa, execaNode } from 'execa';
 | 
			
		||||
 | 
			
		||||
/** @type {import('execa').ExecaChildProcess | undefined} */
 | 
			
		||||
let backendProcess;
 | 
			
		||||
 | 
			
		||||
async function execBuildAssets() {
 | 
			
		||||
	await execa('pnpm', ['run', 'build-assets'], {
 | 
			
		||||
		cwd: '../../',
 | 
			
		||||
		stdout: process.stdout,
 | 
			
		||||
		stderr: process.stderr,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function execStart() {
 | 
			
		||||
	// pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので
 | 
			
		||||
	// 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい
 | 
			
		||||
	backendProcess = execaNode('./built/boot/entry.js', [], {
 | 
			
		||||
		stdout: process.stdout,
 | 
			
		||||
		stderr: process.stderr,
 | 
			
		||||
		env: {
 | 
			
		||||
			'NODE_ENV': 'development',
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function killProc() {
 | 
			
		||||
	if (backendProcess) {
 | 
			
		||||
		backendProcess.kill();
 | 
			
		||||
		await new Promise(resolve => backendProcess.on('exit', resolve));
 | 
			
		||||
		backendProcess = undefined;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
(async () => {
 | 
			
		||||
	execaNode(
 | 
			
		||||
		'./node_modules/nodemon/bin/nodemon.js',
 | 
			
		||||
		[
 | 
			
		||||
			'-w', 'src',
 | 
			
		||||
			'-e', 'ts,js,mjs,cjs,json',
 | 
			
		||||
			'--exec', 'pnpm', 'run', 'build',
 | 
			
		||||
		],
 | 
			
		||||
		{
 | 
			
		||||
			stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
 | 
			
		||||
		})
 | 
			
		||||
		.on('message', async (message) => {
 | 
			
		||||
			if (message.type === 'exit') {
 | 
			
		||||
				// かならずbuild->build-assetsの順番で呼び出したいので、
 | 
			
		||||
				// 少々トリッキーだがnodemonからのexitイベントを利用してbuild-assets->startを行う。
 | 
			
		||||
				// pnpm restartをbuildが終わる前にbuild-assetsが動いてしまうので、バラバラに呼び出す必要がある
 | 
			
		||||
 | 
			
		||||
				await killProc();
 | 
			
		||||
				await execBuildAssets();
 | 
			
		||||
				execStart();
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			@ -3,8 +3,8 @@
 | 
			
		|||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { loadConfig } from './built/config.js'
 | 
			
		||||
import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
 | 
			
		||||
import { loadConfig } from '../built/config.js'
 | 
			
		||||
import { genOpenapiSpec } from '../built/server/api/openapi/gen-spec.js'
 | 
			
		||||
import { writeFileSync } from "node:fs";
 | 
			
		||||
 | 
			
		||||
const config = loadConfig();
 | 
			
		||||
| 
						 | 
				
			
			@ -305,7 +305,7 @@ export class AccountMoveService {
 | 
			
		|||
		let resultUser: MiLocalUser | MiRemoteUser | null = null;
 | 
			
		||||
 | 
			
		||||
		if (this.userEntityService.isRemoteUser(dst)) {
 | 
			
		||||
			if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
 | 
			
		||||
			if (Date.now() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
 | 
			
		||||
				await this.apPersonService.updatePerson(dst.uri);
 | 
			
		||||
			}
 | 
			
		||||
			dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst;
 | 
			
		||||
| 
						 | 
				
			
			@ -321,7 +321,7 @@ export class AccountMoveService {
 | 
			
		|||
				if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー
 | 
			
		||||
 | 
			
		||||
				if (this.userEntityService.isRemoteUser(dst)) {
 | 
			
		||||
					if ((new Date()).getTime() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
 | 
			
		||||
					if (Date.now() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) {
 | 
			
		||||
						await this.apPersonService.updatePerson(srcUri);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,11 +8,13 @@ import * as crypto from 'node:crypto';
 | 
			
		|||
import * as stream from 'node:stream/promises';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import * as fileType from 'file-type';
 | 
			
		||||
import FFmpeg from 'fluent-ffmpeg';
 | 
			
		||||
import isSvg from 'is-svg';
 | 
			
		||||
import probeImageSize from 'probe-image-size';
 | 
			
		||||
import sharp from 'sharp';
 | 
			
		||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
 | 
			
		||||
import { encode } from 'blurhash';
 | 
			
		||||
import { LoggerService } from '@/core/LoggerService.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
 | 
			
		||||
export type FileInfo = {
 | 
			
		||||
| 
						 | 
				
			
			@ -43,8 +45,12 @@ const TYPE_SVG = {
 | 
			
		|||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class FileInfoService {
 | 
			
		||||
	private logger: Logger;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private loggerService: LoggerService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.logger = this.loggerService.getLogger('file-info');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
| 
						 | 
				
			
			@ -147,6 +153,34 @@ export class FileInfoService {
 | 
			
		|||
		return mime;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * ビデオファイルにビデオトラックがあるかどうかチェック
 | 
			
		||||
	 * (ない場合:m4a, webmなど)
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param path ファイルパス
 | 
			
		||||
	 * @returns ビデオトラックがあるかどうか(エラー発生時は常に`true`を返す)
 | 
			
		||||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private hasVideoTrackOnVideoFile(path: string): Promise<boolean> {
 | 
			
		||||
		const sublogger = this.logger.createSubLogger('ffprobe');
 | 
			
		||||
		sublogger.info(`Checking the video file. File path: ${path}`);
 | 
			
		||||
		return new Promise((resolve) => {
 | 
			
		||||
			try {
 | 
			
		||||
				FFmpeg.ffprobe(path, (err, metadata) => {
 | 
			
		||||
					if (err) {
 | 
			
		||||
						sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err);
 | 
			
		||||
						resolve(true);
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
					resolve(metadata.streams.some((stream) => stream.codec_type === 'video'));
 | 
			
		||||
				});
 | 
			
		||||
			} catch (err) {
 | 
			
		||||
				sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err as Error);
 | 
			
		||||
				resolve(true);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Detect MIME Type and extension
 | 
			
		||||
	 */
 | 
			
		||||
| 
						 | 
				
			
			@ -169,6 +203,20 @@ export class FileInfoService {
 | 
			
		|||
				return TYPE_SVG;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if ((type.mime.startsWith('video') || type.mime === 'application/ogg') && !(await this.hasVideoTrackOnVideoFile(path))) {
 | 
			
		||||
				const newMime = `audio/${type.mime.split('/')[1]}`;
 | 
			
		||||
				if (newMime === 'audio/mp4') {
 | 
			
		||||
					return {
 | 
			
		||||
						mime: 'audio/mp4',
 | 
			
		||||
						ext: 'm4a',
 | 
			
		||||
					};
 | 
			
		||||
				}
 | 
			
		||||
				return {
 | 
			
		||||
					mime: newMime,
 | 
			
		||||
					ext: type.ext,
 | 
			
		||||
				};
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return {
 | 
			
		||||
				mime: this.fixMime(type.mime),
 | 
			
		||||
				ext: type.ext,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -101,7 +101,7 @@ export class PushNotificationService implements OnApplicationShutdown {
 | 
			
		|||
				type,
 | 
			
		||||
				body: (type === 'notification' || type === 'unreadAntennaNote') ? truncateBody(type, body) : body,
 | 
			
		||||
				userId,
 | 
			
		||||
				dateTime: (new Date()).getTime(),
 | 
			
		||||
				dateTime: Date.now(),
 | 
			
		||||
			}), {
 | 
			
		||||
				proxy: this.config.proxy,
 | 
			
		||||
			}).catch((err: any) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -63,7 +63,7 @@ export class CleanRemoteFilesProcessorService {
 | 
			
		|||
				isLink: false,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			job.updateProgress(deletedCount / total);
 | 
			
		||||
			job.updateProgress(100 / total * deletedCount);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.logger.succ('All cached remote files has been deleted.');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
 | 
			
		||||
			const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
 | 
			
		||||
				me,
 | 
			
		||||
				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
 | 
			
		||||
				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
 | 
			
		||||
				true,
 | 
			
		||||
			);
 | 
			
		||||
			if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
 | 
			
		||||
			const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
 | 
			
		||||
				me,
 | 
			
		||||
				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
 | 
			
		||||
				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
 | 
			
		||||
				true,
 | 
			
		||||
			);
 | 
			
		||||
			if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
 | 
			
		||||
			const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
 | 
			
		||||
				me,
 | 
			
		||||
				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
 | 
			
		||||
				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
 | 
			
		||||
				true,
 | 
			
		||||
			);
 | 
			
		||||
			if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
 | 
			
		||||
			const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
 | 
			
		||||
				me,
 | 
			
		||||
				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
 | 
			
		||||
				(old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
 | 
			
		||||
				true,
 | 
			
		||||
			);
 | 
			
		||||
			if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,7 @@ export const meta = {
 | 
			
		|||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		optional: true, nullable: false,
 | 
			
		||||
		properties: {
 | 
			
		||||
			sourceLang: { type: 'string' },
 | 
			
		||||
			text: { type: 'string' },
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +39,11 @@ export const meta = {
 | 
			
		|||
			code: 'NO_SUCH_NOTE',
 | 
			
		||||
			id: 'bea9b03f-36e0-49c5-a4db-627a029f8971',
 | 
			
		||||
		},
 | 
			
		||||
		cannotTranslateInvisibleNote: {
 | 
			
		||||
			message: 'Cannot translate invisible note.',
 | 
			
		||||
			code: 'CANNOT_TRANSLATE_INVISIBLE_NOTE',
 | 
			
		||||
			id: 'ea29f2ca-c368-43b3-aaf1-5ac3e74bbe5d',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -72,21 +77,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
			});
 | 
			
		||||
 | 
			
		||||
			if (!(await this.noteEntityService.isVisibleForMe(note, me.id))) {
 | 
			
		||||
				return 204; // TODO: 良い感じのエラー返す
 | 
			
		||||
				throw new ApiError(meta.errors.cannotTranslateInvisibleNote);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (note.text == null) {
 | 
			
		||||
				return 204;
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const instance = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
			if (instance.deeplAuthKey == null && !instance.deeplFreeMode) {
 | 
			
		||||
				return 204; // TODO: 良い感じのエラー返す
 | 
			
		||||
			if (instance.deeplAuthKey == null && !instance.deeplFree				throw new ApiError(meta.errors.unavailable);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (instance.deeplFreeMode && !instance.deeplFreeInstance) {
 | 
			
		||||
				return 204;
 | 
			
		||||
				throw new ApiError(meta.errors.unavailable);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			let targetLang = ps.targetLang;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@
 | 
			
		|||
import { IsNull } from 'typeorm';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js';
 | 
			
		||||
import { birthdaySchema } from '@/models/User.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { QueryService } from '@/core/QueryService.js';
 | 
			
		||||
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -66,7 +67,7 @@ export const paramDef = {
 | 
			
		|||
			description: 'The local host is represented with `null`.',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		birthday: { type: 'string', nullable: true },
 | 
			
		||||
		birthday: { ...birthdaySchema, nullable: true },
 | 
			
		||||
	},
 | 
			
		||||
	anyOf: [
 | 
			
		||||
		{ required: ['userId'] },
 | 
			
		||||
| 
						 | 
				
			
			@ -127,9 +128,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
 | 
			
		||||
			if (ps.birthday) {
 | 
			
		||||
				try {
 | 
			
		||||
					const d = new Date(ps.birthday);
 | 
			
		||||
					d.setHours(0, 0, 0, 0);
 | 
			
		||||
					const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
 | 
			
		||||
					const birthday = ps.birthday.substring(5, 10);
 | 
			
		||||
					const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
 | 
			
		||||
					birthdayUserQuery.select('user_profile.userId')
 | 
			
		||||
						.where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,12 +8,13 @@ process.env.NODE_ENV = 'test';
 | 
			
		|||
import * as assert from 'assert';
 | 
			
		||||
import { MiNote } from '@/models/Note.js';
 | 
			
		||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
 | 
			
		||||
import { api, initTestDb, post, signup, uploadFile, uploadUrl } from '../utils.js';
 | 
			
		||||
import { api, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('Note', () => {
 | 
			
		||||
	let Notes: any;
 | 
			
		||||
 | 
			
		||||
	let root: misskey.entities.SignupResponse;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
	let tom: misskey.entities.SignupResponse;
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +22,7 @@ describe('Note', () => {
 | 
			
		|||
	beforeAll(async () => {
 | 
			
		||||
		const connection = await initTestDb(true);
 | 
			
		||||
		Notes = connection.getRepository(MiNote);
 | 
			
		||||
		root = await signup({ username: 'root' });
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
		tom = await signup({ username: 'tom', host: 'example.com' });
 | 
			
		||||
| 
						 | 
				
			
			@ -473,14 +475,14 @@ describe('Note', () => {
 | 
			
		|||
						value: true,
 | 
			
		||||
					},
 | 
			
		||||
				} as any,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
 | 
			
		||||
			const assign = await api('admin/roles/assign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(assign.status, 204);
 | 
			
		||||
			assert.strictEqual(file.body!.isSensitive, false);
 | 
			
		||||
| 
						 | 
				
			
			@ -508,11 +510,11 @@ describe('Note', () => {
 | 
			
		|||
			await api('admin/roles/unassign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			});
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			await api('admin/roles/delete', {
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -644,7 +646,7 @@ describe('Note', () => {
 | 
			
		|||
				sensitiveWords: [
 | 
			
		||||
					'test',
 | 
			
		||||
				],
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(sensitive.status, 204);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -663,7 +665,7 @@ describe('Note', () => {
 | 
			
		|||
				sensitiveWords: [
 | 
			
		||||
					'/Test/i',
 | 
			
		||||
				],
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(sensitive.status, 204);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -680,7 +682,7 @@ describe('Note', () => {
 | 
			
		|||
				sensitiveWords: [
 | 
			
		||||
					'Test hoge',
 | 
			
		||||
				],
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(sensitive.status, 204);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -697,7 +699,7 @@ describe('Note', () => {
 | 
			
		|||
				prohibitedWords: [
 | 
			
		||||
					'test',
 | 
			
		||||
				],
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(prohibited.status, 204);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -716,7 +718,7 @@ describe('Note', () => {
 | 
			
		|||
				prohibitedWords: [
 | 
			
		||||
					'/Test/i',
 | 
			
		||||
				],
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(prohibited.status, 204);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -733,7 +735,7 @@ describe('Note', () => {
 | 
			
		|||
				prohibitedWords: [
 | 
			
		||||
					'Test hoge',
 | 
			
		||||
				],
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(prohibited.status, 204);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -750,7 +752,7 @@ describe('Note', () => {
 | 
			
		|||
				prohibitedWords: [
 | 
			
		||||
					'test',
 | 
			
		||||
				],
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(prohibited.status, 204);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -785,7 +787,7 @@ describe('Note', () => {
 | 
			
		|||
						value: 0,
 | 
			
		||||
					},
 | 
			
		||||
				} as any,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -794,7 +796,7 @@ describe('Note', () => {
 | 
			
		|||
			const assign = await api('admin/roles/assign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(assign.status, 204);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -810,11 +812,11 @@ describe('Note', () => {
 | 
			
		|||
			await api('admin/roles/unassign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			});
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			await api('admin/roles/delete', {
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('ダイレクト投稿もエラーになる', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -839,7 +841,7 @@ describe('Note', () => {
 | 
			
		|||
						value: 0,
 | 
			
		||||
					},
 | 
			
		||||
				} as any,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -848,7 +850,7 @@ describe('Note', () => {
 | 
			
		|||
			const assign = await api('admin/roles/assign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(assign.status, 204);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -866,11 +868,11 @@ describe('Note', () => {
 | 
			
		|||
			await api('admin/roles/unassign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			});
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			await api('admin/roles/delete', {
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('ダイレクトの宛先とメンションが同じ場合は重複してカウントしない', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -895,7 +897,7 @@ describe('Note', () => {
 | 
			
		|||
						value: 1,
 | 
			
		||||
					},
 | 
			
		||||
				} as any,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -904,7 +906,7 @@ describe('Note', () => {
 | 
			
		|||
			const assign = await api('admin/roles/assign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(assign.status, 204);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -921,11 +923,11 @@ describe('Note', () => {
 | 
			
		|||
			await api('admin/roles/unassign', {
 | 
			
		||||
				userId: alice.id,
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			});
 | 
			
		||||
			}, root);
 | 
			
		||||
 | 
			
		||||
			await api('admin/roles/delete', {
 | 
			
		||||
				roleId: res.body.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			}, root);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -960,4 +962,61 @@ describe('Note', () => {
 | 
			
		|||
			assert.strictEqual(mainNote.repliesCount, 0);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('notes/translate', () => {
 | 
			
		||||
		describe('翻訳機能の利用が許可されていない場合', () => {
 | 
			
		||||
			let cannotTranslateRole: misskey.entities.Role;
 | 
			
		||||
 | 
			
		||||
			beforeAll(async () => {
 | 
			
		||||
				cannotTranslateRole = await role(root, {}, { canUseTranslator: false });
 | 
			
		||||
				await api('admin/roles/assign', { roleId: cannotTranslateRole.id, userId: alice.id }, root);
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			test('翻訳機能の利用が許可されていない場合翻訳できない', async () => {
 | 
			
		||||
				const aliceNote = await post(alice, { text: 'Hello' });
 | 
			
		||||
				const res = await api('notes/translate', {
 | 
			
		||||
					noteId: aliceNote.id,
 | 
			
		||||
					targetLang: 'ja',
 | 
			
		||||
				}, alice);
 | 
			
		||||
 | 
			
		||||
				assert.strictEqual(res.status, 400);
 | 
			
		||||
				assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			afterAll(async () => {
 | 
			
		||||
				await api('admin/roles/unassign', { roleId: cannotTranslateRole.id, userId: alice.id }, root);
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('存在しないノートは翻訳できない', async () => {
 | 
			
		||||
			const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
			assert.strictEqual(res.body.error.code, 'NO_SUCH_NOTE');
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('不可視なノートは翻訳できない', async () => {
 | 
			
		||||
			const aliceNote = await post(alice, { visibility: 'followers', text: 'Hello' });
 | 
			
		||||
			const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(bobTranslateAttempt.status, 400);
 | 
			
		||||
			assert.strictEqual(bobTranslateAttempt.body.error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE');
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => {
 | 
			
		||||
			const aliceNote = await post(alice, { text: null, poll: { choices: ['kinoko', 'takenoko'] } });
 | 
			
		||||
			const res = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 204);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('サーバーに DeepL 認証キーが登録されていない場合翻訳できない', async () => {
 | 
			
		||||
			const aliceNote = await post(alice, { text: 'Hello' });
 | 
			
		||||
			const res = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, alice);
 | 
			
		||||
 | 
			
		||||
			// NOTE: デフォルトでは登録されていないので落ちる
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
			assert.strictEqual(res.body.error.code, 'UNAVAILABLE');
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										
											BIN
										
									
								
								packages/backend/test/resources/kick_gaba7.m4a
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/backend/test/resources/kick_gaba7.m4a
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -14,6 +14,7 @@ import { afterAll, beforeAll, describe, test } from '@jest/globals';
 | 
			
		|||
import { GlobalModule } from '@/GlobalModule.js';
 | 
			
		||||
import { FileInfoService } from '@/core/FileInfoService.js';
 | 
			
		||||
//import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { LoggerService } from '@/core/LoggerService.js';
 | 
			
		||||
import type { TestingModule } from '@nestjs/testing';
 | 
			
		||||
import type { MockFunctionMetadata } from 'jest-mock';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +34,7 @@ describe('FileInfoService', () => {
 | 
			
		|||
				GlobalModule,
 | 
			
		||||
			],
 | 
			
		||||
			providers: [
 | 
			
		||||
				LoggerService,
 | 
			
		||||
				FileInfoService,
 | 
			
		||||
			],
 | 
			
		||||
		})
 | 
			
		||||
| 
						 | 
				
			
			@ -318,8 +320,26 @@ describe('FileInfoService', () => {
 | 
			
		|||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		/*
 | 
			
		||||
		 * video/webmとして検出されてしまう
 | 
			
		||||
		test('MPEG-4 AUDIO (M4A)', async () => {
 | 
			
		||||
			const path = `${resources}/kick_gaba7.m4a`;
 | 
			
		||||
			const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
 | 
			
		||||
			delete info.warnings;
 | 
			
		||||
			delete info.blurhash;
 | 
			
		||||
			delete info.sensitive;
 | 
			
		||||
			delete info.porn;
 | 
			
		||||
			delete info.width;
 | 
			
		||||
			delete info.height;
 | 
			
		||||
			delete info.orientation;
 | 
			
		||||
			assert.deepStrictEqual(info, {
 | 
			
		||||
				size: 9817,
 | 
			
		||||
				md5: '74c9279a4abe98789565f1dc1a541a42',
 | 
			
		||||
				type: {
 | 
			
		||||
					mime: 'audio/mp4',
 | 
			
		||||
					ext: 'm4a',
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('WEBM AUDIO', async () => {
 | 
			
		||||
			const path = `${resources}/kick_gaba7.webm`;
 | 
			
		||||
			const info = await fileInfoService.getFileInfo(path) as any;
 | 
			
		||||
| 
						 | 
				
			
			@ -332,13 +352,12 @@ describe('FileInfoService', () => {
 | 
			
		|||
			delete info.orientation;
 | 
			
		||||
			assert.deepStrictEqual(info, {
 | 
			
		||||
				size: 8879,
 | 
			
		||||
				md5: '3350083dec312419cfdc06c16413aca7',
 | 
			
		||||
				md5: '53bc1adcb6acbbda67ff9bd484896438',
 | 
			
		||||
				type: {
 | 
			
		||||
					mime: 'audio/webm',
 | 
			
		||||
					ext: 'webm',
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
		 */
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,7 @@
 | 
			
		|||
		"@twemoji/parser": "15.0.0",
 | 
			
		||||
		"@vitejs/plugin-vue": "5.0.4",
 | 
			
		||||
		"@vue/compiler-sfc": "3.4.21",
 | 
			
		||||
		"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.2",
 | 
			
		||||
		"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.4",
 | 
			
		||||
		"astring": "1.8.6",
 | 
			
		||||
		"broadcast-channel": "7.0.0",
 | 
			
		||||
		"buraha": "0.0.1",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
	v-else class="_button"
 | 
			
		||||
	:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
 | 
			
		||||
	:to="to ?? '#'"
 | 
			
		||||
	:behavior="linkBehavior"
 | 
			
		||||
	@mousedown="onMousedown"
 | 
			
		||||
>
 | 
			
		||||
	<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
 | 
			
		||||
| 
						 | 
				
			
			@ -43,6 +44,7 @@ const props = defineProps<{
 | 
			
		|||
	inline?: boolean;
 | 
			
		||||
	link?: boolean;
 | 
			
		||||
	to?: string;
 | 
			
		||||
	linkBehavior?: null | 'window' | 'browser';
 | 
			
		||||
	autofocus?: boolean;
 | 
			
		||||
	wait?: boolean;
 | 
			
		||||
	danger?: boolean;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -80,11 +80,9 @@ function copy() {
 | 
			
		|||
.codePlaceholderRoot {
 | 
			
		||||
	display: block;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	background: none;
 | 
			
		||||
	border: none;
 | 
			
		||||
	outline: none;
 | 
			
		||||
  font: inherit;
 | 
			
		||||
  color: inherit;
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,12 +47,12 @@ onMounted(() => {
 | 
			
		|||
	const width = rootEl.value!.offsetWidth;
 | 
			
		||||
	const height = rootEl.value!.offsetHeight;
 | 
			
		||||
 | 
			
		||||
	if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
 | 
			
		||||
		left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset;
 | 
			
		||||
	if (left + width - window.scrollX >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
 | 
			
		||||
		left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.scrollX;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (top + height - window.pageYOffset >= (window.innerHeight - SCROLLBAR_THICKNESS)) {
 | 
			
		||||
		top = (window.innerHeight - SCROLLBAR_THICKNESS) - height + window.pageYOffset;
 | 
			
		||||
	if (top + height - window.scrollY >= (window.innerHeight - SCROLLBAR_THICKNESS)) {
 | 
			
		||||
		top = (window.innerHeight - SCROLLBAR_THICKNESS) - height + window.scrollY;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (top < 0) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -161,7 +161,7 @@ function onKeydown(evt: KeyboardEvent) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function onInputKeydown(evt: KeyboardEvent) {
 | 
			
		||||
	if (evt.key === 'Enter') {
 | 
			
		||||
	if (evt.key === 'Enter' && okButtonDisabledReason.value === null) {
 | 
			
		||||
		evt.preventDefault();
 | 
			
		||||
		evt.stopPropagation();
 | 
			
		||||
		ok();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,11 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
 | 
			
		||||
<template>
 | 
			
		||||
<div
 | 
			
		||||
	ref="playerEl"
 | 
			
		||||
	v-hotkey="keymap"
 | 
			
		||||
	tabindex="0"
 | 
			
		||||
	:class="[
 | 
			
		||||
		$style.audioContainer,
 | 
			
		||||
		(audio.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive,
 | 
			
		||||
	]"
 | 
			
		||||
	@contextmenu.stop
 | 
			
		||||
	@keydown.stop
 | 
			
		||||
>
 | 
			
		||||
	<button v-if="hide" :class="$style.hidden" @click="hide = false">
 | 
			
		||||
		<div :class="$style.hiddenTextWrapper">
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +22,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
 | 
			
		||||
		</div>
 | 
			
		||||
	</button>
 | 
			
		||||
 | 
			
		||||
	<div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.nativeAudioContainer">
 | 
			
		||||
		<audio
 | 
			
		||||
			ref="audioEl"
 | 
			
		||||
			preload="metadata"
 | 
			
		||||
			controls
 | 
			
		||||
			:class="$style.nativeAudio"
 | 
			
		||||
			@keydown.prevent
 | 
			
		||||
		>
 | 
			
		||||
			<source :src="audio.url">
 | 
			
		||||
		</audio>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div v-else :class="$style.audioControls">
 | 
			
		||||
		<audio
 | 
			
		||||
			ref="audioEl"
 | 
			
		||||
| 
						 | 
				
			
			@ -75,6 +92,41 @@ const props = defineProps<{
 | 
			
		|||
	audio: Misskey.entities.DriveFile;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const keymap = {
 | 
			
		||||
	'up': () => {
 | 
			
		||||
		if (hasFocus() && audioEl.value) {
 | 
			
		||||
			volume.value = Math.min(volume.value + 0.1, 1);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	'down': () => {
 | 
			
		||||
		if (hasFocus() && audioEl.value) {
 | 
			
		||||
			volume.value = Math.max(volume.value - 0.1, 0);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	'left': () => {
 | 
			
		||||
		if (hasFocus() && audioEl.value) {
 | 
			
		||||
			audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	'right': () => {
 | 
			
		||||
		if (hasFocus() && audioEl.value) {
 | 
			
		||||
			audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	'space': () => {
 | 
			
		||||
		if (hasFocus()) {
 | 
			
		||||
			togglePlayPause();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// PlayerElもしくはその子要素にフォーカスがあるかどうか
 | 
			
		||||
function hasFocus() {
 | 
			
		||||
	if (!playerEl.value) return false;
 | 
			
		||||
	return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const playerEl = shallowRef<HTMLDivElement>();
 | 
			
		||||
const audioEl = shallowRef<HTMLAudioElement>();
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line vue/no-setup-props-destructure
 | 
			
		||||
| 
						 | 
				
			
			@ -88,6 +140,30 @@ function showMenu(ev: MouseEvent) {
 | 
			
		|||
 | 
			
		||||
	menu = [
 | 
			
		||||
		// TODO: 再生キューに追加
 | 
			
		||||
		{
 | 
			
		||||
			type: 'switch',
 | 
			
		||||
			text: i18n.ts._mediaControls.loop,
 | 
			
		||||
			icon: 'ti ti-repeat',
 | 
			
		||||
			ref: loop,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			type: 'radio',
 | 
			
		||||
			text: i18n.ts._mediaControls.playbackRate,
 | 
			
		||||
			icon: 'ti ti-clock-play',
 | 
			
		||||
			ref: speed,
 | 
			
		||||
			options: {
 | 
			
		||||
				'0.25x': 0.25,
 | 
			
		||||
				'0.5x': 0.5,
 | 
			
		||||
				'0.75x': 0.75,
 | 
			
		||||
				'1.0x': 1,
 | 
			
		||||
				'1.25x': 1.25,
 | 
			
		||||
				'1.5x': 1.5,
 | 
			
		||||
				'2.0x': 2,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			type: 'divider',
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			text: i18n.ts.hide,
 | 
			
		||||
			icon: 'ph-eye-closed ph-bold ph-lg',
 | 
			
		||||
| 
						 | 
				
			
			@ -150,6 +226,8 @@ const rangePercent = computed({
 | 
			
		|||
	},
 | 
			
		||||
});
 | 
			
		||||
const volume = ref(.25);
 | 
			
		||||
const speed = ref(1);
 | 
			
		||||
const loop = ref(false); // TODO: ドライブファイルのフラグに置き換える
 | 
			
		||||
const bufferedEnd = ref(0);
 | 
			
		||||
const bufferedDataRatio = computed(() => {
 | 
			
		||||
	if (!audioEl.value) return 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -179,6 +257,7 @@ function toggleMute() {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
let onceInit = false;
 | 
			
		||||
let mediaTickFrameId: number | null = null;
 | 
			
		||||
let stopAudioElWatch: () => void;
 | 
			
		||||
 | 
			
		||||
function init() {
 | 
			
		||||
| 
						 | 
				
			
			@ -198,8 +277,12 @@ function init() {
 | 
			
		|||
					}
 | 
			
		||||
 | 
			
		||||
					elapsedTimeMs.value = audioEl.value.currentTime * 1000;
 | 
			
		||||
 | 
			
		||||
					if (audioEl.value.loop !== loop.value) {
 | 
			
		||||
						loop.value = audioEl.value.loop;
 | 
			
		||||
					}
 | 
			
		||||
				window.requestAnimationFrame(updateMediaTick);
 | 
			
		||||
				}
 | 
			
		||||
				mediaTickFrameId = window.requestAnimationFrame(updateMediaTick);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			updateMediaTick();
 | 
			
		||||
| 
						 | 
				
			
			@ -237,6 +320,14 @@ watch(volume, (to) => {
 | 
			
		|||
	if (audioEl.value) audioEl.value.volume = to;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(speed, (to) => {
 | 
			
		||||
	if (audioEl.value) audioEl.value.playbackRate = to;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(loop, (to) => {
 | 
			
		||||
	if (audioEl.value) audioEl.value.loop = to;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	init();
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -255,6 +346,10 @@ onDeactivated(() => {
 | 
			
		|||
	hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore');
 | 
			
		||||
	stopAudioElWatch();
 | 
			
		||||
	onceInit = false;
 | 
			
		||||
	if (mediaTickFrameId) {
 | 
			
		||||
		window.cancelAnimationFrame(mediaTickFrameId);
 | 
			
		||||
		mediaTickFrameId = null;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -265,6 +360,10 @@ onDeactivated(() => {
 | 
			
		|||
	border: .5px solid var(--divider);
 | 
			
		||||
	border-radius: var(--radius);
 | 
			
		||||
	overflow: clip;
 | 
			
		||||
 | 
			
		||||
	&:focus {
 | 
			
		||||
		outline: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sensitive {
 | 
			
		||||
| 
						 | 
				
			
			@ -370,4 +469,15 @@ onDeactivated(() => {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nativeAudioContainer {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	padding: 6px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nativeAudio {
 | 
			
		||||
	display: block;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
<template>
 | 
			
		||||
<div
 | 
			
		||||
	ref="playerEl"
 | 
			
		||||
	v-hotkey="keymap"
 | 
			
		||||
	tabindex="0"
 | 
			
		||||
	:class="[
 | 
			
		||||
		$style.videoContainer,
 | 
			
		||||
		controlsShowing && $style.active,
 | 
			
		||||
| 
						 | 
				
			
			@ -14,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
	@mouseover="onMouseOver"
 | 
			
		||||
	@mouseleave="onMouseLeave"
 | 
			
		||||
	@contextmenu.stop
 | 
			
		||||
	@keydown.stop
 | 
			
		||||
>
 | 
			
		||||
	<button v-if="hide" :class="$style.hidden" @click="hide = false">
 | 
			
		||||
		<div :class="$style.hiddenTextWrapper">
 | 
			
		||||
| 
						 | 
				
			
			@ -22,7 +25,28 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
 | 
			
		||||
		</div>
 | 
			
		||||
	</button>
 | 
			
		||||
	<div v-else :class="$style.videoRoot" @click.self="togglePlayPause">
 | 
			
		||||
 | 
			
		||||
	<div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.videoRoot">
 | 
			
		||||
		<video
 | 
			
		||||
			ref="videoEl"
 | 
			
		||||
			:class="$style.video"
 | 
			
		||||
			:poster="video.thumbnailUrl ?? undefined"
 | 
			
		||||
			:title="video.comment ?? undefined"
 | 
			
		||||
			:alt="video.comment"
 | 
			
		||||
			preload="metadata"
 | 
			
		||||
			controls
 | 
			
		||||
			@keydown.prevent
 | 
			
		||||
		>
 | 
			
		||||
			<source :src="video.url">
 | 
			
		||||
		</video>
 | 
			
		||||
		<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
 | 
			
		||||
		<div :class="$style.indicators">
 | 
			
		||||
			<div v-if="video.comment" :class="$style.indicator">ALT</div>
 | 
			
		||||
			<div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div v-else :class="$style.videoRoot">
 | 
			
		||||
		<video
 | 
			
		||||
			ref="videoEl"
 | 
			
		||||
			:class="$style.video"
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +55,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			:alt="video.comment"
 | 
			
		||||
			preload="metadata"
 | 
			
		||||
			playsinline
 | 
			
		||||
			@keydown.prevent
 | 
			
		||||
			@click.self="togglePlayPause"
 | 
			
		||||
		>
 | 
			
		||||
			<source :src="video.url">
 | 
			
		||||
		</video>
 | 
			
		||||
| 
						 | 
				
			
			@ -103,6 +129,40 @@ const props = defineProps<{
 | 
			
		|||
	video: Misskey.entities.DriveFile;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const keymap = {
 | 
			
		||||
	'up': () => {
 | 
			
		||||
		if (hasFocus() && videoEl.value) {
 | 
			
		||||
			volume.value = Math.min(volume.value + 0.1, 1);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	'down': () => {
 | 
			
		||||
		if (hasFocus() && videoEl.value) {
 | 
			
		||||
			volume.value = Math.max(volume.value - 0.1, 0);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	'left': () => {
 | 
			
		||||
		if (hasFocus() && videoEl.value) {
 | 
			
		||||
			videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	'right': () => {
 | 
			
		||||
		if (hasFocus() && videoEl.value) {
 | 
			
		||||
			videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration);
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	'space': () => {
 | 
			
		||||
		if (hasFocus()) {
 | 
			
		||||
			togglePlayPause();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// PlayerElもしくはその子要素にフォーカスがあるかどうか
 | 
			
		||||
function hasFocus() {
 | 
			
		||||
	if (!playerEl.value) return false;
 | 
			
		||||
	return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line vue/no-setup-props-destructure
 | 
			
		||||
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -114,6 +174,35 @@ function showMenu(ev: MouseEvent) {
 | 
			
		|||
 | 
			
		||||
	menu = [
 | 
			
		||||
		// TODO: 再生キューに追加
 | 
			
		||||
		{
 | 
			
		||||
			type: 'switch',
 | 
			
		||||
			text: i18n.ts._mediaControls.loop,
 | 
			
		||||
			icon: 'ti ti-repeat',
 | 
			
		||||
			ref: loop,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			type: 'radio',
 | 
			
		||||
			text: i18n.ts._mediaControls.playbackRate,
 | 
			
		||||
			icon: 'ti ti-clock-play',
 | 
			
		||||
			ref: speed,
 | 
			
		||||
			options: {
 | 
			
		||||
				'0.25x': 0.25,
 | 
			
		||||
				'0.5x': 0.5,
 | 
			
		||||
				'0.75x': 0.75,
 | 
			
		||||
				'1.0x': 1,
 | 
			
		||||
				'1.25x': 1.25,
 | 
			
		||||
				'1.5x': 1.5,
 | 
			
		||||
				'2.0x': 2,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		...(document.pictureInPictureEnabled ? [{
 | 
			
		||||
			text: i18n.ts._mediaControls.pip,
 | 
			
		||||
			icon: 'ti ti-picture-in-picture',
 | 
			
		||||
			action: togglePictureInPicture,
 | 
			
		||||
		}] : []),
 | 
			
		||||
		{
 | 
			
		||||
			type: 'divider',
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			text: i18n.ts.hide,
 | 
			
		||||
			icon: 'ph-eye-closed ph-bold ph-lg',
 | 
			
		||||
| 
						 | 
				
			
			@ -189,6 +278,8 @@ const rangePercent = computed({
 | 
			
		|||
	},
 | 
			
		||||
});
 | 
			
		||||
const volume = ref(.25);
 | 
			
		||||
const speed = ref(1);
 | 
			
		||||
const loop = ref(false); // TODO: ドライブファイルのフラグに置き換える
 | 
			
		||||
const bufferedEnd = ref(0);
 | 
			
		||||
const bufferedDataRatio = computed(() => {
 | 
			
		||||
	if (!videoEl.value) return 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -246,6 +337,16 @@ function toggleFullscreen() {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function togglePictureInPicture() {
 | 
			
		||||
	if (videoEl.value) {
 | 
			
		||||
		if (document.pictureInPictureElement) {
 | 
			
		||||
			document.exitPictureInPicture();
 | 
			
		||||
		} else {
 | 
			
		||||
			videoEl.value.requestPictureInPicture();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleMute() {
 | 
			
		||||
	if (volume.value === 0) {
 | 
			
		||||
		volume.value = .25;
 | 
			
		||||
| 
						 | 
				
			
			@ -255,6 +356,7 @@ function toggleMute() {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
let onceInit = false;
 | 
			
		||||
let mediaTickFrameId: number | null = null;
 | 
			
		||||
let stopVideoElWatch: () => void;
 | 
			
		||||
 | 
			
		||||
function init() {
 | 
			
		||||
| 
						 | 
				
			
			@ -274,8 +376,12 @@ function init() {
 | 
			
		|||
					}
 | 
			
		||||
 | 
			
		||||
					elapsedTimeMs.value = videoEl.value.currentTime * 1000;
 | 
			
		||||
 | 
			
		||||
					if (videoEl.value.loop !== loop.value) {
 | 
			
		||||
						loop.value = videoEl.value.loop;
 | 
			
		||||
					}
 | 
			
		||||
				window.requestAnimationFrame(updateMediaTick);
 | 
			
		||||
				}
 | 
			
		||||
				mediaTickFrameId = window.requestAnimationFrame(updateMediaTick);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			updateMediaTick();
 | 
			
		||||
| 
						 | 
				
			
			@ -319,6 +425,14 @@ watch(volume, (to) => {
 | 
			
		|||
	if (videoEl.value) videoEl.value.volume = to;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(speed, (to) => {
 | 
			
		||||
	if (videoEl.value) videoEl.value.playbackRate = to;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(loop, (to) => {
 | 
			
		||||
	if (videoEl.value) videoEl.value.loop = to;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(hide, (to) => {
 | 
			
		||||
	if (to && isFullscreen.value) {
 | 
			
		||||
		document.exitFullscreen();
 | 
			
		||||
| 
						 | 
				
			
			@ -344,6 +458,10 @@ onDeactivated(() => {
 | 
			
		|||
	hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore');
 | 
			
		||||
	stopVideoElWatch();
 | 
			
		||||
	onceInit = false;
 | 
			
		||||
	if (mediaTickFrameId) {
 | 
			
		||||
		window.cancelAnimationFrame(mediaTickFrameId);
 | 
			
		||||
		mediaTickFrameId = null;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -352,6 +470,10 @@ onDeactivated(() => {
 | 
			
		|||
	container-type: inline-size;
 | 
			
		||||
	position: relative;
 | 
			
		||||
	overflow: clip;
 | 
			
		||||
 | 
			
		||||
	&:focus {
 | 
			
		||||
		outline: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sensitive {
 | 
			
		||||
| 
						 | 
				
			
			@ -415,7 +537,7 @@ onDeactivated(() => {
 | 
			
		|||
	font: inherit;
 | 
			
		||||
	color: inherit;
 | 
			
		||||
	cursor: pointer;
 | 
			
		||||
	padding: 120px 0;
 | 
			
		||||
	padding: 60px 0;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	justify-content: center;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,9 +42,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				</div>
 | 
			
		||||
			</button>
 | 
			
		||||
			<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
			
		||||
				<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
 | 
			
		||||
				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 | 
			
		||||
				<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
 | 
			
		||||
				<div :class="$style.item_content">
 | 
			
		||||
					<span :class="[$style.item_content_text, $style.switchText]">{{ item.text }}</span>
 | 
			
		||||
					<span :class="[$style.item_content_text, { [$style.switchText]: !item.icon }]">{{ item.text }}</span>
 | 
			
		||||
					<MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
 | 
			
		||||
				</div>
 | 
			
		||||
			</button>
 | 
			
		||||
			<button v-else-if="item.type === 'radio'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showRadioOptions(item, $event)" @click="!preferClick ? null : showRadioOptions(item, $event)">
 | 
			
		||||
				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
 | 
			
		||||
				<div :class="$style.item_content">
 | 
			
		||||
					<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
 | 
			
		||||
					<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</button>
 | 
			
		||||
			<button v-else-if="item.type === 'radioOption'" :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.radioActive]: item.active }]" @click="clicked(item.action, $event, false)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
			
		||||
				<div :class="$style.icon">
 | 
			
		||||
					<span :class="[$style.radio, { [$style.radioChecked]: item.active }]"></span>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div :class="$style.item_content">
 | 
			
		||||
					<span :class="$style.item_content_text">{{ item.text }}</span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</button>
 | 
			
		||||
			<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
 | 
			
		||||
| 
						 | 
				
			
			@ -77,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
 | 
			
		||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
 | 
			
		||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
 | 
			
		||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu.js';
 | 
			
		||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { isTouchUsing } from '@/scripts/touch.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -168,6 +185,31 @@ function onItemMouseLeave(item) {
 | 
			
		|||
	if (childCloseTimer) window.clearTimeout(childCloseTimer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
 | 
			
		||||
	const children: MenuItem[] = Object.keys(item.options).map<MenuRadioOption>(key => {
 | 
			
		||||
		const value = item.options[key];
 | 
			
		||||
		return {
 | 
			
		||||
			type: 'radioOption',
 | 
			
		||||
			text: key,
 | 
			
		||||
			action: () => {
 | 
			
		||||
				item.ref = value;
 | 
			
		||||
			},
 | 
			
		||||
			active: computed(() => item.ref === value),
 | 
			
		||||
		};
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (props.asDrawer) {
 | 
			
		||||
		os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
 | 
			
		||||
			emit('close');
 | 
			
		||||
		});
 | 
			
		||||
		emit('hide');
 | 
			
		||||
	} else {
 | 
			
		||||
		childTarget.value = (ev.currentTarget ?? ev.target) as HTMLElement;
 | 
			
		||||
		childMenu.value = children;
 | 
			
		||||
		childShowingItem.value = item;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function showChildren(item: MenuParent, ev: MouseEvent) {
 | 
			
		||||
	const children: MenuItem[] = await (async () => {
 | 
			
		||||
		if (childrenCache.has(item)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -196,8 +238,10 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function clicked(fn: MenuAction, ev: MouseEvent) {
 | 
			
		||||
function clicked(fn: MenuAction, ev: MouseEvent, doClose = true) {
 | 
			
		||||
	fn(ev);
 | 
			
		||||
 | 
			
		||||
	if (!doClose) return;
 | 
			
		||||
	close(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -350,6 +394,15 @@ onBeforeUnmount(() => {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.radioActive {
 | 
			
		||||
		color: var(--accent) !important;
 | 
			
		||||
		opacity: 1;
 | 
			
		||||
 | 
			
		||||
		&:before {
 | 
			
		||||
			background-color: var(--accentedBg) !important;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:not(:active):focus-visible {
 | 
			
		||||
		box-shadow: 0 0 0 2px var(--focus) inset;
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -417,11 +470,11 @@ onBeforeUnmount(() => {
 | 
			
		|||
 | 
			
		||||
.switchButton {
 | 
			
		||||
	margin-left: -2px;
 | 
			
		||||
	--height: 1.35em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.switchText {
 | 
			
		||||
	margin-left: 8px;
 | 
			
		||||
	margin-top: 2px;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -461,4 +514,32 @@ onBeforeUnmount(() => {
 | 
			
		|||
	margin: 8px 0;
 | 
			
		||||
	border-top: solid 0.5px var(--divider);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.radio {
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	position: relative;
 | 
			
		||||
	width: 1em;
 | 
			
		||||
	height: 1em;
 | 
			
		||||
	vertical-align: -.125em;
 | 
			
		||||
	border-radius: 50%;
 | 
			
		||||
	border: solid 2px var(--divider);
 | 
			
		||||
	background-color: var(--panel);
 | 
			
		||||
 | 
			
		||||
	&.radioChecked {
 | 
			
		||||
		border-color: var(--accent);
 | 
			
		||||
 | 
			
		||||
		&::after {
 | 
			
		||||
			content: "";
 | 
			
		||||
			display: block;
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			top: 50%;
 | 
			
		||||
			left: 50%;
 | 
			
		||||
			transform: translate(-50%, -50%);
 | 
			
		||||
			width: 50%;
 | 
			
		||||
			height: 50%;
 | 
			
		||||
			border-radius: 50%;
 | 
			
		||||
			background-color: var(--accent);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -175,8 +175,8 @@ const align = () => {
 | 
			
		|||
	let left;
 | 
			
		||||
	let top;
 | 
			
		||||
 | 
			
		||||
	const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
 | 
			
		||||
	const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);
 | 
			
		||||
	const x = srcRect.left + (fixed.value ? 0 : window.scrollX);
 | 
			
		||||
	const y = srcRect.top + (fixed.value ? 0 : window.scrollY);
 | 
			
		||||
 | 
			
		||||
	if (props.anchor.x === 'center') {
 | 
			
		||||
		left = x + (props.src.offsetWidth / 2) - (width / 2);
 | 
			
		||||
| 
						 | 
				
			
			@ -220,24 +220,24 @@ const align = () => {
 | 
			
		|||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		// 画面から横にはみ出る場合
 | 
			
		||||
		if (left + width - window.pageXOffset > (window.innerWidth - SCROLLBAR_THICKNESS)) {
 | 
			
		||||
			left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset - 1;
 | 
			
		||||
		if (left + width - window.scrollX > (window.innerWidth - SCROLLBAR_THICKNESS)) {
 | 
			
		||||
			left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.scrollX - 1;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const underSpace = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - (top - window.pageYOffset);
 | 
			
		||||
		const underSpace = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - (top - window.scrollY);
 | 
			
		||||
		const upperSpace = (srcRect.top - MARGIN);
 | 
			
		||||
 | 
			
		||||
		// 画面から縦にはみ出る場合
 | 
			
		||||
		if (top + height - window.pageYOffset > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
 | 
			
		||||
		if (top + height - window.scrollY > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
 | 
			
		||||
			if (props.noOverlap && props.anchor.x === 'center') {
 | 
			
		||||
				if (underSpace >= (upperSpace / 3)) {
 | 
			
		||||
					maxHeight.value = underSpace;
 | 
			
		||||
				} else {
 | 
			
		||||
					maxHeight.value = upperSpace;
 | 
			
		||||
					top = window.pageYOffset + ((upperSpace + MARGIN) - height);
 | 
			
		||||
					top = window.scrollY + ((upperSpace + MARGIN) - height);
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.pageYOffset - 1;
 | 
			
		||||
				top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.scrollY - 1;
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			maxHeight.value = underSpace;
 | 
			
		||||
| 
						 | 
				
			
			@ -255,15 +255,15 @@ const align = () => {
 | 
			
		|||
	let transformOriginX = 'center';
 | 
			
		||||
	let transformOriginY = 'center';
 | 
			
		||||
 | 
			
		||||
	if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.pageYOffset)) {
 | 
			
		||||
	if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.scrollY)) {
 | 
			
		||||
		transformOriginY = 'top';
 | 
			
		||||
	} else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.pageYOffset)) {
 | 
			
		||||
	} else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.scrollY)) {
 | 
			
		||||
		transformOriginY = 'bottom';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.pageXOffset)) {
 | 
			
		||||
	if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.scrollX)) {
 | 
			
		||||
		transformOriginX = 'left';
 | 
			
		||||
	} else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.pageXOffset)) {
 | 
			
		||||
	} else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.scrollX)) {
 | 
			
		||||
		transformOriginX = 'right';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -101,7 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			</div>
 | 
			
		||||
			<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
 | 
			
		||||
				<template #more>
 | 
			
		||||
					<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
 | 
			
		||||
					<MkA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</MkA>
 | 
			
		||||
				</template>
 | 
			
		||||
			</MkReactionsViewer>
 | 
			
		||||
			<footer :class="$style.footer">
 | 
			
		||||
| 
						 | 
				
			
			@ -275,6 +275,7 @@ if (noteViewInterruptors.length > 0) {
 | 
			
		|||
 | 
			
		||||
const isRenote = (
 | 
			
		||||
	note.value.renote != null &&
 | 
			
		||||
	note.value.reply == null &&
 | 
			
		||||
	note.value.text == null &&
 | 
			
		||||
	note.value.cw == null &&
 | 
			
		||||
	note.value.fileIds && note.value.fileIds.length === 0 &&
 | 
			
		||||
| 
						 | 
				
			
			@ -1254,10 +1255,9 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
 | 
			
		||||
.reactionOmitted {
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	height: 32px;
 | 
			
		||||
	margin: 2px;
 | 
			
		||||
	padding: 0 6px;
 | 
			
		||||
	margin-left: 8px;
 | 
			
		||||
	opacity: .8;
 | 
			
		||||
	font-size: 95%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clickToOpen {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<div :class="$style.noteContent">
 | 
			
		||||
			<p v-if="appearNote.cw != null" :class="$style.cw">
 | 
			
		||||
				<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
 | 
			
		||||
				<MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
 | 
			
		||||
				<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/>
 | 
			
		||||
			</p>
 | 
			
		||||
			<div v-show="appearNote.cw == null || showContent">
 | 
			
		||||
				<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
 | 
			
		||||
| 
						 | 
				
			
			@ -264,10 +264,13 @@ import MkButton from '@/components/MkButton.vue';
 | 
			
		|||
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
 | 
			
		||||
import { isEnabledUrlPreview } from '@/instance.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	note: Misskey.entities.Note;
 | 
			
		||||
	expandAllCws?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
	initialTab: string;
 | 
			
		||||
}>(), {
 | 
			
		||||
	initialTab: 'replies',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const inChannel = inject('inChannel', null);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -294,7 +297,9 @@ if (noteViewInterruptors.length > 0) {
 | 
			
		|||
 | 
			
		||||
const isRenote = (
 | 
			
		||||
	note.value.renote != null &&
 | 
			
		||||
	note.value.reply == null &&
 | 
			
		||||
	note.value.text == null &&
 | 
			
		||||
	note.value.cw == null &&
 | 
			
		||||
	note.value.fileIds && note.value.fileIds.length === 0 &&
 | 
			
		||||
	note.value.poll == null
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			@ -357,7 +362,7 @@ provide('react', (reaction: string) => {
 | 
			
		|||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const tab = ref('replies');
 | 
			
		||||
const tab = ref(props.initialTab);
 | 
			
		||||
const reactionTabType = ref<string | null>(null);
 | 
			
		||||
 | 
			
		||||
const renotesPagination = computed<Paging>(() => ({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -255,7 +255,13 @@ const maxTextLength = computed((): number => {
 | 
			
		|||
 | 
			
		||||
const canPost = computed((): boolean => {
 | 
			
		||||
	return !props.mock && !posting.value && !posted.value &&
 | 
			
		||||
		(1 <= textLength.value || 1 <= files.value.length || !!poll.value || !!props.renote) &&
 | 
			
		||||
		(
 | 
			
		||||
			1 <= textLength.value ||
 | 
			
		||||
			1 <= files.value.length ||
 | 
			
		||||
			poll.value != null ||
 | 
			
		||||
			props.renote != null ||
 | 
			
		||||
			(props.reply != null && quoteId.value != null)
 | 
			
		||||
		) &&
 | 
			
		||||
		(textLength.value <= maxTextLength.value) &&
 | 
			
		||||
		(!poll.value || poll.value.choices.length >= 2);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -100,6 +100,9 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.root {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-wrap: wrap;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	margin: 4px -2px 0 -2px;
 | 
			
		||||
	cursor: auto; /* not clickToOpen-able */
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,13 +41,15 @@ const toggle = () => {
 | 
			
		|||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.button {
 | 
			
		||||
	--height: 21px;
 | 
			
		||||
 | 
			
		||||
	position: relative;
 | 
			
		||||
	display: inline-flex;
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
	width: 32px;
 | 
			
		||||
	height: 23px;
 | 
			
		||||
	width: calc(var(--height) * 1.6);
 | 
			
		||||
	height: calc(var(--height) + 2px); // 枠線
 | 
			
		||||
	outline: none;
 | 
			
		||||
	background: var(--switchOffBg);
 | 
			
		||||
	background-clip: content-box;
 | 
			
		||||
| 
						 | 
				
			
			@ -69,9 +71,10 @@ const toggle = () => {
 | 
			
		|||
 | 
			
		||||
.knob {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
	top: 3px;
 | 
			
		||||
	width: 15px;
 | 
			
		||||
	height: 15px;
 | 
			
		||||
	width: calc(var(--height) - 6px);
 | 
			
		||||
	height: calc(var(--height) - 6px);
 | 
			
		||||
	border-radius: var(--radius-ellipse);
 | 
			
		||||
	transition: all 0.2s ease;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +85,7 @@ const toggle = () => {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.knobChecked {
 | 
			
		||||
	left: 12px;
 | 
			
		||||
	left: calc(calc(100% - var(--height)) + 3px);
 | 
			
		||||
	background: var(--switchOnFg);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,8 +33,8 @@ const left = ref(0);
 | 
			
		|||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	const rect = props.source.getBoundingClientRect();
 | 
			
		||||
	const x = Math.max((rect.left + (props.source.offsetWidth / 2)) - (300 / 2), 6) + window.pageXOffset;
 | 
			
		||||
	const y = rect.top + props.source.offsetHeight + window.pageYOffset;
 | 
			
		||||
	const x = Math.max((rect.left + (props.source.offsetWidth / 2)) - (300 / 2), 6) + window.scrollX;
 | 
			
		||||
	const y = rect.top + props.source.offsetHeight + window.scrollY;
 | 
			
		||||
 | 
			
		||||
	top.value = y;
 | 
			
		||||
	left.value = x;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -118,8 +118,8 @@ onMounted(() => {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	const rect = props.source.getBoundingClientRect();
 | 
			
		||||
	const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
 | 
			
		||||
	const y = rect.top + props.source.offsetHeight + window.pageYOffset;
 | 
			
		||||
	const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.scrollX;
 | 
			
		||||
	const y = rect.top + props.source.offsetHeight + window.scrollY;
 | 
			
		||||
 | 
			
		||||
	top.value = y;
 | 
			
		||||
	left.value = x;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,7 +47,7 @@ const invalid = Number.isNaN(_time);
 | 
			
		|||
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line vue/no-setup-props-destructure
 | 
			
		||||
const now = ref((props.origin ?? new Date()).getTime());
 | 
			
		||||
const now = ref(props.origin?.getTime() ?? Date.now());
 | 
			
		||||
const ago = computed(() => (now.value - _time) / 1000/*ms*/);
 | 
			
		||||
 | 
			
		||||
const relative = computed<string>(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -77,7 +77,7 @@ let tickId: number;
 | 
			
		|||
let currentInterval: number;
 | 
			
		||||
 | 
			
		||||
function tick() {
 | 
			
		||||
	now.value = (new Date()).getTime();
 | 
			
		||||
	now.value = Date.now();
 | 
			
		||||
	const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000;
 | 
			
		||||
 | 
			
		||||
	if (currentInterval !== nextInterval) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ import XText from './page.text.vue';
 | 
			
		|||
import XSection from './page.section.vue';
 | 
			
		||||
import XImage from './page.image.vue';
 | 
			
		||||
import XNote from './page.note.vue';
 | 
			
		||||
import XDynamic from './page.dynamic.vue';
 | 
			
		||||
 | 
			
		||||
function getComponent(type: string) {
 | 
			
		||||
	switch (type) {
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +22,20 @@ function getComponent(type: string) {
 | 
			
		|||
		case 'section': return XSection;
 | 
			
		||||
		case 'image': return XImage;
 | 
			
		||||
		case 'note': return XNote;
 | 
			
		||||
 | 
			
		||||
		// 動的ページの代替用ブロック
 | 
			
		||||
		case 'button':
 | 
			
		||||
		case 'if':
 | 
			
		||||
		case 'textarea':
 | 
			
		||||
		case 'post':
 | 
			
		||||
		case 'canvas':
 | 
			
		||||
		case 'numberInput':
 | 
			
		||||
		case 'textInput':
 | 
			
		||||
		case 'switch':
 | 
			
		||||
		case 'radioButton':
 | 
			
		||||
		case 'counter':
 | 
			
		||||
			return XDynamic;
 | 
			
		||||
 | 
			
		||||
		default: return null;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										43
									
								
								packages/frontend/src/components/page/page.dynamic.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								packages/frontend/src/components/page/page.dynamic.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<!-- 動的ページのブロックの代替。利用できないということを表示する -->
 | 
			
		||||
<template>
 | 
			
		||||
<div :class="$style.root">
 | 
			
		||||
	<div :class="$style.heading"><i class="ti ti-dice-5"></i> {{ i18n.ts._pages.blocks.dynamic }}</div>
 | 
			
		||||
	<I18n :src="i18n.ts._pages.blocks.dynamicDescription" tag="div" :class="$style.text">
 | 
			
		||||
		<template #play>
 | 
			
		||||
			<MkA to="/play" class="_link">Play</MkA>
 | 
			
		||||
		</template>
 | 
			
		||||
	</I18n>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	block: Misskey.entities.PageBlock,
 | 
			
		||||
	page: Misskey.entities.Page,
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	border: 1px solid var(--divider);
 | 
			
		||||
	border-radius: var(--radius);
 | 
			
		||||
	padding: var(--margin);
 | 
			
		||||
	text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.heading {
 | 
			
		||||
	font-weight: 700;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text {
 | 
			
		||||
	font-size: 90%;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
<template>
 | 
			
		||||
<div class="_gaps" :class="$style.textRoot">
 | 
			
		||||
	<Mfm :text="block.text ?? ''" :isNote="false"/>
 | 
			
		||||
	<div v-if="isEnabledUrlPreview">
 | 
			
		||||
	<div v-if="isEnabledUrlPreview" class="_gaps_s">
 | 
			
		||||
		<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,11 +21,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
						</div>
 | 
			
		||||
						<div v-if="defaultStore.state.noteDesign === 'misskey'" class="_margin _gaps_s">
 | 
			
		||||
							<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
 | 
			
		||||
							<MkNoteDetailed :key="note.id" v-model:note="note" :class="$style.note" :expandAllCws="expandAllCws"/>
 | 
			
		||||
							<MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div v-else-if="defaultStore.state.noteDesign === 'sharkey'" class="_margin _gaps_s">
 | 
			
		||||
							<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
 | 
			
		||||
							<SkNoteDetailed :key="note.id" v-model:note="note" :class="$style.note" :expandAllCws="expandAllCws"/>
 | 
			
		||||
							<SkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div v-if="clips && clips.length > 0" class="_margin">
 | 
			
		||||
							<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +71,7 @@ import { defaultStore } from '@/store.js';
 | 
			
		|||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	noteId: string;
 | 
			
		||||
	initialTab?: string;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const note = ref<null | Misskey.entities.Note>();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<div style="height: 100cqh; overflow: auto; text-align: center;">
 | 
			
		||||
					<MkSpacer :marginMin="20" :marginMax="28">
 | 
			
		||||
						<div class="_gaps">
 | 
			
		||||
							<MkInfo><MkLink url="https://misskey-hub.net/docs/for-users/stepped-guides/how-to-enable-2fa/" target="_blank">{{ i18n.ts._2fa.moreDetailedGuideHere }}</MkLink></MkInfo>
 | 
			
		||||
 | 
			
		||||
							<I18n :src="i18n.ts._2fa.step1" tag="div">
 | 
			
		||||
								<template #a>
 | 
			
		||||
									<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
 | 
			
		||||
| 
						 | 
				
			
			@ -33,8 +35,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
									<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a>
 | 
			
		||||
								</template>
 | 
			
		||||
							</I18n>
 | 
			
		||||
							<div>{{ i18n.ts._2fa.step2 }}<br>{{ i18n.ts._2fa.step2Click }}</div>
 | 
			
		||||
							<a :href="twoFactorData.url"><img :class="$style.qr" :src="twoFactorData.qr"></a>
 | 
			
		||||
							<div>{{ i18n.ts._2fa.step2 }}</div>
 | 
			
		||||
							<div>
 | 
			
		||||
								<a :class="$style.qrRoot" :href="twoFactorData.url"><img :class="$style.qr" :src="twoFactorData.qr"></a>
 | 
			
		||||
								<!-- QRコード側にマージンが入っているので直下でOK -->
 | 
			
		||||
								<div><MkButton inline rounded link :to="twoFactorData.url" :linkBehavior="'browser'">{{ i18n.ts.launchApp }}</MkButton></div>
 | 
			
		||||
							</div>
 | 
			
		||||
							<MkKeyValue :copy="twoFactorData.url">
 | 
			
		||||
								<template #key>{{ i18n.ts._2fa.step2Uri }}</template>
 | 
			
		||||
								<template #value>{{ twoFactorData.url }}</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -109,6 +115,7 @@ import { i18n } from '@/i18n.js';
 | 
			
		|||
import * as os from '@/os.js';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import MkInfo from '@/components/MkInfo.vue';
 | 
			
		||||
import MkLink from '@/components/MkLink.vue';
 | 
			
		||||
import { confetti } from '@/scripts/confetti.js';
 | 
			
		||||
import { signinRequired } from '@/account.js';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -177,8 +184,14 @@ function allDone() {
 | 
			
		|||
	transform: translateX(-50px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.qr {
 | 
			
		||||
.qrRoot {
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: 0 auto;
 | 
			
		||||
	width: 200px;
 | 
			
		||||
	max-width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.qr {
 | 
			
		||||
	width: 100%;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<MkButton v-else danger @click="unregisterTOTP">{{ i18n.ts.unregister }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<MkButton v-else-if="!$i.twoFactorEnabled" primary gradate @click="registerTOTP">{{ i18n.ts._2fa.registerTOTP }}</MkButton>
 | 
			
		||||
			<div v-else-if="!$i.twoFactorEnabled" class="_gaps_s">
 | 
			
		||||
				<MkButton primary gradate @click="registerTOTP">{{ i18n.ts._2fa.registerTOTP }}</MkButton>
 | 
			
		||||
				<MkLink url="https://misskey-hub.net/docs/for-users/stepped-guides/how-to-enable-2fa/" target="_blank"><i class="ti ti-help-circle"></i> {{ i18n.ts.learnMore }}</MkLink>
 | 
			
		||||
			</div>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
 | 
			
		||||
		<MkFolder>
 | 
			
		||||
| 
						 | 
				
			
			@ -79,6 +82,7 @@ import MkInfo from '@/components/MkInfo.vue';
 | 
			
		|||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
import FormSection from '@/components/form/section.vue';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import MkLink from '@/components/MkLink.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { signinRequired, updateAccount } from '@/account.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -148,6 +148,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
 | 
			
		||||
				<MkSwitch v-model="oneko">{{ i18n.ts.oneko }}</MkSwitch>
 | 
			
		||||
				<MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch>
 | 
			
		||||
				<MkSwitch v-model="useNativeUIForVideoAudioPlayer">{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</MkSwitch>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div>
 | 
			
		||||
				<MkRadios v-model="emojiStyle">
 | 
			
		||||
| 
						 | 
				
			
			@ -364,6 +365,7 @@ const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enabl
 | 
			
		|||
const showVisibilitySelectorOnBoost = computed(defaultStore.makeGetterSetter('showVisibilitySelectorOnBoost'));
 | 
			
		||||
const visibilityOnBoost = computed(defaultStore.makeGetterSetter('visibilityOnBoost'));
 | 
			
		||||
const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
 | 
			
		||||
const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer'));
 | 
			
		||||
 | 
			
		||||
watch(lang, () => {
 | 
			
		||||
	miLocalStorage.setItem('lang', lang.value as string);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,7 +35,7 @@ const routes: RouteDef[] = [{
 | 
			
		|||
	component: page(() => import('@/pages/user/index.vue')),
 | 
			
		||||
}, {
 | 
			
		||||
	name: 'note',
 | 
			
		||||
	path: '/notes/:noteId',
 | 
			
		||||
	path: '/notes/:noteId/:initialTab?',
 | 
			
		||||
	component: page(() => import('@/pages/note.vue')),
 | 
			
		||||
}, {
 | 
			
		||||
	name: 'list',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,7 @@ export default (input: string): string[] => {
 | 
			
		|||
export const aliases = {
 | 
			
		||||
	'esc': 'Escape',
 | 
			
		||||
	'enter': ['Enter', 'NumpadEnter'],
 | 
			
		||||
	'space': [' ', 'Spacebar'],
 | 
			
		||||
	'up': 'ArrowUp',
 | 
			
		||||
	'down': 'ArrowDown',
 | 
			
		||||
	'left': 'ArrowLeft',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,8 +26,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
 | 
			
		|||
		let top: number;
 | 
			
		||||
 | 
			
		||||
		if (props.anchorElement) {
 | 
			
		||||
			left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
 | 
			
		||||
			top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin;
 | 
			
		||||
			left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2);
 | 
			
		||||
			top = (rect.top + window.scrollY - contentHeight) - props.innerMargin;
 | 
			
		||||
		} else {
 | 
			
		||||
			left = props.x;
 | 
			
		||||
			top = (props.y - contentHeight) - props.innerMargin;
 | 
			
		||||
| 
						 | 
				
			
			@ -35,8 +35,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
 | 
			
		|||
 | 
			
		||||
		left -= (el.offsetWidth / 2);
 | 
			
		||||
 | 
			
		||||
		if (left + contentWidth - window.pageXOffset > window.innerWidth) {
 | 
			
		||||
			left = window.innerWidth - contentWidth + window.pageXOffset - 1;
 | 
			
		||||
		if (left + contentWidth - window.scrollX > window.innerWidth) {
 | 
			
		||||
			left = window.innerWidth - contentWidth + window.scrollX - 1;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return [left, top];
 | 
			
		||||
| 
						 | 
				
			
			@ -47,8 +47,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
 | 
			
		|||
		let top: number;
 | 
			
		||||
 | 
			
		||||
		if (props.anchorElement) {
 | 
			
		||||
			left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
 | 
			
		||||
			top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin;
 | 
			
		||||
			left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2);
 | 
			
		||||
			top = (rect.top + window.scrollY + props.anchorElement.offsetHeight) + props.innerMargin;
 | 
			
		||||
		} else {
 | 
			
		||||
			left = props.x;
 | 
			
		||||
			top = (props.y) + props.innerMargin;
 | 
			
		||||
| 
						 | 
				
			
			@ -56,8 +56,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
 | 
			
		|||
 | 
			
		||||
		left -= (el.offsetWidth / 2);
 | 
			
		||||
 | 
			
		||||
		if (left + contentWidth - window.pageXOffset > window.innerWidth) {
 | 
			
		||||
			left = window.innerWidth - contentWidth + window.pageXOffset - 1;
 | 
			
		||||
		if (left + contentWidth - window.scrollX > window.innerWidth) {
 | 
			
		||||
			left = window.innerWidth - contentWidth + window.scrollX - 1;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return [left, top];
 | 
			
		||||
| 
						 | 
				
			
			@ -68,8 +68,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
 | 
			
		|||
		let top: number;
 | 
			
		||||
 | 
			
		||||
		if (props.anchorElement) {
 | 
			
		||||
			left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin;
 | 
			
		||||
			top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
 | 
			
		||||
			left = (rect.left + window.scrollX - contentWidth) - props.innerMargin;
 | 
			
		||||
			top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2);
 | 
			
		||||
		} else {
 | 
			
		||||
			left = (props.x - contentWidth) - props.innerMargin;
 | 
			
		||||
			top = props.y;
 | 
			
		||||
| 
						 | 
				
			
			@ -77,8 +77,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
 | 
			
		|||
 | 
			
		||||
		top -= (el.offsetHeight / 2);
 | 
			
		||||
 | 
			
		||||
		if (top + contentHeight - window.pageYOffset > window.innerHeight) {
 | 
			
		||||
			top = window.innerHeight - contentHeight + window.pageYOffset - 1;
 | 
			
		||||
		if (top + contentHeight - window.scrollY > window.innerHeight) {
 | 
			
		||||
			top = window.innerHeight - contentHeight + window.scrollY - 1;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return [left, top];
 | 
			
		||||
| 
						 | 
				
			
			@ -89,15 +89,15 @@ export function calcPopupPosition(el: HTMLElement, props: {
 | 
			
		|||
		let top: number;
 | 
			
		||||
 | 
			
		||||
		if (props.anchorElement) {
 | 
			
		||||
			left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin;
 | 
			
		||||
			left = (rect.left + props.anchorElement.offsetWidth + window.scrollX) + props.innerMargin;
 | 
			
		||||
 | 
			
		||||
			if (props.align === 'top') {
 | 
			
		||||
				top = rect.top + window.pageYOffset;
 | 
			
		||||
				top = rect.top + window.scrollY;
 | 
			
		||||
				if (props.alignOffset != null) top += props.alignOffset;
 | 
			
		||||
			} else if (props.align === 'bottom') {
 | 
			
		||||
				// TODO
 | 
			
		||||
			} else { // center
 | 
			
		||||
				top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
 | 
			
		||||
				top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2);
 | 
			
		||||
				top -= (el.offsetHeight / 2);
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -106,8 +106,8 @@ export function calcPopupPosition(el: HTMLElement, props: {
 | 
			
		|||
			top -= (el.offsetHeight / 2);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (top + contentHeight - window.pageYOffset > window.innerHeight) {
 | 
			
		||||
			top = window.innerHeight - contentHeight + window.pageYOffset - 1;
 | 
			
		||||
		if (top + contentHeight - window.scrollY > window.innerHeight) {
 | 
			
		||||
			top = window.innerHeight - contentHeight + window.scrollY - 1;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return [left, top];
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +123,7 @@ export function calcPopupPosition(el: HTMLElement, props: {
 | 
			
		|||
				const [left, top] = calcPosWhenTop();
 | 
			
		||||
 | 
			
		||||
				// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
 | 
			
		||||
				if (top - window.pageYOffset < 0) {
 | 
			
		||||
				if (top - window.scrollY < 0) {
 | 
			
		||||
					const [left, top] = calcPosWhenBottom();
 | 
			
		||||
					return { left, top, transformOrigin: 'center top' };
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			@ -141,7 +141,7 @@ export function calcPopupPosition(el: HTMLElement, props: {
 | 
			
		|||
				const [left, top] = calcPosWhenLeft();
 | 
			
		||||
 | 
			
		||||
				// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
 | 
			
		||||
				if (left - window.pageXOffset < 0) {
 | 
			
		||||
				if (left - window.scrollX < 0) {
 | 
			
		||||
					const [left, top] = calcPosWhenRight();
 | 
			
		||||
					return { left, top, transformOrigin: 'left center' };
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,11 +53,11 @@ export function useChartTooltip(opts: { position: 'top' | 'middle' } = { positio
 | 
			
		|||
		const rect = context.chart.canvas.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
		tooltipShowing.value = true;
 | 
			
		||||
		tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX;
 | 
			
		||||
		tooltipX.value = rect.left + window.scrollX + context.tooltip.caretX;
 | 
			
		||||
		if (opts.position === 'top') {
 | 
			
		||||
			tooltipY.value = rect.top + window.pageYOffset;
 | 
			
		||||
			tooltipY.value = rect.top + window.scrollY;
 | 
			
		||||
		} else if (opts.position === 'middle') {
 | 
			
		||||
			tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY;
 | 
			
		||||
			tooltipY.value = rect.top + window.scrollY + context.tooltip.caretY;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -495,6 +495,10 @@ export const defaultStore = markRaw(new Storage('base', {
 | 
			
		|||
		where: 'device',
 | 
			
		||||
		default: true,
 | 
			
		||||
	},
 | 
			
		||||
	useNativeUIForVideoAudioPlayer: {
 | 
			
		||||
		where: 'device',
 | 
			
		||||
		default: false,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	sound_masterVolume: {
 | 
			
		||||
		where: 'device',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,8 @@
 | 
			
		|||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { ComputedRef, Ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
interface MenuRadioOptionsDef extends Record<string, any> { }
 | 
			
		||||
 | 
			
		||||
export type MenuAction = (ev: MouseEvent) => void;
 | 
			
		||||
 | 
			
		||||
export type MenuDivider = { type: 'divider' };
 | 
			
		||||
| 
						 | 
				
			
			@ -14,13 +16,15 @@ export type MenuLabel = { type: 'label', text: string };
 | 
			
		|||
export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
 | 
			
		||||
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean };
 | 
			
		||||
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
 | 
			
		||||
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean | Ref<boolean> };
 | 
			
		||||
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, icon?: string, disabled?: boolean | Ref<boolean> };
 | 
			
		||||
export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
 | 
			
		||||
export type MenuRadio = { type: 'radio', text: string, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
 | 
			
		||||
export type MenuRadioOption = { type: 'radioOption', text: string, action: MenuAction; active?: boolean | ComputedRef<boolean> };
 | 
			
		||||
export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
 | 
			
		||||
 | 
			
		||||
export type MenuPending = { type: 'pending' };
 | 
			
		||||
 | 
			
		||||
type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent;
 | 
			
		||||
type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuParent;
 | 
			
		||||
type OuterPromiseMenuItem = Promise<MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent>;
 | 
			
		||||
export type MenuItem = OuterMenuItem | OuterPromiseMenuItem;
 | 
			
		||||
export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent;
 | 
			
		||||
export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuRadio | MenuRadioOption | MenuParent;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
<MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings">
 | 
			
		||||
	<template #icon><i class="ph-cake ph-bold ph-lg"></i></template>
 | 
			
		||||
	<template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template>
 | 
			
		||||
	<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="actualFetch()"><i class="ti ti-refresh"></i></button></template>
 | 
			
		||||
 | 
			
		||||
	<div :class="$style.bdayFRoot">
 | 
			
		||||
		<MkLoading v-if="fetching"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +54,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 | 
			
		|||
	emit,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const users = ref<Misskey.entities.FollowingFolloweePopulated[]>([]);
 | 
			
		||||
const users = ref<Misskey.Endpoints['users/following']['res']>([]);
 | 
			
		||||
const fetching = ref(true);
 | 
			
		||||
let lastFetchedAt = '1970-01-01';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -70,19 +71,35 @@ const fetch = () => {
 | 
			
		|||
	now.setHours(0, 0, 0, 0);
 | 
			
		||||
 | 
			
		||||
	if (now > lfAtD) {
 | 
			
		||||
		misskeyApi('users/following', {
 | 
			
		||||
			limit: 18,
 | 
			
		||||
			birthday: now.toISOString(),
 | 
			
		||||
			userId: $i.id,
 | 
			
		||||
		}).then(res => {
 | 
			
		||||
			users.value = res;
 | 
			
		||||
			fetching.value = false;
 | 
			
		||||
		});
 | 
			
		||||
		actualFetch();
 | 
			
		||||
 | 
			
		||||
		lastFetchedAt = now.toISOString();
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function actualFetch() {
 | 
			
		||||
	if ($i == null) {
 | 
			
		||||
		users.value = [];
 | 
			
		||||
		fetching.value = false;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const now = new Date();
 | 
			
		||||
	now.setHours(0, 0, 0, 0);
 | 
			
		||||
	fetching.value = true;
 | 
			
		||||
	misskeyApi('users/following', {
 | 
			
		||||
		limit: 18,
 | 
			
		||||
		birthday: `${now.getFullYear().toString().padStart(4, '0')}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`,
 | 
			
		||||
		userId: $i.id,
 | 
			
		||||
	}).then(res => {
 | 
			
		||||
		users.value = res;
 | 
			
		||||
		window.setTimeout(() => {
 | 
			
		||||
			// 早すぎるとチカチカする
 | 
			
		||||
			fetching.value = false;
 | 
			
		||||
		}, 100);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
useInterval(fetch, 1000 * 60, {
 | 
			
		||||
	immediate: true,
 | 
			
		||||
	afterMounted: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -68,9 +68,9 @@ watch(showColon, (v) => {
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
const tick = () => {
 | 
			
		||||
	const now = new Date();
 | 
			
		||||
	ss.value = Math.floor(now.getTime() / 1000).toString();
 | 
			
		||||
	ms.value = Math.floor(now.getTime() % 1000 / 10).toString().padStart(2, '0');
 | 
			
		||||
	const now = Date.now();
 | 
			
		||||
	ss.value = Math.floor(now / 1000).toString();
 | 
			
		||||
	ms.value = Math.floor(now % 1000 / 10).toString().padStart(2, '0');
 | 
			
		||||
	if (ss.value !== prevSec) showColon.value = true;
 | 
			
		||||
	prevSec = ss.value;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,3 +5,4 @@ node_modules
 | 
			
		|||
/jest.config.ts
 | 
			
		||||
/test
 | 
			
		||||
/test-d
 | 
			
		||||
build.js
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,31 +1,105 @@
 | 
			
		|||
import * as esbuild from "esbuild";
 | 
			
		||||
import { build } from "esbuild";
 | 
			
		||||
import { globSync } from "glob";
 | 
			
		||||
import { execa } from "execa";
 | 
			
		||||
import fs from "node:fs";
 | 
			
		||||
import { fileURLToPath } from "node:url";
 | 
			
		||||
import { dirname } from "node:path";
 | 
			
		||||
 | 
			
		||||
const _filename = fileURLToPath(import.meta.url);
 | 
			
		||||
const _dirname = dirname(_filename);
 | 
			
		||||
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
 | 
			
		||||
 | 
			
		||||
const entryPoints = globSync("./src/**/**.{ts,tsx}");
 | 
			
		||||
 | 
			
		||||
/** @type {import('esbuild').BuildOptions} */
 | 
			
		||||
const options = {
 | 
			
		||||
	entryPoints,
 | 
			
		||||
  minify: true,
 | 
			
		||||
  outdir: "./built/esm",
 | 
			
		||||
	minify: process.env.NODE_ENV === 'production',
 | 
			
		||||
	outdir: "./built",
 | 
			
		||||
	target: "es2022",
 | 
			
		||||
	platform: "browser",
 | 
			
		||||
	format: "esm",
 | 
			
		||||
	sourcemap: 'linked',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if (process.env.WATCH === "true") {
 | 
			
		||||
  options.watch = {
 | 
			
		||||
    onRebuild(error, result) {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        console.error("watch build failed:", error);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log("watch build succeeded:", result);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
// built配下をすべて削除する
 | 
			
		||||
fs.rmSync('./built', { recursive: true, force: true });
 | 
			
		||||
 | 
			
		||||
if (process.argv.map(arg => arg.toLowerCase()).includes("--watch")) {
 | 
			
		||||
	await watchSrc();
 | 
			
		||||
} else {
 | 
			
		||||
	await buildSrc();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build(options).catch((err) => {
 | 
			
		||||
async function buildSrc() {
 | 
			
		||||
	console.log(`[${_package.name}] start building...`);
 | 
			
		||||
 | 
			
		||||
	await build(options)
 | 
			
		||||
		.then(it => {
 | 
			
		||||
			console.log(`[${_package.name}] build succeeded.`);
 | 
			
		||||
		})
 | 
			
		||||
		.catch((err) => {
 | 
			
		||||
			process.stderr.write(err.stderr);
 | 
			
		||||
			process.exit(1);
 | 
			
		||||
});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	if (process.env.NODE_ENV === 'production') {
 | 
			
		||||
		console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`);
 | 
			
		||||
	} else {
 | 
			
		||||
		await buildDts();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	console.log(`[${_package.name}] finish building.`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildDts() {
 | 
			
		||||
	return execa(
 | 
			
		||||
		'tsc',
 | 
			
		||||
		[
 | 
			
		||||
			'--project', 'tsconfig.json',
 | 
			
		||||
			'--outDir', 'built',
 | 
			
		||||
			'--declaration', 'true',
 | 
			
		||||
			'--emitDeclarationOnly', 'true',
 | 
			
		||||
		],
 | 
			
		||||
		{
 | 
			
		||||
			stdout: process.stdout,
 | 
			
		||||
			stderr: process.stderr,
 | 
			
		||||
		}
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function watchSrc() {
 | 
			
		||||
	const plugins = [{
 | 
			
		||||
		name: 'gen-dts',
 | 
			
		||||
		setup(build) {
 | 
			
		||||
			build.onStart(() => {
 | 
			
		||||
				console.log(`[${_package.name}] detect changed...`);
 | 
			
		||||
			});
 | 
			
		||||
			build.onEnd(async result => {
 | 
			
		||||
				if (result.errors.length > 0) {
 | 
			
		||||
					console.error(`[${_package.name}] watch build failed:`, result);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
				await buildDts();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
	}];
 | 
			
		||||
 | 
			
		||||
	console.log(`[${_package.name}] start watching...`)
 | 
			
		||||
 | 
			
		||||
	const context = await esbuild.context({ ...options, plugins });
 | 
			
		||||
	await context.watch();
 | 
			
		||||
 | 
			
		||||
	await new Promise((resolve, reject) => {
 | 
			
		||||
		process.on('SIGHUP', resolve);
 | 
			
		||||
		process.on('SIGINT', resolve);
 | 
			
		||||
		process.on('SIGTERM', resolve);
 | 
			
		||||
		process.on('SIGKILL', resolve);
 | 
			
		||||
		process.on('uncaughtException', reject);
 | 
			
		||||
		process.on('exit', resolve);
 | 
			
		||||
	}).finally(async () => {
 | 
			
		||||
		await context.dispose();
 | 
			
		||||
		console.log(`[${_package.name}] finish watching.`);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,24 +2,21 @@
 | 
			
		|||
	"type": "module",
 | 
			
		||||
	"name": "misskey-bubble-game",
 | 
			
		||||
	"version": "0.0.1",
 | 
			
		||||
	"types": "./built/dts/index.d.ts",
 | 
			
		||||
	"main": "./built/index.js",
 | 
			
		||||
	"types": "./built/index.d.ts",
 | 
			
		||||
	"exports": {
 | 
			
		||||
		".": {
 | 
			
		||||
			"import": "./built/esm/index.js",
 | 
			
		||||
			"types": "./built/dts/index.d.ts"
 | 
			
		||||
			"import": "./built/index.js",
 | 
			
		||||
			"types": "./built/index.d.ts"
 | 
			
		||||
		},
 | 
			
		||||
		"./*": {
 | 
			
		||||
			"import": "./built/esm/*",
 | 
			
		||||
			"types": "./built/dts/*"
 | 
			
		||||
			"import": "./built/*",
 | 
			
		||||
			"types": "./built/*"
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"build": "node ./build.js",
 | 
			
		||||
		"build:tsc": "npm run tsc",
 | 
			
		||||
		"tsc": "npm run tsc-esm && npm run tsc-dts",
 | 
			
		||||
		"tsc-esm": "tsc --outDir built/esm",
 | 
			
		||||
		"tsc-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
 | 
			
		||||
		"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build:tsc\"",
 | 
			
		||||
		"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
 | 
			
		||||
		"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
 | 
			
		||||
		"typecheck": "tsc --noEmit",
 | 
			
		||||
		"lint": "pnpm typecheck && pnpm eslint"
 | 
			
		||||
| 
						 | 
				
			
			@ -27,21 +24,22 @@
 | 
			
		|||
	"devDependencies": {
 | 
			
		||||
		"@misskey-dev/eslint-plugin": "1.0.0",
 | 
			
		||||
		"@types/matter-js": "0.19.6",
 | 
			
		||||
		"@types/node": "20.11.5",
 | 
			
		||||
		"@types/seedrandom": "3.0.8",
 | 
			
		||||
		"@types/node": "20.11.5",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "7.1.0",
 | 
			
		||||
		"@typescript-eslint/parser": "7.1.0",
 | 
			
		||||
		"eslint": "8.57.0",
 | 
			
		||||
		"nodemon": "3.0.2",
 | 
			
		||||
		"typescript": "5.3.3"
 | 
			
		||||
		"execa": "8.0.1",
 | 
			
		||||
		"typescript": "5.3.3",
 | 
			
		||||
		"esbuild": "0.19.11",
 | 
			
		||||
		"glob": "10.3.10"
 | 
			
		||||
	},
 | 
			
		||||
	"files": [
 | 
			
		||||
		"built"
 | 
			
		||||
	],
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"esbuild": "0.19.11",
 | 
			
		||||
		"eventemitter3": "5.0.1",
 | 
			
		||||
		"glob": "^10.3.10",
 | 
			
		||||
		"matter-js": "0.19.0",
 | 
			
		||||
		"seedrandom": "3.0.5"
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,5 +6,9 @@
 | 
			
		|||
import { DropAndFusionGame, Mono } from './game.js';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	DropAndFusionGame, Mono,
 | 
			
		||||
	DropAndFusionGame,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type {
 | 
			
		||||
	Mono,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
		"moduleResolution": "nodenext",
 | 
			
		||||
		"declaration": true,
 | 
			
		||||
		"declarationMap": true,
 | 
			
		||||
		"sourceMap": true,
 | 
			
		||||
		"sourceMap": false,
 | 
			
		||||
		"outDir": "./built/",
 | 
			
		||||
		"removeComments": true,
 | 
			
		||||
		"strict": true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,3 +5,4 @@ node_modules
 | 
			
		|||
/jest.config.ts
 | 
			
		||||
/test
 | 
			
		||||
/test-d
 | 
			
		||||
build.js
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,7 +45,7 @@
 | 
			
		|||
   *
 | 
			
		||||
   * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
 | 
			
		||||
   */
 | 
			
		||||
  "mainEntryPointFilePath": "<projectFolder>/built/dts/index.d.ts",
 | 
			
		||||
  "mainEntryPointFilePath": "<projectFolder>/built/index.d.ts",
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A list of NPM package names whose exports should be treated as part of this package.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										105
									
								
								packages/misskey-js/build.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								packages/misskey-js/build.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,105 @@
 | 
			
		|||
import * as esbuild from "esbuild";
 | 
			
		||||
import { build } from "esbuild";
 | 
			
		||||
import { globSync } from "glob";
 | 
			
		||||
import { execa } from "execa";
 | 
			
		||||
import fs from "node:fs";
 | 
			
		||||
import { fileURLToPath } from "node:url";
 | 
			
		||||
import { dirname } from "node:path";
 | 
			
		||||
 | 
			
		||||
const _filename = fileURLToPath(import.meta.url);
 | 
			
		||||
const _dirname = dirname(_filename);
 | 
			
		||||
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
 | 
			
		||||
 | 
			
		||||
const entryPoints = globSync("./src/**/**.{ts,tsx}");
 | 
			
		||||
 | 
			
		||||
/** @type {import('esbuild').BuildOptions} */
 | 
			
		||||
const options = {
 | 
			
		||||
	entryPoints,
 | 
			
		||||
	minify: process.env.NODE_ENV === 'production',
 | 
			
		||||
	outdir: "./built",
 | 
			
		||||
	target: "es2022",
 | 
			
		||||
	platform: "browser",
 | 
			
		||||
	format: "esm",
 | 
			
		||||
	sourcemap: 'linked',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// built配下をすべて削除する
 | 
			
		||||
fs.rmSync('./built', { recursive: true, force: true });
 | 
			
		||||
 | 
			
		||||
if (process.argv.map(arg => arg.toLowerCase()).includes("--watch")) {
 | 
			
		||||
	await watchSrc();
 | 
			
		||||
} else {
 | 
			
		||||
	await buildSrc();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function buildSrc() {
 | 
			
		||||
	console.log(`[${_package.name}] start building...`);
 | 
			
		||||
 | 
			
		||||
	await build(options)
 | 
			
		||||
		.then(it => {
 | 
			
		||||
			console.log(`[${_package.name}] build succeeded.`);
 | 
			
		||||
		})
 | 
			
		||||
		.catch((err) => {
 | 
			
		||||
			process.stderr.write(err.stderr);
 | 
			
		||||
			process.exit(1);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	if (process.env.NODE_ENV === 'production') {
 | 
			
		||||
		console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`);
 | 
			
		||||
	} else {
 | 
			
		||||
		await buildDts();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	console.log(`[${_package.name}] finish building.`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildDts() {
 | 
			
		||||
	return execa(
 | 
			
		||||
		'tsc',
 | 
			
		||||
		[
 | 
			
		||||
			'--project', 'tsconfig.json',
 | 
			
		||||
			'--outDir', 'built',
 | 
			
		||||
			'--declaration', 'true',
 | 
			
		||||
			'--emitDeclarationOnly', 'true',
 | 
			
		||||
		],
 | 
			
		||||
		{
 | 
			
		||||
			stdout: process.stdout,
 | 
			
		||||
			stderr: process.stderr,
 | 
			
		||||
		}
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function watchSrc() {
 | 
			
		||||
	const plugins = [{
 | 
			
		||||
		name: 'gen-dts',
 | 
			
		||||
		setup(build) {
 | 
			
		||||
			build.onStart(() => {
 | 
			
		||||
				console.log(`[${_package.name}] detect changed...`);
 | 
			
		||||
			});
 | 
			
		||||
			build.onEnd(async result => {
 | 
			
		||||
				if (result.errors.length > 0) {
 | 
			
		||||
					console.error(`[${_package.name}] watch build failed:`, result);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
				await buildDts();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
	}];
 | 
			
		||||
 | 
			
		||||
	console.log(`[${_package.name}] start watching...`)
 | 
			
		||||
 | 
			
		||||
	const context = await esbuild.context({ ...options, plugins });
 | 
			
		||||
	await context.watch();
 | 
			
		||||
 | 
			
		||||
	await new Promise((resolve, reject) => {
 | 
			
		||||
		process.on('SIGHUP', resolve);
 | 
			
		||||
		process.on('SIGINT', resolve);
 | 
			
		||||
		process.on('SIGTERM', resolve);
 | 
			
		||||
		process.on('SIGKILL', resolve);
 | 
			
		||||
		process.on('uncaughtException', reject);
 | 
			
		||||
		process.on('exit', resolve);
 | 
			
		||||
	}).finally(async () => {
 | 
			
		||||
		await context.dispose();
 | 
			
		||||
		console.log(`[${_package.name}] finish watching.`);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
				
			
			@ -60,13 +60,17 @@ async function generateEndpoints(
 | 
			
		|||
	// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
 | 
			
		||||
	const paths = openApiDocs.paths ?? {};
 | 
			
		||||
	const postPathItems = Object.keys(paths)
 | 
			
		||||
		.map(it => paths[it]?.post)
 | 
			
		||||
		.map(it => ({
 | 
			
		||||
			_path_: it.replace(/^\//, ''),
 | 
			
		||||
			...paths[it]?.post,
 | 
			
		||||
		}))
 | 
			
		||||
		.filter(filterUndefined);
 | 
			
		||||
 | 
			
		||||
	for (const operation of postPathItems) {
 | 
			
		||||
		const path = operation._path_;
 | 
			
		||||
		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
			
		||||
		const operationId = operation.operationId!;
 | 
			
		||||
		const endpoint = new Endpoint(operationId);
 | 
			
		||||
		const endpoint = new Endpoint(path);
 | 
			
		||||
		endpoints.push(endpoint);
 | 
			
		||||
 | 
			
		||||
		if (isRequestBodyObject(operation.requestBody)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -76,19 +80,21 @@ async function generateEndpoints(
 | 
			
		|||
				// いまのところ複数のメディアタイプをとるエンドポイントは無いので決め打ちする
 | 
			
		||||
				endpoint.request = new OperationTypeAlias(
 | 
			
		||||
					operationId,
 | 
			
		||||
					path,
 | 
			
		||||
					supportMediaTypes[0],
 | 
			
		||||
					OperationsAliasType.REQUEST,
 | 
			
		||||
				);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (isResponseObject(operation.responses['200']) && operation.responses['200'].content) {
 | 
			
		||||
		if (operation.responses && isResponseObject(operation.responses['200']) && operation.responses['200'].content) {
 | 
			
		||||
			const resContent = operation.responses['200'].content;
 | 
			
		||||
			const supportMediaTypes = Object.keys(resContent);
 | 
			
		||||
			if (supportMediaTypes.length > 0) {
 | 
			
		||||
				// いまのところ複数のメディアタイプを返すエンドポイントは無いので決め打ちする
 | 
			
		||||
				endpoint.response = new OperationTypeAlias(
 | 
			
		||||
					operationId,
 | 
			
		||||
					path,
 | 
			
		||||
					supportMediaTypes[0],
 | 
			
		||||
					OperationsAliasType.RESPONSE,
 | 
			
		||||
				);
 | 
			
		||||
| 
						 | 
				
			
			@ -140,12 +146,19 @@ async function generateApiClientJSDoc(
 | 
			
		|||
	endpointsFileName: string,
 | 
			
		||||
	warningsOutputPath: string,
 | 
			
		||||
) {
 | 
			
		||||
	const endpoints: { operationId: string; description: string; }[] = [];
 | 
			
		||||
	const endpoints: {
 | 
			
		||||
		operationId: string;
 | 
			
		||||
		path: string;
 | 
			
		||||
		description: string;
 | 
			
		||||
	}[] = [];
 | 
			
		||||
 | 
			
		||||
	// misskey-jsはPOST固定で送っているので、こちらも決め打ちする。別メソッドに対応することがあればこちらも直す必要あり
 | 
			
		||||
	const paths = openApiDocs.paths ?? {};
 | 
			
		||||
	const postPathItems = Object.keys(paths)
 | 
			
		||||
		.map(it => paths[it]?.post)
 | 
			
		||||
		.map(it => ({
 | 
			
		||||
			_path_: it.replace(/^\//, ''),
 | 
			
		||||
			...paths[it]?.post,
 | 
			
		||||
		}))
 | 
			
		||||
		.filter(filterUndefined);
 | 
			
		||||
 | 
			
		||||
	for (const operation of postPathItems) {
 | 
			
		||||
| 
						 | 
				
			
			@ -155,6 +168,7 @@ async function generateApiClientJSDoc(
 | 
			
		|||
		if (operation.description) {
 | 
			
		||||
			endpoints.push({
 | 
			
		||||
				operationId: operationId,
 | 
			
		||||
				path: operation._path_,
 | 
			
		||||
				description: operation.description,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -175,7 +189,7 @@ async function generateApiClientJSDoc(
 | 
			
		|||
			'    /**',
 | 
			
		||||
			`     * ${endpoint.description.split('\n').join('\n     * ')}`,
 | 
			
		||||
			'     */',
 | 
			
		||||
			`    request<E extends '${endpoint.operationId}', P extends Endpoints[E][\'req\']>(`,
 | 
			
		||||
			`    request<E extends '${endpoint.path}', P extends Endpoints[E][\'req\']>(`,
 | 
			
		||||
			'      endpoint: E,',
 | 
			
		||||
			'      params: P,',
 | 
			
		||||
			'      credential?: string | null,',
 | 
			
		||||
| 
						 | 
				
			
			@ -234,21 +248,24 @@ interface IOperationTypeAlias {
 | 
			
		|||
 | 
			
		||||
class OperationTypeAlias implements IOperationTypeAlias {
 | 
			
		||||
	public readonly operationId: string;
 | 
			
		||||
	public readonly path: string;
 | 
			
		||||
	public readonly mediaType: string;
 | 
			
		||||
	public readonly type: OperationsAliasType;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		operationId: string,
 | 
			
		||||
		path: string,
 | 
			
		||||
		mediaType: string,
 | 
			
		||||
		type: OperationsAliasType,
 | 
			
		||||
	) {
 | 
			
		||||
		this.operationId = operationId;
 | 
			
		||||
		this.path = path;
 | 
			
		||||
		this.mediaType = mediaType;
 | 
			
		||||
		this.type = type;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	generateName(): string {
 | 
			
		||||
		const nameBase = this.operationId.replace(/\//g, '-');
 | 
			
		||||
		const nameBase = this.path.replace(/\//g, '-');
 | 
			
		||||
		return toPascal(nameBase + this.type);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -281,19 +298,19 @@ const emptyRequest = new EmptyTypeAlias(OperationsAliasType.REQUEST);
 | 
			
		|||
const emptyResponse = new EmptyTypeAlias(OperationsAliasType.RESPONSE);
 | 
			
		||||
 | 
			
		||||
class Endpoint {
 | 
			
		||||
	public readonly operationId: string;
 | 
			
		||||
	public readonly path: string;
 | 
			
		||||
	public request?: IOperationTypeAlias;
 | 
			
		||||
	public response?: IOperationTypeAlias;
 | 
			
		||||
 | 
			
		||||
	constructor(operationId: string) {
 | 
			
		||||
		this.operationId = operationId;
 | 
			
		||||
	constructor(path: string) {
 | 
			
		||||
		this.path = path;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	toLine(): string {
 | 
			
		||||
		const reqName = this.request?.generateName() ?? emptyRequest.generateName();
 | 
			
		||||
		const resName = this.response?.generateName() ?? emptyResponse.generateName();
 | 
			
		||||
 | 
			
		||||
		return `'${this.operationId}': { req: ${reqName}; res: ${resName} };`;
 | 
			
		||||
		return `'${this.path}': { req: ${reqName}; res: ${resName} };`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,23 +3,21 @@
 | 
			
		|||
	"name": "misskey-js",
 | 
			
		||||
	"version": "2024.3.1",
 | 
			
		||||
	"description": "Misskey SDK for JavaScript",
 | 
			
		||||
	"types": "./built/dts/index.d.ts",
 | 
			
		||||
	"main": "./built/index.js",
 | 
			
		||||
	"types": "./built/index.d.ts",
 | 
			
		||||
	"exports": {
 | 
			
		||||
		".": {
 | 
			
		||||
			"import": "./built/esm/index.js",
 | 
			
		||||
			"types": "./built/dts/index.d.ts"
 | 
			
		||||
			"import": "./built/index.js",
 | 
			
		||||
			"types": "./built/index.d.ts"
 | 
			
		||||
		},
 | 
			
		||||
		"./*": {
 | 
			
		||||
			"import": "./built/esm/*",
 | 
			
		||||
			"types": "./built/dts/*"
 | 
			
		||||
			"import": "./built/*",
 | 
			
		||||
			"types": "./built/*"
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"build": "npm run ts",
 | 
			
		||||
		"ts": "npm run ts-esm && npm run ts-dts",
 | 
			
		||||
		"ts-esm": "tsc --outDir built/esm",
 | 
			
		||||
		"ts-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
 | 
			
		||||
		"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run ts\"",
 | 
			
		||||
		"build": "node ./build.js",
 | 
			
		||||
		"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
 | 
			
		||||
		"tsd": "tsd",
 | 
			
		||||
		"api": "pnpm api-extractor run --local --verbose",
 | 
			
		||||
		"api-prod": "pnpm api-extractor run --verbose",
 | 
			
		||||
| 
						 | 
				
			
			@ -49,17 +47,16 @@
 | 
			
		|||
		"mock-socket": "9.3.1",
 | 
			
		||||
		"ncp": "2.0.0",
 | 
			
		||||
		"nodemon": "3.1.0",
 | 
			
		||||
		"execa": "8.0.1",
 | 
			
		||||
		"tsd": "0.30.7",
 | 
			
		||||
		"typescript": "5.3.3"
 | 
			
		||||
		"typescript": "5.3.3",
 | 
			
		||||
		"esbuild": "0.19.11",
 | 
			
		||||
		"glob": "10.3.10"
 | 
			
		||||
	},
 | 
			
		||||
	"files": [
 | 
			
		||||
		"built",
 | 
			
		||||
		"built/esm",
 | 
			
		||||
		"built/dts"
 | 
			
		||||
		"built"
 | 
			
		||||
	],
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@swc/cli": "0.1.63",
 | 
			
		||||
		"@swc/core": "1.3.105",
 | 
			
		||||
		"eventemitter3": "5.0.1",
 | 
			
		||||
		"reconnecting-websocket": "4.4.0"
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import './autogen/apiClientJSDoc.js';
 | 
			
		|||
import { SwitchCaseResponseType } from './api.types.js';
 | 
			
		||||
import type { Endpoints } from './api.types.js';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
export type {
 | 
			
		||||
	SwitchCaseResponseType,
 | 
			
		||||
} from './api.types.js';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
				
			
			@ -1,17 +1,20 @@
 | 
			
		|||
import { Endpoints } from './api.types.js';
 | 
			
		||||
import { type Endpoints } from './api.types.js';
 | 
			
		||||
import Stream, { Connection } from './streaming.js';
 | 
			
		||||
import { Channels } from './streaming.types.js';
 | 
			
		||||
import { Acct } from './acct.js';
 | 
			
		||||
import { type Channels } from './streaming.types.js';
 | 
			
		||||
import { type Acct } from './acct.js';
 | 
			
		||||
import * as consts from './consts.js';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
export type {
 | 
			
		||||
	Endpoints,
 | 
			
		||||
	Stream,
 | 
			
		||||
	Connection as ChannelConnection,
 | 
			
		||||
	Channels,
 | 
			
		||||
	Acct,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	Stream,
 | 
			
		||||
	Connection as ChannelConnection,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const permissions = consts.permissions;
 | 
			
		||||
export const notificationTypes = consts.notificationTypes;
 | 
			
		||||
export const noteVisibilities = consts.noteVisibilities;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
		"moduleResolution": "nodenext",
 | 
			
		||||
		"declaration": true,
 | 
			
		||||
		"declarationMap": true,
 | 
			
		||||
		"sourceMap": true,
 | 
			
		||||
		"sourceMap": false,
 | 
			
		||||
		"outDir": "./built/",
 | 
			
		||||
		"removeComments": true,
 | 
			
		||||
		"strict": true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,3 +5,4 @@ node_modules
 | 
			
		|||
/jest.config.ts
 | 
			
		||||
/test
 | 
			
		||||
/test-d
 | 
			
		||||
build.js
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,31 +1,105 @@
 | 
			
		|||
import * as esbuild from "esbuild";
 | 
			
		||||
import { build } from "esbuild";
 | 
			
		||||
import { globSync } from "glob";
 | 
			
		||||
import { execa } from "execa";
 | 
			
		||||
import fs from "node:fs";
 | 
			
		||||
import { fileURLToPath } from "node:url";
 | 
			
		||||
import { dirname } from "node:path";
 | 
			
		||||
 | 
			
		||||
const _filename = fileURLToPath(import.meta.url);
 | 
			
		||||
const _dirname = dirname(_filename);
 | 
			
		||||
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
 | 
			
		||||
 | 
			
		||||
const entryPoints = globSync("./src/**/**.{ts,tsx}");
 | 
			
		||||
 | 
			
		||||
/** @type {import('esbuild').BuildOptions} */
 | 
			
		||||
const options = {
 | 
			
		||||
	entryPoints,
 | 
			
		||||
  minify: true,
 | 
			
		||||
  outdir: "./built/esm",
 | 
			
		||||
	minify: process.env.NODE_ENV === 'production',
 | 
			
		||||
	outdir: "./built",
 | 
			
		||||
	target: "es2022",
 | 
			
		||||
	platform: "browser",
 | 
			
		||||
	format: "esm",
 | 
			
		||||
	sourcemap: 'linked',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if (process.env.WATCH === "true") {
 | 
			
		||||
  options.watch = {
 | 
			
		||||
    onRebuild(error, result) {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        console.error("watch build failed:", error);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log("watch build succeeded:", result);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
// built配下をすべて削除する
 | 
			
		||||
fs.rmSync('./built', { recursive: true, force: true });
 | 
			
		||||
 | 
			
		||||
if (process.argv.map(arg => arg.toLowerCase()).includes("--watch")) {
 | 
			
		||||
	await watchSrc();
 | 
			
		||||
} else {
 | 
			
		||||
	await buildSrc();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build(options).catch((err) => {
 | 
			
		||||
async function buildSrc() {
 | 
			
		||||
	console.log(`[${_package.name}] start building...`);
 | 
			
		||||
 | 
			
		||||
	await build(options)
 | 
			
		||||
		.then(it => {
 | 
			
		||||
			console.log(`[${_package.name}] build succeeded.`);
 | 
			
		||||
		})
 | 
			
		||||
		.catch((err) => {
 | 
			
		||||
			process.stderr.write(err.stderr);
 | 
			
		||||
			process.exit(1);
 | 
			
		||||
});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	if (process.env.NODE_ENV === 'production') {
 | 
			
		||||
		console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`);
 | 
			
		||||
	} else {
 | 
			
		||||
		await buildDts();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	console.log(`[${_package.name}] finish building.`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildDts() {
 | 
			
		||||
	return execa(
 | 
			
		||||
		'tsc',
 | 
			
		||||
		[
 | 
			
		||||
			'--project', 'tsconfig.json',
 | 
			
		||||
			'--outDir', 'built',
 | 
			
		||||
			'--declaration', 'true',
 | 
			
		||||
			'--emitDeclarationOnly', 'true',
 | 
			
		||||
		],
 | 
			
		||||
		{
 | 
			
		||||
			stdout: process.stdout,
 | 
			
		||||
			stderr: process.stderr,
 | 
			
		||||
		}
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function watchSrc() {
 | 
			
		||||
	const plugins = [{
 | 
			
		||||
		name: 'gen-dts',
 | 
			
		||||
		setup(build) {
 | 
			
		||||
			build.onStart(() => {
 | 
			
		||||
				console.log(`[${_package.name}] detect changed...`);
 | 
			
		||||
			});
 | 
			
		||||
			build.onEnd(async result => {
 | 
			
		||||
				if (result.errors.length > 0) {
 | 
			
		||||
					console.error(`[${_package.name}] watch build failed:`, result);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
				await buildDts();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
	}];
 | 
			
		||||
 | 
			
		||||
	console.log(`[${_package.name}] start watching...`)
 | 
			
		||||
 | 
			
		||||
	const context = await esbuild.context({ ...options, plugins });
 | 
			
		||||
	await context.watch();
 | 
			
		||||
 | 
			
		||||
	await new Promise((resolve, reject) => {
 | 
			
		||||
		process.on('SIGHUP', resolve);
 | 
			
		||||
		process.on('SIGINT', resolve);
 | 
			
		||||
		process.on('SIGTERM', resolve);
 | 
			
		||||
		process.on('SIGKILL', resolve);
 | 
			
		||||
		process.on('uncaughtException', reject);
 | 
			
		||||
		process.on('exit', resolve);
 | 
			
		||||
	}).finally(async () => {
 | 
			
		||||
		await context.dispose();
 | 
			
		||||
		console.log(`[${_package.name}] finish watching.`);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,24 +2,21 @@
 | 
			
		|||
	"type": "module",
 | 
			
		||||
	"name": "misskey-reversi",
 | 
			
		||||
	"version": "0.0.1",
 | 
			
		||||
	"types": "./built/dts/index.d.ts",
 | 
			
		||||
	"main": "./built/index.js",
 | 
			
		||||
	"types": "./built/index.d.ts",
 | 
			
		||||
	"exports": {
 | 
			
		||||
		".": {
 | 
			
		||||
			"import": "./built/esm/index.js",
 | 
			
		||||
			"types": "./built/dts/index.d.ts"
 | 
			
		||||
			"import": "./built/index.js",
 | 
			
		||||
			"types": "./built/index.d.ts"
 | 
			
		||||
		},
 | 
			
		||||
		"./*": {
 | 
			
		||||
			"import": "./built/esm/*",
 | 
			
		||||
			"types": "./built/dts/*"
 | 
			
		||||
			"import": "./built/*",
 | 
			
		||||
			"types": "./built/*"
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"build": "node ./build.js",
 | 
			
		||||
		"build:tsc": "npm run tsc",
 | 
			
		||||
		"tsc": "npm run tsc-esm && npm run tsc-dts",
 | 
			
		||||
		"tsc-esm": "tsc --outDir built/esm",
 | 
			
		||||
		"tsc-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
 | 
			
		||||
		"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build:tsc\"",
 | 
			
		||||
		"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
 | 
			
		||||
		"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
 | 
			
		||||
		"typecheck": "tsc --noEmit",
 | 
			
		||||
		"lint": "pnpm typecheck && pnpm eslint"
 | 
			
		||||
| 
						 | 
				
			
			@ -30,15 +27,16 @@
 | 
			
		|||
		"@typescript-eslint/eslint-plugin": "7.1.0",
 | 
			
		||||
		"@typescript-eslint/parser": "7.1.0",
 | 
			
		||||
		"eslint": "8.57.0",
 | 
			
		||||
		"execa": "8.0.1",
 | 
			
		||||
		"nodemon": "3.0.2",
 | 
			
		||||
		"typescript": "5.3.3"
 | 
			
		||||
		"typescript": "5.3.3",
 | 
			
		||||
		"esbuild": "0.19.11",
 | 
			
		||||
		"glob": "10.3.10"
 | 
			
		||||
	},
 | 
			
		||||
	"files": [
 | 
			
		||||
		"built"
 | 
			
		||||
	],
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"crc-32": "1.2.2",
 | 
			
		||||
		"esbuild": "0.19.11",
 | 
			
		||||
		"glob": "10.3.10"
 | 
			
		||||
		"crc-32": "1.2.2"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
		"moduleResolution": "nodenext",
 | 
			
		||||
		"declaration": true,
 | 
			
		||||
		"declarationMap": true,
 | 
			
		||||
		"sourceMap": true,
 | 
			
		||||
		"sourceMap": false,
 | 
			
		||||
		"outDir": "./built/",
 | 
			
		||||
		"removeComments": true,
 | 
			
		||||
		"strict": true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -76,7 +76,7 @@ globalThis.addEventListener('push', ev => {
 | 
			
		|||
			case 'notification':
 | 
			
		||||
			case 'unreadAntennaNote':
 | 
			
		||||
				// 1日以上経過している場合は無視
 | 
			
		||||
				if ((new Date()).getTime() - data.dateTime > 1000 * 60 * 60 * 24) break;
 | 
			
		||||
				if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break;
 | 
			
		||||
 | 
			
		||||
				return createNotification(data);
 | 
			
		||||
			case 'readAllNotifications':
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										232
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										232
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -15,12 +15,18 @@ importers:
 | 
			
		|||
      cssnano:
 | 
			
		||||
        specifier: 6.0.5
 | 
			
		||||
        version: 6.0.5(postcss@8.4.35)
 | 
			
		||||
      esbuild:
 | 
			
		||||
        specifier: 0.19.11
 | 
			
		||||
        version: 0.19.11
 | 
			
		||||
      execa:
 | 
			
		||||
        specifier: 8.0.1
 | 
			
		||||
        version: 8.0.1
 | 
			
		||||
      fast-glob:
 | 
			
		||||
        specifier: 3.3.2
 | 
			
		||||
        version: 3.3.2
 | 
			
		||||
      glob:
 | 
			
		||||
        specifier: 10.3.10
 | 
			
		||||
        version: 10.3.10
 | 
			
		||||
      ignore-walk:
 | 
			
		||||
        specifier: 6.0.4
 | 
			
		||||
        version: 6.0.4
 | 
			
		||||
| 
						 | 
				
			
			@ -724,8 +730,8 @@ importers:
 | 
			
		|||
        specifier: 3.4.21
 | 
			
		||||
        version: 3.4.21
 | 
			
		||||
      aiscript-vscode:
 | 
			
		||||
        specifier: github:aiscript-dev/aiscript-vscode#v0.1.2
 | 
			
		||||
        version: github.com/aiscript-dev/aiscript-vscode/793211d40243c8775f6b85f015c221c82cbffb07
 | 
			
		||||
        specifier: github:aiscript-dev/aiscript-vscode#v0.1.4
 | 
			
		||||
        version: github.com/aiscript-dev/aiscript-vscode/3f79d6f0550369267220aa67702287948d885424
 | 
			
		||||
      astring:
 | 
			
		||||
        specifier: 1.8.6
 | 
			
		||||
        version: 1.8.6
 | 
			
		||||
| 
						 | 
				
			
			@ -1129,22 +1135,16 @@ importers:
 | 
			
		|||
        version: 3.1.0
 | 
			
		||||
      ts-jest:
 | 
			
		||||
        specifier: ^29.1.1
 | 
			
		||||
        version: 29.1.1(@babel/core@7.24.0)(jest@29.7.0)(typescript@5.1.6)
 | 
			
		||||
        version: 29.1.1(@babel/core@7.24.0)(esbuild@0.19.11)(jest@29.7.0)(typescript@5.1.6)
 | 
			
		||||
      typedoc:
 | 
			
		||||
        specifier: ^0.25.3
 | 
			
		||||
        version: 0.25.3(typescript@5.1.6)
 | 
			
		||||
 | 
			
		||||
  packages/misskey-bubble-game:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      esbuild:
 | 
			
		||||
        specifier: 0.19.11
 | 
			
		||||
        version: 0.19.11
 | 
			
		||||
      eventemitter3:
 | 
			
		||||
        specifier: 5.0.1
 | 
			
		||||
        version: 5.0.1
 | 
			
		||||
      glob:
 | 
			
		||||
        specifier: ^10.3.10
 | 
			
		||||
        version: 10.3.10
 | 
			
		||||
      matter-js:
 | 
			
		||||
        specifier: 0.19.0
 | 
			
		||||
        version: 0.19.0
 | 
			
		||||
| 
						 | 
				
			
			@ -1170,9 +1170,18 @@ importers:
 | 
			
		|||
      '@typescript-eslint/parser':
 | 
			
		||||
        specifier: 7.1.0
 | 
			
		||||
        version: 7.1.0(eslint@8.57.0)(typescript@5.3.3)
 | 
			
		||||
      esbuild:
 | 
			
		||||
        specifier: 0.19.11
 | 
			
		||||
        version: 0.19.11
 | 
			
		||||
      eslint:
 | 
			
		||||
        specifier: 8.57.0
 | 
			
		||||
        version: 8.57.0
 | 
			
		||||
      execa:
 | 
			
		||||
        specifier: 8.0.1
 | 
			
		||||
        version: 8.0.1
 | 
			
		||||
      glob:
 | 
			
		||||
        specifier: 10.3.10
 | 
			
		||||
        version: 10.3.10
 | 
			
		||||
      nodemon:
 | 
			
		||||
        specifier: 3.0.2
 | 
			
		||||
        version: 3.0.2
 | 
			
		||||
| 
						 | 
				
			
			@ -1182,12 +1191,6 @@ importers:
 | 
			
		|||
 | 
			
		||||
  packages/misskey-js:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@swc/cli':
 | 
			
		||||
        specifier: 0.1.63
 | 
			
		||||
        version: 0.1.63(@swc/core@1.3.105)
 | 
			
		||||
      '@swc/core':
 | 
			
		||||
        specifier: 1.3.105
 | 
			
		||||
        version: 1.3.105
 | 
			
		||||
      eventemitter3:
 | 
			
		||||
        specifier: 5.0.1
 | 
			
		||||
        version: 5.0.1
 | 
			
		||||
| 
						 | 
				
			
			@ -1203,7 +1206,7 @@ importers:
 | 
			
		|||
        version: 1.0.0(@typescript-eslint/eslint-plugin@7.1.0)(@typescript-eslint/parser@7.1.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
 | 
			
		||||
      '@swc/jest':
 | 
			
		||||
        specifier: 0.2.31
 | 
			
		||||
        version: 0.2.31(@swc/core@1.3.105)
 | 
			
		||||
        version: 0.2.31(@swc/core@1.3.107)
 | 
			
		||||
      '@types/jest':
 | 
			
		||||
        specifier: 29.5.12
 | 
			
		||||
        version: 29.5.12
 | 
			
		||||
| 
						 | 
				
			
			@ -1216,9 +1219,18 @@ importers:
 | 
			
		|||
      '@typescript-eslint/parser':
 | 
			
		||||
        specifier: 7.1.0
 | 
			
		||||
        version: 7.1.0(eslint@8.57.0)(typescript@5.3.3)
 | 
			
		||||
      esbuild:
 | 
			
		||||
        specifier: 0.19.11
 | 
			
		||||
        version: 0.19.11
 | 
			
		||||
      eslint:
 | 
			
		||||
        specifier: 8.57.0
 | 
			
		||||
        version: 8.57.0
 | 
			
		||||
      execa:
 | 
			
		||||
        specifier: 8.0.1
 | 
			
		||||
        version: 8.0.1
 | 
			
		||||
      glob:
 | 
			
		||||
        specifier: 10.3.10
 | 
			
		||||
        version: 10.3.10
 | 
			
		||||
      jest:
 | 
			
		||||
        specifier: 29.7.0
 | 
			
		||||
        version: 29.7.0(@types/node@20.11.22)
 | 
			
		||||
| 
						 | 
				
			
			@ -1285,12 +1297,6 @@ importers:
 | 
			
		|||
      crc-32:
 | 
			
		||||
        specifier: 1.2.2
 | 
			
		||||
        version: 1.2.2
 | 
			
		||||
      esbuild:
 | 
			
		||||
        specifier: 0.19.11
 | 
			
		||||
        version: 0.19.11
 | 
			
		||||
      glob:
 | 
			
		||||
        specifier: 10.3.10
 | 
			
		||||
        version: 10.3.10
 | 
			
		||||
    devDependencies:
 | 
			
		||||
      '@misskey-dev/eslint-plugin':
 | 
			
		||||
        specifier: 1.0.0
 | 
			
		||||
| 
						 | 
				
			
			@ -1304,9 +1310,18 @@ importers:
 | 
			
		|||
      '@typescript-eslint/parser':
 | 
			
		||||
        specifier: 7.1.0
 | 
			
		||||
        version: 7.1.0(eslint@8.57.0)(typescript@5.3.3)
 | 
			
		||||
      esbuild:
 | 
			
		||||
        specifier: 0.19.11
 | 
			
		||||
        version: 0.19.11
 | 
			
		||||
      eslint:
 | 
			
		||||
        specifier: 8.57.0
 | 
			
		||||
        version: 8.57.0
 | 
			
		||||
      execa:
 | 
			
		||||
        specifier: 8.0.1
 | 
			
		||||
        version: 8.0.1
 | 
			
		||||
      glob:
 | 
			
		||||
        specifier: 10.3.10
 | 
			
		||||
        version: 10.3.10
 | 
			
		||||
      nodemon:
 | 
			
		||||
        specifier: 3.0.2
 | 
			
		||||
        version: 3.0.2
 | 
			
		||||
| 
						 | 
				
			
			@ -5262,7 +5277,7 @@ packages:
 | 
			
		|||
    resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==}
 | 
			
		||||
    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      semver: 7.5.4
 | 
			
		||||
      semver: 7.6.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /@nuxtjs/opencollective@0.3.2:
 | 
			
		||||
| 
						 | 
				
			
			@ -6953,32 +6968,12 @@ packages:
 | 
			
		|||
      ts-dedent: 2.2.0
 | 
			
		||||
      type-fest: 2.19.0
 | 
			
		||||
      vue: 3.4.21(typescript@5.3.3)
 | 
			
		||||
      vue-component-type-helpers: 2.0.7
 | 
			
		||||
      vue-component-type-helpers: 2.0.12
 | 
			
		||||
    transitivePeerDependencies:
 | 
			
		||||
      - encoding
 | 
			
		||||
      - supports-color
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /@swc/cli@0.1.63(@swc/core@1.3.105):
 | 
			
		||||
    resolution: {integrity: sha512-EM9oxxHzmmsprYRbGqsS2M4M/Gr5Gkcl0ROYYIdlUyTkhOiX822EQiRCpPCwdutdnzH2GyaTN7wc6i0Y+CKd3A==}
 | 
			
		||||
    engines: {node: '>= 12.13'}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      '@swc/core': ^1.2.66
 | 
			
		||||
      chokidar: 3.5.3
 | 
			
		||||
    peerDependenciesMeta:
 | 
			
		||||
      chokidar:
 | 
			
		||||
        optional: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@mole-inc/bin-wrapper': 8.0.1
 | 
			
		||||
      '@swc/core': 1.3.105
 | 
			
		||||
      commander: 7.2.0
 | 
			
		||||
      fast-glob: 3.3.2
 | 
			
		||||
      semver: 7.5.4
 | 
			
		||||
      slash: 3.0.0
 | 
			
		||||
      source-map: 0.7.4
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /@swc/cli@0.1.63(@swc/core@1.3.107)(chokidar@3.5.3):
 | 
			
		||||
    resolution: {integrity: sha512-EM9oxxHzmmsprYRbGqsS2M4M/Gr5Gkcl0ROYYIdlUyTkhOiX822EQiRCpPCwdutdnzH2GyaTN7wc6i0Y+CKd3A==}
 | 
			
		||||
    engines: {node: '>= 12.13'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7011,14 +7006,6 @@ packages:
 | 
			
		|||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-darwin-arm64@1.3.105:
 | 
			
		||||
    resolution: {integrity: sha512-buWeweLVDXXmcnfIemH4PGnpjwsDTUGitnPchdftb0u1FU8zSSP/lw/pUCBDG/XvWAp7c/aFxgN4CyG0j7eayA==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [darwin]
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-darwin-arm64@1.3.107:
 | 
			
		||||
    resolution: {integrity: sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7036,14 +7023,6 @@ packages:
 | 
			
		|||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-darwin-x64@1.3.105:
 | 
			
		||||
    resolution: {integrity: sha512-hFmXPApqjA/8sy/9NpljHVaKi1OvL9QkJ2MbbTCCbJERuHMpMUeMBUWipHRfepGHFhU+9B9zkEup/qJaJR4XIg==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [darwin]
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-darwin-x64@1.3.107:
 | 
			
		||||
    resolution: {integrity: sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7072,14 +7051,6 @@ packages:
 | 
			
		|||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-linux-arm-gnueabihf@1.3.105:
 | 
			
		||||
    resolution: {integrity: sha512-mwXyMC41oMKkKrPpL8uJpOxw7fyfQoVtIw3Y5p0Blabk+espNYqix0E8VymHdRKuLmM//z5wVmMsuHdGBHvZeg==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    cpu: [arm]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-linux-arm-gnueabihf@1.3.107:
 | 
			
		||||
    resolution: {integrity: sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7097,14 +7068,6 @@ packages:
 | 
			
		|||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-linux-arm64-gnu@1.3.105:
 | 
			
		||||
    resolution: {integrity: sha512-H7yEIVydnUtqBSUxwmO6vpIQn7j+Rr0DF6ZOORPyd/SFzQJK9cJRtmJQ3ZMzlJ1Bb+1gr3MvjgLEnmyCYEm2Hg==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-linux-arm64-gnu@1.3.107:
 | 
			
		||||
    resolution: {integrity: sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7122,14 +7085,6 @@ packages:
 | 
			
		|||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-linux-arm64-musl@1.3.105:
 | 
			
		||||
    resolution: {integrity: sha512-Jg7RTFT3pGFdGt5elPV6oDkinRy7q9cXpenjXnJnM2uvx3jOwnsAhexPyCDHom8SHL0j+9kaLLC66T3Gz1E4UA==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-linux-arm64-musl@1.3.107:
 | 
			
		||||
    resolution: {integrity: sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7147,14 +7102,6 @@ packages:
 | 
			
		|||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-linux-x64-gnu@1.3.105:
 | 
			
		||||
    resolution: {integrity: sha512-DJghplpyusAmp1X5pW/y93MmS/u83Sx5GrpJxI6KLPa82+NItTgMcl8KBQmW5GYAJpVKZyaIvBanS5TdR8aN2w==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-linux-x64-gnu@1.3.107:
 | 
			
		||||
    resolution: {integrity: sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7172,14 +7119,6 @@ packages:
 | 
			
		|||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-linux-x64-musl@1.3.105:
 | 
			
		||||
    resolution: {integrity: sha512-wD5jL2dZH/5nPNssBo6jhOvkI0lmWnVR4vnOXWjuXgjq1S0AJpO5jdre/6pYLmf26hft3M42bteDnjR4AAZ38w==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [linux]
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-linux-x64-musl@1.3.107:
 | 
			
		||||
    resolution: {integrity: sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7197,14 +7136,6 @@ packages:
 | 
			
		|||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-win32-arm64-msvc@1.3.105:
 | 
			
		||||
    resolution: {integrity: sha512-UqJtwILUHRw2+3UTPnRkZrzM/bGdQtbR4UFdp79mZQYfryeOUVNg7aJj/bWUTkKtLiZ3o+FBNrM/x2X1mJX5bA==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    cpu: [arm64]
 | 
			
		||||
    os: [win32]
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-win32-arm64-msvc@1.3.107:
 | 
			
		||||
    resolution: {integrity: sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7222,14 +7153,6 @@ packages:
 | 
			
		|||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-win32-ia32-msvc@1.3.105:
 | 
			
		||||
    resolution: {integrity: sha512-Z95C6vZgBEJ1snidYyjVKnVWiy/ZpPiIFIXGWkDr4ZyBgL3eZX12M6LzZ+NApHKffrbO4enbFyFomueBQgS2oA==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    cpu: [ia32]
 | 
			
		||||
    os: [win32]
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-win32-ia32-msvc@1.3.107:
 | 
			
		||||
    resolution: {integrity: sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7247,14 +7170,6 @@ packages:
 | 
			
		|||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-win32-x64-msvc@1.3.105:
 | 
			
		||||
    resolution: {integrity: sha512-3J8fkyDPFsS3mszuYUY4Wfk7/B2oio9qXUwF3DzOs2MK+XgdyMLIptIxL7gdfitXJBH8k39uVjrIw1JGJDjyFA==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    cpu: [x64]
 | 
			
		||||
    os: [win32]
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core-win32-x64-msvc@1.3.107:
 | 
			
		||||
    resolution: {integrity: sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7272,30 +7187,6 @@ packages:
 | 
			
		|||
    dev: false
 | 
			
		||||
    optional: true
 | 
			
		||||
 | 
			
		||||
  /@swc/core@1.3.105:
 | 
			
		||||
    resolution: {integrity: sha512-me2VZyr3OjqRpFrYQJJYy7x/zbFSl9nt+MAGnIcBtjDsN00iTVqEaKxBjPBFQV9BDAgPz2SRWes/DhhVm5SmMw==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    requiresBuild: true
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      '@swc/helpers': ^0.5.0
 | 
			
		||||
    peerDependenciesMeta:
 | 
			
		||||
      '@swc/helpers':
 | 
			
		||||
        optional: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@swc/counter': 0.1.2
 | 
			
		||||
      '@swc/types': 0.1.5
 | 
			
		||||
    optionalDependencies:
 | 
			
		||||
      '@swc/core-darwin-arm64': 1.3.105
 | 
			
		||||
      '@swc/core-darwin-x64': 1.3.105
 | 
			
		||||
      '@swc/core-linux-arm-gnueabihf': 1.3.105
 | 
			
		||||
      '@swc/core-linux-arm64-gnu': 1.3.105
 | 
			
		||||
      '@swc/core-linux-arm64-musl': 1.3.105
 | 
			
		||||
      '@swc/core-linux-x64-gnu': 1.3.105
 | 
			
		||||
      '@swc/core-linux-x64-musl': 1.3.105
 | 
			
		||||
      '@swc/core-win32-arm64-msvc': 1.3.105
 | 
			
		||||
      '@swc/core-win32-ia32-msvc': 1.3.105
 | 
			
		||||
      '@swc/core-win32-x64-msvc': 1.3.105
 | 
			
		||||
 | 
			
		||||
  /@swc/core@1.3.107:
 | 
			
		||||
    resolution: {integrity: sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
| 
						 | 
				
			
			@ -7323,17 +7214,6 @@ packages:
 | 
			
		|||
  /@swc/counter@0.1.2:
 | 
			
		||||
    resolution: {integrity: sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==}
 | 
			
		||||
 | 
			
		||||
  /@swc/jest@0.2.31(@swc/core@1.3.105):
 | 
			
		||||
    resolution: {integrity: sha512-Gh0Ste380O8KUY1IqsKr+aOvqqs2Loa+WcWWVNwl+lhXqOWK1iTFAP1K0IDfLqAuFP68+D/PxcpBJn21e6Quvw==}
 | 
			
		||||
    engines: {npm: '>= 7.0.0'}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      '@swc/core': '*'
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@jest/create-cache-key-function': 29.7.0
 | 
			
		||||
      '@swc/core': 1.3.105
 | 
			
		||||
      jsonc-parser: 3.2.0
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /@swc/jest@0.2.31(@swc/core@1.3.107):
 | 
			
		||||
    resolution: {integrity: sha512-Gh0Ste380O8KUY1IqsKr+aOvqqs2Loa+WcWWVNwl+lhXqOWK1iTFAP1K0IDfLqAuFP68+D/PxcpBJn21e6Quvw==}
 | 
			
		||||
    engines: {npm: '>= 7.0.0'}
 | 
			
		||||
| 
						 | 
				
			
			@ -11123,7 +11003,7 @@ packages:
 | 
			
		|||
      '@one-ini/wasm': 0.1.1
 | 
			
		||||
      commander: 10.0.1
 | 
			
		||||
      minimatch: 9.0.1
 | 
			
		||||
      semver: 7.5.4
 | 
			
		||||
      semver: 7.6.0
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /ee-first@1.1.1:
 | 
			
		||||
| 
						 | 
				
			
			@ -13614,7 +13494,7 @@ packages:
 | 
			
		|||
      '@babel/parser': 7.23.6
 | 
			
		||||
      '@istanbuljs/schema': 0.1.3
 | 
			
		||||
      istanbul-lib-coverage: 3.2.2
 | 
			
		||||
      semver: 7.5.4
 | 
			
		||||
      semver: 7.6.0
 | 
			
		||||
    transitivePeerDependencies:
 | 
			
		||||
      - supports-color
 | 
			
		||||
    dev: true
 | 
			
		||||
| 
						 | 
				
			
			@ -14708,6 +14588,8 @@ packages:
 | 
			
		|||
  /lru-cache@10.1.0:
 | 
			
		||||
    resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==}
 | 
			
		||||
    engines: {node: 14 || >=16.14}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      semver: 7.6.0
 | 
			
		||||
 | 
			
		||||
  /lru-cache@4.1.5:
 | 
			
		||||
    resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
 | 
			
		||||
| 
						 | 
				
			
			@ -14790,7 +14672,7 @@ packages:
 | 
			
		|||
    resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
 | 
			
		||||
    engines: {node: '>=10'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      semver: 7.5.4
 | 
			
		||||
      semver: 7.6.0
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /make-error@1.3.6:
 | 
			
		||||
| 
						 | 
				
			
			@ -15858,7 +15740,7 @@ packages:
 | 
			
		|||
    dependencies:
 | 
			
		||||
      hosted-git-info: 4.1.0
 | 
			
		||||
      is-core-module: 2.13.1
 | 
			
		||||
      semver: 7.5.4
 | 
			
		||||
      semver: 7.6.0
 | 
			
		||||
      validate-npm-package-license: 3.0.4
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -17939,7 +17821,6 @@ packages:
 | 
			
		|||
    hasBin: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      lru-cache: 6.0.0
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /send@0.18.0:
 | 
			
		||||
    resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
 | 
			
		||||
| 
						 | 
				
			
			@ -19060,7 +18941,7 @@ packages:
 | 
			
		|||
    engines: {node: '>=6.10'}
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /ts-jest@29.1.1(@babel/core@7.24.0)(jest@29.7.0)(typescript@5.1.6):
 | 
			
		||||
  /ts-jest@29.1.1(@babel/core@7.24.0)(esbuild@0.19.11)(jest@29.7.0)(typescript@5.1.6):
 | 
			
		||||
    resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==}
 | 
			
		||||
    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
| 
						 | 
				
			
			@ -19083,6 +18964,7 @@ packages:
 | 
			
		|||
    dependencies:
 | 
			
		||||
      '@babel/core': 7.24.0
 | 
			
		||||
      bs-logger: 0.2.6
 | 
			
		||||
      esbuild: 0.19.11
 | 
			
		||||
      fast-json-stable-stringify: 2.1.0
 | 
			
		||||
      jest: 29.7.0(@types/node@20.11.30)
 | 
			
		||||
      jest-util: 29.7.0
 | 
			
		||||
| 
						 | 
				
			
			@ -19843,7 +19725,7 @@ packages:
 | 
			
		|||
    engines: {vscode: ^1.82.0}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      minimatch: 5.1.6
 | 
			
		||||
      semver: 7.5.4
 | 
			
		||||
      semver: 7.6.0
 | 
			
		||||
      vscode-languageserver-protocol: 3.17.5
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -19900,8 +19782,8 @@ packages:
 | 
			
		|||
    resolution: {integrity: sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==}
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /vue-component-type-helpers@2.0.7:
 | 
			
		||||
    resolution: {integrity: sha512-7e12Evdll7JcTIocojgnCgwocX4WzIYStGClBQ+QuWPinZo/vQolv2EMq4a3lg16TKfwWafLimG77bxb56UauA==}
 | 
			
		||||
  /vue-component-type-helpers@2.0.12:
 | 
			
		||||
    resolution: {integrity: sha512-iVJugClQdu3ZyF0N4CF3Egi+gWYfnxlIPPGtFXZG29rF3kQIuziP+k7rVGCCHiibIOQ1SlspKjrh+LRYzMpwTA==}
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /vue-demi@0.14.7(vue@3.4.21):
 | 
			
		||||
| 
						 | 
				
			
			@ -20441,10 +20323,10 @@ packages:
 | 
			
		|||
    resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  '@github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.5/aiscript-dev-aiscript-languageserver-0.1.5.tgz':
 | 
			
		||||
    resolution: {tarball: https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.5/aiscript-dev-aiscript-languageserver-0.1.5.tgz}
 | 
			
		||||
  '@github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz':
 | 
			
		||||
    resolution: {tarball: https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz}
 | 
			
		||||
    name: '@aiscript-dev/aiscript-languageserver'
 | 
			
		||||
    version: 0.1.5
 | 
			
		||||
    version: 0.1.6
 | 
			
		||||
    hasBin: true
 | 
			
		||||
    dependencies:
 | 
			
		||||
      seedrandom: 3.0.5
 | 
			
		||||
| 
						 | 
				
			
			@ -20454,13 +20336,13 @@ packages:
 | 
			
		|||
      vscode-languageserver-textdocument: 1.0.11
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  github.com/aiscript-dev/aiscript-vscode/793211d40243c8775f6b85f015c221c82cbffb07:
 | 
			
		||||
    resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/793211d40243c8775f6b85f015c221c82cbffb07}
 | 
			
		||||
  github.com/aiscript-dev/aiscript-vscode/3f79d6f0550369267220aa67702287948d885424:
 | 
			
		||||
    resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/3f79d6f0550369267220aa67702287948d885424}
 | 
			
		||||
    name: aiscript-vscode
 | 
			
		||||
    version: 0.1.2
 | 
			
		||||
    version: 0.1.4
 | 
			
		||||
    engines: {vscode: ^1.83.0}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@aiscript-dev/aiscript-languageserver': '@github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.5/aiscript-dev-aiscript-languageserver-0.1.5.tgz'
 | 
			
		||||
      '@aiscript-dev/aiscript-languageserver': '@github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz'
 | 
			
		||||
      vscode-languageclient: 9.0.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,41 +16,41 @@ await execa('pnpm', ['clean'], {
 | 
			
		|||
	stderr: process.stderr,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
await execa('pnpm', ['build-pre'], {
 | 
			
		||||
await Promise.all([
 | 
			
		||||
	execa('pnpm', ['build-pre'], {
 | 
			
		||||
		cwd: _dirname + '/../',
 | 
			
		||||
		stdout: process.stdout,
 | 
			
		||||
		stderr: process.stderr,
 | 
			
		||||
});
 | 
			
		||||
	}),
 | 
			
		||||
	execa('pnpm', ['build-assets'], {
 | 
			
		||||
		cwd: _dirname + '/../',
 | 
			
		||||
		stdout: process.stdout,
 | 
			
		||||
		stderr: process.stderr,
 | 
			
		||||
	}),
 | 
			
		||||
	execa('pnpm', ['--filter', 'misskey-js', 'build'], {
 | 
			
		||||
		cwd: _dirname + '/../',
 | 
			
		||||
		stdout: process.stdout,
 | 
			
		||||
		stderr: process.stderr,
 | 
			
		||||
	}),
 | 
			
		||||
	execa('pnpm', ['--filter', 'megalodon', 'build'], {
 | 
			
		||||
		cwd: _dirname + '/../',
 | 
			
		||||
		stdout: process.stdout,
 | 
			
		||||
		stderr: process.stderr,
 | 
			
		||||
	}),
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
await execa('pnpm', ['build-assets'], {
 | 
			
		||||
await Promise.all([
 | 
			
		||||
	execa('pnpm', ['--filter', 'misskey-reversi', 'build'], {
 | 
			
		||||
		cwd: _dirname + '/../',
 | 
			
		||||
		stdout: process.stdout,
 | 
			
		||||
		stderr: process.stderr,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
await execa('pnpm', ['--filter', 'misskey-js', 'ts'], {
 | 
			
		||||
	}),
 | 
			
		||||
	execa('pnpm', ['--filter', 'misskey-bubble-game', 'build'], {
 | 
			
		||||
		cwd: _dirname + '/../',
 | 
			
		||||
		stdout: process.stdout,
 | 
			
		||||
		stderr: process.stderr,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
await execa("pnpm", ['--filter', 'megalodon', 'build'], {
 | 
			
		||||
	cwd: _dirname + '/../',
 | 
			
		||||
	stdout: process.stdout,
 | 
			
		||||
	stderr: process.stderr,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
await execa('pnpm', ['--filter', 'misskey-reversi', 'build:tsc'], {
 | 
			
		||||
	cwd: _dirname + '/../',
 | 
			
		||||
	stdout: process.stdout,
 | 
			
		||||
	stderr: process.stderr,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
await execa('pnpm', ['--filter', 'misskey-bubble-game', 'build:tsc'], {
 | 
			
		||||
	cwd: _dirname + '/../',
 | 
			
		||||
	stdout: process.stdout,
 | 
			
		||||
	stderr: process.stderr,
 | 
			
		||||
});
 | 
			
		||||
	}),
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
execa('pnpm', ['build-pre', '--watch'], {
 | 
			
		||||
	cwd: _dirname + '/../',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue