Compare commits

..

35 Commits

Author SHA1 Message Date
syuilo a27aacb0c6 New translations ja-jp.yml (Thai) 2025-06-11 13:10:35 +09:00
syuilo f066d9c668 New translations ja-jp.yml (Vietnamese) 2025-06-11 13:10:33 +09:00
github-actions[bot] 8f66ffc14d Bump version to 2025.6.1-beta.1 2025-06-11 03:45:02 +00:00
syuilo 63e8935c86 fix(frontend): disable note_view_interruptor temporary to prevent rendering glitch 2025-06-11 12:42:49 +09:00
renovate[bot] b16a05b9a7
fix(deps): update [backend] update dependencies (#16143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 09:56:29 +09:00
おさむのひと 090262f3c6
fix: pnpm-lock.yamlの再生成 (#16182) 2025-06-11 08:57:42 +09:00
renovate[bot] bc5a33d87f
chore(deps): update [misskey-js] update dependencies (#16140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 16:08:04 +09:00
renovate[bot] 0ffd9e267a
fix(deps): update [frontend] update dependencies (#16144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 16:07:34 +09:00
renovate[bot] 81bc27d804
chore(deps): update [tools] update dependencies (#16141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 15:53:04 +09:00
github-actions[bot] f50abed98d Bump version to 2025.6.1-beta.0 2025-06-10 04:43:58 +00:00
syuilo 8ab574a31a
New Crowdin updates (#16163)
* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (German)
2025-06-10 13:42:09 +09:00
syuilo 9a3219f12e fix(frontend): Plugin:register_note_view_interruptor()によるノートの書き換えが機能しない問題を修正
Fix #16180
2025-06-10 09:51:45 +09:00
zyoshoka b5767c315a
fix(backend): correct outbox pagination (#16176) 2025-06-08 09:12:59 +09:00
github-actions[bot] ac9206f192 Bump version to 2025.6.1-alpha.4 2025-06-07 10:52:03 +00:00
かっこかり e2b38edb3a
deps(misskey-js): Update openapi-typescript to v7 (#15491)
* deps(misskey-js): Update openapi-typescript to v7

* update openapi-typescript to v7.7.3

* generate misskey-js types

* bump openapi-typescript

* enhance: 生成物からnever型を除去するように

* regenerate api types

* refactor: 処理共通化

---------

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2025-06-07 19:36:00 +09:00
syuilo c5dc0fd51b Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-06-07 09:26:51 +09:00
syuilo dda2ad6bcd fix(frontend): support non-image files 2025-06-07 09:26:49 +09:00
github-actions[bot] a5429ebeee Bump version to 2025.6.1-alpha.3 2025-06-06 23:36:20 +00:00
syuilo 1c3604c7fb 🎨 2025-06-07 08:15:31 +09:00
syuilo 4906f1f45c 🎨 2025-06-07 08:07:23 +09:00
syuilo 6506429f09 enhance(frontend): アップローダー内でセンシティブフラグを設定可能に 2025-06-07 07:57:23 +09:00
syuilo 9bd5f887de
enhance(frontend): 投稿フォームにアップローダーを埋め込み (#16173)
* wip

* Update MkPostForm.vue

* wip

* wip

* Update MkPostForm.vue

* wip

* wip

* add tip

* Update tips.ts

* Update MkPostForm.vue
2025-06-07 07:47:43 +09:00
syuilo be35fe468b refactor(frontend): refactor tips 2025-06-06 21:03:35 +09:00
syuilo 4b9b3ced01 enhance(frontend): improve MkTip usability 2025-06-06 20:59:01 +09:00
syuilo 20b8148ddf chore(frontend): tweak ui 2025-06-06 09:02:47 +09:00
github-actions[bot] 019dfbdc1c Bump version to 2025.6.1-alpha.2 2025-06-05 13:27:28 +00:00
かっこかり 95ea62f222
enhance(frontend): 画像エフェクトの操作でRangeをダブルクリックしたらデフォルトの値に戻るように (#16171)
* enhance(frontend): エフェクトの操作でRangeをダブルクリックしたらデフォルトの値に戻るように

* fix: trackの計算方法を修正

* remove unnecessary async
2025-06-05 22:25:49 +09:00
syuilo fde67dca74 enhance(frontend): tweak server setup wizard 2025-06-05 21:05:11 +09:00
かっこかり a603a4970e
enhance(frontend): 画像エフェクト「色調補正」を追加 (#16170) 2025-06-05 20:29:02 +09:00
zyoshoka f37a1e84bd
chore: fix failure to publish misskey-js to npm registry (#16169) 2025-06-05 19:21:15 +09:00
syuilo 6c9e055aae add note 2025-06-05 15:05:00 +09:00
syuilo a971e44cee refactor(frontend): refactor ImageEffector 2025-06-05 15:00:17 +09:00
syuilo c6808f1eb6 refactor(frontend): refactor ImageEffector 2025-06-05 12:58:32 +09:00
syuilo 2a78360588 refactor(frontend): refactor ImageEffector 2025-06-05 12:25:22 +09:00
zyoshoka 65ba33867b
fix(backend): avoid deadlock when deleting account (#16162) 2025-06-04 19:14:11 +09:00
51 changed files with 38739 additions and 33024 deletions

View File

@ -26,6 +26,8 @@ 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,5 +1,8 @@
## 2025.6.1 ## 2025.6.1
### Note
- Misskey Webプラグインのnote_view_interruptorは不具合の影響により現在一時的に無効化されています。
### General ### General
- -
@ -12,9 +15,12 @@
- 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,7 +34,6 @@ 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,6 +9584,14 @@ export interface Locale extends ILocale {
"disableFederationDescription": string; "disableFederationDescription": string;
}; };
"_postForm": { "_postForm": {
/**
*
*/
"quitInspiteOfThereAreUnuploadedFilesConfirm": string;
/**
* 稿
*/
"uploaderTip": string;
/** /**
* ... * ...
*/ */
@ -12176,6 +12184,10 @@ export interface Locale extends ILocale {
* *
*/ */
"grayscale": string; "grayscale": string;
/**
* 調
*/
"colorAdjust": string;
/** /**
* *
*/ */

View File

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

View File

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

View File

@ -220,6 +220,7 @@ 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"
@ -1211,6 +1212,9 @@ 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"
@ -1223,6 +1227,8 @@ _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-alpha.1", "version": "2025.6.1-beta.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.11.29", "@swc/core-darwin-arm64": "1.12.0",
"@swc/core-darwin-x64": "1.11.29", "@swc/core-darwin-x64": "1.12.0",
"@swc/core-freebsd-x64": "1.3.11", "@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.11.29", "@swc/core-linux-arm-gnueabihf": "1.12.0",
"@swc/core-linux-arm64-gnu": "1.11.29", "@swc/core-linux-arm64-gnu": "1.12.0",
"@swc/core-linux-arm64-musl": "1.11.29", "@swc/core-linux-arm64-musl": "1.12.0",
"@swc/core-linux-x64-gnu": "1.11.29", "@swc/core-linux-x64-gnu": "1.12.0",
"@swc/core-linux-x64-musl": "1.11.29", "@swc/core-linux-x64-musl": "1.12.0",
"@swc/core-win32-arm64-msvc": "1.11.29", "@swc/core-win32-arm64-msvc": "1.12.0",
"@swc/core-win32-ia32-msvc": "1.11.29", "@swc/core-win32-ia32-msvc": "1.12.0",
"@swc/core-win32-x64-msvc": "1.11.29", "@swc/core-win32-x64-msvc": "1.12.0",
"@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.817.0", "@aws-sdk/client-s3": "3.826.0",
"@aws-sdk/lib-storage": "3.817.0", "@aws-sdk/lib-storage": "3.826.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.70", "@napi-rs/canvas": "0.1.71",
"@nestjs/common": "11.1.2", "@nestjs/common": "11.1.3",
"@nestjs/core": "11.1.2", "@nestjs/core": "11.1.3",
"@nestjs/testing": "11.1.2", "@nestjs/testing": "11.1.3",
"@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.11.29", "@swc/core": "1.12.0",
"@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.0", "bullmq": "5.53.2",
"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.2", "form-data": "4.0.3",
"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.50.0", "meilisearch": "0.51.0",
"mfm-js": "0.24.0", "mfm-js": "0.24.0",
"microformats-parser": "2.0.2", "microformats-parser": "2.0.3",
"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.18", "@nestjs/platform-express": "10.4.19",
"@sentry/vue": "9.22.0", "@sentry/vue": "9.28.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.5", "@types/body-parser": "1.19.6",
"@types/color-convert": "2.0.4", "@types/color-convert": "2.0.4",
"@types/content-disposition": "0.5.8", "@types/content-disposition": "0.5.9",
"@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.21", "@types/node": "22.15.31",
"@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.2", "@types/pg": "8.15.4",
"@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.32.1", "@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.32.1", "@typescript-eslint/parser": "8.34.0",
"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);
} }
this.deletePostProcess(file, isExpired, deleter); await 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) {
this.driveFilesRepository.update(file.id, { await 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 {
this.driveFilesRepository.delete(file.id); await this.driveFilesRepository.delete(file.id);
} }
this.driveChart.update(file, false); this.driveChart.update(file, false);

View File

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

View File

@ -380,9 +380,7 @@ 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 });
// NOTE: user deletion query is slow await sleep();
// 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
@ -480,9 +478,7 @@ 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 });
// NOTE: user deletion query is slow await sleep();
// 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.41.1", "rollup": "4.42.0",
"sass": "1.89.0", "sass": "1.89.2",
"shiki": "3.4.2", "shiki": "3.6.0",
"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.33.0", "@tabler/icons-webfont": "3.34.0",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/estree": "1.0.7", "@types/estree": "1.0.8",
"@types/micromatch": "4.0.9", "@types/micromatch": "4.0.9",
"@types/node": "22.15.28", "@types/node": "22.15.31",
"@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.33.0", "@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.33.0", "@typescript-eslint/parser": "8.34.0",
"@vitest/coverage-v8": "3.1.4", "@vitest/coverage-v8": "3.2.3",
"@vue/runtime-core": "3.5.16", "@vue/runtime-core": "3.5.16",
"acorn": "8.14.1", "acorn": "8.15.0",
"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.1.0", "eslint-plugin-vue": "10.2.0",
"fast-glob": "3.3.3", "fast-glob": "3.3.3",
"happy-dom": "17.5.6", "happy-dom": "17.6.3",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"msw": "2.8.6", "msw": "2.10.2",
"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.28", "@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.33.0", "@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.33.0", "@typescript-eslint/parser": "8.34.0",
"esbuild": "0.25.5", "esbuild": "0.25.5",
"eslint-plugin-vue": "10.1.0", "eslint-plugin-vue": "10.2.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.24.0", "@sentry/vue": "9.27.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.41.1", "rollup": "4.42.0",
"sanitize-html": "2.17.0", "sanitize-html": "2.17.0",
"sass": "1.89.0", "sass": "1.89.2",
"shiki": "3.4.2", "shiki": "3.6.0",
"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.33.0", "@tabler/icons-webfont": "3.34.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.7", "@types/estree": "1.0.8",
"@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.28", "@types/node": "22.15.31",
"@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.33.0", "@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.33.0", "@typescript-eslint/parser": "8.34.0",
"@vitest/coverage-v8": "3.1.4", "@vitest/coverage-v8": "3.2.3",
"@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.14.1", "acorn": "8.15.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "14.4.0", "cypress": "14.4.1",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "10.1.0", "eslint-plugin-vue": "10.2.0",
"fast-glob": "3.3.3", "fast-glob": "3.3.3",
"happy-dom": "17.5.6", "happy-dom": "17.6.3",
"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.8.6", "msw": "2.10.2",
"msw-storybook-addon": "2.0.4", "msw-storybook-addon": "2.0.5",
"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.1.4", "vitest": "3.2.3",
"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,22 +16,54 @@ 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 v-if="v.type === 'boolean'" v-model="layer.params[k]"> <MkSwitch
v-if="v.type === 'boolean'"
v-model="layer.params[k]"
>
<template #label>{{ k }}</template> <template #label>{{ k }}</template>
</MkSwitch> </MkSwitch>
<MkRange v-else-if="v.type === 'number'" v-model="layer.params[k]" continuousUpdate :min="v.min" :max="v.max" :step="v.step"> <MkRange
v-else-if="v.type === 'number'"
v-model="layer.params[k]"
continuousUpdate
:min="v.min"
:max="v.max"
:step="v.step"
@thumbDoubleClicked="() => {
if (fx.params[k].default != null) {
layer.params[k] = fx.params[k].default;
} else {
layer.params[k] = v.min;
}
}"
>
<template #label>{{ k }}</template> <template #label>{{ k }}</template>
</MkRange> </MkRange>
<MkRadios v-else-if="v.type === 'number:enum'" v-model="layer.params[k]"> <MkRadios
v-else-if="v.type === 'number:enum'"
v-model="layer.params[k]"
>
<template #label>{{ k }}</template> <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 v-model="layer.params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1"> <MkRange
v-model="layer.params[k]"
continuousUpdate
type="number"
:min="0"
:max="10000"
:step="1"
>
<template #label>{{ k }}</template> <template #label>{{ k }}</template>
</MkRange> </MkRange>
</div> </div>
<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; }"> <MkInput
v-else-if="v.type === 'color'"
:modelValue="getHex(layer.params[k])"
type="color"
@update:modelValue="v => { const c = getRgb(v); if (c != null) layer.params[k] = c; }"
>
<template #label>{{ k }}</template> <template #label>{{ k }}</template>
</MkInput> </MkInput>
</div> </div>
@ -40,22 +72,14 @@ 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 });
@ -69,6 +93,24 @@ 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);
// plugin // Transition
const noteViewInterruptors = getPluginHandlers('note_view_interruptor'); // https://github.com/aiscript-dev/aiscript/issues/937
if (noteViewInterruptors.length > 0) { //// plugin
onMounted(async () => { //const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
let result: Misskey.entities.Note | null = deepClone(note); //if (noteViewInterruptors.length > 0) {
for (const interruptor of noteViewInterruptors) { // let result: Misskey.entities.Note | null = deepClone(note);
try { // for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result!) as Misskey.entities.Note | null; // try {
} catch (err) { // result = await interruptor.handler(result!) as Misskey.entities.Note | null;
console.error(err); // } catch (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,21 +286,20 @@ const inChannel = inject('inChannel', null);
let note = deepClone(props.note); let note = deepClone(props.note);
// plugin // Transition
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

@ -72,24 +72,29 @@ 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" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button> <button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.upload + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromPc"><i class="ti ti-photo-plus"></i></button>
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.fromDrive + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromDrive"><i class="ti ti-cloud-download"></i></button>
<button v-tooltip="i18n.ts.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.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></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.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button> <button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></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> <button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></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>
</div> </div>
<div :class="$style.footerRight"> <div :class="$style.footerRight">
<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.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></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">
@ -105,10 +110,12 @@ 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';
@ -120,7 +127,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 { selectFile } from '@/utility/drive.js'; import { chooseDriveFile } 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';
@ -138,6 +145,7 @@ 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();
@ -201,6 +209,15 @@ 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}` : '';
@ -258,10 +275,11 @@ 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 && return !props.mock && !posting.value && !posted.value && !uploader.uploading.value && (uploader.items.value.length === 0 || uploader.readyForUpload.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
@ -434,17 +452,20 @@ function focus() {
} }
} }
function chooseFileFrom(ev) { function chooseFileFromPc(ev: MouseEvent) {
if (props.mock) return; if (props.mock) return;
selectFile({ os.chooseFileFromPc({ multiple: true }).then(files => {
anchorElement: ev.currentTarget ?? ev.target, if (files.length === 0) return;
multiple: true, uploader.addFiles(files);
label: i18n.ts.attachFile, });
}).then(files_ => { }
for (const file of files_) {
files.value.push(file); function chooseFileFromDrive(ev: MouseEvent) {
} if (props.mock) return;
chooseDriveFile({ multiple: true }).then(driveFiles => {
files.value.push(...driveFiles);
}); });
} }
@ -571,6 +592,11 @@ 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,
@ -797,6 +823,15 @@ 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;
@ -840,6 +875,10 @@ 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,
@ -1043,6 +1082,16 @@ 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();
@ -1111,8 +1160,23 @@ 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="modal?.close()" @click="_close()"
@closed="onModalClosed()" @closed="onModalClosed()"
@esc="modal?.close()" @esc="_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="modal?.close()" @cancel="_close()"
@esc="modal?.close()" @esc="_close()"
/> />
</MkModal> </MkModal>
</template> </template>
@ -43,6 +43,7 @@ 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({
@ -50,6 +51,12 @@ 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: ((steppedRawValue - minRatio) * 100) + '%', left: (Math.abs(Math.min(0, min)) / (max + Math.abs(Math.min(0, min)))) * 100 + '%' }"> <div class="highlight right" :style="{ width: rightTrackWidth, left: rightTrackPosition }">
<div class="shine right"></div> <div class="shine right"></div>
</div> </div>
<div class="highlight left" :style="{ width: ((minRatio - steppedRawValue) * 100) + '%', left: (steppedRawValue) * 100 + '%' }"> <div class="highlight left" :style="{ width: leftTrackWidth, left: leftTrackPosition }">
<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, ref, useTemplateRef, watch } from 'vue'; import { computed, defineAsyncComponent, onMounted, onUnmounted, onBeforeUnmount, 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,13 +58,14 @@ const props = withDefaults(defineProps<{
continuousUpdate?: boolean; continuousUpdate?: boolean;
}>(), { }>(), {
step: 1, step: 1,
textConverter: (v) => v.toString(), textConverter: (v: number) => (Math.round(v * 1000) / 1000).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');
@ -73,7 +74,24 @@ 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 rawValue = ref((props.modelValue - props.min) / (props.max - props.min)); const rightTrackWidth = computed(() => {
return Math.max(0, (steppedRawValue.value - minRatio.value) * 100) + '%';
});
const leftTrackWidth = computed(() => {
return Math.max(0, (minRatio.value - steppedRawValue.value) * 100) + '%';
});
const rightTrackPosition = computed(() => {
return (Math.abs(Math.min(0, props.min)) / (props.max + Math.abs(Math.min(0, props.min)))) * 100 + '%';
});
const leftTrackPosition = computed(() => {
return (Math.min(minRatio.value, steppedRawValue.value) * 100) + '%';
});
const calcRawValue = (value: number) => {
return (value - props.min) / (props.max - props.min);
};
const rawValue = ref(calcRawValue(props.modelValue));
const 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);
@ -103,6 +121,11 @@ 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;
@ -128,6 +151,12 @@ 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;
@ -138,7 +167,7 @@ function onMouseenter() {
text: computed(() => { text: computed(() => {
return props.textConverter(finalValue.value); return props.textConverter(finalValue.value);
}), }),
targetElement: thumbEl, targetElement: thumbEl.value ?? undefined,
}, { }, {
closed: () => dispose(), closed: () => dispose(),
}); });
@ -148,6 +177,8 @@ 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();
@ -158,7 +189,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
text: computed(() => { text: computed(() => {
return props.textConverter(finalValue.value); return props.textConverter(finalValue.value);
}), }),
targetElement: thumbEl, targetElement: thumbEl.value ?? undefined,
}, { }, {
closed: () => dispose(), closed: () => dispose(),
}); });
@ -203,6 +234,20 @@ 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,37 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._uploader.tip }} {{ i18n.ts._uploader.tip }}
</MkTip> </MkTip>
<div class="_gaps_s"> <MkUploaderItems :items="items" @showMenu="(item, ev) => showPerItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerItemMenuViaContextmenu(item, ev)"/>
<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>
@ -69,8 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #footer> <template #footer>
<div class="_buttonsCenter"> <div class="_buttonsCenter">
<MkButton v-if="isUploading" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton> <MkButton v-if="uploader.uploading.value" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
<MkButton v-else-if="!firstUploadAttempted" primary rounded @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton> <MkButton v-else-if="!firstUploadAttempted" primary rounded :disabled="!uploader.readyForUpload.value" @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
<MkButton v-if="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>
@ -79,110 +49,51 @@ 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, markRaw, onMounted, onUnmounted, ref, triggerRef, useTemplateRef, watch } from 'vue'; import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { genId } from '@/utility/id.js'; import type { UploaderFeatures, UploaderItem } from '@/composables/use-uploader.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 { WatermarkRenderer } from '@/utility/watermark.js'; import { useUploader } from '@/composables/use-uploader.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?: UploaderDialogFeatures; features?: UploaderFeatures;
}>(), { }>(), {
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 isUploading = computed(() => items.value.some(item => item.uploading)); const canRetry = computed(() => firstUploadAttempted.value && uploader.readyForUpload.value);
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;
@ -195,27 +106,6 @@ 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');
@ -238,11 +128,16 @@ async function cancel() {
}); });
if (canceled) return; if (canceled) return;
abortAll(); uploader.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',
@ -252,11 +147,11 @@ async function abortWithConfirm() {
}); });
if (canceled) return; if (canceled) return;
abortAll(); uploader.abortAll();
} }
async function done() { async function done() {
if (items.value.some(item => item.uploaded == null)) { if (!uploader.allItemsUploaded.value) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'question', type: 'question',
text: i18n.ts._uploader.doneConfirm, text: i18n.ts._uploader.doneConfirm,
@ -270,381 +165,20 @@ async function done() {
dialog.value?.close(); dialog.value?.close();
} }
function showMenu(ev: MouseEvent, item: UploaderItem) { async function chooseFile(ev: MouseEvent) {
const menu: MenuItem[] = []; const newFiles = await os.chooseFileFromPc({ multiple: true });
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);
} }
async function upload() { // function showPerItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
firstUploadAttempted.value = true; const menu = uploader.getMenu(item);
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>
@ -666,127 +200,4 @@ onUnmounted(() => {
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

@ -0,0 +1,196 @@
<!--
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,7 +7,10 @@ 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>
<MkButton primary rounded small @click="closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton> <div>
<MkButton inline primary rounded small @click="_closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
<button class="_button" style="padding: 8px; margin-left: 4px;" @click="showMenu"><i class="ti ti-dots"></i></button>
</div>
</div> </div>
</template> </template>
@ -15,19 +18,30 @@ 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: keyof (typeof store['s']['tips']); k: typeof TIPS[number];
warn?: boolean; warn?: boolean;
}>(), { }>(), {
warn: false, warn: false,
}); });
function closeTip() { function _closeTip() {
store.set('tips', { closeTip(props.k);
...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

@ -0,0 +1,576 @@
/*
* 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 { store, TIPS } from '@/store.js'; import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
const $i = ensureSignin(); const $i = ensureSignin();
@ -205,16 +205,12 @@ function migrate() {
} }
function resetAllTips() { function resetAllTips() {
store.set('tips', {}); _resetAllTips();
os.success(); os.success();
} }
function hideAllTips() { function hideAllTips() {
const v = {}; _hideAllTips();
for (const k of TIPS) {
v[k] = true;
}
store.set('tips', v);
os.success(); os.success();
} }

View File

@ -226,6 +226,7 @@ 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,
}); });
@ -233,6 +234,7 @@ 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,
@ -241,10 +243,12 @@ 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,18 +81,6 @@ 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>
@ -105,12 +93,17 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._serverSetupWizard.skipSettings }} {{ i18n.ts._serverSetupWizard.skipSettings }}
</MkButton> </MkButton>
</div> </div>
<div v-else-if="step === 3" class="_gaps_m"> <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><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 }}
@ -232,4 +225,11 @@ 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,22 +10,11 @@ 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

@ -0,0 +1,38 @@
/*
* 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,20 +11,22 @@ 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: string, caption?: string }; export type MenuLabel = { type: 'label', text: Text, caption?: Text };
export type MenuLink = { type: 'link', to: string, text: string, caption?: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; export type MenuLink = { type: 'link', to: string, text: Text, caption?: Text, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, caption?: string, icon?: string, indicate?: boolean }; export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: Text, caption?: Text, 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: string, caption?: string, icon?: string, disabled?: boolean | Ref<boolean> }; export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: Text, caption?: Text, icon?: string, disabled?: boolean | Ref<boolean> };
export type MenuButton = { type?: 'button', text: string, caption?: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction }; export type MenuButton = { type?: 'button', text: Text, caption?: Text, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
export type MenuRadio = { type: 'radio', text: string, caption?: string, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> }; export type MenuRadio = { type: 'radio', text: Text, caption?: Text, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
export type MenuRadioOption = { type: 'radioOption', text: string, caption?: string, action: MenuAction; active?: boolean | ComputedRef<boolean> }; export type MenuRadioOption = { type: 'radioOption', text: Text, caption?: Text, 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: string, caption?: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) }; export type MenuParent = { type: 'parent', text: Text, caption?: Text, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
export type MenuPending = { type: 'pending' }; export type MenuPending = { type: 'pending' };

View File

@ -6,6 +6,7 @@
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';
@ -16,7 +17,6 @@ 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,6 +32,7 @@ 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();
@ -140,6 +141,7 @@ 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);
@ -156,7 +158,7 @@ export function uploadFile(file: File | Blob, options: {
export function chooseFileFromPcAndUpload( export function chooseFileFromPcAndUpload(
options: { options: {
multiple?: boolean; multiple?: boolean;
features?: UploaderDialogFeatures; features?: UploaderFeatures;
folderId?: string | null; folderId?: string | null;
} = {}, } = {},
): Promise<Misskey.entities.DriveFile[]> { ): Promise<Misskey.entities.DriveFile[]> {
@ -254,7 +256,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,6 +4,7 @@
*/ */
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;
@ -60,8 +61,6 @@ 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;
@ -70,6 +69,7 @@ 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.renderTextureProgram = this.initShaderProgram(`#version 300 es this.nopProgram = initShaderProgram(this.gl, `#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, 0.0, 1.0); gl_Position = vec4(position * vec2(1.0, -1.0), 0.0, 1.0);
} }
`, `#version 300 es `, `#version 300 es
precision mediump float; precision mediump float;
@ -134,82 +134,28 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
} }
`); `);
this.renderInvertedTextureProgram = this.initShaderProgram(`#version 300 es // レジスタ番号はシェーダープログラムに属しているわけではなく、独立の存在なので、とりあえず nopProgram を使って設定する(その後は効果が持続する)
in vec2 position; // ref. https://qiita.com/emadurandal/items/5966c8374f03d4de3266
out vec2 in_uv; const positionLocation = gl.getAttribLocation(this.nopProgram, 'position');
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
void main() { gl.enableVertexAttribArray(positionLocation);
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);
}
`);
} }
public loadShader(type: GLenum, source: string): WebGLShader { private renderLayer(layer: ImageEffectorLayer, preTexture: WebGLTexture, invert = false) {
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 ?? this.initShaderProgram(`#version 300 es const shaderProgram = cachedShader ?? initShaderProgram(this.gl, `#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 = vec4(position, 0.0, 1.0); gl_Position = u_invert ? vec4(position * vec2(1.0, -1.0), 0.0, 1.0) : vec4(position, 0.0, 1.0);
} }
`, fx.shader); `, fx.shader);
if (cachedShader == null) { if (cachedShader == null) {
@ -221,6 +167,9 @@ 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');
@ -253,27 +202,23 @@ 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.renderTextureProgram); gl.useProgram(this.nopProgram);
const u_texture = gl.getUniformLocation(this.renderTextureProgram, 'u_texture'); gl.uniform1i(gl.getUniformLocation(this.nopProgram, 'u_texture')!, 0);
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) {
@ -283,30 +228,22 @@ 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);
const cachedResultFrameBuffer = this.perLayerResultFrameBuffers.get(layer.id); if (isLast) {
const resultFrameBuffer = cachedResultFrameBuffer ?? gl.createFramebuffer()!; gl.bindFramebuffer(gl.FRAMEBUFFER, null);
if (cachedResultFrameBuffer == null) { } else {
this.perLayerResultFrameBuffers.set(layer.id, resultFrameBuffer); const cachedResultFrameBuffer = this.perLayerResultFrameBuffers.get(layer.id);
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); this.renderLayer(layer, preTexture, isLast);
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[]) {
@ -366,6 +303,8 @@ 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);
} }
@ -386,8 +325,6 @@ 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,6 +5,7 @@
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';
@ -26,6 +27,7 @@ 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

