Compare commits

..

26 Commits

Author SHA1 Message Date
syuilo 24b4586ee6 New translations ja-jp.yml (German) 2025-06-08 23:08:23 +09:00
syuilo 25c4143d5c New translations ja-jp.yml (German) 2025-06-08 21:53:21 +09:00
syuilo 7f504f9ec1 New translations ja-jp.yml (German) 2025-06-08 20:57:23 +09:00
syuilo e5c3868d3e New translations ja-jp.yml (Catalan) 2025-06-08 17:40:17 +09:00
syuilo 366f24176e New translations ja-jp.yml (Korean) 2025-06-08 13:45:50 +09:00
syuilo 2f7a47e53c New translations ja-jp.yml (Spanish) 2025-06-08 00:11:35 +09:00
syuilo 77608a70f6 New translations ja-jp.yml (Chinese Simplified) 2025-06-07 20:27:36 +09:00
syuilo e854e95d1c New translations ja-jp.yml (Chinese Traditional) 2025-06-07 18:08:40 +09:00
syuilo b57019e4ab New translations ja-jp.yml (Korean) 2025-06-06 12:37:08 +09:00
syuilo 81182b613c New translations ja-jp.yml (Chinese Traditional) 2025-06-06 11:14:18 +09:00
syuilo 0da3d34825 New translations ja-jp.yml (English) 2025-06-06 02:18:03 +09:00
syuilo 598e764123 New translations ja-jp.yml (Chinese Simplified) 2025-06-06 02:18:01 +09:00
syuilo dbea1b4d70 New translations ja-jp.yml (Catalan) 2025-06-06 02:18:00 +09:00
syuilo e5a717286c New translations ja-jp.yml (Spanish) 2025-06-06 02:17:58 +09:00
syuilo 07c5dc46b0 New translations ja-jp.yml (Chinese Simplified) 2025-06-06 01:22:13 +09:00
syuilo 89e8020d51 New translations ja-jp.yml (Italian) 2025-06-06 00:12:59 +09:00
syuilo 25fcdcb5ea New translations ja-jp.yml (English) 2025-06-05 20:59:13 +09:00
syuilo 573887c831 New translations ja-jp.yml (Spanish) 2025-06-05 20:58:51 +09:00
syuilo c749af4752 New translations ja-jp.yml (Chinese Traditional) 2025-06-05 18:18:53 +09:00
syuilo 43821c1920 New translations ja-jp.yml (English) 2025-06-05 07:27:05 +09:00
syuilo ddb440e0fa New translations ja-jp.yml (Spanish) 2025-06-05 07:27:04 +09:00
syuilo 4cc44e869c New translations ja-jp.yml (Spanish) 2025-06-05 05:46:25 +09:00
syuilo c6285b7569 New translations ja-jp.yml (Catalan) 2025-06-05 04:16:50 +09:00
syuilo de158dcd7a New translations ja-jp.yml (Korean) 2025-06-04 23:54:55 +09:00
syuilo 289783aa50 New translations ja-jp.yml (Korean) 2025-06-04 22:39:59 +09:00
syuilo 41c8788b9a New translations ja-jp.yml (Chinese Simplified) 2025-06-04 19:02:14 +09:00
51 changed files with 33076 additions and 38791 deletions

View File

@ -26,8 +26,6 @@ jobs:
with:
node-version-file: '.node-version'
cache: 'pnpm'
# see https://docs.github.com/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry
registry-url: 'https://registry.npmjs.org'
- name: Publish package
run: |
pnpm i --frozen-lockfile

View File

@ -1,8 +1,5 @@
## 2025.6.1
### Note
- Misskey Webプラグインのnote_view_interruptorは不具合の影響により現在一時的に無効化されています。
### General
-
@ -15,12 +12,9 @@
- Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正
- Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正
- Fix: タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正
- Fix: Plugin:register_note_view_interruptor()によるノートの書き換えが機能しない問題を修正
### Server
- Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all)
- Fix: アカウント削除が正常に行われないことがあった問題を修正
- Fix: outboxのページネーションが正しく行われない問題を修正
## 2025.6.0

View File

