Compare commits

..

34 Commits

Author SHA1 Message Date
github-actions[bot] ac9206f192 Bump version to 2025.6.1-alpha.4 2025-06-07 10:52:03 +00:00
かっこかり e2b38edb3a deps(misskey-js): Update openapi-typescript to v7 (#15491)
* deps(misskey-js): Update openapi-typescript to v7

* update openapi-typescript to v7.7.3

* generate misskey-js types

* bump openapi-typescript

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

* regenerate api types

* refactor: 処理共通化

---------

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

* Update MkPostForm.vue

* wip

* wip

* Update MkPostForm.vue

* wip

* wip

* add tip

* Update tips.ts

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

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

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

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

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

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Catalan)

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

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

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Portuguese)

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

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

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Japanese, Kansai)
2025-06-04 18:25:39 +09:00
かっこかり 80f73c6712 enhance(gh): renovateのicons-subsetterをfrontendグループに統合 (#16161) 2025-06-04 16:39:06 +09:00
syuilo 60fc9a5195 🎨 2025-06-04 16:33:55 +09:00
かっこかり b43dfa260b fix/refactor(frontend): 画像編集機能の修正・型強化 (#16156)
* enhance: refine uploadFile

* fix: missing locale

* refactor: harden types

* refactor: シェーダーファイルをlazy-loadingできるように

* fix(frontend): omit console.log in production environment

* fix: glslのバージョン表記は最初の行になければならない

* fix: シェーダーの読み込みが完了してからレンダリングを行うように

* fix merge failure

* fix: ウォーターマークのプリセットがない場合にdividerが2重に表示される問題を修正

* fix: アップローダーダイアログの機能設定でウォーターマークが無効な場合でもデフォルトのプリセットが適用されてしまう問題を修正

* fix lint

* Revert "fix: シェーダーの読み込みが完了してからレンダリングを行うように"

This reverts commit e06f37a7d4.

* Revert "fix: glslのバージョン表記は最初の行になければならない"

This reverts commit afcc37d886.

* Revert "refactor: シェーダーファイルをlazy-loadingできるように"

This reverts commit a1ab2fa38c.

* fix: ウォーターマークのFX定義を分ける

* Update packages/frontend/src/components/MkWatermarkEditorDialog.vue

* Update packages/frontend/src/components/MkWatermarkEditorDialog.vue

* Update packages/frontend/src/components/MkWatermarkEditorDialog.vue

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-06-04 16:22:09 +09:00
syuilo e3b57a118d 🎨 2025-06-04 16:15:06 +09:00
syuilo fdcb6a09a9 fix(frontend): タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正 2025-06-04 16:10:57 +09:00
syuilo 218df19d9c enhance(frontend): スマホでのMkModalWindowの表示を最適化 2025-06-04 12:28:50 +09:00
syuilo b766589c00 enhance(frontend): ミュートした絵文字をデバイス間で同期できるように
Resolve #16158
2025-06-04 12:01:33 +09:00
かっこかり d94bdff639 fix(misskey-js): build misskey-js with types (#16159) 2025-06-04 11:49:29 +09:00
github-actions[bot] 3a4288fe24 Bump version to 2025.6.1-alpha.1 2025-06-03 12:13:26 +00:00
syuilo 12c71de4c7 New Crowdin updates (#16153)
* New translations ja-jp.yml (Romanian)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Greek)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Dutch)

* New translations ja-jp.yml (Norwegian)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (Turkish)

* New translations ja-jp.yml (Ukrainian)

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

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

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Uzbek)

* New translations ja-jp.yml (Lao)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Korean (Gyeongsang))
2025-06-03 21:07:02 +09:00
syuilo b7aa013a41 feat: 全てのチャットメッセージを既読にできるように 2025-06-03 21:06:37 +09:00
93 changed files with 37663 additions and 31420 deletions
+2
View File
@@ -26,6 +26,8 @@ jobs:
with:
node-version-file: '.node-version'
cache: 'pnpm'
# see https://docs.github.com/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry
registry-url: 'https://registry.npmjs.org'
- name: Publish package
run: |
pnpm i --frozen-lockfile
+5 -1
View File
@@ -6,12 +6,16 @@
### Client
- Feat: 画像にウォーターマークを付与できるようになりました
- Enhance: ノートのリアクション一覧で、押せるリアクションを優先して表示できるようにするオプションを追加
- Enhance: 全てのチャットメッセージを既読にできるように(設定→その他)
- Enhance: ミュートした絵文字をデバイス間で同期できるように
- Fix: ドライブファイルの選択が不安定な問題を修正
- Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正
- Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正
- Fix: タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正
### Server
-
- Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all)
- Fix: アカウント削除が正常に行われないことがあった問題を修正
## 2025.6.0
-1
View File
@@ -34,7 +34,6 @@ describe('Before setup instance', () => {
cy.intercept('POST', '/api/admin/update-meta').as('update-meta');
cy.get('[data-cy-next]').click();
cy.get('[data-cy-next]').click();
cy.get('[data-cy-server-name] input').type('Testskey');
cy.get('[data-cy-server-setup-wizard-apply]').click();
+8
View File
@@ -1589,3 +1589,11 @@ _search:
searchScopeAll: "الكل"
searchScopeLocal: "المحلي"
searchScopeUser: "مستخدم محدد"
_watermarkEditor:
opacity: "الشفافية"
scale: "الحجم"
text: "نص"
position: "الموضع"
type: "نوع"
image: "صور"
advanced: "متقدم"
+6
View File
@@ -1349,3 +1349,9 @@ _remoteLookupErrors:
_search:
searchScopeAll: "সবগুলো"
searchScopeLocal: "স্থানীয়"
_watermarkEditor:
opacity: "অস্বচ্ছতা"
scale: "আকার"
text: "লেখা"
image: "ছবি"
advanced: "উন্নত"
+48
View File
@@ -1365,6 +1365,8 @@ abort: "Cancel·lar"
tip: "Trucs i consells"
redisplayAllTips: "Torna ha mostrat tots els trucs i consells"
hideAllTips: "Amagar tots els trucs i consells"
defaultImageCompressionLevel: "Nivell de comprensió de la imatge per defecte"
defaultImageCompressionLevel_description: "Baixa, conserva la qualitat de la imatge però la mida de l'arxiu és més gran. <br>Alta, redueix la mida de l'arxiu però també la qualitat de la imatge."
_chat:
noMessagesYet: "Encara no tens missatges "
newMessage: "Missatge nou"
@@ -1452,6 +1454,7 @@ _settings:
contentsUpdateFrequency_description: "Com més alt sigui l'adquisició de contingut en temps real, més baixa el rendiment i més consum de dades i bateria."
contentsUpdateFrequency_description2: "Quan s'activa el mode en temps real, el contingut s'actualitza en temps real, independentment d'aquesta configuració."
showUrlPreview: "Mostrar vista prèvia d'URL"
showAvailableReactionsFirstInNote: "Mostra les reacciones que pots fer servir al damunt"
_chat:
showSenderName: "Mostrar el nom del remitent"
sendOnEnter: "Introdueix per enviar"
@@ -3117,3 +3120,48 @@ _clip:
tip: "Clip és una funció que permet organitzar les teves notes."
_userLists:
tip: "Es poden crear llistes amb qualsevol usuari. La llista creada es pot mostrar com una línia de temps."
watermark: "Marca d'aigua "
defaultPreset: "Per defecte"
_watermarkEditor:
tip: "A la imatge es pot afegir una marca d'aigua com informació sobre drets."
quitWithoutSaveConfirm: "Sortir sense desar?"
driveFileTypeWarn: "Fitxer no suportat "
title: "Editar la marca d'aigua "
cover: "Cobrir-ho tot"
repeat: "Repetir"
opacity: "Opacitat"
scale: "Mida"
text: "Text"
position: "Posició "
type: "Tipus"
image: "Imatges"
advanced: "Avançat"
stripe: "Bandes"
stripeWidth: "Amplada de la banda"
stripeFrequency: "Freqüència de la banda"
angle: "Angle"
polkadot: "Lunars"
checker: "Escacs"
polkadotMainDotOpacity: "Opacitat del lunar principal"
polkadotMainDotRadius: "Mida del lunar principal"
polkadotSubDotOpacity: "Opacitat del lunar secundari"
polkadotSubDotRadius: "Mida del lunar secundari"
polkadotSubDotDivisions: "Nombre de punts secundaris"
_imageEffector:
title: "Efecte"
addEffect: "Afegeix un efecte"
discardChangesConfirm: "Vols descartar els canvis i sortir?"
_fxs:
chromaticAberration: "Aberració cromàtica"
glitch: "Glitch"
mirror: "Mirall"
invert: "Inversió cromàtica "
grayscale: "Monocrom "
colorClamp: "Compressió cromàtica "
colorClampAdvanced: "Compressió de cromàtica avançada "
distort: "Distorsió "
threshold: "Binarització"
zoomLines: "Saturació de línies "
stripe: "Bandes"
polkadot: "Lunars"
checker: "Escacs"
+8
View File
@@ -2043,3 +2043,11 @@ _search:
searchScopeAll: "Vše"
searchScopeLocal: "Místní"
searchScopeUser: "Upřesnit uživatele"
_watermarkEditor:
opacity: "Průhlednost"
scale: "Velikost"
text: "Text"
position: "Pozice"
type: "Typ"
image: "Obrázky"
advanced: "Pokročilé"
+10
View File
@@ -3001,3 +3001,13 @@ _search:
pleaseEnterServerHost: "Gib den Server-Host ein"
pleaseSelectUser: "Benutzer auswählen"
serverHostPlaceholder: "Beispiel: misskey.example.com"
_watermarkEditor:
driveFileTypeWarn: "Diese Datei wird nicht unterstützt"
opacity: "Transparenz"
scale: "Größe"
text: "Text"
position: "Position"
type: "Art"
image: "Bilder"
advanced: "Fortgeschritten"
angle: "Winkel"
+2
View File
@@ -403,3 +403,5 @@ _reversi:
total: "Σύνολο"
_search:
searchScopeLocal: "Τοπικό"
_watermarkEditor:
image: "Εικόνες"
+10
View File
@@ -3117,3 +3117,13 @@ _clip:
tip: "Clip is a feature that allows you to organize your notes."
_userLists:
tip: "Lists can contain any user you specify when creating, the created list can then be displayed as a timeline showing only the specified users."
_watermarkEditor:
driveFileTypeWarn: "This file is not supported"
opacity: "Opacity"
scale: "Size"
text: "Text"
position: "Position"
type: "Type"
image: "Images"
advanced: "Advanced"
angle: "Angle"
+10
View File
@@ -2933,3 +2933,13 @@ _search:
searchScopeUser: "Especificar usuario"
_uploader:
allowedTypes: "Tipos de archivos que se pueden cargar."
_watermarkEditor:
driveFileTypeWarn: "Este archivo es incompatible"
opacity: "Opacidad"
scale: "Tamaño"
text: "Texto"
position: "Posición"
type: "Tipo"
image: "Imágenes"
advanced: "Avanzado"
angle: "Ángulo"
+10
View File
@@ -2360,3 +2360,13 @@ _search:
searchScopeAll: "Tous"
searchScopeLocal: "Local"
searchScopeUser: "Spécifier l'utilisateur·rice"
_watermarkEditor:
driveFileTypeWarn: "Ce fichier n'est pas pris en charge"
opacity: "Transparence"
scale: "Taille"
text: "Texte"
position: "Position"
type: "Type"
image: "Images"
advanced: "Avancé"
angle: "Angle"
+10
View File
@@ -2608,3 +2608,13 @@ _search:
searchScopeAll: "Semua"
searchScopeLocal: "Lokal"
searchScopeUser: "Pengguna spesifik"
_watermarkEditor:
driveFileTypeWarn: "Berkas ini tidak didukung"
opacity: "Opasitas"
scale: "Ukuran"
text: "Teks"
position: "Posisi"
type: "Tipe"
image: "Gambar"
advanced: "Tingkat lanjut"
angle: "Sudut"
+20
View File
@@ -9584,6 +9584,14 @@ export interface Locale extends ILocale {
"disableFederationDescription": string;
};
"_postForm": {
/**
*
*/
"quitInspiteOfThereAreUnuploadedFilesConfirm": string;
/**
* 稿
*/
"uploaderTip": string;
/**
* ...
*/
@@ -12049,6 +12057,14 @@ export interface Locale extends ILocale {
*
*/
"quitWithoutSaveConfirm": string;
/**
*
*/
"driveFileTypeWarn": string;
/**
*
*/
"driveFileTypeWarnDescription": string;
/**
*
*/
@@ -12168,6 +12184,10 @@ export interface Locale extends ILocale {
*
*/
"grayscale": string;
/**
* 調
*/
"colorAdjust": string;
/**
*
*/
+10
View File
@@ -3112,3 +3112,13 @@ _clip:
tip: "Le clip sono una funzionalità che consente di raggruppare le Note."
_userLists:
tip: "Puoi creare un elenco di Note create da qualsiasi profilo. L'elenco è visualizzato come una sequenza temporale."
_watermarkEditor:
driveFileTypeWarn: "Formato file non supportato"
opacity: "Opacità"
scale: "Dimensioni"
text: "Testo"
position: "Posizione"
type: "Tipo"
image: "Immagini"
advanced: "Avanzato"
angle: "Angolo"
+5
View File
@@ -2522,6 +2522,8 @@ _visibility:
disableFederationDescription: "他サーバーへの配信を行いません"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされていないファイルがありますが、破棄してフォームを閉じますか?"
uploaderTip: "ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。"
replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..."
channelPlaceholder: "チャンネルに投稿..."
@@ -3227,6 +3229,8 @@ defaultPreset: "デフォルトのプリセット"
_watermarkEditor:
tip: "画像にクレジット情報などのウォーターマークを追加することができます。"
quitWithoutSaveConfirm: "保存せずに終了しますか?"
driveFileTypeWarn: "このファイルは対応していません"
driveFileTypeWarnDescription: "画像ファイルを選択してください"
title: "ウォーターマークの編集"
cover: "全体に被せる"
repeat: "敷き詰める"
@@ -3260,6 +3264,7 @@ _imageEffector:
mirror: "ミラー"
invert: "色の反転"
grayscale: "白黒"
colorAdjust: "色調補正"
colorClamp: "色の圧縮"
colorClampAdvanced: "色の圧縮(高度)"
distort: "歪み"
+10
View File
@@ -2848,3 +2848,13 @@ _search:
searchScopeAll: "みんな"
searchScopeLocal: "ローカル"
searchScopeUser: "ユーザー指定"
_watermarkEditor:
driveFileTypeWarn: "このファイルは対応しとらへん"
opacity: "不透明度"
scale: "大きさ"
text: "テキスト"
position: "位置"
type: "タイプ"
image: "画像"
advanced: "高度"
angle: "角度"
+2
View File
@@ -848,3 +848,5 @@ _remoteLookupErrors:
_search:
searchScopeAll: "말캉"
searchScopeUser: "사용자 지정"
_watermarkEditor:
image: "이미지"
+48
View File
@@ -1365,6 +1365,8 @@ abort: "중지"
tip: "팁과 유용한 정보"
redisplayAllTips: "모든 '팁과 유용한 정보'를 재표시"
hideAllTips: "모든 '팁과 유용한 정보'를 비표시"
defaultImageCompressionLevel: "기본 이미지 압축 정도"
defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다. <br>높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다."
_chat:
noMessagesYet: "아직 메시지가 없습니다"
newMessage: "새로운 메시지"
@@ -1452,6 +1454,7 @@ _settings:
contentsUpdateFrequency_description: "높을수록 실시간으로 콘텐츠가 업데이트됩니다만, 성능이 저하되고 데이터 사용량과 배터리의 소비가 증가합니다."
contentsUpdateFrequency_description2: "실시간 모드가 켜져 있을 때는 이 설정과 상관없이 실시간으로 콘텐츠가 업데이트됩니다."
showUrlPreview: "URL 미리보기 표시"
showAvailableReactionsFirstInNote: "이용 가능한 리액션을 선두로 표시"
_chat:
showSenderName: "발신자 이름 표시"
sendOnEnter: "엔터로 보내기"
@@ -3117,3 +3120,48 @@ _clip:
tip: "클립은 노트를 정리할 수 있는 기능입니다."
_userLists:
tip: "임의의 유저가 포함된 리스트를 작성할 수 있습니다. 작성한 리스트는 타임라인으로 표시가 가능합니다."
watermark: "워터마크"
defaultPreset: "기본 프리셋"
_watermarkEditor:
tip: "이미지에 크레딧 정보 등의 워터마크를 추가할 수 있습니다."
quitWithoutSaveConfirm: "보존하지 않고 종료하시겠습니까?"
driveFileTypeWarn: "이 파이"
title: "워터마크 편집"
cover: "전체에 붙이기"
repeat: "전면에 깔기"
opacity: "불투명도"
scale: "크기"
text: "텍스트"
position: "위치"
type: "종류"
image: "이미지"
advanced: "고급"
stripe: "줄무늬"
stripeWidth: "라인의 폭"
stripeFrequency: "라인의 수"
angle: "각도"
polkadot: "물방울 무늬"
checker: "체크 무늬"
polkadotMainDotOpacity: "주요 물방울의 불투명도"
polkadotMainDotRadius: "주요 물방울의 크기"
polkadotSubDotOpacity: "서브 물방울의 불투명도"
polkadotSubDotRadius: "서브 물방울의 크기"
polkadotSubDotDivisions: "서브 물방울의 수"
_imageEffector:
title: "이펙트"
addEffect: "이펙트를 추가"
discardChangesConfirm: "변경을 취소하고 종료하시겠습니까?"
_fxs:
chromaticAberration: "색수차"
glitch: "글리치"
mirror: "미러"
invert: "색 반전"
grayscale: "흑백"
colorClamp: "색 압축"
colorClampAdvanced: "색 압축(고급)"
distort: "뒤틀림"
threshold: "이진화"
zoomLines: "집중선"
stripe: "줄무늬"
polkadot: "물방울 무늬"
checker: "체크 무늬"
+2
View File
@@ -483,3 +483,5 @@ _remoteLookupErrors:
title: "ບໍ່ພົບ"
_search:
searchScopeAll: "ທັງໝົດ"
_watermarkEditor:
image: "ຮູບພາບ"
+3
View File
@@ -1078,3 +1078,6 @@ _remoteLookupErrors:
title: "Niet gevonden"
_search:
searchScopeAll: "Alle"
_watermarkEditor:
image: "Afbeeldingen"
advanced: "Geavanceerd"
+5
View File
@@ -735,3 +735,8 @@ _remoteLookupErrors:
title: "Ikke funnet"
_search:
searchScopeAll: "Alle"
_watermarkEditor:
scale: "Størrelse"
text: "Tekst"
type: "Type"
image: "Bilder"
+7
View File
@@ -1584,3 +1584,10 @@ _remoteLookupErrors:
_search:
searchScopeAll: "Wszystkie"
searchScopeLocal: "Lokalne"
_watermarkEditor:
opacity: "Przezroczystość"
scale: "Rozmiar"
text: "Tekst"
type: "Typ"
image: "Zdjęcia"
advanced: "Zaawansowane"
+10
View File
@@ -3084,3 +3084,13 @@ _clientPerformanceIssueTip:
makeSureDisabledCustomCss_description: "Substituir o estilo da página pode afetar o desempenho. Certifique-se que o CSS personalizado ou extensões que modifiquem o estilo da página estejam desabilitados."
makeSureDisabledAddons: "Desabilite extensões"
makeSureDisabledAddons_description: "Algumas extensões podem afetar comportamentos do cliente e afetar o desempenho. Por favor, desative as extensões do seu navegador e veja se isso melhora a situação."
_watermarkEditor:
driveFileTypeWarn: "Esse arquivo não é compatível"
opacity: "Opacidade"
scale: "Tamanho"
text: "Texto"
position: "Posição"
type: "Tipo"
image: "imagem"
advanced: "Avançado"
angle: "Ângulo"
+7
View File
@@ -1391,3 +1391,10 @@ _search:
searchScopeLocal: "Local"
searchScopeUser: "Utilizator specific"
serverHostPlaceholder: "Exemplu: misskey.example.com"
_watermarkEditor:
scale: "Dimensiune"
text: "Text"
position: "Poziție"
type: "Tip"
image: "Imagini"
advanced: "Avansat"
+9
View File
@@ -2191,3 +2191,12 @@ _search:
searchScopeAll: "Все"
searchScopeLocal: "Местная"
searchScopeUser: "Указанный пользователь"
_watermarkEditor:
opacity: "Непрозрачность"
scale: "Размер"
text: "Текст"
position: "Позиция"
type: "Тип"
image: "Изображения"
advanced: "Для продвинутых"
angle: "Угол"
+7
View File
@@ -1450,3 +1450,10 @@ _remoteLookupErrors:
_search:
searchScopeAll: "Všetko"
searchScopeLocal: "Lokálne"
_watermarkEditor:
opacity: "Priehľadnosť"
scale: "Veľkosť"
text: "Text"
type: "Typ"
image: "Obrázky"
advanced: "Rozšírené"
+3
View File
@@ -711,3 +711,6 @@ _selfXssPrevention:
warning: "VARNING"
_search:
searchScopeAll: "Allt"
_watermarkEditor:
scale: "Storlek"
image: "Bilder"
+10
View File
@@ -2722,3 +2722,13 @@ _search:
searchScopeAll: "ทั้งหมด"
searchScopeLocal: "ท้องถิ่น"
searchScopeUser: "ผู้ใช้เฉพาะ"
_watermarkEditor:
driveFileTypeWarn: "ไม่รองรับไฟล์นี้"
opacity: "ความทึบแสง"
scale: "ขนาด"
text: "ข้อความ"
position: "ตำแหน่ง"
type: "รูปแบบ"
image: "รูปภาพ"
advanced: "ขั้นสูง"
angle: "แองเกิล"
+2
View File
@@ -460,3 +460,5 @@ _moderationLogTypes:
resetPassword: "Şifre sıfırlama"
_search:
searchScopeAll: "Tümü"
_watermarkEditor:
image: "Görseller"
+7
View File
@@ -1625,3 +1625,10 @@ _remoteLookupErrors:
_search:
searchScopeAll: "Всі"
searchScopeLocal: "Локальна"
_watermarkEditor:
opacity: "Непрозорість"
scale: "Розмір"
text: "Текст"
type: "Тип"
image: "Зображення"
advanced: "Розширені"
+5
View File
@@ -1097,3 +1097,8 @@ _remoteLookupErrors:
_search:
searchScopeAll: "Barcha"
searchScopeLocal: "Mahalliy"
_watermarkEditor:
text: "Matn"
type: "turi"
image: "Rasmlar"
advanced: "Murakkab"
+9
View File
@@ -2074,3 +2074,12 @@ _search:
searchScopeAll: "Tất cả"
searchScopeLocal: "Máy chủ này"
searchScopeUser: "Người dùng chỉ định"
_watermarkEditor:
opacity: "Độ trong suốt"
scale: "Kích thước"
text: "Văn bản"
position: "Vị trí"
type: "Loại"
image: "Hình ảnh"
advanced: "Nâng cao"
angle: "Góc"
+48
View File
@@ -1365,6 +1365,8 @@ abort: "中止"
tip: "提示和技巧"
redisplayAllTips: "重新显示所有的提示和技巧"
hideAllTips: "隐藏所有的提示和技巧"
defaultImageCompressionLevel: "默认图像压缩等级"
defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。"
_chat:
noMessagesYet: "还没有消息"
newMessage: "新消息"
@@ -1452,6 +1454,7 @@ _settings:
contentsUpdateFrequency_description: "设置越高,内容更新越实时,但性能会降低,并且会消耗更多的流量和电池。"
contentsUpdateFrequency_description2: "当实时模式开启时,无论此设置如何,内容都会实时更新。"
showUrlPreview: "显示 URL 预览"
showAvailableReactionsFirstInNote: "在顶部显示可用的回应"
_chat:
showSenderName: "显示发送者的名字"
sendOnEnter: "回车键发送"
@@ -3117,3 +3120,48 @@ _clip:
tip: "便签功能可以将帖子合并在一起。"
_userLists:
tip: "可创建包含任意用户的列表。已创建的列表可作为时间线查看。"
watermark: "水印"
defaultPreset: "默认预设"
_watermarkEditor:
tip: "可在图像内增加包含作者等信息的水印。"
quitWithoutSaveConfirm: "不保存就退出吗?"
driveFileTypeWarn: "不支持此文件"
title: "编辑水印"
cover: "覆盖全体"
repeat: "平铺"
opacity: "不透明度"
scale: "大小"
text: "文本"
position: "位置"
type: "类型"
image: "图片"
advanced: "高级"
stripe: "条纹"
stripeWidth: "线条宽度"
stripeFrequency: "线条数量"
angle: "角度"
polkadot: "波点"
checker: "检查"
polkadotMainDotOpacity: "主波点的不透明度"
polkadotMainDotRadius: "主波点的大小"
polkadotSubDotOpacity: "副波点的不透明度"
polkadotSubDotRadius: "副波点的大小"
polkadotSubDotDivisions: "副波点的数量"
_imageEffector:
title: "效果"
addEffect: "添加效果"
discardChangesConfirm: "丢弃当前设置并退出?"
_fxs:
chromaticAberration: "色差"
glitch: "故障"
mirror: "镜像"
invert: "反转颜色"
grayscale: "黑白"
colorClamp: "颜色限制"
colorClampAdvanced: "颜色限制(高级)"
distort: "失真"
threshold: "二值化"
zoomLines: "集中线"
stripe: "条纹"
polkadot: "波点"
checker: "检查"
+48
View File
@@ -1365,6 +1365,8 @@ abort: "取消"
tip: "提示與技巧"
redisplayAllTips: "重新顯示所有「提示與技巧」"
hideAllTips: "隱藏所有「提示與技巧」"
defaultImageCompressionLevel: "預設的影像壓縮程度"
defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。<br>高的話可以減少檔案大小,但是會降低畫質。"
_chat:
noMessagesYet: "尚無訊息"
newMessage: "新訊息"
@@ -1452,6 +1454,7 @@ _settings:
contentsUpdateFrequency_description: "頻率越高,內容更新越即時,但可能會降低效能,並增加資料傳輸量與電池消耗。\n"
contentsUpdateFrequency_description2: "當即時模式開啟時,不論此設定為何,內容都會即時更新。"
showUrlPreview: "顯示網址預覽"
showAvailableReactionsFirstInNote: "將可用的反應顯示在頂部"
_chat:
showSenderName: "顯示發送者的名稱"
sendOnEnter: "按下 Enter 發送訊息"
@@ -3117,3 +3120,48 @@ _clip:
tip: "摘錄是一項可以用來整理貼文的功能。"
_userLists:
tip: "您可以建立包含任意使用者的清單。建立後的清單可以作為時間軸顯示。\n"
watermark: "浮水印"
defaultPreset: "預設值"
_watermarkEditor:
tip: "可以在圖片中以浮水印加上出處等資訊。"
quitWithoutSaveConfirm: "不儲存就退出嗎?"
driveFileTypeWarn: "不支援此檔案"
title: "編輯浮水印"
cover: "覆蓋整體"
repeat: "佈局"
opacity: "透明度"
scale: "大小"
text: "文字"
position: "位置"
type: "類型"
image: "圖片"
advanced: "進階"
stripe: "條紋"
stripeWidth: "線條寬度"
stripeFrequency: "線條數量"
angle: "角度"
polkadot: "波卡圓點"
checker: "棋盤格"
polkadotMainDotOpacity: "主圓點的不透明度"
polkadotMainDotRadius: "主圓點的尺寸"
polkadotSubDotOpacity: "子圓點的不透明度"
polkadotSubDotRadius: "子圓點的尺寸"
polkadotSubDotDivisions: "子圓點的數量"
_imageEffector:
title: "特效"
addEffect: "新增特效"
discardChangesConfirm: "捨棄更改並退出嗎?"
_fxs:
chromaticAberration: "色差"
glitch: "異常雜訊效果"
mirror: "鏡像"
invert: "反轉色彩"
grayscale: "黑白"
colorClamp: "壓縮色彩"
colorClampAdvanced: "壓縮色彩(進階)"
distort: "變形"
threshold: "閾值"
zoomLines: "速度線"
stripe: "條紋"
polkadot: "波卡圓點"
checker: "棋盤格"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.6.1-alpha.0",
"version": "2025.6.1-alpha.4",
"codename": "nasubi",
"repository": {
"type": "git",
+10
View File
@@ -331,6 +331,16 @@ export class ChatService {
await redisPipeline.exec();
}
@bindThis
public async readAllChatMessages(
readerId: MiUser['id'],
): Promise<void> {
const redisPipeline = this.redisClient.pipeline();
// TODO: newUserChatMessageExists とか newRoomChatMessageExists も消したい(けどキーの列挙が必要になって面倒)
redisPipeline.del(`newChatMessagesExists:${readerId}`);
await redisPipeline.exec();
}
@bindThis
public findMessageById(messageId: MiChatMessage['id']) {
return this.chatMessagesRepository.findOneBy({ id: messageId });
+3 -3
View File
@@ -803,14 +803,14 @@ export class DriveService {
await Promise.all(promises);
}
this.deletePostProcess(file, isExpired, deleter);
await this.deletePostProcess(file, isExpired, deleter);
}
@bindThis
private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
// リモートファイル期限切れ削除後は直リンクにする
if (isExpired && file.userHost !== null && file.uri != null) {
this.driveFilesRepository.update(file.id, {
await this.driveFilesRepository.update(file.id, {
isLink: true,
url: file.uri,
thumbnailUrl: null,
@@ -822,7 +822,7 @@ export class DriveService {
webpublicAccessKey: 'webpublic-' + randomUUID(),
});
} else {
this.driveFilesRepository.delete(file.id);
await this.driveFilesRepository.delete(file.id);
}
this.driveChart.update(file, false);
@@ -428,4 +428,5 @@ export * as 'chat/rooms/invitations/ignore' from './endpoints/chat/rooms/invitat
export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitations/inbox.js';
export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.js';
export * as 'chat/history' from './endpoints/chat/history.js';
export * as 'chat/read-all' from './endpoints/chat/read-all.js';
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
@@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ChatService } from '@/core/ChatService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['chat'],
requireCredential: true,
kind: 'write:chat',
errors: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
},
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.readAllChatMessages(me.id);
});
}
}
@@ -380,9 +380,7 @@ describe('User', () => {
strictEqual(followers.length, 1); // followed by Bob
await alice.client.request('i/delete-account', { password: alice.password });
// NOTE: user deletion query is slow
// FIXME: ensure user is removed successfully
await sleep(10000);
await sleep();
const following = await bob.client.request('users/following', { userId: bob.id });
strictEqual(following.length, 0); // no following relation
@@ -480,9 +478,7 @@ describe('User', () => {
strictEqual(followers.length, 1); // followed by Bob
await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
// NOTE: user deletion query is slow
// FIXME: ensure user is removed successfully
await sleep(10000);
await sleep();
const following = await bob.client.request('users/following', { userId: bob.id });
strictEqual(following.length, 0); // no following relation
@@ -51,7 +51,10 @@ if (props.fileId) {
}
function selectButton(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target).then(async (file) => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
}).then(async (file) => {
if (!file) return;
if (props.validate && !await props.validate(file)) return;
@@ -16,22 +16,54 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root" class="_gaps">
<div v-for="[k, v] in Object.entries(fx.params)" :key="k">
<MkSwitch v-if="v.type === 'boolean'" v-model="layer.params[k]">
<MkSwitch
v-if="v.type === 'boolean'"
v-model="layer.params[k]"
>
<template #label>{{ k }}</template>
</MkSwitch>
<MkRange v-else-if="v.type === 'number'" v-model="layer.params[k]" continuousUpdate :min="v.min" :max="v.max" :step="v.step">
<MkRange
v-else-if="v.type === 'number'"
v-model="layer.params[k]"
continuousUpdate
:min="v.min"
:max="v.max"
:step="v.step"
@thumbDoubleClicked="() => {
if (fx.params[k].default != null) {
layer.params[k] = fx.params[k].default;
} else {
layer.params[k] = v.min;
}
}"
>
<template #label>{{ k }}</template>
</MkRange>
<MkRadios v-else-if="v.type === 'number:enum'" v-model="layer.params[k]">
<MkRadios
v-else-if="v.type === 'number:enum'"
v-model="layer.params[k]"
>
<template #label>{{ k }}</template>
<option v-for="item in v.enum" :value="item.value">{{ item.label }}</option>
</MkRadios>
<div v-else-if="v.type === 'seed'">
<MkRange v-model="layer.params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">
<MkRange
v-model="layer.params[k]"
continuousUpdate
type="number"
:min="0"
:max="10000"
:step="1"
>
<template #label>{{ k }}</template>
</MkRange>
</div>
<MkInput v-else-if="v.type === 'color'" :modelValue="`#${(layer.params[k][0] * 255).toString(16).padStart(2, '0')}${(layer.params[k][1] * 255).toString(16).padStart(2, '0')}${(layer.params[k][2] * 255).toString(16).padStart(2, '0')}`" type="color" @update:modelValue="v => { const c = v.slice(1).match(/.{2}/g)?.map(x => parseInt(x, 16) / 255); if (c) layer.params[k] = c; }">
<MkInput
v-else-if="v.type === 'color'"
:modelValue="getHex(layer.params[k])"
type="color"
@update:modelValue="v => { const c = getRgb(v); if (c != null) layer.params[k] = c; }"
>
<template #label>{{ k }}</template>
</MkInput>
</div>
@@ -40,22 +72,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue';
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { i18n } from '@/i18n.js';
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import FormSlot from '@/components/form/slot.vue';
import MkPositionSelector from '@/components/MkPositionSelector.vue';
import * as os from '@/os.js';
import { selectFile } from '@/utility/drive.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js';
import { FXS } from '@/utility/image-effector/fxs.js';
const layer = defineModel<ImageEffectorLayer>('layer', { required: true });
@@ -69,6 +93,24 @@ const emit = defineEmits<{
(e: 'swapUp'): void;
(e: 'swapDown'): void;
}>();
function getHex(c: [number, number, number]) {
return `#${c.map(x => (x * 255).toString(16).padStart(2, '0')).join('')}`;
}
function getRgb(hex: string | number): [number, number, number] | null {
if (
typeof hex === 'number' ||
typeof hex !== 'string' ||
!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)
) {
return null;
}
const m = hex.slice(1).match(/[0-9a-fA-F]{2}/g);
if (m == null) return [0, 0, 0];
return m.map(x => parseInt(x, 16) / 255) as [number, number, number];
}
</script>
<style module>
@@ -77,6 +77,7 @@ const dialog = useTemplateRef('dialog');
async function cancel() {
if (layers.length > 0) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts._imageEffector.discardChangesConfirm,
});
if (canceled) return;
@@ -132,7 +133,7 @@ function onLayerDelete(layer: ImageEffectorLayer) {
const canvasEl = useTemplateRef('canvasEl');
let renderer: ImageEffector | null = null;
let renderer: ImageEffector<typeof FXS> | null = null;
let imageBitmap: ImageBitmap | null = null;
onMounted(async () => {
@@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')">
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }">
<MkModal ref="modal" v-slot="{ type }" :preferType="deviceKind === 'smartphone' ? 'drawer' : 'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')">
<div ref="rootEl" :class="[$style.root, type === 'drawer' ? $style.asDrawer : null]" :style="{ width: type === 'drawer' ? '' : `${width}px`, height: type === 'drawer' ? '' : `min(${height}px, 100%)` }">
<div :class="$style.header">
<button v-if="withCloseButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="emit('close')"><i class="ti ti-x"></i></button>
<span :class="$style.title">
@@ -30,6 +30,7 @@ import { onMounted, onUnmounted, useTemplateRef, ref } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n';
import { deviceKind } from '@/utility/device-kind.js';
const props = withDefaults(defineProps<{
withOkButton?: boolean;
@@ -85,6 +86,19 @@ defineExpose({
@media (max-width: 500px) {
--root-margin: 16px;
}
&.asDrawer {
height: calc(100dvh - 30px);
border-radius: 0;
.body {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
.footer {
padding-bottom: max(12px, env(safe-area-inset-bottom, 0px));
}
}
}
.header {
+81 -13
View File
@@ -72,24 +72,29 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<div v-if="uploader.items.value.length > 0" style="padding: 12px;">
<MkTip k="postFormUploader">
{{ i18n.ts._postForm.uploaderTip }}
</MkTip>
<MkUploaderItems :items="uploader.items.value" @showMenu="(item, ev) => showPerUploadItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerUploadItemMenuViaContextmenu(item, ev)"/>
</div>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
<div v-if="showingOptions" style="padding: 8px 16px;">
</div>
<footer :class="$style.footer">
<div :class="$style.footerLeft">
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.upload + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromPc"><i class="ti ti-photo-plus"></i></button>
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.fromDrive + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromDrive"><i class="ti ti-cloud-download"></i></button>
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
<button v-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-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-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
</div>
<div :class="$style.footerRight">
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
<!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button>-->
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
</div>
</footer>
<datalist id="hashtags">
@@ -105,10 +110,12 @@ import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
import { toASCII } from 'punycode.js';
import { host, url } from '@@/js/config.js';
import MkUploaderItems from './MkUploaderItems.vue';
import type { ShallowRef } from 'vue';
import type { PostFormProps } from '@/types/post-form.js';
import type { MenuItem } from '@/types/menu.js';
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
import type { UploaderItem } from '@/composables/use-uploader.js';
import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
import XTextCounter from '@/components/MkPostForm.TextCounter.vue';
@@ -120,7 +127,7 @@ import { formatTimeString } from '@/utility/format-time-string.js';
import { Autocomplete } from '@/utility/autocomplete.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { selectFiles } from '@/utility/drive.js';
import { chooseDriveFile } from '@/utility/drive.js';
import { store } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
@@ -138,6 +145,7 @@ import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js';
import { globalEvents } from '@/events.js';
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
import { useUploader } from '@/composables/use-uploader.js';
const $i = ensureSignin();
@@ -201,6 +209,15 @@ const justEndedComposition = ref(false);
const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote);
const postFormActions = getPluginHandlers('post_form_action');
const uploader = useUploader({
multiple: true,
});
uploader.events.on('itemUploaded', ctx => {
files.value.push(ctx.item.uploaded!);
uploader.removeItem(ctx.item);
});
const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : '';
@@ -258,10 +275,11 @@ const cwTextLength = computed((): number => {
const maxCwTextLength = 100;
const canPost = computed((): boolean => {
return !props.mock && !posting.value && !posted.value &&
return !props.mock && !posting.value && !posted.value && !uploader.uploading.value && (uploader.items.value.length === 0 || uploader.readyForUpload.value) &&
(
1 <= textLength.value ||
1 <= files.value.length ||
1 <= uploader.items.value.length ||
poll.value != null ||
renoteTargetNote.value != null ||
quoteId.value != null
@@ -434,13 +452,20 @@ function focus() {
}
}
function chooseFileFrom(ev) {
function chooseFileFromPc(ev: MouseEvent) {
if (props.mock) return;
selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
for (const file of files_) {
files.value.push(file);
}
os.chooseFileFromPc({ multiple: true }).then(files => {
if (files.length === 0) return;
uploader.addFiles(files);
});
}
function chooseFileFromDrive(ev: MouseEvent) {
if (props.mock) return;
chooseDriveFile({ multiple: true }).then(driveFiles => {
files.value.push(...driveFiles);
});
}
@@ -567,6 +592,11 @@ function showOtherSettings() {
toggleReactionAcceptance();
},
}, { type: 'divider' }, {
type: 'switch',
icon: 'ti ti-eye',
text: i18n.ts.preview,
ref: showPreview,
}, {
icon: 'ti ti-trash',
text: i18n.ts.reset,
danger: true,
@@ -793,6 +823,15 @@ function isAnnoying(text: string): boolean {
text.includes('$[position');
}
async function uploadFiles() {
await uploader.upload();
for (const uploadedItem of uploader.items.value.filter(x => x.uploaded != null)) {
files.value.push(uploadedItem.uploaded!);
uploader.removeItem(uploadedItem);
}
}
async function post(ev?: MouseEvent) {
if (ev) {
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
@@ -836,6 +875,10 @@ async function post(ev?: MouseEvent) {
}
}
if (uploader.items.value.some(x => x.uploaded == null)) {
await uploadFiles();
}
let postData = {
text: text.value === '' ? null : text.value,
fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
@@ -1039,6 +1082,16 @@ function openAccountMenu(ev: MouseEvent) {
}, ev);
}
function showPerUploadItemMenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.contextMenu(menu, ev);
}
onMounted(() => {
if (props.autofocus) {
focus();
@@ -1107,8 +1160,23 @@ onMounted(() => {
});
});
async function canClose() {
if (!uploader.allItemsUploaded.value) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts._postForm.quitInspiteOfThereAreUnuploadedFilesConfirm,
okText: i18n.ts.yes,
cancelText: i18n.ts.no,
});
if (canceled) return false;
}
return true;
}
defineExpose({
clear,
canClose,
});
</script>
@@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkModal
ref="modal"
:preferType="'dialog'"
@click="modal?.close()"
@click="_close()"
@closed="onModalClosed()"
@esc="modal?.close()"
@esc="_close()"
>
<MkPostForm
ref="form"
@@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
autofocus
freezeAfterPosted
@posted="onPosted"
@cancel="modal?.close()"
@esc="modal?.close()"
@cancel="_close()"
@esc="_close()"
/>
</MkModal>
</template>
@@ -43,6 +43,7 @@ const emit = defineEmits<{
}>();
const modal = useTemplateRef('modal');
const form = useTemplateRef('form');
function onPosted() {
modal.value?.close({
@@ -50,6 +51,12 @@ function onPosted() {
});
}
async function _close() {
const canClose = await form.value?.canClose();
if (!canClose) return;
modal.value?.close();
}
function onModalClosed() {
emit('closed');
}
+52 -7
View File
@@ -12,10 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<slot name="prefix"></slot>
<div ref="containerEl" class="container">
<div class="track">
<div class="highlight right" :style="{ width: ((steppedRawValue - minRatio) * 100) + '%', left: (Math.abs(Math.min(0, min)) / (max + Math.abs(Math.min(0, min)))) * 100 + '%' }">
<div class="highlight right" :style="{ width: rightTrackWidth, left: rightTrackPosition }">
<div class="shine right"></div>
</div>
<div class="highlight left" :style="{ width: ((minRatio - steppedRawValue) * 100) + '%', left: (steppedRawValue) * 100 + '%' }">
<div class="highlight left" :style="{ width: leftTrackWidth, left: leftTrackPosition }">
<div class="shine left"></div>
</div>
</div>
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
import { computed, defineAsyncComponent, onMounted, onUnmounted, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue';
import { isTouchUsing } from '@/utility/touch.js';
import * as os from '@/os.js';
@@ -58,13 +58,14 @@ const props = withDefaults(defineProps<{
continuousUpdate?: boolean;
}>(), {
step: 1,
textConverter: (v) => v.toString(),
textConverter: (v: number) => (Math.round(v * 1000) / 1000).toString(),
easing: false,
});
const emit = defineEmits<{
(ev: 'update:modelValue', value: number): void;
(ev: 'dragEnded', value: number): void;
(ev: 'thumbDoubleClicked'): void;
}>();
const containerEl = useTemplateRef('containerEl');
@@ -73,7 +74,24 @@ const thumbEl = useTemplateRef('thumbEl');
const maxRatio = computed(() => Math.abs(props.max) / (props.max + Math.abs(Math.min(0, props.min))));
const minRatio = computed(() => Math.abs(Math.min(0, props.min)) / (props.max + Math.abs(Math.min(0, props.min))));
const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
const rightTrackWidth = computed(() => {
return Math.max(0, (steppedRawValue.value - minRatio.value) * 100) + '%';
});
const leftTrackWidth = computed(() => {
return Math.max(0, (minRatio.value - steppedRawValue.value) * 100) + '%';
});
const rightTrackPosition = computed(() => {
return (Math.abs(Math.min(0, props.min)) / (props.max + Math.abs(Math.min(0, props.min)))) * 100 + '%';
});
const leftTrackPosition = computed(() => {
return (Math.min(minRatio.value, steppedRawValue.value) * 100) + '%';
});
const calcRawValue = (value: number) => {
return (value - props.min) / (props.max - props.min);
};
const rawValue = ref(calcRawValue(props.modelValue));
const steppedRawValue = computed(() => {
if (props.step) {
const step = props.step / (props.max - props.min);
@@ -103,6 +121,11 @@ const calcThumbPosition = () => {
}
};
watch([steppedRawValue, containerEl], calcThumbPosition);
watch(() => props.modelValue, (newVal) => {
const newRawValue = calcRawValue(newVal);
if (rawValue.value === newRawValue) return;
rawValue.value = newRawValue;
});
let ro: ResizeObserver | undefined;
@@ -128,6 +151,12 @@ const steps = computed(() => {
const tooltipForDragShowing = ref(false);
const tooltipForHoverShowing = ref(false);
onBeforeUnmount(() => {
//
tooltipForDragShowing.value = false;
tooltipForHoverShowing.value = false;
});
function onMouseenter() {
if (isTouchUsing) return;
@@ -138,7 +167,7 @@ function onMouseenter() {
text: computed(() => {
return props.textConverter(finalValue.value);
}),
targetElement: thumbEl,
targetElement: thumbEl.value ?? undefined,
}, {
closed: () => dispose(),
});
@@ -148,6 +177,8 @@ function onMouseenter() {
}, { once: true, passive: true });
}
let lastClickTime: number | null = null;
function onMousedown(ev: MouseEvent | TouchEvent) {
ev.preventDefault();
@@ -158,7 +189,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
text: computed(() => {
return props.textConverter(finalValue.value);
}),
targetElement: thumbEl,
targetElement: thumbEl.value ?? undefined,
}, {
closed: () => dispose(),
});
@@ -203,6 +234,20 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
window.addEventListener('touchmove', onDrag);
window.addEventListener('mouseup', onMouseup, { once: true });
window.addEventListener('touchend', onMouseup, { once: true });
if (lastClickTime == null) {
lastClickTime = Date.now();
return;
} else {
const now = Date.now();
if (now - lastClickTime < 300) { // 300ms
lastClickTime = null;
emit('thumbDoubleClicked');
return;
} else {
lastClickTime = now;
}
}
}
</script>
@@ -23,37 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._uploader.tip }}
</MkTip>
<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>
<MkUploaderItems :items="items" @showMenu="(item, ev) => showPerItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerItemMenuViaContextmenu(item, ev)"/>
<div v-if="props.multiple">
<MkButton style="margin: auto;" :iconOnly="true" rounded @click="chooseFile($event)"><i class="ti ti-plus"></i></MkButton>
@@ -69,8 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #footer>
<div class="_buttonsCenter">
<MkButton v-if="isUploading" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
<MkButton v-else-if="!firstUploadAttempted" primary rounded @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
<MkButton v-if="uploader.uploading.value" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
<MkButton v-else-if="!firstUploadAttempted" primary rounded :disabled="!uploader.readyForUpload.value" @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
<MkButton v-if="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>
@@ -80,57 +50,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, markRaw, onMounted, onUnmounted, ref, triggerRef, useTemplateRef, watch } from 'vue';
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-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 type { UploaderFeatures, UploaderItem } from '@/composables/use-uploader.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import MkButton from '@/components/MkButton.vue';
import bytes from '@/filters/bytes.js';
import MkSelect from '@/components/MkSelect.vue';
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';
import { useUploader } from '@/composables/use-uploader.js';
import MkUploaderItems from '@/components/MkUploaderItems.vue';
const $i = ensureSignin();
const COMPRESSION_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
'image/svg+xml',
];
const CROPPING_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
];
const IMAGE_EDITING_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
];
const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES;
const mimeTypeMap = {
'image/webp': 'webp',
'image/jpeg': 'jpg',
'image/png': 'png',
} as const;
const props = withDefaults(defineProps<{
files: File[];
folderId?: string | null;
multiple?: boolean;
features?: UploaderFeatures;
}>(), {
multiple: true,
});
@@ -141,30 +78,22 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
const items = ref<{
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: number;
compressedSize?: number | null;
preprocessedFile?: Blob | null;
file: File;
watermarkPresetId: string | null;
abort?: (() => void) | null;
}[]>([]);
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 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 canRetry = computed(() => firstUploadAttempted.value && uploader.readyForUpload.value);
const canDone = computed(() => items.value.some(item => item.uploaded != null));
const overallProgress = computed(() => {
const max = items.value.length;
@@ -177,27 +106,6 @@ const overallProgress = computed(() => {
return Math.round((v / max) * 100);
});
function getCompressionSettings(level: 0 | 1 | 2 | 3) {
if (level === 1) {
return {
maxWidth: 2000,
maxHeight: 2000,
};
} else if (level === 2) {
return {
maxWidth: 2000 * 0.75, // =1500
maxHeight: 2000 * 0.75, // =1500
};
} else if (level === 3) {
return {
maxWidth: 2000 * 0.75 * 0.75, // =1125
maxHeight: 2000 * 0.75 * 0.75, // =1125
};
} else {
return null;
}
}
watch(items, () => {
if (items.value.length === 0) {
emit('canceled');
@@ -220,11 +128,16 @@ async function cancel() {
});
if (canceled) return;
abortAll();
uploader.abortAll();
emit('canceled');
dialog.value?.close();
}
function upload() {
firstUploadAttempted.value = true;
uploader.upload();
}
async function abortWithConfirm() {
const { canceled } = await os.confirm({
type: 'question',
@@ -234,11 +147,11 @@ async function abortWithConfirm() {
});
if (canceled) return;
abortAll();
uploader.abortAll();
}
async function done() {
if (items.value.some(item => item.uploaded == null)) {
if (!uploader.allItemsUploaded.value) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts._uploader.doneConfirm,
@@ -252,364 +165,20 @@ async function done() {
dialog.value?.close();
}
function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
const menu: MenuItem[] = [];
menu.push({
icon: 'ti ti-cursor-text',
text: i18n.ts.rename,
action: async () => {
const { result, canceled } = await os.inputText({
type: 'text',
title: i18n.ts.rename,
placeholder: item.name,
default: item.name,
});
if (canceled) return;
if (result.trim() === '') return;
item.name = result;
},
});
if (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 (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 (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',
text: preset.name,
active: computed(() => item.watermarkPresetId === preset.id),
action: () => changeWatermarkPreset(preset.id),
})), {
type: 'divider',
}, {
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();
}
},
});
}
async function chooseFile(ev: MouseEvent) {
const newFiles = await os.chooseFileFromPc({ multiple: true });
uploader.addFiles(newFiles);
}
function showPerItemMenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
async function upload() { //
firstUploadAttempted.value = true;
items.value = items.value.map(item => ({
...item,
aborted: false,
uploadFailed: false,
uploading: false,
}));
for (const item of items.value.filter(item => item.uploaded == null)) {
// Array filter
if (item.aborted) {
continue;
}
item.uploadFailed = false;
item.uploading = true;
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
name: item.uploadName ?? item.name,
folderId: props.folderId,
onProgress: (progress) => {
if (item.progress == null) {
item.progress = { max: progress.total, value: progress.loaded };
} else {
item.progress.value = progress.loaded;
item.progress.max = progress.total;
}
},
});
item.abort = () => {
item.abort = null;
abort();
item.uploading = false;
item.uploadFailed = true;
};
await filePromise.then((file) => {
item.uploaded = file;
item.abort = null;
}).catch(err => {
item.uploadFailed = true;
item.progress = null;
if (!(err instanceof UploadAbortedError)) {
throw err;
}
}).finally(() => {
item.uploading = false;
});
}
function showPerItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
const menu = uploader.getMenu(item);
os.contextMenu(menu, ev);
}
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: prefer.s.defaultWatermarkPresetId,
file: markRaw(file),
};
items.value.push(item);
preprocess(item).then(() => {
triggerRef(items);
});
}
onMounted(() => {
for (const file of props.files) {
initializeFile(file);
}
});
onUnmounted(() => {
for (const item of items.value) {
URL.revokeObjectURL(item.thumbnail);
}
});
</script>
<style lang="scss" module>
@@ -631,127 +200,4 @@ onUnmounted(() => {
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>
@@ -0,0 +1,196 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root" class="_gaps_s">
<div
v-for="item in props.items"
:key="item.id"
v-panel
:class="[$style.item, { [$style.itemWaiting]: item.preprocessing, [$style.itemCompleted]: item.uploaded, [$style.itemFailed]: item.uploadFailed }]"
:style="{ '--p': item.progress != null ? `${item.progress.value / item.progress.max * 100}%` : '0%' }"
@contextmenu.prevent.stop="onContextmenu(item, $event)"
>
<div :class="$style.itemInner">
<div :class="$style.itemActionWrapper">
<MkButton :iconOnly="true" rounded @click="emit('showMenu', item, $event)"><i class="ti ti-dots"></i></MkButton>
</div>
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ item.thumbnail })` }" @click="onThumbnailClick(item, $event)"></div>
<div :class="$style.itemBody">
<div><MkCondensedLine :minScale="2 / 3">{{ item.name }}</MkCondensedLine></div>
<div :class="$style.itemInfo">
<span>{{ item.file.type }}</span>
<span v-if="item.compressedSize">({{ i18n.tsx._uploader.compressedToX({ x: bytes(item.compressedSize) }) }} = {{ i18n.tsx._uploader.savedXPercent({ x: Math.round((1 - item.compressedSize / item.file.size) * 100) }) }})</span>
<span v-else>{{ bytes(item.file.size) }}</span>
</div>
<div>
</div>
</div>
<div :class="$style.itemIconWrapper">
<MkSystemIcon v-if="item.uploading" :class="$style.itemIcon" type="waiting"/>
<MkSystemIcon v-else-if="item.uploaded" :class="$style.itemIcon" type="success"/>
<MkSystemIcon v-else-if="item.uploadFailed" :class="$style.itemIcon" type="error"/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { isLink } from '@@/js/is-link.js';
import type { UploaderItem } from '@/composables/use-uploader.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import bytes from '@/filters/bytes.js';
const props = defineProps<{
items: UploaderItem[];
}>();
const emit = defineEmits<{
(ev: 'showMenu', item: UploaderItem, event: MouseEvent): void;
(ev: 'showMenuViaContextmenu', item: UploaderItem, event: MouseEvent): void;
}>();
function onContextmenu(item: UploaderItem, ev: MouseEvent) {
if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== '') return;
emit('showMenuViaContextmenu', item, ev);
}
function onThumbnailClick(item: UploaderItem, ev: MouseEvent) {
// TODO: preview when item is image
}
</script>
<style lang="scss" module>
.root {
position: relative;
}
.item {
position: relative;
border-radius: 10px;
overflow: clip;
&::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: var(--p);
height: 100%;
background: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
transition: width 0.2s ease, left 0.2s ease;
}
&.itemWaiting {
&::after {
--c: color(from var(--MI_THEME-accent) srgb r g b / 0.25);
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(-45deg, transparent 25%, var(--c) 25%,var(--c) 50%, transparent 50%, transparent 75%, var(--c) 75%, var(--c));
background-size: 25px 25px;
animation: stripe .8s infinite linear;
}
}
&.itemCompleted {
&::before {
left: 100%;
width: var(--p);
}
.itemBody {
color: var(--MI_THEME-accent);
}
}
&.itemFailed {
.itemBody {
color: var(--MI_THEME-error);
}
}
}
@keyframes stripe {
0% { background-position-x: 0; }
100% { background-position-x: -25px; }
}
.itemInner {
position: relative;
z-index: 1;
padding: 8px 16px;
display: flex;
align-items: center;
gap: 12px;
}
.itemThumbnail {
width: 70px;
height: 70px;
background-color: var(--MI_THEME-bg);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
border-radius: 6px;
}
.itemBody {
flex: 1;
min-width: 0;
}
.itemInfo {
opacity: 0.7;
margin-top: 4px;
font-size: 90%;
display: flex;
gap: 8px;
}
.itemIcon {
width: 35px;
}
@container (max-width: 500px) {
.itemInner {
flex-direction: column;
gap: 8px;
}
.itemBody {
font-size: 90%;
text-align: center;
width: 100%;
min-width: 0;
}
.itemActionWrapper {
position: absolute;
top: 8px;
left: 8px;
}
.itemInfo {
justify-content: center;
}
.itemIconWrapper {
position: absolute;
top: 8px;
right: 8px;
}
}
</style>
@@ -262,10 +262,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue';
import { ref, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import type { WatermarkPreset } from '@/utility/watermark.js';
import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -275,11 +275,10 @@ 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';
const layer = defineModel<WatermarkPreset['layers'][number]>('layer', { required: true });
const driveFile = ref();
const driveFile = ref<Misskey.entities.DriveFile | null>(null);
const driveFileError = ref(false);
onMounted(async () => {
if (layer.value.type === 'image' && layer.value.imageId != null) {
@@ -294,7 +293,15 @@ onMounted(async () => {
});
function chooseFile(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then((file) => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
label: i18n.ts.selectFile,
features: {
watermark: false,
},
}).then((file) => {
if (layer.value.type !== 'image') return;
if (!file.type.startsWith('image')) {
os.alert({
type: 'warning',
@@ -124,7 +124,7 @@ function createStripeLayer(): WatermarkPreset['layers'][number] {
angle: 0.5,
frequency: 10,
threshold: 0.1,
black: false,
color: [1, 1, 1],
opacity: 0.75,
};
}
@@ -140,7 +140,7 @@ function createPolkadotLayer(): WatermarkPreset['layers'][number] {
majorOpacity: 0.75,
minorOpacity: 0.5,
minorDivisions: 4,
black: false,
color: [1, 1, 1],
opacity: 0.75,
};
}
@@ -151,7 +151,7 @@ function createCheckerLayer(): WatermarkPreset['layers'][number] {
type: 'checker',
angle: 0.5,
scale: 3,
black: false,
color: [1, 1, 1],
opacity: 0.75,
};
}
@@ -177,6 +177,7 @@ const dialog = useTemplateRef('dialog');
async function cancel() {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts._watermarkEditor.quitWithoutSaveConfirm,
});
if (canceled) return;
@@ -7,7 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="!store.r.tips.value[props.k]" :class="[$style.root, { [$style.warn]: warn }]" class="_selectable _gaps_s">
<div style="font-weight: bold;"><i class="ti ti-bulb"></i> {{ i18n.ts.tip }}:</div>
<div><slot></slot></div>
<MkButton primary rounded small @click="closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
<div>
<MkButton inline primary rounded small @click="_closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
<button class="_button" style="padding: 8px; margin-left: 4px;" @click="showMenu"><i class="ti ti-dots"></i></button>
</div>
</div>
</template>
@@ -15,19 +18,30 @@ SPDX-License-Identifier: AGPL-3.0-only
import { i18n } from '@/i18n.js';
import { store } from '@/store.js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { TIPS, hideAllTips, closeTip } from '@/tips.js';
const props = withDefaults(defineProps<{
k: keyof (typeof store['s']['tips']);
k: typeof TIPS[number];
warn?: boolean;
}>(), {
warn: false,
});
function closeTip() {
store.set('tips', {
...store.r.tips.value,
[props.k]: true,
});
function _closeTip() {
closeTip(props.k);
}
function showMenu(ev: MouseEvent) {
os.popupMenu([{
icon: 'ti ti-bulb-off',
text: i18n.ts.hideAllTips,
danger: true,
action: () => {
hideAllTips();
os.success();
},
}], ev.currentTarget ?? ev.target);
}
</script>
@@ -25,7 +25,14 @@ export function useChartTooltip(opts: { position: 'top' | 'middle' } = { positio
series: tooltipSeries,
}, {});
function windowTouchendHandler() {
tooltipShowing.value = false;
}
window.addEventListener('touchend', windowTouchendHandler, { passive: true });
onUnmounted(() => {
window.removeEventListener('touchend', windowTouchendHandler);
disposeTooltipComponent();
});
@@ -0,0 +1,576 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Misskey from 'misskey-js';
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
import isAnimated from 'is-file-animated';
import { EventEmitter } from 'eventemitter3';
import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import { genId } from '@/utility/id.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { isWebpSupported } from '@/utility/isWebpSupported.js';
import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
import * as os from '@/os.js';
import { ensureSignin } from '@/i.js';
import { WatermarkRenderer } from '@/utility/watermark.js';
export type UploaderFeatures = {
effect?: boolean;
watermark?: boolean;
crop?: boolean;
};
const THUMBNAIL_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
'image/svg+xml',
];
const IMAGE_COMPRESSION_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
'image/svg+xml',
];
const CROPPING_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
];
const IMAGE_EDITING_SUPPORTED_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
];
const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES;
const IMAGE_PREPROCESS_NEEDED_TYPES = [
...WATERMARK_SUPPORTED_TYPES,
...IMAGE_COMPRESSION_SUPPORTED_TYPES,
...CROPPING_SUPPORTED_TYPES,
...IMAGE_EDITING_SUPPORTED_TYPES,
];
const mimeTypeMap = {
'image/webp': 'webp',
'image/jpeg': 'jpg',
'image/png': 'png',
} as const;
export type UploaderItem = {
id: string;
name: string;
uploadName?: string;
progress: { max: number; value: number } | null;
thumbnail: string | null;
preprocessing: boolean;
uploading: boolean;
uploaded: Misskey.entities.DriveFile | null;
uploadFailed: boolean;
aborted: boolean;
compressionLevel: 0 | 1 | 2 | 3;
compressedSize?: number | null;
preprocessedFile?: Blob | null;
file: File;
watermarkPresetId: string | null;
isSensitive?: boolean;
abort?: (() => void) | null;
};
function getCompressionSettings(level: 0 | 1 | 2 | 3) {
if (level === 1) {
return {
maxWidth: 2000,
maxHeight: 2000,
};
} else if (level === 2) {
return {
maxWidth: 2000 * 0.75, // =1500
maxHeight: 2000 * 0.75, // =1500
};
} else if (level === 3) {
return {
maxWidth: 2000 * 0.75 * 0.75, // =1125
maxHeight: 2000 * 0.75 * 0.75, // =1125
};
} else {
return null;
}
}
export function useUploader(options: {
folderId?: string | null;
multiple?: boolean;
features?: UploaderFeatures;
} = {}) {
const $i = ensureSignin();
const events = new EventEmitter<{
'itemUploaded': (ctx: { item: UploaderItem; }) => void;
}>();
const uploaderFeatures = computed<Required<UploaderFeatures>>(() => {
return {
effect: options.features?.effect ?? true,
watermark: options.features?.watermark ?? true,
crop: options.features?.crop ?? true,
};
});
const items = ref<UploaderItem[]>([]);
function initializeFile(file: File) {
const id = genId();
const filename = file.name ?? 'untitled';
const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : '';
items.value.push({
id,
name: prefer.s.keepOriginalFilename ? filename : id + extension,
progress: null,
thumbnail: THUMBNAIL_SUPPORTED_TYPES.includes(file.type) ? window.URL.createObjectURL(file) : null,
preprocessing: false,
uploading: false,
aborted: false,
uploaded: null,
uploadFailed: false,
compressionLevel: prefer.s.defaultImageCompressionLevel,
watermarkPresetId: uploaderFeatures.value.watermark ? prefer.s.defaultWatermarkPresetId : null,
file: markRaw(file),
});
const reactiveItem = items.value.at(-1)!;
preprocess(reactiveItem).then(() => {
triggerRef(items);
});
}
function addFiles(newFiles: File[]) {
for (const file of newFiles) {
initializeFile(file);
}
}
function removeItem(item: UploaderItem) {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
items.value.splice(items.value.indexOf(item), 1);
}
function getMenu(item: UploaderItem): MenuItem[] {
const menu: MenuItem[] = [];
if (
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({
icon: 'ti ti-forms',
text: i18n.ts.rename,
action: async () => {
const { result, canceled } = await os.inputText({
type: 'text',
title: i18n.ts.rename,
placeholder: item.name,
default: item.name,
});
if (canceled) return;
if (result.trim() === '') return;
item.name = result;
},
}, {
type: 'switch',
text: i18n.ts.sensitive,
icon: 'ti ti-eye-exclamation',
ref: computed({
get: () => item.isSensitive ?? false,
set: (value) => item.isSensitive = value,
}),
}, {
type: 'divider',
});
}
if (
uploaderFeatures.value.crop &&
CROPPING_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({
icon: 'ti ti-crop',
text: i18n.ts.cropImage,
action: async () => {
const cropped = await os.cropImageFile(item.file, { aspectRatio: null });
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
items.value.splice(items.value.indexOf(item), 1, {
...item,
file: markRaw(cropped),
thumbnail: window.URL.createObjectURL(cropped),
});
const reactiveItem = items.value.find(x => x.id === item.id)!;
preprocess(reactiveItem).then(() => {
triggerRef(items);
});
},
});
}
if (
uploaderFeatures.value.effect &&
IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
menu.push({
icon: 'ti ti-sparkles',
text: i18n.ts._imageEffector.title + ' (BETA)',
action: async () => {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImageEffectorDialog.vue').then(x => x.default), {
image: item.file,
}, {
ok: (file) => {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
items.value.splice(items.value.indexOf(item), 1, {
...item,
file: markRaw(file),
thumbnail: window.URL.createObjectURL(file),
});
const reactiveItem = items.value.find(x => x.id === item.id)!;
preprocess(reactiveItem).then(() => {
triggerRef(items);
});
},
closed: () => dispose(),
});
},
});
}
if (
uploaderFeatures.value.watermark &&
WATERMARK_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
function changeWatermarkPreset(presetId: string | null) {
item.watermarkPresetId = presetId;
preprocess(item).then(() => {
triggerRef(items);
});
}
menu.push({
icon: 'ti ti-copyright',
text: i18n.ts.watermark,
caption: computed(() => item.watermarkPresetId == null ? null : prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId)?.name),
type: 'parent',
children: [{
type: 'radioOption',
text: i18n.ts.none,
active: computed(() => item.watermarkPresetId == null),
action: () => changeWatermarkPreset(null),
}, {
type: 'divider',
}, ...prefer.s.watermarkPresets.map(preset => ({
type: 'radioOption' as const,
text: preset.name,
active: computed(() => item.watermarkPresetId === preset.id),
action: () => changeWatermarkPreset(preset.id),
})), ...(prefer.s.watermarkPresets.length > 0 ? [{
type: 'divider' as const,
}] : []), {
type: 'button',
icon: 'ti ti-plus',
text: i18n.ts.add,
action: async () => {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkWatermarkEditorDialog.vue').then(x => x.default), {
image: item.file,
}, {
ok: (preset) => {
prefer.commit('watermarkPresets', [...prefer.s.watermarkPresets, preset]);
changeWatermarkPreset(preset.id);
},
closed: () => dispose(),
});
},
}],
});
}
if (
IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
) {
function changeCompressionLevel(level: 0 | 1 | 2 | 3) {
item.compressionLevel = level;
preprocess(item).then(() => {
triggerRef(items);
});
}
menu.push({
icon: 'ti ti-leaf',
text: computed(() => {
let text = i18n.ts.compress;
if (item.compressionLevel === 0 || item.compressionLevel == null) {
text += `: ${i18n.ts.none}`;
} else if (item.compressionLevel === 1) {
text += `: ${i18n.ts.low}`;
} else if (item.compressionLevel === 2) {
text += `: ${i18n.ts.medium}`;
} else if (item.compressionLevel === 3) {
text += `: ${i18n.ts.high}`;
}
return text;
}),
type: 'parent',
children: [{
type: 'radioOption',
text: i18n.ts.none,
active: computed(() => item.compressionLevel === 0 || item.compressionLevel == null),
action: () => changeCompressionLevel(0),
}, {
type: 'divider',
}, {
type: 'radioOption',
text: i18n.ts.low,
active: computed(() => item.compressionLevel === 1),
action: () => changeCompressionLevel(1),
}, {
type: 'radioOption',
text: i18n.ts.medium,
active: computed(() => item.compressionLevel === 2),
action: () => changeCompressionLevel(2),
}, {
type: 'radioOption',
text: i18n.ts.high,
active: computed(() => item.compressionLevel === 3),
action: () => changeCompressionLevel(3),
}],
});
}
if (!item.preprocessing && !item.uploading && !item.uploaded) {
menu.push({
type: 'divider',
}, {
icon: 'ti ti-upload',
text: i18n.ts.upload,
action: () => {
uploadOne(item);
},
}, {
icon: 'ti ti-x',
text: i18n.ts.remove,
danger: true,
action: () => {
removeItem(item);
},
});
} else if (item.uploading) {
menu.push({
type: 'divider',
}, {
icon: 'ti ti-cloud-pause',
text: i18n.ts.abort,
danger: true,
action: () => {
if (item.abort != null) {
item.abort();
}
},
});
}
return menu;
}
async function uploadOne(item: UploaderItem): Promise<void> {
item.uploadFailed = false;
item.uploading = true;
const { filePromise, abort } = uploadFile(item.preprocessedFile ?? item.file, {
name: item.uploadName ?? item.name,
folderId: options.folderId,
isSensitive: item.isSensitive ?? false,
onProgress: (progress) => {
if (item.progress == null) {
item.progress = { max: progress.total, value: progress.loaded };
} else {
item.progress.value = progress.loaded;
item.progress.max = progress.total;
}
},
});
item.abort = () => {
item.abort = null;
abort();
item.uploading = false;
item.uploadFailed = true;
};
await filePromise.then((file) => {
item.uploaded = file;
item.abort = null;
events.emit('itemUploaded', { item });
}).catch(err => {
item.uploadFailed = true;
item.progress = null;
if (!(err instanceof UploadAbortedError)) {
throw err;
}
}).finally(() => {
item.uploading = false;
});
}
async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
items.value = items.value.map(item => ({
...item,
aborted: false,
uploadFailed: false,
uploading: false,
}));
for (const item of items.value.filter(item => item.uploaded == null)) {
// アップロード処理途中で値が変わる場合(途中で全キャンセルされたりなど)もあるので、Array filterではなくここでチェック
if (item.aborted) {
continue;
}
await uploadOne(item);
}
}
function abortAll() {
for (const item of items.value) {
if (item.uploaded != null) {
continue;
}
if (item.abort != null) {
item.abort();
}
item.aborted = true;
item.uploadFailed = true;
}
}
async function preprocess(item: UploaderItem): Promise<void> {
item.preprocessing = true;
try {
if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
await preprocessForImage(item);
}
} catch (err) {
console.error('Failed to preprocess image', err);
// nop
}
item.preprocessing = false;
}
async function preprocessForImage(item: UploaderItem): Promise<void> {
const imageBitmap = await window.createImageBitmap(item.file);
let preprocessedFile: Blob | File = item.file;
const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(preprocessedFile.type);
const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId);
if (needsWatermark && preset != null) {
const canvas = window.document.createElement('canvas');
const renderer = new WatermarkRenderer({
canvas: canvas,
renderWidth: imageBitmap.width,
renderHeight: imageBitmap.height,
image: imageBitmap,
});
await renderer.setLayers(preset.layers);
renderer.render();
preprocessedFile = await new Promise<Blob>((resolve) => {
canvas.toBlob((blob) => {
if (blob == null) {
throw new Error('Failed to convert canvas to blob');
}
resolve(blob);
renderer.destroy();
}, 'image/png');
});
}
const compressionSettings = getCompressionSettings(item.compressionLevel);
const needsCompress = item.compressionLevel !== 0 && compressionSettings && IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(preprocessedFile.type) && !(await isAnimated(preprocessedFile));
if (needsCompress) {
const config = {
mimeType: isWebpSupported() ? 'image/webp' : 'image/jpeg',
maxWidth: compressionSettings.maxWidth,
maxHeight: compressionSettings.maxHeight,
quality: isWebpSupported() ? 0.85 : 0.8,
};
try {
const result = await readAndCompressImage(preprocessedFile, config);
if (result.size < preprocessedFile.size || preprocessedFile.type === 'image/webp') {
// The compression may not always reduce the file size
// (and WebP is not browser safe yet)
preprocessedFile = result;
item.compressedSize = result.size;
item.uploadName = preprocessedFile.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
}
} catch (err) {
console.error('Failed to resize image', err);
}
} else {
item.compressedSize = null;
item.uploadName = item.name;
}
imageBitmap.close();
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null;
item.preprocessedFile = markRaw(preprocessedFile);
}
onUnmounted(() => {
for (const item of items.value) {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
}
});
return {
items,
addFiles,
removeItem,
abortAll,
upload,
getMenu,
uploading: computed(() => items.value.some(item => item.uploading)),
readyForUpload: computed(() => items.value.length > 0 && items.value.some(item => item.uploaded == null) && !items.value.some(item => item.uploading || item.preprocessing)),
allItemsUploaded: computed(() => items.value.every(item => item.uploaded != null)),
events,
};
}
+3
View File
@@ -13,6 +13,7 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js';
import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue';
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
@@ -836,6 +837,7 @@ export function launchUploader(
options?: {
folderId?: string | null;
multiple?: boolean;
features?: UploaderDialogFeatures;
},
): Promise<Misskey.entities.DriveFile[]> {
return new Promise(async (res, rej) => {
@@ -844,6 +846,7 @@ export function launchUploader(
files: markRaw(files),
folderId: options?.folderId,
multiple: options?.multiple,
features: options?.features,
}, {
done: driveFiles => {
if (driveFiles.length === 0) return rej();
@@ -174,7 +174,10 @@ function setupGrid(): GridSetting {
{
bindTo: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required],
async customValueEditor(row, col, value, cellElement) {
const file = await selectFile(cellElement);
const file = await selectFile({
anchorElement: cellElement,
multiple: false,
});
gridItems.value[row.index].url = file.url;
gridItems.value[row.index].fileId = file.id;
@@ -188,7 +188,10 @@ async function archive() {
}
function setBannerImage(evt) {
selectFile(evt.currentTarget ?? evt.target, null).then(file => {
selectFile({
anchorElement: evt.currentTarget ?? evt.target,
multiple: false,
}).then(file => {
bannerId.value = file.id;
});
}
@@ -168,7 +168,11 @@ function onKeydown(ev: KeyboardEvent) {
}
function chooseFile(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then(selectedFile => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
label: i18n.ts.selectFile,
}).then(selectedFile => {
file.value = selectedFile;
});
}
@@ -214,7 +214,10 @@ const menu = (ev: MouseEvent) => {
icon: 'ti ti-upload',
text: i18n.ts.import,
action: async () => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('admin/emoji/import-zip', {
fileId: file.id,
})
@@ -121,7 +121,10 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? props.emoji.url : null);
async function changeImage(ev: Event) {
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
file.value = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
const candidate = file.value.name.replace(/\.(.+)$/, '');
if (candidate.match(/^[a-z0-9_]+$/)) {
name.value = candidate;
+7 -4
View File
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="name">{{ file.name }}</div>
<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button>
</div>
<MkButton primary @click="selectFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</MkButton>
<MkButton primary @click="chooseFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</MkButton>
</div>
<MkSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</MkSwitch>
@@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import { selectFiles } from '@/utility/drive.js';
import { selectFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';
@@ -63,8 +63,11 @@ const description = ref<string | null>(null);
const title = ref<string | null>(null);
const isSensitive = ref(false);
function selectFile(evt) {
selectFiles(evt.currentTarget ?? evt.target, null).then(selected => {
function chooseFile(evt) {
selectFile({
anchorElement: evt.currentTarget ?? evt.target,
multiple: true,
}).then(selected => {
files.value = files.value.concat(selected);
});
}
@@ -205,7 +205,10 @@ async function add() {
}
function setEyeCatchingImage(img: Event) {
selectFile(img.currentTarget ?? img.target, null).then(file => {
selectFile({
anchorElement: img.currentTarget ?? img.target,
multiple: false,
}).then(file => {
eyeCatchingImageId.value = file.id;
});
}
@@ -233,7 +233,10 @@ const exportAntennas = () => {
};
const importFollowing = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('i/import-following', {
fileId: file.id,
withReplies: withReplies.value,
@@ -241,22 +244,34 @@ const importFollowing = async (ev) => {
};
const importUserLists = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importMuting = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importBlocking = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importAntennas = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
const file = await selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
});
misskeyApi('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
@@ -114,7 +114,10 @@ watch(wallpaper, async () => {
});
function setWallpaper(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target, null).then(file => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
}).then(file => {
wallpaper.value = file.url;
});
}
@@ -4,33 +4,45 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.emojis">
<div v-for="emoji in emojis" :key="`emojiMute-${emoji}`" :class="$style.emoji" @click="onEmojiClick($event, emoji)">
<MkCustomEmoji
v-if="emoji.startsWith(':')"
:name="customEmojiName(emoji)"
:host="customEmojiHost(emoji)"
:normal="true"
:menu="false"
:menuReaction="false"
:ignoreMuted="true"
/>
<MkEmoji
v-else
:emoji="emoji"
:menu="false"
:menuReaction="false"
:ignoreMuted="true"
></MkEmoji>
<div class="_gaps_m">
<div :class="$style.emojis">
<div v-for="emoji in emojis" :key="`emojiMute-${emoji}`" :class="$style.emoji" @click="onEmojiClick($event, emoji)">
<MkCustomEmoji
v-if="emoji.startsWith(':')"
:name="customEmojiName(emoji)"
:host="customEmojiHost(emoji)"
:normal="true"
:menu="false"
:menuReaction="false"
:ignoreMuted="true"
/>
<MkEmoji
v-else
:emoji="emoji"
:menu="false"
:menuReaction="false"
:ignoreMuted="true"
></MkEmoji>
</div>
</div>
</div>
<MkButton primary inline @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton primary inline @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<hr>
<SearchMarker :keywords="['sync', 'devices']">
<MkSwitch :modelValue="syncEnabled" @update:modelValue="changeSyncEnabled">
<template #label><i class="ti ti-cloud-cog"></i> <SearchLabel>{{ i18n.ts.syncBetweenDevices }}</SearchLabel></template>
</MkSwitch>
</SearchMarker>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import type { MenuItem } from '@/types/menu';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
@@ -79,7 +91,23 @@ function unmute(emoji: string) {
unmuteEmoji(emoji);
});
}
const syncEnabled = ref(prefer.isSyncEnabled('mutingEmojis'));
function changeSyncEnabled(value: boolean) {
if (value) {
prefer.enableSync('mutingEmojis').then((res) => {
if (res == null) return;
if (res.enabled) syncEnabled.value = true;
});
} else {
prefer.disableSync('mutingEmojis');
syncEnabled.value = false;
}
}
</script>
<style module>
.emojis {
display: flex;
@@ -57,9 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-mood-off"></i></template>
<template #label>{{ i18n.ts.emojiMute }}</template>
<div class="_gaps_m">
<XEmojiMute/>
</div>
<XEmojiMute/>
</mkfolder>
</SearchMarker>
+11 -7
View File
@@ -128,6 +128,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<hr>
<MkButton @click="readAllChatMessages">Read all chat messages</MkButton>
<hr>
<FormSlot>
<MkButton danger @click="migrate"><i class="ti ti-refresh"></i> {{ i18n.ts.migrateOldSettings }}</MkButton>
<template #caption>{{ i18n.ts.migrateOldSettings_description }}</template>
@@ -157,7 +161,7 @@ import { prefer } from '@/preferences.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js';
import { store, TIPS } from '@/store.js';
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
const $i = ensureSignin();
@@ -201,19 +205,19 @@ function migrate() {
}
function resetAllTips() {
store.set('tips', {});
_resetAllTips();
os.success();
}
function hideAllTips() {
const v = {};
for (const k of TIPS) {
v[k] = true;
}
store.set('tips', v);
_hideAllTips();
os.success();
}
function readAllChatMessages() {
os.apiWithDialog('chat/read-all', {});
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
@@ -94,7 +94,11 @@ const friendlyFileName = computed<string>(() => {
});
function selectSound(ev) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts._soundSettings.driveFile).then(async (file) => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
label: i18n.ts._soundSettings.driveFile,
}).then(async (file) => {
if (!file.type.startsWith('audio')) {
os.alert({
type: 'warning',
+4
View File
@@ -226,6 +226,7 @@ const headerActions = computed(() => {
menuItems.push({
type: 'switch',
icon: 'ti ti-repeat',
text: i18n.ts.showRenotes,
ref: withRenotes,
});
@@ -233,6 +234,7 @@ const headerActions = computed(() => {
if (isBasicTimeline(src.value) && hasWithReplies(src.value)) {
menuItems.push({
type: 'switch',
icon: 'ti ti-messages',
text: i18n.ts.showRepliesToOthersInTimeline,
ref: withReplies,
disabled: onlyFiles,
@@ -241,10 +243,12 @@ const headerActions = computed(() => {
menuItems.push({
type: 'switch',
icon: 'ti ti-eye-exclamation',
text: i18n.ts.withSensitive,
ref: withSensitive,
}, {
type: 'switch',
icon: 'ti ti-photo',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,
+13 -13
View File
@@ -81,18 +81,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkButton>
</div>
<div v-else-if="step === 1" class="_gaps_m">
<div style="text-align: center;" class="_gaps_s">
<div><b>{{ i18n.ts._serverSetupWizard.donationRequest }}</b></div>
<div>{{ i18n.ts._serverSetupWizard._donationRequest.text1 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text2 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text3 }}</div>
</div>
<MkLink target="_blank" url="https://misskey-hub.net/docs/donate/" style="margin: 0 auto;">{{ i18n.ts.learnMore }}</MkLink>
<div class="_buttonsCenter">
<MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="step++">
{{ i18n.ts.next }}
</MkButton>
</div>
</div>
<div v-else-if="step === 2" class="_gaps_m">
<div style="text-align: center;" class="_gaps_s">
<div style="font-size: 120%;"><b>{{ i18n.ts._serverSetupWizard.serverSetting }}</b></div>
<div>{{ i18n.ts._serverSetupWizard.youCanEasilyConfigureOptimalServerSettingsWithThisWizard }}</div>
@@ -105,12 +93,17 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._serverSetupWizard.skipSettings }}
</MkButton>
</div>
<div v-else-if="step === 3" class="_gaps_m">
<div v-else-if="step === 2" class="_gaps_m">
<div style="text-align: center;" class="_gaps_s">
<div><b>{{ i18n.ts._serverSetupWizard.settingsCompleted }}</b></div>
<div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description }}</div>
<div>{{ i18n.ts._serverSetupWizard.settingsCompleted_description2 }}</div>
</div>
<div class="_gaps_s" :class="$style.donation">
<div><b>{{ i18n.ts._serverSetupWizard.donationRequest }}</b></div>
<div>{{ i18n.ts._serverSetupWizard._donationRequest.text1 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text2 }}<br>{{ i18n.ts._serverSetupWizard._donationRequest.text3 }}</div>
<MkLink target="_blank" url="https://misskey-hub.net/docs/donate/" style="margin: 0 auto;">{{ i18n.ts.learnMore }}</MkLink>
</div>
<div class="_buttonsCenter">
<MkButton gradate large rounded data-cy-next style="margin: 0 auto;" @click="finish">
{{ i18n.ts.start }}
@@ -232,4 +225,11 @@ function finish() {
font-weight: normal;
opacity: 0.7;
}
.donation {
background: var(--MI_THEME-accentedBg);
border-radius: 12px;
padding: 16px;
text-align: center;
}
</style>
+1 -1
View File
@@ -422,7 +422,7 @@ export const PREF_DEF = definePreferences({
default: null as WatermarkPreset['id'] | null,
},
defaultImageCompressionLevel: {
default: 2,
default: 2 as 0 | 1 | 2 | 3,
},
'sound.masterVolume': {
+1 -12
View File
@@ -10,22 +10,11 @@ import darkTheme from '@@/themes/d-green-lime.json5';
import { hemisphere } from '@@/js/intl-const.js';
import type { DeviceKind } from '@/utility/device-kind.js';
import type { Plugin } from '@/plugin.js';
import type { TIPS } from '@/tips.js';
import { miLocalStorage } from '@/local-storage.js';
import { Pizzax } from '@/lib/pizzax.js';
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
export const TIPS = [
'drive',
'uploader',
'clips',
'userLists',
'tl.home',
'tl.local',
'tl.social',
'tl.global',
'abuses',
] as const;
/**
* (not)
*/
+38
View File
@@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { store } from '@/store.js';
export const TIPS = [
'drive',
'uploader',
'postFormUploader',
'clips',
'userLists',
'tl.home',
'tl.local',
'tl.social',
'tl.global',
'abuses',
] as const;
export function closeTip(tip: typeof TIPS[number]) {
store.set('tips', {
...store.r.tips.value,
[tip]: true,
});
}
export function resetAllTips() {
store.set('tips', {});
}
export function hideAllTips() {
const v = {};
for (const k of TIPS) {
v[k] = true;
}
store.set('tips', v);
}
+10 -8
View File
@@ -11,20 +11,22 @@ type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<
type MenuRadioOptionsDef = Record<string, any>;
type Text = string | ComputedRef<string>;
export type MenuAction = (ev: MouseEvent) => void;
export type MenuDivider = { type: 'divider' };
export type MenuNull = undefined;
export type MenuLabel = { type: 'label', text: string, caption?: string };
export type MenuLink = { type: 'link', to: string, text: string, caption?: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, caption?: string, icon?: string, indicate?: boolean };
export type MenuLabel = { type: 'label', text: Text, caption?: Text };
export type MenuLink = { type: 'link', to: string, text: Text, caption?: Text, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User };
export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: Text, caption?: Text, icon?: string, indicate?: boolean };
export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, caption?: string, icon?: string, disabled?: boolean | Ref<boolean> };
export type MenuButton = { type?: 'button', text: string, caption?: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
export type MenuRadio = { type: 'radio', text: string, caption?: string, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
export type MenuRadioOption = { type: 'radioOption', text: string, caption?: string, action: MenuAction; active?: boolean | ComputedRef<boolean> };
export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: Text, caption?: Text, icon?: string, disabled?: boolean | Ref<boolean> };
export type MenuButton = { type?: 'button', text: Text, caption?: Text, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
export type MenuRadio = { type: 'radio', text: Text, caption?: Text, icon?: string, ref: Ref<MenuRadioOptionsDef[keyof MenuRadioOptionsDef]>, options: MenuRadioOptionsDef, disabled?: boolean | Ref<boolean> };
export type MenuRadioOption = { type: 'radioOption', text: Text, caption?: Text, action: MenuAction; active?: boolean | ComputedRef<boolean> };
export type MenuComponent<T extends Component = any> = { type: 'component', component: T, props?: ComponentProps<T> };
export type MenuParent = { type: 'parent', text: string, caption?: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
export type MenuParent = { type: 'parent', text: Text, caption?: Text, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
export type MenuPending = { type: 'pending' };
+20 -8
View File
@@ -6,6 +6,7 @@
import { defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import { apiUrl } from '@@/js/config.js';
import type { UploaderFeatures } from '@/composables/use-uploader.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { useStream } from '@/stream.js';
@@ -31,6 +32,7 @@ export class UploadAbortedError extends Error {
export function uploadFile(file: File | Blob, options: {
name?: string;
folderId?: string | null;
isSensitive?: boolean;
onProgress?: (ctx: { total: number; loaded: number; }) => void;
} = {}): UploadReturnType {
const xhr = new XMLHttpRequest();
@@ -139,6 +141,7 @@ export function uploadFile(file: File | Blob, options: {
formData.append('force', 'true');
formData.append('file', file);
formData.append('name', options.name ?? (file instanceof File ? file.name : 'untitled'));
formData.append('isSensitive', options.isSensitive ? 'true' : 'false');
if (options.folderId) formData.append('folderId', options.folderId);
xhr.send(formData);
@@ -155,6 +158,7 @@ export function uploadFile(file: File | Blob, options: {
export function chooseFileFromPcAndUpload(
options: {
multiple?: boolean;
features?: UploaderFeatures;
folderId?: string | null;
} = {},
): Promise<Misskey.entities.DriveFile[]> {
@@ -163,6 +167,7 @@ export function chooseFileFromPcAndUpload(
if (files.length === 0) return;
os.launchUploader(files, {
folderId: options.folderId,
features: options.features,
}).then(driveFiles => {
res(driveFiles);
});
@@ -194,7 +199,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
type: 'url',
placeholder: i18n.ts.uploadFromUrlDescription,
}).then(({ canceled, result: url }) => {
if (canceled) return;
if (canceled || url == null) return;
const marker = genId();
@@ -221,7 +226,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
});
}
function select(anchorElement: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
function select(anchorElement: HTMLElement | EventTarget | null, label: string | null, multiple: boolean, features?: UploaderDialogFeatures): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => {
os.popupMenu([label ? {
text: label,
@@ -229,7 +234,7 @@ function select(anchorElement: HTMLElement | EventTarget | null, label: string |
} : undefined, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
action: () => chooseFileFromPcAndUpload({ multiple }).then(files => res(files)),
action: () => chooseFileFromPcAndUpload({ multiple, features }).then(files => res(files)),
}, {
text: i18n.ts.fromDrive,
icon: 'ti ti-cloud',
@@ -242,12 +247,19 @@ function select(anchorElement: HTMLElement | EventTarget | null, label: string |
});
}
export function selectFile(anchorElement: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
return select(anchorElement, label, false).then(files => files[0]);
}
type SelectFileOptions<M extends boolean> = {
anchorElement: HTMLElement | EventTarget | null;
multiple: M;
label?: string | null;
features?: UploaderDialogFeatures;
};
export function selectFiles(anchorElement: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
return select(anchorElement, label, true);
export async function selectFile<
M extends boolean,
MR extends M extends true ? Misskey.entities.DriveFile[] : Misskey.entities.DriveFile,
>(opts: SelectFileOptions<M>): Promise<MR> {
const files = await select(opts.anchorElement, opts.label ?? null, opts.multiple ?? false, opts.features);
return opts.multiple ? (files as MR) : (files[0]! as MR);
}
export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: {
@@ -4,6 +4,7 @@
*/
import { getProxiedImageUrl } from '../media-proxy.js';
import { initShaderProgram } from '../webgl.js';
type ParamTypeToPrimitive = {
'number': number;
@@ -57,11 +58,9 @@ function getValue<T extends keyof ParamTypeToPrimitive>(params: Record<string, a
return params[k];
}
export class ImageEffector {
export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, any>>> {
private gl: WebGL2RenderingContext;
private canvas: HTMLCanvasElement | null = null;
private renderTextureProgram: WebGLProgram;
private renderInvertedTextureProgram: WebGLProgram;
private renderWidth: number;
private renderHeight: number;
private originalImage: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
@@ -70,7 +69,8 @@ export class ImageEffector {
private shaderCache: Map<string, WebGLProgram> = new Map();
private perLayerResultTextures: Map<string, WebGLTexture> = new Map();
private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map();
private fxs: ImageEffectorFx[];
private nopProgram: WebGLProgram;
private fxs: [...IEX];
private paramTextures: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map();
constructor(options: {
@@ -78,7 +78,7 @@ export class ImageEffector {
renderWidth: number;
renderHeight: number;
image: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
fxs: ImageEffectorFx[];
fxs: [...IEX];
}) {
this.canvas = options.canvas;
this.renderWidth = options.renderWidth;
@@ -114,13 +114,13 @@ export class ImageEffector {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.originalImage.width, this.originalImage.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.originalImage);
gl.bindTexture(gl.TEXTURE_2D, null);
this.renderTextureProgram = this.initShaderProgram(`#version 300 es
this.nopProgram = initShaderProgram(this.gl, `#version 300 es
in vec2 position;
out vec2 in_uv;
void main() {
in_uv = (position + 1.0) / 2.0;
gl_Position = vec4(position, 0.0, 1.0);
gl_Position = vec4(position * vec2(1.0, -1.0), 0.0, 1.0);
}
`, `#version 300 es
precision mediump float;
@@ -134,82 +134,28 @@ export class ImageEffector {
}
`);
this.renderInvertedTextureProgram = this.initShaderProgram(`#version 300 es
in vec2 position;
out vec2 in_uv;
void main() {
in_uv = (position + 1.0) / 2.0;
in_uv.y = 1.0 - in_uv.y;
gl_Position = vec4(position, 0.0, 1.0);
}
`, `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D u_texture;
out vec4 out_color;
void main() {
out_color = texture(u_texture, in_uv);
}
`);
// レジスタ番号はシェーダープログラムに属しているわけではなく、独立の存在なので、とりあえず nopProgram を使って設定する(その後は効果が持続する)
// ref. https://qiita.com/emadurandal/items/5966c8374f03d4de3266
const positionLocation = gl.getAttribLocation(this.nopProgram, 'position');
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation);
}
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) {
private renderLayer(layer: ImageEffectorLayer, preTexture: WebGLTexture, invert = false) {
const gl = this.gl;
const fx = this.fxs.find(fx => fx.id === layer.fxId);
if (fx == null) return;
const cachedShader = this.shaderCache.get(fx.id);
const shaderProgram = cachedShader ?? this.initShaderProgram(`#version 300 es
const shaderProgram = cachedShader ?? initShaderProgram(this.gl, `#version 300 es
in vec2 position;
uniform bool u_invert;
out vec2 in_uv;
void main() {
in_uv = (position + 1.0) / 2.0;
gl_Position = vec4(position, 0.0, 1.0);
gl_Position = u_invert ? vec4(position * vec2(1.0, -1.0), 0.0, 1.0) : vec4(position, 0.0, 1.0);
}
`, fx.shader);
if (cachedShader == null) {
@@ -221,6 +167,9 @@ export class ImageEffector {
const in_resolution = gl.getUniformLocation(shaderProgram, 'in_resolution');
gl.uniform2fv(in_resolution, [this.renderWidth, this.renderHeight]);
const u_invert = gl.getUniformLocation(shaderProgram, 'u_invert');
gl.uniform1i(u_invert, invert ? 1 : 0);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, preTexture);
const in_texture = gl.getUniformLocation(shaderProgram, 'in_texture');
@@ -230,7 +179,7 @@ export class ImageEffector {
gl: gl,
program: shaderProgram,
params: Object.fromEntries(
Object.entries(fx.params).map(([key, param]) => {
Object.entries(fx.params as ImageEffectorFxParamDefs).map(([key, param]) => {
return [key, layer.params[key] ?? param.default];
}),
),
@@ -238,7 +187,7 @@ export class ImageEffector {
width: this.renderWidth,
height: this.renderHeight,
textures: Object.fromEntries(
Object.entries(fx.params).map(([k, v]) => {
Object.entries(fx.params as ImageEffectorFxParamDefs).map(([k, v]) => {
if (v.type !== 'texture') return [k, null];
const param = getValue<typeof v.type>(layer.params, k);
if (param == null) return [k, null];
@@ -253,27 +202,23 @@ export class ImageEffector {
public render() {
const gl = this.gl;
{
// 入力をそのまま出力
if (this.layers.length === 0) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.originalImageTexture);
gl.useProgram(this.renderTextureProgram);
const u_texture = gl.getUniformLocation(this.renderTextureProgram, 'u_texture');
gl.uniform1i(u_texture, 0);
const u_resolution = gl.getUniformLocation(this.renderTextureProgram, 'u_resolution');
gl.uniform2fv(u_resolution, [this.renderWidth, this.renderHeight]);
const positionLocation = gl.getAttribLocation(this.renderTextureProgram, 'position');
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation);
gl.useProgram(this.nopProgram);
gl.uniform1i(gl.getUniformLocation(this.nopProgram, 'u_texture')!, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
return;
}
// --------------------
let preTexture = this.originalImageTexture;
for (const layer of this.layers) {
const isLast = layer === this.layers.at(-1);
const cachedResultTexture = this.perLayerResultTextures.get(layer.id);
const resultTexture = cachedResultTexture ?? createTexture(gl);
if (cachedResultTexture == null) {
@@ -283,30 +228,22 @@ export class ImageEffector {
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);
const cachedResultFrameBuffer = this.perLayerResultFrameBuffers.get(layer.id);
const resultFrameBuffer = cachedResultFrameBuffer ?? gl.createFramebuffer()!;
if (cachedResultFrameBuffer == null) {
this.perLayerResultFrameBuffers.set(layer.id, resultFrameBuffer);
if (isLast) {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
} else {
const cachedResultFrameBuffer = this.perLayerResultFrameBuffers.get(layer.id);
const resultFrameBuffer = cachedResultFrameBuffer ?? gl.createFramebuffer()!;
if (cachedResultFrameBuffer == null) {
this.perLayerResultFrameBuffers.set(layer.id, resultFrameBuffer);
}
gl.bindFramebuffer(gl.FRAMEBUFFER, resultFrameBuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, resultTexture, 0);
}
gl.bindFramebuffer(gl.FRAMEBUFFER, resultFrameBuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, resultTexture, 0);
this.renderLayer(layer, preTexture);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
this.renderLayer(layer, preTexture, isLast);
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[]) {
@@ -329,7 +266,7 @@ export class ImageEffector {
unused.delete(textureKey);
if (this.paramTextures.has(textureKey)) continue;
console.log(`Baking texture of <${textureKey}>...`);
if (_DEV_) console.log(`Baking texture of <${textureKey}>...`);
const texture = v.type === 'text' ? await createTextureFromText(this.gl, v.text) : v.type === 'url' ? await createTextureFromUrl(this.gl, v.url) : null;
if (texture == null) continue;
@@ -339,7 +276,7 @@ export class ImageEffector {
}
for (const k of unused) {
console.log(`Dispose unused texture <${k}>...`);
if (_DEV_) console.log(`Dispose unused texture <${k}>...`);
this.gl.deleteTexture(this.paramTextures.get(k)!.texture);
this.paramTextures.delete(k);
}
@@ -366,6 +303,8 @@ export class ImageEffector {
* disposeCanvas = true loseContextを呼ぶためcanvasも再利用不可になるので注意
*/
public destroy(disposeCanvas = true) {
this.gl.deleteProgram(this.nopProgram);
for (const shader of this.shaderCache.values()) {
this.gl.deleteProgram(shader);
}
@@ -386,8 +325,6 @@ export class ImageEffector {
}
this.paramTextures.clear();
this.gl.deleteProgram(this.renderTextureProgram);
this.gl.deleteProgram(this.renderInvertedTextureProgram);
this.gl.deleteTexture(this.originalImageTexture);
if (disposeCanvas) {
@@ -5,6 +5,7 @@
import { FX_checker } from './fxs/checker.js';
import { FX_chromaticAberration } from './fxs/chromaticAberration.js';
import { FX_colorAdjust } from './fxs/colorAdjust.js';
import { FX_colorClamp } from './fxs/colorClamp.js';
import { FX_colorClampAdvanced } from './fxs/colorClampAdvanced.js';
import { FX_distort } from './fxs/distort.js';
@@ -26,6 +27,7 @@ export const FXS = [
FX_mirror,
FX_invert,
FX_grayscale,
FX_colorAdjust,
FX_colorClamp,
FX_colorClampAdvanced,
FX_distort,
@@ -0,0 +1,136 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_brightness;
uniform float u_contrast;
uniform float u_hue;
uniform float u_lightness;
uniform float u_saturation;
out vec4 out_color;
// RGB to HSL
vec3 rgb2hsl(vec3 c) {
float maxc = max(max(c.r, c.g), c.b);
float minc = min(min(c.r, c.g), c.b);
float l = (maxc + minc) * 0.5;
float s = 0.0;
float h = 0.0;
if (maxc != minc) {
float d = maxc - minc;
s = l > 0.5 ? d / (2.0 - maxc - minc) : d / (maxc + minc);
if (maxc == c.r) {
h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
} else if (maxc == c.g) {
h = (c.b - c.r) / d + 2.0;
} else {
h = (c.r - c.g) / d + 4.0;
}
h /= 6.0;
}
return vec3(h, s, l);
}
// HSL to RGB
float hue2rgb(float p, float q, float t) {
if (t < 0.0) t += 1.0;
if (t > 1.0) t -= 1.0;
if (t < 1.0/6.0) return p + (q - p) * 6.0 * t;
if (t < 1.0/2.0) return q;
if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
return p;
}
vec3 hsl2rgb(vec3 hsl) {
float r, g, b;
float h = hsl.x;
float s = hsl.y;
float l = hsl.z;
if (s == 0.0) {
r = g = b = l;
} else {
float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
float p = 2.0 * l - q;
r = hue2rgb(p, q, h + 1.0/3.0);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1.0/3.0);
}
return vec3(r, g, b);
}
void main() {
vec4 in_color = texture(in_texture, in_uv);
vec3 color = in_color.rgb;
color = color * u_brightness;
color += vec3(clamp(u_lightness, 0.0, 2.0) - 1.0);
color = (color - 0.5) * u_contrast + 0.5;
vec3 hsl = rgb2hsl(color);
hsl.x = mod(hsl.x + u_hue, 1.0);
hsl.y = clamp(hsl.y * u_saturation, 0.0, 1.0);
color = hsl2rgb(hsl);
out_color = vec4(color, in_color.a);
}
`;
export const FX_colorAdjust = defineImageEffectorFx({
id: 'colorAdjust' as const,
name: i18n.ts._imageEffector._fxs.colorAdjust,
shader,
uniforms: ['lightness', 'contrast', 'hue', 'brightness', 'saturation'] as const,
params: {
lightness: {
type: 'number' as const,
default: 100,
min: 0,
max: 200,
step: 1,
},
contrast: {
type: 'number' as const,
default: 100,
min: 0,
max: 200,
step: 1,
},
hue: {
type: 'number' as const,
default: 0,
min: -360,
max: 360,
step: 1,
},
brightness: {
type: 'number' as const,
default: 100,
min: 0,
max: 200,
step: 1,
},
saturation: {
type: 'number' as const,
default: 100,
min: 0,
max: 200,
step: 1,
},
},
main: ({ gl, u, params }) => {
gl.uniform1f(u.brightness, params.brightness / 100);
gl.uniform1f(u.contrast, params.contrast / 100);
gl.uniform1f(u.hue, params.hue / 360);
gl.uniform1f(u.lightness, params.lightness / 100);
gl.uniform1f(u.saturation, params.saturation / 100);
},
});
+16 -7
View File
@@ -3,13 +3,20 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FX_watermarkPlacement } from './image-effector/fxs/watermarkPlacement.js';
import { FX_stripe } from './image-effector/fxs/stripe.js';
import { FX_polkadot } from './image-effector/fxs/polkadot.js';
import { FX_checker } from './image-effector/fxs/checker.js';
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
import { FX_stripe } from '@/utility/image-effector/fxs/stripe.js';
import { FX_polkadot } from '@/utility/image-effector/fxs/polkadot.js';
import { FX_checker } from '@/utility/image-effector/fxs/checker.js';
import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
const WATERMARK_FXS = [
FX_watermarkPlacement,
FX_stripe,
FX_polkadot,
FX_checker,
] as const satisfies ImageEffectorFx<string, any>[];
export type WatermarkPreset = {
id: string;
name: string;
@@ -64,7 +71,7 @@ export type WatermarkPreset = {
};
export class WatermarkRenderer {
private effector: ImageEffector;
private effector: ImageEffector<typeof WATERMARK_FXS>;
private layers: WatermarkPreset['layers'] = [];
constructor(options: {
@@ -78,7 +85,7 @@ export class WatermarkRenderer {
renderWidth: options.renderWidth,
renderHeight: options.renderHeight,
image: options.image,
fxs: [FX_watermarkPlacement, FX_stripe, FX_polkadot, FX_checker],
fxs: WATERMARK_FXS,
});
}
@@ -157,6 +164,8 @@ export class WatermarkRenderer {
opacity: layer.opacity,
},
};
} else {
throw new Error(`Unknown layer type`);
}
});
}
+40
View File
@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function loadShader(gl: WebGL2RenderingContext, type: GLenum, source: string): WebGLShader {
const shader = gl.createShader(type);
if (shader == null) {
throw new Error('falied to create shader');
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
gl.deleteShader(shader);
throw new Error(`falied to compile shader: ${gl.getShaderInfoLog(shader)}`);
}
return shader;
}
export function initShaderProgram(gl: WebGL2RenderingContext, vsSource: string, fsSource: string): WebGLProgram {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
console.error(`failed to init shader: ${gl.getProgramInfoLog(shaderProgram)}`);
throw new Error('failed to init shader');
}
return shaderProgram;
}
+3 -2
View File
@@ -3212,10 +3212,11 @@ type PinnedUsersResponse = operations['pinned-users']['responses']['200']['conte
type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json'];
// Warning: (ae-forgotten-export) The symbol "AllNullRecord" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "AllNullOrOptionalRecord" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "NonNullableRecord" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>> & {
type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'text'>> & AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>> & {
files: [];
fileIds: [];
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
@@ -3748,7 +3749,7 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons
// Warnings were encountered during analysis:
//
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/entities.ts:54:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
// src/streaming.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
+1 -1
View File
@@ -12,7 +12,7 @@
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"openapi-types": "12.1.3",
"openapi-typescript": "6.7.6",
"openapi-typescript": "7.8.0",
"ts-case-convert": "2.1.0",
"tsx": "4.19.4",
"typescript": "5.8.3"
@@ -0,0 +1,130 @@
import * as ts from 'typescript';
/**
* TypeScript AST 'never'
* 'paths' TypeAliasDeclaration
* 'operations' InterfaceDeclaration
* TypeLiteralNode/Interfaceから 'PropertySignature' 'never'
* 'never'
*
* @param astNodes ts.Node (: `openapi-typescript` )
* @returns 'never' ts.Node
*/
export function removeNeverPropertiesFromAST(astNodes: readonly ts.Node[]): ts.Node[] {
const factory = ts.factory;
/**
* TypeLiteralNodeやInterfaceDeclarationのmembersからneverプロパティを除去し
*/
function removeNeverPropertiesFromMembers(
members: readonly ts.TypeElement[],
visitType: (node: ts.Node) => ts.Node | undefined,
): { newMembers: ts.TypeElement[]; hasChanged: boolean } {
const newMembers: ts.TypeElement[] = [];
let hasChanged = false;
for (const member of members) {
if (ts.isPropertySignature(member)) {
if (member.type && member.type.kind === ts.SyntaxKind.NeverKeyword) {
hasChanged = true;
continue;
}
let updatedPropertySignature = member;
if (member.type) {
const visitedMemberType = ts.visitNode(member.type, visitType);
if (visitedMemberType && visitedMemberType !== member.type) {
updatedPropertySignature = factory.updatePropertySignature(
member,
member.modifiers,
member.name,
member.questionToken,
visitedMemberType as ts.TypeNode,
);
hasChanged = true;
} else if (visitedMemberType === undefined) {
// 子の型が消された場合、このプロパティも消す
hasChanged = true;
continue;
}
}
newMembers.push(updatedPropertySignature);
} else {
newMembers.push(member);
}
}
return { newMembers, hasChanged };
}
function typeNodeRecursiveVisitor(node: ts.Node): ts.Node | undefined {
if (ts.isTypeLiteralNode(node)) {
const { newMembers, hasChanged } = removeNeverPropertiesFromMembers(node.members, typeNodeRecursiveVisitor);
if (newMembers.length === 0) {
// すべてのプロパティがneverで消された場合、このTypeLiteralNode自体も消す
return undefined;
}
if (hasChanged) {
return factory.updateTypeLiteralNode(node, factory.createNodeArray(newMembers));
}
return node;
}
return ts.visitEachChild(node, typeNodeRecursiveVisitor, undefined);
}
function interfaceRecursiveVisitor(node: ts.Node): ts.Node | undefined {
if (ts.isInterfaceDeclaration(node)) {
const { newMembers, hasChanged } = removeNeverPropertiesFromMembers(node.members, typeNodeRecursiveVisitor);
if (newMembers.length === 0) {
return undefined;
}
if (hasChanged) {
return factory.updateInterfaceDeclaration(
node,
node.modifiers,
node.name,
node.typeParameters,
node.heritageClauses,
newMembers,
);
}
return node;
}
return ts.visitEachChild(node, interfaceRecursiveVisitor, undefined);
}
function topLevelVisitor(node: ts.Node): ts.Node | undefined {
if (ts.isTypeAliasDeclaration(node) && node.name.escapedText === 'paths') {
const newType = ts.visitNode(node.type, typeNodeRecursiveVisitor);
if (newType && newType !== node.type) {
return factory.updateTypeAliasDeclaration(
node,
node.modifiers,
node.name,
node.typeParameters,
newType as ts.TypeNode,
);
} else if (newType === undefined) {
return undefined;
}
}
if (ts.isInterfaceDeclaration(node) && node.name.escapedText === 'operations') {
const result = interfaceRecursiveVisitor(node);
return result;
}
return ts.visitEachChild(node, topLevelVisitor, undefined);
}
const transformedNodes: ts.Node[] = [];
for (const astNode of astNodes) {
const resultNode = ts.visitNode(astNode, topLevelVisitor);
if (resultNode) {
transformedNodes.push(resultNode);
}
}
return transformedNodes;
}
+19 -12
View File
@@ -1,9 +1,12 @@
import assert from 'assert';
import { mkdir, readFile, writeFile } from 'fs/promises';
import { OpenAPIV3_1 } from 'openapi-types';
import type { OpenAPIV3_1 } from 'openapi-types';
import { toPascal } from 'ts-case-convert';
import OpenAPIParser from '@readme/openapi-parser';
import openapiTS, { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
import openapiTS, { astToString } from 'openapi-typescript';
import type { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
import ts from 'typescript';
import { removeNeverPropertiesFromAST } from './ast-transformer.js';
async function generateBaseTypes(
openApiDocs: OpenAPIV3_1.Document,
@@ -28,13 +31,6 @@ async function generateBaseTypes(
assert('post' in item);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
openApi.paths![key] = {
...('get' in item ? {
get: {
...item.get,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
operationId: ((item as PathItemObject).get as OperationObject).operationId!.replaceAll('get___', ''),
},
} : {}),
post: {
...item.post,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -43,15 +39,26 @@ async function generateBaseTypes(
};
}
const generatedTypes = await openapiTS(openApi, {
const tsNullNode = ts.factory.createLiteralTypeNode(ts.factory.createNull());
const tsBlobNode = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Blob'));
const generatedTypesAst = await openapiTS(openApi, {
exportType: true,
transform(schemaObject) {
if ('format' in schemaObject && schemaObject.format === 'binary') {
return schemaObject.nullable ? 'Blob | null' : 'Blob';
if (schemaObject.nullable) {
return ts.factory.createUnionTypeNode([tsBlobNode, tsNullNode]);
} else {
return tsBlobNode;
}
}
},
});
lines.push(generatedTypes);
const filteredAst = removeNeverPropertiesFromAST(generatedTypesAst);
lines.push(astToString(filteredAst));
lines.push('');
await writeFile(typeFileName, lines.join('\n'));
+1 -1
View File
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.6.1-alpha.0",
"version": "2025.6.1-alpha.4",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
+1 -1
View File
@@ -77,7 +77,7 @@ export class APIClient {
if (mediaType === 'application/json') {
payload = JSON.stringify({
...params,
...(this.assertIsRecord(params) ? params : {}),
i: credential !== undefined ? credential : this.credential,
});
} else if (mediaType === 'multipart/form-data') {
@@ -1710,6 +1710,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:chat*
*/
request<E extends 'chat/read-all', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
@@ -795,6 +795,7 @@ export type Endpoints = {
'chat/messages/show': { req: ChatMessagesShowRequest; res: ChatMessagesShowResponse };
'chat/messages/unreact': { req: ChatMessagesUnreactRequest; res: EmptyResponse };
'chat/messages/user-timeline': { req: ChatMessagesUserTimelineRequest; res: ChatMessagesUserTimelineResponse };
'chat/read-all': { req: EmptyRequest; res: EmptyResponse };
'chat/rooms/create': { req: ChatRoomsCreateRequest; res: ChatRoomsCreateResponse };
'chat/rooms/delete': { req: ChatRoomsDeleteRequest; res: EmptyResponse };
'chat/rooms/invitations/create': { req: ChatRoomsInvitationsCreateRequest; res: ChatRoomsInvitationsCreateResponse };
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -24,10 +24,14 @@ type NonNullableRecord<T> = {
type AllNullRecord<T> = {
[P in keyof T]: null;
};
type AllNullOrOptionalRecord<T> = {
[P in keyof T]: never;
};
export type PureRenote =
Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'>
& AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>>
& AllNullRecord<Pick<Note, 'text'>>
& AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>>
& { files: []; fileIds: []; }
& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
+188 -106
View File
@@ -1434,8 +1434,8 @@ importers:
specifier: 12.1.3
version: 12.1.3
openapi-typescript:
specifier: 6.7.6
version: 6.7.6
specifier: 7.8.0
version: 7.8.0(typescript@5.8.3)
ts-case-convert:
specifier: 2.1.0
version: 2.1.0
@@ -1705,6 +1705,10 @@ packages:
resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==}
engines: {node: '>=6.9.0'}
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.24.7':
resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==}
engines: {node: '>=6.9.0'}
@@ -3407,6 +3411,16 @@ packages:
resolution: {integrity: sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw==}
engines: {node: '>=18'}
'@redocly/ajv@8.11.2':
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
'@redocly/config@0.22.2':
resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==}
'@redocly/openapi-core@1.34.3':
resolution: {integrity: sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg==}
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
'@rollup/plugin-json@6.1.0':
resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==}
engines: {node: '>=14.0.0'}
@@ -4920,18 +4934,12 @@ packages:
'@vue/compiler-core@3.5.13':
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
'@vue/compiler-core@3.5.14':
resolution: {integrity: sha512-k7qMHMbKvoCXIxPhquKQVw3Twid3Kg4s7+oYURxLGRd56LiuHJVrvFKI4fm2AM3c8apqODPfVJGoh8nePbXMRA==}
'@vue/compiler-core@3.5.16':
resolution: {integrity: sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==}
'@vue/compiler-dom@3.5.13':
resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
'@vue/compiler-dom@3.5.14':
resolution: {integrity: sha512-1aOCSqxGOea5I80U2hQJvXYpPm/aXo95xL/m/mMhgyPUsKe9jhjwWpziNAw7tYRnbz1I61rd9Mld4W9KmmRoug==}
'@vue/compiler-dom@3.5.16':
resolution: {integrity: sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==}
@@ -4977,9 +4985,6 @@ packages:
'@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vue/shared@3.5.14':
resolution: {integrity: sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==}
'@vue/shared@3.5.16':
resolution: {integrity: sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==}
@@ -5594,6 +5599,9 @@ packages:
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
change-case@5.4.4:
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
char-regex@1.0.2:
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
engines: {node: '>=10'}
@@ -5765,6 +5773,9 @@ packages:
colord@2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
colorette@1.4.0:
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
colorette@2.0.19:
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
@@ -7232,6 +7243,10 @@ packages:
resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
engines: {node: '>=12'}
index-to-position@1.1.0:
resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==}
engines: {node: '>=18'}
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
@@ -7667,6 +7682,10 @@ packages:
engines: {node: '>=12'}
hasBin: true
js-levenshtein@1.1.6:
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
engines: {node: '>=0.10.0'}
js-stringify@1.0.2:
resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==}
@@ -8621,9 +8640,11 @@ packages:
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
openapi-typescript@6.7.6:
resolution: {integrity: sha512-c/hfooPx+RBIOPM09GSxABOZhYPblDoyaGhqBkD/59vtpN21jEuWKDlM0KYTvqJVlSYjKs0tBcIdeXKChlSPtw==}
openapi-typescript@7.8.0:
resolution: {integrity: sha512-1EeVWmDzi16A+siQlo/SwSGIT7HwaFAVjvMA7/jG5HMLSnrUOzPL7uSTRZZa4v/LCRxHTApHKtNY6glApEoiUQ==}
hasBin: true
peerDependencies:
typescript: ^5.x
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
@@ -8700,6 +8721,10 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
parse-json@8.3.0:
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
engines: {node: '>=18'}
parse-ms@4.0.0:
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
engines: {node: '>=18'}
@@ -8894,6 +8919,10 @@ packages:
resolution: {integrity: sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==}
engines: {node: '>=10'}
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
@@ -10128,6 +10157,10 @@ packages:
resolution: {integrity: sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==}
engines: {node: '>=14.18.0'}
supports-color@10.0.0:
resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==}
engines: {node: '>=18'}
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@@ -10140,10 +10173,6 @@ packages:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
supports-color@9.4.0:
resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==}
engines: {node: '>=12'}
supports-hyperlinks@2.3.0:
resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
engines: {node: '>=8'}
@@ -10414,6 +10443,10 @@ packages:
resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==}
engines: {node: '>=16'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@@ -10603,6 +10636,9 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
uri-js-replace@1.0.1:
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -11029,6 +11065,9 @@ packages:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
yaml-ast-parser@0.0.43:
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
@@ -11609,6 +11648,12 @@ snapshots:
'@babel/highlight': 7.24.7
picocolors: 1.1.1
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.24.7': {}
'@babel/core@7.24.7':
@@ -11619,12 +11664,12 @@ snapshots:
'@babel/helper-compilation-targets': 7.24.7
'@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7)
'@babel/helpers': 7.24.7
'@babel/parser': 7.27.2
'@babel/parser': 7.25.6
'@babel/template': 7.24.7
'@babel/traverse': 7.24.7
'@babel/types': 7.27.1
'@babel/types': 7.25.6
convert-source-map: 2.0.0
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -11633,7 +11678,7 @@ snapshots:
'@babel/generator@7.24.7':
dependencies:
'@babel/types': 7.27.1
'@babel/types': 7.25.6
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
jsesc: 2.5.2
@@ -11648,21 +11693,21 @@ snapshots:
'@babel/helper-environment-visitor@7.24.7':
dependencies:
'@babel/types': 7.27.1
'@babel/types': 7.25.6
'@babel/helper-function-name@7.24.7':
dependencies:
'@babel/template': 7.24.7
'@babel/types': 7.27.1
'@babel/types': 7.25.6
'@babel/helper-hoist-variables@7.24.7':
dependencies:
'@babel/types': 7.27.1
'@babel/types': 7.25.6
'@babel/helper-module-imports@7.24.7':
dependencies:
'@babel/traverse': 7.24.7
'@babel/types': 7.27.1
'@babel/types': 7.25.6
transitivePeerDependencies:
- supports-color
@@ -11673,7 +11718,7 @@ snapshots:
'@babel/helper-module-imports': 7.24.7
'@babel/helper-simple-access': 7.24.7
'@babel/helper-split-export-declaration': 7.24.7
'@babel/helper-validator-identifier': 7.27.1
'@babel/helper-validator-identifier': 7.24.7
transitivePeerDependencies:
- supports-color
@@ -11682,13 +11727,13 @@ snapshots:
'@babel/helper-simple-access@7.24.7':
dependencies:
'@babel/traverse': 7.24.7
'@babel/types': 7.27.1
'@babel/types': 7.25.6
transitivePeerDependencies:
- supports-color
'@babel/helper-split-export-declaration@7.24.7':
dependencies:
'@babel/types': 7.27.1
'@babel/types': 7.25.6
'@babel/helper-string-parser@7.24.8': {}
@@ -11703,7 +11748,7 @@ snapshots:
'@babel/helpers@7.24.7':
dependencies:
'@babel/template': 7.24.7
'@babel/types': 7.27.1
'@babel/types': 7.25.6
'@babel/highlight@7.24.7':
dependencies:
@@ -11797,8 +11842,8 @@ snapshots:
'@babel/template@7.24.7':
dependencies:
'@babel/code-frame': 7.24.7
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
'@babel/traverse@7.24.7':
dependencies:
@@ -11808,9 +11853,9 @@ snapshots:
'@babel/helper-function-name': 7.24.7
'@babel/helper-hoist-variables': 7.24.7
'@babel/helper-split-export-declaration': 7.24.7
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
debug: 4.4.1(supports-color@5.5.0)
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
debug: 4.4.1(supports-color@10.0.0)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -12148,7 +12193,7 @@ snapshots:
'@eslint/config-array@0.20.0':
dependencies:
'@eslint/object-schema': 2.1.6
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -12162,7 +12207,7 @@ snapshots:
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
espree: 10.3.0
globals: 14.0.0
ignore: 5.3.1
@@ -12963,7 +13008,7 @@ snapshots:
dependencies:
agent-base: 7.1.3
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
https-proxy-agent: 7.0.6(supports-color@10.0.0)
lru-cache: 10.4.3
socks-proxy-agent: 8.0.5
transitivePeerDependencies:
@@ -13426,6 +13471,29 @@ snapshots:
'@readme/openapi-schemas@3.1.0': {}
'@redocly/ajv@8.11.2':
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js-replace: 1.0.1
'@redocly/config@0.22.2': {}
'@redocly/openapi-core@1.34.3(supports-color@10.0.0)':
dependencies:
'@redocly/ajv': 8.11.2
'@redocly/config': 0.22.2
colorette: 1.4.0
https-proxy-agent: 7.0.6(supports-color@10.0.0)
js-levenshtein: 1.1.6
js-yaml: 4.1.0
minimatch: 5.1.6
pluralize: 8.0.0
yaml-ast-parser: 0.0.43
transitivePeerDependencies:
- supports-color
'@rollup/plugin-json@6.1.0(rollup@4.41.1)':
dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.41.1)
@@ -14658,7 +14726,7 @@ snapshots:
'@tokenizer/inflate@0.2.7':
dependencies:
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
fflate: 0.8.2
token-types: 6.0.0
transitivePeerDependencies:
@@ -14690,24 +14758,24 @@ snapshots:
'@types/babel__core@7.20.0':
dependencies:
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
'@types/babel__generator': 7.6.4
'@types/babel__template': 7.4.1
'@types/babel__traverse': 7.20.0
'@types/babel__generator@7.6.4':
dependencies:
'@babel/types': 7.27.1
'@babel/types': 7.25.6
'@types/babel__template@7.4.1':
dependencies:
'@babel/parser': 7.27.2
'@babel/types': 7.27.1
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
'@types/babel__traverse@7.20.0':
dependencies:
'@babel/types': 7.27.1
'@babel/types': 7.25.6
'@types/bcryptjs@2.4.6': {}
@@ -15065,7 +15133,7 @@ snapshots:
'@typescript-eslint/types': 8.32.1
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.32.1
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
eslint: 9.27.0
typescript: 5.8.3
transitivePeerDependencies:
@@ -15077,7 +15145,7 @@ snapshots:
'@typescript-eslint/types': 8.33.0
'@typescript-eslint/typescript-estree': 8.33.0(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.33.0
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
eslint: 9.27.0
typescript: 5.8.3
transitivePeerDependencies:
@@ -15087,7 +15155,7 @@ snapshots:
dependencies:
'@typescript-eslint/tsconfig-utils': 8.33.0(typescript@5.8.3)
'@typescript-eslint/types': 8.33.0
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
- typescript
@@ -15110,7 +15178,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 8.32.1(typescript@5.8.3)
'@typescript-eslint/utils': 8.32.1(eslint@9.27.0)(typescript@5.8.3)
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
eslint: 9.27.0
ts-api-utils: 2.1.0(typescript@5.8.3)
typescript: 5.8.3
@@ -15121,7 +15189,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 8.33.0(typescript@5.8.3)
'@typescript-eslint/utils': 8.33.0(eslint@9.27.0)(typescript@5.8.3)
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
eslint: 9.27.0
ts-api-utils: 2.1.0(typescript@5.8.3)
typescript: 5.8.3
@@ -15136,7 +15204,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 8.32.1
'@typescript-eslint/visitor-keys': 8.32.1
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
@@ -15152,7 +15220,7 @@ snapshots:
'@typescript-eslint/tsconfig-utils': 8.33.0(typescript@5.8.3)
'@typescript-eslint/types': 8.33.0
'@typescript-eslint/visitor-keys': 8.33.0
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
@@ -15205,7 +15273,7 @@ snapshots:
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6
@@ -15325,14 +15393,6 @@ snapshots:
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-core@3.5.14':
dependencies:
'@babel/parser': 7.27.2
'@vue/shared': 3.5.14
entities: 4.5.0
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-core@3.5.16':
dependencies:
'@babel/parser': 7.27.2
@@ -15346,11 +15406,6 @@ snapshots:
'@vue/compiler-core': 3.5.13
'@vue/shared': 3.5.13
'@vue/compiler-dom@3.5.14':
dependencies:
'@vue/compiler-core': 3.5.14
'@vue/shared': 3.5.14
'@vue/compiler-dom@3.5.16':
dependencies:
'@vue/compiler-core': 3.5.16
@@ -15381,8 +15436,8 @@ snapshots:
'@vue/language-core@2.0.16(typescript@5.8.3)':
dependencies:
'@volar/language-core': 2.2.0
'@vue/compiler-dom': 3.5.14
'@vue/shared': 3.5.14
'@vue/compiler-dom': 3.5.13
'@vue/shared': 3.5.13
computeds: 0.0.1
minimatch: 9.0.5
path-browserify: 1.0.1
@@ -15395,7 +15450,7 @@ snapshots:
'@volar/language-core': 2.4.11
'@vue/compiler-dom': 3.5.13
'@vue/compiler-vue2': 2.7.16
'@vue/shared': 3.5.14
'@vue/shared': 3.5.13
alien-signals: 1.0.3
minimatch: 9.0.5
muggle-string: 0.4.1
@@ -15427,8 +15482,6 @@ snapshots:
'@vue/shared@3.5.13': {}
'@vue/shared@3.5.14': {}
'@vue/shared@3.5.16': {}
'@vue/test-utils@2.4.1(@vue/server-renderer@3.5.16(vue@3.5.16(typescript@5.8.3)))(vue@3.5.16(typescript@5.8.3))':
@@ -15546,7 +15599,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
optional: true
@@ -15872,7 +15925,7 @@ snapshots:
babel-plugin-jest-hoist@29.6.3:
dependencies:
'@babel/template': 7.24.7
'@babel/types': 7.27.1
'@babel/types': 7.25.6
'@types/babel__core': 7.20.0
'@types/babel__traverse': 7.20.0
@@ -15900,7 +15953,7 @@ snapshots:
babel-walk@3.0.0-canary-5:
dependencies:
'@babel/types': 7.27.1
'@babel/types': 7.25.6
bail@2.0.2: {}
@@ -16176,6 +16229,8 @@ snapshots:
chalk@5.4.1: {}
change-case@5.4.4: {}
char-regex@1.0.2: {}
character-entities-html4@2.1.0: {}
@@ -16341,6 +16396,8 @@ snapshots:
colord@2.9.3: {}
colorette@1.4.0: {}
colorette@2.0.19: {}
colors@1.4.0:
@@ -16403,7 +16460,7 @@ snapshots:
constantinople@4.0.1:
dependencies:
'@babel/parser': 7.27.2
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
content-disposition@0.5.4:
@@ -16668,6 +16725,12 @@ snapshots:
optionalDependencies:
supports-color: 8.1.1
debug@4.4.1(supports-color@10.0.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 10.0.0
debug@4.4.1(supports-color@5.5.0):
dependencies:
ms: 2.1.3
@@ -17037,7 +17100,7 @@ snapshots:
esbuild-register@3.5.0(esbuild@0.25.5):
dependencies:
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
esbuild: 0.25.5
transitivePeerDependencies:
- supports-color
@@ -17251,7 +17314,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
escape-string-regexp: 4.0.0
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
@@ -17679,7 +17742,7 @@ snapshots:
follow-redirects@1.15.9(debug@4.4.1):
optionalDependencies:
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
for-each@0.3.3:
dependencies:
@@ -18077,7 +18140,7 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.3
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
@@ -18105,15 +18168,15 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
optional: true
https-proxy-agent@7.0.6:
https-proxy-agent@7.0.6(supports-color@10.0.0):
dependencies:
agent-base: 7.1.3
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
@@ -18178,6 +18241,8 @@ snapshots:
indent-string@5.0.0: {}
index-to-position@1.1.0: {}
inflight@1.0.6:
dependencies:
once: 1.4.0
@@ -18209,7 +18274,7 @@ snapshots:
dependencies:
'@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
@@ -18419,7 +18484,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1:
dependencies:
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
istanbul-lib-coverage: 3.2.2
source-map: 0.6.1
transitivePeerDependencies:
@@ -18428,7 +18493,7 @@ snapshots:
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@@ -18819,6 +18884,8 @@ snapshots:
glob: 8.1.0
nopt: 6.0.0
js-levenshtein@1.1.6: {}
js-stringify@1.0.2: {}
js-tokens@4.0.0: {}
@@ -18847,7 +18914,7 @@ snapshots:
decimal.js: 10.5.0
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
https-proxy-agent: 7.0.6(supports-color@10.0.0)
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.16
parse5: 7.3.0
@@ -19474,7 +19541,7 @@ snapshots:
micromark@4.0.0:
dependencies:
'@types/debug': 4.1.12
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
decode-named-character-reference: 1.0.2
devlop: 1.1.0
micromark-core-commonmark: 2.0.0
@@ -19667,7 +19734,7 @@ snapshots:
path-to-regexp: 6.3.0
picocolors: 1.1.1
strict-event-emitter: 0.5.1
type-fest: 4.26.1
type-fest: 4.41.0
yargs: 17.7.2
optionalDependencies:
typescript: 5.8.3
@@ -19974,13 +20041,14 @@ snapshots:
openapi-types@12.1.3: {}
openapi-typescript@6.7.6:
openapi-typescript@7.8.0(typescript@5.8.3):
dependencies:
'@redocly/openapi-core': 1.34.3(supports-color@10.0.0)
ansi-colors: 4.1.3
fast-glob: 3.3.3
js-yaml: 4.1.0
supports-color: 9.4.0
undici: 5.28.5
change-case: 5.4.4
parse-json: 8.3.0
supports-color: 10.0.0
typescript: 5.8.3
yargs-parser: 21.1.1
optionator@0.9.4:
@@ -20054,6 +20122,12 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
parse-json@8.3.0:
dependencies:
'@babel/code-frame': 7.27.1
index-to-position: 1.1.0
type-fest: 4.41.0
parse-ms@4.0.0: {}
parse-srcset@1.0.2: {}
@@ -20229,6 +20303,8 @@ snapshots:
dependencies:
irregular-plurals: 3.5.0
pluralize@8.0.0: {}
pngjs@5.0.0: {}
pnpm@10.11.0: {}
@@ -20863,7 +20939,7 @@ snapshots:
require-in-the-middle@7.3.0:
dependencies:
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
module-details-from-path: 1.0.3
resolve: 1.22.8
transitivePeerDependencies:
@@ -21220,7 +21296,7 @@ snapshots:
dependencies:
'@hapi/hoek': 11.0.4
'@hapi/wreck': 18.0.1
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
joi: 17.13.3
transitivePeerDependencies:
- supports-color
@@ -21320,7 +21396,7 @@ snapshots:
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.3
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
socks: 2.8.4
transitivePeerDependencies:
- supports-color
@@ -21417,7 +21493,7 @@ snapshots:
arg: 5.0.2
bluebird: 3.7.2
check-more-types: 2.24.0
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
execa: 5.1.1
lazy-ass: 1.6.0
ps-tree: 1.2.0
@@ -21595,7 +21671,7 @@ snapshots:
dependencies:
component-emitter: 1.3.1
cookiejar: 2.1.4
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
fast-safe-stringify: 2.1.1
form-data: 4.0.2
formidable: 3.5.4
@@ -21612,6 +21688,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
supports-color@10.0.0: {}
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
@@ -21624,8 +21702,6 @@ snapshots:
dependencies:
has-flag: 4.0.0
supports-color@9.4.0: {}
supports-hyperlinks@2.3.0:
dependencies:
has-flag: 4.0.0
@@ -21894,6 +21970,8 @@ snapshots:
type-fest@4.26.1: {}
type-fest@4.41.0: {}
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
@@ -21940,7 +22018,7 @@ snapshots:
app-root-path: 3.1.0
buffer: 6.0.3
dayjs: 1.11.13
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
dedent: 1.6.0
dotenv: 16.4.7
glob: 10.4.5
@@ -22061,6 +22139,8 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
uri-js-replace@1.0.1: {}
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
@@ -22136,7 +22216,7 @@ snapshots:
vite-node@3.1.4(@types/node@22.15.28)(sass@1.89.0)(terser@5.39.2)(tsx@4.19.4):
dependencies:
cac: 6.7.14
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.3.5(@types/node@22.15.28)(sass@1.89.0)(terser@5.39.2)(tsx@4.19.4)
@@ -22185,7 +22265,7 @@ snapshots:
'@vitest/spy': 3.1.4
'@vitest/utils': 3.1.4
chai: 5.2.0
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
expect-type: 1.2.1
magic-string: 0.30.17
pathe: 2.0.3
@@ -22257,7 +22337,7 @@ snapshots:
vue-docgen-api@4.75.1(vue@3.5.16(typescript@5.8.3)):
dependencies:
'@babel/parser': 7.27.2
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
'@vue/compiler-dom': 3.5.13
'@vue/compiler-sfc': 3.5.16
@@ -22272,7 +22352,7 @@ snapshots:
vue-eslint-parser@10.1.3(eslint@9.27.0):
dependencies:
debug: 4.4.1(supports-color@5.5.0)
debug: 4.4.1(supports-color@10.0.0)
eslint: 9.27.0
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
@@ -22341,7 +22421,7 @@ snapshots:
dependencies:
asn1.js: 5.4.1
http_ece: 1.2.0
https-proxy-agent: 7.0.6
https-proxy-agent: 7.0.6(supports-color@10.0.0)
jws: 4.0.0
minimist: 1.2.8
transitivePeerDependencies:
@@ -22435,7 +22515,7 @@ snapshots:
with@7.0.2:
dependencies:
'@babel/parser': 7.27.2
'@babel/parser': 7.25.6
'@babel/types': 7.25.6
assert-never: 1.2.1
babel-walk: 3.0.0-canary-5
@@ -22503,6 +22583,8 @@ snapshots:
yallist@5.0.0: {}
yaml-ast-parser@0.0.43: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
+1 -6
View File
@@ -40,6 +40,7 @@
'packages/misskey-bubble-game/**/package.json',
'packages/misskey-reversi/**/package.json',
'packages/sw/**/package.json',
'packages/icons-subsetter/**/package.json',
],
// prevent wastage of Chromatic snapshots
rebaseWhen: 'never',
@@ -62,12 +63,6 @@
'scripts/**/package.json',
],
},
{
groupName: '[icons-subsetter] Update dependencies',
matchFileNames: [
'packages/icons-subsetter/**/package.json',
],
},
{
groupName: '[GitHub Actions] Update dependencies',
matchFileNames: [