@ -0,0 +1,136 @@
/*
* 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

@ -0,0 +1,40 @@
/*
* 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.21", "@types/node": "22.15.31",
"@types/wawoff2": "1.0.2", "@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.32.1", "@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.32.1" "@typescript-eslint/parser": "8.34.0"
}, },
"dependencies": { "dependencies": {
"@tabler/icons-webfont": "3.33.0", "@tabler/icons-webfont": "3.34.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.28", "@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.33.0", "@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.33.0", "@typescript-eslint/parser": "8.34.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,10 +3212,11 @@ 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, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>> & { type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'text'>> & AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>> & {
files: []; files: [];
fileIds: []; fileIds: [];
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>; } & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
@ -3748,7 +3749,7 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons
// Warnings were encountered during analysis: // Warnings were encountered during analysis:
// //
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/entities.ts:54: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.21", "@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.32.1", "@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.32.1", "@typescript-eslint/parser": "8.34.0",
"openapi-types": "12.1.3", "openapi-types": "12.1.3",
"openapi-typescript": "6.7.6", "openapi-typescript": "7.8.0",
"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

@ -0,0 +1,130 @@
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,9 +1,12 @@
import assert from 'assert'; import assert from 'assert';
import { mkdir, readFile, writeFile } from 'fs/promises'; import { mkdir, readFile, writeFile } from 'fs/promises';
import { OpenAPIV3_1 } from 'openapi-types'; import type { 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, { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript'; import openapiTS, { astToString } 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,
@ -28,13 +31,6 @@ 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
@ -43,15 +39,26 @@ async function generateBaseTypes(
}; };
} }
const generatedTypes = await openapiTS(openApi, { const tsNullNode = ts.factory.createLiteralTypeNode(ts.factory.createNull());
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') {
return schemaObject.nullable ? 'Blob | null' : 'Blob'; if (schemaObject.nullable) {
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-alpha.1", "version": "2025.6.1-beta.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.21", "@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.32.1", "@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.32.1", "@typescript-eslint/parser": "8.34.0",
"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.4", "esbuild": "0.25.5",
"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({
...params, ...(this.assertIsRecord(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,10 +24,14 @@ 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, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>> & AllNullRecord<Pick<Note, 'text'>>
& 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.28", "@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.33.0", "@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.33.0", "@typescript-eslint/parser": "8.34.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.33.0", "@typescript-eslint/parser": "8.34.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.21", "@types/node": "22.15.31",
"@vitest/coverage-v8": "3.1.4", "@vitest/coverage-v8": "3.2.3",
"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.1.4", "vite-node": "3.2.3",
"vitest": "3.1.4" "vitest": "3.2.3"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@ -890,6 +890,16 @@
"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",
@ -899,6 +909,13 @@
"@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",
@ -923,9 +940,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.15.21", "version": "22.15.31",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz",
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -939,15 +956,16 @@
"dev": true "dev": true
}, },
"node_modules/@vitest/coverage-v8": { "node_modules/@vitest/coverage-v8": {
"version": "3.1.4", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.3.tgz",
"integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==", "integrity": "sha512-D1QKzngg8PcDoCE8FHSZhREDuEy+zcKmMiMafYse41RZpBE5EDJyKOTdqK3RQfsV2S2nyKor5KCs8PyPRFqKPg==",
"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",
"debug": "^4.4.0", "ast-v8-to-istanbul": "^0.3.3",
"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",
@ -962,8 +980,8 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
}, },
"peerDependencies": { "peerDependencies": {
"@vitest/browser": "3.1.4", "@vitest/browser": "3.2.3",
"vitest": "3.1.4" "vitest": "3.2.3"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@vitest/browser": { "@vitest/browser": {
@ -972,14 +990,15 @@
} }
}, },
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
"version": "3.1.4", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz",
"integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", "integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/spy": "3.1.4", "@types/chai": "^5.2.2",
"@vitest/utils": "3.1.4", "@vitest/spy": "3.2.3",
"@vitest/utils": "3.2.3",
"chai": "^5.2.0", "chai": "^5.2.0",
"tinyrainbow": "^2.0.0" "tinyrainbow": "^2.0.0"
}, },
@ -988,13 +1007,13 @@
} }
}, },
"node_modules/@vitest/mocker": { "node_modules/@vitest/mocker": {
"version": "3.1.4", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz",
"integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", "integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/spy": "3.1.4", "@vitest/spy": "3.2.3",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"magic-string": "^0.30.17" "magic-string": "^0.30.17"
}, },
@ -1003,7 +1022,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"msw": "^2.4.9", "msw": "^2.4.9",
"vite": "^5.0.0 || ^6.0.0" "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"msw": { "msw": {
@ -1015,9 +1034,9 @@
} }
}, },
"node_modules/@vitest/pretty-format": { "node_modules/@vitest/pretty-format": {
"version": "3.1.4", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz",
"integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1028,27 +1047,28 @@
} }
}, },
"node_modules/@vitest/runner": { "node_modules/@vitest/runner": {
"version": "3.1.4", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz",
"integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", "integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/utils": "3.1.4", "@vitest/utils": "3.2.3",
"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.1.4", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz",
"integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", "integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/pretty-format": "3.1.4", "@vitest/pretty-format": "3.2.3",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"pathe": "^2.0.3" "pathe": "^2.0.3"
}, },
@ -1057,26 +1077,26 @@
} }
}, },
"node_modules/@vitest/spy": { "node_modules/@vitest/spy": {
"version": "3.1.4", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz",
"integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", "integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tinyspy": "^3.0.2" "tinyspy": "^4.0.3"
}, },
"funding": { "funding": {
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/@vitest/utils": { "node_modules/@vitest/utils": {
"version": "3.1.4", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz",
"integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", "integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/pretty-format": "3.1.4", "@vitest/pretty-format": "3.2.3",
"loupe": "^3.1.3", "loupe": "^3.1.3",
"tinyrainbow": "^2.0.0" "tinyrainbow": "^2.0.0"
}, },
@ -1120,6 +1140,18 @@
"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",
@ -1228,9 +1260,9 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1557,6 +1589,13 @@
"@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",
@ -2561,6 +2600,19 @@
"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",
@ -2603,9 +2655,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.13", "version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2620,9 +2672,9 @@
} }
}, },
"node_modules/tinypool": { "node_modules/tinypool": {
"version": "1.0.2", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz",
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", "integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2640,9 +2692,9 @@
} }
}, },
"node_modules/tinyspy": { "node_modules/tinyspy": {
"version": "3.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2860,17 +2912,17 @@
} }
}, },
"node_modules/vite-node": { "node_modules/vite-node": {
"version": "3.1.4", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz",
"integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", "integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cac": "^6.7.14", "cac": "^6.7.14",
"debug": "^4.4.0", "debug": "^4.4.1",
"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" "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
}, },
"bin": { "bin": {
"vite-node": "vite-node.mjs" "vite-node": "vite-node.mjs"
@ -2883,32 +2935,34 @@
} }
}, },
"node_modules/vitest": { "node_modules/vitest": {
"version": "3.1.4", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz",
"integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/expect": "3.1.4", "@types/chai": "^5.2.2",
"@vitest/mocker": "3.1.4", "@vitest/expect": "3.2.3",
"@vitest/pretty-format": "^3.1.4", "@vitest/mocker": "3.2.3",
"@vitest/runner": "3.1.4", "@vitest/pretty-format": "^3.2.3",
"@vitest/snapshot": "3.1.4", "@vitest/runner": "3.2.3",
"@vitest/spy": "3.1.4", "@vitest/snapshot": "3.2.3",
"@vitest/utils": "3.1.4", "@vitest/spy": "3.2.3",
"@vitest/utils": "3.2.3",
"chai": "^5.2.0", "chai": "^5.2.0",
"debug": "^4.4.0", "debug": "^4.4.1",
"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.13", "tinyglobby": "^0.2.14",
"tinypool": "^1.0.2", "tinypool": "^1.1.0",
"tinyrainbow": "^2.0.0", "tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
"vite-node": "3.1.4", "vite-node": "3.2.3",
"why-is-node-running": "^2.3.0" "why-is-node-running": "^2.3.0"
}, },
"bin": { "bin": {
@ -2924,8 +2978,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.1.4", "@vitest/browser": "3.2.3",
"@vitest/ui": "3.1.4", "@vitest/ui": "3.2.3",
"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.21", "@types/node": "22.15.31",
"@vitest/coverage-v8": "3.1.4", "@vitest/coverage-v8": "3.2.3",
"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.1.4", "vite-node": "3.2.3",
"vitest": "3.1.4" "vitest": "3.2.3"
} }
} }