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: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' 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 - name: Publish package
run: | run: |
pnpm i --frozen-lockfile pnpm i --frozen-lockfile

View File

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

View File

@ -34,6 +34,7 @@ describe('Before setup instance', () => {
cy.intercept('POST', '/api/admin/update-meta').as('update-meta'); 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-next]').click();
cy.get('[data-cy-server-name] input').type('Testskey'); cy.get('[data-cy-server-name] input').type('Testskey');
cy.get('[data-cy-server-setup-wizard-apply]').click(); 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; "disableFederationDescription": string;
}; };
"_postForm": { "_postForm": {
/**
*
*/
"quitInspiteOfThereAreUnuploadedFilesConfirm": string;
/**
* 稿
*/
"uploaderTip": string;
/** /**
* ... * ...
*/ */
@ -12184,10 +12176,6 @@ export interface Locale extends ILocale {
* *
*/ */
"grayscale": string; "grayscale": string;
/**
* 調
*/
"colorAdjust": string;
/** /**
* *
*/ */

View File

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

View File

@ -220,7 +220,6 @@ silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี
mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้" mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้"
operations: "ดำเนินการ" operations: "ดำเนินการ"
software: "ซอฟต์แวร์" software: "ซอฟต์แวร์"
softwareName: "ชื่อซอฟต์แวร์"
version: "เวอร์ชั่น" version: "เวอร์ชั่น"
metadata: "Metadata" metadata: "Metadata"
withNFiles: "{n} ไฟล์" withNFiles: "{n} ไฟล์"
@ -1294,10 +1293,6 @@ federationDisabled: "เซิร์ฟเวอร์นี้ปิดกา
reactAreYouSure: "คุณต้องการที่จะตอบสนองต่อ \" {emoji}\" หรือไม่?" reactAreYouSure: "คุณต้องการที่จะตอบสนองต่อ \" {emoji}\" หรือไม่?"
markAsSensitiveConfirm: "คุณต้องการทำเครื่องหมายสื่อนี้ว่าละเอียดอ่อนหรือไม่?" markAsSensitiveConfirm: "คุณต้องการทำเครื่องหมายสื่อนี้ว่าละเอียดอ่อนหรือไม่?"
unmarkAsSensitiveConfirm: "คุณต้องการลบการกำหนดความไวของสื่อนี้หรือไม่?" unmarkAsSensitiveConfirm: "คุณต้องการลบการกำหนดความไวของสื่อนี้หรือไม่?"
preferences: "การตั้งค่าสภาพแวดล้อม"
preferencesProfile: "โปรไฟล์การกำหนดค่า"
preferenceSyncConflictTitle: "การตั้งค่ามีอยู่บนเซิร์ฟเวอร์"
preferenceSyncConflictText: "รายการการตั้งค่าที่เปิดใช้งานการซิงโครไนซ์จะจัดเก็บค่าไว้บนเซิร์ฟเวอร์ และพบค่าที่จัดเก็บบนเซิร์ฟเวอร์สำหรับรายการการตั้งค่านี้ คุณต้องการทำอย่างไร?"
postForm: "แบบฟอร์มการโพสต์" postForm: "แบบฟอร์มการโพสต์"
information: "เกี่ยวกับ" information: "เกี่ยวกับ"
right: "ขวา" right: "ขวา"
@ -1310,7 +1305,6 @@ _chat:
send: "ส่ง" send: "ส่ง"
_settings: _settings:
webhook: "Webhook" webhook: "Webhook"
preferencesBanner: "คุณสามารถกำหนดค่าพฤติกรรมโดยรวมของไคลเอนต์ได้ตามความต้องการของคุณ"
_accountSettings: _accountSettings:
requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา" requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา"
requireSigninToViewContentsDescription1: "ต้องเข้าสู่ระบบเพื่อดูบันทึกและเนื้อหาอื่น ๆ ทั้งหมดที่คุณสร้าง คาดว่าจะมีประสิทธิผลในการป้องกันไม่ให้ข้อมูลถูกเก็บรวบรวมโดยโปรแกรมรวบรวมข้อมูล" 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" mediaSilenceThisInstance: "Tắt nội dung đa phương tiện từ máy chủ này"
operations: "Vận hành" operations: "Vận hành"
software: "Phần mềm" software: "Phần mềm"
softwareName: "Tên phần mềm"
version: "Phiên bản" version: "Phiên bản"
metadata: "Metadata" metadata: "Metadata"
withNFiles: "{n} tập tin" 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?" reactAreYouSure: "Bạn có muốn phản hồi với \" {emoji} \" không?"
preferences: "Thiết lập môi trường" preferences: "Thiết lập môi trường"
accessibility: "Khả năng tiếp cận" 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" paste: "dán"
postForm: "Mẫu đăng" postForm: "Mẫu đăng"
information: "Giới thiệu" information: "Giới thiệu"
@ -1227,8 +1223,6 @@ _chat:
members: "Thành viên" members: "Thành viên"
home: "Trang chính" home: "Trang chính"
send: "Gửi" 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: _accountSettings:
requireSigninToViewContents: "Yêu cầu đăng nhập để xem nội dung" 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." 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", "name": "misskey",
"version": "2025.6.1-beta.1", "version": "2025.6.1-alpha.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

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

View File

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

View File

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

View File

@ -380,7 +380,9 @@ describe('User', () => {
strictEqual(followers.length, 1); // followed by Bob strictEqual(followers.length, 1); // followed by Bob
await alice.client.request('i/delete-account', { password: alice.password }); 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 }); const following = await bob.client.request('users/following', { userId: bob.id });
strictEqual(following.length, 0); // no following relation strictEqual(following.length, 0); // no following relation
@ -478,7 +480,9 @@ describe('User', () => {
strictEqual(followers.length, 1); // followed by Bob strictEqual(followers.length, 1); // followed by Bob
await aAdmin.client.request('admin/suspend-user', { userId: alice.id }); 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 }); const following = await bob.client.request('users/following', { userId: bob.id });
strictEqual(following.length, 0); // no following relation strictEqual(following.length, 0); // no following relation

View File

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

View File

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

View File

@ -24,7 +24,7 @@
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2", "@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.1.4", "@rollup/pluginutils": "5.1.4",
"@sentry/vue": "9.27.0", "@sentry/vue": "9.24.0",
"@syuilo/aiscript": "0.19.0", "@syuilo/aiscript": "0.19.0",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.4", "@vitejs/plugin-vue": "5.2.4",
@ -60,10 +60,10 @@
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"photoswipe": "5.4.4", "photoswipe": "5.4.4",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"rollup": "4.42.0", "rollup": "4.41.1",
"sanitize-html": "2.17.0", "sanitize-html": "2.17.0",
"sass": "1.89.2", "sass": "1.89.0",
"shiki": "3.6.0", "shiki": "3.4.2",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.177.0", "three": "0.177.0",
@ -98,36 +98,36 @@
"@storybook/types": "8.6.14", "@storybook/types": "8.6.14",
"@storybook/vue3": "8.6.14", "@storybook/vue3": "8.6.14",
"@storybook/vue3-vite": "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", "@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0", "@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8", "@types/estree": "1.0.7",
"@types/matter-js": "0.19.8", "@types/matter-js": "0.19.8",
"@types/micromatch": "4.0.9", "@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/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0", "@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.34.0", "@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.34.0", "@typescript-eslint/parser": "8.33.0",
"@vitest/coverage-v8": "3.2.3", "@vitest/coverage-v8": "3.1.4",
"@vue/compiler-core": "3.5.16", "@vue/compiler-core": "3.5.16",
"@vue/runtime-core": "3.5.16", "@vue/runtime-core": "3.5.16",
"acorn": "8.15.0", "acorn": "8.14.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "14.4.1", "cypress": "14.4.0",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "10.2.0", "eslint-plugin-vue": "10.1.0",
"fast-glob": "3.3.3", "fast-glob": "3.3.3",
"happy-dom": "17.6.3", "happy-dom": "17.5.6",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"minimatch": "10.0.1", "minimatch": "10.0.1",
"msw": "2.10.2", "msw": "2.8.6",
"msw-storybook-addon": "2.0.5", "msw-storybook-addon": "2.0.4",
"nodemon": "3.1.10", "nodemon": "3.1.10",
"prettier": "3.5.3", "prettier": "3.5.3",
"react": "19.1.0", "react": "19.1.0",
@ -137,7 +137,7 @@
"storybook": "8.6.14", "storybook": "8.6.14",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.3", "vitest": "3.1.4",
"vitest-fetch-mock": "0.4.5", "vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "2.2.10", "vue-component-type-helpers": "2.2.10",
"vue-eslint-parser": "10.1.3", "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 :class="$style.root" class="_gaps">
<div v-for="[k, v] in Object.entries(fx.params)" :key="k"> <div v-for="[k, v] in Object.entries(fx.params)" :key="k">
<MkSwitch <MkSwitch v-if="v.type === 'boolean'" v-model="layer.params[k]">
v-if="v.type === 'boolean'"
v-model="layer.params[k]"
>
<template #label>{{ k }}</template> <template #label>{{ k }}</template>
</MkSwitch> </MkSwitch>
<MkRange <MkRange v-else-if="v.type === 'number'" v-model="layer.params[k]" continuousUpdate :min="v.min" :max="v.max" :step="v.step">
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;
}
}"
>
<template #label>{{ k }}</template> <template #label>{{ k }}</template>
</MkRange> </MkRange>
<MkRadios <MkRadios v-else-if="v.type === 'number:enum'" v-model="layer.params[k]">
v-else-if="v.type === 'number:enum'"
v-model="layer.params[k]"
>
<template #label>{{ k }}</template> <template #label>{{ k }}</template>
<option v-for="item in v.enum" :value="item.value">{{ item.label }}</option> <option v-for="item in v.enum" :value="item.value">{{ item.label }}</option>
</MkRadios> </MkRadios>
<div v-else-if="v.type === 'seed'"> <div v-else-if="v.type === 'seed'">
<MkRange <MkRange v-model="layer.params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">
v-model="layer.params[k]"
continuousUpdate
type="number"
:min="0"
:max="10000"
:step="1"
>
<template #label>{{ k }}</template> <template #label>{{ k }}</template>
</MkRange> </MkRange>
</div> </div>
<MkInput <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; }">
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; }"
>
<template #label>{{ k }}</template> <template #label>{{ k }}</template>
</MkInput> </MkInput>
</div> </div>
@ -72,14 +40,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue';
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js'; import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.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'; import { FXS } from '@/utility/image-effector/fxs.js';
const layer = defineModel<ImageEffectorLayer>('layer', { required: true }); const layer = defineModel<ImageEffectorLayer>('layer', { required: true });
@ -93,24 +69,6 @@ const emit = defineEmits<{
(e: 'swapUp'): void; (e: 'swapUp'): void;
(e: 'swapDown'): 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> </script>
<style module> <style module>

View File

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

View File

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

View File

@ -72,29 +72,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> <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"/> <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"/> <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"/> <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 v-if="showingOptions" style="padding: 8px 16px;">
</div> </div>
<footer :class="$style.footer"> <footer :class="$style.footer">
<div :class="$style.footerLeft"> <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" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><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.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></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.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-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-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>
<div :class="$style.footerRight"> <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> </div>
</footer> </footer>
<datalist id="hashtags"> <datalist id="hashtags">
@ -110,12 +105,10 @@ import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor'; import insertTextAtCursor from 'insert-text-at-cursor';
import { toASCII } from 'punycode.js'; import { toASCII } from 'punycode.js';
import { host, url } from '@@/js/config.js'; import { host, url } from '@@/js/config.js';
import MkUploaderItems from './MkUploaderItems.vue';
import type { ShallowRef } from 'vue'; import type { ShallowRef } from 'vue';
import type { PostFormProps } from '@/types/post-form.js'; import type { PostFormProps } from '@/types/post-form.js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue'; import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
import type { UploaderItem } from '@/composables/use-uploader.js';
import MkNotePreview from '@/components/MkNotePreview.vue'; import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
import XTextCounter from '@/components/MkPostForm.TextCounter.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 { Autocomplete } from '@/utility/autocomplete.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.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 { store } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -145,7 +138,6 @@ import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js'; import { DI } from '@/di.js';
import { globalEvents } from '@/events.js'; import { globalEvents } from '@/events.js';
import { checkDragDataType, getDragData } from '@/drag-and-drop.js'; import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
import { useUploader } from '@/composables/use-uploader.js';
const $i = ensureSignin(); const $i = ensureSignin();
@ -209,15 +201,6 @@ const justEndedComposition = ref(false);
const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote); const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote);
const postFormActions = getPluginHandlers('post_form_action'); 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 => { const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : ''; let key = props.channel ? `channel:${props.channel.id}` : '';
@ -275,11 +258,10 @@ const cwTextLength = computed((): number => {
const maxCwTextLength = 100; const maxCwTextLength = 100;
const canPost = computed((): boolean => { 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 <= textLength.value ||
1 <= files.value.length || 1 <= files.value.length ||
1 <= uploader.items.value.length ||
poll.value != null || poll.value != null ||
renoteTargetNote.value != null || renoteTargetNote.value != null ||
quoteId.value != null quoteId.value != null
@ -452,20 +434,17 @@ function focus() {
} }
} }
function chooseFileFromPc(ev: MouseEvent) { function chooseFileFrom(ev) {
if (props.mock) return; if (props.mock) return;
os.chooseFileFromPc({ multiple: true }).then(files => { selectFile({
if (files.length === 0) return; anchorElement: ev.currentTarget ?? ev.target,
uploader.addFiles(files); multiple: true,
}); label: i18n.ts.attachFile,
} }).then(files_ => {
for (const file of files_) {
function chooseFileFromDrive(ev: MouseEvent) { files.value.push(file);
if (props.mock) return; }
chooseDriveFile({ multiple: true }).then(driveFiles => {
files.value.push(...driveFiles);
}); });
} }
@ -592,11 +571,6 @@ function showOtherSettings() {
toggleReactionAcceptance(); toggleReactionAcceptance();
}, },
}, { type: 'divider' }, { }, { type: 'divider' }, {
type: 'switch',
icon: 'ti ti-eye',
text: i18n.ts.preview,
ref: showPreview,
}, {
icon: 'ti ti-trash', icon: 'ti ti-trash',
text: i18n.ts.reset, text: i18n.ts.reset,
danger: true, danger: true,
@ -823,15 +797,6 @@ function isAnnoying(text: string): boolean {
text.includes('$[position'); 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) { async function post(ev?: MouseEvent) {
if (ev) { if (ev) {
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null; 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 = { let postData = {
text: text.value === '' ? null : text.value, text: text.value === '' ? null : text.value,
fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined, fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
@ -1082,16 +1043,6 @@ function openAccountMenu(ev: MouseEvent) {
}, ev); }, 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(() => { onMounted(() => {
if (props.autofocus) { if (props.autofocus) {
focus(); 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({ defineExpose({
clear, clear,
canClose,
}); });
</script> </script>

View File

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

View File

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

View File

@ -23,7 +23,37 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._uploader.tip }} {{ i18n.ts._uploader.tip }}
</MkTip> </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"> <div v-if="props.multiple">
<MkButton style="margin: auto;" :iconOnly="true" rounded @click="chooseFile($event)"><i class="ti ti-plus"></i></MkButton> <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> <template #footer>
<div class="_buttonsCenter"> <div class="_buttonsCenter">
<MkButton v-if="uploader.uploading.value" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</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 :disabled="!uploader.readyForUpload.value" @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</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="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> <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> </MkModalWindow>
</template> </template>
<script lang="ts">
export type UploaderDialogFeatures = {
effect?: boolean;
watermark?: boolean;
crop?: boolean;
};
</script>
<script lang="ts" setup> <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 * 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 MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import MkButton from '@/components/MkButton.vue'; 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 * as os from '@/os.js';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';
import { useUploader } from '@/composables/use-uploader.js'; import { WatermarkRenderer } from '@/utility/watermark.js';
import MkUploaderItems from '@/components/MkUploaderItems.vue';
const $i = ensureSignin(); 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<{ const props = withDefaults(defineProps<{
files: File[]; files: File[];
folderId?: string | null; folderId?: string | null;
multiple?: boolean; multiple?: boolean;
features?: UploaderFeatures; features?: UploaderDialogFeatures;
}>(), { }>(), {
multiple: true, 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<{ const emit = defineEmits<{
(ev: 'done', driveFiles: Misskey.entities.DriveFile[]): void; (ev: 'done', driveFiles: Misskey.entities.DriveFile[]): void;
(ev: 'canceled'): void; (ev: 'canceled'): void;
(ev: 'closed'): 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 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 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 canDone = computed(() => items.value.some(item => item.uploaded != null));
const overallProgress = computed(() => { const overallProgress = computed(() => {
const max = items.value.length; const max = items.value.length;
@ -106,6 +195,27 @@ const overallProgress = computed(() => {
return Math.round((v / max) * 100); 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, () => { watch(items, () => {
if (items.value.length === 0) { if (items.value.length === 0) {
emit('canceled'); emit('canceled');
@ -128,16 +238,11 @@ async function cancel() {
}); });
if (canceled) return; if (canceled) return;
uploader.abortAll(); abortAll();
emit('canceled'); emit('canceled');
dialog.value?.close(); dialog.value?.close();
} }
function upload() {
firstUploadAttempted.value = true;
uploader.upload();
}
async function abortWithConfirm() { async function abortWithConfirm() {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'question', type: 'question',
@ -147,11 +252,11 @@ async function abortWithConfirm() {
}); });
if (canceled) return; if (canceled) return;
uploader.abortAll(); abortAll();
} }
async function done() { async function done() {
if (!uploader.allItemsUploaded.value) { if (items.value.some(item => item.uploaded == null)) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'question', type: 'question',
text: i18n.ts._uploader.doneConfirm, text: i18n.ts._uploader.doneConfirm,
@ -165,20 +270,381 @@ async function done() {
dialog.value?.close(); dialog.value?.close();
} }
async function chooseFile(ev: MouseEvent) { function showMenu(ev: MouseEvent, item: UploaderItem) {
const newFiles = await os.chooseFileFromPc({ multiple: true }); const menu: MenuItem[] = [];
uploader.addFiles(newFiles);
} 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); os.popupMenu(menu, ev.currentTarget ?? ev.target);
} }
function showPerItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) { async function upload() { //
const menu = uploader.getMenu(item); firstUploadAttempted.value = true;
os.contextMenu(menu, ev);
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> </script>
<style lang="scss" module> <style lang="scss" module>
@ -200,4 +666,127 @@ function showPerItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
background: var(--MI_THEME-warn); 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> </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 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 style="font-weight: bold;"><i class="ti ti-bulb"></i> {{ i18n.ts.tip }}:</div>
<div><slot></slot></div> <div><slot></slot></div>
<div> <MkButton primary rounded small @click="closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
<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>
</div> </div>
</template> </template>
@ -18,30 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { TIPS, hideAllTips, closeTip } from '@/tips.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
k: typeof TIPS[number]; k: keyof (typeof store['s']['tips']);
warn?: boolean; warn?: boolean;
}>(), { }>(), {
warn: false, warn: false,
}); });
function _closeTip() { function closeTip() {
closeTip(props.k); store.set('tips', {
} ...store.r.tips.value,
[props.k]: true,
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);
} }
</script> </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 MkRolePreview from '@/components/MkRolePreview.vue';
import { signout } from '@/signout.js'; import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.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(); const $i = ensureSignin();
@ -205,12 +205,16 @@ function migrate() {
} }
function resetAllTips() { function resetAllTips() {
_resetAllTips(); store.set('tips', {});
os.success(); os.success();
} }
function hideAllTips() { function hideAllTips() {
_hideAllTips(); const v = {};
for (const k of TIPS) {
v[k] = true;
}
store.set('tips', v);
os.success(); os.success();
} }

View File

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

View File

@ -81,6 +81,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkButton> </MkButton>
</div> </div>
<div v-else-if="step === 1" class="_gaps_m"> <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="text-align: center;" class="_gaps_s">
<div style="font-size: 120%;"><b>{{ i18n.ts._serverSetupWizard.serverSetting }}</b></div> <div style="font-size: 120%;"><b>{{ i18n.ts._serverSetupWizard.serverSetting }}</b></div>
<div>{{ i18n.ts._serverSetupWizard.youCanEasilyConfigureOptimalServerSettingsWithThisWizard }}</div> <div>{{ i18n.ts._serverSetupWizard.youCanEasilyConfigureOptimalServerSettingsWithThisWizard }}</div>
@ -93,17 +105,12 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._serverSetupWizard.skipSettings }} {{ i18n.ts._serverSetupWizard.skipSettings }}
</MkButton> </MkButton>
</div> </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 style="text-align: center;" class="_gaps_s">
<div><b>{{ i18n.ts._serverSetupWizard.settingsCompleted }}</b></div> <div><b>{{ i18n.ts._serverSetupWizard.settingsCompleted }}</b></div>
<div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description }}</div> <div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description }}</div>
<div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description2 }}</div> <div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description2 }}</div>
</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"> <div class="_buttonsCenter">
<MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="finish"> <MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="finish">
{{ i18n.ts.start }} {{ i18n.ts.start }}
@ -225,11 +232,4 @@ function finish() {
font-weight: normal; font-weight: normal;
opacity: 0.7; opacity: 0.7;
} }
.donation {
background: var(--MI_THEME-accentedBg);
border-radius: 12px;
padding: 16px;
text-align: center;
}
</style> </style>

View File

@ -10,11 +10,22 @@ import darkTheme from '@@/themes/d-green-lime.json5';
import { hemisphere } from '@@/js/intl-const.js'; import { hemisphere } from '@@/js/intl-const.js';
import type { DeviceKind } from '@/utility/device-kind.js'; import type { DeviceKind } from '@/utility/device-kind.js';
import type { Plugin } from '@/plugin.js'; import type { Plugin } from '@/plugin.js';
import type { TIPS } from '@/tips.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { Pizzax } from '@/lib/pizzax.js'; import { Pizzax } from '@/lib/pizzax.js';
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.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) * (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 MenuRadioOptionsDef = Record<string, any>;
type Text = string | ComputedRef<string>;
export type MenuAction = (ev: MouseEvent) => void; export type MenuAction = (ev: MouseEvent) => void;
export type MenuDivider = { type: 'divider' }; export type MenuDivider = { type: 'divider' };
export type MenuNull = undefined; export type MenuNull = undefined;
export type MenuLabel = { type: 'label', text: Text, caption?: Text }; export type MenuLabel = { type: 'label', text: string, caption?: string };
export type MenuLink = { type: 'link', to: string, text: Text, caption?: Text, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; 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: Text, caption?: Text, icon?: string, indicate?: boolean }; 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 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 MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, caption?: string, 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 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: Text, caption?: Text, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> }; 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: Text, caption?: Text, action: MenuAction; active?: boolean | ComputedRef<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 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' }; export type MenuPending = { type: 'pending' };

View File

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

View File

@ -4,7 +4,6 @@
*/ */
import { getProxiedImageUrl } from '../media-proxy.js'; import { getProxiedImageUrl } from '../media-proxy.js';
import { initShaderProgram } from '../webgl.js';
type ParamTypeToPrimitive = { type ParamTypeToPrimitive = {
'number': number; '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>>> { export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, any>>> {
private gl: WebGL2RenderingContext; private gl: WebGL2RenderingContext;
private canvas: HTMLCanvasElement | null = null; private canvas: HTMLCanvasElement | null = null;
private renderTextureProgram: WebGLProgram;
private renderInvertedTextureProgram: WebGLProgram;
private renderWidth: number; private renderWidth: number;
private renderHeight: number; private renderHeight: number;
private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement; 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 shaderCache: Map<string, WebGLProgram> = new Map();
private perLayerResultTextures: Map<string, WebGLTexture> = new Map(); private perLayerResultTextures: Map<string, WebGLTexture> = new Map();
private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map(); private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map();
private nopProgram: WebGLProgram;
private fxs: [...IEX]; private fxs: [...IEX];
private paramTextures: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map(); 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.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); gl.bindTexture(gl.TEXTURE_2D, null);
this.nopProgram = initShaderProgram(this.gl, `#version 300 es this.renderTextureProgram = this.initShaderProgram(`#version 300 es
in vec2 position; in vec2 position;
out vec2 in_uv; out vec2 in_uv;
void main() { void main() {
in_uv = (position + 1.0) / 2.0; 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 `, `#version 300 es
precision mediump float; precision mediump float;
@ -134,28 +134,82 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
} }
`); `);
// レジスタ番号はシェーダープログラムに属しているわけではなく、独立の存在なので、とりあえず nopProgram を使って設定する(その後は効果が持続する) this.renderInvertedTextureProgram = this.initShaderProgram(`#version 300 es
// ref. https://qiita.com/emadurandal/items/5966c8374f03d4de3266 in vec2 position;
const positionLocation = gl.getAttribLocation(this.nopProgram, 'position'); out vec2 in_uv;
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation); 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 gl = this.gl;
const fx = this.fxs.find(fx => fx.id === layer.fxId); const fx = this.fxs.find(fx => fx.id === layer.fxId);
if (fx == null) return; if (fx == null) return;
const cachedShader = this.shaderCache.get(fx.id); 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; in vec2 position;
uniform bool u_invert;
out vec2 in_uv; out vec2 in_uv;
void main() { void main() {
in_uv = (position + 1.0) / 2.0; 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); `, fx.shader);
if (cachedShader == null) { 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'); const in_resolution = gl.getUniformLocation(shaderProgram, 'in_resolution');
gl.uniform2fv(in_resolution, [this.renderWidth, this.renderHeight]); 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.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, preTexture); gl.bindTexture(gl.TEXTURE_2D, preTexture);
const in_texture = gl.getUniformLocation(shaderProgram, 'in_texture'); const in_texture = gl.getUniformLocation(shaderProgram, 'in_texture');
@ -202,23 +253,27 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
public render() { public render() {
const gl = this.gl; const gl = this.gl;
// 入力をそのまま出力 {
if (this.layers.length === 0) {
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture); gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture);
gl.useProgram(this.nopProgram); gl.useProgram(this.renderTextureProgram);
gl.uniform1i(gl.getUniformLocation(this.nopProgram, 'u_texture')!, 0); 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); gl.drawArrays(gl.TRIANGLES, 0, 6);
return;
} }
// --------------------
let preTexture = this.originalImageTexture; let preTexture = this.originalImageTexture;
for (const layer of this.layers) { for (const layer of this.layers) {
const isLast = layer === this.layers.at(-1);
const cachedResultTexture = this.perLayerResultTextures.get(layer.id); const cachedResultTexture = this.perLayerResultTextures.get(layer.id);
const resultTexture = cachedResultTexture ?? createTexture(gl); const resultTexture = cachedResultTexture ?? createTexture(gl);
if (cachedResultTexture == null) { if (cachedResultTexture == null) {
@ -228,22 +283,30 @@ 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.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.renderWidth, this.renderHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.bindTexture(gl.TEXTURE_2D, null); gl.bindTexture(gl.TEXTURE_2D, null);
if (isLast) { const cachedResultFrameBuffer = this.perLayerResultFrameBuffers.get(layer.id);
gl.bindFramebuffer(gl.FRAMEBUFFER, null); const resultFrameBuffer = cachedResultFrameBuffer ?? gl.createFramebuffer()!;
} else { if (cachedResultFrameBuffer == null) {
const cachedResultFrameBuffer = this.perLayerResultFrameBuffers.get(layer.id); this.perLayerResultFrameBuffers.set(layer.id, resultFrameBuffer);
const resultFrameBuffer = cachedResultFrameBuffer ?? gl.createFramebuffer()!;
if (cachedResultFrameBuffer == null) {
this.perLayerResultFrameBuffers.set(layer.id, resultFrameBuffer);
}
gl.bindFramebuffer(gl.FRAMEBUFFER, resultFrameBuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, resultTexture, 0);
} }
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; 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[]) { public async setLayers(layers: ImageEffectorLayer[]) {
@ -303,8 +366,6 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
* disposeCanvas = true loseContextを呼ぶためcanvasも再利用不可になるので注意 * disposeCanvas = true loseContextを呼ぶためcanvasも再利用不可になるので注意
*/ */
public destroy(disposeCanvas = true) { public destroy(disposeCanvas = true) {
this.gl.deleteProgram(this.nopProgram);
for (const shader of this.shaderCache.values()) { for (const shader of this.shaderCache.values()) {
this.gl.deleteProgram(shader); this.gl.deleteProgram(shader);
} }
@ -325,6 +386,8 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
} }
this.paramTextures.clear(); this.paramTextures.clear();
this.gl.deleteProgram(this.renderTextureProgram);
this.gl.deleteProgram(this.renderInvertedTextureProgram);
this.gl.deleteTexture(this.originalImageTexture); this.gl.deleteTexture(this.originalImageTexture);
if (disposeCanvas) { if (disposeCanvas) {

View File

@ -5,7 +5,6 @@
import { FX_checker } from './fxs/checker.js'; import { FX_checker } from './fxs/checker.js';
import { FX_chromaticAberration } from './fxs/chromaticAberration.js'; import { FX_chromaticAberration } from './fxs/chromaticAberration.js';
import { FX_colorAdjust } from './fxs/colorAdjust.js';
import { FX_colorClamp } from './fxs/colorClamp.js'; import { FX_colorClamp } from './fxs/colorClamp.js';
import { FX_colorClampAdvanced } from './fxs/colorClampAdvanced.js'; import { FX_colorClampAdvanced } from './fxs/colorClampAdvanced.js';
import { FX_distort } from './fxs/distort.js'; import { FX_distort } from './fxs/distort.js';
@ -27,7 +26,6 @@ export const FXS = [
FX_mirror, FX_mirror,
FX_invert, FX_invert,
FX_grayscale, FX_grayscale,
FX_colorAdjust,
FX_colorClamp, FX_colorClamp,
FX_colorClampAdvanced, FX_colorClampAdvanced,
FX_distort, 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" "lint": "pnpm typecheck && pnpm eslint"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "22.15.31", "@types/node": "22.15.21",
"@types/wawoff2": "1.0.2", "@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.34.0", "@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.34.0" "@typescript-eslint/parser": "8.32.1"
}, },
"dependencies": { "dependencies": {
"@tabler/icons-webfont": "3.34.0", "@tabler/icons-webfont": "3.33.0",
"harfbuzzjs": "0.4.7", "harfbuzzjs": "0.4.7",
"tiny-glob": "0.2.9", "tiny-glob": "0.2.9",
"tsx": "4.19.4", "tsx": "4.19.4",

View File

@ -24,9 +24,9 @@
"devDependencies": { "devDependencies": {
"@types/matter-js": "0.19.8", "@types/matter-js": "0.19.8",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/node": "22.15.31", "@types/node": "22.15.28",
"@typescript-eslint/eslint-plugin": "8.34.0", "@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.34.0", "@typescript-eslint/parser": "8.33.0",
"nodemon": "3.1.10", "nodemon": "3.1.10",
"execa": "9.6.0", "execa": "9.6.0",
"typescript": "5.8.3", "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']; 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 "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 // Warning: (ae-forgotten-export) The symbol "NonNullableRecord" needs to be exported by the entry point index.d.ts
// //
// @public (undocumented) // @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: []; files: [];
fileIds: []; fileIds: [];
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>; } & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
@ -3749,7 +3748,7 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons
// Warnings were encountered during analysis: // 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.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: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 // 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": { "devDependencies": {
"@readme/openapi-parser": "2.7.0", "@readme/openapi-parser": "2.7.0",
"@types/node": "22.15.31", "@types/node": "22.15.21",
"@typescript-eslint/eslint-plugin": "8.34.0", "@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.34.0", "@typescript-eslint/parser": "8.32.1",
"openapi-types": "12.1.3", "openapi-types": "12.1.3",
"openapi-typescript": "7.8.0", "openapi-typescript": "6.7.6",
"ts-case-convert": "2.1.0", "ts-case-convert": "2.1.0",
"tsx": "4.19.4", "tsx": "4.19.4",
"typescript": "5.8.3" "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 assert from 'assert';
import { mkdir, readFile, writeFile } from 'fs/promises'; 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 { toPascal } from 'ts-case-convert';
import OpenAPIParser from '@readme/openapi-parser'; import OpenAPIParser from '@readme/openapi-parser';
import openapiTS, { astToString } from 'openapi-typescript'; import openapiTS, { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
import type { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
import ts from 'typescript';
import { removeNeverPropertiesFromAST } from './ast-transformer.js';
async function generateBaseTypes( async function generateBaseTypes(
openApiDocs: OpenAPIV3_1.Document, openApiDocs: OpenAPIV3_1.Document,
@ -31,6 +28,13 @@ async function generateBaseTypes(
assert('post' in item); assert('post' in item);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
openApi.paths![key] = { 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: { post: {
...item.post, ...item.post,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // 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 generatedTypes = await openapiTS(openApi, {
const tsBlobNode = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Blob'));
const generatedTypesAst = await openapiTS(openApi, {
exportType: true, exportType: true,
transform(schemaObject) { transform(schemaObject) {
if ('format' in schemaObject && schemaObject.format === 'binary') { if ('format' in schemaObject && schemaObject.format === 'binary') {
if (schemaObject.nullable) { return schemaObject.nullable ? 'Blob | null' : 'Blob';
return ts.factory.createUnionTypeNode([tsBlobNode, tsNullNode]);
} else {
return tsBlobNode;
}
} }
}, },
}); });
lines.push(generatedTypes);
const filteredAst = removeNeverPropertiesFromAST(generatedTypesAst);
lines.push(astToString(filteredAst));
lines.push(''); lines.push('');
await writeFile(typeFileName, lines.join('\n')); await writeFile(typeFileName, lines.join('\n'));

View File

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

View File

@ -77,7 +77,7 @@ export class APIClient {
if (mediaType === 'application/json') { if (mediaType === 'application/json') {
payload = JSON.stringify({ payload = JSON.stringify({
...(this.assertIsRecord(params) ? params : {}), ...params,
i: credential !== undefined ? credential : this.credential, i: credential !== undefined ? credential : this.credential,
}); });
} else if (mediaType === 'multipart/form-data') { } 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> = { type AllNullRecord<T> = {
[P in keyof T]: null; [P in keyof T]: null;
}; };
type AllNullOrOptionalRecord<T> = {
[P in keyof T]: never;
};
export type PureRenote = export type PureRenote =
Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'>
& AllNullRecord<Pick<Note, 'text'>> & AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>>
& AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>>
& { files: []; fileIds: []; } & { files: []; fileIds: []; }
& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>; & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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