@ -34,6 +34,7 @@ describe('Before setup instance', () => {
cy.intercept('POST', '/api/admin/update-meta').as('update-meta');
cy.get('[data-cy-next]').click();
cy.get('[data-cy-next]').click();
cy.get('[data-cy-server-name] input').type('Testskey');
cy.get('[data-cy-server-setup-wizard-apply]').click();

12
locales/index.d.ts vendored
View File

@ -9584,14 +9584,6 @@ export interface Locale extends ILocale {
"disableFederationDescription": string;
};
"_postForm": {
/**
*
*/
"quitInspiteOfThereAreUnuploadedFilesConfirm": string;
/**
* 稿
*/
"uploaderTip": string;
/**
* ...
*/
@ -12184,10 +12176,6 @@ export interface Locale extends ILocale {
*
*/
"grayscale": string;
/**
* 調
*/
"colorAdjust": string;
/**
*
*/

View File

@ -2522,8 +2522,6 @@ _visibility:
disableFederationDescription: "他サーバーへの配信を行いません"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされていないファイルがありますが、破棄してフォームを閉じますか?"
uploaderTip: "ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。"
replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..."
channelPlaceholder: "チャンネルに投稿..."
@ -3264,7 +3262,6 @@ _imageEffector:
mirror: "ミラー"
invert: "色の反転"
grayscale: "白黒"
colorAdjust: "色調補正"
colorClamp: "色の圧縮"
colorClampAdvanced: "色の圧縮(高度)"
distort: "歪み"

View File

@ -220,7 +220,6 @@ silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี
mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้"
operations: "ดำเนินการ"
software: "ซอฟต์แวร์"
softwareName: "ชื่อซอฟต์แวร์"
version: "เวอร์ชั่น"
metadata: "Metadata"
withNFiles: "{n} ไฟล์"
@ -1294,10 +1293,6 @@ federationDisabled: "เซิร์ฟเวอร์นี้ปิดกา
reactAreYouSure: "คุณต้องการที่จะตอบสนองต่อ \" {emoji}\" หรือไม่?"
markAsSensitiveConfirm: "คุณต้องการทำเครื่องหมายสื่อนี้ว่าละเอียดอ่อนหรือไม่?"
unmarkAsSensitiveConfirm: "คุณต้องการลบการกำหนดความไวของสื่อนี้หรือไม่?"
preferences: "การตั้งค่าสภาพแวดล้อม"
preferencesProfile: "โปรไฟล์การกำหนดค่า"
preferenceSyncConflictTitle: "การตั้งค่ามีอยู่บนเซิร์ฟเวอร์"
preferenceSyncConflictText: "รายการการตั้งค่าที่เปิดใช้งานการซิงโครไนซ์จะจัดเก็บค่าไว้บนเซิร์ฟเวอร์ และพบค่าที่จัดเก็บบนเซิร์ฟเวอร์สำหรับรายการการตั้งค่านี้ คุณต้องการทำอย่างไร?"
postForm: "แบบฟอร์มการโพสต์"
information: "เกี่ยวกับ"
right: "ขวา"
@ -1310,7 +1305,6 @@ _chat:
send: "ส่ง"
_settings:
webhook: "Webhook"
preferencesBanner: "คุณสามารถกำหนดค่าพฤติกรรมโดยรวมของไคลเอนต์ได้ตามความต้องการของคุณ"
_accountSettings:
requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา"
requireSigninToViewContentsDescription1: "ต้องเข้าสู่ระบบเพื่อดูบันทึกและเนื้อหาอื่น ๆ ทั้งหมดที่คุณสร้าง คาดว่าจะมีประสิทธิผลในการป้องกันไม่ให้ข้อมูลถูกเก็บรวบรวมโดยโปรแกรมรวบรวมข้อมูล"

View File

@ -220,7 +220,6 @@ silenceThisInstance: "Máy chủ im lặng"
mediaSilenceThisInstance: "Tắt nội dung đa phương tiện từ máy chủ này"
operations: "Vận hành"
software: "Phần mềm"
softwareName: "Tên phần mềm"
version: "Phiên bản"
metadata: "Metadata"
withNFiles: "{n} tập tin"
@ -1212,9 +1211,6 @@ federationDisabled: "Liên kết bị vô hiệu hóa trên máy chủ này. B
reactAreYouSure: "Bạn có muốn phản hồi với \" {emoji} \" không?"
preferences: "Thiết lập môi trường"
accessibility: "Khả năng tiếp cận"
preferencesProfile: "Hồ sơ sở thích"
preferenceSyncConflictTitle: "Cài đặt tồn tại trên máy chủ"
preferenceSyncConflictText: "Các thiết lập đồng bộ hóa được bật sẽ lưu các giá trị của chúng vào máy chủ. Tuy nhiên, có những giá trị hiện có trên máy chủ. Bạn muốn ghi đè lên bộ giá trị nào?"
paste: "dán"
postForm: "Mẫu đăng"
information: "Giới thiệu"
@ -1227,8 +1223,6 @@ _chat:
members: "Thành viên"
home: "Trang chính"
send: "Gửi"
_settings:
preferencesBanner: "Bạn có thể cấu hình hành vi chung của máy khách theo sở thích của mình."
_accountSettings:
requireSigninToViewContents: "Yêu cầu đăng nhập để xem nội dung"
requireSigninToViewContentsDescription1: "Yêu cầu đăng nhập để xem tất cả ghi chú và nội dung khác mà bạn tạo. Điều này được kỳ vọng sẽ có hiệu quả trong việc ngăn chặn thông tin bị thu thập bởi các trình thu thập thông tin."

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.6.1-beta.1",
"version": "2025.6.1-alpha.1",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -37,17 +37,17 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.12.0",
"@swc/core-darwin-x64": "1.12.0",
"@swc/core-darwin-arm64": "1.11.29",
"@swc/core-darwin-x64": "1.11.29",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.12.0",
"@swc/core-linux-arm64-gnu": "1.12.0",
"@swc/core-linux-arm64-musl": "1.12.0",
"@swc/core-linux-x64-gnu": "1.12.0",
"@swc/core-linux-x64-musl": "1.12.0",
"@swc/core-win32-arm64-msvc": "1.12.0",
"@swc/core-win32-ia32-msvc": "1.12.0",
"@swc/core-win32-x64-msvc": "1.12.0",
"@swc/core-linux-arm-gnueabihf": "1.11.29",
"@swc/core-linux-arm64-gnu": "1.11.29",
"@swc/core-linux-arm64-musl": "1.11.29",
"@swc/core-linux-x64-gnu": "1.11.29",
"@swc/core-linux-x64-musl": "1.11.29",
"@swc/core-win32-arm64-msvc": "1.11.29",
"@swc/core-win32-ia32-msvc": "1.11.29",
"@swc/core-win32-x64-msvc": "1.11.29",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9",
@ -67,8 +67,8 @@
"utf-8-validate": "6.0.5"
},
"dependencies": {
"@aws-sdk/client-s3": "3.826.0",
"@aws-sdk/lib-storage": "3.826.0",
"@aws-sdk/client-s3": "3.817.0",
"@aws-sdk/lib-storage": "3.817.0",
"@discordapp/twemoji": "15.1.0",
"@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2",
@ -80,10 +80,10 @@
"@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.1",
"@napi-rs/canvas": "0.1.71",
"@nestjs/common": "11.1.3",
"@nestjs/core": "11.1.3",
"@nestjs/testing": "11.1.3",
"@napi-rs/canvas": "0.1.70",
"@nestjs/common": "11.1.2",
"@nestjs/core": "11.1.2",
"@nestjs/testing": "11.1.2",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "8.55.0",
"@sentry/profiling-node": "8.55.0",
@ -91,7 +91,7 @@
"@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.7.7",
"@swc/core": "1.12.0",
"@swc/core": "1.11.29",
"@twemoji/parser": "15.1.1",
"@types/redis-info": "3.0.3",
"accepts": "1.3.8",
@ -101,7 +101,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.53.2",
"bullmq": "5.53.0",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.4.1",
@ -117,7 +117,7 @@
"feed": "4.2.2",
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.3",
"form-data": "4.0.2",
"got": "14.4.7",
"happy-dom": "16.8.1",
"hpagent": "1.2.0",
@ -133,9 +133,9 @@
"jsonld": "8.3.3",
"jsrsasign": "11.1.0",
"juice": "11.0.1",
"meilisearch": "0.51.0",
"meilisearch": "0.50.0",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.3",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
@ -188,16 +188,16 @@
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@nestjs/platform-express": "10.4.19",
"@sentry/vue": "9.28.0",
"@nestjs/platform-express": "10.4.18",
"@sentry/vue": "9.22.0",
"@simplewebauthn/types": "12.0.0",
"@swc/jest": "0.2.38",
"@types/accepts": "1.3.7",
"@types/archiver": "6.0.3",
"@types/bcryptjs": "2.4.6",
"@types/body-parser": "1.19.6",
"@types/body-parser": "1.19.5",
"@types/color-convert": "2.0.4",
"@types/content-disposition": "0.5.9",
"@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.27",
"@types/htmlescape": "1.1.3",
"@types/http-link-header": "1.0.7",
@ -208,12 +208,12 @@
"@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "22.15.31",
"@types/node": "22.15.21",
"@types/nodemailer": "6.4.17",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.15.4",
"@types/pg": "8.15.2",
"@types/pug": "2.0.10",
"@types/qrcode": "1.5.5",
"@types/random-seed": "0.3.5",
@ -229,8 +229,8 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0",

View File

@ -803,14 +803,14 @@ export class DriveService {
await Promise.all(promises);
}
await this.deletePostProcess(file, isExpired, deleter);
this.deletePostProcess(file, isExpired, deleter);
}
@bindThis
private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
// リモートファイル期限切れ削除後は直リンクにする
if (isExpired && file.userHost !== null && file.uri != null) {
await this.driveFilesRepository.update(file.id, {
this.driveFilesRepository.update(file.id, {
isLink: true,
url: file.uri,
thumbnailUrl: null,
@ -822,7 +822,7 @@ export class DriveService {
webpublicAccessKey: 'webpublic-' + randomUUID(),
});
} else {
await this.driveFilesRepository.delete(file.id);
this.driveFilesRepository.delete(file.id);
}
this.driveChart.update(file, false);

View File

@ -482,19 +482,9 @@ export class ActivityPubServerService {
return true;
},
dbFallback: async (untilId, sinceId, limit) => {
return await this.getUserNotesFromDb({
untilId,
sinceId,
limit,
userId: user.id,
});
return await this.getUserNotesFromDb(sinceId, untilId, limit, user.id);
},
}) : await this.getUserNotesFromDb({
untilId: untilId ?? null,
sinceId: sinceId ?? null,
limit,
userId: user.id,
});
}) : await this.getUserNotesFromDb(sinceId ?? null, untilId ?? null, limit, user.id);
if (sinceId) notes.reverse();
@ -533,21 +523,16 @@ export class ActivityPubServerService {
}
@bindThis
private async getUserNotesFromDb(ps: {
untilId: string | null,
sinceId: string | null,
limit: number,
userId: MiUser['id'],
}) {
return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere('note.userId = :userId', { userId: ps.userId })
private async getUserNotesFromDb(untilId: string | null, sinceId: string | null, limit: number, userId: MiUser['id']) {
return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
.andWhere('note.userId = :userId', { userId })
.andWhere(new Brackets(qb => {
qb
.where('note.visibility = \'public\'')
.orWhere('note.visibility = \'home\'');
}))
.andWhere('note.localOnly = FALSE')
.limit(ps.limit)
.limit(limit)
.getMany();
}

View File

@ -380,7 +380,9 @@ describe('User', () => {
strictEqual(followers.length, 1); // followed by Bob
await alice.client.request('i/delete-account', { password: alice.password });
await sleep();
// NOTE: user deletion query is slow
// FIXME: ensure user is removed successfully
await sleep(10000);
const following = await bob.client.request('users/following', { userId: bob.id });
strictEqual(following.length, 0); // no following relation
@ -478,7 +480,9 @@ describe('User', () => {
strictEqual(followers.length, 1); // followed by Bob
await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
await sleep();
// NOTE: user deletion query is slow
// FIXME: ensure user is removed successfully
await sleep(10000);
const following = await bob.client.request('users/following', { userId: bob.id });
strictEqual(following.length, 0); // no following relation

View File

@ -26,9 +26,9 @@
"mfm-js": "0.24.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.42.0",
"sass": "1.89.2",
"shiki": "3.6.0",
"rollup": "4.41.1",
"sass": "1.89.0",
"shiki": "3.4.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
@ -39,27 +39,27 @@
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.1",
"@tabler/icons-webfont": "3.34.0",
"@tabler/icons-webfont": "3.33.0",
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/estree": "1.0.7",
"@types/micromatch": "4.0.9",
"@types/node": "22.15.31",
"@types/node": "22.15.28",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"@vitest/coverage-v8": "3.2.3",
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"@vitest/coverage-v8": "3.1.4",
"@vue/runtime-core": "3.5.16",
"acorn": "8.15.0",
"acorn": "8.14.1",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "10.2.0",
"eslint-plugin-vue": "10.1.0",
"fast-glob": "3.3.3",
"happy-dom": "17.6.3",
"happy-dom": "17.5.6",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.10.2",
"msw": "2.8.6",
"nodemon": "3.1.10",
"prettier": "3.5.3",
"start-server-and-test": "2.0.12",

View File

@ -21,11 +21,11 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"@types/node": "22.15.28",
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"esbuild": "0.25.5",
"eslint-plugin-vue": "10.2.0",
"eslint-plugin-vue": "10.1.0",
"nodemon": "3.1.10",
"typescript": "5.8.3",
"vue-eslint-parser": "10.1.3"

View File

@ -24,7 +24,7 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.1.4",
"@sentry/vue": "9.27.0",
"@sentry/vue": "9.24.0",
"@syuilo/aiscript": "0.19.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.4",
@ -60,10 +60,10 @@
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode.js": "2.3.1",
"rollup": "4.42.0",
"rollup": "4.41.1",
"sanitize-html": "2.17.0",
"sass": "1.89.2",
"shiki": "3.6.0",
"sass": "1.89.0",
"shiki": "3.4.2",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.177.0",
@ -98,36 +98,36 @@
"@storybook/types": "8.6.14",
"@storybook/vue3": "8.6.14",
"@storybook/vue3-vite": "8.6.14",
"@tabler/icons-webfont": "3.34.0",
"@tabler/icons-webfont": "3.33.0",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8",
"@types/estree": "1.0.7",
"@types/matter-js": "0.19.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.15.31",
"@types/node": "22.15.28",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"@vitest/coverage-v8": "3.2.3",
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"@vitest/coverage-v8": "3.1.4",
"@vue/compiler-core": "3.5.16",
"@vue/runtime-core": "3.5.16",
"acorn": "8.15.0",
"acorn": "8.14.1",
"cross-env": "7.0.3",
"cypress": "14.4.1",
"cypress": "14.4.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "10.2.0",
"eslint-plugin-vue": "10.1.0",
"fast-glob": "3.3.3",
"happy-dom": "17.6.3",
"happy-dom": "17.5.6",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.0.1",
"msw": "2.10.2",
"msw-storybook-addon": "2.0.5",
"msw": "2.8.6",
"msw-storybook-addon": "2.0.4",
"nodemon": "3.1.10",
"prettier": "3.5.3",
"react": "19.1.0",
@ -137,7 +137,7 @@
"storybook": "8.6.14",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.3",
"vitest": "3.1.4",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "2.2.10",
"vue-eslint-parser": "10.1.3",

View File

@ -16,54 +16,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root" class="_gaps">
<div v-for="[k, v] in Object.entries(fx.params)" :key="k">
<MkSwitch
v-if="v.type === 'boolean'"
v-model="layer.params[k]"
>
<MkSwitch v-if="v.type === 'boolean'" v-model="layer.params[k]">
<template #label>{{ k }}</template>
</MkSwitch>
<MkRange
v-else-if="v.type === 'number'"
v-model="layer.params[k]"
continuousUpdate
:min="v.min"
:max="v.max"
:step="v.step"
@thumbDoubleClicked="() => {
if (fx.params[k].default != null) {
layer.params[k] = fx.params[k].default;
} else {
layer.params[k] = v.min;
}
}"
>
<MkRange v-else-if="v.type === 'number'" v-model="layer.params[k]" continuousUpdate :min="v.min" :max="v.max" :step="v.step">
<template #label>{{ k }}</template>
</MkRange>
<MkRadios
v-else-if="v.type === 'number:enum'"
v-model="layer.params[k]"
>
<MkRadios v-else-if="v.type === 'number:enum'" v-model="layer.params[k]">
<template #label>{{ k }}</template>
<option v-for="item in v.enum" :value="item.value">{{ item.label }}</option>
</MkRadios>
<div v-else-if="v.type === 'seed'">
<MkRange
v-model="layer.params[k]"
continuousUpdate
type="number"
:min="0"
:max="10000"
:step="1"
>
<MkRange v-model="layer.params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">
<template #label>{{ k }}</template>
</MkRange>
</div>
<MkInput
v-else-if="v.type === 'color'"
:modelValue="getHex(layer.params[k])"
type="color"
@update:modelValue="v => { const c = getRgb(v); if (c != null) layer.params[k] = c; }"
>
<MkInput v-else-if="v.type === 'color'" :modelValue="`#${(layer.params[k][0] * 255).toString(16).padStart(2, '0')}${(layer.params[k][1] * 255).toString(16).padStart(2, '0')}${(layer.params[k][2] * 255).toString(16).padStart(2, '0')}`" type="color" @update:modelValue="v => { const c = v.slice(1).match(/.{2}/g)?.map(x => parseInt(x, 16) / 255); if (c) layer.params[k] = c; }">
<template #label>{{ k }}</template>
</MkInput>
</div>
@ -72,14 +40,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue';
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { i18n } from '@/i18n.js';
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import FormSlot from '@/components/form/slot.vue';
import MkPositionSelector from '@/components/MkPositionSelector.vue';
import * as os from '@/os.js';
import { selectFile } from '@/utility/drive.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js';
import { FXS } from '@/utility/image-effector/fxs.js';
const layer = defineModel<ImageEffectorLayer>('layer', { required: true });
@ -93,24 +69,6 @@ const emit = defineEmits<{
(e: 'swapUp'): void;
(e: 'swapDown'): void;
}>();
function getHex(c: [number, number, number]) {
return `#${c.map(x => (x * 255).toString(16).padStart(2, '0')).join('')}`;
}
function getRgb(hex: string | number): [number, number, number] | null {
if (
typeof hex === 'number' ||
typeof hex !== 'string' ||
!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)
) {
return null;
}
const m = hex.slice(1).match(/[0-9a-fA-F]{2}/g);
if (m == null) return [0, 0, 0];
return m.map(x => parseInt(x, 16) / 255) as [number, number, number];
}
</script>
<style module>

View File

@ -265,21 +265,21 @@ const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', nul
let note = deepClone(props.note);
// Transition
// https://github.com/aiscript-dev/aiscript/issues/937
//// plugin
//const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
//if (noteViewInterruptors.length > 0) {
// let result: Misskey.entities.Note | null = deepClone(note);
// for (const interruptor of noteViewInterruptors) {
// try {
// result = await interruptor.handler(result!) as Misskey.entities.Note | null;
// } catch (err) {
// console.error(err);
// }
// }
// note = result as Misskey.entities.Note;
//}
// plugin
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result: Misskey.entities.Note | null = deepClone(note);
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
} catch (err) {
console.error(err);
}
}
note = result as Misskey.entities.Note;
});
}
const isRenote = Misskey.note.isPureRenote(note);
const appearNote = getAppearNote(note);

View File

@ -286,20 +286,21 @@ const inChannel = inject('inChannel', null);
let note = deepClone(props.note);
// Transition
//// plugin
//const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
//if (noteViewInterruptors.length > 0) {
// let result: Misskey.entities.Note | null = deepClone(note);
// for (const interruptor of noteViewInterruptors) {
// try {
// result = await interruptor.handler(result!) as Misskey.entities.Note | null;
// } catch (err) {
// console.error(err);
// }
// }
// note = result as Misskey.entities.Note;
//}
// plugin
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result: Misskey.entities.Note | null = deepClone(note);
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
} catch (err) {
console.error(err);
}
}
note = result as Misskey.entities.Note;
});
}
const isRenote = Misskey.note.isPureRenote(note);
const appearNote = getAppearNote(note);

View File

