Compare commits
26 Commits
a27aacb0c6
...
24b4586ee6
Author | SHA1 | Date |
---|---|---|
|
24b4586ee6 | |
|
25c4143d5c | |
|
7f504f9ec1 | |
|
e5c3868d3e | |
|
366f24176e | |
|
2f7a47e53c | |
|
77608a70f6 | |
|
e854e95d1c | |
|
b57019e4ab | |
|
81182b613c | |
|
0da3d34825 | |
|
598e764123 | |
|
dbea1b4d70 | |
|
e5a717286c | |
|
07c5dc46b0 | |
|
89e8020d51 | |
|
25fcdcb5ea | |
|
573887c831 | |
|
c749af4752 | |
|
43821c1920 | |
|
ddb440e0fa | |
|
4cc44e869c | |
|
c6285b7569 | |
|
de158dcd7a | |
|
289783aa50 | |
|
41c8788b9a |
|
@ -26,8 +26,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
# see https://docs.github.com/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry
|
|
||||||
registry-url: 'https://registry.npmjs.org'
|
|
||||||
- name: Publish package
|
- name: Publish package
|
||||||
run: |
|
run: |
|
||||||
pnpm i --frozen-lockfile
|
pnpm i --frozen-lockfile
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
## 2025.6.1
|
## 2025.6.1
|
||||||
|
|
||||||
### Note
|
|
||||||
- Misskey Webプラグインのnote_view_interruptorは不具合の影響により現在一時的に無効化されています。
|
|
||||||
|
|
||||||
### General
|
### General
|
||||||
-
|
-
|
||||||
|
|
||||||
|
@ -15,12 +12,9 @@
|
||||||
- Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正
|
- Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正
|
||||||
- Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正
|
- Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正
|
||||||
- Fix: タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正
|
- Fix: タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正
|
||||||
- Fix: Plugin:register_note_view_interruptor()によるノートの書き換えが機能しない問題を修正
|
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all)
|
- Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all)
|
||||||
- Fix: アカウント削除が正常に行われないことがあった問題を修正
|
|
||||||
- Fix: outboxのページネーションが正しく行われない問題を修正
|
|
||||||
|
|
||||||
|
|
||||||
## 2025.6.0
|
## 2025.6.0
|
||||||
|
|
|
@ -34,6 +34,7 @@ describe('Before setup instance', () => {
|
||||||
|
|
||||||
cy.intercept('POST', '/api/admin/update-meta').as('update-meta');
|
cy.intercept('POST', '/api/admin/update-meta').as('update-meta');
|
||||||
|
|
||||||
|
cy.get('[data-cy-next]').click();
|
||||||
cy.get('[data-cy-next]').click();
|
cy.get('[data-cy-next]').click();
|
||||||
cy.get('[data-cy-server-name] input').type('Testskey');
|
cy.get('[data-cy-server-name] input').type('Testskey');
|
||||||
cy.get('[data-cy-server-setup-wizard-apply]').click();
|
cy.get('[data-cy-server-setup-wizard-apply]').click();
|
||||||
|
|
|
@ -9584,14 +9584,6 @@ export interface Locale extends ILocale {
|
||||||
"disableFederationDescription": string;
|
"disableFederationDescription": string;
|
||||||
};
|
};
|
||||||
"_postForm": {
|
"_postForm": {
|
||||||
/**
|
|
||||||
* アップロードされていないファイルがありますが、破棄してフォームを閉じますか?
|
|
||||||
*/
|
|
||||||
"quitInspiteOfThereAreUnuploadedFilesConfirm": string;
|
|
||||||
/**
|
|
||||||
* ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。
|
|
||||||
*/
|
|
||||||
"uploaderTip": string;
|
|
||||||
/**
|
/**
|
||||||
* このノートに返信...
|
* このノートに返信...
|
||||||
*/
|
*/
|
||||||
|
@ -12184,10 +12176,6 @@ export interface Locale extends ILocale {
|
||||||
* 白黒
|
* 白黒
|
||||||
*/
|
*/
|
||||||
"grayscale": string;
|
"grayscale": string;
|
||||||
/**
|
|
||||||
* 色調補正
|
|
||||||
*/
|
|
||||||
"colorAdjust": string;
|
|
||||||
/**
|
/**
|
||||||
* 色の圧縮
|
* 色の圧縮
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2522,8 +2522,6 @@ _visibility:
|
||||||
disableFederationDescription: "他サーバーへの配信を行いません"
|
disableFederationDescription: "他サーバーへの配信を行いません"
|
||||||
|
|
||||||
_postForm:
|
_postForm:
|
||||||
quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされていないファイルがありますが、破棄してフォームを閉じますか?"
|
|
||||||
uploaderTip: "ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。"
|
|
||||||
replyPlaceholder: "このノートに返信..."
|
replyPlaceholder: "このノートに返信..."
|
||||||
quotePlaceholder: "このノートを引用..."
|
quotePlaceholder: "このノートを引用..."
|
||||||
channelPlaceholder: "チャンネルに投稿..."
|
channelPlaceholder: "チャンネルに投稿..."
|
||||||
|
@ -3264,7 +3262,6 @@ _imageEffector:
|
||||||
mirror: "ミラー"
|
mirror: "ミラー"
|
||||||
invert: "色の反転"
|
invert: "色の反転"
|
||||||
grayscale: "白黒"
|
grayscale: "白黒"
|
||||||
colorAdjust: "色調補正"
|
|
||||||
colorClamp: "色の圧縮"
|
colorClamp: "色の圧縮"
|
||||||
colorClampAdvanced: "色の圧縮(高度)"
|
colorClampAdvanced: "色の圧縮(高度)"
|
||||||
distort: "歪み"
|
distort: "歪み"
|
||||||
|
|
|
@ -220,7 +220,6 @@ silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี
|
||||||
mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้"
|
mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้"
|
||||||
operations: "ดำเนินการ"
|
operations: "ดำเนินการ"
|
||||||
software: "ซอฟต์แวร์"
|
software: "ซอฟต์แวร์"
|
||||||
softwareName: "ชื่อซอฟต์แวร์"
|
|
||||||
version: "เวอร์ชั่น"
|
version: "เวอร์ชั่น"
|
||||||
metadata: "Metadata"
|
metadata: "Metadata"
|
||||||
withNFiles: "{n} ไฟล์"
|
withNFiles: "{n} ไฟล์"
|
||||||
|
@ -1294,10 +1293,6 @@ federationDisabled: "เซิร์ฟเวอร์นี้ปิดกา
|
||||||
reactAreYouSure: "คุณต้องการที่จะตอบสนองต่อ \" {emoji}\" หรือไม่?"
|
reactAreYouSure: "คุณต้องการที่จะตอบสนองต่อ \" {emoji}\" หรือไม่?"
|
||||||
markAsSensitiveConfirm: "คุณต้องการทำเครื่องหมายสื่อนี้ว่าละเอียดอ่อนหรือไม่?"
|
markAsSensitiveConfirm: "คุณต้องการทำเครื่องหมายสื่อนี้ว่าละเอียดอ่อนหรือไม่?"
|
||||||
unmarkAsSensitiveConfirm: "คุณต้องการลบการกำหนดความไวของสื่อนี้หรือไม่?"
|
unmarkAsSensitiveConfirm: "คุณต้องการลบการกำหนดความไวของสื่อนี้หรือไม่?"
|
||||||
preferences: "การตั้งค่าสภาพแวดล้อม"
|
|
||||||
preferencesProfile: "โปรไฟล์การกำหนดค่า"
|
|
||||||
preferenceSyncConflictTitle: "การตั้งค่ามีอยู่บนเซิร์ฟเวอร์"
|
|
||||||
preferenceSyncConflictText: "รายการการตั้งค่าที่เปิดใช้งานการซิงโครไนซ์จะจัดเก็บค่าไว้บนเซิร์ฟเวอร์ และพบค่าที่จัดเก็บบนเซิร์ฟเวอร์สำหรับรายการการตั้งค่านี้ คุณต้องการทำอย่างไร?"
|
|
||||||
postForm: "แบบฟอร์มการโพสต์"
|
postForm: "แบบฟอร์มการโพสต์"
|
||||||
information: "เกี่ยวกับ"
|
information: "เกี่ยวกับ"
|
||||||
right: "ขวา"
|
right: "ขวา"
|
||||||
|
@ -1310,7 +1305,6 @@ _chat:
|
||||||
send: "ส่ง"
|
send: "ส่ง"
|
||||||
_settings:
|
_settings:
|
||||||
webhook: "Webhook"
|
webhook: "Webhook"
|
||||||
preferencesBanner: "คุณสามารถกำหนดค่าพฤติกรรมโดยรวมของไคลเอนต์ได้ตามความต้องการของคุณ"
|
|
||||||
_accountSettings:
|
_accountSettings:
|
||||||
requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา"
|
requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา"
|
||||||
requireSigninToViewContentsDescription1: "ต้องเข้าสู่ระบบเพื่อดูบันทึกและเนื้อหาอื่น ๆ ทั้งหมดที่คุณสร้าง คาดว่าจะมีประสิทธิผลในการป้องกันไม่ให้ข้อมูลถูกเก็บรวบรวมโดยโปรแกรมรวบรวมข้อมูล"
|
requireSigninToViewContentsDescription1: "ต้องเข้าสู่ระบบเพื่อดูบันทึกและเนื้อหาอื่น ๆ ทั้งหมดที่คุณสร้าง คาดว่าจะมีประสิทธิผลในการป้องกันไม่ให้ข้อมูลถูกเก็บรวบรวมโดยโปรแกรมรวบรวมข้อมูล"
|
||||||
|
|
|
@ -220,7 +220,6 @@ silenceThisInstance: "Máy chủ im lặng"
|
||||||
mediaSilenceThisInstance: "Tắt nội dung đa phương tiện từ máy chủ này"
|
mediaSilenceThisInstance: "Tắt nội dung đa phương tiện từ máy chủ này"
|
||||||
operations: "Vận hành"
|
operations: "Vận hành"
|
||||||
software: "Phần mềm"
|
software: "Phần mềm"
|
||||||
softwareName: "Tên phần mềm"
|
|
||||||
version: "Phiên bản"
|
version: "Phiên bản"
|
||||||
metadata: "Metadata"
|
metadata: "Metadata"
|
||||||
withNFiles: "{n} tập tin"
|
withNFiles: "{n} tập tin"
|
||||||
|
@ -1212,9 +1211,6 @@ federationDisabled: "Liên kết bị vô hiệu hóa trên máy chủ này. B
|
||||||
reactAreYouSure: "Bạn có muốn phản hồi với \" {emoji} \" không?"
|
reactAreYouSure: "Bạn có muốn phản hồi với \" {emoji} \" không?"
|
||||||
preferences: "Thiết lập môi trường"
|
preferences: "Thiết lập môi trường"
|
||||||
accessibility: "Khả năng tiếp cận"
|
accessibility: "Khả năng tiếp cận"
|
||||||
preferencesProfile: "Hồ sơ sở thích"
|
|
||||||
preferenceSyncConflictTitle: "Cài đặt tồn tại trên máy chủ"
|
|
||||||
preferenceSyncConflictText: "Các thiết lập đồng bộ hóa được bật sẽ lưu các giá trị của chúng vào máy chủ. Tuy nhiên, có những giá trị hiện có trên máy chủ. Bạn muốn ghi đè lên bộ giá trị nào?"
|
|
||||||
paste: "dán"
|
paste: "dán"
|
||||||
postForm: "Mẫu đăng"
|
postForm: "Mẫu đăng"
|
||||||
information: "Giới thiệu"
|
information: "Giới thiệu"
|
||||||
|
@ -1227,8 +1223,6 @@ _chat:
|
||||||
members: "Thành viên"
|
members: "Thành viên"
|
||||||
home: "Trang chính"
|
home: "Trang chính"
|
||||||
send: "Gửi"
|
send: "Gửi"
|
||||||
_settings:
|
|
||||||
preferencesBanner: "Bạn có thể cấu hình hành vi chung của máy khách theo sở thích của mình."
|
|
||||||
_accountSettings:
|
_accountSettings:
|
||||||
requireSigninToViewContents: "Yêu cầu đăng nhập để xem nội dung"
|
requireSigninToViewContents: "Yêu cầu đăng nhập để xem nội dung"
|
||||||
requireSigninToViewContentsDescription1: "Yêu cầu đăng nhập để xem tất cả ghi chú và nội dung khác mà bạn tạo. Điều này được kỳ vọng sẽ có hiệu quả trong việc ngăn chặn thông tin bị thu thập bởi các trình thu thập thông tin."
|
requireSigninToViewContentsDescription1: "Yêu cầu đăng nhập để xem tất cả ghi chú và nội dung khác mà bạn tạo. Điều này được kỳ vọng sẽ có hiệu quả trong việc ngăn chặn thông tin bị thu thập bởi các trình thu thập thông tin."
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2025.6.1-beta.1",
|
"version": "2025.6.1-alpha.1",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -37,17 +37,17 @@
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-android-arm64": "1.3.11",
|
"@swc/core-android-arm64": "1.3.11",
|
||||||
"@swc/core-darwin-arm64": "1.12.0",
|
"@swc/core-darwin-arm64": "1.11.29",
|
||||||
"@swc/core-darwin-x64": "1.12.0",
|
"@swc/core-darwin-x64": "1.11.29",
|
||||||
"@swc/core-freebsd-x64": "1.3.11",
|
"@swc/core-freebsd-x64": "1.3.11",
|
||||||
"@swc/core-linux-arm-gnueabihf": "1.12.0",
|
"@swc/core-linux-arm-gnueabihf": "1.11.29",
|
||||||
"@swc/core-linux-arm64-gnu": "1.12.0",
|
"@swc/core-linux-arm64-gnu": "1.11.29",
|
||||||
"@swc/core-linux-arm64-musl": "1.12.0",
|
"@swc/core-linux-arm64-musl": "1.11.29",
|
||||||
"@swc/core-linux-x64-gnu": "1.12.0",
|
"@swc/core-linux-x64-gnu": "1.11.29",
|
||||||
"@swc/core-linux-x64-musl": "1.12.0",
|
"@swc/core-linux-x64-musl": "1.11.29",
|
||||||
"@swc/core-win32-arm64-msvc": "1.12.0",
|
"@swc/core-win32-arm64-msvc": "1.11.29",
|
||||||
"@swc/core-win32-ia32-msvc": "1.12.0",
|
"@swc/core-win32-ia32-msvc": "1.11.29",
|
||||||
"@swc/core-win32-x64-msvc": "1.12.0",
|
"@swc/core-win32-x64-msvc": "1.11.29",
|
||||||
"@tensorflow/tfjs": "4.22.0",
|
"@tensorflow/tfjs": "4.22.0",
|
||||||
"@tensorflow/tfjs-node": "4.22.0",
|
"@tensorflow/tfjs-node": "4.22.0",
|
||||||
"bufferutil": "4.0.9",
|
"bufferutil": "4.0.9",
|
||||||
|
@ -67,8 +67,8 @@
|
||||||
"utf-8-validate": "6.0.5"
|
"utf-8-validate": "6.0.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.826.0",
|
"@aws-sdk/client-s3": "3.817.0",
|
||||||
"@aws-sdk/lib-storage": "3.826.0",
|
"@aws-sdk/lib-storage": "3.817.0",
|
||||||
"@discordapp/twemoji": "15.1.0",
|
"@discordapp/twemoji": "15.1.0",
|
||||||
"@fastify/accepts": "5.0.2",
|
"@fastify/accepts": "5.0.2",
|
||||||
"@fastify/cookie": "11.0.2",
|
"@fastify/cookie": "11.0.2",
|
||||||
|
@ -80,10 +80,10 @@
|
||||||
"@fastify/view": "10.0.2",
|
"@fastify/view": "10.0.2",
|
||||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||||
"@misskey-dev/summaly": "5.2.1",
|
"@misskey-dev/summaly": "5.2.1",
|
||||||
"@napi-rs/canvas": "0.1.71",
|
"@napi-rs/canvas": "0.1.70",
|
||||||
"@nestjs/common": "11.1.3",
|
"@nestjs/common": "11.1.2",
|
||||||
"@nestjs/core": "11.1.3",
|
"@nestjs/core": "11.1.2",
|
||||||
"@nestjs/testing": "11.1.3",
|
"@nestjs/testing": "11.1.2",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sentry/node": "8.55.0",
|
"@sentry/node": "8.55.0",
|
||||||
"@sentry/profiling-node": "8.55.0",
|
"@sentry/profiling-node": "8.55.0",
|
||||||
|
@ -91,7 +91,7 @@
|
||||||
"@sinonjs/fake-timers": "11.3.1",
|
"@sinonjs/fake-timers": "11.3.1",
|
||||||
"@smithy/node-http-handler": "2.5.0",
|
"@smithy/node-http-handler": "2.5.0",
|
||||||
"@swc/cli": "0.7.7",
|
"@swc/cli": "0.7.7",
|
||||||
"@swc/core": "1.12.0",
|
"@swc/core": "1.11.29",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"@types/redis-info": "3.0.3",
|
"@types/redis-info": "3.0.3",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
|
@ -101,7 +101,7 @@
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "1.20.3",
|
"body-parser": "1.20.3",
|
||||||
"bullmq": "5.53.2",
|
"bullmq": "5.53.0",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.2",
|
"cbor": "9.0.2",
|
||||||
"chalk": "5.4.1",
|
"chalk": "5.4.1",
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "19.6.0",
|
"file-type": "19.6.0",
|
||||||
"fluent-ffmpeg": "2.1.3",
|
"fluent-ffmpeg": "2.1.3",
|
||||||
"form-data": "4.0.3",
|
"form-data": "4.0.2",
|
||||||
"got": "14.4.7",
|
"got": "14.4.7",
|
||||||
"happy-dom": "16.8.1",
|
"happy-dom": "16.8.1",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
|
@ -133,9 +133,9 @@
|
||||||
"jsonld": "8.3.3",
|
"jsonld": "8.3.3",
|
||||||
"jsrsasign": "11.1.0",
|
"jsrsasign": "11.1.0",
|
||||||
"juice": "11.0.1",
|
"juice": "11.0.1",
|
||||||
"meilisearch": "0.51.0",
|
"meilisearch": "0.50.0",
|
||||||
"mfm-js": "0.24.0",
|
"mfm-js": "0.24.0",
|
||||||
"microformats-parser": "2.0.3",
|
"microformats-parser": "2.0.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"misskey-reversi": "workspace:*",
|
"misskey-reversi": "workspace:*",
|
||||||
|
@ -188,16 +188,16 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.7.0",
|
"@jest/globals": "29.7.0",
|
||||||
"@nestjs/platform-express": "10.4.19",
|
"@nestjs/platform-express": "10.4.18",
|
||||||
"@sentry/vue": "9.28.0",
|
"@sentry/vue": "9.22.0",
|
||||||
"@simplewebauthn/types": "12.0.0",
|
"@simplewebauthn/types": "12.0.0",
|
||||||
"@swc/jest": "0.2.38",
|
"@swc/jest": "0.2.38",
|
||||||
"@types/accepts": "1.3.7",
|
"@types/accepts": "1.3.7",
|
||||||
"@types/archiver": "6.0.3",
|
"@types/archiver": "6.0.3",
|
||||||
"@types/bcryptjs": "2.4.6",
|
"@types/bcryptjs": "2.4.6",
|
||||||
"@types/body-parser": "1.19.6",
|
"@types/body-parser": "1.19.5",
|
||||||
"@types/color-convert": "2.0.4",
|
"@types/color-convert": "2.0.4",
|
||||||
"@types/content-disposition": "0.5.9",
|
"@types/content-disposition": "0.5.8",
|
||||||
"@types/fluent-ffmpeg": "2.1.27",
|
"@types/fluent-ffmpeg": "2.1.27",
|
||||||
"@types/htmlescape": "1.1.3",
|
"@types/htmlescape": "1.1.3",
|
||||||
"@types/http-link-header": "1.0.7",
|
"@types/http-link-header": "1.0.7",
|
||||||
|
@ -208,12 +208,12 @@
|
||||||
"@types/jsrsasign": "10.5.15",
|
"@types/jsrsasign": "10.5.15",
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/ms": "0.7.34",
|
"@types/ms": "0.7.34",
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.21",
|
||||||
"@types/nodemailer": "6.4.17",
|
"@types/nodemailer": "6.4.17",
|
||||||
"@types/oauth": "0.9.6",
|
"@types/oauth": "0.9.6",
|
||||||
"@types/oauth2orize": "1.11.5",
|
"@types/oauth2orize": "1.11.5",
|
||||||
"@types/oauth2orize-pkce": "0.1.2",
|
"@types/oauth2orize-pkce": "0.1.2",
|
||||||
"@types/pg": "8.15.4",
|
"@types/pg": "8.15.2",
|
||||||
"@types/pug": "2.0.10",
|
"@types/pug": "2.0.10",
|
||||||
"@types/qrcode": "1.5.5",
|
"@types/qrcode": "1.5.5",
|
||||||
"@types/random-seed": "0.3.5",
|
"@types/random-seed": "0.3.5",
|
||||||
|
@ -229,8 +229,8 @@
|
||||||
"@types/vary": "1.1.3",
|
"@types/vary": "1.1.3",
|
||||||
"@types/web-push": "3.6.4",
|
"@types/web-push": "3.6.4",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "8.34.0",
|
"@typescript-eslint/eslint-plugin": "8.32.1",
|
||||||
"@typescript-eslint/parser": "8.34.0",
|
"@typescript-eslint/parser": "8.32.1",
|
||||||
"aws-sdk-client-mock": "4.1.0",
|
"aws-sdk-client-mock": "4.1.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
|
|
|
@ -803,14 +803,14 @@ export class DriveService {
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.deletePostProcess(file, isExpired, deleter);
|
this.deletePostProcess(file, isExpired, deleter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
|
private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
|
||||||
// リモートファイル期限切れ削除後は直リンクにする
|
// リモートファイル期限切れ削除後は直リンクにする
|
||||||
if (isExpired && file.userHost !== null && file.uri != null) {
|
if (isExpired && file.userHost !== null && file.uri != null) {
|
||||||
await this.driveFilesRepository.update(file.id, {
|
this.driveFilesRepository.update(file.id, {
|
||||||
isLink: true,
|
isLink: true,
|
||||||
url: file.uri,
|
url: file.uri,
|
||||||
thumbnailUrl: null,
|
thumbnailUrl: null,
|
||||||
|
@ -822,7 +822,7 @@ export class DriveService {
|
||||||
webpublicAccessKey: 'webpublic-' + randomUUID(),
|
webpublicAccessKey: 'webpublic-' + randomUUID(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await this.driveFilesRepository.delete(file.id);
|
this.driveFilesRepository.delete(file.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.driveChart.update(file, false);
|
this.driveChart.update(file, false);
|
||||||
|
|
|
@ -482,19 +482,9 @@ export class ActivityPubServerService {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
dbFallback: async (untilId, sinceId, limit) => {
|
dbFallback: async (untilId, sinceId, limit) => {
|
||||||
return await this.getUserNotesFromDb({
|
return await this.getUserNotesFromDb(sinceId, untilId, limit, user.id);
|
||||||
untilId,
|
|
||||||
sinceId,
|
|
||||||
limit,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
}) : await this.getUserNotesFromDb({
|
}) : await this.getUserNotesFromDb(sinceId ?? null, untilId ?? null, limit, user.id);
|
||||||
untilId: untilId ?? null,
|
|
||||||
sinceId: sinceId ?? null,
|
|
||||||
limit,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sinceId) notes.reverse();
|
if (sinceId) notes.reverse();
|
||||||
|
|
||||||
|
@ -533,21 +523,16 @@ export class ActivityPubServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async getUserNotesFromDb(ps: {
|
private async getUserNotesFromDb(untilId: string | null, sinceId: string | null, limit: number, userId: MiUser['id']) {
|
||||||
untilId: string | null,
|
return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
|
||||||
sinceId: string | null,
|
.andWhere('note.userId = :userId', { userId })
|
||||||
limit: number,
|
|
||||||
userId: MiUser['id'],
|
|
||||||
}) {
|
|
||||||
return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
|
||||||
.andWhere('note.userId = :userId', { userId: ps.userId })
|
|
||||||
.andWhere(new Brackets(qb => {
|
.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
.where('note.visibility = \'public\'')
|
.where('note.visibility = \'public\'')
|
||||||
.orWhere('note.visibility = \'home\'');
|
.orWhere('note.visibility = \'home\'');
|
||||||
}))
|
}))
|
||||||
.andWhere('note.localOnly = FALSE')
|
.andWhere('note.localOnly = FALSE')
|
||||||
.limit(ps.limit)
|
.limit(limit)
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -380,7 +380,9 @@ describe('User', () => {
|
||||||
strictEqual(followers.length, 1); // followed by Bob
|
strictEqual(followers.length, 1); // followed by Bob
|
||||||
|
|
||||||
await alice.client.request('i/delete-account', { password: alice.password });
|
await alice.client.request('i/delete-account', { password: alice.password });
|
||||||
await sleep();
|
// NOTE: user deletion query is slow
|
||||||
|
// FIXME: ensure user is removed successfully
|
||||||
|
await sleep(10000);
|
||||||
|
|
||||||
const following = await bob.client.request('users/following', { userId: bob.id });
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
strictEqual(following.length, 0); // no following relation
|
strictEqual(following.length, 0); // no following relation
|
||||||
|
@ -478,7 +480,9 @@ describe('User', () => {
|
||||||
strictEqual(followers.length, 1); // followed by Bob
|
strictEqual(followers.length, 1); // followed by Bob
|
||||||
|
|
||||||
await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
|
await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
|
||||||
await sleep();
|
// NOTE: user deletion query is slow
|
||||||
|
// FIXME: ensure user is removed successfully
|
||||||
|
await sleep(10000);
|
||||||
|
|
||||||
const following = await bob.client.request('users/following', { userId: bob.id });
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
strictEqual(following.length, 0); // no following relation
|
strictEqual(following.length, 0); // no following relation
|
||||||
|
|
|
@ -26,9 +26,9 @@
|
||||||
"mfm-js": "0.24.0",
|
"mfm-js": "0.24.0",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"punycode.js": "2.3.1",
|
"punycode.js": "2.3.1",
|
||||||
"rollup": "4.42.0",
|
"rollup": "4.41.1",
|
||||||
"sass": "1.89.2",
|
"sass": "1.89.0",
|
||||||
"shiki": "3.6.0",
|
"shiki": "3.4.2",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
|
@ -39,27 +39,27 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/summaly": "5.2.1",
|
"@misskey-dev/summaly": "5.2.1",
|
||||||
"@tabler/icons-webfont": "3.34.0",
|
"@tabler/icons-webfont": "3.33.0",
|
||||||
"@testing-library/vue": "8.1.0",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/estree": "1.0.8",
|
"@types/estree": "1.0.7",
|
||||||
"@types/micromatch": "4.0.9",
|
"@types/micromatch": "4.0.9",
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.28",
|
||||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "8.34.0",
|
"@typescript-eslint/eslint-plugin": "8.33.0",
|
||||||
"@typescript-eslint/parser": "8.34.0",
|
"@typescript-eslint/parser": "8.33.0",
|
||||||
"@vitest/coverage-v8": "3.2.3",
|
"@vitest/coverage-v8": "3.1.4",
|
||||||
"@vue/runtime-core": "3.5.16",
|
"@vue/runtime-core": "3.5.16",
|
||||||
"acorn": "8.15.0",
|
"acorn": "8.14.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-vue": "10.2.0",
|
"eslint-plugin-vue": "10.1.0",
|
||||||
"fast-glob": "3.3.3",
|
"fast-glob": "3.3.3",
|
||||||
"happy-dom": "17.6.3",
|
"happy-dom": "17.5.6",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"msw": "2.10.2",
|
"msw": "2.8.6",
|
||||||
"nodemon": "3.1.10",
|
"nodemon": "3.1.10",
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"start-server-and-test": "2.0.12",
|
"start-server-and-test": "2.0.12",
|
||||||
|
|
|
@ -21,11 +21,11 @@
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.28",
|
||||||
"@typescript-eslint/eslint-plugin": "8.34.0",
|
"@typescript-eslint/eslint-plugin": "8.33.0",
|
||||||
"@typescript-eslint/parser": "8.34.0",
|
"@typescript-eslint/parser": "8.33.0",
|
||||||
"esbuild": "0.25.5",
|
"esbuild": "0.25.5",
|
||||||
"eslint-plugin-vue": "10.2.0",
|
"eslint-plugin-vue": "10.1.0",
|
||||||
"nodemon": "3.1.10",
|
"nodemon": "3.1.10",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"vue-eslint-parser": "10.1.3"
|
"vue-eslint-parser": "10.1.3"
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
"@rollup/plugin-json": "6.1.0",
|
"@rollup/plugin-json": "6.1.0",
|
||||||
"@rollup/plugin-replace": "6.0.2",
|
"@rollup/plugin-replace": "6.0.2",
|
||||||
"@rollup/pluginutils": "5.1.4",
|
"@rollup/pluginutils": "5.1.4",
|
||||||
"@sentry/vue": "9.27.0",
|
"@sentry/vue": "9.24.0",
|
||||||
"@syuilo/aiscript": "0.19.0",
|
"@syuilo/aiscript": "0.19.0",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"@vitejs/plugin-vue": "5.2.4",
|
"@vitejs/plugin-vue": "5.2.4",
|
||||||
|
@ -60,10 +60,10 @@
|
||||||
"misskey-reversi": "workspace:*",
|
"misskey-reversi": "workspace:*",
|
||||||
"photoswipe": "5.4.4",
|
"photoswipe": "5.4.4",
|
||||||
"punycode.js": "2.3.1",
|
"punycode.js": "2.3.1",
|
||||||
"rollup": "4.42.0",
|
"rollup": "4.41.1",
|
||||||
"sanitize-html": "2.17.0",
|
"sanitize-html": "2.17.0",
|
||||||
"sass": "1.89.2",
|
"sass": "1.89.0",
|
||||||
"shiki": "3.6.0",
|
"shiki": "3.4.2",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.177.0",
|
"three": "0.177.0",
|
||||||
|
@ -98,36 +98,36 @@
|
||||||
"@storybook/types": "8.6.14",
|
"@storybook/types": "8.6.14",
|
||||||
"@storybook/vue3": "8.6.14",
|
"@storybook/vue3": "8.6.14",
|
||||||
"@storybook/vue3-vite": "8.6.14",
|
"@storybook/vue3-vite": "8.6.14",
|
||||||
"@tabler/icons-webfont": "3.34.0",
|
"@tabler/icons-webfont": "3.33.0",
|
||||||
"@testing-library/vue": "8.1.0",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/canvas-confetti": "1.9.0",
|
"@types/canvas-confetti": "1.9.0",
|
||||||
"@types/estree": "1.0.8",
|
"@types/estree": "1.0.7",
|
||||||
"@types/matter-js": "0.19.8",
|
"@types/matter-js": "0.19.8",
|
||||||
"@types/micromatch": "4.0.9",
|
"@types/micromatch": "4.0.9",
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.28",
|
||||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||||
"@types/sanitize-html": "2.16.0",
|
"@types/sanitize-html": "2.16.0",
|
||||||
"@types/seedrandom": "3.0.8",
|
"@types/seedrandom": "3.0.8",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
"@types/throttle-debounce": "5.0.2",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "8.34.0",
|
"@typescript-eslint/eslint-plugin": "8.33.0",
|
||||||
"@typescript-eslint/parser": "8.34.0",
|
"@typescript-eslint/parser": "8.33.0",
|
||||||
"@vitest/coverage-v8": "3.2.3",
|
"@vitest/coverage-v8": "3.1.4",
|
||||||
"@vue/compiler-core": "3.5.16",
|
"@vue/compiler-core": "3.5.16",
|
||||||
"@vue/runtime-core": "3.5.16",
|
"@vue/runtime-core": "3.5.16",
|
||||||
"acorn": "8.15.0",
|
"acorn": "8.14.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "14.4.1",
|
"cypress": "14.4.0",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-vue": "10.2.0",
|
"eslint-plugin-vue": "10.1.0",
|
||||||
"fast-glob": "3.3.3",
|
"fast-glob": "3.3.3",
|
||||||
"happy-dom": "17.6.3",
|
"happy-dom": "17.5.6",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"minimatch": "10.0.1",
|
"minimatch": "10.0.1",
|
||||||
"msw": "2.10.2",
|
"msw": "2.8.6",
|
||||||
"msw-storybook-addon": "2.0.5",
|
"msw-storybook-addon": "2.0.4",
|
||||||
"nodemon": "3.1.10",
|
"nodemon": "3.1.10",
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
"storybook": "8.6.14",
|
"storybook": "8.6.14",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vitest": "3.2.3",
|
"vitest": "3.1.4",
|
||||||
"vitest-fetch-mock": "0.4.5",
|
"vitest-fetch-mock": "0.4.5",
|
||||||
"vue-component-type-helpers": "2.2.10",
|
"vue-component-type-helpers": "2.2.10",
|
||||||
"vue-eslint-parser": "10.1.3",
|
"vue-eslint-parser": "10.1.3",
|
||||||
|
|
|
@ -16,54 +16,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<div :class="$style.root" class="_gaps">
|
<div :class="$style.root" class="_gaps">
|
||||||
<div v-for="[k, v] in Object.entries(fx.params)" :key="k">
|
<div v-for="[k, v] in Object.entries(fx.params)" :key="k">
|
||||||
<MkSwitch
|
<MkSwitch v-if="v.type === 'boolean'" v-model="layer.params[k]">
|
||||||
v-if="v.type === 'boolean'"
|
|
||||||
v-model="layer.params[k]"
|
|
||||||
>
|
|
||||||
<template #label>{{ k }}</template>
|
<template #label>{{ k }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkRange
|
<MkRange v-else-if="v.type === 'number'" v-model="layer.params[k]" continuousUpdate :min="v.min" :max="v.max" :step="v.step">
|
||||||
v-else-if="v.type === 'number'"
|
|
||||||
v-model="layer.params[k]"
|
|
||||||
continuousUpdate
|
|
||||||
:min="v.min"
|
|
||||||
:max="v.max"
|
|
||||||
:step="v.step"
|
|
||||||
@thumbDoubleClicked="() => {
|
|
||||||
if (fx.params[k].default != null) {
|
|
||||||
layer.params[k] = fx.params[k].default;
|
|
||||||
} else {
|
|
||||||
layer.params[k] = v.min;
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #label>{{ k }}</template>
|
<template #label>{{ k }}</template>
|
||||||
</MkRange>
|
</MkRange>
|
||||||
<MkRadios
|
<MkRadios v-else-if="v.type === 'number:enum'" v-model="layer.params[k]">
|
||||||
v-else-if="v.type === 'number:enum'"
|
|
||||||
v-model="layer.params[k]"
|
|
||||||
>
|
|
||||||
<template #label>{{ k }}</template>
|
<template #label>{{ k }}</template>
|
||||||
<option v-for="item in v.enum" :value="item.value">{{ item.label }}</option>
|
<option v-for="item in v.enum" :value="item.value">{{ item.label }}</option>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<div v-else-if="v.type === 'seed'">
|
<div v-else-if="v.type === 'seed'">
|
||||||
<MkRange
|
<MkRange v-model="layer.params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">
|
||||||
v-model="layer.params[k]"
|
|
||||||
continuousUpdate
|
|
||||||
type="number"
|
|
||||||
:min="0"
|
|
||||||
:max="10000"
|
|
||||||
:step="1"
|
|
||||||
>
|
|
||||||
<template #label>{{ k }}</template>
|
<template #label>{{ k }}</template>
|
||||||
</MkRange>
|
</MkRange>
|
||||||
</div>
|
</div>
|
||||||
<MkInput
|
<MkInput v-else-if="v.type === 'color'" :modelValue="`#${(layer.params[k][0] * 255).toString(16).padStart(2, '0')}${(layer.params[k][1] * 255).toString(16).padStart(2, '0')}${(layer.params[k][2] * 255).toString(16).padStart(2, '0')}`" type="color" @update:modelValue="v => { const c = v.slice(1).match(/.{2}/g)?.map(x => parseInt(x, 16) / 255); if (c) layer.params[k] = c; }">
|
||||||
v-else-if="v.type === 'color'"
|
|
||||||
:modelValue="getHex(layer.params[k])"
|
|
||||||
type="color"
|
|
||||||
@update:modelValue="v => { const c = getRgb(v); if (c != null) layer.params[k] = c; }"
|
|
||||||
>
|
|
||||||
<template #label>{{ k }}</template>
|
<template #label>{{ k }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,14 +40,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue';
|
||||||
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
|
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkRange from '@/components/MkRange.vue';
|
import MkRange from '@/components/MkRange.vue';
|
||||||
|
import FormSlot from '@/components/form/slot.vue';
|
||||||
|
import MkPositionSelector from '@/components/MkPositionSelector.vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { selectFile } from '@/utility/drive.js';
|
||||||
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
import { prefer } from '@/preferences.js';
|
||||||
import { FXS } from '@/utility/image-effector/fxs.js';
|
import { FXS } from '@/utility/image-effector/fxs.js';
|
||||||
|
|
||||||
const layer = defineModel<ImageEffectorLayer>('layer', { required: true });
|
const layer = defineModel<ImageEffectorLayer>('layer', { required: true });
|
||||||
|
@ -93,24 +69,6 @@ const emit = defineEmits<{
|
||||||
(e: 'swapUp'): void;
|
(e: 'swapUp'): void;
|
||||||
(e: 'swapDown'): void;
|
(e: 'swapDown'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function getHex(c: [number, number, number]) {
|
|
||||||
return `#${c.map(x => (x * 255).toString(16).padStart(2, '0')).join('')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRgb(hex: string | number): [number, number, number] | null {
|
|
||||||
if (
|
|
||||||
typeof hex === 'number' ||
|
|
||||||
typeof hex !== 'string' ||
|
|
||||||
!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const m = hex.slice(1).match(/[0-9a-fA-F]{2}/g);
|
|
||||||
if (m == null) return [0, 0, 0];
|
|
||||||
return m.map(x => parseInt(x, 16) / 255) as [number, number, number];
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
|
|
|
@ -265,21 +265,21 @@ const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', nul
|
||||||
|
|
||||||
let note = deepClone(props.note);
|
let note = deepClone(props.note);
|
||||||
|
|
||||||
// コンポーネント初期化に非同期的な処理を行うとTransitionのレンダリングがバグるため同期的に実行できるメソッドが実装されるのを待つ必要がある
|
// plugin
|
||||||
// https://github.com/aiscript-dev/aiscript/issues/937
|
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
||||||
//// plugin
|
if (noteViewInterruptors.length > 0) {
|
||||||
//const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
onMounted(async () => {
|
||||||
//if (noteViewInterruptors.length > 0) {
|
let result: Misskey.entities.Note | null = deepClone(note);
|
||||||
// let result: Misskey.entities.Note | null = deepClone(note);
|
for (const interruptor of noteViewInterruptors) {
|
||||||
// for (const interruptor of noteViewInterruptors) {
|
try {
|
||||||
// try {
|
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
// result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
} catch (err) {
|
||||||
// } catch (err) {
|
console.error(err);
|
||||||
// console.error(err);
|
}
|
||||||
// }
|
}
|
||||||
// }
|
note = result as Misskey.entities.Note;
|
||||||
// note = result as Misskey.entities.Note;
|
});
|
||||||
//}
|
}
|
||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
const appearNote = getAppearNote(note);
|
const appearNote = getAppearNote(note);
|
||||||
|
|
|
@ -286,20 +286,21 @@ const inChannel = inject('inChannel', null);
|
||||||
|
|
||||||
let note = deepClone(props.note);
|
let note = deepClone(props.note);
|
||||||
|
|
||||||
// コンポーネント初期化に非同期的な処理を行うとTransitionのレンダリングがバグるため同期的に実行できるメソッドが実装されるのを待つ必要がある
|
// plugin
|
||||||
//// plugin
|
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
||||||
//const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
if (noteViewInterruptors.length > 0) {
|
||||||
//if (noteViewInterruptors.length > 0) {
|
onMounted(async () => {
|
||||||
// let result: Misskey.entities.Note | null = deepClone(note);
|
let result: Misskey.entities.Note | null = deepClone(note);
|
||||||
// for (const interruptor of noteViewInterruptors) {
|
for (const interruptor of noteViewInterruptors) {
|
||||||
// try {
|
try {
|
||||||
// result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
// } catch (err) {
|
} catch (err) {
|
||||||
// console.error(err);
|
console.error(err);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// note = result as Misskey.entities.Note;
|
note = result as Misskey.entities.Note;
|
||||||
//}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
const appearNote = getAppearNote(note);
|
const appearNote = getAppearNote(note);
|
||||||
|
|
|
@ -72,29 +72,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
|
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
|
||||||
<div v-if="uploader.items.value.length > 0" style="padding: 12px;">
|
|
||||||
<MkTip k="postFormUploader">
|
|
||||||
{{ i18n.ts._postForm.uploaderTip }}
|
|
||||||
</MkTip>
|
|
||||||
<MkUploaderItems :items="uploader.items.value" @showMenu="(item, ev) => showPerUploadItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerUploadItemMenuViaContextmenu(item, ev)"/>
|
|
||||||
</div>
|
|
||||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
||||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||||
</div>
|
</div>
|
||||||
<footer :class="$style.footer">
|
<footer :class="$style.footer">
|
||||||
<div :class="$style.footerLeft">
|
<div :class="$style.footerLeft">
|
||||||
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.upload + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromPc"><i class="ti ti-photo-plus"></i></button>
|
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
|
||||||
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.fromDrive + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromDrive"><i class="ti ti-cloud-download"></i></button>
|
|
||||||
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
||||||
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
||||||
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
|
||||||
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
||||||
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
|
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
||||||
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
||||||
|
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
||||||
|
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.footerRight">
|
<div :class="$style.footerRight">
|
||||||
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
|
||||||
|
<!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button>-->
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<datalist id="hashtags">
|
<datalist id="hashtags">
|
||||||
|
@ -110,12 +105,10 @@ import * as Misskey from 'misskey-js';
|
||||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||||
import { toASCII } from 'punycode.js';
|
import { toASCII } from 'punycode.js';
|
||||||
import { host, url } from '@@/js/config.js';
|
import { host, url } from '@@/js/config.js';
|
||||||
import MkUploaderItems from './MkUploaderItems.vue';
|
|
||||||
import type { ShallowRef } from 'vue';
|
import type { ShallowRef } from 'vue';
|
||||||
import type { PostFormProps } from '@/types/post-form.js';
|
import type { PostFormProps } from '@/types/post-form.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||||
import type { UploaderItem } from '@/composables/use-uploader.js';
|
|
||||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||||
import XTextCounter from '@/components/MkPostForm.TextCounter.vue';
|
import XTextCounter from '@/components/MkPostForm.TextCounter.vue';
|
||||||
|
@ -127,7 +120,7 @@ import { formatTimeString } from '@/utility/format-time-string.js';
|
||||||
import { Autocomplete } from '@/utility/autocomplete.js';
|
import { Autocomplete } from '@/utility/autocomplete.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { chooseDriveFile } from '@/utility/drive.js';
|
import { selectFile } from '@/utility/drive.js';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -145,7 +138,6 @@ import { getPluginHandlers } from '@/plugin.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
import { globalEvents } from '@/events.js';
|
import { globalEvents } from '@/events.js';
|
||||||
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
|
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
|
||||||
import { useUploader } from '@/composables/use-uploader.js';
|
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
@ -209,15 +201,6 @@ const justEndedComposition = ref(false);
|
||||||
const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote);
|
const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote);
|
||||||
const postFormActions = getPluginHandlers('post_form_action');
|
const postFormActions = getPluginHandlers('post_form_action');
|
||||||
|
|
||||||
const uploader = useUploader({
|
|
||||||
multiple: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
uploader.events.on('itemUploaded', ctx => {
|
|
||||||
files.value.push(ctx.item.uploaded!);
|
|
||||||
uploader.removeItem(ctx.item);
|
|
||||||
});
|
|
||||||
|
|
||||||
const draftKey = computed((): string => {
|
const draftKey = computed((): string => {
|
||||||
let key = props.channel ? `channel:${props.channel.id}` : '';
|
let key = props.channel ? `channel:${props.channel.id}` : '';
|
||||||
|
|
||||||
|
@ -275,11 +258,10 @@ const cwTextLength = computed((): number => {
|
||||||
const maxCwTextLength = 100;
|
const maxCwTextLength = 100;
|
||||||
|
|
||||||
const canPost = computed((): boolean => {
|
const canPost = computed((): boolean => {
|
||||||
return !props.mock && !posting.value && !posted.value && !uploader.uploading.value && (uploader.items.value.length === 0 || uploader.readyForUpload.value) &&
|
return !props.mock && !posting.value && !posted.value &&
|
||||||
(
|
(
|
||||||
1 <= textLength.value ||
|
1 <= textLength.value ||
|
||||||
1 <= files.value.length ||
|
1 <= files.value.length ||
|
||||||
1 <= uploader.items.value.length ||
|
|
||||||
poll.value != null ||
|
poll.value != null ||
|
||||||
renoteTargetNote.value != null ||
|
renoteTargetNote.value != null ||
|
||||||
quoteId.value != null
|
quoteId.value != null
|
||||||
|
@ -452,20 +434,17 @@ function focus() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseFileFromPc(ev: MouseEvent) {
|
function chooseFileFrom(ev) {
|
||||||
if (props.mock) return;
|
if (props.mock) return;
|
||||||
|
|
||||||
os.chooseFileFromPc({ multiple: true }).then(files => {
|
selectFile({
|
||||||
if (files.length === 0) return;
|
anchorElement: ev.currentTarget ?? ev.target,
|
||||||
uploader.addFiles(files);
|
multiple: true,
|
||||||
});
|
label: i18n.ts.attachFile,
|
||||||
}
|
}).then(files_ => {
|
||||||
|
for (const file of files_) {
|
||||||
function chooseFileFromDrive(ev: MouseEvent) {
|
files.value.push(file);
|
||||||
if (props.mock) return;
|
}
|
||||||
|
|
||||||
chooseDriveFile({ multiple: true }).then(driveFiles => {
|
|
||||||
files.value.push(...driveFiles);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -592,11 +571,6 @@ function showOtherSettings() {
|
||||||
toggleReactionAcceptance();
|
toggleReactionAcceptance();
|
||||||
},
|
},
|
||||||
}, { type: 'divider' }, {
|
}, { type: 'divider' }, {
|
||||||
type: 'switch',
|
|
||||||
icon: 'ti ti-eye',
|
|
||||||
text: i18n.ts.preview,
|
|
||||||
ref: showPreview,
|
|
||||||
}, {
|
|
||||||
icon: 'ti ti-trash',
|
icon: 'ti ti-trash',
|
||||||
text: i18n.ts.reset,
|
text: i18n.ts.reset,
|
||||||
danger: true,
|
danger: true,
|
||||||
|
@ -823,15 +797,6 @@ function isAnnoying(text: string): boolean {
|
||||||
text.includes('$[position');
|
text.includes('$[position');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFiles() {
|
|
||||||
await uploader.upload();
|
|
||||||
|
|
||||||
for (const uploadedItem of uploader.items.value.filter(x => x.uploaded != null)) {
|
|
||||||
files.value.push(uploadedItem.uploaded!);
|
|
||||||
uploader.removeItem(uploadedItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function post(ev?: MouseEvent) {
|
async function post(ev?: MouseEvent) {
|
||||||
if (ev) {
|
if (ev) {
|
||||||
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
|
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
|
||||||
|
@ -875,10 +840,6 @@ async function post(ev?: MouseEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploader.items.value.some(x => x.uploaded == null)) {
|
|
||||||
await uploadFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
let postData = {
|
let postData = {
|
||||||
text: text.value === '' ? null : text.value,
|
text: text.value === '' ? null : text.value,
|
||||||
fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
|
fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
|
||||||
|
@ -1082,16 +1043,6 @@ function openAccountMenu(ev: MouseEvent) {
|
||||||
}, ev);
|
}, ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPerUploadItemMenu(item: UploaderItem, ev: MouseEvent) {
|
|
||||||
const menu = uploader.getMenu(item);
|
|
||||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
|
|
||||||
const menu = uploader.getMenu(item);
|
|
||||||
os.contextMenu(menu, ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.autofocus) {
|
if (props.autofocus) {
|
||||||
focus();
|
focus();
|
||||||
|
@ -1160,23 +1111,8 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function canClose() {
|
|
||||||
if (!uploader.allItemsUploaded.value) {
|
|
||||||
const { canceled } = await os.confirm({
|
|
||||||
type: 'question',
|
|
||||||
text: i18n.ts._postForm.quitInspiteOfThereAreUnuploadedFilesConfirm,
|
|
||||||
okText: i18n.ts.yes,
|
|
||||||
cancelText: i18n.ts.no,
|
|
||||||
});
|
|
||||||
if (canceled) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
clear,
|
clear,
|
||||||
canClose,
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkModal
|
<MkModal
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:preferType="'dialog'"
|
:preferType="'dialog'"
|
||||||
@click="_close()"
|
@click="modal?.close()"
|
||||||
@closed="onModalClosed()"
|
@closed="onModalClosed()"
|
||||||
@esc="_close()"
|
@esc="modal?.close()"
|
||||||
>
|
>
|
||||||
<MkPostForm
|
<MkPostForm
|
||||||
ref="form"
|
ref="form"
|
||||||
|
@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
autofocus
|
autofocus
|
||||||
freezeAfterPosted
|
freezeAfterPosted
|
||||||
@posted="onPosted"
|
@posted="onPosted"
|
||||||
@cancel="_close()"
|
@cancel="modal?.close()"
|
||||||
@esc="_close()"
|
@esc="modal?.close()"
|
||||||
/>
|
/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -43,7 +43,6 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = useTemplateRef('modal');
|
const modal = useTemplateRef('modal');
|
||||||
const form = useTemplateRef('form');
|
|
||||||
|
|
||||||
function onPosted() {
|
function onPosted() {
|
||||||
modal.value?.close({
|
modal.value?.close({
|
||||||
|
@ -51,12 +50,6 @@ function onPosted() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _close() {
|
|
||||||
const canClose = await form.value?.canClose();
|
|
||||||
if (!canClose) return;
|
|
||||||
modal.value?.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onModalClosed() {
|
function onModalClosed() {
|
||||||
emit('closed');
|
emit('closed');
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<slot name="prefix"></slot>
|
<slot name="prefix"></slot>
|
||||||
<div ref="containerEl" class="container">
|
<div ref="containerEl" class="container">
|
||||||
<div class="track">
|
<div class="track">
|
||||||
<div class="highlight right" :style="{ width: rightTrackWidth, left: rightTrackPosition }">
|
<div class="highlight right" :style="{ width: ((steppedRawValue - minRatio) * 100) + '%', left: (Math.abs(Math.min(0, min)) / (max + Math.abs(Math.min(0, min)))) * 100 + '%' }">
|
||||||
<div class="shine right"></div>
|
<div class="shine right"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="highlight left" :style="{ width: leftTrackWidth, left: leftTrackPosition }">
|
<div class="highlight left" :style="{ width: ((minRatio - steppedRawValue) * 100) + '%', left: (steppedRawValue) * 100 + '%' }">
|
||||||
<div class="shine left"></div>
|
<div class="shine left"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
|
||||||
import { isTouchUsing } from '@/utility/touch.js';
|
import { isTouchUsing } from '@/utility/touch.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
|
@ -58,14 +58,13 @@ const props = withDefaults(defineProps<{
|
||||||
continuousUpdate?: boolean;
|
continuousUpdate?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
step: 1,
|
step: 1,
|
||||||
textConverter: (v: number) => (Math.round(v * 1000) / 1000).toString(),
|
textConverter: (v) => v.toString(),
|
||||||
easing: false,
|
easing: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: number): void;
|
(ev: 'update:modelValue', value: number): void;
|
||||||
(ev: 'dragEnded', value: number): void;
|
(ev: 'dragEnded', value: number): void;
|
||||||
(ev: 'thumbDoubleClicked'): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const containerEl = useTemplateRef('containerEl');
|
const containerEl = useTemplateRef('containerEl');
|
||||||
|
@ -74,24 +73,7 @@ const thumbEl = useTemplateRef('thumbEl');
|
||||||
const maxRatio = computed(() => Math.abs(props.max) / (props.max + Math.abs(Math.min(0, props.min))));
|
const maxRatio = computed(() => Math.abs(props.max) / (props.max + Math.abs(Math.min(0, props.min))));
|
||||||
const minRatio = computed(() => Math.abs(Math.min(0, props.min)) / (props.max + Math.abs(Math.min(0, props.min))));
|
const minRatio = computed(() => Math.abs(Math.min(0, props.min)) / (props.max + Math.abs(Math.min(0, props.min))));
|
||||||
|
|
||||||
const rightTrackWidth = computed(() => {
|
const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
|
||||||
return Math.max(0, (steppedRawValue.value - minRatio.value) * 100) + '%';
|
|
||||||
});
|
|
||||||
const leftTrackWidth = computed(() => {
|
|
||||||
return Math.max(0, (minRatio.value - steppedRawValue.value) * 100) + '%';
|
|
||||||
});
|
|
||||||
const rightTrackPosition = computed(() => {
|
|
||||||
return (Math.abs(Math.min(0, props.min)) / (props.max + Math.abs(Math.min(0, props.min)))) * 100 + '%';
|
|
||||||
});
|
|
||||||
const leftTrackPosition = computed(() => {
|
|
||||||
return (Math.min(minRatio.value, steppedRawValue.value) * 100) + '%';
|
|
||||||
});
|
|
||||||
|
|
||||||
const calcRawValue = (value: number) => {
|
|
||||||
return (value - props.min) / (props.max - props.min);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rawValue = ref(calcRawValue(props.modelValue));
|
|
||||||
const steppedRawValue = computed(() => {
|
const steppedRawValue = computed(() => {
|
||||||
if (props.step) {
|
if (props.step) {
|
||||||
const step = props.step / (props.max - props.min);
|
const step = props.step / (props.max - props.min);
|
||||||
|
@ -121,11 +103,6 @@ const calcThumbPosition = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
watch([steppedRawValue, containerEl], calcThumbPosition);
|
watch([steppedRawValue, containerEl], calcThumbPosition);
|
||||||
watch(() => props.modelValue, (newVal) => {
|
|
||||||
const newRawValue = calcRawValue(newVal);
|
|
||||||
if (rawValue.value === newRawValue) return;
|
|
||||||
rawValue.value = newRawValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
let ro: ResizeObserver | undefined;
|
let ro: ResizeObserver | undefined;
|
||||||
|
|
||||||
|
@ -151,12 +128,6 @@ const steps = computed(() => {
|
||||||
const tooltipForDragShowing = ref(false);
|
const tooltipForDragShowing = ref(false);
|
||||||
const tooltipForHoverShowing = ref(false);
|
const tooltipForHoverShowing = ref(false);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
// 何らかの問題で表示されっぱなしでもコンポーネントを離れたら消えるように
|
|
||||||
tooltipForDragShowing.value = false;
|
|
||||||
tooltipForHoverShowing.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
function onMouseenter() {
|
function onMouseenter() {
|
||||||
if (isTouchUsing) return;
|
if (isTouchUsing) return;
|
||||||
|
|
||||||
|
@ -167,7 +138,7 @@ function onMouseenter() {
|
||||||
text: computed(() => {
|
text: computed(() => {
|
||||||
return props.textConverter(finalValue.value);
|
return props.textConverter(finalValue.value);
|
||||||
}),
|
}),
|
||||||
targetElement: thumbEl.value ?? undefined,
|
targetElement: thumbEl,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
|
@ -177,8 +148,6 @@ function onMouseenter() {
|
||||||
}, { once: true, passive: true });
|
}, { once: true, passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastClickTime: number | null = null;
|
|
||||||
|
|
||||||
function onMousedown(ev: MouseEvent | TouchEvent) {
|
function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
|
@ -189,7 +158,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
text: computed(() => {
|
text: computed(() => {
|
||||||
return props.textConverter(finalValue.value);
|
return props.textConverter(finalValue.value);
|
||||||
}),
|
}),
|
||||||
targetElement: thumbEl.value ?? undefined,
|
targetElement: thumbEl,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
|
@ -234,20 +203,6 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
window.addEventListener('touchmove', onDrag);
|
window.addEventListener('touchmove', onDrag);
|
||||||
window.addEventListener('mouseup', onMouseup, { once: true });
|
window.addEventListener('mouseup', onMouseup, { once: true });
|
||||||
window.addEventListener('touchend', onMouseup, { once: true });
|
window.addEventListener('touchend', onMouseup, { once: true });
|
||||||
|
|
||||||
if (lastClickTime == null) {
|
|
||||||
lastClickTime = Date.now();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastClickTime < 300) { // 300ms以内のクリックはダブルクリックとみなす
|
|
||||||
lastClickTime = null;
|
|
||||||
emit('thumbDoubleClicked');
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
lastClickTime = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,37 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
{{ i18n.ts._uploader.tip }}
|
{{ i18n.ts._uploader.tip }}
|
||||||
</MkTip>
|
</MkTip>
|
||||||
|
|
||||||
<MkUploaderItems :items="items" @showMenu="(item, ev) => showPerItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerItemMenuViaContextmenu(item, ev)"/>
|
<div class="_gaps_s">
|
||||||
|
<div
|
||||||
|
v-for="ctx in items"
|
||||||
|
:key="ctx.id"
|
||||||
|
v-panel
|
||||||
|
:class="[$style.item, ctx.preprocessing ? $style.itemWaiting : null, ctx.uploaded ? $style.itemCompleted : null, ctx.uploadFailed ? $style.itemFailed : null]"
|
||||||
|
:style="{ '--p': ctx.progress != null ? `${ctx.progress.value / ctx.progress.max * 100}%` : '0%' }"
|
||||||
|
>
|
||||||
|
<div :class="$style.itemInner">
|
||||||
|
<div :class="$style.itemActionWrapper">
|
||||||
|
<MkButton :iconOnly="true" rounded @click="showMenu($event, ctx)"><i class="ti ti-dots"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ ctx.thumbnail })` }"></div>
|
||||||
|
<div :class="$style.itemBody">
|
||||||
|
<div><MkCondensedLine :minScale="2 / 3">{{ ctx.name }}</MkCondensedLine></div>
|
||||||
|
<div :class="$style.itemInfo">
|
||||||
|
<span>{{ ctx.file.type }}</span>
|
||||||
|
<span v-if="ctx.compressedSize">({{ i18n.tsx._uploader.compressedToX({ x: bytes(ctx.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - ctx.compressedSize / ctx.file.size) * 100) }) }})</span>
|
||||||
|
<span v-else>{{ bytes(ctx.file.size) }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.itemIconWrapper">
|
||||||
|
<MkSystemIcon v-if="ctx.uploading" :class="$style.itemIcon" type="waiting"/>
|
||||||
|
<MkSystemIcon v-else-if="ctx.uploaded" :class="$style.itemIcon" type="success"/>
|
||||||
|
<MkSystemIcon v-else-if="ctx.uploadFailed" :class="$style.itemIcon" type="error"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="props.multiple">
|
<div v-if="props.multiple">
|
||||||
<MkButton style="margin: auto;" :iconOnly="true" rounded @click="chooseFile($event)"><i class="ti ti-plus"></i></MkButton>
|
<MkButton style="margin: auto;" :iconOnly="true" rounded @click="chooseFile($event)"><i class="ti ti-plus"></i></MkButton>
|
||||||
|
@ -39,8 +69,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="_buttonsCenter">
|
<div class="_buttonsCenter">
|
||||||
<MkButton v-if="uploader.uploading.value" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
|
<MkButton v-if="isUploading" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
|
||||||
<MkButton v-else-if="!firstUploadAttempted" primary rounded :disabled="!uploader.readyForUpload.value" @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
|
<MkButton v-else-if="!firstUploadAttempted" primary rounded @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
|
||||||
|
|
||||||
<MkButton v-if="canRetry" rounded @click="upload()"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
|
<MkButton v-if="canRetry" rounded @click="upload()"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
|
||||||
<MkButton v-if="canDone" rounded @click="done()"><i class="ti ti-arrow-right"></i> {{ i18n.ts.done }}</MkButton>
|
<MkButton v-if="canDone" rounded @click="done()"><i class="ti ti-arrow-right"></i> {{ i18n.ts.done }}</MkButton>
|
||||||
|
@ -49,51 +79,110 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkModalWindow>
|
</MkModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export type UploaderDialogFeatures = {
|
||||||
|
effect?: boolean;
|
||||||
|
watermark?: boolean;
|
||||||
|
crop?: boolean;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';
|
import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef, useTemplateRef, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import type { UploaderFeatures, UploaderItem } from '@/composables/use-uploader.js';
|
import { genId } from '@/utility/id.js';
|
||||||
|
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
||||||
|
import isAnimated from 'is-file-animated';
|
||||||
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { prefer } from '@/preferences.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import bytes from '@/filters/bytes.js';
|
||||||
|
import { isWebpSupported } from '@/utility/isWebpSupported.js';
|
||||||
|
import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { ensureSignin } from '@/i.js';
|
import { ensureSignin } from '@/i.js';
|
||||||
import { useUploader } from '@/composables/use-uploader.js';
|
import { WatermarkRenderer } from '@/utility/watermark.js';
|
||||||
import MkUploaderItems from '@/components/MkUploaderItems.vue';
|
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
const COMPRESSION_SUPPORTED_TYPES = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/webp',
|
||||||
|
'image/svg+xml',
|
||||||
|
];
|
||||||
|
|
||||||
|
const CROPPING_SUPPORTED_TYPES = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/webp',
|
||||||
|
];
|
||||||
|
|
||||||
|
const IMAGE_EDITING_SUPPORTED_TYPES = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/webp',
|
||||||
|
];
|
||||||
|
|
||||||
|
const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES;
|
||||||
|
|
||||||
|
const mimeTypeMap = {
|
||||||
|
'image/webp': 'webp',
|
||||||
|
'image/jpeg': 'jpg',
|
||||||
|
'image/png': 'png',
|
||||||
|
} as const;
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
files: File[];
|
files: File[];
|
||||||
folderId?: string | null;
|
folderId?: string | null;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
features?: UploaderFeatures;
|
features?: UploaderDialogFeatures;
|
||||||
}>(), {
|
}>(), {
|
||||||
multiple: true,
|
multiple: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const uploaderFeatures = computed<Required<UploaderDialogFeatures>>(() => {
|
||||||
|
return {
|
||||||
|
effect: props.features?.effect ?? true,
|
||||||
|
watermark: props.features?.watermark ?? true,
|
||||||
|
crop: props.features?.crop ?? true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', driveFiles: Misskey.entities.DriveFile[]): void;
|
(ev: 'done', driveFiles: Misskey.entities.DriveFile[]): void;
|
||||||
(ev: 'canceled'): void;
|
(ev: 'canceled'): void;
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
type UploaderItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
uploadName?: string;
|
||||||
|
progress: { max: number; value: number } | null;
|
||||||
|
thumbnail: string;
|
||||||
|
preprocessing: boolean;
|
||||||
|
uploading: boolean;
|
||||||
|
uploaded: Misskey.entities.DriveFile | null;
|
||||||
|
uploadFailed: boolean;
|
||||||
|
aborted: boolean;
|
||||||
|
compressionLevel: 0 | 1 | 2 | 3;
|
||||||
|
compressedSize?: number | null;
|
||||||
|
preprocessedFile?: Blob | null;
|
||||||
|
file: File;
|
||||||
|
watermarkPresetId: string | null;
|
||||||
|
abort?: (() => void) | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const items = ref<UploaderItem[]>([]);
|
||||||
|
|
||||||
const dialog = useTemplateRef('dialog');
|
const dialog = useTemplateRef('dialog');
|
||||||
|
|
||||||
const uploader = useUploader({
|
|
||||||
multiple: props.multiple,
|
|
||||||
folderId: props.folderId,
|
|
||||||
features: props.features,
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
uploader.addFiles(props.files);
|
|
||||||
});
|
|
||||||
|
|
||||||
const items = uploader.items;
|
|
||||||
|
|
||||||
const firstUploadAttempted = ref(false);
|
const firstUploadAttempted = ref(false);
|
||||||
const canRetry = computed(() => firstUploadAttempted.value && uploader.readyForUpload.value);
|
const isUploading = computed(() => items.value.some(item => item.uploading));
|
||||||
|
const canRetry = computed(() => firstUploadAttempted.value && !items.value.some(item => item.uploading || item.preprocessing) && items.value.some(item => item.uploaded == null));
|
||||||
const canDone = computed(() => items.value.some(item => item.uploaded != null));
|
const canDone = computed(() => items.value.some(item => item.uploaded != null));
|
||||||
const overallProgress = computed(() => {
|
const overallProgress = computed(() => {
|
||||||
const max = items.value.length;
|
const max = items.value.length;
|
||||||
|
@ -106,6 +195,27 @@ const overallProgress = computed(() => {
|
||||||
return Math.round((v / max) * 100);
|
return Math.round((v / max) * 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getCompressionSettings(level: 0 | 1 | 2 | 3) {
|
||||||
|
if (level === 1) {
|
||||||
|
return {
|
||||||
|
maxWidth: 2000,
|
||||||
|
maxHeight: 2000,
|
||||||
|
};
|
||||||
|
} else if (level === 2) {
|
||||||
|
return {
|
||||||
|
maxWidth: 2000 * 0.75, // =1500
|
||||||
|
maxHeight: 2000 * 0.75, // =1500
|
||||||
|
};
|
||||||
|
} else if (level === 3) {
|
||||||
|
return {
|
||||||
|
maxWidth: 2000 * 0.75 * 0.75, // =1125
|
||||||
|
maxHeight: 2000 * 0.75 * 0.75, // =1125
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(items, () => {
|
watch(items, () => {
|
||||||
if (items.value.length === 0) {
|
if (items.value.length === 0) {
|
||||||
emit('canceled');
|
emit('canceled');
|
||||||
|
@ -128,16 +238,11 @@ async function cancel() {
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
uploader.abortAll();
|
abortAll();
|
||||||
emit('canceled');
|
emit('canceled');
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function upload() {
|
|
||||||
firstUploadAttempted.value = true;
|
|
||||||
uploader.upload();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function abortWithConfirm() {
|
async function abortWithConfirm() {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
|
@ -147,11 +252,11 @@ async function abortWithConfirm() {
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
uploader.abortAll();
|
abortAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function done() {
|
async function done() {
|
||||||
if (!uploader.allItemsUploaded.value) {
|
if (items.value.some(item => item.uploaded == null)) {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
text: i18n.ts._uploader.doneConfirm,
|
text: i18n.ts._uploader.doneConfirm,
|
||||||
|
@ -165,20 +270,381 @@ async function done() {
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseFile(ev: MouseEvent) {
|
function showMenu(ev: MouseEvent, item: UploaderItem) {
|
||||||
const newFiles = await os.chooseFileFromPc({ multiple: true });
|
const menu: MenuItem[] = [];
|
||||||
uploader.addFiles(newFiles);
|
|
||||||
}
|
menu.push({
|
||||||
|
icon: 'ti ti-cursor-text',
|
||||||
|
text: i18n.ts.rename,
|
||||||
|
action: async () => {
|
||||||
|
const { result, canceled } = await os.inputText({
|
||||||
|
type: 'text',
|
||||||
|
title: i18n.ts.rename,
|
||||||
|
placeholder: item.name,
|
||||||
|
default: item.name,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
if (result.trim() === '') return;
|
||||||
|
|
||||||
|
item.name = result;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
uploaderFeatures.value.crop &&
|
||||||
|
CROPPING_SUPPORTED_TYPES.includes(item.file.type) &&
|
||||||
|
!item.preprocessing &&
|
||||||
|
!item.uploading &&
|
||||||
|
!item.uploaded
|
||||||
|
) {
|
||||||
|
menu.push({
|
||||||
|
icon: 'ti ti-crop',
|
||||||
|
text: i18n.ts.cropImage,
|
||||||
|
action: async () => {
|
||||||
|
const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
|
||||||
|
URL.revokeObjectURL(item.thumbnail);
|
||||||
|
const newItem = {
|
||||||
|
...item,
|
||||||
|
file: markRaw(cropped),
|
||||||
|
thumbnail: window.URL.createObjectURL(cropped),
|
||||||
|
};
|
||||||
|
items.value.splice(items.value.indexOf(item), 1, newItem);
|
||||||
|
preprocess(newItem).then(() => {
|
||||||
|
triggerRef(items);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
uploaderFeatures.value.effect &&
|
||||||
|
IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) &&
|
||||||
|
!item.preprocessing &&
|
||||||
|
!item.uploading &&
|
||||||
|
!item.uploaded
|
||||||
|
) {
|
||||||
|
menu.push({
|
||||||
|
icon: 'ti ti-sparkles',
|
||||||
|
text: i18n.ts._imageEffector.title + ' (BETA)',
|
||||||
|
action: async () => {
|
||||||
|
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageEffectorDialog.vue').then(x => x.default), {
|
||||||
|
image: item.file,
|
||||||
|
}, {
|
||||||
|
ok: (file) => {
|
||||||
|
URL.revokeObjectURL(item.thumbnail);
|
||||||
|
const newItem = {
|
||||||
|
...item,
|
||||||
|
file: markRaw(file),
|
||||||
|
thumbnail: window.URL.createObjectURL(file),
|
||||||
|
};
|
||||||
|
items.value.splice(items.value.indexOf(item), 1, newItem);
|
||||||
|
preprocess(newItem).then(() => {
|
||||||
|
triggerRef(items);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
uploaderFeatures.value.watermark &&
|
||||||
|
WATERMARK_SUPPORTED_TYPES.includes(item.file.type) &&
|
||||||
|
!item.preprocessing &&
|
||||||
|
!item.uploading &&
|
||||||
|
!item.uploaded
|
||||||
|
) {
|
||||||
|
function changeWatermarkPreset(presetId: string | null) {
|
||||||
|
item.watermarkPresetId = presetId;
|
||||||
|
preprocess(item).then(() => {
|
||||||
|
triggerRef(items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.push({
|
||||||
|
icon: 'ti ti-copyright',
|
||||||
|
text: i18n.ts.watermark,
|
||||||
|
type: 'parent',
|
||||||
|
children: [{
|
||||||
|
type: 'radioOption',
|
||||||
|
text: i18n.ts.none,
|
||||||
|
active: computed(() => item.watermarkPresetId == null),
|
||||||
|
action: () => changeWatermarkPreset(null),
|
||||||
|
}, {
|
||||||
|
type: 'divider',
|
||||||
|
}, ...prefer.s.watermarkPresets.map(preset => ({
|
||||||
|
type: 'radioOption' as const,
|
||||||
|
text: preset.name,
|
||||||
|
active: computed(() => item.watermarkPresetId === preset.id),
|
||||||
|
action: () => changeWatermarkPreset(preset.id),
|
||||||
|
})), ...(prefer.s.watermarkPresets.length > 0 ? [{
|
||||||
|
type: 'divider' as const,
|
||||||
|
}] : []), {
|
||||||
|
type: 'button',
|
||||||
|
icon: 'ti ti-plus',
|
||||||
|
text: i18n.ts.add,
|
||||||
|
action: async () => {
|
||||||
|
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkWatermarkEditorDialog.vue').then(x => x.default), {
|
||||||
|
image: item.file,
|
||||||
|
}, {
|
||||||
|
ok: (preset) => {
|
||||||
|
prefer.commit('watermarkPresets', [...prefer.s.watermarkPresets, preset]);
|
||||||
|
changeWatermarkPreset(preset.id);
|
||||||
|
},
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded) {
|
||||||
|
function changeCompressionLevel(level: 0 | 1 | 2 | 3) {
|
||||||
|
item.compressionLevel = level;
|
||||||
|
preprocess(item).then(() => {
|
||||||
|
triggerRef(items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.push({
|
||||||
|
icon: 'ti ti-leaf',
|
||||||
|
text: i18n.ts.compress,
|
||||||
|
type: 'parent',
|
||||||
|
children: [{
|
||||||
|
type: 'radioOption',
|
||||||
|
text: i18n.ts.none,
|
||||||
|
active: computed(() => item.compressionLevel === 0 || item.compressionLevel == null),
|
||||||
|
action: () => changeCompressionLevel(0),
|
||||||
|
}, {
|
||||||
|
type: 'divider',
|
||||||
|
}, {
|
||||||
|
type: 'radioOption',
|
||||||
|
text: i18n.ts.low,
|
||||||
|
active: computed(() => item.compressionLevel === 1),
|
||||||
|
action: () => changeCompressionLevel(1),
|
||||||
|
}, {
|
||||||
|
type: 'radioOption',
|
||||||
|
text: i18n.ts.medium,
|
||||||
|
active: computed(() => item.compressionLevel === 2),
|
||||||
|
action: () => changeCompressionLevel(2),
|
||||||
|
}, {
|
||||||
|
type: 'radioOption',
|
||||||
|
text: i18n.ts.high,
|
||||||
|
active: computed(() => item.compressionLevel === 3),
|
||||||
|
action: () => changeCompressionLevel(3),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.preprocessing && !item.uploading && !item.uploaded) {
|
||||||
|
menu.push({
|
||||||
|
type: 'divider',
|
||||||
|
}, {
|
||||||
|
icon: 'ti ti-x',
|
||||||
|
text: i18n.ts.remove,
|
||||||
|
action: () => {
|
||||||
|
URL.revokeObjectURL(item.thumbnail);
|
||||||
|
items.value.splice(items.value.indexOf(item), 1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (item.uploading) {
|
||||||
|
menu.push({
|
||||||
|
type: 'divider',
|
||||||
|
}, {
|
||||||
|
icon: 'ti ti-cloud-pause',
|
||||||
|
text: i18n.ts.abort,
|
||||||
|
danger: true,
|
||||||
|
action: () => {
|
||||||
|
if (item.abort != null) {
|
||||||
|
item.abort();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function showPerItemMenu(item: UploaderItem, ev: MouseEvent) {
|
|
||||||
const menu = uploader.getMenu(item);
|
|
||||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPerItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
|
async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
|
||||||
const menu = uploader.getMenu(item);
|
firstUploadAttempted.value = true;
|
||||||
os.contextMenu(menu, ev);
|
|
||||||
|
items.value = items.value.map(item => ({
|
||||||
|
...item,
|
||||||
|
aborted: false,
|
||||||
|
uploadFailed: false,
|
||||||
|
uploading: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const item of items.value.filter(item => item.uploaded == null)) {
|
||||||
|
// アップロード処理途中で値が変わる場合(途中で全キャンセルされたりなど)もあるので、Array filterではなくここでチェック
|
||||||
|
if (item.aborted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.uploadFailed = false;
|
||||||
|
item.uploading = true;
|
||||||
|
|
||||||
|
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
|
||||||
|
name: item.uploadName ?? item.name,
|
||||||
|
folderId: props.folderId,
|
||||||
|
onProgress: (progress) => {
|
||||||
|
if (item.progress == null) {
|
||||||
|
item.progress = { max: progress.total, value: progress.loaded };
|
||||||
|
} else {
|
||||||
|
item.progress.value = progress.loaded;
|
||||||
|
item.progress.max = progress.total;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
item.abort = () => {
|
||||||
|
item.abort = null;
|
||||||
|
abort();
|
||||||
|
item.uploading = false;
|
||||||
|
item.uploadFailed = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
await filePromise.then((file) => {
|
||||||
|
item.uploaded = file;
|
||||||
|
item.abort = null;
|
||||||
|
}).catch(err => {
|
||||||
|
item.uploadFailed = true;
|
||||||
|
item.progress = null;
|
||||||
|
if (!(err instanceof UploadAbortedError)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
item.uploading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function abortAll() {
|
||||||
|
for (const item of items.value) {
|
||||||
|
if (item.uploaded != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.abort != null) {
|
||||||
|
item.abort();
|
||||||
|
}
|
||||||
|
item.aborted = true;
|
||||||
|
item.uploadFailed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseFile(ev: MouseEvent) {
|
||||||
|
const newFiles = await os.chooseFileFromPc({ multiple: true });
|
||||||
|
|
||||||
|
for (const file of newFiles) {
|
||||||
|
initializeFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preprocess(item: (typeof items)['value'][number]): Promise<void> {
|
||||||
|
item.preprocessing = true;
|
||||||
|
|
||||||
|
let file: Blob | File = item.file;
|
||||||
|
const imageBitmap = await window.createImageBitmap(file);
|
||||||
|
|
||||||
|
const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(file.type);
|
||||||
|
const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId);
|
||||||
|
if (needsWatermark && preset != null) {
|
||||||
|
const canvas = window.document.createElement('canvas');
|
||||||
|
const renderer = new WatermarkRenderer({
|
||||||
|
canvas: canvas,
|
||||||
|
renderWidth: imageBitmap.width,
|
||||||
|
renderHeight: imageBitmap.height,
|
||||||
|
image: imageBitmap,
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderer.setLayers(preset.layers);
|
||||||
|
|
||||||
|
renderer.render();
|
||||||
|
|
||||||
|
file = await new Promise<Blob>((resolve) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob == null) {
|
||||||
|
throw new Error('Failed to convert canvas to blob');
|
||||||
|
}
|
||||||
|
resolve(blob);
|
||||||
|
renderer.destroy();
|
||||||
|
}, 'image/png');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const compressionSettings = getCompressionSettings(item.compressionLevel);
|
||||||
|
const needsCompress = item.compressionLevel !== 0 && compressionSettings && COMPRESSION_SUPPORTED_TYPES.includes(file.type) && !(await isAnimated(file));
|
||||||
|
|
||||||
|
if (needsCompress) {
|
||||||
|
const config = {
|
||||||
|
mimeType: isWebpSupported() ? 'image/webp' : 'image/jpeg',
|
||||||
|
maxWidth: compressionSettings.maxWidth,
|
||||||
|
maxHeight: compressionSettings.maxHeight,
|
||||||
|
quality: isWebpSupported() ? 0.85 : 0.8,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await readAndCompressImage(file, config);
|
||||||
|
if (result.size < file.size || file.type === 'image/webp') {
|
||||||
|
// The compression may not always reduce the file size
|
||||||
|
// (and WebP is not browser safe yet)
|
||||||
|
file = result;
|
||||||
|
item.compressedSize = result.size;
|
||||||
|
item.uploadName = file.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to resize image', err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.compressedSize = null;
|
||||||
|
item.uploadName = item.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
URL.revokeObjectURL(item.thumbnail);
|
||||||
|
item.thumbnail = window.URL.createObjectURL(file);
|
||||||
|
item.preprocessedFile = markRaw(file);
|
||||||
|
item.preprocessing = false;
|
||||||
|
|
||||||
|
imageBitmap.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeFile(file: File) {
|
||||||
|
const id = genId();
|
||||||
|
const filename = file.name ?? 'untitled';
|
||||||
|
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
|
||||||
|
const item = {
|
||||||
|
id,
|
||||||
|
name: prefer.s.keepOriginalFilename ? filename : id + extension,
|
||||||
|
progress: null,
|
||||||
|
thumbnail: window.URL.createObjectURL(file),
|
||||||
|
preprocessing: false,
|
||||||
|
uploading: false,
|
||||||
|
aborted: false,
|
||||||
|
uploaded: null,
|
||||||
|
uploadFailed: false,
|
||||||
|
compressionLevel: prefer.s.defaultImageCompressionLevel,
|
||||||
|
watermarkPresetId: uploaderFeatures.value.watermark ? prefer.s.defaultWatermarkPresetId : null,
|
||||||
|
file: markRaw(file),
|
||||||
|
} satisfies UploaderItem;
|
||||||
|
items.value.push(item);
|
||||||
|
preprocess(item).then(() => {
|
||||||
|
triggerRef(items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
for (const file of props.files) {
|
||||||
|
initializeFile(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
for (const item of items.value) {
|
||||||
|
URL.revokeObjectURL(item.thumbnail);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -200,4 +666,127 @@ function showPerItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
|
||||||
background: var(--MI_THEME-warn);
|
background: var(--MI_THEME-warn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: clip;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: var(--p);
|
||||||
|
height: 100%;
|
||||||
|
background: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
|
||||||
|
transition: width 0.2s ease, left 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.itemWaiting {
|
||||||
|
&::after {
|
||||||
|
--c: color(from var(--MI_THEME-accent) srgb r g b / 0.25);
|
||||||
|
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(-45deg, transparent 25%, var(--c) 25%,var(--c) 50%, transparent 50%, transparent 75%, var(--c) 75%, var(--c));
|
||||||
|
background-size: 25px 25px;
|
||||||
|
animation: stripe .8s infinite linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.itemCompleted {
|
||||||
|
&::before {
|
||||||
|
left: 100%;
|
||||||
|
width: var(--p);
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemBody {
|
||||||
|
color: var(--MI_THEME-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.itemFailed {
|
||||||
|
.itemBody {
|
||||||
|
color: var(--MI_THEME-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stripe {
|
||||||
|
0% { background-position-x: 0; }
|
||||||
|
100% { background-position-x: -25px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInner {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 8px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemThumbnail {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
background-color: var(--MI_THEME-bg);
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemBody {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInfo {
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 90%;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemIcon {
|
||||||
|
width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 500px) {
|
||||||
|
.itemInner {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemBody {
|
||||||
|
font-size: 90%;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemActionWrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInfo {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemIconWrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,196 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
-->
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="$style.root" class="_gaps_s">
|
|
||||||
<div
|
|
||||||
v-for="item in props.items"
|
|
||||||
:key="item.id"
|
|
||||||
v-panel
|
|
||||||
:class="[$style.item, { [$style.itemWaiting]: item.preprocessing, [$style.itemCompleted]: item.uploaded, [$style.itemFailed]: item.uploadFailed }]"
|
|
||||||
:style="{ '--p': item.progress != null ? `${item.progress.value / item.progress.max * 100}%` : '0%' }"
|
|
||||||
@contextmenu.prevent.stop="onContextmenu(item, $event)"
|
|
||||||
>
|
|
||||||
<div :class="$style.itemInner">
|
|
||||||
<div :class="$style.itemActionWrapper">
|
|
||||||
<MkButton :iconOnly="true" rounded @click="emit('showMenu', item, $event)"><i class="ti ti-dots"></i></MkButton>
|
|
||||||
</div>
|
|
||||||
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }" @click="onThumbnailClick(item, $event)"></div>
|
|
||||||
<div :class="$style.itemBody">
|
|
||||||
<div><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div>
|
|
||||||
<div :class="$style.itemInfo">
|
|
||||||
<span>{{ item.file.type }}</span>
|
|
||||||
<span v-if="item.compressedSize">({{ i18n.tsx._uploader.compressedToX({ x: bytes(item.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - item.compressedSize / item.file.size) * 100) }) }})</span>
|
|
||||||
<span v-else>{{ bytes(item.file.size) }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div :class="$style.itemIconWrapper">
|
|
||||||
<MkSystemIcon v-if="item.uploading" :class="$style.itemIcon" type="waiting"/>
|
|
||||||
<MkSystemIcon v-else-if="item.uploaded" :class="$style.itemIcon" type="success"/>
|
|
||||||
<MkSystemIcon v-else-if="item.uploadFailed" :class="$style.itemIcon" type="error"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { isLink } from '@@/js/is-link.js';
|
|
||||||
import type { UploaderItem } from '@/composables/use-uploader.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
|
||||||
import bytes from '@/filters/bytes.js';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
items: UploaderItem[];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: 'showMenu', item: UploaderItem, event: MouseEvent): void;
|
|
||||||
(ev: 'showMenuViaContextmenu', item: UploaderItem, event: MouseEvent): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
function onContextmenu(item: UploaderItem, ev: MouseEvent) {
|
|
||||||
if (ev.target && isLink(ev.target as HTMLElement)) return;
|
|
||||||
if (window.getSelection()?.toString() !== '') return;
|
|
||||||
|
|
||||||
emit('showMenuViaContextmenu', item, ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onThumbnailClick(item: UploaderItem, ev: MouseEvent) {
|
|
||||||
// TODO: preview when item is image
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.root {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
position: relative;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: clip;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: var(--p);
|
|
||||||
height: 100%;
|
|
||||||
background: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
|
|
||||||
transition: width 0.2s ease, left 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.itemWaiting {
|
|
||||||
&::after {
|
|
||||||
--c: color(from var(--MI_THEME-accent) srgb r g b / 0.25);
|
|
||||||
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(-45deg, transparent 25%, var(--c) 25%,var(--c) 50%, transparent 50%, transparent 75%, var(--c) 75%, var(--c));
|
|
||||||
background-size: 25px 25px;
|
|
||||||
animation: stripe .8s infinite linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.itemCompleted {
|
|
||||||
&::before {
|
|
||||||
left: 100%;
|
|
||||||
width: var(--p);
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemBody {
|
|
||||||
color: var(--MI_THEME-accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.itemFailed {
|
|
||||||
.itemBody {
|
|
||||||
color: var(--MI_THEME-error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes stripe {
|
|
||||||
0% { background-position-x: 0; }
|
|
||||||
100% { background-position-x: -25px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemInner {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 8px 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemThumbnail {
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
background-color: var(--MI_THEME-bg);
|
|
||||||
background-size: contain;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemBody {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemInfo {
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 90%;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemIcon {
|
|
||||||
width: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (max-width: 500px) {
|
|
||||||
.itemInner {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemBody {
|
|
||||||
font-size: 90%;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemActionWrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemInfo {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemIconWrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -7,10 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="!store.r.tips.value[props.k]" :class="[$style.root, { [$style.warn]: warn }]" class="_selectable _gaps_s">
|
<div v-if="!store.r.tips.value[props.k]" :class="[$style.root, { [$style.warn]: warn }]" class="_selectable _gaps_s">
|
||||||
<div style="font-weight: bold;"><i class="ti ti-bulb"></i> {{ i18n.ts.tip }}:</div>
|
<div style="font-weight: bold;"><i class="ti ti-bulb"></i> {{ i18n.ts.tip }}:</div>
|
||||||
<div><slot></slot></div>
|
<div><slot></slot></div>
|
||||||
<div>
|
<MkButton primary rounded small @click="closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||||
<MkButton inline primary rounded small @click="_closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
|
||||||
<button class="_button" style="padding: 8px; margin-left: 4px;" @click="showMenu"><i class="ti ti-dots"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -18,30 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
|
||||||
import { TIPS, hideAllTips, closeTip } from '@/tips.js';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
k: typeof TIPS[number];
|
k: keyof (typeof store['s']['tips']);
|
||||||
warn?: boolean;
|
warn?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
warn: false,
|
warn: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
function _closeTip() {
|
function closeTip() {
|
||||||
closeTip(props.k);
|
store.set('tips', {
|
||||||
}
|
...store.r.tips.value,
|
||||||
|
[props.k]: true,
|
||||||
function showMenu(ev: MouseEvent) {
|
});
|
||||||
os.popupMenu([{
|
|
||||||
icon: 'ti ti-bulb-off',
|
|
||||||
text: i18n.ts.hideAllTips,
|
|
||||||
danger: true,
|
|
||||||
action: () => {
|
|
||||||
hideAllTips();
|
|
||||||
os.success();
|
|
||||||
},
|
|
||||||
}], ev.currentTarget ?? ev.target);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,576 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as Misskey from 'misskey-js';
|
|
||||||
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
|
||||||
import isAnimated from 'is-file-animated';
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
|
||||||
import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef } from 'vue';
|
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
|
||||||
import { genId } from '@/utility/id.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
|
||||||
import { prefer } from '@/preferences.js';
|
|
||||||
import { isWebpSupported } from '@/utility/isWebpSupported.js';
|
|
||||||
import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
|
|
||||||
import * as os from '@/os.js';
|
|
||||||
import { ensureSignin } from '@/i.js';
|
|
||||||
import { WatermarkRenderer } from '@/utility/watermark.js';
|
|
||||||
|
|
||||||
export type UploaderFeatures = {
|
|
||||||
effect?: boolean;
|
|
||||||
watermark?: boolean;
|
|
||||||
crop?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const THUMBNAIL_SUPPORTED_TYPES = [
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/webp',
|
|
||||||
'image/svg+xml',
|
|
||||||
];
|
|
||||||
|
|
||||||
const IMAGE_COMPRESSION_SUPPORTED_TYPES = [
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/webp',
|
|
||||||
'image/svg+xml',
|
|
||||||
];
|
|
||||||
|
|
||||||
const CROPPING_SUPPORTED_TYPES = [
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/webp',
|
|
||||||
];
|
|
||||||
|
|
||||||
const IMAGE_EDITING_SUPPORTED_TYPES = [
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/webp',
|
|
||||||
];
|
|
||||||
|
|
||||||
const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES;
|
|
||||||
|
|
||||||
const IMAGE_PREPROCESS_NEEDED_TYPES = [
|
|
||||||
...WATERMARK_SUPPORTED_TYPES,
|
|
||||||
...IMAGE_COMPRESSION_SUPPORTED_TYPES,
|
|
||||||
...CROPPING_SUPPORTED_TYPES,
|
|
||||||
...IMAGE_EDITING_SUPPORTED_TYPES,
|
|
||||||
];
|
|
||||||
|
|
||||||
const mimeTypeMap = {
|
|
||||||
'image/webp': 'webp',
|
|
||||||
'image/jpeg': 'jpg',
|
|
||||||
'image/png': 'png',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type UploaderItem = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
uploadName?: string;
|
|
||||||
progress: { max: number; value: number } | null;
|
|
||||||
thumbnail: string | null;
|
|
||||||
preprocessing: boolean;
|
|
||||||
uploading: boolean;
|
|
||||||
uploaded: Misskey.entities.DriveFile | null;
|
|
||||||
uploadFailed: boolean;
|
|
||||||
aborted: boolean;
|
|
||||||
compressionLevel: 0 | 1 | 2 | 3;
|
|
||||||
compressedSize?: number | null;
|
|
||||||
preprocessedFile?: Blob | null;
|
|
||||||
file: File;
|
|
||||||
watermarkPresetId: string | null;
|
|
||||||
isSensitive?: boolean;
|
|
||||||
abort?: (() => void) | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getCompressionSettings(level: 0 | 1 | 2 | 3) {
|
|
||||||
if (level === 1) {
|
|
||||||
return {
|
|
||||||
maxWidth: 2000,
|
|
||||||
maxHeight: 2000,
|
|
||||||
};
|
|
||||||
} else if (level === 2) {
|
|
||||||
return {
|
|
||||||
maxWidth: 2000 * 0.75, // =1500
|
|
||||||
maxHeight: 2000 * 0.75, // =1500
|
|
||||||
};
|
|
||||||
} else if (level === 3) {
|
|
||||||
return {
|
|
||||||
maxWidth: 2000 * 0.75 * 0.75, // =1125
|
|
||||||
maxHeight: 2000 * 0.75 * 0.75, // =1125
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUploader(options: {
|
|
||||||
folderId?: string | null;
|
|
||||||
multiple?: boolean;
|
|
||||||
features?: UploaderFeatures;
|
|
||||||
} = {}) {
|
|
||||||
const $i = ensureSignin();
|
|
||||||
|
|
||||||
const events = new EventEmitter<{
|
|
||||||
'itemUploaded': (ctx: { item: UploaderItem; }) => void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const uploaderFeatures = computed<Required<UploaderFeatures>>(() => {
|
|
||||||
return {
|
|
||||||
effect: options.features?.effect ?? true,
|
|
||||||
watermark: options.features?.watermark ?? true,
|
|
||||||
crop: options.features?.crop ?? true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const items = ref<UploaderItem[]>([]);
|
|
||||||
|
|
||||||
function initializeFile(file: File) {
|
|
||||||
const id = genId();
|
|
||||||
const filename = file.name ?? 'untitled';
|
|
||||||
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
|
|
||||||
items.value.push({
|
|
||||||
id,
|
|
||||||
name: prefer.s.keepOriginalFilename ? filename : id + extension,
|
|
||||||
progress: null,
|
|
||||||
thumbnail: THUMBNAIL_SUPPORTED_TYPES.includes(file.type) ? window.URL.createObjectURL(file) : null,
|
|
||||||
preprocessing: false,
|
|
||||||
uploading: false,
|
|
||||||
aborted: false,
|
|
||||||
uploaded: null,
|
|
||||||
uploadFailed: false,
|
|
||||||
compressionLevel: prefer.s.defaultImageCompressionLevel,
|
|
||||||
watermarkPresetId: uploaderFeatures.value.watermark ? prefer.s.defaultWatermarkPresetId : null,
|
|
||||||
file: markRaw(file),
|
|
||||||
});
|
|
||||||
const reactiveItem = items.value.at(-1)!;
|
|
||||||
preprocess(reactiveItem).then(() => {
|
|
||||||
triggerRef(items);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addFiles(newFiles: File[]) {
|
|
||||||
for (const file of newFiles) {
|
|
||||||
initializeFile(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeItem(item: UploaderItem) {
|
|
||||||
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
|
|
||||||
items.value.splice(items.value.indexOf(item), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMenu(item: UploaderItem): MenuItem[] {
|
|
||||||
const menu: MenuItem[] = [];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!item.preprocessing &&
|
|
||||||
!item.uploading &&
|
|
||||||
!item.uploaded
|
|
||||||
) {
|
|
||||||
menu.push({
|
|
||||||
icon: 'ti ti-forms',
|
|
||||||
text: i18n.ts.rename,
|
|
||||||
action: async () => {
|
|
||||||
const { result, canceled } = await os.inputText({
|
|
||||||
type: 'text',
|
|
||||||
title: i18n.ts.rename,
|
|
||||||
placeholder: item.name,
|
|
||||||
default: item.name,
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
if (result.trim() === '') return;
|
|
||||||
|
|
||||||
item.name = result;
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
type: 'switch',
|
|
||||||
text: i18n.ts.sensitive,
|
|
||||||
icon: 'ti ti-eye-exclamation',
|
|
||||||
ref: computed({
|
|
||||||
get: () => item.isSensitive ?? false,
|
|
||||||
set: (value) => item.isSensitive = value,
|
|
||||||
}),
|
|
||||||
}, {
|
|
||||||
type: 'divider',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
uploaderFeatures.value.crop &&
|
|
||||||
CROPPING_SUPPORTED_TYPES.includes(item.file.type) &&
|
|
||||||
!item.preprocessing &&
|
|
||||||
!item.uploading &&
|
|
||||||
!item.uploaded
|
|
||||||
) {
|
|
||||||
menu.push({
|
|
||||||
icon: 'ti ti-crop',
|
|
||||||
text: i18n.ts.cropImage,
|
|
||||||
action: async () => {
|
|
||||||
const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
|
|
||||||
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
|
|
||||||
items.value.splice(items.value.indexOf(item), 1, {
|
|
||||||
...item,
|
|
||||||
file: markRaw(cropped),
|
|
||||||
thumbnail: window.URL.createObjectURL(cropped),
|
|
||||||
});
|
|
||||||
const reactiveItem = items.value.find(x => x.id === item.id)!;
|
|
||||||
preprocess(reactiveItem).then(() => {
|
|
||||||
triggerRef(items);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
uploaderFeatures.value.effect &&
|
|
||||||
IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) &&
|
|
||||||
!item.preprocessing &&
|
|
||||||
!item.uploading &&
|
|
||||||
!item.uploaded
|
|
||||||
) {
|
|
||||||
menu.push({
|
|
||||||
icon: 'ti ti-sparkles',
|
|
||||||
text: i18n.ts._imageEffector.title + ' (BETA)',
|
|
||||||
action: async () => {
|
|
||||||
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageEffectorDialog.vue').then(x => x.default), {
|
|
||||||
image: item.file,
|
|
||||||
}, {
|
|
||||||
ok: (file) => {
|
|
||||||
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
|
|
||||||
items.value.splice(items.value.indexOf(item), 1, {
|
|
||||||
...item,
|
|
||||||
file: markRaw(file),
|
|
||||||
thumbnail: window.URL.createObjectURL(file),
|
|
||||||
});
|
|
||||||
const reactiveItem = items.value.find(x => x.id === item.id)!;
|
|
||||||
preprocess(reactiveItem).then(() => {
|
|
||||||
triggerRef(items);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
closed: () => dispose(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
uploaderFeatures.value.watermark &&
|
|
||||||
WATERMARK_SUPPORTED_TYPES.includes(item.file.type) &&
|
|
||||||
!item.preprocessing &&
|
|
||||||
!item.uploading &&
|
|
||||||
!item.uploaded
|
|
||||||
) {
|
|
||||||
function changeWatermarkPreset(presetId: string | null) {
|
|
||||||
item.watermarkPresetId = presetId;
|
|
||||||
preprocess(item).then(() => {
|
|
||||||
triggerRef(items);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.push({
|
|
||||||
icon: 'ti ti-copyright',
|
|
||||||
text: i18n.ts.watermark,
|
|
||||||
caption: computed(() => item.watermarkPresetId == null ? null : prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId)?.name),
|
|
||||||
type: 'parent',
|
|
||||||
children: [{
|
|
||||||
type: 'radioOption',
|
|
||||||
text: i18n.ts.none,
|
|
||||||
active: computed(() => item.watermarkPresetId == null),
|
|
||||||
action: () => changeWatermarkPreset(null),
|
|
||||||
}, {
|
|
||||||
type: 'divider',
|
|
||||||
}, ...prefer.s.watermarkPresets.map(preset => ({
|
|
||||||
type: 'radioOption' as const,
|
|
||||||
text: preset.name,
|
|
||||||
active: computed(() => item.watermarkPresetId === preset.id),
|
|
||||||
action: () => changeWatermarkPreset(preset.id),
|
|
||||||
})), ...(prefer.s.watermarkPresets.length > 0 ? [{
|
|
||||||
type: 'divider' as const,
|
|
||||||
}] : []), {
|
|
||||||
type: 'button',
|
|
||||||
icon: 'ti ti-plus',
|
|
||||||
text: i18n.ts.add,
|
|
||||||
action: async () => {
|
|
||||||
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkWatermarkEditorDialog.vue').then(x => x.default), {
|
|
||||||
image: item.file,
|
|
||||||
}, {
|
|
||||||
ok: (preset) => {
|
|
||||||
prefer.commit('watermarkPresets', [...prefer.s.watermarkPresets, preset]);
|
|
||||||
changeWatermarkPreset(preset.id);
|
|
||||||
},
|
|
||||||
closed: () => dispose(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) &&
|
|
||||||
!item.preprocessing &&
|
|
||||||
!item.uploading &&
|
|
||||||
!item.uploaded
|
|
||||||
) {
|
|
||||||
function changeCompressionLevel(level: 0 | 1 | 2 | 3) {
|
|
||||||
item.compressionLevel = level;
|
|
||||||
preprocess(item).then(() => {
|
|
||||||
triggerRef(items);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.push({
|
|
||||||
icon: 'ti ti-leaf',
|
|
||||||
text: computed(() => {
|
|
||||||
let text = i18n.ts.compress;
|
|
||||||
|
|
||||||
if (item.compressionLevel === 0 || item.compressionLevel == null) {
|
|
||||||
text += `: ${i18n.ts.none}`;
|
|
||||||
} else if (item.compressionLevel === 1) {
|
|
||||||
text += `: ${i18n.ts.low}`;
|
|
||||||
} else if (item.compressionLevel === 2) {
|
|
||||||
text += `: ${i18n.ts.medium}`;
|
|
||||||
} else if (item.compressionLevel === 3) {
|
|
||||||
text += `: ${i18n.ts.high}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}),
|
|
||||||
type: 'parent',
|
|
||||||
children: [{
|
|
||||||
type: 'radioOption',
|
|
||||||
text: i18n.ts.none,
|
|
||||||
active: computed(() => item.compressionLevel === 0 || item.compressionLevel == null),
|
|
||||||
action: () => changeCompressionLevel(0),
|
|
||||||
}, {
|
|
||||||
type: 'divider',
|
|
||||||
}, {
|
|
||||||
type: 'radioOption',
|
|
||||||
text: i18n.ts.low,
|
|
||||||
active: computed(() => item.compressionLevel === 1),
|
|
||||||
action: () => changeCompressionLevel(1),
|
|
||||||
}, {
|
|
||||||
type: 'radioOption',
|
|
||||||
text: i18n.ts.medium,
|
|
||||||
active: computed(() => item.compressionLevel === 2),
|
|
||||||
action: () => changeCompressionLevel(2),
|
|
||||||
}, {
|
|
||||||
type: 'radioOption',
|
|
||||||
text: i18n.ts.high,
|
|
||||||
active: computed(() => item.compressionLevel === 3),
|
|
||||||
action: () => changeCompressionLevel(3),
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.preprocessing && !item.uploading && !item.uploaded) {
|
|
||||||
menu.push({
|
|
||||||
type: 'divider',
|
|
||||||
}, {
|
|
||||||
icon: 'ti ti-upload',
|
|
||||||
text: i18n.ts.upload,
|
|
||||||
action: () => {
|
|
||||||
uploadOne(item);
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
icon: 'ti ti-x',
|
|
||||||
text: i18n.ts.remove,
|
|
||||||
danger: true,
|
|
||||||
action: () => {
|
|
||||||
removeItem(item);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (item.uploading) {
|
|
||||||
menu.push({
|
|
||||||
type: 'divider',
|
|
||||||
}, {
|
|
||||||
icon: 'ti ti-cloud-pause',
|
|
||||||
text: i18n.ts.abort,
|
|
||||||
danger: true,
|
|
||||||
action: () => {
|
|
||||||
if (item.abort != null) {
|
|
||||||
item.abort();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadOne(item: UploaderItem): Promise<void> {
|
|
||||||
item.uploadFailed = false;
|
|
||||||
item.uploading = true;
|
|
||||||
|
|
||||||
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
|
|
||||||
name: item.uploadName ?? item.name,
|
|
||||||
folderId: options.folderId,
|
|
||||||
isSensitive: item.isSensitive ?? false,
|
|
||||||
onProgress: (progress) => {
|
|
||||||
if (item.progress == null) {
|
|
||||||
item.progress = { max: progress.total, value: progress.loaded };
|
|
||||||
} else {
|
|
||||||
item.progress.value = progress.loaded;
|
|
||||||
item.progress.max = progress.total;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
item.abort = () => {
|
|
||||||
item.abort = null;
|
|
||||||
abort();
|
|
||||||
item.uploading = false;
|
|
||||||
item.uploadFailed = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
await filePromise.then((file) => {
|
|
||||||
item.uploaded = file;
|
|
||||||
item.abort = null;
|
|
||||||
events.emit('itemUploaded', { item });
|
|
||||||
}).catch(err => {
|
|
||||||
item.uploadFailed = true;
|
|
||||||
item.progress = null;
|
|
||||||
if (!(err instanceof UploadAbortedError)) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
item.uploading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
|
|
||||||
items.value = items.value.map(item => ({
|
|
||||||
...item,
|
|
||||||
aborted: false,
|
|
||||||
uploadFailed: false,
|
|
||||||
uploading: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
for (const item of items.value.filter(item => item.uploaded == null)) {
|
|
||||||
// アップロード処理途中で値が変わる場合(途中で全キャンセルされたりなど)もあるので、Array filterではなくここでチェック
|
|
||||||
if (item.aborted) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploadOne(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function abortAll() {
|
|
||||||
for (const item of items.value) {
|
|
||||||
if (item.uploaded != null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.abort != null) {
|
|
||||||
item.abort();
|
|
||||||
}
|
|
||||||
item.aborted = true;
|
|
||||||
item.uploadFailed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function preprocess(item: UploaderItem): Promise<void> {
|
|
||||||
item.preprocessing = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
|
|
||||||
await preprocessForImage(item);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to preprocess image', err);
|
|
||||||
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
|
|
||||||
item.preprocessing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function preprocessForImage(item: UploaderItem): Promise<void> {
|
|
||||||
const imageBitmap = await window.createImageBitmap(item.file);
|
|
||||||
|
|
||||||
let preprocessedFile: Blob | File = item.file;
|
|
||||||
|
|
||||||
const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(preprocessedFile.type);
|
|
||||||
const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId);
|
|
||||||
if (needsWatermark && preset != null) {
|
|
||||||
const canvas = window.document.createElement('canvas');
|
|
||||||
const renderer = new WatermarkRenderer({
|
|
||||||
canvas: canvas,
|
|
||||||
renderWidth: imageBitmap.width,
|
|
||||||
renderHeight: imageBitmap.height,
|
|
||||||
image: imageBitmap,
|
|
||||||
});
|
|
||||||
|
|
||||||
await renderer.setLayers(preset.layers);
|
|
||||||
|
|
||||||
renderer.render();
|
|
||||||
|
|
||||||
preprocessedFile = await new Promise<Blob>((resolve) => {
|
|
||||||
canvas.toBlob((blob) => {
|
|
||||||
if (blob == null) {
|
|
||||||
throw new Error('Failed to convert canvas to blob');
|
|
||||||
}
|
|
||||||
resolve(blob);
|
|
||||||
renderer.destroy();
|
|
||||||
}, 'image/png');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const compressionSettings = getCompressionSettings(item.compressionLevel);
|
|
||||||
const needsCompress = item.compressionLevel !== 0 && compressionSettings && IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(preprocessedFile.type) && !(await isAnimated(preprocessedFile));
|
|
||||||
|
|
||||||
if (needsCompress) {
|
|
||||||
const config = {
|
|
||||||
mimeType: isWebpSupported() ? 'image/webp' : 'image/jpeg',
|
|
||||||
maxWidth: compressionSettings.maxWidth,
|
|
||||||
maxHeight: compressionSettings.maxHeight,
|
|
||||||
quality: isWebpSupported() ? 0.85 : 0.8,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await readAndCompressImage(preprocessedFile, config);
|
|
||||||
if (result.size < preprocessedFile.size || preprocessedFile.type === 'image/webp') {
|
|
||||||
// The compression may not always reduce the file size
|
|
||||||
// (and WebP is not browser safe yet)
|
|
||||||
preprocessedFile = result;
|
|
||||||
item.compressedSize = result.size;
|
|
||||||
item.uploadName = preprocessedFile.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to resize image', err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
item.compressedSize = null;
|
|
||||||
item.uploadName = item.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
imageBitmap.close();
|
|
||||||
|
|
||||||
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
|
|
||||||
item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null;
|
|
||||||
item.preprocessedFile = markRaw(preprocessedFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
for (const item of items.value) {
|
|
||||||
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
items,
|
|
||||||
addFiles,
|
|
||||||
removeItem,
|
|
||||||
abortAll,
|
|
||||||
upload,
|
|
||||||
getMenu,
|
|
||||||
uploading: computed(() => items.value.some(item => item.uploading)),
|
|
||||||
readyForUpload: computed(() => items.value.length > 0 && items.value.some(item => item.uploaded == null) && !items.value.some(item => item.uploading || item.preprocessing)),
|
|
||||||
allItemsUploaded: computed(() => items.value.every(item => item.uploaded != null)),
|
|
||||||
events,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -161,7 +161,7 @@ import { prefer } from '@/preferences.js';
|
||||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||||
import { signout } from '@/signout.js';
|
import { signout } from '@/signout.js';
|
||||||
import { migrateOldSettings } from '@/pref-migrate.js';
|
import { migrateOldSettings } from '@/pref-migrate.js';
|
||||||
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
|
import { store, TIPS } from '@/store.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
@ -205,12 +205,16 @@ function migrate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetAllTips() {
|
function resetAllTips() {
|
||||||
_resetAllTips();
|
store.set('tips', {});
|
||||||
os.success();
|
os.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideAllTips() {
|
function hideAllTips() {
|
||||||
_hideAllTips();
|
const v = {};
|
||||||
|
for (const k of TIPS) {
|
||||||
|
v[k] = true;
|
||||||
|
}
|
||||||
|
store.set('tips', v);
|
||||||
os.success();
|
os.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -226,7 +226,6 @@ const headerActions = computed(() => {
|
||||||
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
icon: 'ti ti-repeat',
|
|
||||||
text: i18n.ts.showRenotes,
|
text: i18n.ts.showRenotes,
|
||||||
ref: withRenotes,
|
ref: withRenotes,
|
||||||
});
|
});
|
||||||
|
@ -234,7 +233,6 @@ const headerActions = computed(() => {
|
||||||
if (isBasicTimeline(src.value) && hasWithReplies(src.value)) {
|
if (isBasicTimeline(src.value) && hasWithReplies(src.value)) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
icon: 'ti ti-messages',
|
|
||||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||||
ref: withReplies,
|
ref: withReplies,
|
||||||
disabled: onlyFiles,
|
disabled: onlyFiles,
|
||||||
|
@ -243,12 +241,10 @@ const headerActions = computed(() => {
|
||||||
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
icon: 'ti ti-eye-exclamation',
|
|
||||||
text: i18n.ts.withSensitive,
|
text: i18n.ts.withSensitive,
|
||||||
ref: withSensitive,
|
ref: withSensitive,
|
||||||
}, {
|
}, {
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
icon: 'ti ti-photo',
|
|
||||||
text: i18n.ts.fileAttachedOnly,
|
text: i18n.ts.fileAttachedOnly,
|
||||||
ref: onlyFiles,
|
ref: onlyFiles,
|
||||||
disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,
|
disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,
|
||||||
|
|
|
@ -81,6 +81,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="step === 1" class="_gaps_m">
|
<div v-else-if="step === 1" class="_gaps_m">
|
||||||
|
<div style="text-align: center;" class="_gaps_s">
|
||||||
|
<div><b>{{ i18n.ts._serverSetupWizard.donationRequest }}</b></div>
|
||||||
|
<div>{{ i18n.ts._serverSetupWizard._donationRequest.text1 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text2 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text3 }}</div>
|
||||||
|
</div>
|
||||||
|
<MkLink target="_blank" url="https://misskey-hub.net/docs/donate/" style="margin: 0 auto;">{{ i18n.ts.learnMore }}</MkLink>
|
||||||
|
<div class="_buttonsCenter">
|
||||||
|
<MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="step++">
|
||||||
|
{{ i18n.ts.next }}
|
||||||
|
</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="step === 2" class="_gaps_m">
|
||||||
<div style="text-align: center;" class="_gaps_s">
|
<div style="text-align: center;" class="_gaps_s">
|
||||||
<div style="font-size: 120%;"><b>{{ i18n.ts._serverSetupWizard.serverSetting }}</b></div>
|
<div style="font-size: 120%;"><b>{{ i18n.ts._serverSetupWizard.serverSetting }}</b></div>
|
||||||
<div>{{ i18n.ts._serverSetupWizard.youCanEasilyConfigureOptimalServerSettingsWithThisWizard }}</div>
|
<div>{{ i18n.ts._serverSetupWizard.youCanEasilyConfigureOptimalServerSettingsWithThisWizard }}</div>
|
||||||
|
@ -93,17 +105,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
{{ i18n.ts._serverSetupWizard.skipSettings }}
|
{{ i18n.ts._serverSetupWizard.skipSettings }}
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="step === 2" class="_gaps_m">
|
<div v-else-if="step === 3" class="_gaps_m">
|
||||||
<div style="text-align: center;" class="_gaps_s">
|
<div style="text-align: center;" class="_gaps_s">
|
||||||
<div><b>{{ i18n.ts._serverSetupWizard.settingsCompleted }}</b></div>
|
<div><b>{{ i18n.ts._serverSetupWizard.settingsCompleted }}</b></div>
|
||||||
<div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description }}</div>
|
<div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description }}</div>
|
||||||
<div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description2 }}</div>
|
<div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description2 }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="_gaps_s" :class="$style.donation">
|
|
||||||
<div><b>{{ i18n.ts._serverSetupWizard.donationRequest }}</b></div>
|
|
||||||
<div>{{ i18n.ts._serverSetupWizard._donationRequest.text1 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text2 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text3 }}</div>
|
|
||||||
<MkLink target="_blank" url="https://misskey-hub.net/docs/donate/" style="margin: 0 auto;">{{ i18n.ts.learnMore }}</MkLink>
|
|
||||||
</div>
|
|
||||||
<div class="_buttonsCenter">
|
<div class="_buttonsCenter">
|
||||||
<MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="finish">
|
<MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="finish">
|
||||||
{{ i18n.ts.start }}
|
{{ i18n.ts.start }}
|
||||||
|
@ -225,11 +232,4 @@ function finish() {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donation {
|
|
||||||
background: var(--MI_THEME-accentedBg);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,11 +10,22 @@ import darkTheme from '@@/themes/d-green-lime.json5';
|
||||||
import { hemisphere } from '@@/js/intl-const.js';
|
import { hemisphere } from '@@/js/intl-const.js';
|
||||||
import type { DeviceKind } from '@/utility/device-kind.js';
|
import type { DeviceKind } from '@/utility/device-kind.js';
|
||||||
import type { Plugin } from '@/plugin.js';
|
import type { Plugin } from '@/plugin.js';
|
||||||
import type { TIPS } from '@/tips.js';
|
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { Pizzax } from '@/lib/pizzax.js';
|
import { Pizzax } from '@/lib/pizzax.js';
|
||||||
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
|
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
|
||||||
|
|
||||||
|
export const TIPS = [
|
||||||
|
'drive',
|
||||||
|
'uploader',
|
||||||
|
'clips',
|
||||||
|
'userLists',
|
||||||
|
'tl.home',
|
||||||
|
'tl.local',
|
||||||
|
'tl.social',
|
||||||
|
'tl.global',
|
||||||
|
'abuses',
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「状態」を管理するストア(not「設定」)
|
* 「状態」を管理するストア(not「設定」)
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { store } from '@/store.js';
|
|
||||||
|
|
||||||
export const TIPS = [
|
|
||||||
'drive',
|
|
||||||
'uploader',
|
|
||||||
'postFormUploader',
|
|
||||||
'clips',
|
|
||||||
'userLists',
|
|
||||||
'tl.home',
|
|
||||||
'tl.local',
|
|
||||||
'tl.social',
|
|
||||||
'tl.global',
|
|
||||||
'abuses',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export function closeTip(tip: typeof TIPS[number]) {
|
|
||||||
store.set('tips', {
|
|
||||||
...store.r.tips.value,
|
|
||||||
[tip]: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetAllTips() {
|
|
||||||
store.set('tips', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hideAllTips() {
|
|
||||||
const v = {};
|
|
||||||
for (const k of TIPS) {
|
|
||||||
v[k] = true;
|
|
||||||
}
|
|
||||||
store.set('tips', v);
|
|
||||||
}
|
|
|
@ -11,22 +11,20 @@ type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<
|
||||||
|
|
||||||
type MenuRadioOptionsDef = Record<string, any>;
|
type MenuRadioOptionsDef = Record<string, any>;
|
||||||
|
|
||||||
type Text = string | ComputedRef<string>;
|
|
||||||
|
|
||||||
export type MenuAction = (ev: MouseEvent) => void;
|
export type MenuAction = (ev: MouseEvent) => void;
|
||||||
|
|
||||||
export type MenuDivider = { type: 'divider' };
|
export type MenuDivider = { type: 'divider' };
|
||||||
export type MenuNull = undefined;
|
export type MenuNull = undefined;
|
||||||
export type MenuLabel = { type: 'label', text: Text, caption?: Text };
|
export type MenuLabel = { type: 'label', text: string, caption?: string };
|
||||||
export type MenuLink = { type: 'link', to: string, text: Text, caption?: Text, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
|
export type MenuLink = { type: 'link', to: string, text: string, caption?: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
|
||||||
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: Text, caption?: Text, icon?: string, indicate?: boolean };
|
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, caption?: string, icon?: string, indicate?: boolean };
|
||||||
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
|
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
|
||||||
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: Text, caption?: Text, icon?: string, disabled?: boolean | Ref<boolean> };
|
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, caption?: string, icon?: string, disabled?: boolean | Ref<boolean> };
|
||||||
export type MenuButton = { type?: 'button', text: Text, caption?: Text, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
|
export type MenuButton = { type?: 'button', text: string, caption?: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
|
||||||
export type MenuRadio = { type: 'radio', text: Text, caption?: Text, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
|
export type MenuRadio = { type: 'radio', text: string, caption?: string, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
|
||||||
export type MenuRadioOption = { type: 'radioOption', text: Text, caption?: Text, action: MenuAction; active?: boolean | ComputedRef<boolean> };
|
export type MenuRadioOption = { type: 'radioOption', text: string, caption?: string, action: MenuAction; active?: boolean | ComputedRef<boolean> };
|
||||||
export type MenuComponent<T extends Component = any> = { type: 'component', component: T, props?: ComponentProps<T> };
|
export type MenuComponent<T extends Component = any> = { type: 'component', component: T, props?: ComponentProps<T> };
|
||||||
export type MenuParent = { type: 'parent', text: Text, caption?: Text, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
|
export type MenuParent = { type: 'parent', text: string, caption?: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
|
||||||
|
|
||||||
export type MenuPending = { type: 'pending' };
|
export type MenuPending = { type: 'pending' };
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { apiUrl } from '@@/js/config.js';
|
import { apiUrl } from '@@/js/config.js';
|
||||||
import type { UploaderFeatures } from '@/composables/use-uploader.js';
|
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
|
@ -17,6 +16,7 @@ import { instance } from '@/instance.js';
|
||||||
import { globalEvents } from '@/events.js';
|
import { globalEvents } from '@/events.js';
|
||||||
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
|
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
|
import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue';
|
||||||
|
|
||||||
type UploadReturnType = {
|
type UploadReturnType = {
|
||||||
filePromise: Promise<Misskey.entities.DriveFile>;
|
filePromise: Promise<Misskey.entities.DriveFile>;
|
||||||
|
@ -32,7 +32,6 @@ export class UploadAbortedError extends Error {
|
||||||
export function uploadFile(file: File | Blob, options: {
|
export function uploadFile(file: File | Blob, options: {
|
||||||
name?: string;
|
name?: string;
|
||||||
folderId?: string | null;
|
folderId?: string | null;
|
||||||
isSensitive?: boolean;
|
|
||||||
onProgress?: (ctx: { total: number; loaded: number; }) => void;
|
onProgress?: (ctx: { total: number; loaded: number; }) => void;
|
||||||
} = {}): UploadReturnType {
|
} = {}): UploadReturnType {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
@ -141,7 +140,6 @@ export function uploadFile(file: File | Blob, options: {
|
||||||
formData.append('force', 'true');
|
formData.append('force', 'true');
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('name', options.name ?? (file instanceof File ? file.name : 'untitled'));
|
formData.append('name', options.name ?? (file instanceof File ? file.name : 'untitled'));
|
||||||
formData.append('isSensitive', options.isSensitive ? 'true' : 'false');
|
|
||||||
if (options.folderId) formData.append('folderId', options.folderId);
|
if (options.folderId) formData.append('folderId', options.folderId);
|
||||||
|
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
|
@ -158,7 +156,7 @@ export function uploadFile(file: File | Blob, options: {
|
||||||
export function chooseFileFromPcAndUpload(
|
export function chooseFileFromPcAndUpload(
|
||||||
options: {
|
options: {
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
features?: UploaderFeatures;
|
features?: UploaderDialogFeatures;
|
||||||
folderId?: string | null;
|
folderId?: string | null;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<Misskey.entities.DriveFile[]> {
|
): Promise<Misskey.entities.DriveFile[]> {
|
||||||
|
@ -256,7 +254,7 @@ type SelectFileOptions<M extends boolean> = {
|
||||||
|
|
||||||
export async function selectFile<
|
export async function selectFile<
|
||||||
M extends boolean,
|
M extends boolean,
|
||||||
MR extends M extends true ? Misskey.entities.DriveFile[] : Misskey.entities.DriveFile,
|
MR extends M extends true ? Misskey.entities.DriveFile[] : Misskey.entities.DriveFile
|
||||||
>(opts: SelectFileOptions<M>): Promise<MR> {
|
>(opts: SelectFileOptions<M>): Promise<MR> {
|
||||||
const files = await select(opts.anchorElement, opts.label ?? null, opts.multiple ?? false, opts.features);
|
const files = await select(opts.anchorElement, opts.label ?? null, opts.multiple ?? false, opts.features);
|
||||||
return opts.multiple ? (files as MR) : (files[0]! as MR);
|
return opts.multiple ? (files as MR) : (files[0]! as MR);
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getProxiedImageUrl } from '../media-proxy.js';
|
import { getProxiedImageUrl } from '../media-proxy.js';
|
||||||
import { initShaderProgram } from '../webgl.js';
|
|
||||||
|
|
||||||
type ParamTypeToPrimitive = {
|
type ParamTypeToPrimitive = {
|
||||||
'number': number;
|
'number': number;
|
||||||
|
@ -61,6 +60,8 @@ function getValue<T extends keyof ParamTypeToPrimitive>(params: Record<string, a
|
||||||
export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, any>>> {
|
export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, any>>> {
|
||||||
private gl: WebGL2RenderingContext;
|
private gl: WebGL2RenderingContext;
|
||||||
private canvas: HTMLCanvasElement | null = null;
|
private canvas: HTMLCanvasElement | null = null;
|
||||||
|
private renderTextureProgram: WebGLProgram;
|
||||||
|
private renderInvertedTextureProgram: WebGLProgram;
|
||||||
private renderWidth: number;
|
private renderWidth: number;
|
||||||
private renderHeight: number;
|
private renderHeight: number;
|
||||||
private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
|
private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
|
||||||
|
@ -69,7 +70,6 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
|
||||||
private shaderCache: Map<string, WebGLProgram> = new Map();
|
private shaderCache: Map<string, WebGLProgram> = new Map();
|
||||||
private perLayerResultTextures: Map<string, WebGLTexture> = new Map();
|
private perLayerResultTextures: Map<string, WebGLTexture> = new Map();
|
||||||
private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map();
|
private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map();
|
||||||
private nopProgram: WebGLProgram;
|
|
||||||
private fxs: [...IEX];
|
private fxs: [...IEX];
|
||||||
private paramTextures: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map();
|
private paramTextures: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map();
|
||||||
|
|
||||||
|
@ -114,13 +114,13 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.originalImage.width, this.originalImage.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.originalImage);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.originalImage.width, this.originalImage.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.originalImage);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||||
|
|
||||||
this.nopProgram = initShaderProgram(this.gl, `#version 300 es
|
this.renderTextureProgram = this.initShaderProgram(`#version 300 es
|
||||||
in vec2 position;
|
in vec2 position;
|
||||||
out vec2 in_uv;
|
out vec2 in_uv;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
in_uv = (position + 1.0) / 2.0;
|
in_uv = (position + 1.0) / 2.0;
|
||||||
gl_Position = vec4(position * vec2(1.0, -1.0), 0.0, 1.0);
|
gl_Position = vec4(position, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
`, `#version 300 es
|
`, `#version 300 es
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
|
@ -134,28 +134,82 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// レジスタ番号はシェーダープログラムに属しているわけではなく、独立の存在なので、とりあえず nopProgram を使って設定する(その後は効果が持続する)
|
this.renderInvertedTextureProgram = this.initShaderProgram(`#version 300 es
|
||||||
// ref. https://qiita.com/emadurandal/items/5966c8374f03d4de3266
|
in vec2 position;
|
||||||
const positionLocation = gl.getAttribLocation(this.nopProgram, 'position');
|
out vec2 in_uv;
|
||||||
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
||||||
gl.enableVertexAttribArray(positionLocation);
|
void main() {
|
||||||
|
in_uv = (position + 1.0) / 2.0;
|
||||||
|
in_uv.y = 1.0 - in_uv.y;
|
||||||
|
gl_Position = vec4(position, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
`, `#version 300 es
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
in vec2 in_uv;
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
out vec4 out_color;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
out_color = texture(u_texture, in_uv);
|
||||||
|
}
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderLayer(layer: ImageEffectorLayer, preTexture: WebGLTexture, invert = false) {
|
public loadShader(type: GLenum, source: string): WebGLShader {
|
||||||
|
const gl = this.gl;
|
||||||
|
|
||||||
|
const shader = gl.createShader(type);
|
||||||
|
if (shader == null) {
|
||||||
|
throw new Error('falied to create shader');
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.shaderSource(shader, source);
|
||||||
|
gl.compileShader(shader);
|
||||||
|
|
||||||
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
|
console.error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
|
||||||
|
gl.deleteShader(shader);
|
||||||
|
throw new Error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public initShaderProgram(vsSource: string, fsSource: string): WebGLProgram {
|
||||||
|
const gl = this.gl;
|
||||||
|
|
||||||
|
const vertexShader = this.loadShader(gl.VERTEX_SHADER, vsSource);
|
||||||
|
const fragmentShader = this.loadShader(gl.FRAGMENT_SHADER, fsSource);
|
||||||
|
|
||||||
|
const shaderProgram = gl.createProgram();
|
||||||
|
|
||||||
|
gl.attachShader(shaderProgram, vertexShader);
|
||||||
|
gl.attachShader(shaderProgram, fragmentShader);
|
||||||
|
gl.linkProgram(shaderProgram);
|
||||||
|
|
||||||
|
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
||||||
|
console.error(`failed to init shader: ${gl.getProgramInfoLog(shaderProgram)}`);
|
||||||
|
throw new Error('failed to init shader');
|
||||||
|
}
|
||||||
|
|
||||||
|
return shaderProgram;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLayer(layer: ImageEffectorLayer, preTexture: WebGLTexture) {
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
|
|
||||||
const fx = this.fxs.find(fx => fx.id === layer.fxId);
|
const fx = this.fxs.find(fx => fx.id === layer.fxId);
|
||||||
if (fx == null) return;
|
if (fx == null) return;
|
||||||
|
|
||||||
const cachedShader = this.shaderCache.get(fx.id);
|
const cachedShader = this.shaderCache.get(fx.id);
|
||||||
const shaderProgram = cachedShader ?? initShaderProgram(this.gl, `#version 300 es
|
const shaderProgram = cachedShader ?? this.initShaderProgram(`#version 300 es
|
||||||
in vec2 position;
|
in vec2 position;
|
||||||
uniform bool u_invert;
|
|
||||||
out vec2 in_uv;
|
out vec2 in_uv;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
in_uv = (position + 1.0) / 2.0;
|
in_uv = (position + 1.0) / 2.0;
|
||||||
gl_Position = u_invert ? vec4(position * vec2(1.0, -1.0), 0.0, 1.0) : vec4(position, 0.0, 1.0);
|
gl_Position = vec4(position, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
`, fx.shader);
|
`, fx.shader);
|
||||||
if (cachedShader == null) {
|
if (cachedShader == null) {
|
||||||
|
@ -167,9 +221,6 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
|
||||||
const in_resolution = gl.getUniformLocation(shaderProgram, 'in_resolution');
|
const in_resolution = gl.getUniformLocation(shaderProgram, 'in_resolution');
|
||||||
gl.uniform2fv(in_resolution, [this.renderWidth, this.renderHeight]);
|
gl.uniform2fv(in_resolution, [this.renderWidth, this.renderHeight]);
|
||||||
|
|
||||||
const u_invert = gl.getUniformLocation(shaderProgram, 'u_invert');
|
|
||||||
gl.uniform1i(u_invert, invert ? 1 : 0);
|
|
||||||
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, preTexture);
|
gl.bindTexture(gl.TEXTURE_2D, preTexture);
|
||||||
const in_texture = gl.getUniformLocation(shaderProgram, 'in_texture');
|
const in_texture = gl.getUniformLocation(shaderProgram, 'in_texture');
|
||||||
|
@ -202,23 +253,27 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
|
||||||
public render() {
|
public render() {
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
|
|
||||||
// 入力をそのまま出力
|
{
|
||||||
if (this.layers.length === 0) {
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture);
|
gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture);
|
||||||
|
|
||||||
gl.useProgram(this.nopProgram);
|
gl.useProgram(this.renderTextureProgram);
|
||||||
gl.uniform1i(gl.getUniformLocation(this.nopProgram, 'u_texture')!, 0);
|
const u_texture = gl.getUniformLocation(this.renderTextureProgram, 'u_texture');
|
||||||
|
gl.uniform1i(u_texture, 0);
|
||||||
|
const u_resolution = gl.getUniformLocation(this.renderTextureProgram, 'u_resolution');
|
||||||
|
gl.uniform2fv(u_resolution, [this.renderWidth, this.renderHeight]);
|
||||||
|
const positionLocation = gl.getAttribLocation(this.renderTextureProgram, 'position');
|
||||||
|
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
gl.enableVertexAttribArray(positionLocation);
|
||||||
|
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
|
||||||
let preTexture = this.originalImageTexture;
|
let preTexture = this.originalImageTexture;
|
||||||
|
|
||||||
for (const layer of this.layers) {
|
for (const layer of this.layers) {
|
||||||
const isLast = layer === this.layers.at(-1);
|
|
||||||
|
|
||||||
const cachedResultTexture = this.perLayerResultTextures.get(layer.id);
|
const cachedResultTexture = this.perLayerResultTextures.get(layer.id);
|
||||||
const resultTexture = cachedResultTexture ?? createTexture(gl);
|
const resultTexture = cachedResultTexture ?? createTexture(gl);
|
||||||
if (cachedResultTexture == null) {
|
if (cachedResultTexture == null) {
|
||||||
|
@ -228,22 +283,30 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.renderWidth, this.renderHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.renderWidth, this.renderHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||||
|
|
||||||
if (isLast) {
|
const cachedResultFrameBuffer = this.perLayerResultFrameBuffers.get(layer.id);
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
const resultFrameBuffer = cachedResultFrameBuffer ?? gl.createFramebuffer()!;
|
||||||
} else {
|
if (cachedResultFrameBuffer == null) {
|
||||||
const cachedResultFrameBuffer = this.perLayerResultFrameBuffers.get(layer.id);
|
this.perLayerResultFrameBuffers.set(layer.id, resultFrameBuffer);
|
||||||
const resultFrameBuffer = cachedResultFrameBuffer ?? gl.createFramebuffer()!;
|
|
||||||
if (cachedResultFrameBuffer == null) {
|
|
||||||
this.perLayerResultFrameBuffers.set(layer.id, resultFrameBuffer);
|
|
||||||
}
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, resultFrameBuffer);
|
|
||||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, resultTexture, 0);
|
|
||||||
}
|
}
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, resultFrameBuffer);
|
||||||
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, resultTexture, 0);
|
||||||
|
|
||||||
this.renderLayer(layer, preTexture, isLast);
|
this.renderLayer(layer, preTexture);
|
||||||
|
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
|
||||||
preTexture = resultTexture;
|
preTexture = resultTexture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
|
||||||
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
gl.useProgram(this.renderInvertedTextureProgram);
|
||||||
|
|
||||||
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, preTexture);
|
||||||
|
|
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setLayers(layers: ImageEffectorLayer[]) {
|
public async setLayers(layers: ImageEffectorLayer[]) {
|
||||||
|
@ -303,8 +366,6 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
|
||||||
* disposeCanvas = true だとloseContextを呼ぶため、コンストラクタで渡されたcanvasも再利用不可になるので注意
|
* disposeCanvas = true だとloseContextを呼ぶため、コンストラクタで渡されたcanvasも再利用不可になるので注意
|
||||||
*/
|
*/
|
||||||
public destroy(disposeCanvas = true) {
|
public destroy(disposeCanvas = true) {
|
||||||
this.gl.deleteProgram(this.nopProgram);
|
|
||||||
|
|
||||||
for (const shader of this.shaderCache.values()) {
|
for (const shader of this.shaderCache.values()) {
|
||||||
this.gl.deleteProgram(shader);
|
this.gl.deleteProgram(shader);
|
||||||
}
|
}
|
||||||
|
@ -325,6 +386,8 @@ export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, a
|
||||||
}
|
}
|
||||||
this.paramTextures.clear();
|
this.paramTextures.clear();
|
||||||
|
|
||||||
|
this.gl.deleteProgram(this.renderTextureProgram);
|
||||||
|
this.gl.deleteProgram(this.renderInvertedTextureProgram);
|
||||||
this.gl.deleteTexture(this.originalImageTexture);
|
this.gl.deleteTexture(this.originalImageTexture);
|
||||||
|
|
||||||
if (disposeCanvas) {
|
if (disposeCanvas) {
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import { FX_checker } from './fxs/checker.js';
|
import { FX_checker } from './fxs/checker.js';
|
||||||
import { FX_chromaticAberration } from './fxs/chromaticAberration.js';
|
import { FX_chromaticAberration } from './fxs/chromaticAberration.js';
|
||||||
import { FX_colorAdjust } from './fxs/colorAdjust.js';
|
|
||||||
import { FX_colorClamp } from './fxs/colorClamp.js';
|
import { FX_colorClamp } from './fxs/colorClamp.js';
|
||||||
import { FX_colorClampAdvanced } from './fxs/colorClampAdvanced.js';
|
import { FX_colorClampAdvanced } from './fxs/colorClampAdvanced.js';
|
||||||
import { FX_distort } from './fxs/distort.js';
|
import { FX_distort } from './fxs/distort.js';
|
||||||
|
@ -27,7 +26,6 @@ export const FXS = [
|
||||||
FX_mirror,
|
FX_mirror,
|
||||||
FX_invert,
|
FX_invert,
|
||||||
FX_grayscale,
|
FX_grayscale,
|
||||||
FX_colorAdjust,
|
|
||||||
FX_colorClamp,
|
FX_colorClamp,
|
||||||
FX_colorClampAdvanced,
|
FX_colorClampAdvanced,
|
||||||
FX_distort,
|
FX_distort,
|
||||||
|
|
|
@ -1,136 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { defineImageEffectorFx } from '../ImageEffector.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
|
||||||
|
|
||||||
const shader = `#version 300 es
|
|
||||||
precision mediump float;
|
|
||||||
|
|
||||||
in vec2 in_uv;
|
|
||||||
uniform sampler2D in_texture;
|
|
||||||
uniform vec2 in_resolution;
|
|
||||||
uniform float u_brightness;
|
|
||||||
uniform float u_contrast;
|
|
||||||
uniform float u_hue;
|
|
||||||
uniform float u_lightness;
|
|
||||||
uniform float u_saturation;
|
|
||||||
out vec4 out_color;
|
|
||||||
|
|
||||||
// RGB to HSL
|
|
||||||
vec3 rgb2hsl(vec3 c) {
|
|
||||||
float maxc = max(max(c.r, c.g), c.b);
|
|
||||||
float minc = min(min(c.r, c.g), c.b);
|
|
||||||
float l = (maxc + minc) * 0.5;
|
|
||||||
float s = 0.0;
|
|
||||||
float h = 0.0;
|
|
||||||
if (maxc != minc) {
|
|
||||||
float d = maxc - minc;
|
|
||||||
s = l > 0.5 ? d / (2.0 - maxc - minc) : d / (maxc + minc);
|
|
||||||
if (maxc == c.r) {
|
|
||||||
h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
|
|
||||||
} else if (maxc == c.g) {
|
|
||||||
h = (c.b - c.r) / d + 2.0;
|
|
||||||
} else {
|
|
||||||
h = (c.r - c.g) / d + 4.0;
|
|
||||||
}
|
|
||||||
h /= 6.0;
|
|
||||||
}
|
|
||||||
return vec3(h, s, l);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HSL to RGB
|
|
||||||
float hue2rgb(float p, float q, float t) {
|
|
||||||
if (t < 0.0) t += 1.0;
|
|
||||||
if (t > 1.0) t -= 1.0;
|
|
||||||
if (t < 1.0/6.0) return p + (q - p) * 6.0 * t;
|
|
||||||
if (t < 1.0/2.0) return q;
|
|
||||||
if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
vec3 hsl2rgb(vec3 hsl) {
|
|
||||||
float r, g, b;
|
|
||||||
float h = hsl.x;
|
|
||||||
float s = hsl.y;
|
|
||||||
float l = hsl.z;
|
|
||||||
if (s == 0.0) {
|
|
||||||
r = g = b = l;
|
|
||||||
} else {
|
|
||||||
float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
|
|
||||||
float p = 2.0 * l - q;
|
|
||||||
r = hue2rgb(p, q, h + 1.0/3.0);
|
|
||||||
g = hue2rgb(p, q, h);
|
|
||||||
b = hue2rgb(p, q, h - 1.0/3.0);
|
|
||||||
}
|
|
||||||
return vec3(r, g, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec4 in_color = texture(in_texture, in_uv);
|
|
||||||
vec3 color = in_color.rgb;
|
|
||||||
|
|
||||||
color = color * u_brightness;
|
|
||||||
color += vec3(clamp(u_lightness, 0.0, 2.0) - 1.0);
|
|
||||||
color = (color - 0.5) * u_contrast + 0.5;
|
|
||||||
|
|
||||||
vec3 hsl = rgb2hsl(color);
|
|
||||||
hsl.x = mod(hsl.x + u_hue, 1.0);
|
|
||||||
hsl.y = clamp(hsl.y * u_saturation, 0.0, 1.0);
|
|
||||||
|
|
||||||
color = hsl2rgb(hsl);
|
|
||||||
out_color = vec4(color, in_color.a);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const FX_colorAdjust = defineImageEffectorFx({
|
|
||||||
id: 'colorAdjust' as const,
|
|
||||||
name: i18n.ts._imageEffector._fxs.colorAdjust,
|
|
||||||
shader,
|
|
||||||
uniforms: ['lightness', 'contrast', 'hue', 'brightness', 'saturation'] as const,
|
|
||||||
params: {
|
|
||||||
lightness: {
|
|
||||||
type: 'number' as const,
|
|
||||||
default: 100,
|
|
||||||
min: 0,
|
|
||||||
max: 200,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
contrast: {
|
|
||||||
type: 'number' as const,
|
|
||||||
default: 100,
|
|
||||||
min: 0,
|
|
||||||
max: 200,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
hue: {
|
|
||||||
type: 'number' as const,
|
|
||||||
default: 0,
|
|
||||||
min: -360,
|
|
||||||
max: 360,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
brightness: {
|
|
||||||
type: 'number' as const,
|
|
||||||
default: 100,
|
|
||||||
min: 0,
|
|
||||||
max: 200,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
saturation: {
|
|
||||||
type: 'number' as const,
|
|
||||||
default: 100,
|
|
||||||
min: 0,
|
|
||||||
max: 200,
|
|
||||||
step: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
main: ({ gl, u, params }) => {
|
|
||||||
gl.uniform1f(u.brightness, params.brightness / 100);
|
|
||||||
gl.uniform1f(u.contrast, params.contrast / 100);
|
|
||||||
gl.uniform1f(u.hue, params.hue / 360);
|
|
||||||
gl.uniform1f(u.lightness, params.lightness / 100);
|
|
||||||
gl.uniform1f(u.saturation, params.saturation / 100);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function loadShader(gl: WebGL2RenderingContext, type: GLenum, source: string): WebGLShader {
|
|
||||||
const shader = gl.createShader(type);
|
|
||||||
if (shader == null) {
|
|
||||||
throw new Error('falied to create shader');
|
|
||||||
}
|
|
||||||
|
|
||||||
gl.shaderSource(shader, source);
|
|
||||||
gl.compileShader(shader);
|
|
||||||
|
|
||||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
||||||
console.error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
|
|
||||||
gl.deleteShader(shader);
|
|
||||||
throw new Error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initShaderProgram(gl: WebGL2RenderingContext, vsSource: string, fsSource: string): WebGLProgram {
|
|
||||||
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
|
|
||||||
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
|
||||||
|
|
||||||
const shaderProgram = gl.createProgram();
|
|
||||||
|
|
||||||
gl.attachShader(shaderProgram, vertexShader);
|
|
||||||
gl.attachShader(shaderProgram, fragmentShader);
|
|
||||||
gl.linkProgram(shaderProgram);
|
|
||||||
|
|
||||||
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
|
||||||
console.error(`failed to init shader: ${gl.getProgramInfoLog(shaderProgram)}`);
|
|
||||||
throw new Error('failed to init shader');
|
|
||||||
}
|
|
||||||
|
|
||||||
return shaderProgram;
|
|
||||||
}
|
|
|
@ -11,13 +11,13 @@
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.21",
|
||||||
"@types/wawoff2": "1.0.2",
|
"@types/wawoff2": "1.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "8.34.0",
|
"@typescript-eslint/eslint-plugin": "8.32.1",
|
||||||
"@typescript-eslint/parser": "8.34.0"
|
"@typescript-eslint/parser": "8.32.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tabler/icons-webfont": "3.34.0",
|
"@tabler/icons-webfont": "3.33.0",
|
||||||
"harfbuzzjs": "0.4.7",
|
"harfbuzzjs": "0.4.7",
|
||||||
"tiny-glob": "0.2.9",
|
"tiny-glob": "0.2.9",
|
||||||
"tsx": "4.19.4",
|
"tsx": "4.19.4",
|
||||||
|
|
|
@ -24,9 +24,9 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/matter-js": "0.19.8",
|
"@types/matter-js": "0.19.8",
|
||||||
"@types/seedrandom": "3.0.8",
|
"@types/seedrandom": "3.0.8",
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.28",
|
||||||
"@typescript-eslint/eslint-plugin": "8.34.0",
|
"@typescript-eslint/eslint-plugin": "8.33.0",
|
||||||
"@typescript-eslint/parser": "8.34.0",
|
"@typescript-eslint/parser": "8.33.0",
|
||||||
"nodemon": "3.1.10",
|
"nodemon": "3.1.10",
|
||||||
"execa": "9.6.0",
|
"execa": "9.6.0",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
|
|
|
@ -3212,11 +3212,10 @@ type PinnedUsersResponse = operations['pinned-users']['responses']['200']['conte
|
||||||
type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
|
type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// Warning: (ae-forgotten-export) The symbol "AllNullRecord" needs to be exported by the entry point index.d.ts
|
// Warning: (ae-forgotten-export) The symbol "AllNullRecord" needs to be exported by the entry point index.d.ts
|
||||||
// Warning: (ae-forgotten-export) The symbol "AllNullOrOptionalRecord" needs to be exported by the entry point index.d.ts
|
|
||||||
// Warning: (ae-forgotten-export) The symbol "NonNullableRecord" needs to be exported by the entry point index.d.ts
|
// Warning: (ae-forgotten-export) The symbol "NonNullableRecord" needs to be exported by the entry point index.d.ts
|
||||||
//
|
//
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'text'>> & AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>> & {
|
type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>> & {
|
||||||
files: [];
|
files: [];
|
||||||
fileIds: [];
|
fileIds: [];
|
||||||
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
|
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
|
||||||
|
@ -3749,7 +3748,7 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons
|
||||||
|
|
||||||
// Warnings were encountered during analysis:
|
// Warnings were encountered during analysis:
|
||||||
//
|
//
|
||||||
// src/entities.ts:54:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||||
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
|
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
|
||||||
// src/streaming.types.ts:218:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
|
// src/streaming.types.ts:218:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
|
||||||
// src/streaming.types.ts:228:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
|
// src/streaming.types.ts:228:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@readme/openapi-parser": "2.7.0",
|
"@readme/openapi-parser": "2.7.0",
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.21",
|
||||||
"@typescript-eslint/eslint-plugin": "8.34.0",
|
"@typescript-eslint/eslint-plugin": "8.32.1",
|
||||||
"@typescript-eslint/parser": "8.34.0",
|
"@typescript-eslint/parser": "8.32.1",
|
||||||
"openapi-types": "12.1.3",
|
"openapi-types": "12.1.3",
|
||||||
"openapi-typescript": "7.8.0",
|
"openapi-typescript": "6.7.6",
|
||||||
"ts-case-convert": "2.1.0",
|
"ts-case-convert": "2.1.0",
|
||||||
"tsx": "4.19.4",
|
"tsx": "4.19.4",
|
||||||
"typescript": "5.8.3"
|
"typescript": "5.8.3"
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
import * as ts from 'typescript';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TypeScript AST ノードから 'never' 型のプロパティを削除します。
|
|
||||||
* この関数は、特に 'paths' という名前の TypeAliasDeclaration 内や
|
|
||||||
* 'operations' という名前の InterfaceDeclaration 内を再帰的に探索し、
|
|
||||||
* そこに含まれるすべての TypeLiteralNode/Interfaceから 'PropertySignature' で型が 'never' であるものを削除します。
|
|
||||||
* さらに、すべてのプロパティが 'never' で除去された場合は、そのオブジェクト自体も削除します。
|
|
||||||
*
|
|
||||||
* @param astNodes 処理対象の ts.Node 配列 (例: `openapi-typescript` から出力されたもの)。
|
|
||||||
* @returns 'never' 型プロパティが削除された新しい ts.Node 配列。
|
|
||||||
*/
|
|
||||||
export function removeNeverPropertiesFromAST(astNodes: readonly ts.Node[]): ts.Node[] {
|
|
||||||
const factory = ts.factory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TypeLiteralNodeやInterfaceDeclarationのmembersからneverプロパティを除去し、必要なら型も再帰的に処理する共通関数
|
|
||||||
*/
|
|
||||||
function removeNeverPropertiesFromMembers(
|
|
||||||
members: readonly ts.TypeElement[],
|
|
||||||
visitType: (node: ts.Node) => ts.Node | undefined,
|
|
||||||
): { newMembers: ts.TypeElement[]; hasChanged: boolean } {
|
|
||||||
const newMembers: ts.TypeElement[] = [];
|
|
||||||
let hasChanged = false;
|
|
||||||
|
|
||||||
for (const member of members) {
|
|
||||||
if (ts.isPropertySignature(member)) {
|
|
||||||
if (member.type && member.type.kind === ts.SyntaxKind.NeverKeyword) {
|
|
||||||
hasChanged = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let updatedPropertySignature = member;
|
|
||||||
if (member.type) {
|
|
||||||
const visitedMemberType = ts.visitNode(member.type, visitType);
|
|
||||||
if (visitedMemberType && visitedMemberType !== member.type) {
|
|
||||||
updatedPropertySignature = factory.updatePropertySignature(
|
|
||||||
member,
|
|
||||||
member.modifiers,
|
|
||||||
member.name,
|
|
||||||
member.questionToken,
|
|
||||||
visitedMemberType as ts.TypeNode,
|
|
||||||
);
|
|
||||||
hasChanged = true;
|
|
||||||
} else if (visitedMemberType === undefined) {
|
|
||||||
// 子の型が消された場合、このプロパティも消す
|
|
||||||
hasChanged = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newMembers.push(updatedPropertySignature);
|
|
||||||
} else {
|
|
||||||
newMembers.push(member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { newMembers, hasChanged };
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeNodeRecursiveVisitor(node: ts.Node): ts.Node | undefined {
|
|
||||||
if (ts.isTypeLiteralNode(node)) {
|
|
||||||
const { newMembers, hasChanged } = removeNeverPropertiesFromMembers(node.members, typeNodeRecursiveVisitor);
|
|
||||||
|
|
||||||
if (newMembers.length === 0) {
|
|
||||||
// すべてのプロパティがneverで消された場合、このTypeLiteralNode自体も消す
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChanged) {
|
|
||||||
return factory.updateTypeLiteralNode(node, factory.createNodeArray(newMembers));
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ts.visitEachChild(node, typeNodeRecursiveVisitor, undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
function interfaceRecursiveVisitor(node: ts.Node): ts.Node | undefined {
|
|
||||||
if (ts.isInterfaceDeclaration(node)) {
|
|
||||||
const { newMembers, hasChanged } = removeNeverPropertiesFromMembers(node.members, typeNodeRecursiveVisitor);
|
|
||||||
|
|
||||||
if (newMembers.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasChanged) {
|
|
||||||
return factory.updateInterfaceDeclaration(
|
|
||||||
node,
|
|
||||||
node.modifiers,
|
|
||||||
node.name,
|
|
||||||
node.typeParameters,
|
|
||||||
node.heritageClauses,
|
|
||||||
newMembers,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
return ts.visitEachChild(node, interfaceRecursiveVisitor, undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
function topLevelVisitor(node: ts.Node): ts.Node | undefined {
|
|
||||||
if (ts.isTypeAliasDeclaration(node) && node.name.escapedText === 'paths') {
|
|
||||||
const newType = ts.visitNode(node.type, typeNodeRecursiveVisitor);
|
|
||||||
if (newType && newType !== node.type) {
|
|
||||||
return factory.updateTypeAliasDeclaration(
|
|
||||||
node,
|
|
||||||
node.modifiers,
|
|
||||||
node.name,
|
|
||||||
node.typeParameters,
|
|
||||||
newType as ts.TypeNode,
|
|
||||||
);
|
|
||||||
} else if (newType === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ts.isInterfaceDeclaration(node) && node.name.escapedText === 'operations') {
|
|
||||||
const result = interfaceRecursiveVisitor(node);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return ts.visitEachChild(node, topLevelVisitor, undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
const transformedNodes: ts.Node[] = [];
|
|
||||||
for (const astNode of astNodes) {
|
|
||||||
const resultNode = ts.visitNode(astNode, topLevelVisitor);
|
|
||||||
if (resultNode) {
|
|
||||||
transformedNodes.push(resultNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformedNodes;
|
|
||||||
}
|
|
|
@ -1,12 +1,9 @@
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { mkdir, readFile, writeFile } from 'fs/promises';
|
import { mkdir, readFile, writeFile } from 'fs/promises';
|
||||||
import type { OpenAPIV3_1 } from 'openapi-types';
|
import { OpenAPIV3_1 } from 'openapi-types';
|
||||||
import { toPascal } from 'ts-case-convert';
|
import { toPascal } from 'ts-case-convert';
|
||||||
import OpenAPIParser from '@readme/openapi-parser';
|
import OpenAPIParser from '@readme/openapi-parser';
|
||||||
import openapiTS, { astToString } from 'openapi-typescript';
|
import openapiTS, { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
|
||||||
import type { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
|
|
||||||
import ts from 'typescript';
|
|
||||||
import { removeNeverPropertiesFromAST } from './ast-transformer.js';
|
|
||||||
|
|
||||||
async function generateBaseTypes(
|
async function generateBaseTypes(
|
||||||
openApiDocs: OpenAPIV3_1.Document,
|
openApiDocs: OpenAPIV3_1.Document,
|
||||||
|
@ -31,6 +28,13 @@ async function generateBaseTypes(
|
||||||
assert('post' in item);
|
assert('post' in item);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
openApi.paths![key] = {
|
openApi.paths![key] = {
|
||||||
|
...('get' in item ? {
|
||||||
|
get: {
|
||||||
|
...item.get,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
operationId: ((item as PathItemObject).get as OperationObject).operationId!.replaceAll('get___', ''),
|
||||||
|
},
|
||||||
|
} : {}),
|
||||||
post: {
|
post: {
|
||||||
...item.post,
|
...item.post,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
@ -39,26 +43,15 @@ async function generateBaseTypes(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const tsNullNode = ts.factory.createLiteralTypeNode(ts.factory.createNull());
|
const generatedTypes = await openapiTS(openApi, {
|
||||||
const tsBlobNode = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Blob'));
|
|
||||||
|
|
||||||
const generatedTypesAst = await openapiTS(openApi, {
|
|
||||||
exportType: true,
|
exportType: true,
|
||||||
transform(schemaObject) {
|
transform(schemaObject) {
|
||||||
if ('format' in schemaObject && schemaObject.format === 'binary') {
|
if ('format' in schemaObject && schemaObject.format === 'binary') {
|
||||||
if (schemaObject.nullable) {
|
return schemaObject.nullable ? 'Blob | null' : 'Blob';
|
||||||
return ts.factory.createUnionTypeNode([tsBlobNode, tsNullNode]);
|
|
||||||
} else {
|
|
||||||
return tsBlobNode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
lines.push(generatedTypes);
|
||||||
const filteredAst = removeNeverPropertiesFromAST(generatedTypesAst);
|
|
||||||
|
|
||||||
lines.push(astToString(filteredAst));
|
|
||||||
|
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
await writeFile(typeFileName, lines.join('\n'));
|
await writeFile(typeFileName, lines.join('\n'));
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2025.6.1-beta.1",
|
"version": "2025.6.1-alpha.1",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
@ -38,9 +38,9 @@
|
||||||
"@microsoft/api-extractor": "7.52.8",
|
"@microsoft/api-extractor": "7.52.8",
|
||||||
"@swc/jest": "0.2.38",
|
"@swc/jest": "0.2.38",
|
||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.21",
|
||||||
"@typescript-eslint/eslint-plugin": "8.34.0",
|
"@typescript-eslint/eslint-plugin": "8.32.1",
|
||||||
"@typescript-eslint/parser": "8.34.0",
|
"@typescript-eslint/parser": "8.32.1",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-fetch-mock": "3.0.3",
|
"jest-fetch-mock": "3.0.3",
|
||||||
"jest-websocket-mock": "2.5.0",
|
"jest-websocket-mock": "2.5.0",
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
"execa": "8.0.1",
|
"execa": "8.0.1",
|
||||||
"tsd": "0.32.0",
|
"tsd": "0.32.0",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"esbuild": "0.25.5",
|
"esbuild": "0.25.4",
|
||||||
"glob": "11.0.2"
|
"glob": "11.0.2"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|
|
@ -77,7 +77,7 @@ export class APIClient {
|
||||||
|
|
||||||
if (mediaType === 'application/json') {
|
if (mediaType === 'application/json') {
|
||||||
payload = JSON.stringify({
|
payload = JSON.stringify({
|
||||||
...(this.assertIsRecord(params) ? params : {}),
|
...params,
|
||||||
i: credential !== undefined ? credential : this.credential,
|
i: credential !== undefined ? credential : this.credential,
|
||||||
});
|
});
|
||||||
} else if (mediaType === 'multipart/form-data') {
|
} else if (mediaType === 'multipart/form-data') {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -24,14 +24,10 @@ type NonNullableRecord<T> = {
|
||||||
type AllNullRecord<T> = {
|
type AllNullRecord<T> = {
|
||||||
[P in keyof T]: null;
|
[P in keyof T]: null;
|
||||||
};
|
};
|
||||||
type AllNullOrOptionalRecord<T> = {
|
|
||||||
[P in keyof T]: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PureRenote =
|
export type PureRenote =
|
||||||
Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'>
|
Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'>
|
||||||
& AllNullRecord<Pick<Note, 'text'>>
|
& AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>>
|
||||||
& AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>>
|
|
||||||
& { files: []; fileIds: []; }
|
& { files: []; fileIds: []; }
|
||||||
& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
|
& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,9 @@
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.28",
|
||||||
"@typescript-eslint/eslint-plugin": "8.34.0",
|
"@typescript-eslint/eslint-plugin": "8.33.0",
|
||||||
"@typescript-eslint/parser": "8.34.0",
|
"@typescript-eslint/parser": "8.33.0",
|
||||||
"execa": "9.6.0",
|
"execa": "9.6.0",
|
||||||
"nodemon": "3.1.10",
|
"nodemon": "3.1.10",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"misskey-js": "workspace:*"
|
"misskey-js": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/parser": "8.34.0",
|
"@typescript-eslint/parser": "8.33.0",
|
||||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
|
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"nodemon": "3.1.10",
|
"nodemon": "3.1.10",
|
||||||
|
|
3133
pnpm-lock.yaml
3133
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -9,16 +9,16 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mdast": "4.0.4",
|
"@types/mdast": "4.0.4",
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.21",
|
||||||
"@vitest/coverage-v8": "3.2.3",
|
"@vitest/coverage-v8": "3.1.4",
|
||||||
"mdast-util-to-string": "4.0.0",
|
"mdast-util-to-string": "4.0.0",
|
||||||
"remark": "15.0.1",
|
"remark": "15.0.1",
|
||||||
"remark-parse": "11.0.0",
|
"remark-parse": "11.0.0",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"unified": "11.0.5",
|
"unified": "11.0.5",
|
||||||
"vite": "6.3.5",
|
"vite": "6.3.5",
|
||||||
"vite-node": "3.2.3",
|
"vite-node": "3.1.4",
|
||||||
"vitest": "3.2.3"
|
"vitest": "3.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
|
@ -890,16 +890,6 @@
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@types/chai": {
|
|
||||||
"version": "5.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
|
|
||||||
"integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/deep-eql": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
|
@ -909,13 +899,6 @@
|
||||||
"@types/ms": "*"
|
"@types/ms": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/deep-eql": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||||
|
@ -940,9 +923,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.15.31",
|
"version": "22.15.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
||||||
"integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==",
|
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -956,16 +939,15 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/coverage-v8": {
|
"node_modules/@vitest/coverage-v8": {
|
||||||
"version": "3.2.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz",
|
||||||
"integrity": "sha512-D1QKzngg8PcDoCE8FHSZhREDuEy+zcKmMiMafYse41RZpBE5EDJyKOTdqK3RQfsV2S2nyKor5KCs8PyPRFqKPg==",
|
"integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.3.0",
|
"@ampproject/remapping": "^2.3.0",
|
||||||
"@bcoe/v8-coverage": "^1.0.2",
|
"@bcoe/v8-coverage": "^1.0.2",
|
||||||
"ast-v8-to-istanbul": "^0.3.3",
|
"debug": "^4.4.0",
|
||||||
"debug": "^4.4.1",
|
|
||||||
"istanbul-lib-coverage": "^3.2.2",
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
"istanbul-lib-report": "^3.0.1",
|
"istanbul-lib-report": "^3.0.1",
|
||||||
"istanbul-lib-source-maps": "^5.0.6",
|
"istanbul-lib-source-maps": "^5.0.6",
|
||||||
|
@ -980,8 +962,8 @@
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@vitest/browser": "3.2.3",
|
"@vitest/browser": "3.1.4",
|
||||||
"vitest": "3.2.3"
|
"vitest": "3.1.4"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@vitest/browser": {
|
"@vitest/browser": {
|
||||||
|
@ -990,15 +972,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "3.2.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz",
|
||||||
"integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==",
|
"integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@vitest/spy": "3.1.4",
|
||||||
"@vitest/spy": "3.2.3",
|
"@vitest/utils": "3.1.4",
|
||||||
"@vitest/utils": "3.2.3",
|
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
@ -1007,13 +988,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/mocker": {
|
"node_modules/@vitest/mocker": {
|
||||||
"version": "3.2.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz",
|
||||||
"integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==",
|
"integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "3.2.3",
|
"@vitest/spy": "3.1.4",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"magic-string": "^0.30.17"
|
"magic-string": "^0.30.17"
|
||||||
},
|
},
|
||||||
|
@ -1022,7 +1003,7 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"msw": "^2.4.9",
|
"msw": "^2.4.9",
|
||||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
"vite": "^5.0.0 || ^6.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"msw": {
|
"msw": {
|
||||||
|
@ -1034,9 +1015,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/pretty-format": {
|
"node_modules/@vitest/pretty-format": {
|
||||||
"version": "3.2.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz",
|
||||||
"integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==",
|
"integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1047,28 +1028,27 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "3.2.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz",
|
||||||
"integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==",
|
"integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "3.2.3",
|
"@vitest/utils": "3.1.4",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3"
|
||||||
"strip-literal": "^3.0.0"
|
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "3.2.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz",
|
||||||
"integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==",
|
"integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.2.3",
|
"@vitest/pretty-format": "3.1.4",
|
||||||
"magic-string": "^0.30.17",
|
"magic-string": "^0.30.17",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
|
@ -1077,26 +1057,26 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/spy": {
|
||||||
"version": "3.2.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz",
|
||||||
"integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==",
|
"integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tinyspy": "^4.0.3"
|
"tinyspy": "^3.0.2"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "3.2.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz",
|
||||||
"integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==",
|
"integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.2.3",
|
"@vitest/pretty-format": "3.1.4",
|
||||||
"loupe": "^3.1.3",
|
"loupe": "^3.1.3",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
@ -1140,18 +1120,6 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ast-v8-to-istanbul": {
|
|
||||||
"version": "0.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz",
|
|
||||||
"integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
|
||||||
"estree-walker": "^3.0.3",
|
|
||||||
"js-tokens": "^9.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/bail": {
|
"node_modules/bail": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||||
|
@ -1260,9 +1228,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1589,13 +1557,6 @@
|
||||||
"@pkgjs/parseargs": "^0.11.0"
|
"@pkgjs/parseargs": "^0.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-tokens": {
|
|
||||||
"version": "9.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
|
||||||
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/longest-streak": {
|
"node_modules/longest-streak": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
|
||||||
|
@ -2600,19 +2561,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-literal": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"js-tokens": "^9.0.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/antfu"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
@ -2655,9 +2603,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.13",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -2672,9 +2620,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinypool": {
|
"node_modules/tinypool": {
|
||||||
"version": "1.1.0",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
|
||||||
"integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==",
|
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -2692,9 +2640,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyspy": {
|
"node_modules/tinyspy": {
|
||||||
"version": "4.0.3",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
|
||||||
"integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
|
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -2912,17 +2860,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-node": {
|
"node_modules/vite-node": {
|
||||||
"version": "3.2.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz",
|
||||||
"integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==",
|
"integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.0",
|
||||||
"es-module-lexer": "^1.7.0",
|
"es-module-lexer": "^1.7.0",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
"vite": "^5.0.0 || ^6.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite-node": "vite-node.mjs"
|
"vite-node": "vite-node.mjs"
|
||||||
|
@ -2935,34 +2883,32 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "3.2.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz",
|
||||||
"integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==",
|
"integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@vitest/expect": "3.1.4",
|
||||||
"@vitest/expect": "3.2.3",
|
"@vitest/mocker": "3.1.4",
|
||||||
"@vitest/mocker": "3.2.3",
|
"@vitest/pretty-format": "^3.1.4",
|
||||||
"@vitest/pretty-format": "^3.2.3",
|
"@vitest/runner": "3.1.4",
|
||||||
"@vitest/runner": "3.2.3",
|
"@vitest/snapshot": "3.1.4",
|
||||||
"@vitest/snapshot": "3.2.3",
|
"@vitest/spy": "3.1.4",
|
||||||
"@vitest/spy": "3.2.3",
|
"@vitest/utils": "3.1.4",
|
||||||
"@vitest/utils": "3.2.3",
|
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.0",
|
||||||
"expect-type": "^1.2.1",
|
"expect-type": "^1.2.1",
|
||||||
"magic-string": "^0.30.17",
|
"magic-string": "^0.30.17",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"picomatch": "^4.0.2",
|
|
||||||
"std-env": "^3.9.0",
|
"std-env": "^3.9.0",
|
||||||
"tinybench": "^2.9.0",
|
"tinybench": "^2.9.0",
|
||||||
"tinyexec": "^0.3.2",
|
"tinyexec": "^0.3.2",
|
||||||
"tinyglobby": "^0.2.14",
|
"tinyglobby": "^0.2.13",
|
||||||
"tinypool": "^1.1.0",
|
"tinypool": "^1.0.2",
|
||||||
"tinyrainbow": "^2.0.0",
|
"tinyrainbow": "^2.0.0",
|
||||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
|
"vite": "^5.0.0 || ^6.0.0",
|
||||||
"vite-node": "3.2.3",
|
"vite-node": "3.1.4",
|
||||||
"why-is-node-running": "^2.3.0"
|
"why-is-node-running": "^2.3.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -2978,8 +2924,8 @@
|
||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||||
"@vitest/browser": "3.2.3",
|
"@vitest/browser": "3.1.4",
|
||||||
"@vitest/ui": "3.2.3",
|
"@vitest/ui": "3.1.4",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*"
|
"jsdom": "*"
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,15 +10,15 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mdast": "4.0.4",
|
"@types/mdast": "4.0.4",
|
||||||
"@types/node": "22.15.31",
|
"@types/node": "22.15.21",
|
||||||
"@vitest/coverage-v8": "3.2.3",
|
"@vitest/coverage-v8": "3.1.4",
|
||||||
"mdast-util-to-string": "4.0.0",
|
"mdast-util-to-string": "4.0.0",
|
||||||
"remark": "15.0.1",
|
"remark": "15.0.1",
|
||||||
"remark-parse": "11.0.0",
|
"remark-parse": "11.0.0",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"unified": "11.0.5",
|
"unified": "11.0.5",
|
||||||
"vite": "6.3.5",
|
"vite": "6.3.5",
|
||||||
"vite-node": "3.2.3",
|
"vite-node": "3.1.4",
|
||||||
"vitest": "3.2.3"
|
"vitest": "3.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue