mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-31 04:15:51 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac9206f192 | |||
| e2b38edb3a | |||
| c5dc0fd51b | |||
| dda2ad6bcd | |||
| a5429ebeee | |||
| 1c3604c7fb | |||
| 4906f1f45c | |||
| 6506429f09 | |||
| 9bd5f887de | |||
| be35fe468b | |||
| 4b9b3ced01 | |||
| 20b8148ddf | |||
| 019dfbdc1c | |||
| 95ea62f222 | |||
| fde67dca74 | |||
| a603a4970e | |||
| f37a1e84bd | |||
| 6c9e055aae | |||
| a971e44cee | |||
| c6808f1eb6 | |||
| 2a78360588 | |||
| 65ba33867b | |||
| b55cc03621 | |||
| 80f73c6712 | |||
| 60fc9a5195 | |||
| b43dfa260b | |||
| e3b57a118d | |||
| fdcb6a09a9 | |||
| 218df19d9c | |||
| b766589c00 | |||
| d94bdff639 | |||
| 3a4288fe24 | |||
| 12c71de4c7 | |||
| b7aa013a41 |
@@ -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
@@ -6,12 +6,16 @@
|
||||
### Client
|
||||
- Feat: 画像にウォーターマークを付与できるようになりました
|
||||
- Enhance: ノートのリアクション一覧で、押せるリアクションを優先して表示できるようにするオプションを追加
|
||||
- Enhance: 全てのチャットメッセージを既読にできるように(設定→その他)
|
||||
- Enhance: ミュートした絵文字をデバイス間で同期できるように
|
||||
- Fix: ドライブファイルの選択が不安定な問題を修正
|
||||
- Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正
|
||||
- Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正
|
||||
- Fix: タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正
|
||||
|
||||
### Server
|
||||
-
|
||||
- Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all)
|
||||
- Fix: アカウント削除が正常に行われないことがあった問題を修正
|
||||
|
||||
|
||||
## 2025.6.0
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1589,3 +1589,11 @@ _search:
|
||||
searchScopeAll: "الكل"
|
||||
searchScopeLocal: "المحلي"
|
||||
searchScopeUser: "مستخدم محدد"
|
||||
_watermarkEditor:
|
||||
opacity: "الشفافية"
|
||||
scale: "الحجم"
|
||||
text: "نص"
|
||||
position: "الموضع"
|
||||
type: "نوع"
|
||||
image: "صور"
|
||||
advanced: "متقدم"
|
||||
|
||||
@@ -1349,3 +1349,9 @@ _remoteLookupErrors:
|
||||
_search:
|
||||
searchScopeAll: "সবগুলো"
|
||||
searchScopeLocal: "স্থানীয়"
|
||||
_watermarkEditor:
|
||||
opacity: "অস্বচ্ছতা"
|
||||
scale: "আকার"
|
||||
text: "লেখা"
|
||||
image: "ছবি"
|
||||
advanced: "উন্নত"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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é"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -403,3 +403,5 @@ _reversi:
|
||||
total: "Σύνολο"
|
||||
_search:
|
||||
searchScopeLocal: "Τοπικό"
|
||||
_watermarkEditor:
|
||||
image: "Εικόνες"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
+20
@@ -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;
|
||||
/**
|
||||
* 色の圧縮
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: "歪み"
|
||||
|
||||
@@ -2848,3 +2848,13 @@ _search:
|
||||
searchScopeAll: "みんな"
|
||||
searchScopeLocal: "ローカル"
|
||||
searchScopeUser: "ユーザー指定"
|
||||
_watermarkEditor:
|
||||
driveFileTypeWarn: "このファイルは対応しとらへん"
|
||||
opacity: "不透明度"
|
||||
scale: "大きさ"
|
||||
text: "テキスト"
|
||||
position: "位置"
|
||||
type: "タイプ"
|
||||
image: "画像"
|
||||
advanced: "高度"
|
||||
angle: "角度"
|
||||
|
||||
@@ -848,3 +848,5 @@ _remoteLookupErrors:
|
||||
_search:
|
||||
searchScopeAll: "말캉"
|
||||
searchScopeUser: "사용자 지정"
|
||||
_watermarkEditor:
|
||||
image: "이미지"
|
||||
|
||||
@@ -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: "체크 무늬"
|
||||
|
||||
@@ -483,3 +483,5 @@ _remoteLookupErrors:
|
||||
title: "ບໍ່ພົບ"
|
||||
_search:
|
||||
searchScopeAll: "ທັງໝົດ"
|
||||
_watermarkEditor:
|
||||
image: "ຮູບພາບ"
|
||||
|
||||
@@ -1078,3 +1078,6 @@ _remoteLookupErrors:
|
||||
title: "Niet gevonden"
|
||||
_search:
|
||||
searchScopeAll: "Alle"
|
||||
_watermarkEditor:
|
||||
image: "Afbeeldingen"
|
||||
advanced: "Geavanceerd"
|
||||
|
||||
@@ -735,3 +735,8 @@ _remoteLookupErrors:
|
||||
title: "Ikke funnet"
|
||||
_search:
|
||||
searchScopeAll: "Alle"
|
||||
_watermarkEditor:
|
||||
scale: "Størrelse"
|
||||
text: "Tekst"
|
||||
type: "Type"
|
||||
image: "Bilder"
|
||||
|
||||
@@ -1584,3 +1584,10 @@ _remoteLookupErrors:
|
||||
_search:
|
||||
searchScopeAll: "Wszystkie"
|
||||
searchScopeLocal: "Lokalne"
|
||||
_watermarkEditor:
|
||||
opacity: "Przezroczystość"
|
||||
scale: "Rozmiar"
|
||||
text: "Tekst"
|
||||
type: "Typ"
|
||||
image: "Zdjęcia"
|
||||
advanced: "Zaawansowane"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2191,3 +2191,12 @@ _search:
|
||||
searchScopeAll: "Все"
|
||||
searchScopeLocal: "Местная"
|
||||
searchScopeUser: "Указанный пользователь"
|
||||
_watermarkEditor:
|
||||
opacity: "Непрозрачность"
|
||||
scale: "Размер"
|
||||
text: "Текст"
|
||||
position: "Позиция"
|
||||
type: "Тип"
|
||||
image: "Изображения"
|
||||
advanced: "Для продвинутых"
|
||||
angle: "Угол"
|
||||
|
||||
@@ -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é"
|
||||
|
||||
@@ -711,3 +711,6 @@ _selfXssPrevention:
|
||||
warning: "VARNING"
|
||||
_search:
|
||||
searchScopeAll: "Allt"
|
||||
_watermarkEditor:
|
||||
scale: "Storlek"
|
||||
image: "Bilder"
|
||||
|
||||
@@ -2722,3 +2722,13 @@ _search:
|
||||
searchScopeAll: "ทั้งหมด"
|
||||
searchScopeLocal: "ท้องถิ่น"
|
||||
searchScopeUser: "ผู้ใช้เฉพาะ"
|
||||
_watermarkEditor:
|
||||
driveFileTypeWarn: "ไม่รองรับไฟล์นี้"
|
||||
opacity: "ความทึบแสง"
|
||||
scale: "ขนาด"
|
||||
text: "ข้อความ"
|
||||
position: "ตำแหน่ง"
|
||||
type: "รูปแบบ"
|
||||
image: "รูปภาพ"
|
||||
advanced: "ขั้นสูง"
|
||||
angle: "แองเกิล"
|
||||
|
||||
@@ -460,3 +460,5 @@ _moderationLogTypes:
|
||||
resetPassword: "Şifre sıfırlama"
|
||||
_search:
|
||||
searchScopeAll: "Tümü"
|
||||
_watermarkEditor:
|
||||
image: "Görseller"
|
||||
|
||||
@@ -1625,3 +1625,10 @@ _remoteLookupErrors:
|
||||
_search:
|
||||
searchScopeAll: "Всі"
|
||||
searchScopeLocal: "Локальна"
|
||||
_watermarkEditor:
|
||||
opacity: "Непрозорість"
|
||||
scale: "Розмір"
|
||||
text: "Текст"
|
||||
type: "Тип"
|
||||
image: "Зображення"
|
||||
advanced: "Розширені"
|
||||
|
||||
@@ -1097,3 +1097,8 @@ _remoteLookupErrors:
|
||||
_search:
|
||||
searchScopeAll: "Barcha"
|
||||
searchScopeLocal: "Mahalliy"
|
||||
_watermarkEditor:
|
||||
text: "Matn"
|
||||
type: "turi"
|
||||
image: "Rasmlar"
|
||||
advanced: "Murakkab"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: "检查"
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.6.1-alpha.0",
|
||||
"version": "2025.6.1-alpha.4",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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「設定」)
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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' };
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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 };
|
||||
|
||||
+35318
-30428
File diff suppressed because it is too large
Load Diff
@@ -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'>>;
|
||||
|
||||
|
||||
Generated
+188
-106
@@ -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
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user