@ -72,29 +72,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<div v-if="uploader.items.value.length > 0" style="padding: 12px;">
<MkTip k="postFormUploader">
{{ i18n.ts._postForm.uploaderTip }}
</MkTip>
<MkUploaderItems :items="uploader.items.value" @showMenu="(item, ev) => showPerUploadItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerUploadItemMenuViaContextmenu(item, ev)"/>
</div>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
<div v-if="showingOptions" style="padding: 8px 16px;">
</div>
<footer :class="$style.footer">
<div :class="$style.footerLeft">
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.upload + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromPc"><i class="ti ti-photo-plus"></i></button>
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.fromDrive + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromDrive"><i class="ti ti-cloud-download"></i></button>
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
</div>
<div :class="$style.footerRight">
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
<!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button>-->
</div>
</footer>
<datalist id="hashtags">
@ -110,12 +105,10 @@ import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
import { toASCII } from 'punycode.js';
import { host, url } from '@@/js/config.js';
import MkUploaderItems from './MkUploaderItems.vue';
import type { ShallowRef } from 'vue';
import type { PostFormProps } from '@/types/post-form.js';
import type { MenuItem } from '@/types/menu.js';
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
import type { UploaderItem } from '@/composables/use-uploader.js';
import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
import XTextCounter from '@/components/MkPostForm.TextCounter.vue';
@ -127,7 +120,7 @@ import { formatTimeString } from '@/utility/format-time-string.js';
import { Autocomplete } from '@/utility/autocomplete.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { chooseDriveFile } from '@/utility/drive.js';
import { selectFile } from '@/utility/drive.js';
import { store } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
@ -145,7 +138,6 @@ import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js';
import { globalEvents } from '@/events.js';
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
import { useUploader } from '@/composables/use-uploader.js';
const $i = ensureSignin();
@ -209,15 +201,6 @@ const justEndedComposition = ref(false);
const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote);
const postFormActions = getPluginHandlers('post_form_action');
const uploader = useUploader({
multiple: true,
});
uploader.events.on('itemUploaded', ctx => {
files.value.push(ctx.item.uploaded!);
uploader.removeItem(ctx.item);
});
const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : '';
@ -275,11 +258,10 @@ const cwTextLength = computed((): number => {
const maxCwTextLength = 100;
const canPost = computed((): boolean => {
return !props.mock && !posting.value && !posted.value && !uploader.uploading.value && (uploader.items.value.length === 0 || uploader.readyForUpload.value) &&
return !props.mock && !posting.value && !posted.value &&
(
1 <= textLength.value ||
1 <= files.value.length ||
1 <= uploader.items.value.length ||
poll.value != null ||
renoteTargetNote.value != null ||
quoteId.value != null
@ -452,20 +434,17 @@ function focus() {
}
}
function chooseFileFromPc(ev: MouseEvent) {
function chooseFileFrom(ev) {
if (props.mock) return;
os.chooseFileFromPc({ multiple: true }).then(files => {
if (files.length === 0) return;
uploader.addFiles(files);
});
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: true,
label: i18n.ts.attachFile,
}).then(files_ => {
for (const file of files_) {
files.value.push(file);
}
function chooseFileFromDrive(ev: MouseEvent) {
if (props.mock) return;
chooseDriveFile({ multiple: true }).then(driveFiles => {
files.value.push(...driveFiles);
});
}
@ -592,11 +571,6 @@ function showOtherSettings() {
toggleReactionAcceptance();
},
}, { type: 'divider' }, {
type: 'switch',
icon: 'ti ti-eye',
text: i18n.ts.preview,
ref: showPreview,
}, {
icon: 'ti ti-trash',
text: i18n.ts.reset,
danger: true,
@ -823,15 +797,6 @@ function isAnnoying(text: string): boolean {
text.includes('$[position');
}
async function uploadFiles() {
await uploader.upload();
for (const uploadedItem of uploader.items.value.filter(x => x.uploaded != null)) {
files.value.push(uploadedItem.uploaded!);
uploader.removeItem(uploadedItem);
}
}
async function post(ev?: MouseEvent) {
if (ev) {
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
@ -875,10 +840,6 @@ async function post(ev?: MouseEvent) {
}
}
if (uploader.items.value.some(x => x.uploaded == null)) {
await uploadFiles();
}
let postData = {
text: text.value === '' ? null : text.value,
fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
@ -1082,16 +1043,6 @@ function openAccountMenu(ev: MouseEvent) {
}, ev);
}
function showPerUploadItemMenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.contextMenu(menu, ev);
}
onMounted(() => {
if (props.autofocus) {
focus();
@ -1160,23 +1111,8 @@ onMounted(() => {
});
});
async function canClose() {
if (!uploader.allItemsUploaded.value) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts._postForm.quitInspiteOfThereAreUnuploadedFilesConfirm,
okText: i18n.ts.yes,
cancelText: i18n.ts.no,
});
if (canceled) return false;
}
return true;
}
defineExpose({
clear,
canClose,
});
</script>

View File

@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkModal
ref="modal"
:preferType="'dialog'"
@click="_close()"
@click="modal?.close()"
@closed="onModalClosed()"
@esc="_close()"
@esc="modal?.close()"
>
<MkPostForm
ref="form"
@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
autofocus
freezeAfterPosted
@posted="onPosted"
@cancel="_close()"
@esc="_close()"
@cancel="modal?.close()"
@esc="modal?.close()"
/>
</MkModal>
</template>
@ -43,7 +43,6 @@ const emit = defineEmits<{
}>();
const modal = useTemplateRef('modal');
const form = useTemplateRef('form');
function onPosted() {
modal.value?.close({
@ -51,12 +50,6 @@ function onPosted() {
});
}
async function _close() {
const canClose = await form.value?.canClose();
if (!canClose) return;
modal.value?.close();
}
function onModalClosed() {
emit('closed');
}

View File

@ -12,10 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<slot name="prefix"></slot>
<div ref="containerEl" class="container">
<div class="track">
<div class="highlight right" :style="{ width: rightTrackWidth, left: rightTrackPosition }">
<div class="highlight right" :style="{ width: ((steppedRawValue - minRatio) * 100) + '%', left: (Math.abs(Math.min(0, min)) / (max + Math.abs(Math.min(0, min)))) * 100 + '%' }">
<div class="shine right"></div>
</div>
<div class="highlight left" :style="{ width: leftTrackWidth, left: leftTrackPosition }">
<div class="highlight left" :style="{ width: ((minRatio - steppedRawValue) * 100) + '%', left: (steppedRawValue) * 100 + '%' }">
<div class="shine left"></div>
</div>
</div>
@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, onMounted, onUnmounted, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue';
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
import { isTouchUsing } from '@/utility/touch.js';
import * as os from '@/os.js';
@ -58,14 +58,13 @@ const props = withDefaults(defineProps<{
continuousUpdate?: boolean;
}>(), {
step: 1,
textConverter: (v: number) => (Math.round(v * 1000) / 1000).toString(),
textConverter: (v) => v.toString(),
easing: false,
});
const emit = defineEmits<{
(ev: 'update:modelValue', value: number): void;
(ev: 'dragEnded', value: number): void;
(ev: 'thumbDoubleClicked'): void;
}>();
const containerEl = useTemplateRef('containerEl');
@ -74,24 +73,7 @@ const thumbEl = useTemplateRef('thumbEl');
const maxRatio = computed(() => Math.abs(props.max) / (props.max + Math.abs(Math.min(0, props.min))));
const minRatio = computed(() => Math.abs(Math.min(0, props.min)) / (props.max + Math.abs(Math.min(0, props.min))));
const rightTrackWidth = computed(() => {
return Math.max(0, (steppedRawValue.value - minRatio.value) * 100) + '%';
});
const leftTrackWidth = computed(() => {
return Math.max(0, (minRatio.value - steppedRawValue.value) * 100) + '%';
});
const rightTrackPosition = computed(() => {
return (Math.abs(Math.min(0, props.min)) / (props.max + Math.abs(Math.min(0, props.min)))) * 100 + '%';
});
const leftTrackPosition = computed(() => {
return (Math.min(minRatio.value, steppedRawValue.value) * 100) + '%';
});
const calcRawValue = (value: number) => {
return (value - props.min) / (props.max - props.min);
};
const rawValue = ref(calcRawValue(props.modelValue));
const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
const steppedRawValue = computed(() => {
if (props.step) {
const step = props.step / (props.max - props.min);
@ -121,11 +103,6 @@ const calcThumbPosition = () => {
}
};
watch([steppedRawValue, containerEl], calcThumbPosition);
watch(() => props.modelValue, (newVal) => {
const newRawValue = calcRawValue(newVal);
if (rawValue.value === newRawValue) return;
rawValue.value = newRawValue;
});
let ro: ResizeObserver | undefined;
@ -151,12 +128,6 @@ const steps = computed(() => {
const tooltipForDragShowing = ref(false);
const tooltipForHoverShowing = ref(false);
onBeforeUnmount(() => {
//
tooltipForDragShowing.value = false;
tooltipForHoverShowing.value = false;
});
function onMouseenter() {
if (isTouchUsing) return;
@ -167,7 +138,7 @@ function onMouseenter() {
text: computed(() => {
return props.textConverter(finalValue.value);
}),
targetElement: thumbEl.value ?? undefined,
targetElement: thumbEl,
}, {
closed: () => dispose(),
});
@ -177,8 +148,6 @@ function onMouseenter() {
}, { once: true, passive: true });
}
let lastClickTime: number | null = null;
function onMousedown(ev: MouseEvent | TouchEvent) {
ev.preventDefault();
@ -189,7 +158,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
text: computed(() => {
return props.textConverter(finalValue.value);
}),
targetElement: thumbEl.value ?? undefined,
targetElement: thumbEl,
}, {
closed: () => dispose(),
});
@ -234,20 +203,6 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
window.addEventListener('touchmove', onDrag);
window.addEventListener('mouseup', onMouseup, { once: true });
window.addEventListener('touchend', onMouseup, { once: true });
if (lastClickTime == null) {
lastClickTime = Date.now();
return;
} else {
const now = Date.now();
if (now - lastClickTime < 300) { // 300ms
lastClickTime = null;
emit('thumbDoubleClicked');
return;
} else {
lastClickTime = now;
}
}
}
</script>

View File

@ -23,7 +23,37 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._uploader.tip }}
</MkTip>
<MkUploaderItems :items="items" @showMenu="(item, ev) => showPerItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerItemMenuViaContextmenu(item, ev)"/>
<div class="_gaps_s">
<div
v-for="ctx in items"
:key="ctx.id"
v-panel
:class="[$style.item, ctx.preprocessing ? $style.itemWaiting : null, ctx.uploaded ? $style.itemCompleted : null, ctx.uploadFailed ? $style.itemFailed : null]"
:style="{ '--p': ctx.progress != null ? `${ctx.progress.value / ctx.progress.max * 100}%` : '0%' }"
>
<div :class="$style.itemInner">
<div :class="$style.itemActionWrapper">
<MkButton :iconOnly="true" rounded @click="showMenu($event, ctx)"><i class="ti ti-dots"></i></MkButton>
</div>
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ ctx.thumbnail })` }"></div>
<div :class="$style.itemBody">
<div><MkCondensedLine :minScale="2 / 3">{{ ctx.name }}</MkCondensedLine></div>
<div :class="$style.itemInfo">
<span>{{ ctx.file.type }}</span>
<span v-if="ctx.compressedSize">({{ i18n.tsx._uploader.compressedToX({ x: bytes(ctx.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - ctx.compressedSize / ctx.file.size) * 100) }) }})</span>
<span v-else>{{ bytes(ctx.file.size) }}</span>
</div>
<div>
</div>
</div>
<div :class="$style.itemIconWrapper">
<MkSystemIcon v-if="ctx.uploading" :class="$style.itemIcon" type="waiting"/>
<MkSystemIcon v-else-if="ctx.uploaded" :class="$style.itemIcon" type="success"/>
<MkSystemIcon v-else-if="ctx.uploadFailed" :class="$style.itemIcon" type="error"/>
</div>
</div>
</div>
</div>
<div v-if="props.multiple">
<MkButton style="margin: auto;" :iconOnly="true" rounded @click="chooseFile($event)"><i class="ti ti-plus"></i></MkButton>
@ -39,8 +69,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #footer>
<div class="_buttonsCenter">
<MkButton v-if="uploader.uploading.value" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
<MkButton v-else-if="!firstUploadAttempted" primary rounded :disabled="!uploader.readyForUpload.value" @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
<MkButton v-if="isUploading" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
<MkButton v-else-if="!firstUploadAttempted" primary rounded @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
<MkButton v-if="canRetry" rounded @click="upload()"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
<MkButton v-if="canDone" rounded @click="done()"><i class="ti ti-arrow-right"></i> {{ i18n.ts.done }}</MkButton>
@ -49,51 +79,110 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkModalWindow>
</template>
<script lang="ts">
export type UploaderDialogFeatures = {
effect?: boolean;
watermark?: boolean;
crop?: boolean;
};
</script>
<script lang="ts" setup>
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';
import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import type { UploaderFeatures, UploaderItem } from '@/composables/use-uploader.js';
import { genId } from '@/utility/id.js';
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
import isAnimated from 'is-file-animated';
import type { MenuItem } from '@/types/menu.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import MkButton from '@/components/MkButton.vue';
import bytes from '@/filters/bytes.js';
import { isWebpSupported } from '@/utility/isWebpSupported.js';
import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
import * as os from '@/os.js';
import { ensureSignin } from '@/i.js';
import { useUploader } from '@/composables/use-uploader.js';
import MkUploaderItems from '@/components/MkUploaderItems.vue';
import { WatermarkRenderer } from '@/utility/watermark.js';
const $i = ensureSignin();
const COMPRESSION_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
'image/svg+xml',
];
const CROPPING_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
];
const IMAGE_EDITING_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
];
const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES;
const mimeTypeMap = {
'image/webp': 'webp',
'image/jpeg': 'jpg',
'image/png': 'png',
} as const;
const props = withDefaults(defineProps<{
files: File[];
folderId?: string | null;
multiple?: boolean;
features?: UploaderFeatures;
features?: UploaderDialogFeatures;
}>(), {
multiple: true,
});
const uploaderFeatures = computed<Required<UploaderDialogFeatures>>(() => {
return {
effect: props.features?.effect ?? true,
watermark: props.features?.watermark ?? true,
crop: props.features?.crop ?? true,
};
});
const emit = defineEmits<{
(ev: 'done', driveFiles: Misskey.entities.DriveFile[]): void;
(ev: 'canceled'): void;
(ev: 'closed'): void;
}>();
type UploaderItem = {
id: string;
name: string;
uploadName?: string;
progress: { max: number; value: number } | null;
thumbnail: string;
preprocessing: boolean;
uploading: boolean;
uploaded: Misskey.entities.DriveFile | null;
uploadFailed: boolean;
aborted: boolean;
compressionLevel: 0 | 1 | 2 | 3;
compressedSize?: number | null;
preprocessedFile?: Blob | null;
file: File;
watermarkPresetId: string | null;
abort?: (() => void) | null;
};
const items = ref<UploaderItem[]>([]);
const dialog = useTemplateRef('dialog');
const uploader = useUploader({
multiple: props.multiple,
folderId: props.folderId,
features: props.features,
});
onMounted(() => {
uploader.addFiles(props.files);
});
const items = uploader.items;
const firstUploadAttempted = ref(false);
const canRetry = computed(() => firstUploadAttempted.value && uploader.readyForUpload.value);
const isUploading = computed(() => items.value.some(item => item.uploading));
const canRetry = computed(() => firstUploadAttempted.value && !items.value.some(item => item.uploading || item.preprocessing) && items.value.some(item => item.uploaded == null));
const canDone = computed(() => items.value.some(item => item.uploaded != null));
const overallProgress = computed(() => {
const max = items.value.length;
@ -106,6 +195,27 @@ const overallProgress = computed(() => {
return Math.round((v / max) * 100);
});
function getCompressionSettings(level: 0 | 1 | 2 | 3) {
if (level === 1) {
return {
maxWidth: 2000,
maxHeight: 2000,
};
} else if (level === 2) {
return {
maxWidth: 2000 * 0.75, // =1500
maxHeight: 2000 * 0.75, // =1500
};
} else if (level === 3) {
return {
maxWidth: 2000 * 0.75 * 0.75, // =1125
maxHeight: 2000 * 0.75 * 0.75, // =1125
};
} else {
return null;
}
}
watch(items, () => {
if (items.value.length === 0) {
emit('canceled');
@ -128,16 +238,11 @@ async function cancel() {
});
if (canceled) return;
uploader.abortAll();
abortAll();
emit('canceled');
dialog.value?.close();
}
function upload() {
firstUploadAttempted.value = true;
uploader.upload();
}
async function abortWithConfirm() {
const { canceled } = await os.confirm({
type: 'question',
@ -147,11 +252,11 @@ async function abortWithConfirm() {
});
if (canceled) return;
uploader.abortAll();
abortAll();
}
async function done() {
if (!uploader.allItemsUploaded.value) {
if (items.value.some(item => item.uploaded == null)) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts._uploader.doneConfirm,
@ -165,20 +270,381 @@ async function done() {
dialog.value?.close();
}
async function chooseFile(ev: MouseEvent) {
const newFiles = await os.chooseFileFromPc({ multiple: true });
uploader.addFiles(newFiles);
function showMenu(ev: MouseEvent, item: UploaderItem) {
const menu: MenuItem[] = [];
menu.push({
icon: 'ti ti-cursor-text',
text: i18n.ts.rename,
action: async () => {
const { result, canceled } = await os.inputText({
type: 'text',
title: i18n.ts.rename,
placeholder: item.name,
default: item.name,
});
if (canceled) return;
if (result.trim() === '') return;
item.name = result;
},
});
if (
uploaderFeatures.value.crop &&
CROPPING_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({
icon: 'ti ti-crop',
text: i18n.ts.cropImage,
action: async () => {
const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
URL.revokeObjectURL(item.thumbnail);
const newItem = {
...item,
file: markRaw(cropped),
thumbnail: window.URL.createObjectURL(cropped),
};
items.value.splice(items.value.indexOf(item), 1, newItem);
preprocess(newItem).then(() => {
triggerRef(items);
});
},
});
}
if (
uploaderFeatures.value.effect &&
IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({
icon: 'ti ti-sparkles',
text: i18n.ts._imageEffector.title + ' (BETA)',
action: async () => {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageEffectorDialog.vue').then(x => x.default), {
image: item.file,
}, {
ok: (file) => {
URL.revokeObjectURL(item.thumbnail);
const newItem = {
...item,
file: markRaw(file),
thumbnail: window.URL.createObjectURL(file),
};
items.value.splice(items.value.indexOf(item), 1, newItem);
preprocess(newItem).then(() => {
triggerRef(items);
});
},
closed: () => dispose(),
});
},
});
}
if (
uploaderFeatures.value.watermark &&
WATERMARK_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
function changeWatermarkPreset(presetId: string | null) {
item.watermarkPresetId = presetId;
preprocess(item).then(() => {
triggerRef(items);
});
}
menu.push({
icon: 'ti ti-copyright',
text: i18n.ts.watermark,
type: 'parent',
children: [{
type: 'radioOption',
text: i18n.ts.none,
active: computed(() => item.watermarkPresetId == null),
action: () => changeWatermarkPreset(null),
}, {
type: 'divider',
}, ...prefer.s.watermarkPresets.map(preset => ({
type: 'radioOption' as const,
text: preset.name,
active: computed(() => item.watermarkPresetId === preset.id),
action: () => changeWatermarkPreset(preset.id),
})), ...(prefer.s.watermarkPresets.length > 0 ? [{
type: 'divider' as const,
}] : []), {
type: 'button',
icon: 'ti ti-plus',
text: i18n.ts.add,
action: async () => {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkWatermarkEditorDialog.vue').then(x => x.default), {
image: item.file,
}, {
ok: (preset) => {
prefer.commit('watermarkPresets', [...prefer.s.watermarkPresets, preset]);
changeWatermarkPreset(preset.id);
},
closed: () => dispose(),
});
},
}],
});
}
if (COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded) {
function changeCompressionLevel(level: 0 | 1 | 2 | 3) {
item.compressionLevel = level;
preprocess(item).then(() => {
triggerRef(items);
});
}
menu.push({
icon: 'ti ti-leaf',
text: i18n.ts.compress,
type: 'parent',
children: [{
type: 'radioOption',
text: i18n.ts.none,
active: computed(() => item.compressionLevel === 0 || item.compressionLevel == null),
action: () => changeCompressionLevel(0),
}, {
type: 'divider',
}, {
type: 'radioOption',
text: i18n.ts.low,
active: computed(() => item.compressionLevel === 1),
action: () => changeCompressionLevel(1),
}, {
type: 'radioOption',
text: i18n.ts.medium,
active: computed(() => item.compressionLevel === 2),
action: () => changeCompressionLevel(2),
}, {
type: 'radioOption',
text: i18n.ts.high,
active: computed(() => item.compressionLevel === 3),
action: () => changeCompressionLevel(3),
}],
});
}
if (!item.preprocessing && !item.uploading && !item.uploaded) {
menu.push({
type: 'divider',
}, {
icon: 'ti ti-x',
text: i18n.ts.remove,
action: () => {
URL.revokeObjectURL(item.thumbnail);
items.value.splice(items.value.indexOf(item), 1);
},
});
} else if (item.uploading) {
menu.push({
type: 'divider',
}, {
icon: 'ti ti-cloud-pause',
text: i18n.ts.abort,
danger: true,
action: () => {
if (item.abort != null) {
item.abort();
}
},
});
}
function showPerItemMenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
function showPerItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.contextMenu(menu, ev);
async function upload() { //
firstUploadAttempted.value = true;
items.value = items.value.map(item => ({
...item,
aborted: false,
uploadFailed: false,
uploading: false,
}));
for (const item of items.value.filter(item => item.uploaded == null)) {
// Array filter
if (item.aborted) {
continue;
}
item.uploadFailed = false;
item.uploading = true;
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
name: item.uploadName ?? item.name,
folderId: props.folderId,
onProgress: (progress) => {
if (item.progress == null) {
item.progress = { max: progress.total, value: progress.loaded };
} else {
item.progress.value = progress.loaded;
item.progress.max = progress.total;
}
},
});
item.abort = () => {
item.abort = null;
abort();
item.uploading = false;
item.uploadFailed = true;
};
await filePromise.then((file) => {
item.uploaded = file;
item.abort = null;
}).catch(err => {
item.uploadFailed = true;
item.progress = null;
if (!(err instanceof UploadAbortedError)) {
throw err;
}
}).finally(() => {
item.uploading = false;
});
}
}
function abortAll() {
for (const item of items.value) {
if (item.uploaded != null) {
continue;
}
if (item.abort != null) {
item.abort();
}
item.aborted = true;
item.uploadFailed = true;
}
}
async function chooseFile(ev: MouseEvent) {
const newFiles = await os.chooseFileFromPc({ multiple: true });
for (const file of newFiles) {
initializeFile(file);
}
}
async function preprocess(item: (typeof items)['value'][number]): Promise<void> {
item.preprocessing = true;
let file: Blob | File = item.file;
const imageBitmap = await window.createImageBitmap(file);
const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(file.type);
const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId);
if (needsWatermark && preset != null) {
const canvas = window.document.createElement('canvas');
const renderer = new WatermarkRenderer({
canvas: canvas,
renderWidth: imageBitmap.width,
renderHeight: imageBitmap.height,
image: imageBitmap,
});
await renderer.setLayers(preset.layers);
renderer.render();
file = await new Promise<Blob>((resolve) => {
canvas.toBlob((blob) => {
if (blob == null) {
throw new Error('Failed to convert canvas to blob');
}
resolve(blob);
renderer.destroy();
}, 'image/png');
});
}
const compressionSettings = getCompressionSettings(item.compressionLevel);
const needsCompress = item.compressionLevel !== 0 && compressionSettings && COMPRESSION_SUPPORTED_TYPES.includes(file.type) && !(await isAnimated(file));
if (needsCompress) {
const config = {
mimeType: isWebpSupported() ? 'image/webp' : 'image/jpeg',
maxWidth: compressionSettings.maxWidth,
maxHeight: compressionSettings.maxHeight,
quality: isWebpSupported() ? 0.85 : 0.8,
};
try {
const result = await readAndCompressImage(file, config);
if (result.size < file.size || file.type === 'image/webp') {
// The compression may not always reduce the file size
// (and WebP is not browser safe yet)
file = result;
item.compressedSize = result.size;
item.uploadName = file.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
}
} catch (err) {
console.error('Failed to resize image', err);
}
} else {
item.compressedSize = null;
item.uploadName = item.name;
}
URL.revokeObjectURL(item.thumbnail);
item.thumbnail = window.URL.createObjectURL(file);
item.preprocessedFile = markRaw(file);
item.preprocessing = false;
imageBitmap.close();
}
function initializeFile(file: File) {
const id = genId();
const filename = file.name ?? 'untitled';
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
const item = {
id,
name: prefer.s.keepOriginalFilename ? filename : id + extension,
progress: null,
thumbnail: window.URL.createObjectURL(file),
preprocessing: false,
uploading: false,
aborted: false,
uploaded: null,
uploadFailed: false,
compressionLevel: prefer.s.defaultImageCompressionLevel,
watermarkPresetId: uploaderFeatures.value.watermark ? prefer.s.defaultWatermarkPresetId : null,
file: markRaw(file),
} satisfies UploaderItem;
items.value.push(item);
preprocess(item).then(() => {
triggerRef(items);
});
}
onMounted(() => {
for (const file of props.files) {
initializeFile(file);
}
});
onUnmounted(() => {
for (const item of items.value) {
URL.revokeObjectURL(item.thumbnail);
}
});
</script>
<style lang="scss" module>
@ -200,4 +666,127 @@ function showPerItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
background: var(--MI_THEME-warn);
}
}
.item {
position: relative;
border-radius: 10px;
overflow: clip;
&::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: var(--p);
height: 100%;
background: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
transition: width 0.2s ease, left 0.2s ease;
}
&.itemWaiting {
&::after {
--c: color(from var(--MI_THEME-accent) srgb r g b / 0.25);
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(-45deg, transparent 25%, var(--c) 25%,var(--c) 50%, transparent 50%, transparent 75%, var(--c) 75%, var(--c));
background-size: 25px 25px;
animation: stripe .8s infinite linear;
}
}
&.itemCompleted {
&::before {
left: 100%;
width: var(--p);
}
.itemBody {
color: var(--MI_THEME-accent);
}
}
&.itemFailed {
.itemBody {
color: var(--MI_THEME-error);
}
}
}
@keyframes stripe {
0% { background-position-x: 0; }
100% { background-position-x: -25px; }
}
.itemInner {
position: relative;
z-index: 1;
padding: 8px 16px;
display: flex;
align-items: center;
gap: 12px;
}
.itemThumbnail {
width: 70px;
height: 70px;
background-color: var(--MI_THEME-bg);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
border-radius: 6px;
}
.itemBody {
flex: 1;
min-width: 0;
}
.itemInfo {
opacity: 0.7;
margin-top: 4px;
font-size: 90%;
display: flex;
gap: 8px;
}
.itemIcon {
width: 35px;
}
@container (max-width: 500px) {
.itemInner {
flex-direction: column;
gap: 8px;
}
.itemBody {
font-size: 90%;
text-align: center;
width: 100%;
min-width: 0;
}
.itemActionWrapper {
position: absolute;
top: 8px;
left: 8px;
}
.itemInfo {
justify-content: center;
}
.itemIconWrapper {
position: absolute;
top: 8px;
right: 8px;
}
}
</style>

View File

@ -1,196 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root" class="_gaps_s">
<div
v-for="item in props.items"
:key="item.id"
v-panel
:class="[$style.item, { [$style.itemWaiting]: item.preprocessing, [$style.itemCompleted]: item.uploaded, [$style.itemFailed]: item.uploadFailed }]"
:style="{ '--p': item.progress != null ? `${item.progress.value / item.progress.max * 100}%` : '0%' }"
@contextmenu.prevent.stop="onContextmenu(item, $event)"
>
<div :class="$style.itemInner">
<div :class="$style.itemActionWrapper">
<MkButton :iconOnly="true" rounded @click="emit('showMenu', item, $event)"><i class="ti ti-dots"></i></MkButton>
</div>
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }" @click="onThumbnailClick(item, $event)"></div>
<div :class="$style.itemBody">
<div><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div>
<div :class="$style.itemInfo">
<span>{{ item.file.type }}</span>
<span v-if="item.compressedSize">({{ i18n.tsx._uploader.compressedToX({ x: bytes(item.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - item.compressedSize / item.file.size) * 100) }) }})</span>
<span v-else>{{ bytes(item.file.size) }}</span>
</div>
<div>
</div>
</div>
<div :class="$style.itemIconWrapper">
<MkSystemIcon v-if="item.uploading" :class="$style.itemIcon" type="waiting"/>
<MkSystemIcon v-else-if="item.uploaded" :class="$style.itemIcon" type="success"/>
<MkSystemIcon v-else-if="item.uploadFailed" :class="$style.itemIcon" type="error"/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { isLink } from '@@/js/is-link.js';
import type { UploaderItem } from '@/composables/use-uploader.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import bytes from '@/filters/bytes.js';
const props = defineProps<{
items: UploaderItem[];
}>();
const emit = defineEmits<{
(ev: 'showMenu', item: UploaderItem, event: MouseEvent): void;
(ev: 'showMenuViaContextmenu', item: UploaderItem, event: MouseEvent): void;
}>();
function onContextmenu(item: UploaderItem, ev: MouseEvent) {
if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== '') return;
emit('showMenuViaContextmenu', item, ev);
}
function onThumbnailClick(item: UploaderItem, ev: MouseEvent) {
// TODO: preview when item is image
}
</script>
<style lang="scss" module>
.root {
position: relative;
}
.item {
position: relative;
border-radius: 10px;
overflow: clip;
&::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: var(--p);
height: 100%;
background: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
transition: width 0.2s ease, left 0.2s ease;
}
&.itemWaiting {
&::after {
--c: color(from var(--MI_THEME-accent) srgb r g b / 0.25);
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(-45deg, transparent 25%, var(--c) 25%,var(--c) 50%, transparent 50%, transparent 75%, var(--c) 75%, var(--c));
background-size: 25px 25px;
animation: stripe .8s infinite linear;
}
}
&.itemCompleted {
&::before {
left: 100%;
width: var(--p);
}
.itemBody {
color: var(--MI_THEME-accent);
}
}
&.itemFailed {
.itemBody {
color: var(--MI_THEME-error);
}
}
}
@keyframes stripe {
0% { background-position-x: 0; }
100% { background-position-x: -25px; }
}
.itemInner {
position: relative;
z-index: 1;
padding: 8px 16px;
display: flex;
align-items: center;
gap: 12px;
}
.itemThumbnail {
width: 70px;
height: 70px;
background-color: var(--MI_THEME-bg);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
border-radius: 6px;
}
.itemBody {
flex: 1;
min-width: 0;
}
.itemInfo {
opacity: 0.7;
margin-top: 4px;
font-size: 90%;
display: flex;
gap: 8px;
}
.itemIcon {
width: 35px;
}
@container (max-width: 500px) {
.itemInner {
flex-direction: column;
gap: 8px;
}
.itemBody {
font-size: 90%;
text-align: center;
width: 100%;
min-width: 0;
}
.itemActionWrapper {
position: absolute;
top: 8px;
left: 8px;
}
.itemInfo {
justify-content: center;
}
.itemIconWrapper {
position: absolute;
top: 8px;
right: 8px;
}
}
</style>

View File

@ -7,10 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="!store.r.tips.value[props.k]" :class="[$style.root, { [$style.warn]: warn }]" class="_selectable _gaps_s">
<div style="font-weight: bold;"><i class="ti ti-bulb"></i> {{ i18n.ts.tip }}:</div>
<div><slot></slot></div>
<div>
<MkButton inline primary rounded small @click="_closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
<button class="_button" style="padding: 8px; margin-left: 4px;" @click="showMenu"><i class="ti ti-dots"></i></button>
</div>
<MkButton primary rounded small @click="closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
</div>
</template>
@ -18,30 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
import { i18n } from '@/i18n.js';
import { store } from '@/store.js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { TIPS, hideAllTips, closeTip } from '@/tips.js';
const props = withDefaults(defineProps<{
k: typeof TIPS[number];
k: keyof (typeof store['s']['tips']);
warn?: boolean;
}>(), {
warn: false,
});
function _closeTip() {
closeTip(props.k);
}
function showMenu(ev: MouseEvent) {
os.popupMenu([{
icon: 'ti ti-bulb-off',
text: i18n.ts.hideAllTips,
danger: true,
action: () => {
hideAllTips();
os.success();
},
}], ev.currentTarget ?? ev.target);
function closeTip() {
store.set('tips', {
...store.r.tips.value,
[props.k]: true,
});
}
</script>

View File

@ -1,576 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Misskey from 'misskey-js';
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
import isAnimated from 'is-file-animated';
import { EventEmitter } from 'eventemitter3';
import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import { genId } from '@/utility/id.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { isWebpSupported } from '@/utility/isWebpSupported.js';
import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
import * as os from '@/os.js';
import { ensureSignin } from '@/i.js';
import { WatermarkRenderer } from '@/utility/watermark.js';
export type UploaderFeatures = {
effect?: boolean;
watermark?: boolean;
crop?: boolean;
};
const THUMBNAIL_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
'image/svg+xml',
];
const IMAGE_COMPRESSION_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
'image/svg+xml',
];
const CROPPING_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
];
const IMAGE_EDITING_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
];
const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES;
const IMAGE_PREPROCESS_NEEDED_TYPES = [
...WATERMARK_SUPPORTED_TYPES,
...IMAGE_COMPRESSION_SUPPORTED_TYPES,
...CROPPING_SUPPORTED_TYPES,
...IMAGE_EDITING_SUPPORTED_TYPES,
];
const mimeTypeMap = {
'image/webp': 'webp',
'image/jpeg': 'jpg',
'image/png': 'png',
} as const;
export type UploaderItem = {
id: string;
name: string;
uploadName?: string;
progress: { max: number; value: number } | null;
thumbnail: string | null;
preprocessing: boolean;
uploading: boolean;
uploaded: Misskey.entities.DriveFile | null;
uploadFailed: boolean;
aborted: boolean;
compressionLevel: 0 | 1 | 2 | 3;
compressedSize?: number | null;
preprocessedFile?: Blob | null;
file: File;
watermarkPresetId: string | null;
isSensitive?: boolean;
abort?: (() => void) | null;
};
function getCompressionSettings(level: 0 | 1 | 2 | 3) {
if (level === 1) {
return {
maxWidth: 2000,
maxHeight: 2000,
};
} else if (level === 2) {
return {
maxWidth: 2000 * 0.75, // =1500
maxHeight: 2000 * 0.75, // =1500
};
} else if (level === 3) {
return {
maxWidth: 2000 * 0.75 * 0.75, // =1125
maxHeight: 2000 * 0.75 * 0.75, // =1125
};
} else {
return null;
}
}
export function useUploader(options: {
folderId?: string | null;
multiple?: boolean;
features?: UploaderFeatures;
} = {}) {
const $i = ensureSignin();
const events = new EventEmitter<{
'itemUploaded': (ctx: { item: UploaderItem; }) => void;
}>();
const uploaderFeatures = computed<Required<UploaderFeatures>>(() => {
return {
effect: options.features?.effect ?? true,
watermark: options.features?.watermark ?? true,
crop: options.features?.crop ?? true,
};
});
const items = ref<UploaderItem[]>([]);
function initializeFile(file: File) {
const id = genId();
const filename = file.name ?? 'untitled';
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
items.value.push({
id,
name: prefer.s.keepOriginalFilename ? filename : id + extension,
progress: null,
thumbnail: THUMBNAIL_SUPPORTED_TYPES.includes(file.type) ? window.URL.createObjectURL(file) : null,
preprocessing: false,
uploading: false,
aborted: false,
uploaded: null,
uploadFailed: false,
compressionLevel: prefer.s.defaultImageCompressionLevel,
watermarkPresetId: uploaderFeatures.value.watermark ? prefer.s.defaultWatermarkPresetId : null,
file: markRaw(file),
});
const reactiveItem = items.value.at(-1)!;
preprocess(reactiveItem).then(() => {
triggerRef(items);
});
}
function addFiles(newFiles: File[]) {
for (const file of newFiles) {
initializeFile(file);
}
}
function removeItem(item: UploaderItem) {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
items.value.splice(items.value.indexOf(item), 1);
}
function getMenu(item: UploaderItem): MenuItem[] {
const menu: MenuItem[] = [];
if (
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({
icon: 'ti ti-forms',
text: i18n.ts.rename,
action: async () => {
const { result, canceled } = await os.inputText({
type: 'text',
title: i18n.ts.rename,
placeholder: item.name,
default: item.name,
});
if (canceled) return;
if (result.trim() === '') return;
item.name = result;
},
}, {
type: 'switch',
text: i18n.ts.sensitive,
icon: 'ti ti-eye-exclamation',
ref: computed({
get: () => item.isSensitive ?? false,
set: (value) => item.isSensitive = value,
}),
}, {
type: 'divider',
});
}
if (
uploaderFeatures.value.crop &&
CROPPING_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({
icon: 'ti ti-crop',
text: i18n.ts.cropImage,
action: async () => {
const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
items.value.splice(items.value.indexOf(item), 1, {
...item,
file: markRaw(cropped),
thumbnail: window.URL.createObjectURL(cropped),
});
const reactiveItem = items.value.find(x => x.id === item.id)!;
preprocess(reactiveItem).then(() => {
triggerRef(items);
});
},
});
}
if (
uploaderFeatures.value.effect &&
IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({
icon: 'ti ti-sparkles',
text: i18n.ts._imageEffector.title + ' (BETA)',
action: async () => {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageEffectorDialog.vue').then(x => x.default), {
image: item.file,
}, {
ok: (file) => {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
items.value.splice(items.value.indexOf(item), 1, {
...item,
file: markRaw(file),
thumbnail: window.URL.createObjectURL(file),
});
const reactiveItem = items.value.find(x => x.id === item.id)!;
preprocess(reactiveItem).then(() => {
triggerRef(items);
});
},
closed: () => dispose(),
});
},
});
}
if (
uploaderFeatures.value.watermark &&
WATERMARK_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
function changeWatermarkPreset(presetId: string | null) {
item.watermarkPresetId = presetId;
preprocess(item).then(() => {
triggerRef(items);
});
}
menu.push({
icon: 'ti ti-copyright',
text: i18n.ts.watermark,
caption: computed(() => item.watermarkPresetId == null ? null : prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId)?.name),
type: 'parent',
children: [{
type: 'radioOption',
text: i18n.ts.none,
active: computed(() => item.watermarkPresetId == null),
action: () => changeWatermarkPreset(null),
}, {
type: 'divider',
}, ...prefer.s.watermarkPresets.map(preset => ({
type: 'radioOption' as const,
text: preset.name,
active: computed(() => item.watermarkPresetId === preset.id),
action: () => changeWatermarkPreset(preset.id),
})), ...(prefer.s.watermarkPresets.length > 0 ? [{
type: 'divider' as const,
}] : []), {
type: 'button',
icon: 'ti ti-plus',
text: i18n.ts.add,
action: async () => {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkWatermarkEditorDialog.vue').then(x => x.default), {
image: item.file,
}, {
ok: (preset) => {
prefer.commit('watermarkPresets', [...prefer.s.watermarkPresets, preset]);
changeWatermarkPreset(preset.id);
},
closed: () => dispose(),
});
},
}],
});
}
if (
IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
function changeCompressionLevel(level: 0 | 1 | 2 | 3) {
item.compressionLevel = level;
preprocess(item).then(() => {
triggerRef(items);
});
}
menu.push({
icon: 'ti ti-leaf',
text: computed(() => {
let text = i18n.ts.compress;
if (item.compressionLevel === 0 || item.compressionLevel == null) {
text += `: ${i18n.ts.none}`;
} else if (item.compressionLevel === 1) {
text += `: ${i18n.ts.low}`;
} else if (item.compressionLevel === 2) {
text += `: ${i18n.ts.medium}`;
} else if (item.compressionLevel === 3) {
text += `: ${i18n.ts.high}`;
}
return text;
}),
type: 'parent',
children: [{
type: 'radioOption',
text: i18n.ts.none,
active: computed(() => item.compressionLevel === 0 || item.compressionLevel == null),
action: () => changeCompressionLevel(0),
}, {
type: 'divider',
}, {
type: 'radioOption',
text: i18n.ts.low,
active: computed(() => item.compressionLevel === 1),
action: () => changeCompressionLevel(1),
}, {
type: 'radioOption',
text: i18n.ts.medium,
active: computed(() => item.compressionLevel === 2),
action: () => changeCompressionLevel(2),
}, {
type: 'radioOption',
text: i18n.ts.high,
active: computed(() => item.compressionLevel === 3),
action: () => changeCompressionLevel(3),
}],
});
}
if (!item.preprocessing && !item.uploading && !item.uploaded) {
menu.push({
type: 'divider',
}, {
icon: 'ti ti-upload',
text: i18n.ts.upload,
action: () => {
uploadOne(item);
},
}, {
icon: 'ti ti-x',
text: i18n.ts.remove,
danger: true,
action: () => {
removeItem(item);
},
});
} else if (item.uploading) {
menu.push({
type: 'divider',
}, {
icon: 'ti ti-cloud-pause',
text: i18n.ts.abort,
danger: true,
action: () => {
if (item.abort != null) {
item.abort();
}
},
});
}
return menu;
}
async function uploadOne(item: UploaderItem): Promise<void> {
item.uploadFailed = false;
item.uploading = true;
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
name: item.uploadName ?? item.name,
folderId: options.folderId,
isSensitive: item.isSensitive ?? false,
onProgress: (progress) => {
if (item.progress == null) {
item.progress = { max: progress.total, value: progress.loaded };
} else {
item.progress.value = progress.loaded;
item.progress.max = progress.total;
}
},
});
item.abort = () => {
item.abort = null;
abort();
item.uploading = false;
item.uploadFailed = true;
};
await filePromise.then((file) => {
item.uploaded = file;
item.abort = null;
events.emit('itemUploaded', { item });
}).catch(err => {
item.uploadFailed = true;
item.progress = null;
if (!(err instanceof UploadAbortedError)) {
throw err;
}
}).finally(() => {
item.uploading = false;
});
}
async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
items.value = items.value.map(item => ({
...item,
aborted: false,
uploadFailed: false,
uploading: false,
}));
for (const item of items.value.filter(item => item.uploaded == null)) {
// アップロード処理途中で値が変わる場合途中で全キャンセルされたりなどもあるので、Array filterではなくここでチェック
if (item.aborted) {
continue;
}
await uploadOne(item);
}
}
function abortAll() {
for (const item of items.value) {
if (item.uploaded != null) {
continue;
}
if (item.abort != null) {
item.abort();
}
item.aborted = true;
item.uploadFailed = true;
}
}
async function preprocess(item: UploaderItem): Promise<void> {
item.preprocessing = true;
try {
if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
await preprocessForImage(item);
}
} catch (err) {
console.error('Failed to preprocess image', err);
// nop
}
item.preprocessing = false;
}
async function preprocessForImage(item: UploaderItem): Promise<void> {
const imageBitmap = await window.createImageBitmap(item.file);
let preprocessedFile: Blob | File = item.file;
const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(preprocessedFile.type);
const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId);
if (needsWatermark && preset != null) {
const canvas = window.document.createElement('canvas');
const renderer = new WatermarkRenderer({
canvas: canvas,
renderWidth: imageBitmap.width,
renderHeight: imageBitmap.height,
image: imageBitmap,
});
await renderer.setLayers(preset.layers);
renderer.render();
preprocessedFile = await new Promise<Blob>((resolve) => {
canvas.toBlob((blob) => {
if (blob == null) {
throw new Error('Failed to convert canvas to blob');
}
resolve(blob);
renderer.destroy();
}, 'image/png');
});
}
const compressionSettings = getCompressionSettings(item.compressionLevel);
const needsCompress = item.compressionLevel !== 0 && compressionSettings && IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(preprocessedFile.type) && !(await isAnimated(preprocessedFile));
if (needsCompress) {
const config = {
mimeType: isWebpSupported() ? 'image/webp' : 'image/jpeg',
maxWidth: compressionSettings.maxWidth,
maxHeight: compressionSettings.maxHeight,
quality: isWebpSupported() ? 0.85 : 0.8,
};
try {
const result = await readAndCompressImage(preprocessedFile, config);
if (result.size < preprocessedFile.size || preprocessedFile.type === 'image/webp') {
// The compression may not always reduce the file size
// (and WebP is not browser safe yet)
preprocessedFile = result;
item.compressedSize = result.size;
item.uploadName = preprocessedFile.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
}
} catch (err) {
console.error('Failed to resize image', err);
}
} else {
item.compressedSize = null;
item.uploadName = item.name;
}
imageBitmap.close();
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null;
item.preprocessedFile = markRaw(preprocessedFile);
}
onUnmounted(() => {
for (const item of items.value) {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
}
});
return {
items,
addFiles,
removeItem,
abortAll,
upload,
getMenu,
uploading: computed(() => items.value.some(item => item.uploading)),
readyForUpload: computed(() => items.value.length > 0 && items.value.some(item => item.uploaded == null) && !items.value.some(item => item.uploading || item.preprocessing)),
allItemsUploaded: computed(() => items.value.every(item => item.uploaded != null)),
events,
};
}

View File

@ -161,7 +161,7 @@ import { prefer } from '@/preferences.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js';
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
import { store, TIPS } from '@/store.js';
const $i = ensureSignin();
@ -205,12 +205,16 @@ function migrate() {
}
function resetAllTips() {
_resetAllTips();
store.set('tips', {});
os.success();
}
function hideAllTips() {
_hideAllTips();
const v = {};
for (const k of TIPS) {
v[k] = true;
}
store.set('tips', v);
os.success();
}

View File

@ -226,7 +226,6 @@ const headerActions = computed(() => {
menuItems.push({
type: 'switch',
icon: 'ti ti-repeat',
text: i18n.ts.showRenotes,
ref: withRenotes,
});
@ -234,7 +233,6 @@ const headerActions = computed(() => {
if (isBasicTimeline(src.value) && hasWithReplies(src.value)) {
menuItems.push({
type: 'switch',
icon: 'ti ti-messages',
text: i18n.ts.showRepliesToOthersInTimeline,
ref: withReplies,
disabled: onlyFiles,
@ -243,12 +241,10 @@ const headerActions = computed(() => {
menuItems.push({
type: 'switch',
icon: 'ti ti-eye-exclamation',
text: i18n.ts.withSensitive,
ref: withSensitive,
}, {
type: 'switch',
icon: 'ti ti-photo',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,

View File

@ -81,6 +81,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkButton>
</div>
<div v-else-if="step === 1" class="_gaps_m">
<div style="text-align: center;" class="_gaps_s">
<div><b>{{ i18n.ts._serverSetupWizard.donationRequest }}</b></div>
<div>{{ i18n.ts._serverSetupWizard._donationRequest.text1 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text2 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text3 }}</div>
</div>
<MkLink target="_blank" url="https://misskey-hub.net/docs/donate/" style="margin: 0 auto;">{{ i18n.ts.learnMore }}</MkLink>
<div class="_buttonsCenter">
<MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="step++">
{{ i18n.ts.next }}
</MkButton>
</div>
</div>
<div v-else-if="step === 2" class="_gaps_m">
<div style="text-align: center;" class="_gaps_s">
<div style="font-size: 120%;"><b>{{ i18n.ts._serverSetupWizard.serverSetting }}</b></div>
<div>{{ i18n.ts._serverSetupWizard.youCanEasilyConfigureOptimalServerSettingsWithThisWizard }}</div>
@ -93,17 +105,12 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._serverSetupWizard.skipSettings }}
</MkButton>
</div>
<div v-else-if="step === 2" class="_gaps_m">
<div v-else-if="step === 3" class="_gaps_m">
<div style="text-align: center;" class="_gaps_s">
<div><b>{{ i18n.ts._serverSetupWizard.settingsCompleted }}</b></div>
<div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description }}</div>
<div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description2 }}</div>
</div>
<div class="_gaps_s" :class="$style.donation">
<div><b>{{ i18n.ts._serverSetupWizard.donationRequest }}</b></div>
<div>{{ i18n.ts._serverSetupWizard._donationRequest.text1 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text2 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text3 }}</div>
<MkLink target="_blank" url="https://misskey-hub.net/docs/donate/" style="margin: 0 auto;">{{ i18n.ts.learnMore }}</MkLink>
</div>
<div class="_buttonsCenter">
<MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="finish">
{{ i18n.ts.start }}
@ -225,11 +232,4 @@ function finish() {
font-weight: normal;
opacity: 0.7;
}
.donation {
background: var(--MI_THEME-accentedBg);
border-radius: 12px;
padding: 16px;
text-align: center;
}
</style>

View File

@ -10,11 +10,22 @@ import darkTheme from '@@/themes/d-green-lime.json5';
import { hemisphere } from '@@/js/intl-const.js';
import type { DeviceKind } from '@/utility/device-kind.js';
import type { Plugin } from '@/plugin.js';
import type { TIPS } from '@/tips.js';
import { miLocalStorage } from '@/local-storage.js';
import { Pizzax } from '@/lib/pizzax.js';
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
export const TIPS = [
'drive',
'uploader',
'clips',
'userLists',
'tl.home',
'tl.local',
'tl.social',
'tl.global',
'abuses',
] as const;
/**
* (not)
*/

View File

@ -1,38 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { store } from '@/store.js';
export const TIPS = [
'drive',
'uploader',
'postFormUploader',
'clips',
'userLists',
'tl.home',
'tl.local',
'tl.social',
'tl.global',
'abuses',
] as const;
export function closeTip(tip: typeof TIPS[number]) {
store.set('tips', {
...store.r.tips.value,
[tip]: true,
});
}
export function resetAllTips() {
store.set('tips', {});
}
export function hideAllTips() {
const v = {};
for (const k of TIPS) {
v[k] = true;
}
store.set('tips', v);
}

View File

@ -11,22 +11,20 @@ type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<
type MenuRadioOptionsDef = Record<string, any>;
type Text = string | ComputedRef<string>;
export type MenuAction = (ev: MouseEvent) => void;
export type MenuDivider = { type: 'divider' };
export type MenuNull = undefined;
export type MenuLabel = { type: 'label', text: Text, caption?: Text };
export type MenuLink = { type: 'link', to: string, text: Text, caption?: Text, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: Text, caption?: Text, icon?: string, indicate?: boolean };
export type MenuLabel = { type: 'label', text: string, caption?: string };
export type MenuLink = { type: 'link', to: string, text: string, caption?: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, caption?: 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: Text, caption?: Text, icon?: string, disabled?: boolean | Ref<boolean> };
export type MenuButton = { type?: 'button', text: Text, caption?: Text, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
export type MenuRadio = { type: 'radio', text: Text, caption?: Text, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
export type MenuRadioOption = { type: 'radioOption', text: Text, caption?: Text, action: MenuAction; active?: boolean | ComputedRef<boolean> };
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, caption?: string, icon?: string, disabled?: boolean | Ref<boolean> };
export type MenuButton = { type?: 'button', text: string, caption?: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
export type MenuRadio = { type: 'radio', text: string, caption?: string, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
export type MenuRadioOption = { type: 'radioOption', text: string, caption?: string, action: MenuAction; active?: boolean | ComputedRef<boolean> };
export type MenuComponent<T extends Component = any> = { type: 'component', component: T, props?: ComponentProps<T> };
export type MenuParent = { type: 'parent', text: Text, caption?: Text, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
export type MenuParent = { type: 'parent', text: string, caption?: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
export type MenuPending = { type: 'pending' };

View File

@ -6,7 +6,6 @@
import { defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import { apiUrl } from '@@/js/config.js';
import type { UploaderFeatures } from '@/composables/use-uploader.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { useStream } from '@/stream.js';
@ -17,6 +16,7 @@ import { instance } from '@/instance.js';
import { globalEvents } from '@/events.js';
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
import { genId } from '@/utility/id.js';
import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue';
type UploadReturnType = {
filePromise: Promise<Misskey.entities.DriveFile>;
@ -32,7 +32,6 @@ export class UploadAbortedError extends Error {
export function uploadFile(file: File | Blob, options: {
name?: string;
folderId?: string | null;
isSensitive?: boolean;
onProgress?: (ctx: { total: number; loaded: number; }) => void;
} = {}): UploadReturnType {
const xhr = new XMLHttpRequest();
@ -141,7 +140,6 @@ export function uploadFile(file: File | Blob, options: {
formData.append('force', 'true');
formData.append('file', file);
formData.append('name', options.name ?? (file instanceof File ? file.name : 'untitled'));
formData.append('isSensitive', options.isSensitive ? 'true' : 'false');
if (options.folderId) formData.append('folderId', options.folderId);
xhr.send(formData);
@ -158,7 +156,7 @@ export function uploadFile(file: File | Blob, options: {
export function chooseFileFromPcAndUpload(
options: {
multiple?: boolean;
features?: UploaderFeatures;
features?: UploaderDialogFeatures;
folderId?: string | null;
} = {},
): Promise<Misskey.entities.DriveFile[]> {
@ -256,7 +254,7 @@ type SelectFileOptions<M extends boolean> = {
export async function selectFile<
M extends boolean,
MR extends M extends true ? Misskey.entities.DriveFile[] : Misskey.entities.DriveFile,
MR extends M extends true ? Misskey.entities.DriveFile[] : Misskey.entities.DriveFile
>(opts: SelectFileOptions<M>): Promise<MR> {
const files = await select(opts.anchorElement, opts.label ?? null, opts.multiple ?? false, opts.features);
return opts.multiple ? (files as MR) : (files[0]! as MR);

View File

@ -4,7 +4,6 @@
*/
import { getProxiedImageUrl } from '../media-proxy.js';
import { initShaderProgram } from '../webgl.js';
type ParamTypeToPrimitive = {
'number': number;
@ -61,6 +60,8 @@ function getValue<T extends keyof ParamTypeToPrimitive>(params: Record<string, a
export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, any>>> {
private gl: WebGL2RenderingContext;
private canvas: HTMLCanvasElement | null = null;
private renderTextureProgram: WebGLProgram;
private renderInvertedTextureProgram: WebGLProgram;
private renderWidth: number;
private renderHeight: number;
private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
@ -69,7 +70,6 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
private shaderCache: Map<string, WebGLProgram> = new Map();
private perLayerResultTextures: Map<string, WebGLTexture> = new Map();
private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map();
private nopProgram: WebGLProgram;
private fxs: [...IEX];
private paramTextures: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map();
@ -114,13 +114,13 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.originalImage.width, this.originalImage.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.originalImage);
gl.bindTexture(gl.TEXTURE_2D, null);
this.nopProgram = initShaderProgram(this.gl, `#version 300 es
this.renderTextureProgram = this.initShaderProgram(`#version 300 es
in vec2 position;
out vec2 in_uv;
void main() {
in_uv = (position + 1.0) / 2.0;
gl_Position = vec4(position * vec2(1.0, -1.0), 0.0, 1.0);
gl_Position = vec4(position, 0.0, 1.0);
}
`, `#version 300 es
precision mediump float;
@ -134,28 +134,82 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
}
`);
// レジスタ番号はシェーダープログラムに属しているわけではなく、独立の存在なので、とりあえず nopProgram を使って設定する(その後は効果が持続する)
// ref. https://qiita.com/emadurandal/items/5966c8374f03d4de3266
const positionLocation = gl.getAttribLocation(this.nopProgram, 'position');
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation);
this.renderInvertedTextureProgram = this.initShaderProgram(`#version 300 es
in vec2 position;
out vec2 in_uv;
void main() {
in_uv = (position + 1.0) / 2.0;
in_uv.y = 1.0 - in_uv.y;
gl_Position = vec4(position, 0.0, 1.0);
}
`, `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D u_texture;
out vec4 out_color;
void main() {
out_color = texture(u_texture, in_uv);
}
`);
}
private renderLayer(layer: ImageEffectorLayer, preTexture: WebGLTexture, invert = false) {
public loadShader(type: GLenum, source: string): WebGLShader {
const gl = this.gl;
const shader = gl.createShader(type);
if (shader == null) {
throw new Error('falied to create shader');
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
gl.deleteShader(shader);
throw new Error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
}
return shader;
}
public initShaderProgram(vsSource: string, fsSource: string): WebGLProgram {
const gl = this.gl;
const vertexShader = this.loadShader(gl.VERTEX_SHADER, vsSource);
const fragmentShader = this.loadShader(gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
console.error(`failed to init shader: ${gl.getProgramInfoLog(shaderProgram)}`);
throw new Error('failed to init shader');
}
return shaderProgram;
}
private renderLayer(layer: ImageEffectorLayer, preTexture: WebGLTexture) {
const gl = this.gl;
const fx = this.fxs.find(fx => fx.id === layer.fxId);
if (fx == null) return;
const cachedShader = this.shaderCache.get(fx.id);
const shaderProgram = cachedShader ?? initShaderProgram(this.gl, `#version 300 es
const shaderProgram = cachedShader ?? this.initShaderProgram(`#version 300 es
in vec2 position;
uniform bool u_invert;
out vec2 in_uv;
void main() {
in_uv = (position + 1.0) / 2.0;
gl_Position = u_invert ? vec4(position * vec2(1.0, -1.0), 0.0, 1.0) : vec4(position, 0.0, 1.0);
gl_Position = vec4(position, 0.0, 1.0);
}
`, fx.shader);
if (cachedShader == null) {
@ -167,9 +221,6 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
const in_resolution = gl.getUniformLocation(shaderProgram, 'in_resolution');
gl.uniform2fv(in_resolution, [this.renderWidth, this.renderHeight]);
const u_invert = gl.getUniformLocation(shaderProgram, 'u_invert');
gl.uniform1i(u_invert, invert ? 1 : 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, preTexture);
const in_texture = gl.getUniformLocation(shaderProgram, 'in_texture');
@ -202,23 +253,27 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
public render() {
const gl = this.gl;
// 入力をそのまま出力
if (this.layers.length === 0) {
{
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture);
gl.useProgram(this.nopProgram);
gl.uniform1i(gl.getUniformLocation(this.nopProgram, 'u_texture')!, 0);
gl.useProgram(this.renderTextureProgram);
const u_texture = gl.getUniformLocation(this.renderTextureProgram, 'u_texture');
gl.uniform1i(u_texture, 0);
const u_resolution = gl.getUniformLocation(this.renderTextureProgram, 'u_resolution');
gl.uniform2fv(u_resolution, [this.renderWidth, this.renderHeight]);
const positionLocation = gl.getAttribLocation(this.renderTextureProgram, 'position');
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation);
gl.drawArrays(gl.TRIANGLES, 0, 6);
return;
}
// --------------------
let preTexture = this.originalImageTexture;
for (const layer of this.layers) {
const isLast = layer === this.layers.at(-1);
const cachedResultTexture = this.perLayerResultTextures.get(layer.id);
const resultTexture = cachedResultTexture ?? createTexture(gl);
if (cachedResultTexture == null) {
@ -228,9 +283,6 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.renderWidth, this.renderHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.bindTexture(gl.TEXTURE_2D, null);
if (isLast) {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
} else {
const cachedResultFrameBuffer = this.perLayerResultFrameBuffers.get(layer.id);
const resultFrameBuffer = cachedResultFrameBuffer ?? gl.createFramebuffer()!;
if (cachedResultFrameBuffer == null) {
@ -238,12 +290,23 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
}
gl.bindFramebuffer(gl.FRAMEBUFFER, resultFrameBuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, resultTexture, 0);
}
this.renderLayer(layer, preTexture, isLast);
this.renderLayer(layer, preTexture);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
preTexture = resultTexture;
}
// --------------------
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.useProgram(this.renderInvertedTextureProgram);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, preTexture);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
public async setLayers(layers: ImageEffectorLayer[]) {
@ -303,8 +366,6 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
* disposeCanvas = true loseContextを呼ぶためcanvasも再利用不可になるので注意
*/
public destroy(disposeCanvas = true) {
this.gl.deleteProgram(this.nopProgram);
for (const shader of this.shaderCache.values()) {
this.gl.deleteProgram(shader);
}
@ -325,6 +386,8 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
}
this.paramTextures.clear();
this.gl.deleteProgram(this.renderTextureProgram);
this.gl.deleteProgram(this.renderInvertedTextureProgram);
this.gl.deleteTexture(this.originalImageTexture);
if (disposeCanvas) {

View File

@ -5,7 +5,6 @@
import { FX_checker } from './fxs/checker.js';
import { FX_chromaticAberration } from './fxs/chromaticAberration.js';
import { FX_colorAdjust } from './fxs/colorAdjust.js';
import { FX_colorClamp } from './fxs/colorClamp.js';
import { FX_colorClampAdvanced } from './fxs/colorClampAdvanced.js';
import { FX_distort } from './fxs/distort.js';
@ -27,7 +26,6 @@ export const FXS = [
FX_mirror,
FX_invert,
FX_grayscale,
FX_colorAdjust,
FX_colorClamp,
FX_colorClampAdvanced,
FX_distort,

View File

@ -1,136 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_brightness;
uniform float u_contrast;
uniform float u_hue;
uniform float u_lightness;
uniform float u_saturation;
out vec4 out_color;
// RGB to HSL
vec3 rgb2hsl(vec3 c) {
float maxc = max(max(c.r, c.g), c.b);
float minc = min(min(c.r, c.g), c.b);
float l = (maxc + minc) * 0.5;
float s = 0.0;
float h = 0.0;
if (maxc != minc) {
float d = maxc - minc;
s = l > 0.5 ? d / (2.0 - maxc - minc) : d / (maxc + minc);
if (maxc == c.r) {
h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
} else if (maxc == c.g) {
h = (c.b - c.r) / d + 2.0;
} else {
h = (c.r - c.g) / d + 4.0;
}
h /= 6.0;
}
return vec3(h, s, l);
}
// HSL to RGB
float hue2rgb(float p, float q, float t) {
if (t < 0.0) t += 1.0;
if (t > 1.0) t -= 1.0;
if (t < 1.0/6.0) return p + (q - p) * 6.0 * t;
if (t < 1.0/2.0) return q;
if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
return p;
}
vec3 hsl2rgb(vec3 hsl) {
float r, g, b;
float h = hsl.x;
float s = hsl.y;
float l = hsl.z;
if (s == 0.0) {
r = g = b = l;
} else {
float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
float p = 2.0 * l - q;
r = hue2rgb(p, q, h + 1.0/3.0);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1.0/3.0);
}
return vec3(r, g, b);
}
void main() {
vec4 in_color = texture(in_texture, in_uv);
vec3 color = in_color.rgb;
color = color * u_brightness;
color += vec3(clamp(u_lightness, 0.0, 2.0) - 1.0);
color = (color - 0.5) * u_contrast + 0.5;
vec3 hsl = rgb2hsl(color);
hsl.x = mod(hsl.x + u_hue, 1.0);
hsl.y = clamp(hsl.y * u_saturation, 0.0, 1.0);
color = hsl2rgb(hsl);
out_color = vec4(color, in_color.a);
}
`;
export const FX_colorAdjust = defineImageEffectorFx({
id: 'colorAdjust' as const,
name: i18n.ts._imageEffector._fxs.colorAdjust,
shader,
uniforms: ['lightness', 'contrast', 'hue', 'brightness', 'saturation'] as const,
params: {
lightness: {
type: 'number' as const,
default: 100,
min: 0,
max: 200,
step: 1,
},
contrast: {
type: 'number' as const,
default: 100,
min: 0,
max: 200,
step: 1,
},
hue: {
type: 'number' as const,
default: 0,
min: -360,
max: 360,
step: 1,
},
brightness: {
type: 'number' as const,
default: 100,
min: 0,
max: 200,
step: 1,
},
saturation: {
type: 'number' as const,
default: 100,
min: 0,
max: 200,
step: 1,
},
},
main: ({ gl, u, params }) => {
gl.uniform1f(u.brightness, params.brightness / 100);
gl.uniform1f(u.contrast, params.contrast / 100);
gl.uniform1f(u.hue, params.hue / 360);
gl.uniform1f(u.lightness, params.lightness / 100);
gl.uniform1f(u.saturation, params.saturation / 100);
},
});

View File

@ -1,40 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function loadShader(gl: WebGL2RenderingContext, type: GLenum, source: string): WebGLShader {
const shader = gl.createShader(type);
if (shader == null) {
throw new Error('falied to create shader');
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
gl.deleteShader(shader);
throw new Error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
}
return shader;
}
export function initShaderProgram(gl: WebGL2RenderingContext, vsSource: string, fsSource: string): WebGLProgram {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
console.error(`failed to init shader: ${gl.getProgramInfoLog(shaderProgram)}`);
throw new Error('failed to init shader');
}
return shaderProgram;
}

View File

@ -11,13 +11,13 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.15.31",
"@types/node": "22.15.21",
"@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0"
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1"
},
"dependencies": {
"@tabler/icons-webfont": "3.34.0",
"@tabler/icons-webfont": "3.33.0",
"harfbuzzjs": "0.4.7",
"tiny-glob": "0.2.9",
"tsx": "4.19.4",

View File

@ -24,9 +24,9 @@
"devDependencies": {
"@types/matter-js": "0.19.8",
"@types/seedrandom": "3.0.8",
"@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"@types/node": "22.15.28",
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"nodemon": "3.1.10",
"execa": "9.6.0",
"typescript": "5.8.3",

View File

@ -3212,11 +3212,10 @@ type PinnedUsersResponse = operations['pinned-users']['responses']['200']['conte
type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
// Warning: (ae-forgotten-export) The symbol "AllNullRecord" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "AllNullOrOptionalRecord" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "NonNullableRecord" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'text'>> & AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>> & {
type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>> & {
files: [];
fileIds: [];
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
@ -3749,7 +3748,7 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons
// Warnings were encountered during analysis:
//
// src/entities.ts:54:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:218:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:228:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts

View File

@ -8,11 +8,11 @@
},
"devDependencies": {
"@readme/openapi-parser": "2.7.0",
"@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"@types/node": "22.15.21",
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"openapi-types": "12.1.3",
"openapi-typescript": "7.8.0",
"openapi-typescript": "6.7.6",
"ts-case-convert": "2.1.0",
"tsx": "4.19.4",
"typescript": "5.8.3"

View File

@ -1,130 +0,0 @@
import * as ts from 'typescript';
/**
* TypeScript AST 'never'
* 'paths' TypeAliasDeclaration
* 'operations' InterfaceDeclaration
* TypeLiteralNode/Interfaceから 'PropertySignature' 'never'
* 'never'
*
* @param astNodes ts.Node (: `openapi-typescript` )
* @returns 'never' ts.Node
*/
export function removeNeverPropertiesFromAST(astNodes: readonly ts.Node[]): ts.Node[] {
const factory = ts.factory;
/**
* TypeLiteralNodeやInterfaceDeclarationのmembersからneverプロパティを除去し
*/
function removeNeverPropertiesFromMembers(
members: readonly ts.TypeElement[],
visitType: (node: ts.Node) => ts.Node | undefined,
): { newMembers: ts.TypeElement[]; hasChanged: boolean } {
const newMembers: ts.TypeElement[] = [];
let hasChanged = false;
for (const member of members) {
if (ts.isPropertySignature(member)) {
if (member.type && member.type.kind === ts.SyntaxKind.NeverKeyword) {
hasChanged = true;
continue;
}
let updatedPropertySignature = member;
if (member.type) {
const visitedMemberType = ts.visitNode(member.type, visitType);
if (visitedMemberType && visitedMemberType !== member.type) {
updatedPropertySignature = factory.updatePropertySignature(
member,
member.modifiers,
member.name,
member.questionToken,
visitedMemberType as ts.TypeNode,
);
hasChanged = true;
} else if (visitedMemberType === undefined) {
// 子の型が消された場合、このプロパティも消す
hasChanged = true;
continue;
}
}
newMembers.push(updatedPropertySignature);
} else {
newMembers.push(member);
}
}
return { newMembers, hasChanged };
}
function typeNodeRecursiveVisitor(node: ts.Node): ts.Node | undefined {
if (ts.isTypeLiteralNode(node)) {
const { newMembers, hasChanged } = removeNeverPropertiesFromMembers(node.members, typeNodeRecursiveVisitor);
if (newMembers.length === 0) {
// すべてのプロパティがneverで消された場合、このTypeLiteralNode自体も消す
return undefined;
}
if (hasChanged) {
return factory.updateTypeLiteralNode(node, factory.createNodeArray(newMembers));
}
return node;
}
return ts.visitEachChild(node, typeNodeRecursiveVisitor, undefined);
}
function interfaceRecursiveVisitor(node: ts.Node): ts.Node | undefined {
if (ts.isInterfaceDeclaration(node)) {
const { newMembers, hasChanged } = removeNeverPropertiesFromMembers(node.members, typeNodeRecursiveVisitor);
if (newMembers.length === 0) {
return undefined;
}
if (hasChanged) {
return factory.updateInterfaceDeclaration(
node,
node.modifiers,
node.name,
node.typeParameters,
node.heritageClauses,
newMembers,
);
}
return node;
}
return ts.visitEachChild(node, interfaceRecursiveVisitor, undefined);
}
function topLevelVisitor(node: ts.Node): ts.Node | undefined {
if (ts.isTypeAliasDeclaration(node) && node.name.escapedText === 'paths') {
const newType = ts.visitNode(node.type, typeNodeRecursiveVisitor);
if (newType && newType !== node.type) {
return factory.updateTypeAliasDeclaration(
node,
node.modifiers,
node.name,
node.typeParameters,
newType as ts.TypeNode,
);
} else if (newType === undefined) {
return undefined;
}
}
if (ts.isInterfaceDeclaration(node) && node.name.escapedText === 'operations') {
const result = interfaceRecursiveVisitor(node);
return result;
}
return ts.visitEachChild(node, topLevelVisitor, undefined);
}
const transformedNodes: ts.Node[] = [];
for (const astNode of astNodes) {
const resultNode = ts.visitNode(astNode, topLevelVisitor);
if (resultNode) {
transformedNodes.push(resultNode);
}
}
return transformedNodes;
}

View File

@ -1,12 +1,9 @@
import assert from 'assert';
import { mkdir, readFile, writeFile } from 'fs/promises';
import type { OpenAPIV3_1 } from 'openapi-types';
import { OpenAPIV3_1 } from 'openapi-types';
import { toPascal } from 'ts-case-convert';
import OpenAPIParser from '@readme/openapi-parser';
import openapiTS, { astToString } from 'openapi-typescript';
import type { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
import ts from 'typescript';
import { removeNeverPropertiesFromAST } from './ast-transformer.js';
import openapiTS, { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
async function generateBaseTypes(
openApiDocs: OpenAPIV3_1.Document,
@ -31,6 +28,13 @@ async function generateBaseTypes(
assert('post' in item);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
openApi.paths![key] = {
...('get' in item ? {
get: {
...item.get,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
operationId: ((item as PathItemObject).get as OperationObject).operationId!.replaceAll('get___', ''),
},
} : {}),
post: {
...item.post,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -39,26 +43,15 @@ async function generateBaseTypes(
};
}
const tsNullNode = ts.factory.createLiteralTypeNode(ts.factory.createNull());
const tsBlobNode = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Blob'));
const generatedTypesAst = await openapiTS(openApi, {
const generatedTypes = await openapiTS(openApi, {
exportType: true,
transform(schemaObject) {
if ('format' in schemaObject && schemaObject.format === 'binary') {
if (schemaObject.nullable) {
return ts.factory.createUnionTypeNode([tsBlobNode, tsNullNode]);
} else {
return tsBlobNode;
}
return schemaObject.nullable ? 'Blob | null' : 'Blob';
}
},
});
const filteredAst = removeNeverPropertiesFromAST(generatedTypesAst);
lines.push(astToString(filteredAst));
lines.push(generatedTypes);
lines.push('');
await writeFile(typeFileName, lines.join('\n'));

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.6.1-beta.1",
"version": "2025.6.1-alpha.1",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@ -38,9 +38,9 @@
"@microsoft/api-extractor": "7.52.8",
"@swc/jest": "0.2.38",
"@types/jest": "29.5.14",
"@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"@types/node": "22.15.21",
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"jest": "29.7.0",
"jest-fetch-mock": "3.0.3",
"jest-websocket-mock": "2.5.0",
@ -50,7 +50,7 @@
"execa": "8.0.1",
"tsd": "0.32.0",
"typescript": "5.8.3",
"esbuild": "0.25.5",
"esbuild": "0.25.4",
"glob": "11.0.2"
},
"files": [

View File

@ -77,7 +77,7 @@ export class APIClient {
if (mediaType === 'application/json') {
payload = JSON.stringify({
...(this.assertIsRecord(params) ? params : {}),
...params,
i: credential !== undefined ? credential : this.credential,
});
} else if (mediaType === 'multipart/form-data') {

File diff suppressed because it is too large Load Diff

View File

@ -24,14 +24,10 @@ type NonNullableRecord<T> = {
type AllNullRecord<T> = {
[P in keyof T]: null;
};
type AllNullOrOptionalRecord<T> = {
[P in keyof T]: never;
};
export type PureRenote =
Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'>
& AllNullRecord<Pick<Note, 'text'>>
& AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>>
& AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>>
& { files: []; fileIds: []; }
& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;

View File

@ -22,9 +22,9 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"@types/node": "22.15.28",
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"execa": "9.6.0",
"nodemon": "3.1.10",
"typescript": "5.8.3",

View File

@ -14,7 +14,7 @@
"misskey-js": "workspace:*"
},
"devDependencies": {
"@typescript-eslint/parser": "8.34.0",
"@typescript-eslint/parser": "8.33.0",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
"eslint-plugin-import": "2.31.0",
"nodemon": "3.1.10",

File diff suppressed because it is too large Load Diff

View File

@ -9,16 +9,16 @@
"version": "1.0.0",
"devDependencies": {
"@types/mdast": "4.0.4",
"@types/node": "22.15.31",
"@vitest/coverage-v8": "3.2.3",
"@types/node": "22.15.21",
"@vitest/coverage-v8": "3.1.4",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
"remark-parse": "11.0.0",
"typescript": "5.8.3",
"unified": "11.0.5",
"vite": "6.3.5",
"vite-node": "3.2.3",
"vitest": "3.2.3"
"vite-node": "3.1.4",
"vitest": "3.1.4"
}
},
"node_modules/@ampproject/remapping": {
@ -890,16 +890,6 @@
"win32"
]
},
"node_modules/@types/chai": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
"integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/deep-eql": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@ -909,13 +899,6 @@
"@types/ms": "*"
}
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@ -940,9 +923,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "22.15.31",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz",
"integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==",
"version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -956,16 +939,15 @@
"dev": true
},
"node_modules/@vitest/coverage-v8": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.3.tgz",
"integrity": "sha512-D1QKzngg8PcDoCE8FHSZhREDuEy+zcKmMiMafYse41RZpBE5EDJyKOTdqK3RQfsV2S2nyKor5KCs8PyPRFqKPg==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz",
"integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@bcoe/v8-coverage": "^1.0.2",
"ast-v8-to-istanbul": "^0.3.3",
"debug": "^4.4.1",
"debug": "^4.4.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-lib-source-maps": "^5.0.6",
@ -980,8 +962,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "3.2.3",
"vitest": "3.2.3"
"@vitest/browser": "3.1.4",
"vitest": "3.1.4"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@ -990,15 +972,14 @@
}
},
"node_modules/@vitest/expect": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz",
"integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz",
"integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/spy": "3.2.3",
"@vitest/utils": "3.2.3",
"@vitest/spy": "3.1.4",
"@vitest/utils": "3.1.4",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
@ -1007,13 +988,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz",
"integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz",
"integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.2.3",
"@vitest/spy": "3.1.4",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
@ -1022,7 +1003,7 @@
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
"vite": "^5.0.0 || ^6.0.0"
},
"peerDependenciesMeta": {
"msw": {
@ -1034,9 +1015,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz",
"integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz",
"integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1047,28 +1028,27 @@
}
},
"node_modules/@vitest/runner": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz",
"integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz",
"integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.2.3",
"pathe": "^2.0.3",
"strip-literal": "^3.0.0"
"@vitest/utils": "3.1.4",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz",
"integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz",
"integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.2.3",
"@vitest/pretty-format": "3.1.4",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
@ -1077,26 +1057,26 @@
}
},
"node_modules/@vitest/spy": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz",
"integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz",
"integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyspy": "^4.0.3"
"tinyspy": "^3.0.2"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz",
"integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz",
"integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.2.3",
"@vitest/pretty-format": "3.1.4",
"loupe": "^3.1.3",
"tinyrainbow": "^2.0.0"
},
@ -1140,18 +1120,6 @@
"node": ">=12"
}
},
"node_modules/ast-v8-to-istanbul": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz",
"integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"estree-walker": "^3.0.3",
"js-tokens": "^9.0.1"
}
},
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@ -1260,9 +1228,9 @@
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1589,13 +1557,6 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@ -2600,19 +2561,6 @@
"node": ">=8"
}
},
"node_modules/strip-literal": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
"integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^9.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -2655,9 +2603,9 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2672,9 +2620,9 @@
}
},
"node_modules/tinypool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz",
"integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
"dev": true,
"license": "MIT",
"engines": {
@ -2692,9 +2640,9 @@
}
},
"node_modules/tinyspy": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
"integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
"dev": true,
"license": "MIT",
"engines": {
@ -2912,17 +2860,17 @@
}
},
"node_modules/vite-node": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz",
"integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz",
"integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.4.1",
"debug": "^4.4.0",
"es-module-lexer": "^1.7.0",
"pathe": "^2.0.3",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
"vite": "^5.0.0 || ^6.0.0"
},
"bin": {
"vite-node": "vite-node.mjs"
@ -2935,34 +2883,32 @@
}
},
"node_modules/vitest": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz",
"integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz",
"integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.3",
"@vitest/mocker": "3.2.3",
"@vitest/pretty-format": "^3.2.3",
"@vitest/runner": "3.2.3",
"@vitest/snapshot": "3.2.3",
"@vitest/spy": "3.2.3",
"@vitest/utils": "3.2.3",
"@vitest/expect": "3.1.4",
"@vitest/mocker": "3.1.4",
"@vitest/pretty-format": "^3.1.4",
"@vitest/runner": "3.1.4",
"@vitest/snapshot": "3.1.4",
"@vitest/spy": "3.1.4",
"@vitest/utils": "3.1.4",
"chai": "^5.2.0",
"debug": "^4.4.1",
"debug": "^4.4.0",
"expect-type": "^1.2.1",
"magic-string": "^0.30.17",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"std-env": "^3.9.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.2",
"tinyglobby": "^0.2.14",
"tinypool": "^1.1.0",
"tinyglobby": "^0.2.13",
"tinypool": "^1.0.2",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
"vite-node": "3.2.3",
"vite": "^5.0.0 || ^6.0.0",
"vite-node": "3.1.4",
"why-is-node-running": "^2.3.0"
},
"bin": {
@ -2978,8 +2924,8 @@
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.2.3",
"@vitest/ui": "3.2.3",
"@vitest/browser": "3.1.4",
"@vitest/ui": "3.1.4",
"happy-dom": "*",
"jsdom": "*"
},

View File

@ -10,15 +10,15 @@
},
"devDependencies": {
"@types/mdast": "4.0.4",
"@types/node": "22.15.31",
"@vitest/coverage-v8": "3.2.3",
"@types/node": "22.15.21",
"@vitest/coverage-v8": "3.1.4",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
"remark-parse": "11.0.0",
"typescript": "5.8.3",
"unified": "11.0.5",
"vite": "6.3.5",
"vite-node": "3.2.3",
"vitest": "3.2.3"
"vite-node": "3.1.4",
"vitest": "3.1.4"
}
}