Compare commits
7 Commits
218df19d9c
...
65ba33867b
Author | SHA1 | Date |
---|---|---|
|
65ba33867b | |
|
b55cc03621 | |
|
80f73c6712 | |
|
60fc9a5195 | |
|
b43dfa260b | |
|
e3b57a118d | |
|
fdcb6a09a9 |
|
@ -11,9 +11,11 @@
|
||||||
- Fix: ドライブファイルの選択が不安定な問題を修正
|
- Fix: ドライブファイルの選択が不安定な問題を修正
|
||||||
- Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正
|
- Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正
|
||||||
- Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正
|
- Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正
|
||||||
|
- Fix: タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all)
|
- Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all)
|
||||||
|
- Fix: アカウント削除が正常に行われないことがあった問題を修正
|
||||||
|
|
||||||
|
|
||||||
## 2025.6.0
|
## 2025.6.0
|
||||||
|
|
|
@ -1365,6 +1365,8 @@ abort: "Cancel·lar"
|
||||||
tip: "Trucs i consells"
|
tip: "Trucs i consells"
|
||||||
redisplayAllTips: "Torna ha mostrat tots els trucs i consells"
|
redisplayAllTips: "Torna ha mostrat tots els trucs i consells"
|
||||||
hideAllTips: "Amagar 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:
|
_chat:
|
||||||
noMessagesYet: "Encara no tens missatges "
|
noMessagesYet: "Encara no tens missatges "
|
||||||
newMessage: "Missatge nou"
|
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_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ó."
|
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"
|
showUrlPreview: "Mostrar vista prèvia d'URL"
|
||||||
|
showAvailableReactionsFirstInNote: "Mostra les reacciones que pots fer servir al damunt"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "Mostrar el nom del remitent"
|
showSenderName: "Mostrar el nom del remitent"
|
||||||
sendOnEnter: "Introdueix per enviar"
|
sendOnEnter: "Introdueix per enviar"
|
||||||
|
@ -3117,7 +3120,15 @@ _clip:
|
||||||
tip: "Clip és una funció que permet organitzar les teves notes."
|
tip: "Clip és una funció que permet organitzar les teves notes."
|
||||||
_userLists:
|
_userLists:
|
||||||
tip: "Es poden crear llistes amb qualsevol usuari. La llista creada es pot mostrar com una línia de temps."
|
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:
|
_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"
|
opacity: "Opacitat"
|
||||||
scale: "Mida"
|
scale: "Mida"
|
||||||
text: "Text"
|
text: "Text"
|
||||||
|
@ -3125,4 +3136,32 @@ _watermarkEditor:
|
||||||
type: "Tipus"
|
type: "Tipus"
|
||||||
image: "Imatges"
|
image: "Imatges"
|
||||||
advanced: "Avançat"
|
advanced: "Avançat"
|
||||||
|
stripe: "Bandes"
|
||||||
|
stripeWidth: "Amplada de la banda"
|
||||||
|
stripeFrequency: "Freqüència de la banda"
|
||||||
angle: "Angle"
|
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"
|
||||||
|
|
|
@ -3002,6 +3002,7 @@ _search:
|
||||||
pleaseSelectUser: "Benutzer auswählen"
|
pleaseSelectUser: "Benutzer auswählen"
|
||||||
serverHostPlaceholder: "Beispiel: misskey.example.com"
|
serverHostPlaceholder: "Beispiel: misskey.example.com"
|
||||||
_watermarkEditor:
|
_watermarkEditor:
|
||||||
|
driveFileTypeWarn: "Diese Datei wird nicht unterstützt"
|
||||||
opacity: "Transparenz"
|
opacity: "Transparenz"
|
||||||
scale: "Größe"
|
scale: "Größe"
|
||||||
text: "Text"
|
text: "Text"
|
||||||
|
|
|
@ -3118,6 +3118,7 @@ _clip:
|
||||||
_userLists:
|
_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."
|
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:
|
_watermarkEditor:
|
||||||
|
driveFileTypeWarn: "This file is not supported"
|
||||||
opacity: "Opacity"
|
opacity: "Opacity"
|
||||||
scale: "Size"
|
scale: "Size"
|
||||||
text: "Text"
|
text: "Text"
|
||||||
|
|
|
@ -2934,6 +2934,7 @@ _search:
|
||||||
_uploader:
|
_uploader:
|
||||||
allowedTypes: "Tipos de archivos que se pueden cargar."
|
allowedTypes: "Tipos de archivos que se pueden cargar."
|
||||||
_watermarkEditor:
|
_watermarkEditor:
|
||||||
|
driveFileTypeWarn: "Este archivo es incompatible"
|
||||||
opacity: "Opacidad"
|
opacity: "Opacidad"
|
||||||
scale: "Tamaño"
|
scale: "Tamaño"
|
||||||
text: "Texto"
|
text: "Texto"
|
||||||
|
|
|
@ -2361,6 +2361,7 @@ _search:
|
||||||
searchScopeLocal: "Local"
|
searchScopeLocal: "Local"
|
||||||
searchScopeUser: "Spécifier l'utilisateur·rice"
|
searchScopeUser: "Spécifier l'utilisateur·rice"
|
||||||
_watermarkEditor:
|
_watermarkEditor:
|
||||||
|
driveFileTypeWarn: "Ce fichier n'est pas pris en charge"
|
||||||
opacity: "Transparence"
|
opacity: "Transparence"
|
||||||
scale: "Taille"
|
scale: "Taille"
|
||||||
text: "Texte"
|
text: "Texte"
|
||||||
|
|
|
@ -2609,6 +2609,7 @@ _search:
|
||||||
searchScopeLocal: "Lokal"
|
searchScopeLocal: "Lokal"
|
||||||
searchScopeUser: "Pengguna spesifik"
|
searchScopeUser: "Pengguna spesifik"
|
||||||
_watermarkEditor:
|
_watermarkEditor:
|
||||||
|
driveFileTypeWarn: "Berkas ini tidak didukung"
|
||||||
opacity: "Opasitas"
|
opacity: "Opasitas"
|
||||||
scale: "Ukuran"
|
scale: "Ukuran"
|
||||||
text: "Teks"
|
text: "Teks"
|
||||||
|
|
|
@ -12049,6 +12049,14 @@ export interface Locale extends ILocale {
|
||||||
* 保存せずに終了しますか?
|
* 保存せずに終了しますか?
|
||||||
*/
|
*/
|
||||||
"quitWithoutSaveConfirm": string;
|
"quitWithoutSaveConfirm": string;
|
||||||
|
/**
|
||||||
|
* このファイルは対応していません
|
||||||
|
*/
|
||||||
|
"driveFileTypeWarn": string;
|
||||||
|
/**
|
||||||
|
* 画像ファイルを選択してください
|
||||||
|
*/
|
||||||
|
"driveFileTypeWarnDescription": string;
|
||||||
/**
|
/**
|
||||||
* ウォーターマークの編集
|
* ウォーターマークの編集
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -3113,6 +3113,7 @@ _clip:
|
||||||
_userLists:
|
_userLists:
|
||||||
tip: "Puoi creare un elenco di Note create da qualsiasi profilo. L'elenco è visualizzato come una sequenza temporale."
|
tip: "Puoi creare un elenco di Note create da qualsiasi profilo. L'elenco è visualizzato come una sequenza temporale."
|
||||||
_watermarkEditor:
|
_watermarkEditor:
|
||||||
|
driveFileTypeWarn: "Formato file non supportato"
|
||||||
opacity: "Opacità"
|
opacity: "Opacità"
|
||||||
scale: "Dimensioni"
|
scale: "Dimensioni"
|
||||||
text: "Testo"
|
text: "Testo"
|
||||||
|
|
|
@ -3227,6 +3227,8 @@ defaultPreset: "デフォルトのプリセット"
|
||||||
_watermarkEditor:
|
_watermarkEditor:
|
||||||
tip: "画像にクレジット情報などのウォーターマークを追加することができます。"
|
tip: "画像にクレジット情報などのウォーターマークを追加することができます。"
|
||||||
quitWithoutSaveConfirm: "保存せずに終了しますか?"
|
quitWithoutSaveConfirm: "保存せずに終了しますか?"
|
||||||
|
driveFileTypeWarn: "このファイルは対応していません"
|
||||||
|
driveFileTypeWarnDescription: "画像ファイルを選択してください"
|
||||||
title: "ウォーターマークの編集"
|
title: "ウォーターマークの編集"
|
||||||
cover: "全体に被せる"
|
cover: "全体に被せる"
|
||||||
repeat: "敷き詰める"
|
repeat: "敷き詰める"
|
||||||
|
|
|
@ -2849,6 +2849,7 @@ _search:
|
||||||
searchScopeLocal: "ローカル"
|
searchScopeLocal: "ローカル"
|
||||||
searchScopeUser: "ユーザー指定"
|
searchScopeUser: "ユーザー指定"
|
||||||
_watermarkEditor:
|
_watermarkEditor:
|
||||||
|
driveFileTypeWarn: "このファイルは対応しとらへん"
|
||||||
opacity: "不透明度"
|
opacity: "不透明度"
|
||||||
scale: "大きさ"
|
scale: "大きさ"
|
||||||
text: "テキスト"
|
text: "テキスト"
|
||||||
|
|
|
@ -1365,6 +1365,8 @@ abort: "중지"
|
||||||
tip: "팁과 유용한 정보"
|
tip: "팁과 유용한 정보"
|
||||||
redisplayAllTips: "모든 '팁과 유용한 정보'를 재표시"
|
redisplayAllTips: "모든 '팁과 유용한 정보'를 재표시"
|
||||||
hideAllTips: "모든 '팁과 유용한 정보'를 비표시"
|
hideAllTips: "모든 '팁과 유용한 정보'를 비표시"
|
||||||
|
defaultImageCompressionLevel: "기본 이미지 압축 정도"
|
||||||
|
defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다. <br>높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다."
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "아직 메시지가 없습니다"
|
noMessagesYet: "아직 메시지가 없습니다"
|
||||||
newMessage: "새로운 메시지"
|
newMessage: "새로운 메시지"
|
||||||
|
@ -1452,6 +1454,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description: "높을수록 실시간으로 콘텐츠가 업데이트됩니다만, 성능이 저하되고 데이터 사용량과 배터리의 소비가 증가합니다."
|
contentsUpdateFrequency_description: "높을수록 실시간으로 콘텐츠가 업데이트됩니다만, 성능이 저하되고 데이터 사용량과 배터리의 소비가 증가합니다."
|
||||||
contentsUpdateFrequency_description2: "실시간 모드가 켜져 있을 때는 이 설정과 상관없이 실시간으로 콘텐츠가 업데이트됩니다."
|
contentsUpdateFrequency_description2: "실시간 모드가 켜져 있을 때는 이 설정과 상관없이 실시간으로 콘텐츠가 업데이트됩니다."
|
||||||
showUrlPreview: "URL 미리보기 표시"
|
showUrlPreview: "URL 미리보기 표시"
|
||||||
|
showAvailableReactionsFirstInNote: "이용 가능한 리액션을 선두로 표시"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "발신자 이름 표시"
|
showSenderName: "발신자 이름 표시"
|
||||||
sendOnEnter: "엔터로 보내기"
|
sendOnEnter: "엔터로 보내기"
|
||||||
|
@ -3117,7 +3120,15 @@ _clip:
|
||||||
tip: "클립은 노트를 정리할 수 있는 기능입니다."
|
tip: "클립은 노트를 정리할 수 있는 기능입니다."
|
||||||
_userLists:
|
_userLists:
|
||||||
tip: "임의의 유저가 포함된 리스트를 작성할 수 있습니다. 작성한 리스트는 타임라인으로 표시가 가능합니다."
|
tip: "임의의 유저가 포함된 리스트를 작성할 수 있습니다. 작성한 리스트는 타임라인으로 표시가 가능합니다."
|
||||||
|
watermark: "워터마크"
|
||||||
|
defaultPreset: "기본 프리셋"
|
||||||
_watermarkEditor:
|
_watermarkEditor:
|
||||||
|
tip: "이미지에 크레딧 정보 등의 워터마크를 추가할 수 있습니다."
|
||||||
|
quitWithoutSaveConfirm: "보존하지 않고 종료하시겠습니까?"
|
||||||
|
driveFileTypeWarn: "이 파이"
|
||||||
|
title: "워터마크 편집"
|
||||||
|
cover: "전체에 붙이기"
|
||||||
|
repeat: "전면에 깔기"
|
||||||
opacity: "불투명도"
|
opacity: "불투명도"
|
||||||
scale: "크기"
|
scale: "크기"
|
||||||
text: "텍스트"
|
text: "텍스트"
|
||||||
|
@ -3125,4 +3136,32 @@ _watermarkEditor:
|
||||||
type: "종류"
|
type: "종류"
|
||||||
image: "이미지"
|
image: "이미지"
|
||||||
advanced: "고급"
|
advanced: "고급"
|
||||||
|
stripe: "줄무늬"
|
||||||
|
stripeWidth: "라인의 폭"
|
||||||
|
stripeFrequency: "라인의 수"
|
||||||
angle: "각도"
|
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: "체크 무늬"
|
||||||
|
|
|
@ -3085,6 +3085,7 @@ _clientPerformanceIssueTip:
|
||||||
makeSureDisabledAddons: "Desabilite extensões"
|
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."
|
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:
|
_watermarkEditor:
|
||||||
|
driveFileTypeWarn: "Esse arquivo não é compatível"
|
||||||
opacity: "Opacidade"
|
opacity: "Opacidade"
|
||||||
scale: "Tamanho"
|
scale: "Tamanho"
|
||||||
text: "Texto"
|
text: "Texto"
|
||||||
|
|
|
@ -2723,6 +2723,7 @@ _search:
|
||||||
searchScopeLocal: "ท้องถิ่น"
|
searchScopeLocal: "ท้องถิ่น"
|
||||||
searchScopeUser: "ผู้ใช้เฉพาะ"
|
searchScopeUser: "ผู้ใช้เฉพาะ"
|
||||||
_watermarkEditor:
|
_watermarkEditor:
|
||||||
|
driveFileTypeWarn: "ไม่รองรับไฟล์นี้"
|
||||||
opacity: "ความทึบแสง"
|
opacity: "ความทึบแสง"
|
||||||
scale: "ขนาด"
|
scale: "ขนาด"
|
||||||
text: "ข้อความ"
|
text: "ข้อความ"
|
||||||
|
|
|
@ -1365,6 +1365,8 @@ abort: "中止"
|
||||||
tip: "提示和技巧"
|
tip: "提示和技巧"
|
||||||
redisplayAllTips: "重新显示所有的提示和技巧"
|
redisplayAllTips: "重新显示所有的提示和技巧"
|
||||||
hideAllTips: "隐藏所有的提示和技巧"
|
hideAllTips: "隐藏所有的提示和技巧"
|
||||||
|
defaultImageCompressionLevel: "默认图像压缩等级"
|
||||||
|
defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。"
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "还没有消息"
|
noMessagesYet: "还没有消息"
|
||||||
newMessage: "新消息"
|
newMessage: "新消息"
|
||||||
|
@ -1452,6 +1454,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description: "设置越高,内容更新越实时,但性能会降低,并且会消耗更多的流量和电池。"
|
contentsUpdateFrequency_description: "设置越高,内容更新越实时,但性能会降低,并且会消耗更多的流量和电池。"
|
||||||
contentsUpdateFrequency_description2: "当实时模式开启时,无论此设置如何,内容都会实时更新。"
|
contentsUpdateFrequency_description2: "当实时模式开启时,无论此设置如何,内容都会实时更新。"
|
||||||
showUrlPreview: "显示 URL 预览"
|
showUrlPreview: "显示 URL 预览"
|
||||||
|
showAvailableReactionsFirstInNote: "在顶部显示可用的回应"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "显示发送者的名字"
|
showSenderName: "显示发送者的名字"
|
||||||
sendOnEnter: "回车键发送"
|
sendOnEnter: "回车键发送"
|
||||||
|
@ -3117,7 +3120,15 @@ _clip:
|
||||||
tip: "便签功能可以将帖子合并在一起。"
|
tip: "便签功能可以将帖子合并在一起。"
|
||||||
_userLists:
|
_userLists:
|
||||||
tip: "可创建包含任意用户的列表。已创建的列表可作为时间线查看。"
|
tip: "可创建包含任意用户的列表。已创建的列表可作为时间线查看。"
|
||||||
|
watermark: "水印"
|
||||||
|
defaultPreset: "默认预设"
|
||||||
_watermarkEditor:
|
_watermarkEditor:
|
||||||
|
tip: "可在图像内增加包含作者等信息的水印。"
|
||||||
|
quitWithoutSaveConfirm: "不保存就退出吗?"
|
||||||
|
driveFileTypeWarn: "不支持此文件"
|
||||||
|
title: "编辑水印"
|
||||||
|
cover: "覆盖全体"
|
||||||
|
repeat: "平铺"
|
||||||
opacity: "不透明度"
|
opacity: "不透明度"
|
||||||
scale: "大小"
|
scale: "大小"
|
||||||
text: "文本"
|
text: "文本"
|
||||||
|
@ -3125,4 +3136,32 @@ _watermarkEditor:
|
||||||
type: "类型"
|
type: "类型"
|
||||||
image: "图片"
|
image: "图片"
|
||||||
advanced: "高级"
|
advanced: "高级"
|
||||||
|
stripe: "条纹"
|
||||||
|
stripeWidth: "线条宽度"
|
||||||
|
stripeFrequency: "线条数量"
|
||||||
angle: "角度"
|
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: "提示與技巧"
|
tip: "提示與技巧"
|
||||||
redisplayAllTips: "重新顯示所有「提示與技巧」"
|
redisplayAllTips: "重新顯示所有「提示與技巧」"
|
||||||
hideAllTips: "隱藏所有「提示與技巧」"
|
hideAllTips: "隱藏所有「提示與技巧」"
|
||||||
|
defaultImageCompressionLevel: "預設的影像壓縮程度"
|
||||||
|
defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。<br>高的話可以減少檔案大小,但是會降低畫質。"
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "尚無訊息"
|
noMessagesYet: "尚無訊息"
|
||||||
newMessage: "新訊息"
|
newMessage: "新訊息"
|
||||||
|
@ -1452,6 +1454,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description: "頻率越高,內容更新越即時,但可能會降低效能,並增加資料傳輸量與電池消耗。\n"
|
contentsUpdateFrequency_description: "頻率越高,內容更新越即時,但可能會降低效能,並增加資料傳輸量與電池消耗。\n"
|
||||||
contentsUpdateFrequency_description2: "當即時模式開啟時,不論此設定為何,內容都會即時更新。"
|
contentsUpdateFrequency_description2: "當即時模式開啟時,不論此設定為何,內容都會即時更新。"
|
||||||
showUrlPreview: "顯示網址預覽"
|
showUrlPreview: "顯示網址預覽"
|
||||||
|
showAvailableReactionsFirstInNote: "將可用的反應顯示在頂部"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "顯示發送者的名稱"
|
showSenderName: "顯示發送者的名稱"
|
||||||
sendOnEnter: "按下 Enter 發送訊息"
|
sendOnEnter: "按下 Enter 發送訊息"
|
||||||
|
@ -3117,7 +3120,15 @@ _clip:
|
||||||
tip: "摘錄是一項可以用來整理貼文的功能。"
|
tip: "摘錄是一項可以用來整理貼文的功能。"
|
||||||
_userLists:
|
_userLists:
|
||||||
tip: "您可以建立包含任意使用者的清單。建立後的清單可以作為時間軸顯示。\n"
|
tip: "您可以建立包含任意使用者的清單。建立後的清單可以作為時間軸顯示。\n"
|
||||||
|
watermark: "浮水印"
|
||||||
|
defaultPreset: "預設值"
|
||||||
_watermarkEditor:
|
_watermarkEditor:
|
||||||
|
tip: "可以在圖片中以浮水印加上出處等資訊。"
|
||||||
|
quitWithoutSaveConfirm: "不儲存就退出嗎?"
|
||||||
|
driveFileTypeWarn: "不支援此檔案"
|
||||||
|
title: "編輯浮水印"
|
||||||
|
cover: "覆蓋整體"
|
||||||
|
repeat: "佈局"
|
||||||
opacity: "透明度"
|
opacity: "透明度"
|
||||||
scale: "大小"
|
scale: "大小"
|
||||||
text: "文字"
|
text: "文字"
|
||||||
|
@ -3125,4 +3136,32 @@ _watermarkEditor:
|
||||||
type: "類型"
|
type: "類型"
|
||||||
image: "圖片"
|
image: "圖片"
|
||||||
advanced: "進階"
|
advanced: "進階"
|
||||||
|
stripe: "條紋"
|
||||||
|
stripeWidth: "線條寬度"
|
||||||
|
stripeFrequency: "線條數量"
|
||||||
angle: "角度"
|
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: "棋盤格"
|
||||||
|
|
|
@ -803,14 +803,14 @@ export class DriveService {
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deletePostProcess(file, isExpired, deleter);
|
await this.deletePostProcess(file, isExpired, deleter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
|
private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
|
||||||
// リモートファイル期限切れ削除後は直リンクにする
|
// リモートファイル期限切れ削除後は直リンクにする
|
||||||
if (isExpired && file.userHost !== null && file.uri != null) {
|
if (isExpired && file.userHost !== null && file.uri != null) {
|
||||||
this.driveFilesRepository.update(file.id, {
|
await this.driveFilesRepository.update(file.id, {
|
||||||
isLink: true,
|
isLink: true,
|
||||||
url: file.uri,
|
url: file.uri,
|
||||||
thumbnailUrl: null,
|
thumbnailUrl: null,
|
||||||
|
@ -822,7 +822,7 @@ export class DriveService {
|
||||||
webpublicAccessKey: 'webpublic-' + randomUUID(),
|
webpublicAccessKey: 'webpublic-' + randomUUID(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.driveFilesRepository.delete(file.id);
|
await this.driveFilesRepository.delete(file.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.driveChart.update(file, false);
|
this.driveChart.update(file, false);
|
||||||
|
|
|
@ -380,9 +380,7 @@ describe('User', () => {
|
||||||
strictEqual(followers.length, 1); // followed by Bob
|
strictEqual(followers.length, 1); // followed by Bob
|
||||||
|
|
||||||
await alice.client.request('i/delete-account', { password: alice.password });
|
await alice.client.request('i/delete-account', { password: alice.password });
|
||||||
// NOTE: user deletion query is slow
|
await sleep();
|
||||||
// FIXME: ensure user is removed successfully
|
|
||||||
await sleep(10000);
|
|
||||||
|
|
||||||
const following = await bob.client.request('users/following', { userId: bob.id });
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
strictEqual(following.length, 0); // no following relation
|
strictEqual(following.length, 0); // no following relation
|
||||||
|
@ -480,9 +478,7 @@ describe('User', () => {
|
||||||
strictEqual(followers.length, 1); // followed by Bob
|
strictEqual(followers.length, 1); // followed by Bob
|
||||||
|
|
||||||
await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
|
await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
|
||||||
// NOTE: user deletion query is slow
|
await sleep();
|
||||||
// FIXME: ensure user is removed successfully
|
|
||||||
await sleep(10000);
|
|
||||||
|
|
||||||
const following = await bob.client.request('users/following', { userId: bob.id });
|
const following = await bob.client.request('users/following', { userId: bob.id });
|
||||||
strictEqual(following.length, 0); // no following relation
|
strictEqual(following.length, 0); // no following relation
|
||||||
|
|
|
@ -51,7 +51,10 @@ if (props.fileId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectButton(ev: MouseEvent) {
|
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 (!file) return;
|
||||||
if (props.validate && !await props.validate(file)) return;
|
if (props.validate && !await props.validate(file)) return;
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,7 @@ const dialog = useTemplateRef('dialog');
|
||||||
async function cancel() {
|
async function cancel() {
|
||||||
if (layers.length > 0) {
|
if (layers.length > 0) {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
text: i18n.ts._imageEffector.discardChangesConfirm,
|
text: i18n.ts._imageEffector.discardChangesConfirm,
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
@ -132,7 +133,7 @@ function onLayerDelete(layer: ImageEffectorLayer) {
|
||||||
|
|
||||||
const canvasEl = useTemplateRef('canvasEl');
|
const canvasEl = useTemplateRef('canvasEl');
|
||||||
|
|
||||||
let renderer: ImageEffector | null = null;
|
let renderer: ImageEffector<typeof FXS> | null = null;
|
||||||
let imageBitmap: ImageBitmap | null = null;
|
let imageBitmap: ImageBitmap | null = null;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
|
@ -90,6 +90,14 @@ defineExpose({
|
||||||
&.asDrawer {
|
&.asDrawer {
|
||||||
height: calc(100dvh - 30px);
|
height: calc(100dvh - 30px);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
||||||
|
.body {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding-bottom: max(12px, env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -120,7 +120,7 @@ import { formatTimeString } from '@/utility/format-time-string.js';
|
||||||
import { Autocomplete } from '@/utility/autocomplete.js';
|
import { Autocomplete } from '@/utility/autocomplete.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { selectFiles } from '@/utility/drive.js';
|
import { selectFile } from '@/utility/drive.js';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -437,7 +437,11 @@ function focus() {
|
||||||
function chooseFileFrom(ev) {
|
function chooseFileFrom(ev) {
|
||||||
if (props.mock) return;
|
if (props.mock) return;
|
||||||
|
|
||||||
selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
|
selectFile({
|
||||||
|
anchorElement: ev.currentTarget ?? ev.target,
|
||||||
|
multiple: true,
|
||||||
|
label: i18n.ts.attachFile,
|
||||||
|
}).then(files_ => {
|
||||||
for (const file of files_) {
|
for (const file of files_) {
|
||||||
files.value.push(file);
|
files.value.push(file);
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,8 +79,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkModalWindow>
|
</MkModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export type UploaderDialogFeatures = {
|
||||||
|
effect?: boolean;
|
||||||
|
watermark?: boolean;
|
||||||
|
crop?: boolean;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, markRaw, onMounted, onUnmounted, ref, triggerRef, useTemplateRef, watch } from 'vue';
|
import { computed, markRaw, onMounted, onUnmounted, ref, triggerRef, useTemplateRef, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
||||||
|
@ -91,7 +99,6 @@ import { i18n } from '@/i18n.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
|
||||||
import { isWebpSupported } from '@/utility/isWebpSupported.js';
|
import { isWebpSupported } from '@/utility/isWebpSupported.js';
|
||||||
import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
|
import { uploadFile, UploadAbortedError } from '@/utility/drive.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
@ -131,17 +138,26 @@ const props = withDefaults(defineProps<{
|
||||||
files: File[];
|
files: File[];
|
||||||
folderId?: string | null;
|
folderId?: string | null;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
features?: UploaderDialogFeatures;
|
||||||
}>(), {
|
}>(), {
|
||||||
multiple: true,
|
multiple: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const uploaderFeatures = computed<Required<UploaderDialogFeatures>>(() => {
|
||||||
|
return {
|
||||||
|
effect: props.features?.effect ?? true,
|
||||||
|
watermark: props.features?.watermark ?? true,
|
||||||
|
crop: props.features?.crop ?? true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', driveFiles: Misskey.entities.DriveFile[]): void;
|
(ev: 'done', driveFiles: Misskey.entities.DriveFile[]): void;
|
||||||
(ev: 'canceled'): void;
|
(ev: 'canceled'): void;
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const items = ref<{
|
type UploaderItem = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
uploadName?: string;
|
uploadName?: string;
|
||||||
|
@ -152,13 +168,15 @@ const items = ref<{
|
||||||
uploaded: Misskey.entities.DriveFile | null;
|
uploaded: Misskey.entities.DriveFile | null;
|
||||||
uploadFailed: boolean;
|
uploadFailed: boolean;
|
||||||
aborted: boolean;
|
aborted: boolean;
|
||||||
compressionLevel: number;
|
compressionLevel: 0 | 1 | 2 | 3;
|
||||||
compressedSize?: number | null;
|
compressedSize?: number | null;
|
||||||
preprocessedFile?: Blob | null;
|
preprocessedFile?: Blob | null;
|
||||||
file: File;
|
file: File;
|
||||||
watermarkPresetId: string | null;
|
watermarkPresetId: string | null;
|
||||||
abort?: (() => void) | null;
|
abort?: (() => void) | null;
|
||||||
}[]>([]);
|
};
|
||||||
|
|
||||||
|
const items = ref<UploaderItem[]>([]);
|
||||||
|
|
||||||
const dialog = useTemplateRef('dialog');
|
const dialog = useTemplateRef('dialog');
|
||||||
|
|
||||||
|
@ -252,7 +270,7 @@ async function done() {
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
function showMenu(ev: MouseEvent, item: UploaderItem) {
|
||||||
const menu: MenuItem[] = [];
|
const menu: MenuItem[] = [];
|
||||||
|
|
||||||
menu.push({
|
menu.push({
|
||||||
|
@ -272,7 +290,13 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (CROPPING_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded) {
|
if (
|
||||||
|
uploaderFeatures.value.crop &&
|
||||||
|
CROPPING_SUPPORTED_TYPES.includes(item.file.type) &&
|
||||||
|
!item.preprocessing &&
|
||||||
|
!item.uploading &&
|
||||||
|
!item.uploaded
|
||||||
|
) {
|
||||||
menu.push({
|
menu.push({
|
||||||
icon: 'ti ti-crop',
|
icon: 'ti ti-crop',
|
||||||
text: i18n.ts.cropImage,
|
text: i18n.ts.cropImage,
|
||||||
|
@ -292,7 +316,13 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded) {
|
if (
|
||||||
|
uploaderFeatures.value.effect &&
|
||||||
|
IMAGE_EDITING_SUPPORTED_TYPES.includes(item.file.type) &&
|
||||||
|
!item.preprocessing &&
|
||||||
|
!item.uploading &&
|
||||||
|
!item.uploaded
|
||||||
|
) {
|
||||||
menu.push({
|
menu.push({
|
||||||
icon: 'ti ti-sparkles',
|
icon: 'ti ti-sparkles',
|
||||||
text: i18n.ts._imageEffector.title + ' (BETA)',
|
text: i18n.ts._imageEffector.title + ' (BETA)',
|
||||||
|
@ -318,7 +348,13 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (WATERMARK_SUPPORTED_TYPES.includes(item.file.type) && !item.preprocessing && !item.uploading && !item.uploaded) {
|
if (
|
||||||
|
uploaderFeatures.value.watermark &&
|
||||||
|
WATERMARK_SUPPORTED_TYPES.includes(item.file.type) &&
|
||||||
|
!item.preprocessing &&
|
||||||
|
!item.uploading &&
|
||||||
|
!item.uploaded
|
||||||
|
) {
|
||||||
function changeWatermarkPreset(presetId: string | null) {
|
function changeWatermarkPreset(presetId: string | null) {
|
||||||
item.watermarkPresetId = presetId;
|
item.watermarkPresetId = presetId;
|
||||||
preprocess(item).then(() => {
|
preprocess(item).then(() => {
|
||||||
|
@ -338,13 +374,13 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
||||||
}, {
|
}, {
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
}, ...prefer.s.watermarkPresets.map(preset => ({
|
}, ...prefer.s.watermarkPresets.map(preset => ({
|
||||||
type: 'radioOption',
|
type: 'radioOption' as const,
|
||||||
text: preset.name,
|
text: preset.name,
|
||||||
active: computed(() => item.watermarkPresetId === preset.id),
|
active: computed(() => item.watermarkPresetId === preset.id),
|
||||||
action: () => changeWatermarkPreset(preset.id),
|
action: () => changeWatermarkPreset(preset.id),
|
||||||
})), {
|
})), ...(prefer.s.watermarkPresets.length > 0 ? [{
|
||||||
type: 'divider',
|
type: 'divider' as const,
|
||||||
}, {
|
}] : []), {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
icon: 'ti ti-plus',
|
icon: 'ti ti-plus',
|
||||||
text: i18n.ts.add,
|
text: i18n.ts.add,
|
||||||
|
@ -397,8 +433,7 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
||||||
text: i18n.ts.high,
|
text: i18n.ts.high,
|
||||||
active: computed(() => item.compressionLevel === 3),
|
active: computed(() => item.compressionLevel === 3),
|
||||||
action: () => changeCompressionLevel(3),
|
action: () => changeCompressionLevel(3),
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -590,9 +625,9 @@ function initializeFile(file: File) {
|
||||||
uploaded: null,
|
uploaded: null,
|
||||||
uploadFailed: false,
|
uploadFailed: false,
|
||||||
compressionLevel: prefer.s.defaultImageCompressionLevel,
|
compressionLevel: prefer.s.defaultImageCompressionLevel,
|
||||||
watermarkPresetId: prefer.s.defaultWatermarkPresetId,
|
watermarkPresetId: uploaderFeatures.value.watermark ? prefer.s.defaultWatermarkPresetId : null,
|
||||||
file: markRaw(file),
|
file: markRaw(file),
|
||||||
};
|
} satisfies UploaderItem;
|
||||||
items.value.push(item);
|
items.value.push(item);
|
||||||
preprocess(item).then(() => {
|
preprocess(item).then(() => {
|
||||||
triggerRef(items);
|
triggerRef(items);
|
||||||
|
|
|
@ -262,10 +262,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, useTemplateRef, watch, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import type { WatermarkPreset } from '@/utility/watermark.js';
|
import type { WatermarkPreset } from '@/utility/watermark.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
@ -275,11 +275,10 @@ import MkPositionSelector from '@/components/MkPositionSelector.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { selectFile } from '@/utility/drive.js';
|
import { selectFile } from '@/utility/drive.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { prefer } from '@/preferences.js';
|
|
||||||
|
|
||||||
const layer = defineModel<WatermarkPreset['layers'][number]>('layer', { required: true });
|
const layer = defineModel<WatermarkPreset['layers'][number]>('layer', { required: true });
|
||||||
|
|
||||||
const driveFile = ref();
|
const driveFile = ref<Misskey.entities.DriveFile | null>(null);
|
||||||
const driveFileError = ref(false);
|
const driveFileError = ref(false);
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (layer.value.type === 'image' && layer.value.imageId != null) {
|
if (layer.value.type === 'image' && layer.value.imageId != null) {
|
||||||
|
@ -294,7 +293,15 @@ onMounted(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function chooseFile(ev: MouseEvent) {
|
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')) {
|
if (!file.type.startsWith('image')) {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
|
|
@ -124,7 +124,7 @@ function createStripeLayer(): WatermarkPreset['layers'][number] {
|
||||||
angle: 0.5,
|
angle: 0.5,
|
||||||
frequency: 10,
|
frequency: 10,
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
black: false,
|
color: [1, 1, 1],
|
||||||
opacity: 0.75,
|
opacity: 0.75,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ function createPolkadotLayer(): WatermarkPreset['layers'][number] {
|
||||||
majorOpacity: 0.75,
|
majorOpacity: 0.75,
|
||||||
minorOpacity: 0.5,
|
minorOpacity: 0.5,
|
||||||
minorDivisions: 4,
|
minorDivisions: 4,
|
||||||
black: false,
|
color: [1, 1, 1],
|
||||||
opacity: 0.75,
|
opacity: 0.75,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -151,7 +151,7 @@ function createCheckerLayer(): WatermarkPreset['layers'][number] {
|
||||||
type: 'checker',
|
type: 'checker',
|
||||||
angle: 0.5,
|
angle: 0.5,
|
||||||
scale: 3,
|
scale: 3,
|
||||||
black: false,
|
color: [1, 1, 1],
|
||||||
opacity: 0.75,
|
opacity: 0.75,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -177,6 +177,7 @@ const dialog = useTemplateRef('dialog');
|
||||||
|
|
||||||
async function cancel() {
|
async function cancel() {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
|
type: 'question',
|
||||||
text: i18n.ts._watermarkEditor.quitWithoutSaveConfirm,
|
text: i18n.ts._watermarkEditor.quitWithoutSaveConfirm,
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
|
@ -25,7 +25,14 @@ export function useChartTooltip(opts: { position: 'top' | 'middle' } = { positio
|
||||||
series: tooltipSeries,
|
series: tooltipSeries,
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
function windowTouchendHandler() {
|
||||||
|
tooltipShowing.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('touchend', windowTouchendHandler, { passive: true });
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('touchend', windowTouchendHandler);
|
||||||
disposeTooltipComponent();
|
disposeTooltipComponent();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
||||||
import type { Form, GetFormResultType } from '@/utility/form.js';
|
import type { Form, GetFormResultType } from '@/utility/form.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import type { PostFormProps } from '@/types/post-form.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 MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
|
||||||
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
|
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
@ -836,6 +837,7 @@ export function launchUploader(
|
||||||
options?: {
|
options?: {
|
||||||
folderId?: string | null;
|
folderId?: string | null;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
features?: UploaderDialogFeatures;
|
||||||
},
|
},
|
||||||
): Promise<Misskey.entities.DriveFile[]> {
|
): Promise<Misskey.entities.DriveFile[]> {
|
||||||
return new Promise(async (res, rej) => {
|
return new Promise(async (res, rej) => {
|
||||||
|
@ -844,6 +846,7 @@ export function launchUploader(
|
||||||
files: markRaw(files),
|
files: markRaw(files),
|
||||||
folderId: options?.folderId,
|
folderId: options?.folderId,
|
||||||
multiple: options?.multiple,
|
multiple: options?.multiple,
|
||||||
|
features: options?.features,
|
||||||
}, {
|
}, {
|
||||||
done: driveFiles => {
|
done: driveFiles => {
|
||||||
if (driveFiles.length === 0) return rej();
|
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],
|
bindTo: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required],
|
||||||
async customValueEditor(row, col, value, cellElement) {
|
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].url = file.url;
|
||||||
gridItems.value[row.index].fileId = file.id;
|
gridItems.value[row.index].fileId = file.id;
|
||||||
|
|
||||||
|
|
|
@ -188,7 +188,10 @@ async function archive() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBannerImage(evt) {
|
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;
|
bannerId.value = file.id;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,7 +168,11 @@ function onKeydown(ev: KeyboardEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseFile(ev: MouseEvent) {
|
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;
|
file.value = selectedFile;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,7 +214,10 @@ const menu = (ev: MouseEvent) => {
|
||||||
icon: 'ti ti-upload',
|
icon: 'ti ti-upload',
|
||||||
text: i18n.ts.import,
|
text: i18n.ts.import,
|
||||||
action: async () => {
|
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', {
|
misskeyApi('admin/emoji/import-zip', {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
})
|
})
|
||||||
|
|
|
@ -121,7 +121,10 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
|
||||||
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? props.emoji.url : null);
|
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? props.emoji.url : null);
|
||||||
|
|
||||||
async function changeImage(ev: Event) {
|
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(/\.(.+)$/, '');
|
const candidate = file.value.name.replace(/\.(.+)$/, '');
|
||||||
if (candidate.match(/^[a-z0-9_]+$/)) {
|
if (candidate.match(/^[a-z0-9_]+$/)) {
|
||||||
name.value = candidate;
|
name.value = candidate;
|
||||||
|
|
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="name">{{ file.name }}</div>
|
<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>
|
<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<MkSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</MkSwitch>
|
<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 MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import FormSuspense from '@/components/form/suspense.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 * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
@ -63,8 +63,11 @@ const description = ref<string | null>(null);
|
||||||
const title = ref<string | null>(null);
|
const title = ref<string | null>(null);
|
||||||
const isSensitive = ref(false);
|
const isSensitive = ref(false);
|
||||||
|
|
||||||
function selectFile(evt) {
|
function chooseFile(evt) {
|
||||||
selectFiles(evt.currentTarget ?? evt.target, null).then(selected => {
|
selectFile({
|
||||||
|
anchorElement: evt.currentTarget ?? evt.target,
|
||||||
|
multiple: true,
|
||||||
|
}).then(selected => {
|
||||||
files.value = files.value.concat(selected);
|
files.value = files.value.concat(selected);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,7 +205,10 @@ async function add() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setEyeCatchingImage(img: Event) {
|
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;
|
eyeCatchingImageId.value = file.id;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -233,7 +233,10 @@ const exportAntennas = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const importFollowing = async (ev) => {
|
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', {
|
misskeyApi('i/import-following', {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
withReplies: withReplies.value,
|
withReplies: withReplies.value,
|
||||||
|
@ -241,22 +244,34 @@ const importFollowing = async (ev) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const importUserLists = 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);
|
misskeyApi('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
|
||||||
};
|
};
|
||||||
|
|
||||||
const importMuting = async (ev) => {
|
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);
|
misskeyApi('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
|
||||||
};
|
};
|
||||||
|
|
||||||
const importBlocking = async (ev) => {
|
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);
|
misskeyApi('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
|
||||||
};
|
};
|
||||||
|
|
||||||
const importAntennas = async (ev) => {
|
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);
|
misskeyApi('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -114,7 +114,10 @@ watch(wallpaper, async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function setWallpaper(ev: MouseEvent) {
|
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;
|
wallpaper.value = file.url;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,11 @@ const friendlyFileName = computed<string>(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function selectSound(ev) {
|
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')) {
|
if (!file.type.startsWith('audio')) {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
|
|
@ -422,7 +422,7 @@ export const PREF_DEF = definePreferences({
|
||||||
default: null as WatermarkPreset['id'] | null,
|
default: null as WatermarkPreset['id'] | null,
|
||||||
},
|
},
|
||||||
defaultImageCompressionLevel: {
|
defaultImageCompressionLevel: {
|
||||||
default: 2,
|
default: 2 as 0 | 1 | 2 | 3,
|
||||||
},
|
},
|
||||||
|
|
||||||
'sound.masterVolume': {
|
'sound.masterVolume': {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { instance } from '@/instance.js';
|
||||||
import { globalEvents } from '@/events.js';
|
import { globalEvents } from '@/events.js';
|
||||||
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
|
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
|
||||||
import { genId } from '@/utility/id.js';
|
import { genId } from '@/utility/id.js';
|
||||||
|
import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue';
|
||||||
|
|
||||||
type UploadReturnType = {
|
type UploadReturnType = {
|
||||||
filePromise: Promise<Misskey.entities.DriveFile>;
|
filePromise: Promise<Misskey.entities.DriveFile>;
|
||||||
|
@ -155,6 +156,7 @@ export function uploadFile(file: File | Blob, options: {
|
||||||
export function chooseFileFromPcAndUpload(
|
export function chooseFileFromPcAndUpload(
|
||||||
options: {
|
options: {
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
features?: UploaderDialogFeatures;
|
||||||
folderId?: string | null;
|
folderId?: string | null;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<Misskey.entities.DriveFile[]> {
|
): Promise<Misskey.entities.DriveFile[]> {
|
||||||
|
@ -163,6 +165,7 @@ export function chooseFileFromPcAndUpload(
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
os.launchUploader(files, {
|
os.launchUploader(files, {
|
||||||
folderId: options.folderId,
|
folderId: options.folderId,
|
||||||
|
features: options.features,
|
||||||
}).then(driveFiles => {
|
}).then(driveFiles => {
|
||||||
res(driveFiles);
|
res(driveFiles);
|
||||||
});
|
});
|
||||||
|
@ -194,7 +197,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
|
||||||
type: 'url',
|
type: 'url',
|
||||||
placeholder: i18n.ts.uploadFromUrlDescription,
|
placeholder: i18n.ts.uploadFromUrlDescription,
|
||||||
}).then(({ canceled, result: url }) => {
|
}).then(({ canceled, result: url }) => {
|
||||||
if (canceled) return;
|
if (canceled || url == null) return;
|
||||||
|
|
||||||
const marker = genId();
|
const marker = genId();
|
||||||
|
|
||||||
|
@ -221,7 +224,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) => {
|
return new Promise((res, rej) => {
|
||||||
os.popupMenu([label ? {
|
os.popupMenu([label ? {
|
||||||
text: label,
|
text: label,
|
||||||
|
@ -229,7 +232,7 @@ function select(anchorElement: HTMLElement | EventTarget | null, label: string |
|
||||||
} : undefined, {
|
} : undefined, {
|
||||||
text: i18n.ts.upload,
|
text: i18n.ts.upload,
|
||||||
icon: 'ti ti-upload',
|
icon: 'ti ti-upload',
|
||||||
action: () => chooseFileFromPcAndUpload({ multiple }).then(files => res(files)),
|
action: () => chooseFileFromPcAndUpload({ multiple, features }).then(files => res(files)),
|
||||||
}, {
|
}, {
|
||||||
text: i18n.ts.fromDrive,
|
text: i18n.ts.fromDrive,
|
||||||
icon: 'ti ti-cloud',
|
icon: 'ti ti-cloud',
|
||||||
|
@ -242,12 +245,19 @@ function select(anchorElement: HTMLElement | EventTarget | null, label: string |
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectFile(anchorElement: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
|
type SelectFileOptions<M extends boolean> = {
|
||||||
return select(anchorElement, label, false).then(files => files[0]);
|
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[]> {
|
export async function selectFile<
|
||||||
return select(anchorElement, label, true);
|
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: {
|
export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: {
|
||||||
|
|
|
@ -57,7 +57,7 @@ function getValue<T extends keyof ParamTypeToPrimitive>(params: Record<string, a
|
||||||
return params[k];
|
return params[k];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImageEffector {
|
export class ImageEffector<IEX extends ReadonlyArray<ImageEffectorFx<any, any, any>>> {
|
||||||
private gl: WebGL2RenderingContext;
|
private gl: WebGL2RenderingContext;
|
||||||
private canvas: HTMLCanvasElement | null = null;
|
private canvas: HTMLCanvasElement | null = null;
|
||||||
private renderTextureProgram: WebGLProgram;
|
private renderTextureProgram: WebGLProgram;
|
||||||
|
@ -70,7 +70,7 @@ export class ImageEffector {
|
||||||
private shaderCache: Map<string, WebGLProgram> = new Map();
|
private shaderCache: Map<string, WebGLProgram> = new Map();
|
||||||
private perLayerResultTextures: Map<string, WebGLTexture> = new Map();
|
private perLayerResultTextures: Map<string, WebGLTexture> = new Map();
|
||||||
private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map();
|
private perLayerResultFrameBuffers: Map<string, WebGLFramebuffer> = new Map();
|
||||||
private fxs: ImageEffectorFx[];
|
private fxs: [...IEX];
|
||||||
private paramTextures: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map();
|
private paramTextures: Map<string, { texture: WebGLTexture; width: number; height: number; }> = new Map();
|
||||||
|
|
||||||
constructor(options: {
|
constructor(options: {
|
||||||
|
@ -78,7 +78,7 @@ export class ImageEffector {
|
||||||
renderWidth: number;
|
renderWidth: number;
|
||||||
renderHeight: number;
|
renderHeight: number;
|
||||||
image: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
|
image: ImageData | ImageBitmap | HTMLImageElement | HTMLCanvasElement;
|
||||||
fxs: ImageEffectorFx[];
|
fxs: [...IEX];
|
||||||
}) {
|
}) {
|
||||||
this.canvas = options.canvas;
|
this.canvas = options.canvas;
|
||||||
this.renderWidth = options.renderWidth;
|
this.renderWidth = options.renderWidth;
|
||||||
|
@ -230,7 +230,7 @@ export class ImageEffector {
|
||||||
gl: gl,
|
gl: gl,
|
||||||
program: shaderProgram,
|
program: shaderProgram,
|
||||||
params: Object.fromEntries(
|
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];
|
return [key, layer.params[key] ?? param.default];
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -238,7 +238,7 @@ export class ImageEffector {
|
||||||
width: this.renderWidth,
|
width: this.renderWidth,
|
||||||
height: this.renderHeight,
|
height: this.renderHeight,
|
||||||
textures: Object.fromEntries(
|
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];
|
if (v.type !== 'texture') return [k, null];
|
||||||
const param = getValue<typeof v.type>(layer.params, k);
|
const param = getValue<typeof v.type>(layer.params, k);
|
||||||
if (param == null) return [k, null];
|
if (param == null) return [k, null];
|
||||||
|
@ -329,7 +329,7 @@ export class ImageEffector {
|
||||||
unused.delete(textureKey);
|
unused.delete(textureKey);
|
||||||
if (this.paramTextures.has(textureKey)) continue;
|
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;
|
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;
|
if (texture == null) continue;
|
||||||
|
@ -339,7 +339,7 @@ export class ImageEffector {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const k of unused) {
|
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.gl.deleteTexture(this.paramTextures.get(k)!.texture);
|
||||||
this.paramTextures.delete(k);
|
this.paramTextures.delete(k);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,20 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FX_watermarkPlacement } from './image-effector/fxs/watermarkPlacement.js';
|
import { FX_watermarkPlacement } from '@/utility/image-effector/fxs/watermarkPlacement.js';
|
||||||
import { FX_stripe } from './image-effector/fxs/stripe.js';
|
import { FX_stripe } from '@/utility/image-effector/fxs/stripe.js';
|
||||||
import { FX_polkadot } from './image-effector/fxs/polkadot.js';
|
import { FX_polkadot } from '@/utility/image-effector/fxs/polkadot.js';
|
||||||
import { FX_checker } from './image-effector/fxs/checker.js';
|
import { FX_checker } from '@/utility/image-effector/fxs/checker.js';
|
||||||
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
|
import type { ImageEffectorFx, ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
|
||||||
import { ImageEffector } 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 = {
|
export type WatermarkPreset = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -64,7 +71,7 @@ export type WatermarkPreset = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class WatermarkRenderer {
|
export class WatermarkRenderer {
|
||||||
private effector: ImageEffector;
|
private effector: ImageEffector<typeof WATERMARK_FXS>;
|
||||||
private layers: WatermarkPreset['layers'] = [];
|
private layers: WatermarkPreset['layers'] = [];
|
||||||
|
|
||||||
constructor(options: {
|
constructor(options: {
|
||||||
|
@ -78,7 +85,7 @@ export class WatermarkRenderer {
|
||||||
renderWidth: options.renderWidth,
|
renderWidth: options.renderWidth,
|
||||||
renderHeight: options.renderHeight,
|
renderHeight: options.renderHeight,
|
||||||
image: options.image,
|
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,
|
opacity: layer.opacity,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown layer type`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
'packages/misskey-bubble-game/**/package.json',
|
'packages/misskey-bubble-game/**/package.json',
|
||||||
'packages/misskey-reversi/**/package.json',
|
'packages/misskey-reversi/**/package.json',
|
||||||
'packages/sw/**/package.json',
|
'packages/sw/**/package.json',
|
||||||
|
'packages/icons-subsetter/**/package.json',
|
||||||
],
|
],
|
||||||
// prevent wastage of Chromatic snapshots
|
// prevent wastage of Chromatic snapshots
|
||||||
rebaseWhen: 'never',
|
rebaseWhen: 'never',
|
||||||
|
@ -62,12 +63,6 @@
|
||||||
'scripts/**/package.json',
|
'scripts/**/package.json',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
groupName: '[icons-subsetter] Update dependencies',
|
|
||||||
matchFileNames: [
|
|
||||||
'packages/icons-subsetter/**/package.json',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
groupName: '[GitHub Actions] Update dependencies',
|
groupName: '[GitHub Actions] Update dependencies',
|
||||||
matchFileNames: [
|
matchFileNames: [
|
||||||
|
|
Loading…
Reference in New Issue