commit
830e2f0a5b
|
@ -26,6 +26,8 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version-file: '.node-version'
|
node-version-file: '.node-version'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
# see https://docs.github.com/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
- name: Publish package
|
- name: Publish package
|
||||||
run: |
|
run: |
|
||||||
pnpm i --frozen-lockfile
|
pnpm i --frozen-lockfile
|
||||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -1,3 +1,30 @@
|
||||||
|
## 2025.6.1
|
||||||
|
|
||||||
|
### Note
|
||||||
|
- AiScript Misskey拡張API(Misskey Webプラグイン)の[note_view_interruptor](https://misskey-hub.net/ja/docs/for-developers/plugin/plugin-api-reference/#pluginregister_note_view_interruptorfn)は不具合の影響により現在一時的に無効化されています。
|
||||||
|
- Misskey Web投稿フォームのプレビュー切り替えは「...」メニュー内に配置されました
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Feat: 画像にウォーターマークを付与できるようになりました
|
||||||
|
- Feat: 画像の加工ができるようになりました(実験的)
|
||||||
|
- Enhance: ノートのリアクション一覧で、押せるリアクションを優先して表示できるようにするオプションを追加
|
||||||
|
- Enhance: 全てのチャットメッセージを既読にできるように(設定→その他)
|
||||||
|
- Enhance: ミュートした絵文字をデバイス間で同期できるように
|
||||||
|
- Fix: ドライブファイルの選択が不安定な問題を修正
|
||||||
|
- Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正
|
||||||
|
- Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正
|
||||||
|
- Fix: タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正
|
||||||
|
- Fix: ウェルカムタイムラインでリアクションが表示されない問題を修正
|
||||||
|
- Fix: デッキのタイムラインカラムで新着ノート時のサウンドが再生されない問題を修正
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all)
|
||||||
|
- Fix: アカウント削除が正常に行われないことがあった問題を修正
|
||||||
|
- Fix: outboxのページネーションが正しく行われない問題を修正
|
||||||
|
|
||||||
|
### Misskey.js
|
||||||
|
- Fix: misskey-jsの drive/file/create でファイルアップロードができない問題を修正
|
||||||
|
|
||||||
## 2025.6.0
|
## 2025.6.0
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
|
|
@ -34,7 +34,6 @@ describe('Before setup instance', () => {
|
||||||
|
|
||||||
cy.intercept('POST', '/api/admin/update-meta').as('update-meta');
|
cy.intercept('POST', '/api/admin/update-meta').as('update-meta');
|
||||||
|
|
||||||
cy.get('[data-cy-next]').click();
|
|
||||||
cy.get('[data-cy-next]').click();
|
cy.get('[data-cy-next]').click();
|
||||||
cy.get('[data-cy-server-name] input').type('Testskey');
|
cy.get('[data-cy-server-name] input').type('Testskey');
|
||||||
cy.get('[data-cy-server-setup-wizard-apply]').click();
|
cy.get('[data-cy-server-setup-wizard-apply]').click();
|
||||||
|
|
|
@ -1589,3 +1589,11 @@ _search:
|
||||||
searchScopeAll: "الكل"
|
searchScopeAll: "الكل"
|
||||||
searchScopeLocal: "المحلي"
|
searchScopeLocal: "المحلي"
|
||||||
searchScopeUser: "مستخدم محدد"
|
searchScopeUser: "مستخدم محدد"
|
||||||
|
_watermarkEditor:
|
||||||
|
opacity: "الشفافية"
|
||||||
|
scale: "الحجم"
|
||||||
|
text: "نص"
|
||||||
|
position: "الموضع"
|
||||||
|
type: "نوع"
|
||||||
|
image: "صور"
|
||||||
|
advanced: "متقدم"
|
||||||
|
|
|
@ -1349,3 +1349,9 @@ _remoteLookupErrors:
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "সবগুলো"
|
searchScopeAll: "সবগুলো"
|
||||||
searchScopeLocal: "স্থানীয়"
|
searchScopeLocal: "স্থানীয়"
|
||||||
|
_watermarkEditor:
|
||||||
|
opacity: "অস্বচ্ছতা"
|
||||||
|
scale: "আকার"
|
||||||
|
text: "লেখা"
|
||||||
|
image: "ছবি"
|
||||||
|
advanced: "উন্নত"
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -2462,6 +2465,8 @@ _visibility:
|
||||||
disableFederation: "Sense federar"
|
disableFederation: "Sense federar"
|
||||||
disableFederationDescription: "No enviar a altres servidors"
|
disableFederationDescription: "No enviar a altres servidors"
|
||||||
_postForm:
|
_postForm:
|
||||||
|
quitInspiteOfThereAreUnuploadedFilesConfirm: "Hi ha arxius que no s'han carregat, vols descartar-los i tancar el formulari?"
|
||||||
|
uploaderTip: "L'arxiu encara no s'ha carregat. Des del menú arxiu pots canviar el nom, retallar imatges, posar marques d'aigua i comprimir o no l'arxiu. Els arxius es carreguen automàticament quan públiques una nota."
|
||||||
replyPlaceholder: "Contestar..."
|
replyPlaceholder: "Contestar..."
|
||||||
quotePlaceholder: "Citar..."
|
quotePlaceholder: "Citar..."
|
||||||
channelPlaceholder: "Publicar a un canal..."
|
channelPlaceholder: "Publicar a un canal..."
|
||||||
|
@ -3117,3 +3122,50 @@ _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:
|
||||||
|
tip: "A la imatge es pot afegir una marca d'aigua com informació sobre drets."
|
||||||
|
quitWithoutSaveConfirm: "Sortir sense desar?"
|
||||||
|
driveFileTypeWarn: "Aquest arxiu no és compatible"
|
||||||
|
driveFileTypeWarnDescription: "Selecciona un arxiu d'imatge "
|
||||||
|
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 "
|
||||||
|
colorAdjust: "Correcció de color"
|
||||||
|
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"
|
searchScopeAll: "Vše"
|
||||||
searchScopeLocal: "Místní"
|
searchScopeLocal: "Místní"
|
||||||
searchScopeUser: "Upřesnit uživatele"
|
searchScopeUser: "Upřesnit uživatele"
|
||||||
|
_watermarkEditor:
|
||||||
|
opacity: "Průhlednost"
|
||||||
|
scale: "Velikost"
|
||||||
|
text: "Text"
|
||||||
|
position: "Pozice"
|
||||||
|
type: "Typ"
|
||||||
|
image: "Obrázky"
|
||||||
|
advanced: "Pokročilé"
|
||||||
|
|
|
@ -298,6 +298,7 @@ uploadFromUrl: "Von einer URL hochladen"
|
||||||
uploadFromUrlDescription: "URL der hochzuladenden Datei"
|
uploadFromUrlDescription: "URL der hochzuladenden Datei"
|
||||||
uploadFromUrlRequested: "Upload angefordert"
|
uploadFromUrlRequested: "Upload angefordert"
|
||||||
uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis das Hochladen abgeschlossen ist."
|
uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis das Hochladen abgeschlossen ist."
|
||||||
|
uploadNFiles: "Lade {n} Dateien hoch"
|
||||||
explore: "Erkunden"
|
explore: "Erkunden"
|
||||||
messageRead: "Gelesen"
|
messageRead: "Gelesen"
|
||||||
noMoreHistory: "Kein weiterer Verlauf vorhanden"
|
noMoreHistory: "Kein weiterer Verlauf vorhanden"
|
||||||
|
@ -326,6 +327,7 @@ dark: "Dunkel"
|
||||||
lightThemes: "Helle Farbschemata"
|
lightThemes: "Helle Farbschemata"
|
||||||
darkThemes: "Dunkle Farbschemata"
|
darkThemes: "Dunkle Farbschemata"
|
||||||
syncDeviceDarkMode: "Einstellung deines Geräts übernehmen"
|
syncDeviceDarkMode: "Einstellung deines Geräts übernehmen"
|
||||||
|
switchDarkModeManuallyWhenSyncEnabledConfirm: "\"{x}\" ist eingeschaltet. Möchtest du die Synchronisation ausschalten und den Modus manuell wechseln?"
|
||||||
drive: "Drive"
|
drive: "Drive"
|
||||||
fileName: "Dateiname"
|
fileName: "Dateiname"
|
||||||
selectFile: "Datei auswählen"
|
selectFile: "Datei auswählen"
|
||||||
|
@ -575,8 +577,10 @@ showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik an
|
||||||
showFixedPostFormInChannel: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen (Kanäle)"
|
showFixedPostFormInChannel: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen (Kanäle)"
|
||||||
withRepliesByDefaultForNewlyFollowed: "Standardmäßig Antworten von neu gefolgten Benutzern in der Chronik anzeigen"
|
withRepliesByDefaultForNewlyFollowed: "Standardmäßig Antworten von neu gefolgten Benutzern in der Chronik anzeigen"
|
||||||
newNoteRecived: "Es gibt neue Notizen"
|
newNoteRecived: "Es gibt neue Notizen"
|
||||||
|
newNote: "Neue Notiz"
|
||||||
sounds: "Töne"
|
sounds: "Töne"
|
||||||
sound: "Töne"
|
sound: "Töne"
|
||||||
|
notificationSoundSettings: "Benachrichtigungston festlegen"
|
||||||
listen: "Anhören"
|
listen: "Anhören"
|
||||||
none: "Nichts"
|
none: "Nichts"
|
||||||
showInPage: "In einer Seite anzeigen"
|
showInPage: "In einer Seite anzeigen"
|
||||||
|
@ -791,6 +795,7 @@ wide: "Breit"
|
||||||
narrow: "Schmal"
|
narrow: "Schmal"
|
||||||
reloadToApplySetting: "Diese Einstellung tritt nach einer Aktualisierung der Seite in Kraft. Jetzt aktualisieren?"
|
reloadToApplySetting: "Diese Einstellung tritt nach einer Aktualisierung der Seite in Kraft. Jetzt aktualisieren?"
|
||||||
needReloadToApply: "Diese Einstellung tritt nach einer Aktualisierung der Seite in Kraft."
|
needReloadToApply: "Diese Einstellung tritt nach einer Aktualisierung der Seite in Kraft."
|
||||||
|
needToRestartServerToApply: "Diese Einstellung tritt nach einem Neustart des Servers in Kraft."
|
||||||
showTitlebar: "Titelleiste anzeigen"
|
showTitlebar: "Titelleiste anzeigen"
|
||||||
clearCache: "Cache leeren"
|
clearCache: "Cache leeren"
|
||||||
onlineUsersCount: "{n} Benutzer sind online"
|
onlineUsersCount: "{n} Benutzer sind online"
|
||||||
|
@ -997,6 +1002,7 @@ failedToUpload: "Hochladen fehlgeschlagen"
|
||||||
cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile der Datei als möglicherweise unangebracht festgestellt wurden."
|
cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile der Datei als möglicherweise unangebracht festgestellt wurden."
|
||||||
cannotUploadBecauseNoFreeSpace: "Die Datei konnte nicht hochgeladen werden, da dein Drive-Speicherplatz aufgebraucht ist."
|
cannotUploadBecauseNoFreeSpace: "Die Datei konnte nicht hochgeladen werden, da dein Drive-Speicherplatz aufgebraucht ist."
|
||||||
cannotUploadBecauseExceedsFileSizeLimit: "Diese Datei kann wegen Überschreitung der Maximalgröße nicht hochgeladen werden."
|
cannotUploadBecauseExceedsFileSizeLimit: "Diese Datei kann wegen Überschreitung der Maximalgröße nicht hochgeladen werden."
|
||||||
|
cannotUploadBecauseUnallowedFileType: "Hochladen nicht möglich wegen unzulässigem Dateityp."
|
||||||
beta: "Beta"
|
beta: "Beta"
|
||||||
enableAutoSensitive: "Automarkierung sensibler Medien"
|
enableAutoSensitive: "Automarkierung sensibler Medien"
|
||||||
enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch Markierungen für sensible Medien. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert."
|
enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch Markierungen für sensible Medien. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert."
|
||||||
|
@ -1324,6 +1330,7 @@ restore: "Wiederherstellen"
|
||||||
syncBetweenDevices: "Zwischen Geräten synchronisieren"
|
syncBetweenDevices: "Zwischen Geräten synchronisieren"
|
||||||
preferenceSyncConflictTitle: "Der konfigurierte Wert ist auf dem Server bereits vorhanden."
|
preferenceSyncConflictTitle: "Der konfigurierte Wert ist auf dem Server bereits vorhanden."
|
||||||
preferenceSyncConflictText: "Die Einstellungen mit aktivierter Synchronisierung werden ihre Werte auf dem Server speichern. Es gibt jedoch bereits Werte auf dem Server. Welche Einstellungswerte sollen überschrieben werden?"
|
preferenceSyncConflictText: "Die Einstellungen mit aktivierter Synchronisierung werden ihre Werte auf dem Server speichern. Es gibt jedoch bereits Werte auf dem Server. Welche Einstellungswerte sollen überschrieben werden?"
|
||||||
|
preferenceSyncConflictChoiceMerge: "Zusammenführen"
|
||||||
preferenceSyncConflictChoiceServer: "Konfigurierte Werte auf dem Server"
|
preferenceSyncConflictChoiceServer: "Konfigurierte Werte auf dem Server"
|
||||||
preferenceSyncConflictChoiceDevice: "Konfigurierte Werte auf dem Gerät"
|
preferenceSyncConflictChoiceDevice: "Konfigurierte Werte auf dem Gerät"
|
||||||
preferenceSyncConflictChoiceCancel: "Einrichten der Synchronisierung abbrechen"
|
preferenceSyncConflictChoiceCancel: "Einrichten der Synchronisierung abbrechen"
|
||||||
|
@ -1346,6 +1353,20 @@ goToDeck: "Zurück zum Deck"
|
||||||
federationJobs: "Föderation Jobs"
|
federationJobs: "Föderation Jobs"
|
||||||
driveAboutTip: "In Drive sehen Sie eine Liste der Dateien, die Sie in der Vergangenheit hochgeladen haben. <br>\nSie können diese Dateien wiederverwenden um sie zu beispiel an Notizen anzuhängen, oder sie können Dateien vorab hochzuladen, um sie später zu versenden! <br>\n<b>Wenn Sie eine Datei löschen, verschwindet sie auch von allen Stellen, an denen Sie sie verwendet haben (Notizen, Seiten, Avatare, Banner usw.).</b><br>\nSie können auch Ordner erstellen, um sie zu organisieren."
|
driveAboutTip: "In Drive sehen Sie eine Liste der Dateien, die Sie in der Vergangenheit hochgeladen haben. <br>\nSie können diese Dateien wiederverwenden um sie zu beispiel an Notizen anzuhängen, oder sie können Dateien vorab hochzuladen, um sie später zu versenden! <br>\n<b>Wenn Sie eine Datei löschen, verschwindet sie auch von allen Stellen, an denen Sie sie verwendet haben (Notizen, Seiten, Avatare, Banner usw.).</b><br>\nSie können auch Ordner erstellen, um sie zu organisieren."
|
||||||
scrollToClose: "Zum Schließen scrollen"
|
scrollToClose: "Zum Schließen scrollen"
|
||||||
|
advice: "Tipps"
|
||||||
|
realtimeMode: "Echtzeit-Modus"
|
||||||
|
turnItOn: "Einschalten"
|
||||||
|
turnItOff: "Ausschalten"
|
||||||
|
emojiMute: "Emoji stummschalten"
|
||||||
|
emojiUnmute: "Emoji-Stummschaltung aufheben"
|
||||||
|
muteX: "{x} stummschalten"
|
||||||
|
unmuteX: "Stummschaltung von {x} aufheben"
|
||||||
|
abort: "Abbrechen"
|
||||||
|
tip: "Tipps und Tricks"
|
||||||
|
redisplayAllTips: "Alle „Tipps und Tricks“ wieder anzeigen"
|
||||||
|
hideAllTips: "Alle „Tipps und Tricks“ ausblenden"
|
||||||
|
defaultImageCompressionLevel: "Standard-Bildkomprimierungsstufe"
|
||||||
|
defaultImageCompressionLevel_description: "Ein niedrigerer Wert erhält die Bildqualität, erhöht aber die Dateigröße. <br>Höhere Werte reduzieren die Dateigröße, verringern aber die Bildqualität."
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "Noch keine Nachrichten"
|
noMessagesYet: "Noch keine Nachrichten"
|
||||||
newMessage: "Neue Nachricht"
|
newMessage: "Neue Nachricht"
|
||||||
|
@ -1379,6 +1400,8 @@ _chat:
|
||||||
chatNotAvailableInOtherAccount: "Die Chatfunktion wurde vom anderen Benutzer deaktiviert."
|
chatNotAvailableInOtherAccount: "Die Chatfunktion wurde vom anderen Benutzer deaktiviert."
|
||||||
cannotChatWithTheUser: "Starten eines Chats mit diesem Benutzer nicht möglich"
|
cannotChatWithTheUser: "Starten eines Chats mit diesem Benutzer nicht möglich"
|
||||||
cannotChatWithTheUser_description: "Der Chat ist entweder nicht verfügbar oder die andere Seite hat den Chat nicht aktiviert."
|
cannotChatWithTheUser_description: "Der Chat ist entweder nicht verfügbar oder die andere Seite hat den Chat nicht aktiviert."
|
||||||
|
youAreNotAMemberOfThisRoomButInvited: "Du bist kein Teilnehmer in diesem Raum, aber du hast eine Einladung erhalten. Bitte nimm die Einladung an, um beizutreten."
|
||||||
|
doYouAcceptInvitation: "Nimmst du die Einladung an?"
|
||||||
chatWithThisUser: "Mit dem Benutzer chatten"
|
chatWithThisUser: "Mit dem Benutzer chatten"
|
||||||
thisUserAllowsChatOnlyFromFollowers: "Dieser Benutzer nimmt nur Chats von Followern an."
|
thisUserAllowsChatOnlyFromFollowers: "Dieser Benutzer nimmt nur Chats von Followern an."
|
||||||
thisUserAllowsChatOnlyFromFollowing: "Dieser Benutzer nimmt nur Chats von Benutzern an, denen er folgt."
|
thisUserAllowsChatOnlyFromFollowing: "Dieser Benutzer nimmt nur Chats von Benutzern an, denen er folgt."
|
||||||
|
@ -1418,12 +1441,20 @@ _settings:
|
||||||
makeEveryTextElementsSelectable: "Alle Textelemente auswählbar machen"
|
makeEveryTextElementsSelectable: "Alle Textelemente auswählbar machen"
|
||||||
makeEveryTextElementsSelectable_description: "Die Aktivierung kann in manchen Situationen die Benutzerfreundlichkeit beeinträchtigen."
|
makeEveryTextElementsSelectable_description: "Die Aktivierung kann in manchen Situationen die Benutzerfreundlichkeit beeinträchtigen."
|
||||||
useStickyIcons: "Icons beim Scrollen folgen lassen"
|
useStickyIcons: "Icons beim Scrollen folgen lassen"
|
||||||
|
enableHighQualityImagePlaceholders: "Zeige Platzhalter für Bilder in hoher Qualität an"
|
||||||
|
uiAnimations: "Animationen der Benutzeroberfläche"
|
||||||
showNavbarSubButtons: "Unterschaltflächen in der Navigationsleiste anzeigen"
|
showNavbarSubButtons: "Unterschaltflächen in der Navigationsleiste anzeigen"
|
||||||
ifOn: "Wenn eingeschaltet"
|
ifOn: "Wenn eingeschaltet"
|
||||||
ifOff: "Wenn ausgeschaltet"
|
ifOff: "Wenn ausgeschaltet"
|
||||||
enableSyncThemesBetweenDevices: "Synchronisierung von installierten Themen auf verschiedenen Endgeräten"
|
enableSyncThemesBetweenDevices: "Synchronisierung von installierten Themen auf verschiedenen Endgeräten"
|
||||||
enablePullToRefresh: "Ziehen zum Aktualisieren"
|
enablePullToRefresh: "Ziehen zum Aktualisieren"
|
||||||
enablePullToRefresh_description: "Bei Benutzung einer Maus, mit gedrücktem Mausrad ziehen"
|
enablePullToRefresh_description: "Bei Benutzung einer Maus, mit gedrücktem Mausrad ziehen"
|
||||||
|
realtimeMode_description: "Stellt eine Verbindung mit dem Server her und aktualisiert die Inhalte in Echtzeit. Kann zu mehr Datenverkehr einem höheren Akkuverbrauch führen."
|
||||||
|
contentsUpdateFrequency: "Häufigkeit des Abrufs von Inhalten"
|
||||||
|
contentsUpdateFrequency_description: "Je höher der Wert, desto häufiger werden die Inhalte aktualisiert, aber die Leistung sinkt und der Datenverkehr und der Akkuverbrauch steigen."
|
||||||
|
contentsUpdateFrequency_description2: "Wenn der Echtzeitmodus aktiviert ist, werden die Inhalte unabhängig von dieser Einstellung in Echtzeit aktualisiert."
|
||||||
|
showUrlPreview: "URL-Vorschau anzeigen"
|
||||||
|
showAvailableReactionsFirstInNote: "Zeige die verfügbaren Reaktionen im oberen Bereich an."
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "Name des Absenders anzeigen"
|
showSenderName: "Name des Absenders anzeigen"
|
||||||
sendOnEnter: "Eingabetaste sendet Nachricht"
|
sendOnEnter: "Eingabetaste sendet Nachricht"
|
||||||
|
@ -1604,6 +1635,20 @@ _serverSettings:
|
||||||
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern."
|
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern."
|
||||||
deliverSuspendedSoftware: "Software, die nicht mehr beliefert wird"
|
deliverSuspendedSoftware: "Software, die nicht mehr beliefert wird"
|
||||||
deliverSuspendedSoftwareDescription: "Sie können eine Auswahl von Namen und Versionen verschiedener Serversoftware angeben, um die Zustellung zu stoppen, z. B. aufgrund von Sicherheitslücken. Diese Versionsinformationen werden vom Server bereitgestellt und ihre Zuverlässigkeit ist nicht garantiert. Es wird jedoch empfohlen, eine Vorabversion anzugeben, wie z. B. >= 2024.3.1-0, da die Angabe >= 2024.3.1 keine benutzerdefinierten Versionen wie 2024.3.1-custom.0 einschließt."
|
deliverSuspendedSoftwareDescription: "Sie können eine Auswahl von Namen und Versionen verschiedener Serversoftware angeben, um die Zustellung zu stoppen, z. B. aufgrund von Sicherheitslücken. Diese Versionsinformationen werden vom Server bereitgestellt und ihre Zuverlässigkeit ist nicht garantiert. Es wird jedoch empfohlen, eine Vorabversion anzugeben, wie z. B. >= 2024.3.1-0, da die Angabe >= 2024.3.1 keine benutzerdefinierten Versionen wie 2024.3.1-custom.0 einschließt."
|
||||||
|
singleUserMode: "Einzelbenutzermodus"
|
||||||
|
singleUserMode_description: "Wenn du der einzige Benutzer dieses Servers bist, optimiert die Aktivierung dieses Modus die Leistung des Servers."
|
||||||
|
signToActivityPubGet: "ActivityPub-GET-Anfragen signieren"
|
||||||
|
signToActivityPubGet_description: "Normalerweise sollte diese Option aktiviert sein. Die Deaktivierung kann Probleme im Zusammenhang mit der Föderation beheben, aber andererseits könnte sie die Föderation mit einigen anderen Servern deaktivieren."
|
||||||
|
proxyRemoteFiles: "Proxy für Dateien fremder Instanzen"
|
||||||
|
proxyRemoteFiles_description: "Wenn diese Einstellung aktiviert ist, werden fremde Dateien über einen Proxyserver übertragen und bereitgestellt. Dies hilft bei der Erstellung von Vorschaubildern und schützt die Privatsphäre der Benutzer."
|
||||||
|
allowExternalApRedirect: "Weiterleitungen für Anfragen über ActivityPub zulassen"
|
||||||
|
allowExternalApRedirect_description: "Wenn diese Option aktiviert ist, können andere Server Inhalte von Drittanbietern über diesen Server abfragen, was jedoch zu Content-Spoofing führen kann."
|
||||||
|
userGeneratedContentsVisibilityForVisitor: "Sichtbarkeit von nutzergenerierten Inhalten für Gäste"
|
||||||
|
userGeneratedContentsVisibilityForVisitor_description: "Dies ist nützlich, um zu verhindern, dass unangemessene Inhalte, die nicht gut moderiert sind, ungewollt über deinen eigenen Server im Internet veröffentlicht werden."
|
||||||
|
_userGeneratedContentsVisibilityForVisitor:
|
||||||
|
all: "Alles ist öffentlich"
|
||||||
|
localOnly: "Nur lokale Inhalte werden veröffentlicht, fremde Inhalte bleiben privat"
|
||||||
|
none: "Alles ist privat"
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "Von einem anderen Konto zu diesem migrieren"
|
moveFrom: "Von einem anderen Konto zu diesem migrieren"
|
||||||
moveFromSub: "Alias für ein anderes Konto erstellen"
|
moveFromSub: "Alias für ein anderes Konto erstellen"
|
||||||
|
@ -1944,6 +1989,9 @@ _role:
|
||||||
canImportMuting: "Importieren von Stummgeschalteten zulassen"
|
canImportMuting: "Importieren von Stummgeschalteten zulassen"
|
||||||
canImportUserLists: "Importieren von Listen erlauben"
|
canImportUserLists: "Importieren von Listen erlauben"
|
||||||
chatAvailability: "Chatten erlauben"
|
chatAvailability: "Chatten erlauben"
|
||||||
|
uploadableFileTypes: "Hochladbare Dateitypen"
|
||||||
|
uploadableFileTypes_caption: "Gibt die zulässigen MIME-/Dateitypen an. Mehrere MIME-Typen können durch einen Zeilenumbruch getrennt angegeben werden, und Platzhalter können mit einem Sternchen (*) angegeben werden. (z. B. image/*)"
|
||||||
|
uploadableFileTypes_caption2: "Bei manchen Dateien ist es nicht möglich, den Typ zu bestimmen. Um solche Dateien zuzulassen, füge {x} der Spezifikation hinzu."
|
||||||
_condition:
|
_condition:
|
||||||
roleAssignedTo: "Manuellen Rollen zugewiesen"
|
roleAssignedTo: "Manuellen Rollen zugewiesen"
|
||||||
isLocal: "Lokaler Benutzer"
|
isLocal: "Lokaler Benutzer"
|
||||||
|
@ -2796,6 +2844,8 @@ _dataSaver:
|
||||||
_avatar:
|
_avatar:
|
||||||
title: "Animierte Profilbilder deaktivieren"
|
title: "Animierte Profilbilder deaktivieren"
|
||||||
description: "Die Animation von Profilbildern wird angehalten. Da animierte Bilder eine größere Dateigröße haben können als normale Bilder, kann dies den Datenverkehr weiter reduzieren."
|
description: "Die Animation von Profilbildern wird angehalten. Da animierte Bilder eine größere Dateigröße haben können als normale Bilder, kann dies den Datenverkehr weiter reduzieren."
|
||||||
|
_disableUrlPreview:
|
||||||
|
title: "URL-Vorschau deaktivieren"
|
||||||
_code:
|
_code:
|
||||||
title: "Code-Hervorhebungen ausblenden"
|
title: "Code-Hervorhebungen ausblenden"
|
||||||
description: "Wenn Code-Hervorhebungen in MFM usw. verwendet werden, werden sie erst geladen, wenn sie angetippt werden. Die Syntaxhervorhebung erfordert das Herunterladen der Definitionsdateien für jede Programmiersprache. Es ist daher zu erwarten, dass die Deaktivierung des automatischen Ladens dieser Dateien die Menge des Datenverkehrs reduziert."
|
description: "Wenn Code-Hervorhebungen in MFM usw. verwendet werden, werden sie erst geladen, wenn sie angetippt werden. Die Syntaxhervorhebung erfordert das Herunterladen der Definitionsdateien für jede Programmiersprache. Es ist daher zu erwarten, dass die Deaktivierung des automatischen Ladens dieser Dateien die Menge des Datenverkehrs reduziert."
|
||||||
|
@ -3001,3 +3051,85 @@ _search:
|
||||||
pleaseEnterServerHost: "Gib den Server-Host ein"
|
pleaseEnterServerHost: "Gib den Server-Host ein"
|
||||||
pleaseSelectUser: "Benutzer auswählen"
|
pleaseSelectUser: "Benutzer auswählen"
|
||||||
serverHostPlaceholder: "Beispiel: misskey.example.com"
|
serverHostPlaceholder: "Beispiel: misskey.example.com"
|
||||||
|
_serverSetupWizard:
|
||||||
|
installCompleted: "Die Installation von Misskey ist abgeschlossen!"
|
||||||
|
firstCreateAccount: "Erstelle zunächst ein Administratorkonto."
|
||||||
|
accountCreated: "Ein Administratorkonto wurde angelegt!"
|
||||||
|
serverSetting: "Servereinstellungen"
|
||||||
|
youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Mit diesem Assistenten lässt sich die optimale Serverkonfiguration leicht einrichten."
|
||||||
|
settingsYouMakeHereCanBeChangedLater: "Die Einstellungen hier können später geändert werden."
|
||||||
|
howWillYouUseMisskey: "Wie wirst du Misskey verwenden?"
|
||||||
|
_use:
|
||||||
|
single: "Ein-Personen-Server"
|
||||||
|
single_description: "Verwende den Server alleine als deinen eigenen."
|
||||||
|
single_youCanCreateMultipleAccounts: "Bei Bedarf können mehrere Konten eingerichtet werden, auch wenn es sich um einen Ein-Personen-Server handelt."
|
||||||
|
group: "Gruppenserver"
|
||||||
|
open: "Offener Server"
|
||||||
|
open_description: "Registrierung für alle öffnen."
|
||||||
|
howManyUsersDoYouExpect: "Mit wie vielen Benutzern rechnest du?"
|
||||||
|
_scale:
|
||||||
|
small: "Weniger als 100 (kleiner Maßstab)"
|
||||||
|
medium: "Mehr als 100 und weniger als 1000 Benutzer (mittelgroß)"
|
||||||
|
large: "Mehr als 1000 (großer Maßstab)"
|
||||||
|
largeScaleServerAdvice: "Für große Server sind unter Umständen fortgeschrittene Kenntnisse erforderlich, z. B. Lastverteilung und Datenbankreplikation."
|
||||||
|
doYouConnectToFediverse: "Mit dem Fediverse verbinden?"
|
||||||
|
doYouConnectToFediverse_description1: "Bei Anschluss an ein Netz von verteilten Servern (Fediverse) können Inhalte mit anderen Servern ausgetauscht werden."
|
||||||
|
doYouConnectToFediverse_description2: "Die Verbindung mit dem Fediverse wird auch als „Föderation“ bezeichnet."
|
||||||
|
youCanConfigureMoreFederationSettingsLater: "Erweiterte Einstellungen, wie z. B. die Angabe von föderierbaren Servern, können später vorgenommen werden."
|
||||||
|
adminInfo_mustBeFilled: "Dies ist auf einem offenen Server oder bei aktivierter Föderation erforderlich."
|
||||||
|
followingSettingsAreRecommended: "Die folgenden Einstellungen werden empfohlen"
|
||||||
|
applyTheseSettings: "Diese Einstellungen anwenden"
|
||||||
|
skipSettings: "Konfiguration überspringen"
|
||||||
|
settingsCompleted: "Einrichtung abgeschlossen!"
|
||||||
|
settingsCompleted_description: "Vielen Dank für deine Zeit. Jetzt, wo alles fertig ist, kannst du den Server sofort benutzen."
|
||||||
|
settingsCompleted_description2: "Detaillierte Servereinstellungen können über die „Systemsteuerung“ vorgenommen werden."
|
||||||
|
donationRequest: "Spendenaufruf"
|
||||||
|
_donationRequest:
|
||||||
|
text1: "Misskey ist eine freie Software, die von Freiwilligen entwickelt wird."
|
||||||
|
text2: "Wir würden uns über deine Unterstützung freuen, damit wir dieses Projekt auch in Zukunft weiterentwickeln können."
|
||||||
|
text3: "Für Unterstützer gibt es auch besondere Vorteile!"
|
||||||
|
_uploader:
|
||||||
|
compressedToX: "Komprimiert zu {x}"
|
||||||
|
savedXPercent: "{x}% gespart"
|
||||||
|
abortConfirm: "Einige Dateien wurden nicht hochgeladen. Möchtest du den Vorgang abbrechen?"
|
||||||
|
doneConfirm: "Einige Dateien wurden nicht hochgeladen. Möchtest du den Vorgang fortsetzen?"
|
||||||
|
maxFileSizeIsX: "Die maximale Dateigröße, die hochgeladen werden kann, beträgt {x}."
|
||||||
|
allowedTypes: "Hochladbare Dateitypen"
|
||||||
|
tip: "Die Datei ist noch nicht hochgeladen worden. In diesem Dialog kannst du die Datei vor dem Hochladen anzeigen, umbenennen, komprimieren und zuschneiden. Wenn du fertig bist, klicke auf „Hochladen“, um den Upload zu starten."
|
||||||
|
_clientPerformanceIssueTip:
|
||||||
|
makeSureDisabledAdBlocker: "Deaktiviere deinen Adblocker"
|
||||||
|
makeSureDisabledAdBlocker_description: "Adblocker können die Leistung beeinträchtigen; vergewissere dich, ob in deinem Betriebssystem, Browser oder deinen Add-ons Adblocker aktiviert sind."
|
||||||
|
makeSureDisabledCustomCss: "Benutzerdefiniertes CSS deaktivieren"
|
||||||
|
makeSureDisabledCustomCss_description: "Das Überschreiben von Stilen kann die Leistung beeinträchtigen. Stelle daher sicher, dass du kein benutzerdefiniertes CSS oder Erweiterungen aktiviert hast, die Stile überschreiben."
|
||||||
|
makeSureDisabledAddons: "Erweiterungen deaktivieren"
|
||||||
|
makeSureDisabledAddons_description: "Einige Erweiterungen können das Verhalten des Clients stören und die Leistung beeinträchtigen. Deaktiviere die Browser-Erweiterungen und prüfe, ob sich die Situation dadurch verbessert."
|
||||||
|
_clip:
|
||||||
|
tip: "Clips sind eine Funktion, mit der du Notizen gruppieren kannst."
|
||||||
|
_userLists:
|
||||||
|
tip: "Es können Listen mit beliebigen Benutzern erstellt werden. Die erstellte Liste kann als eigene Chronik angezeigt werden."
|
||||||
|
watermark: "Wasserzeichen"
|
||||||
|
defaultPreset: "Standard-Voreinstellungen"
|
||||||
|
_watermarkEditor:
|
||||||
|
quitWithoutSaveConfirm: "Nicht gespeicherte Änderungen verwerfen?"
|
||||||
|
driveFileTypeWarn: "Diese Datei wird nicht unterstützt"
|
||||||
|
driveFileTypeWarnDescription: "Bilddatei auswählen"
|
||||||
|
title: "Wasserzeichen bearbeiten"
|
||||||
|
opacity: "Transparenz"
|
||||||
|
scale: "Größe"
|
||||||
|
text: "Text"
|
||||||
|
position: "Position"
|
||||||
|
type: "Art"
|
||||||
|
image: "Bilder"
|
||||||
|
advanced: "Fortgeschritten"
|
||||||
|
angle: "Winkel"
|
||||||
|
_imageEffector:
|
||||||
|
title: "Effekte"
|
||||||
|
addEffect: "Effekte hinzufügen"
|
||||||
|
discardChangesConfirm: "Änderungen verwerfen und beenden?"
|
||||||
|
_fxs:
|
||||||
|
chromaticAberration: "Chromatische Abweichung"
|
||||||
|
glitch: "Glitch"
|
||||||
|
mirror: "Spiegeln"
|
||||||
|
invert: "Farben umkehren"
|
||||||
|
grayscale: "Schwarzweiß"
|
||||||
|
colorAdjust: "Farbkorrektur"
|
||||||
|
|
|
@ -403,3 +403,5 @@ _reversi:
|
||||||
total: "Σύνολο"
|
total: "Σύνολο"
|
||||||
_search:
|
_search:
|
||||||
searchScopeLocal: "Τοπικό"
|
searchScopeLocal: "Τοπικό"
|
||||||
|
_watermarkEditor:
|
||||||
|
image: "Εικόνες"
|
||||||
|
|
|
@ -1365,6 +1365,8 @@ abort: "Abort"
|
||||||
tip: "Tips & Tricks"
|
tip: "Tips & Tricks"
|
||||||
redisplayAllTips: "Show all “Tips & Tricks” again"
|
redisplayAllTips: "Show all “Tips & Tricks” again"
|
||||||
hideAllTips: "Hide all \"Tips & Tricks\""
|
hideAllTips: "Hide all \"Tips & Tricks\""
|
||||||
|
defaultImageCompressionLevel: "Default image compression level"
|
||||||
|
defaultImageCompressionLevel_description: "High, reduces the file size but also the image quality. <br>High, reduces the file size but also the image quality."
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "No messages yet"
|
noMessagesYet: "No messages yet"
|
||||||
newMessage: "New message"
|
newMessage: "New message"
|
||||||
|
@ -1452,6 +1454,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description: "The higher the value the more the content updates but it lowers the performance and increases the traffic and memory consumption."
|
contentsUpdateFrequency_description: "The higher the value the more the content updates but it lowers the performance and increases the traffic and memory consumption."
|
||||||
contentsUpdateFrequency_description2: "When real-time mode is on, content is updated in real time regardless of this setting."
|
contentsUpdateFrequency_description2: "When real-time mode is on, content is updated in real time regardless of this setting."
|
||||||
showUrlPreview: "Show URL preview"
|
showUrlPreview: "Show URL preview"
|
||||||
|
showAvailableReactionsFirstInNote: "Show available reactions at the top."
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "Show sender's name"
|
showSenderName: "Show sender's name"
|
||||||
sendOnEnter: "Press Enter to send"
|
sendOnEnter: "Press Enter to send"
|
||||||
|
@ -2968,7 +2971,7 @@ _customEmojisManager:
|
||||||
markAsDeleteTargetRanges: "Mark rows in the selection as a target to delete"
|
markAsDeleteTargetRanges: "Mark rows in the selection as a target to delete"
|
||||||
alertUpdateEmojisNothingDescription: "There are no updated Emojis."
|
alertUpdateEmojisNothingDescription: "There are no updated Emojis."
|
||||||
alertDeleteEmojisNothingDescription: "There are no Emojis to be deleted."
|
alertDeleteEmojisNothingDescription: "There are no Emojis to be deleted."
|
||||||
confirmMovePage: ""
|
confirmMovePage: "Would you like to move pages?"
|
||||||
confirmChangeView: ""
|
confirmChangeView: ""
|
||||||
confirmUpdateEmojisDescription: "Update {count} Emoji(s). Are you sure to continue?"
|
confirmUpdateEmojisDescription: "Update {count} Emoji(s). Are you sure to continue?"
|
||||||
confirmDeleteEmojisDescription: "Delete checked {count} Emoji(s). Are you sure to continue?"
|
confirmDeleteEmojisDescription: "Delete checked {count} Emoji(s). Are you sure to continue?"
|
||||||
|
@ -3117,3 +3120,50 @@ _clip:
|
||||||
tip: "Clip is a feature that allows you to organize your notes."
|
tip: "Clip is a feature that allows you to organize your notes."
|
||||||
_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."
|
||||||
|
watermark: "Watermark"
|
||||||
|
defaultPreset: "Default Preset"
|
||||||
|
_watermarkEditor:
|
||||||
|
tip: "A watermark, such as credit information, can be added to the image."
|
||||||
|
quitWithoutSaveConfirm: "Discard unsaved changes?"
|
||||||
|
driveFileTypeWarn: "This file is not supported"
|
||||||
|
driveFileTypeWarnDescription: "Choose an image file"
|
||||||
|
title: "Edit Watermark"
|
||||||
|
cover: "Cover everything"
|
||||||
|
repeat: "spread all over"
|
||||||
|
opacity: "Opacity"
|
||||||
|
scale: "Size"
|
||||||
|
text: "Text"
|
||||||
|
position: "Position"
|
||||||
|
type: "Type"
|
||||||
|
image: "Images"
|
||||||
|
advanced: "Advanced"
|
||||||
|
stripe: "Stripes"
|
||||||
|
stripeWidth: "Line width"
|
||||||
|
stripeFrequency: "Lines count"
|
||||||
|
angle: "Angle"
|
||||||
|
polkadot: "Polkadot"
|
||||||
|
checker: "Checker"
|
||||||
|
polkadotMainDotOpacity: "Opacity of the main dot"
|
||||||
|
polkadotMainDotRadius: "Size of the main dot"
|
||||||
|
polkadotSubDotOpacity: "Opacity of the secondary dot"
|
||||||
|
polkadotSubDotRadius: "Size of the secondary dot"
|
||||||
|
polkadotSubDotDivisions: "Number of sub-dots."
|
||||||
|
_imageEffector:
|
||||||
|
title: "Effects"
|
||||||
|
addEffect: "Add Effects"
|
||||||
|
discardChangesConfirm: "Are you sure you want to leave? You have unsaved changes."
|
||||||
|
_fxs:
|
||||||
|
chromaticAberration: "Chromatic Aberration"
|
||||||
|
glitch: "Glitch"
|
||||||
|
mirror: "Mirror"
|
||||||
|
invert: "Invert Colors"
|
||||||
|
grayscale: "white-black"
|
||||||
|
colorAdjust: "Colour Correction"
|
||||||
|
colorClamp: "Color Compression"
|
||||||
|
colorClampAdvanced: "Color Compression (Advanced)"
|
||||||
|
distort: "Distortion"
|
||||||
|
threshold: "Binarize"
|
||||||
|
zoomLines: "Saturated lines"
|
||||||
|
stripe: "Stripes"
|
||||||
|
polkadot: "Polkadot"
|
||||||
|
checker: "Checker"
|
||||||
|
|
|
@ -1365,6 +1365,8 @@ abort: "Abortar"
|
||||||
tip: "Consejos y trucos"
|
tip: "Consejos y trucos"
|
||||||
redisplayAllTips: "Volver a mostrar todos \"Trucos y consejos\""
|
redisplayAllTips: "Volver a mostrar todos \"Trucos y consejos\""
|
||||||
hideAllTips: "Ocultar todos los \"Trucos y consejos\""
|
hideAllTips: "Ocultar todos los \"Trucos y consejos\""
|
||||||
|
defaultImageCompressionLevel: "Nivel de compresión de la imagen por defecto"
|
||||||
|
defaultImageCompressionLevel_description: "Baja, conserva la calidad de la imagen pero la medida del archivo es más grande. <br>Alta, reduce la medida del archivo pero también la calidad de la imagen."
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "Aún no hay mensajes"
|
noMessagesYet: "Aún no hay mensajes"
|
||||||
newMessage: "Mensajes nuevos"
|
newMessage: "Mensajes nuevos"
|
||||||
|
@ -1452,6 +1454,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description: "Cuanto mayor sea el valor, más se actualiza el contenido, pero disminuye el rendimiento y aumenta el tráfico y el consumo de memoria."
|
contentsUpdateFrequency_description: "Cuanto mayor sea el valor, más se actualiza el contenido, pero disminuye el rendimiento y aumenta el tráfico y el consumo de memoria."
|
||||||
contentsUpdateFrequency_description2: "Cuando el modo en tiempo real está activado, el contenido se actualiza en tiempo real independientemente de esta configuración."
|
contentsUpdateFrequency_description2: "Cuando el modo en tiempo real está activado, el contenido se actualiza en tiempo real independientemente de esta configuración."
|
||||||
showUrlPreview: "Mostrar la vista previa de la URL"
|
showUrlPreview: "Mostrar la vista previa de la URL"
|
||||||
|
showAvailableReactionsFirstInNote: "Mostrar las reacciones disponibles en la parte superior."
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "Mostrar el nombre del remitente"
|
showSenderName: "Mostrar el nombre del remitente"
|
||||||
sendOnEnter: "Intro para enviar"
|
sendOnEnter: "Intro para enviar"
|
||||||
|
@ -2462,6 +2465,8 @@ _visibility:
|
||||||
disableFederation: "No federado"
|
disableFederation: "No federado"
|
||||||
disableFederationDescription: "No enviar a otras instancias"
|
disableFederationDescription: "No enviar a otras instancias"
|
||||||
_postForm:
|
_postForm:
|
||||||
|
quitInspiteOfThereAreUnuploadedFilesConfirm: "Hay archivos que no se han cargado, ¿deseas descartarlos y cerrar el formulario?"
|
||||||
|
uploaderTip: "El archivo aún no se ha cargado. Desde el menú Archivo, puedes cambiar el nombre, recortar imágenes, poner marcas de agua y comprimir o no el archivo. Los archivos se cargan automáticamente al publicar una nota."
|
||||||
replyPlaceholder: "Responder a esta nota"
|
replyPlaceholder: "Responder a esta nota"
|
||||||
quotePlaceholder: "Citar esta nota"
|
quotePlaceholder: "Citar esta nota"
|
||||||
channelPlaceholder: "Publicar en el canal"
|
channelPlaceholder: "Publicar en el canal"
|
||||||
|
@ -2627,6 +2632,7 @@ _notification:
|
||||||
flushNotification: "Limpiar notificaciones"
|
flushNotification: "Limpiar notificaciones"
|
||||||
exportOfXCompleted: "La exportación de {x} ha sido completada."
|
exportOfXCompleted: "La exportación de {x} ha sido completada."
|
||||||
login: "Alguien ha iniciado sesión"
|
login: "Alguien ha iniciado sesión"
|
||||||
|
createToken: "Token de acceso creado"
|
||||||
createTokenDescription: "Si no tienes ni idea, elimina el token de acceso a través de \"{text}\"."
|
createTokenDescription: "Si no tienes ni idea, elimina el token de acceso a través de \"{text}\"."
|
||||||
_types:
|
_types:
|
||||||
all: "Todo"
|
all: "Todo"
|
||||||
|
@ -2724,10 +2730,18 @@ _webhookSettings:
|
||||||
_abuseReport:
|
_abuseReport:
|
||||||
_notificationRecipient:
|
_notificationRecipient:
|
||||||
createRecipient: "Añadir destinatario a los informes"
|
createRecipient: "Añadir destinatario a los informes"
|
||||||
|
modifyRecipient: "Editar un destinatario en el informe de moderación\n"
|
||||||
|
recipientType: "Tipo de notificación"
|
||||||
_recipientType:
|
_recipientType:
|
||||||
mail: "Correo"
|
mail: "Correo"
|
||||||
webhook: "Webhook"
|
webhook: "Webhook"
|
||||||
|
_captions:
|
||||||
|
mail: "Enviar un correo electrónico a todos los moderadores cuando reciban un informe de moderación"
|
||||||
|
webhook: "Enviar una notificación al SystemWebhook cuando se reciba o se resuelva un informe de moderación"
|
||||||
keywords: "Palabras Clave"
|
keywords: "Palabras Clave"
|
||||||
|
notifiedUser: "Usuarios a notificar"
|
||||||
|
notifiedWebhook: "Webhook a utilizar"
|
||||||
|
deleteConfirm: "¿Estás seguro de que deseas borrar el destinatario del informe de moderación?"
|
||||||
_moderationLogTypes:
|
_moderationLogTypes:
|
||||||
createRole: "Rol creado"
|
createRole: "Rol creado"
|
||||||
deleteRole: "Rol eliminado"
|
deleteRole: "Rol eliminado"
|
||||||
|
@ -2752,9 +2766,12 @@ _moderationLogTypes:
|
||||||
resetPassword: "Resetear contraseña"
|
resetPassword: "Resetear contraseña"
|
||||||
suspendRemoteInstance: "Instancia remota suspendida"
|
suspendRemoteInstance: "Instancia remota suspendida"
|
||||||
unsuspendRemoteInstance: "Suspensión de instancia remota retirada"
|
unsuspendRemoteInstance: "Suspensión de instancia remota retirada"
|
||||||
|
updateRemoteInstanceNote: "Nota de moderación de una instancia remota actualizada"
|
||||||
markSensitiveDriveFile: "Archivo marcado como sensible"
|
markSensitiveDriveFile: "Archivo marcado como sensible"
|
||||||
unmarkSensitiveDriveFile: "Archivo marcado como no sensible"
|
unmarkSensitiveDriveFile: "Archivo marcado como no sensible"
|
||||||
resolveAbuseReport: "Reporte resuelto"
|
resolveAbuseReport: "Reporte resuelto"
|
||||||
|
forwardAbuseReport: "Informe reenviado"
|
||||||
|
updateAbuseReportNote: "Nota de moderación de un informe actualizada"
|
||||||
createInvitation: "Generar invitación"
|
createInvitation: "Generar invitación"
|
||||||
createAd: "Anuncio creado"
|
createAd: "Anuncio creado"
|
||||||
deleteAd: "Anuncio eliminado"
|
deleteAd: "Anuncio eliminado"
|
||||||
|
@ -2764,6 +2781,18 @@ _moderationLogTypes:
|
||||||
deleteAvatarDecoration: "Decoración de avatar eliminada"
|
deleteAvatarDecoration: "Decoración de avatar eliminada"
|
||||||
unsetUserAvatar: "Quitar decoración de avatar de este usuario"
|
unsetUserAvatar: "Quitar decoración de avatar de este usuario"
|
||||||
unsetUserBanner: "Quitar banner de este usuario"
|
unsetUserBanner: "Quitar banner de este usuario"
|
||||||
|
createSystemWebhook: "Crear un SystemWebhook"
|
||||||
|
updateSystemWebhook: "Actualizar SystemWebhook "
|
||||||
|
deleteSystemWebhook: "Borrar SystemWebHook"
|
||||||
|
createAbuseReportNotificationRecipient: "Crear un destinatario para el informe de moderación"
|
||||||
|
updateAbuseReportNotificationRecipient: "Destinatario de los informes actualizados"
|
||||||
|
deleteAbuseReportNotificationRecipient: "Destinatario de los informes borrado"
|
||||||
|
deleteAccount: "Cuenta Borrada"
|
||||||
|
deletePage: "Página borrada"
|
||||||
|
deleteFlash: "Juego borrado"
|
||||||
|
deleteGalleryPost: "Publicación de la galería, eliminada"
|
||||||
|
deleteChatRoom: "Borrar sala del chat"
|
||||||
|
updateProxyAccountDescription: "Actualizar la descripción de la cuenta proxy"
|
||||||
_fileViewer:
|
_fileViewer:
|
||||||
title: "Detalles del archivo"
|
title: "Detalles del archivo"
|
||||||
type: "Tipo de archivo"
|
type: "Tipo de archivo"
|
||||||
|
@ -2818,17 +2847,46 @@ _dataSaver:
|
||||||
_avatar:
|
_avatar:
|
||||||
title: "Avatares animados"
|
title: "Avatares animados"
|
||||||
description: "Desactiva la animación de los avatares. Las imágenes animadas pueden llegar a ser de mayor tamaño que las normales, por lo que al desactivarlas puedes reducir el consumo de datos."
|
description: "Desactiva la animación de los avatares. Las imágenes animadas pueden llegar a ser de mayor tamaño que las normales, por lo que al desactivarlas puedes reducir el consumo de datos."
|
||||||
|
_urlPreviewThumbnail:
|
||||||
|
title: "Ocultar las miniaturas de las vistas previas de URL"
|
||||||
|
description: "Las imágenes en miniatura de la vista previa de URL no se pueden cargar "
|
||||||
|
_disableUrlPreview:
|
||||||
|
title: "Desactivar la vista previa de las URL"
|
||||||
|
description: "Desactiva la función de previsualización de la URL. A diferencia de solo las imágenes en miniatura, esta función reduce la carga de la propia información vinculada."
|
||||||
_code:
|
_code:
|
||||||
title: "Resaltar código"
|
title: "Resaltar código"
|
||||||
description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos."
|
description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos."
|
||||||
_hemisphere:
|
_hemisphere:
|
||||||
N: "Hemisferio norte"
|
N: "Hemisferio norte"
|
||||||
S: "Hemisferio sur"
|
S: "Hemisferio sur"
|
||||||
|
caption: "Usado en algunos clientes para determinar la estación del año"
|
||||||
_reversi:
|
_reversi:
|
||||||
reversi: "Reversi"
|
reversi: "Reversi"
|
||||||
|
gameSettings: "Configuración del juego"
|
||||||
|
chooseBoard: "Elegir tablero"
|
||||||
|
blackOrWhite: "Negras/Blancas"
|
||||||
|
blackIs: "{name} juega con negras"
|
||||||
rules: "Reglas"
|
rules: "Reglas"
|
||||||
|
thisGameIsStartedSoon: "El juego comenzará en breve"
|
||||||
|
waitingForOther: "Esperando el turno del adversario"
|
||||||
|
waitingForMe: "Esperando tu turno"
|
||||||
|
waitingBoth: "Prepárate"
|
||||||
|
ready: "Listo"
|
||||||
|
cancelReady: "No estoy listo"
|
||||||
|
opponentTurn: "Turno del oponente"
|
||||||
|
myTurn: "¡Tu turno!"
|
||||||
|
turnOf: "Le toca a {name}"
|
||||||
|
pastTurnOf: "Turno de {name}"
|
||||||
|
surrender: "Rendirse"
|
||||||
|
surrendered: "Te has rendido"
|
||||||
|
timeout: "Se acabó el tiempo"
|
||||||
|
drawn: "Empate"
|
||||||
won: "{name} ha ganado"
|
won: "{name} ha ganado"
|
||||||
|
black: "Negras"
|
||||||
|
white: "Blancas"
|
||||||
total: "Total"
|
total: "Total"
|
||||||
|
turnCount: "Turno {count}"
|
||||||
|
myGames: "Mis rondas"
|
||||||
allGames: "Todos los juegos"
|
allGames: "Todos los juegos"
|
||||||
ended: "Finalizado"
|
ended: "Finalizado"
|
||||||
playing: "Jugando actualmente"
|
playing: "Jugando actualmente"
|
||||||
|
@ -2894,11 +2952,68 @@ _customEmojisManager:
|
||||||
sortOrder: "Ordenar"
|
sortOrder: "Ordenar"
|
||||||
registrationLogs: "Log de registros "
|
registrationLogs: "Log de registros "
|
||||||
registrationLogsCaption: "Los registros se mostrarán al actualizar o borrar Emojis. Desaparecerán después de actualizarlos o eliminarlos, pasar a una nueva página o recargar."
|
registrationLogsCaption: "Los registros se mostrarán al actualizar o borrar Emojis. Desaparecerán después de actualizarlos o eliminarlos, pasar a una nueva página o recargar."
|
||||||
|
alertEmojisRegisterFailedDescription: "No se ha podido actualizar o borrar el emoji. Por favor comprueba el log del registro para más detalles."
|
||||||
|
_logs:
|
||||||
|
showSuccessLogSwitch: "Mostrar registro de éxito"
|
||||||
|
failureLogNothing: "No hay log de fallos"
|
||||||
|
logNothing: "No hay logs"
|
||||||
|
_remote:
|
||||||
|
selectionRowDetail: "Detalle de la línea seleccionada"
|
||||||
|
importSelectionRows: "Importar las líneas seleccionadas"
|
||||||
|
importSelectionRangesRows: "Importar las filas seleccionadas"
|
||||||
|
importEmojisButton: "Importar los Emojis marcados"
|
||||||
|
confirmImportEmojisTitle: "Importar Emojis"
|
||||||
|
confirmImportEmojisDescription: "Importar {count} Emoji(s) recibidos del servidor remoto. Por favor, presta mucha atención a la licencia del Emoji. ¿Estás seguro de continuar?"
|
||||||
|
_local:
|
||||||
|
tabTitleList: "Lista de emojis registrados"
|
||||||
|
tabTitleRegister: "Registro de Emojis"
|
||||||
|
_list:
|
||||||
|
emojisNothing: "No hay Emojis registrados"
|
||||||
|
markAsDeleteTargetRows: "Marcar las filas seleccionadas como objetivo a eliminar"
|
||||||
|
markAsDeleteTargetRanges: "Selección de filas para su eliminación"
|
||||||
|
alertUpdateEmojisNothingDescription: "No hay Emojis actualizados"
|
||||||
|
alertDeleteEmojisNothingDescription: "No hay Emojis para borrar"
|
||||||
|
confirmMovePage: "¿Quieres cambiar de página?"
|
||||||
|
confirmChangeView: "¿De verdad quieres cambiar la vista?"
|
||||||
|
confirmUpdateEmojisDescription: "Actualizar {count} Emoji(s). ¿Deseas continuar?"
|
||||||
|
confirmDeleteEmojisDescription: "Borrar {count} Emoji(s) seleccionados. ¿Deseas continuar?"
|
||||||
|
confirmResetDescription: "Se restablecerán todos los cambios hechos hasta ahora."
|
||||||
|
confirmMovePageDesciption: "Se han realizado cambios en los Emojis de esta página.\nSi abandonas la página sin guardar, se descartarán todos los cambios realizados en esta página."
|
||||||
|
dialogSelectRoleTitle: "Buscar Emojis por rol"
|
||||||
|
_register:
|
||||||
|
uploadSettingTitle: "Ajustes de carga"
|
||||||
|
uploadSettingDescription: "En esta pantalla, puedes configurar el comportamiento al cargar Emojis."
|
||||||
|
directoryToCategoryLabel: "Introduce el nombre del directorio en el campo \"categoría\""
|
||||||
|
directoryToCategoryCaption: "Cuando arrastres y sueltes un directorio, introduce el nombre del directorio en el campo \"categoría\"."
|
||||||
|
confirmRegisterEmojisDescription: "Registra los Emojis de la lista como nuevos Emojis personalizados. ¿Estás seguro de continuar? (Para evitar sobrecargas, sólo {count} Emoji(s) en una sola operación)"
|
||||||
|
confirmClearEmojisDescription: "Descartar las ediciones y borrar los Emojis de la lista. ¿Estás seguro de continuar?"
|
||||||
|
confirmUploadEmojisDescription: "Cargar los {count} archivo(s) arrastrado(s) y soltado(s) en la unidad. ¿Estás seguro de continuar?"
|
||||||
|
_embedCodeGen:
|
||||||
|
title: "Personalizar el código de incrustación"
|
||||||
|
header: "Mostrar encabezados"
|
||||||
|
autoload: "Cargar más automáticamente (no recomendado)"
|
||||||
|
maxHeight: "Altura máxima"
|
||||||
|
maxHeightDescription: "0 desactiva el ajuste del valor máximo. Para evitar que el widget siga creciendo verticalmente, especifica algún valor."
|
||||||
|
maxHeightWarn: "El límite de altura máxima está desactivado (0). Si esto no estaba previsto, establece la altura máxima en algún valor."
|
||||||
|
previewIsNotActual: "La visualización difiere de la incrustación real porque excede el rango mostrado en la pantalla de vista previa."
|
||||||
|
rounded: "Bordes Redondeados"
|
||||||
|
border: "Añadir un borde al marco exterior"
|
||||||
|
applyToPreview: "Aplicar a la vista previa"
|
||||||
|
generateCode: "Crear el código para incrustar"
|
||||||
|
codeGenerated: "El código ha sido generado"
|
||||||
|
codeGeneratedDescription: "Pegue el código generado en su sitio web para incrustar el contenido."
|
||||||
|
_selfXssPrevention:
|
||||||
|
warning: "Advertencia"
|
||||||
|
title: "\"Pegar algo en esta pantalla\" es un timo."
|
||||||
|
description1: "Si pegas algo aquí, un usuario malintencionado podría secuestrar tu cuenta o robar tu información personal."
|
||||||
|
description2: "Si no entiendes que estás pegando exactamente, %cdetente ahora mismo y cierra esta ventana"
|
||||||
|
description3: "Para más información visita esto {link}"
|
||||||
_followRequest:
|
_followRequest:
|
||||||
recieved: "Petición de seguimiento recibida"
|
recieved: "Petición de seguimiento recibida"
|
||||||
sent: "Petición de seguimiento enviada"
|
sent: "Petición de seguimiento enviada"
|
||||||
_remoteLookupErrors:
|
_remoteLookupErrors:
|
||||||
_federationNotAllowed:
|
_federationNotAllowed:
|
||||||
|
title: "Incapaz de comunicarse con este servidor."
|
||||||
description: "Es posible que se haya desactivado la comunicación con este servidor o que haya sido bloqueado.\nPonte en contacto con el administrador del servidor.."
|
description: "Es posible que se haya desactivado la comunicación con este servidor o que haya sido bloqueado.\nPonte en contacto con el administrador del servidor.."
|
||||||
_uriInvalid:
|
_uriInvalid:
|
||||||
title: "La URI es inválida"
|
title: "La URI es inválida"
|
||||||
|
@ -2927,9 +3042,130 @@ _captcha:
|
||||||
text: "Se ha producido un error inesperado."
|
text: "Se ha producido un error inesperado."
|
||||||
_bootErrors:
|
_bootErrors:
|
||||||
title: "Fallo al cargar"
|
title: "Fallo al cargar"
|
||||||
|
serverError: "Si el problema persiste después de esperar un momento y volver a cargar, póngase en contacto con el administrador del servidor con el siguiente ID de error."
|
||||||
|
solution: "Lo siguiente puede resolver el problema."
|
||||||
|
solution1: "Actualiza tu navegador web y el sistema operativo a la última versión"
|
||||||
|
solution2: "Desactiva el AdBlocker"
|
||||||
|
solution3: "Borra la memoria caché del navegador web "
|
||||||
|
solution4: "(Navegador Tor) configura dom.webaudio.enabled a true"
|
||||||
|
otherOption: "Otras opciones"
|
||||||
|
otherOption1: "Borra la configuración y la memoria caché del cliente"
|
||||||
|
otherOption2: "Iniciar el cliente simple"
|
||||||
|
otherOption3: "Iniciar la herramienta de reparación"
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "Todo"
|
searchScopeAll: "Todo"
|
||||||
searchScopeLocal: "Local"
|
searchScopeLocal: "Local"
|
||||||
|
searchScopeServer: "Especifica el servidor (Instancia)"
|
||||||
searchScopeUser: "Especificar usuario"
|
searchScopeUser: "Especificar usuario"
|
||||||
|
pleaseEnterServerHost: "Introduce la dirección del servidor/Instancia"
|
||||||
|
pleaseSelectUser: "Selecciona un usuario, por favor"
|
||||||
|
serverHostPlaceholder: "Ejemplo: misskey.example.com"
|
||||||
|
_serverSetupWizard:
|
||||||
|
installCompleted: "¡La instalación de Misskey se ha completado!"
|
||||||
|
firstCreateAccount: "Para comenzar, crea una cuenta de administrador"
|
||||||
|
accountCreated: "¡La cuenta de administrador se ha creado! "
|
||||||
|
serverSetting: "Configuración del servidor"
|
||||||
|
youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Este asistente te facilita una configuración óptima del servidor."
|
||||||
|
settingsYouMakeHereCanBeChangedLater: "Los ajustes que han sido cambiados a través de este asistente pueden ser modificados más tarde."
|
||||||
|
howWillYouUseMisskey: "¿Cómo vas a usar Misskey?"
|
||||||
|
_use:
|
||||||
|
single: "Servidor para un único usuario."
|
||||||
|
single_description: "Utilízalo como tu propio servidor dedicado."
|
||||||
|
single_youCanCreateMultipleAccounts: "Se pueden crear múltiples cuentas según sea necesario, incluso cuando se opera como servidor unipersonal."
|
||||||
|
group: "Servidor de grupo"
|
||||||
|
group_description: "Invita otros usuarios de confianza y úsalo con más de una persona.\n"
|
||||||
|
open: "Servidor público"
|
||||||
|
open_description: "Permite a cualquiera registrarse"
|
||||||
|
openServerAdvice: "Aceptar un número no determinado de usuarios comporta algunos riesgos. Se recomienda operar con un sistema de moderación fiable para hacer frente a los problemas."
|
||||||
|
openServerAntiSpamAdvice: "Para evitar que su servidor se convierta en un trampolín para el spam, también debe prestar mucha atención a la seguridad habilitando funciones anti-bot como reCAPTCHA."
|
||||||
|
howManyUsersDoYouExpect: "¿Cuántas personas esperas?"
|
||||||
|
_scale:
|
||||||
|
small: "Menos de 100 (escala pequeña)"
|
||||||
|
medium: "Más de 100 y menos de 1000 (escala media)\n"
|
||||||
|
large: "Más de 1000(escala grande)"
|
||||||
|
largeScaleServerAdvice: "Los grandes servidores pueden requerir conocimientos avanzados de infraestructura, como equilibrio de carga y replicación de bases de datos."
|
||||||
|
doYouConnectToFediverse: "¿Quieres conectarte al Fediverso?"
|
||||||
|
doYouConnectToFediverse_description1: "Cuando se conecta a una red de servidores distribuidos (Fediverso), el contenido puede intercambiarse con otros servidores."
|
||||||
|
doYouConnectToFediverse_description2: "Conectarse con el Fediverso también se conoce como \"federación\"."
|
||||||
|
youCanConfigureMoreFederationSettingsLater: "Los ajustes avanzados, como la especificación de servidores federados, pueden configurarse más adelante."
|
||||||
|
adminInfo: "Información del administrador"
|
||||||
|
adminInfo_description: "Establece la información del administrador para recibir consultas."
|
||||||
|
adminInfo_mustBeFilled: "Esta información debe ser introducida en el caso de registros abiertos o la federación esté activada."
|
||||||
|
followingSettingsAreRecommended: "Se recomienda los siguientes ajustes"
|
||||||
|
applyTheseSettings: "Aplicar estos ajustes"
|
||||||
|
skipSettings: "Omitir configuración"
|
||||||
|
settingsCompleted: "¡Configuración inicial del servidor completada!"
|
||||||
|
settingsCompleted_description: "Gracias por tu tiempo. Ahora que está todo listo puedes empezar a utilizar el servidor inmediatamente."
|
||||||
|
settingsCompleted_description2: "La configuración avanzada del servidor pueden realizarse a través del \"Panel de control\"."
|
||||||
|
donationRequest: "Por favor Dona"
|
||||||
|
_donationRequest:
|
||||||
|
text1: "Misskey es un software libre desarrollado por voluntarios."
|
||||||
|
text2: "Agradeceríamos su apoyo para que podamos seguir desarrollando este software en el futuro."
|
||||||
|
text3: "También hay beneficios especiales para los donantes"
|
||||||
_uploader:
|
_uploader:
|
||||||
|
compressedToX: "Comprimir a {x}"
|
||||||
|
savedXPercent: "Guardando {x}%"
|
||||||
|
abortConfirm: "Algunos archivos no se han cargado, ¿deseas cancelar?"
|
||||||
|
doneConfirm: "Algunos archivos no se han cargado, ¿deseas continuar de todos modos?"
|
||||||
|
maxFileSizeIsX: "El tamaño máximo de archivo que se puede cargar es de {x}"
|
||||||
allowedTypes: "Tipos de archivos que se pueden cargar."
|
allowedTypes: "Tipos de archivos que se pueden cargar."
|
||||||
|
tip: "El archivo aún no se ha cargado, por lo que este cuadro de diálogo te permite confirmar, renombrar, comprimir y recortar el archivo antes de cargarlo. Cuando esté listo, puedes iniciar la carga pulsando el botón \"Cargar\"."
|
||||||
|
_clientPerformanceIssueTip:
|
||||||
|
title: "Si crees que el consumo de batería es demasiado alto"
|
||||||
|
makeSureDisabledAdBlocker: "Por favor, desactive el bloqueador de publicidad."
|
||||||
|
makeSureDisabledAdBlocker_description: "Los bloqueadores de anuncios pueden afectar al rendimiento. Asegúrate de que no están activados en tu sistema o en las funciones/extensiones de tu navegador."
|
||||||
|
makeSureDisabledCustomCss: "Desactiva el CSS personalizado"
|
||||||
|
makeSureDisabledCustomCss_description: "Anular estilos puede afectar al rendimiento. Asegúrate de que el CSS personalizado o las extensiones que sobrescriben estilos no están activados."
|
||||||
|
makeSureDisabledAddons: "Desactiva las extensiones "
|
||||||
|
makeSureDisabledAddons_description: "Algunas extensiones pueden interferir con el comportamiento del cliente y afectar al rendimiento. Por favor, deshabilita las extensiones de tu navegador y comprueba si esto mejora la situación."
|
||||||
|
_clip:
|
||||||
|
tip: "Clip es una función que permite organizar varias notas."
|
||||||
|
_userLists:
|
||||||
|
tip: "Las listas pueden contener cualquier usuario que especifiques al crearlas, la lista creada puede mostrarse entonces como una línea de tiempo mostrando solo los usuarios especificados."
|
||||||
|
watermark: "Marca de Agua"
|
||||||
|
defaultPreset: "Por defecto"
|
||||||
|
_watermarkEditor:
|
||||||
|
tip: "Se puede añadir a la imagen una marca de agua, como información crediticia."
|
||||||
|
quitWithoutSaveConfirm: "¿Descartar cambios no guardados?"
|
||||||
|
driveFileTypeWarn: "Este archivo es incompatible"
|
||||||
|
driveFileTypeWarnDescription: "Elegir una imagen"
|
||||||
|
title: "Editar la marca de agua"
|
||||||
|
cover: "Cubrir todo"
|
||||||
|
repeat: "Repetir"
|
||||||
|
opacity: "Opacidad"
|
||||||
|
scale: "Tamaño"
|
||||||
|
text: "Texto"
|
||||||
|
position: "Posición"
|
||||||
|
type: "Tipo"
|
||||||
|
image: "Imágenes"
|
||||||
|
advanced: "Avanzado"
|
||||||
|
stripe: "Rayas"
|
||||||
|
stripeWidth: "Anchura de línea"
|
||||||
|
stripeFrequency: "Número de líneas."
|
||||||
|
angle: "Ángulo"
|
||||||
|
polkadot: "Lunares"
|
||||||
|
checker: "verificador"
|
||||||
|
polkadotMainDotOpacity: "Opacidad del círculo principal"
|
||||||
|
polkadotMainDotRadius: "Tamaño del círculo principal."
|
||||||
|
polkadotSubDotOpacity: "Opacidad del círculo secundario"
|
||||||
|
polkadotSubDotRadius: "Tamaño del círculo secundario."
|
||||||
|
polkadotSubDotDivisions: "Número de subpuntos."
|
||||||
|
_imageEffector:
|
||||||
|
title: "Efecto"
|
||||||
|
addEffect: "Añadir Efecto"
|
||||||
|
discardChangesConfirm: "¿Ignorar cambios y salir?"
|
||||||
|
_fxs:
|
||||||
|
chromaticAberration: "Aberración Cromática"
|
||||||
|
glitch: "Glitch"
|
||||||
|
mirror: "Espejo"
|
||||||
|
invert: "Invertir colores"
|
||||||
|
grayscale: "Blanco y negro"
|
||||||
|
colorAdjust: "Corrección de Color"
|
||||||
|
colorClamp: "Compresión cromática"
|
||||||
|
colorClampAdvanced: "Compresión cromática avanzada"
|
||||||
|
distort: "Distorsión"
|
||||||
|
threshold: "umbral"
|
||||||
|
zoomLines: "Saturación de Líneas"
|
||||||
|
stripe: "Rayas"
|
||||||
|
polkadot: "Lunares"
|
||||||
|
checker: "Corrector"
|
||||||
|
|
|
@ -2360,3 +2360,13 @@ _search:
|
||||||
searchScopeAll: "Tous"
|
searchScopeAll: "Tous"
|
||||||
searchScopeLocal: "Local"
|
searchScopeLocal: "Local"
|
||||||
searchScopeUser: "Spécifier l'utilisateur·rice"
|
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"
|
searchScopeAll: "Semua"
|
||||||
searchScopeLocal: "Lokal"
|
searchScopeLocal: "Lokal"
|
||||||
searchScopeUser: "Pengguna spesifik"
|
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"
|
||||||
|
|
|
@ -5481,6 +5481,14 @@ export interface Locale extends ILocale {
|
||||||
* 全ての「ヒントとコツ」を非表示
|
* 全ての「ヒントとコツ」を非表示
|
||||||
*/
|
*/
|
||||||
"hideAllTips": string;
|
"hideAllTips": string;
|
||||||
|
/**
|
||||||
|
* デフォルトの画像圧縮度
|
||||||
|
*/
|
||||||
|
"defaultImageCompressionLevel": string;
|
||||||
|
/**
|
||||||
|
* 低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。
|
||||||
|
*/
|
||||||
|
"defaultImageCompressionLevel_description": string;
|
||||||
"_chat": {
|
"_chat": {
|
||||||
/**
|
/**
|
||||||
* まだメッセージはありません
|
* まだメッセージはありません
|
||||||
|
@ -5821,6 +5829,10 @@ export interface Locale extends ILocale {
|
||||||
* URLプレビューを表示する
|
* URLプレビューを表示する
|
||||||
*/
|
*/
|
||||||
"showUrlPreview": string;
|
"showUrlPreview": string;
|
||||||
|
/**
|
||||||
|
* 利用できるリアクションを先頭に表示
|
||||||
|
*/
|
||||||
|
"showAvailableReactionsFirstInNote": string;
|
||||||
"_chat": {
|
"_chat": {
|
||||||
/**
|
/**
|
||||||
* 送信者の名前を表示
|
* 送信者の名前を表示
|
||||||
|
@ -9572,6 +9584,14 @@ export interface Locale extends ILocale {
|
||||||
"disableFederationDescription": string;
|
"disableFederationDescription": string;
|
||||||
};
|
};
|
||||||
"_postForm": {
|
"_postForm": {
|
||||||
|
/**
|
||||||
|
* アップロードされていないファイルがありますが、破棄してフォームを閉じますか?
|
||||||
|
*/
|
||||||
|
"quitInspiteOfThereAreUnuploadedFilesConfirm": string;
|
||||||
|
/**
|
||||||
|
* ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。
|
||||||
|
*/
|
||||||
|
"uploaderTip": string;
|
||||||
/**
|
/**
|
||||||
* このノートに返信...
|
* このノートに返信...
|
||||||
*/
|
*/
|
||||||
|
@ -12020,6 +12040,196 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"tip": string;
|
"tip": string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* ウォーターマーク
|
||||||
|
*/
|
||||||
|
"watermark": string;
|
||||||
|
/**
|
||||||
|
* デフォルトのプリセット
|
||||||
|
*/
|
||||||
|
"defaultPreset": string;
|
||||||
|
"_watermarkEditor": {
|
||||||
|
/**
|
||||||
|
* 画像にクレジット情報などのウォーターマークを追加することができます。
|
||||||
|
*/
|
||||||
|
"tip": string;
|
||||||
|
/**
|
||||||
|
* 保存せずに終了しますか?
|
||||||
|
*/
|
||||||
|
"quitWithoutSaveConfirm": string;
|
||||||
|
/**
|
||||||
|
* このファイルは対応していません
|
||||||
|
*/
|
||||||
|
"driveFileTypeWarn": string;
|
||||||
|
/**
|
||||||
|
* 画像ファイルを選択してください
|
||||||
|
*/
|
||||||
|
"driveFileTypeWarnDescription": string;
|
||||||
|
/**
|
||||||
|
* ウォーターマークの編集
|
||||||
|
*/
|
||||||
|
"title": string;
|
||||||
|
/**
|
||||||
|
* 全体に被せる
|
||||||
|
*/
|
||||||
|
"cover": string;
|
||||||
|
/**
|
||||||
|
* 敷き詰める
|
||||||
|
*/
|
||||||
|
"repeat": string;
|
||||||
|
/**
|
||||||
|
* 不透明度
|
||||||
|
*/
|
||||||
|
"opacity": string;
|
||||||
|
/**
|
||||||
|
* サイズ
|
||||||
|
*/
|
||||||
|
"scale": string;
|
||||||
|
/**
|
||||||
|
* テキスト
|
||||||
|
*/
|
||||||
|
"text": string;
|
||||||
|
/**
|
||||||
|
* 位置
|
||||||
|
*/
|
||||||
|
"position": string;
|
||||||
|
/**
|
||||||
|
* タイプ
|
||||||
|
*/
|
||||||
|
"type": string;
|
||||||
|
/**
|
||||||
|
* 画像
|
||||||
|
*/
|
||||||
|
"image": string;
|
||||||
|
/**
|
||||||
|
* 高度
|
||||||
|
*/
|
||||||
|
"advanced": string;
|
||||||
|
/**
|
||||||
|
* ストライプ
|
||||||
|
*/
|
||||||
|
"stripe": string;
|
||||||
|
/**
|
||||||
|
* ラインの幅
|
||||||
|
*/
|
||||||
|
"stripeWidth": string;
|
||||||
|
/**
|
||||||
|
* ラインの数
|
||||||
|
*/
|
||||||
|
"stripeFrequency": string;
|
||||||
|
/**
|
||||||
|
* 角度
|
||||||
|
*/
|
||||||
|
"angle": string;
|
||||||
|
/**
|
||||||
|
* ポルカドット
|
||||||
|
*/
|
||||||
|
"polkadot": string;
|
||||||
|
/**
|
||||||
|
* チェッカー
|
||||||
|
*/
|
||||||
|
"checker": string;
|
||||||
|
/**
|
||||||
|
* メインドットの不透明度
|
||||||
|
*/
|
||||||
|
"polkadotMainDotOpacity": string;
|
||||||
|
/**
|
||||||
|
* メインドットの大きさ
|
||||||
|
*/
|
||||||
|
"polkadotMainDotRadius": string;
|
||||||
|
/**
|
||||||
|
* サブドットの不透明度
|
||||||
|
*/
|
||||||
|
"polkadotSubDotOpacity": string;
|
||||||
|
/**
|
||||||
|
* サブドットの大きさ
|
||||||
|
*/
|
||||||
|
"polkadotSubDotRadius": string;
|
||||||
|
/**
|
||||||
|
* サブドットの数
|
||||||
|
*/
|
||||||
|
"polkadotSubDotDivisions": string;
|
||||||
|
};
|
||||||
|
"_imageEffector": {
|
||||||
|
/**
|
||||||
|
* エフェクト
|
||||||
|
*/
|
||||||
|
"title": string;
|
||||||
|
/**
|
||||||
|
* エフェクトを追加
|
||||||
|
*/
|
||||||
|
"addEffect": string;
|
||||||
|
/**
|
||||||
|
* 変更を破棄して終了しますか?
|
||||||
|
*/
|
||||||
|
"discardChangesConfirm": string;
|
||||||
|
"_fxs": {
|
||||||
|
/**
|
||||||
|
* 色収差
|
||||||
|
*/
|
||||||
|
"chromaticAberration": string;
|
||||||
|
/**
|
||||||
|
* グリッチ
|
||||||
|
*/
|
||||||
|
"glitch": string;
|
||||||
|
/**
|
||||||
|
* ミラー
|
||||||
|
*/
|
||||||
|
"mirror": string;
|
||||||
|
/**
|
||||||
|
* 色の反転
|
||||||
|
*/
|
||||||
|
"invert": string;
|
||||||
|
/**
|
||||||
|
* 白黒
|
||||||
|
*/
|
||||||
|
"grayscale": string;
|
||||||
|
/**
|
||||||
|
* 色調補正
|
||||||
|
*/
|
||||||
|
"colorAdjust": string;
|
||||||
|
/**
|
||||||
|
* 色の圧縮
|
||||||
|
*/
|
||||||
|
"colorClamp": string;
|
||||||
|
/**
|
||||||
|
* 色の圧縮(高度)
|
||||||
|
*/
|
||||||
|
"colorClampAdvanced": string;
|
||||||
|
/**
|
||||||
|
* 歪み
|
||||||
|
*/
|
||||||
|
"distort": string;
|
||||||
|
/**
|
||||||
|
* 二値化
|
||||||
|
*/
|
||||||
|
"threshold": string;
|
||||||
|
/**
|
||||||
|
* 集中線
|
||||||
|
*/
|
||||||
|
"zoomLines": string;
|
||||||
|
/**
|
||||||
|
* ストライプ
|
||||||
|
*/
|
||||||
|
"stripe": string;
|
||||||
|
/**
|
||||||
|
* ポルカドット
|
||||||
|
*/
|
||||||
|
"polkadot": string;
|
||||||
|
/**
|
||||||
|
* チェッカー
|
||||||
|
*/
|
||||||
|
"checker": string;
|
||||||
|
/**
|
||||||
|
* ブロックノイズ
|
||||||
|
*/
|
||||||
|
"blockNoise": string;
|
||||||
|
/**
|
||||||
|
* ティアリング
|
||||||
|
*/
|
||||||
|
"tearing": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
@ -327,6 +327,7 @@ dark: "Scuro"
|
||||||
lightThemes: "Tema Chiaro"
|
lightThemes: "Tema Chiaro"
|
||||||
darkThemes: "Tema Scuro"
|
darkThemes: "Tema Scuro"
|
||||||
syncDeviceDarkMode: "Sincronizza il tema scuro con le impostazioni del dispositivo"
|
syncDeviceDarkMode: "Sincronizza il tema scuro con le impostazioni del dispositivo"
|
||||||
|
switchDarkModeManuallyWhenSyncEnabledConfirm: "({x}) è attiva. Vuoi disattivare la sincronizzazione e passare alla modalità manuale?"
|
||||||
drive: "Drive"
|
drive: "Drive"
|
||||||
fileName: "Nome dell'allegato"
|
fileName: "Nome dell'allegato"
|
||||||
selectFile: "Scelta allegato"
|
selectFile: "Scelta allegato"
|
||||||
|
@ -581,7 +582,7 @@ sounds: "Impostazioni suoni"
|
||||||
sound: "Suono"
|
sound: "Suono"
|
||||||
notificationSoundSettings: "Preferenze di notifica"
|
notificationSoundSettings: "Preferenze di notifica"
|
||||||
listen: "Ascolta"
|
listen: "Ascolta"
|
||||||
none: "Nessuno"
|
none: "Nessuna"
|
||||||
showInPage: "Visualizza in pagina"
|
showInPage: "Visualizza in pagina"
|
||||||
popout: "Finestra pop-out"
|
popout: "Finestra pop-out"
|
||||||
volume: "Volume"
|
volume: "Volume"
|
||||||
|
@ -1329,6 +1330,7 @@ restore: "Ripristina"
|
||||||
syncBetweenDevices: "Sincronizzazione tra i dispositivi"
|
syncBetweenDevices: "Sincronizzazione tra i dispositivi"
|
||||||
preferenceSyncConflictTitle: "Sul server esiste già il valore impostato"
|
preferenceSyncConflictTitle: "Sul server esiste già il valore impostato"
|
||||||
preferenceSyncConflictText: "Le impostazione sincronizzata salverà il valore sul server. Però, bada che esiste già un valore sul server. Quale vorresti sovrascrivere?"
|
preferenceSyncConflictText: "Le impostazione sincronizzata salverà il valore sul server. Però, bada che esiste già un valore sul server. Quale vorresti sovrascrivere?"
|
||||||
|
preferenceSyncConflictChoiceMerge: "Integra"
|
||||||
preferenceSyncConflictChoiceServer: "Valore del server"
|
preferenceSyncConflictChoiceServer: "Valore del server"
|
||||||
preferenceSyncConflictChoiceDevice: "Valore del dispositivo"
|
preferenceSyncConflictChoiceDevice: "Valore del dispositivo"
|
||||||
preferenceSyncConflictChoiceCancel: "Annulla la sincronizzazione"
|
preferenceSyncConflictChoiceCancel: "Annulla la sincronizzazione"
|
||||||
|
@ -1340,7 +1342,7 @@ information: "Informazioni"
|
||||||
chat: "Chat"
|
chat: "Chat"
|
||||||
migrateOldSettings: "Migrare le vecchie impostazioni"
|
migrateOldSettings: "Migrare le vecchie impostazioni"
|
||||||
migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali."
|
migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali."
|
||||||
compress: "Comprimi"
|
compress: "Compressione"
|
||||||
right: "Destra"
|
right: "Destra"
|
||||||
bottom: "Sotto"
|
bottom: "Sotto"
|
||||||
top: "Sopra"
|
top: "Sopra"
|
||||||
|
@ -1349,6 +1351,7 @@ settingsMigrating: "Migrazione delle impostazioni. Attendere prego ... (Puoi anc
|
||||||
readonly: "Sola lettura"
|
readonly: "Sola lettura"
|
||||||
goToDeck: "Torna al Deck"
|
goToDeck: "Torna al Deck"
|
||||||
federationJobs: "Coda di federazione"
|
federationJobs: "Coda di federazione"
|
||||||
|
driveAboutTip: "Il Drive mostra l'elenco di file caricati in passato. Puoi organizzarli in cartelle, riusarli allegandoli ad altre note, o caricarli in anticipo e poi pubblicarli in un secondo momento. Tieni presente che se elimini un file, non sarà più visibile in nessuno degli oggetti a cui è allegato (Note, pagine, avatar, banner, ecc.)"
|
||||||
scrollToClose: "Scorri per chiudere"
|
scrollToClose: "Scorri per chiudere"
|
||||||
advice: "Consiglio"
|
advice: "Consiglio"
|
||||||
realtimeMode: "Modalità in tempo reale"
|
realtimeMode: "Modalità in tempo reale"
|
||||||
|
@ -1362,6 +1365,8 @@ abort: "Annulla"
|
||||||
tip: "Suggerimento"
|
tip: "Suggerimento"
|
||||||
redisplayAllTips: "Mostra tutti i suggerimenti"
|
redisplayAllTips: "Mostra tutti i suggerimenti"
|
||||||
hideAllTips: "Nascondi tutti i suggerimenti"
|
hideAllTips: "Nascondi tutti i suggerimenti"
|
||||||
|
defaultImageCompressionLevel: "Livello predefinito di compressione immagini"
|
||||||
|
defaultImageCompressionLevel_description: "La compressione diminuisce la qualità dell'immagine, poca compressione mantiene alta qualità delle immagini. Aumentandola, si riducono le dimensioni del file, a discapito della qualità dell'immagine."
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "Ancora nessun messaggio"
|
noMessagesYet: "Ancora nessun messaggio"
|
||||||
newMessage: "Nuovo messaggio"
|
newMessage: "Nuovo messaggio"
|
||||||
|
@ -1449,6 +1454,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description: "Se l'impostazione è alta, verranno aggiornati più frequentemente, consumando più dati e più batteria."
|
contentsUpdateFrequency_description: "Se l'impostazione è alta, verranno aggiornati più frequentemente, consumando più dati e più batteria."
|
||||||
contentsUpdateFrequency_description2: "Quando la modalità è in tempo reale, arriveranno a prescindere."
|
contentsUpdateFrequency_description2: "Quando la modalità è in tempo reale, arriveranno a prescindere."
|
||||||
showUrlPreview: "Mostra anteprima dell'URL"
|
showUrlPreview: "Mostra anteprima dell'URL"
|
||||||
|
showAvailableReactionsFirstInNote: "Mostra le reazioni disponibili in alto"
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "Mostra il nome del mittente"
|
showSenderName: "Mostra il nome del mittente"
|
||||||
sendOnEnter: "Invio spedisce"
|
sendOnEnter: "Invio spedisce"
|
||||||
|
@ -2459,6 +2465,8 @@ _visibility:
|
||||||
disableFederation: "Senza federazione"
|
disableFederation: "Senza federazione"
|
||||||
disableFederationDescription: "Non spedire attività alle altre istanze remote"
|
disableFederationDescription: "Non spedire attività alle altre istanze remote"
|
||||||
_postForm:
|
_postForm:
|
||||||
|
quitInspiteOfThereAreUnuploadedFilesConfirm: "Alcuni file non sono stati caricati. Vuoi annullare l'operazione?"
|
||||||
|
uploaderTip: "Il file non è ancora stato caricato. Nel menu file (tre puntini), puoi ritagliare l'immagine, mettere la filigrana, decidere la presenza o l'assenza di compressione... Il file verrà caricato automaticamente quando pubblichi la Nota."
|
||||||
replyPlaceholder: "Rispondi a questa nota..."
|
replyPlaceholder: "Rispondi a questa nota..."
|
||||||
quotePlaceholder: "Cita questa nota..."
|
quotePlaceholder: "Cita questa nota..."
|
||||||
channelPlaceholder: "Pubblica sul canale..."
|
channelPlaceholder: "Pubblica sul canale..."
|
||||||
|
@ -2902,6 +2910,8 @@ _offlineScreen:
|
||||||
_urlPreviewSetting:
|
_urlPreviewSetting:
|
||||||
title: "Impostazioni per l'anteprima delle URL"
|
title: "Impostazioni per l'anteprima delle URL"
|
||||||
enable: "Attiva l'anteprima delle URL"
|
enable: "Attiva l'anteprima delle URL"
|
||||||
|
allowRedirect: "Segui i reindirizzamenti per visualizzare le anteprime"
|
||||||
|
allowRedirectDescription: "Se la URL inserita contiene un reindirizzamento, decidi di seguire il reindirizzamento fino alla destinazione, visualizzandone l'anteprima. Disabilitando questa opzione si risparmiano risorse del server, ma il contenuto effettivo dal reindirizzamento, non verrà visualizzato."
|
||||||
timeout: "Timeout dell'anteprima in millisecondi"
|
timeout: "Timeout dell'anteprima in millisecondi"
|
||||||
timeoutDescription: "Impegna al massimo il tempo indicato, altrimenti ignora l'anteprima"
|
timeoutDescription: "Impegna al massimo il tempo indicato, altrimenti ignora l'anteprima"
|
||||||
maximumContentLength: "Grandezza del contenuto (Content-Length in byte)"
|
maximumContentLength: "Grandezza del contenuto (Content-Length in byte)"
|
||||||
|
@ -3112,3 +3122,50 @@ _clip:
|
||||||
tip: "Le clip sono una funzionalità che consente di raggruppare le Note."
|
tip: "Le clip sono una funzionalità che consente di raggruppare le Note."
|
||||||
_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."
|
||||||
|
watermark: "Filigrana"
|
||||||
|
defaultPreset: "Impostazioni predefinite"
|
||||||
|
_watermarkEditor:
|
||||||
|
tip: "Puoi aggiungere una filigrana, ad esempio con i crediti alle tue immagini."
|
||||||
|
quitWithoutSaveConfirm: "Uscire senza salvare?"
|
||||||
|
driveFileTypeWarn: "Formato file non supportato"
|
||||||
|
driveFileTypeWarnDescription: "Per favore seleziona un file immagine"
|
||||||
|
title: "Modifica la filigrana"
|
||||||
|
cover: "Coprire tutto"
|
||||||
|
repeat: "Disposizione"
|
||||||
|
opacity: "Opacità"
|
||||||
|
scale: "Dimensioni"
|
||||||
|
text: "Testo"
|
||||||
|
position: "Posizione"
|
||||||
|
type: "Tipo"
|
||||||
|
image: "Immagini"
|
||||||
|
advanced: "Avanzato"
|
||||||
|
stripe: "Strisce"
|
||||||
|
stripeWidth: "Larghezza della linea"
|
||||||
|
stripeFrequency: "Il numero di linee"
|
||||||
|
angle: "Angolo"
|
||||||
|
polkadot: "A pallini"
|
||||||
|
checker: "revisore"
|
||||||
|
polkadotMainDotOpacity: "Opacità del punto principale"
|
||||||
|
polkadotMainDotRadius: "Dimensione del punto principale"
|
||||||
|
polkadotSubDotOpacity: "Opacità del punto secondario"
|
||||||
|
polkadotSubDotRadius: "Dimensione del punto secondario"
|
||||||
|
polkadotSubDotDivisions: "Quantità di punti secondari"
|
||||||
|
_imageEffector:
|
||||||
|
title: "Effetto"
|
||||||
|
addEffect: "Aggiungi effetto"
|
||||||
|
discardChangesConfirm: "Scarta le modifiche ed esci?"
|
||||||
|
_fxs:
|
||||||
|
chromaticAberration: "Aberrazione cromatica"
|
||||||
|
glitch: "Glitch"
|
||||||
|
mirror: "Specchio"
|
||||||
|
invert: "Inversione colore"
|
||||||
|
grayscale: "Bianco e nero"
|
||||||
|
colorAdjust: "Correzione Colore"
|
||||||
|
colorClamp: "Compressione del colore"
|
||||||
|
colorClampAdvanced: "Compressione del colore (avanzata)"
|
||||||
|
distort: "Distorsione"
|
||||||
|
threshold: "Soglia"
|
||||||
|
zoomLines: "Linea di saturazione"
|
||||||
|
stripe: "Strisce"
|
||||||
|
polkadot: "A pallini"
|
||||||
|
checker: "revisore"
|
||||||
|
|
|
@ -1365,6 +1365,8 @@ abort: "中止"
|
||||||
tip: "ヒントとコツ"
|
tip: "ヒントとコツ"
|
||||||
redisplayAllTips: "全ての「ヒントとコツ」を再表示"
|
redisplayAllTips: "全ての「ヒントとコツ」を再表示"
|
||||||
hideAllTips: "全ての「ヒントとコツ」を非表示"
|
hideAllTips: "全ての「ヒントとコツ」を非表示"
|
||||||
|
defaultImageCompressionLevel: "デフォルトの画像圧縮度"
|
||||||
|
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
|
||||||
|
|
||||||
_chat:
|
_chat:
|
||||||
noMessagesYet: "まだメッセージはありません"
|
noMessagesYet: "まだメッセージはありません"
|
||||||
|
@ -1455,6 +1457,7 @@ _settings:
|
||||||
contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。"
|
contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。"
|
||||||
contentsUpdateFrequency_description2: "リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。"
|
contentsUpdateFrequency_description2: "リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。"
|
||||||
showUrlPreview: "URLプレビューを表示する"
|
showUrlPreview: "URLプレビューを表示する"
|
||||||
|
showAvailableReactionsFirstInNote: "利用できるリアクションを先頭に表示"
|
||||||
|
|
||||||
_chat:
|
_chat:
|
||||||
showSenderName: "送信者の名前を表示"
|
showSenderName: "送信者の名前を表示"
|
||||||
|
@ -2519,6 +2522,8 @@ _visibility:
|
||||||
disableFederationDescription: "他サーバーへの配信を行いません"
|
disableFederationDescription: "他サーバーへの配信を行いません"
|
||||||
|
|
||||||
_postForm:
|
_postForm:
|
||||||
|
quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされていないファイルがありますが、破棄してフォームを閉じますか?"
|
||||||
|
uploaderTip: "ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。"
|
||||||
replyPlaceholder: "このノートに返信..."
|
replyPlaceholder: "このノートに返信..."
|
||||||
quotePlaceholder: "このノートを引用..."
|
quotePlaceholder: "このノートを引用..."
|
||||||
channelPlaceholder: "チャンネルに投稿..."
|
channelPlaceholder: "チャンネルに投稿..."
|
||||||
|
@ -3218,3 +3223,55 @@ _clip:
|
||||||
|
|
||||||
_userLists:
|
_userLists:
|
||||||
tip: "任意のユーザーが含まれるリストを作成できます。作成したリストはタイムラインとして表示可能です。"
|
tip: "任意のユーザーが含まれるリストを作成できます。作成したリストはタイムラインとして表示可能です。"
|
||||||
|
|
||||||
|
watermark: "ウォーターマーク"
|
||||||
|
defaultPreset: "デフォルトのプリセット"
|
||||||
|
_watermarkEditor:
|
||||||
|
tip: "画像にクレジット情報などのウォーターマークを追加することができます。"
|
||||||
|
quitWithoutSaveConfirm: "保存せずに終了しますか?"
|
||||||
|
driveFileTypeWarn: "このファイルは対応していません"
|
||||||
|
driveFileTypeWarnDescription: "画像ファイルを選択してください"
|
||||||
|
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: "白黒"
|
||||||
|
colorAdjust: "色調補正"
|
||||||
|
colorClamp: "色の圧縮"
|
||||||
|
colorClampAdvanced: "色の圧縮(高度)"
|
||||||
|
distort: "歪み"
|
||||||
|
threshold: "二値化"
|
||||||
|
zoomLines: "集中線"
|
||||||
|
stripe: "ストライプ"
|
||||||
|
polkadot: "ポルカドット"
|
||||||
|
checker: "チェッカー"
|
||||||
|
blockNoise: "ブロックノイズ"
|
||||||
|
tearing: "ティアリング"
|
||||||
|
|
|
@ -2848,3 +2848,13 @@ _search:
|
||||||
searchScopeAll: "みんな"
|
searchScopeAll: "みんな"
|
||||||
searchScopeLocal: "ローカル"
|
searchScopeLocal: "ローカル"
|
||||||
searchScopeUser: "ユーザー指定"
|
searchScopeUser: "ユーザー指定"
|
||||||
|
_watermarkEditor:
|
||||||
|
driveFileTypeWarn: "このファイルは対応しとらへん"
|
||||||
|
opacity: "不透明度"
|
||||||
|
scale: "大きさ"
|
||||||
|
text: "テキスト"
|
||||||
|
position: "位置"
|
||||||
|
type: "タイプ"
|
||||||
|
image: "画像"
|
||||||
|
advanced: "高度"
|
||||||
|
angle: "角度"
|
||||||
|
|
|
@ -848,3 +848,5 @@ _remoteLookupErrors:
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "말캉"
|
searchScopeAll: "말캉"
|
||||||
searchScopeUser: "사용자 지정"
|
searchScopeUser: "사용자 지정"
|
||||||
|
_watermarkEditor:
|
||||||
|
image: "이미지"
|
||||||
|
|
|
@ -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: "엔터로 보내기"
|
||||||
|
@ -2462,6 +2465,8 @@ _visibility:
|
||||||
disableFederation: "연합에 보내지 않기"
|
disableFederation: "연합에 보내지 않기"
|
||||||
disableFederationDescription: "다른 서버로 보내지 않습니다"
|
disableFederationDescription: "다른 서버로 보내지 않습니다"
|
||||||
_postForm:
|
_postForm:
|
||||||
|
quitInspiteOfThereAreUnuploadedFilesConfirm: "업로드되지 않은 파일이 있습니다만, 없애고 폼을 닫겠습니까?"
|
||||||
|
uploaderTip: "파일이 아직 업로드돼있지 않습니다. 파일 메뉴에서 이름 바꾸기나 이미지의 자르기, 워터마크 넣기, 압축의 유무 등을 설정할 수 있습니다. 파일은 노트 게시 시 자동으로 업로드됩니다."
|
||||||
replyPlaceholder: "이 노트에 답글..."
|
replyPlaceholder: "이 노트에 답글..."
|
||||||
quotePlaceholder: "이 노트를 인용..."
|
quotePlaceholder: "이 노트를 인용..."
|
||||||
channelPlaceholder: "채널에 게시하기..."
|
channelPlaceholder: "채널에 게시하기..."
|
||||||
|
@ -3078,7 +3083,7 @@ _serverSetupWizard:
|
||||||
small: "100명 이하(소규모)"
|
small: "100명 이하(소규모)"
|
||||||
medium: "100명 이상 1000명 이하(중간 규모)"
|
medium: "100명 이상 1000명 이하(중간 규모)"
|
||||||
large: "1000명 이상(대규모)"
|
large: "1000명 이상(대규모)"
|
||||||
largeScaleServerAdvice: "대규모 서버에서는 부하분산이나 데이터베이스의 레플리케이션 등 높은 인프라스트럭처 지식이 필요할 수 있습니다."
|
largeScaleServerAdvice: "대규모 서버에서는 부하분산이나 데이터베이스의 복제 등 높은 인프라스트럭처 지식이 필요할 수 있습니다."
|
||||||
doYouConnectToFediverse: "Fediverse에 접속하시겠습니까?"
|
doYouConnectToFediverse: "Fediverse에 접속하시겠습니까?"
|
||||||
doYouConnectToFediverse_description1: "분산형 서버로 구성된 네트워크(Fediverse)에 접속하면 다른 서버와 서로 콘텐츠의 주고받기를 할 수 있습니다."
|
doYouConnectToFediverse_description1: "분산형 서버로 구성된 네트워크(Fediverse)에 접속하면 다른 서버와 서로 콘텐츠의 주고받기를 할 수 있습니다."
|
||||||
doYouConnectToFediverse_description2: "Fediverse에 접속하는 것을 '연합'이라고도 부릅니다."
|
doYouConnectToFediverse_description2: "Fediverse에 접속하는 것을 '연합'이라고도 부릅니다."
|
||||||
|
@ -3117,3 +3122,50 @@ _clip:
|
||||||
tip: "클립은 노트를 정리할 수 있는 기능입니다."
|
tip: "클립은 노트를 정리할 수 있는 기능입니다."
|
||||||
_userLists:
|
_userLists:
|
||||||
tip: "임의의 유저가 포함된 리스트를 작성할 수 있습니다. 작성한 리스트는 타임라인으로 표시가 가능합니다."
|
tip: "임의의 유저가 포함된 리스트를 작성할 수 있습니다. 작성한 리스트는 타임라인으로 표시가 가능합니다."
|
||||||
|
watermark: "워터마크"
|
||||||
|
defaultPreset: "기본 프리셋"
|
||||||
|
_watermarkEditor:
|
||||||
|
tip: "이미지에 크레딧 정보 등의 워터마크를 추가할 수 있습니다."
|
||||||
|
quitWithoutSaveConfirm: "보존하지 않고 종료하시겠습니까?"
|
||||||
|
driveFileTypeWarn: "이 파이"
|
||||||
|
driveFileTypeWarnDescription: "이미지 파일을 선택해주십시오."
|
||||||
|
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: "흑백"
|
||||||
|
colorAdjust: "색조 보정"
|
||||||
|
colorClamp: "색 압축"
|
||||||
|
colorClampAdvanced: "색 압축(고급)"
|
||||||
|
distort: "뒤틀림"
|
||||||
|
threshold: "이진화"
|
||||||
|
zoomLines: "집중선"
|
||||||
|
stripe: "줄무늬"
|
||||||
|
polkadot: "물방울 무늬"
|
||||||
|
checker: "체크 무늬"
|
||||||
|
|
|
@ -483,3 +483,5 @@ _remoteLookupErrors:
|
||||||
title: "ບໍ່ພົບ"
|
title: "ບໍ່ພົບ"
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "ທັງໝົດ"
|
searchScopeAll: "ທັງໝົດ"
|
||||||
|
_watermarkEditor:
|
||||||
|
image: "ຮູບພາບ"
|
||||||
|
|
|
@ -1078,3 +1078,6 @@ _remoteLookupErrors:
|
||||||
title: "Niet gevonden"
|
title: "Niet gevonden"
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "Alle"
|
searchScopeAll: "Alle"
|
||||||
|
_watermarkEditor:
|
||||||
|
image: "Afbeeldingen"
|
||||||
|
advanced: "Geavanceerd"
|
||||||
|
|
|
@ -735,3 +735,8 @@ _remoteLookupErrors:
|
||||||
title: "Ikke funnet"
|
title: "Ikke funnet"
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "Alle"
|
searchScopeAll: "Alle"
|
||||||
|
_watermarkEditor:
|
||||||
|
scale: "Størrelse"
|
||||||
|
text: "Tekst"
|
||||||
|
type: "Type"
|
||||||
|
image: "Bilder"
|
||||||
|
|
|
@ -1584,3 +1584,10 @@ _remoteLookupErrors:
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "Wszystkie"
|
searchScopeAll: "Wszystkie"
|
||||||
searchScopeLocal: "Lokalne"
|
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."
|
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: "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:
|
||||||
|
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"
|
searchScopeLocal: "Local"
|
||||||
searchScopeUser: "Utilizator specific"
|
searchScopeUser: "Utilizator specific"
|
||||||
serverHostPlaceholder: "Exemplu: misskey.example.com"
|
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: "Все"
|
searchScopeAll: "Все"
|
||||||
searchScopeLocal: "Местная"
|
searchScopeLocal: "Местная"
|
||||||
searchScopeUser: "Указанный пользователь"
|
searchScopeUser: "Указанный пользователь"
|
||||||
|
_watermarkEditor:
|
||||||
|
opacity: "Непрозрачность"
|
||||||
|
scale: "Размер"
|
||||||
|
text: "Текст"
|
||||||
|
position: "Позиция"
|
||||||
|
type: "Тип"
|
||||||
|
image: "Изображения"
|
||||||
|
advanced: "Для продвинутых"
|
||||||
|
angle: "Угол"
|
||||||
|
|
|
@ -1450,3 +1450,10 @@ _remoteLookupErrors:
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "Všetko"
|
searchScopeAll: "Všetko"
|
||||||
searchScopeLocal: "Lokálne"
|
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"
|
warning: "VARNING"
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "Allt"
|
searchScopeAll: "Allt"
|
||||||
|
_watermarkEditor:
|
||||||
|
scale: "Storlek"
|
||||||
|
image: "Bilder"
|
||||||
|
|
|
@ -220,6 +220,7 @@ silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี
|
||||||
mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้"
|
mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้"
|
||||||
operations: "ดำเนินการ"
|
operations: "ดำเนินการ"
|
||||||
software: "ซอฟต์แวร์"
|
software: "ซอฟต์แวร์"
|
||||||
|
softwareName: "ชื่อซอฟต์แวร์"
|
||||||
version: "เวอร์ชั่น"
|
version: "เวอร์ชั่น"
|
||||||
metadata: "Metadata"
|
metadata: "Metadata"
|
||||||
withNFiles: "{n} ไฟล์"
|
withNFiles: "{n} ไฟล์"
|
||||||
|
@ -1293,6 +1294,10 @@ federationDisabled: "เซิร์ฟเวอร์นี้ปิดกา
|
||||||
reactAreYouSure: "คุณต้องการที่จะตอบสนองต่อ \" {emoji}\" หรือไม่?"
|
reactAreYouSure: "คุณต้องการที่จะตอบสนองต่อ \" {emoji}\" หรือไม่?"
|
||||||
markAsSensitiveConfirm: "คุณต้องการทำเครื่องหมายสื่อนี้ว่าละเอียดอ่อนหรือไม่?"
|
markAsSensitiveConfirm: "คุณต้องการทำเครื่องหมายสื่อนี้ว่าละเอียดอ่อนหรือไม่?"
|
||||||
unmarkAsSensitiveConfirm: "คุณต้องการลบการกำหนดความไวของสื่อนี้หรือไม่?"
|
unmarkAsSensitiveConfirm: "คุณต้องการลบการกำหนดความไวของสื่อนี้หรือไม่?"
|
||||||
|
preferences: "การตั้งค่าสภาพแวดล้อม"
|
||||||
|
preferencesProfile: "โปรไฟล์การกำหนดค่า"
|
||||||
|
preferenceSyncConflictTitle: "การตั้งค่ามีอยู่บนเซิร์ฟเวอร์"
|
||||||
|
preferenceSyncConflictText: "รายการการตั้งค่าที่เปิดใช้งานการซิงโครไนซ์จะจัดเก็บค่าไว้บนเซิร์ฟเวอร์ และพบค่าที่จัดเก็บบนเซิร์ฟเวอร์สำหรับรายการการตั้งค่านี้ คุณต้องการทำอย่างไร?"
|
||||||
postForm: "แบบฟอร์มการโพสต์"
|
postForm: "แบบฟอร์มการโพสต์"
|
||||||
information: "เกี่ยวกับ"
|
information: "เกี่ยวกับ"
|
||||||
right: "ขวา"
|
right: "ขวา"
|
||||||
|
@ -1305,6 +1310,7 @@ _chat:
|
||||||
send: "ส่ง"
|
send: "ส่ง"
|
||||||
_settings:
|
_settings:
|
||||||
webhook: "Webhook"
|
webhook: "Webhook"
|
||||||
|
preferencesBanner: "คุณสามารถกำหนดค่าพฤติกรรมโดยรวมของไคลเอนต์ได้ตามความต้องการของคุณ"
|
||||||
_accountSettings:
|
_accountSettings:
|
||||||
requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา"
|
requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา"
|
||||||
requireSigninToViewContentsDescription1: "ต้องเข้าสู่ระบบเพื่อดูบันทึกและเนื้อหาอื่น ๆ ทั้งหมดที่คุณสร้าง คาดว่าจะมีประสิทธิผลในการป้องกันไม่ให้ข้อมูลถูกเก็บรวบรวมโดยโปรแกรมรวบรวมข้อมูล"
|
requireSigninToViewContentsDescription1: "ต้องเข้าสู่ระบบเพื่อดูบันทึกและเนื้อหาอื่น ๆ ทั้งหมดที่คุณสร้าง คาดว่าจะมีประสิทธิผลในการป้องกันไม่ให้ข้อมูลถูกเก็บรวบรวมโดยโปรแกรมรวบรวมข้อมูล"
|
||||||
|
@ -2722,3 +2728,13 @@ _search:
|
||||||
searchScopeAll: "ทั้งหมด"
|
searchScopeAll: "ทั้งหมด"
|
||||||
searchScopeLocal: "ท้องถิ่น"
|
searchScopeLocal: "ท้องถิ่น"
|
||||||
searchScopeUser: "ผู้ใช้เฉพาะ"
|
searchScopeUser: "ผู้ใช้เฉพาะ"
|
||||||
|
_watermarkEditor:
|
||||||
|
driveFileTypeWarn: "ไม่รองรับไฟล์นี้"
|
||||||
|
opacity: "ความทึบแสง"
|
||||||
|
scale: "ขนาด"
|
||||||
|
text: "ข้อความ"
|
||||||
|
position: "ตำแหน่ง"
|
||||||
|
type: "รูปแบบ"
|
||||||
|
image: "รูปภาพ"
|
||||||
|
advanced: "ขั้นสูง"
|
||||||
|
angle: "แองเกิล"
|
||||||
|
|
|
@ -460,3 +460,5 @@ _moderationLogTypes:
|
||||||
resetPassword: "Şifre sıfırlama"
|
resetPassword: "Şifre sıfırlama"
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "Tümü"
|
searchScopeAll: "Tümü"
|
||||||
|
_watermarkEditor:
|
||||||
|
image: "Görseller"
|
||||||
|
|
|
@ -1625,3 +1625,10 @@ _remoteLookupErrors:
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "Всі"
|
searchScopeAll: "Всі"
|
||||||
searchScopeLocal: "Локальна"
|
searchScopeLocal: "Локальна"
|
||||||
|
_watermarkEditor:
|
||||||
|
opacity: "Непрозорість"
|
||||||
|
scale: "Розмір"
|
||||||
|
text: "Текст"
|
||||||
|
type: "Тип"
|
||||||
|
image: "Зображення"
|
||||||
|
advanced: "Розширені"
|
||||||
|
|
|
@ -1097,3 +1097,8 @@ _remoteLookupErrors:
|
||||||
_search:
|
_search:
|
||||||
searchScopeAll: "Barcha"
|
searchScopeAll: "Barcha"
|
||||||
searchScopeLocal: "Mahalliy"
|
searchScopeLocal: "Mahalliy"
|
||||||
|
_watermarkEditor:
|
||||||
|
text: "Matn"
|
||||||
|
type: "turi"
|
||||||
|
image: "Rasmlar"
|
||||||
|
advanced: "Murakkab"
|
||||||
|
|
|
@ -220,6 +220,7 @@ silenceThisInstance: "Máy chủ im lặng"
|
||||||
mediaSilenceThisInstance: "Tắt nội dung đa phương tiện từ máy chủ này"
|
mediaSilenceThisInstance: "Tắt nội dung đa phương tiện từ máy chủ này"
|
||||||
operations: "Vận hành"
|
operations: "Vận hành"
|
||||||
software: "Phần mềm"
|
software: "Phần mềm"
|
||||||
|
softwareName: "Tên phần mềm"
|
||||||
version: "Phiên bản"
|
version: "Phiên bản"
|
||||||
metadata: "Metadata"
|
metadata: "Metadata"
|
||||||
withNFiles: "{n} tập tin"
|
withNFiles: "{n} tập tin"
|
||||||
|
@ -1211,6 +1212,9 @@ federationDisabled: "Liên kết bị vô hiệu hóa trên máy chủ này. B
|
||||||
reactAreYouSure: "Bạn có muốn phản hồi với \" {emoji} \" không?"
|
reactAreYouSure: "Bạn có muốn phản hồi với \" {emoji} \" không?"
|
||||||
preferences: "Thiết lập môi trường"
|
preferences: "Thiết lập môi trường"
|
||||||
accessibility: "Khả năng tiếp cận"
|
accessibility: "Khả năng tiếp cận"
|
||||||
|
preferencesProfile: "Hồ sơ sở thích"
|
||||||
|
preferenceSyncConflictTitle: "Cài đặt tồn tại trên máy chủ"
|
||||||
|
preferenceSyncConflictText: "Các thiết lập đồng bộ hóa được bật sẽ lưu các giá trị của chúng vào máy chủ. Tuy nhiên, có những giá trị hiện có trên máy chủ. Bạn muốn ghi đè lên bộ giá trị nào?"
|
||||||
paste: "dán"
|
paste: "dán"
|
||||||
postForm: "Mẫu đăng"
|
postForm: "Mẫu đăng"
|
||||||
information: "Giới thiệu"
|
information: "Giới thiệu"
|
||||||
|
@ -1223,6 +1227,8 @@ _chat:
|
||||||
members: "Thành viên"
|
members: "Thành viên"
|
||||||
home: "Trang chính"
|
home: "Trang chính"
|
||||||
send: "Gửi"
|
send: "Gửi"
|
||||||
|
_settings:
|
||||||
|
preferencesBanner: "Bạn có thể cấu hình hành vi chung của máy khách theo sở thích của mình."
|
||||||
_accountSettings:
|
_accountSettings:
|
||||||
requireSigninToViewContents: "Yêu cầu đăng nhập để xem nội dung"
|
requireSigninToViewContents: "Yêu cầu đăng nhập để xem nội dung"
|
||||||
requireSigninToViewContentsDescription1: "Yêu cầu đăng nhập để xem tất cả ghi chú và nội dung khác mà bạn tạo. Điều này được kỳ vọng sẽ có hiệu quả trong việc ngăn chặn thông tin bị thu thập bởi các trình thu thập thông tin."
|
requireSigninToViewContentsDescription1: "Yêu cầu đăng nhập để xem tất cả ghi chú và nội dung khác mà bạn tạo. Điều này được kỳ vọng sẽ có hiệu quả trong việc ngăn chặn thông tin bị thu thập bởi các trình thu thập thông tin."
|
||||||
|
@ -2074,3 +2080,12 @@ _search:
|
||||||
searchScopeAll: "Tất cả"
|
searchScopeAll: "Tất cả"
|
||||||
searchScopeLocal: "Máy chủ này"
|
searchScopeLocal: "Máy chủ này"
|
||||||
searchScopeUser: "Người dùng chỉ định"
|
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: "提示和技巧"
|
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: "回车键发送"
|
||||||
|
@ -1628,7 +1631,7 @@ _serverSettings:
|
||||||
inquiryUrl: "联络地址"
|
inquiryUrl: "联络地址"
|
||||||
inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。"
|
inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。"
|
||||||
openRegistration: "开放注册"
|
openRegistration: "开放注册"
|
||||||
openRegistrationWarning: "开放注册有风险。建议仅当能够持续监控服务器并在出现问题时能够立即响应时才打开它。"
|
openRegistrationWarning: "开放注册有风险。建议仅当能够持续监控服务器,并在出现问题时能够立即响应时才打开它。"
|
||||||
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管理活动,为防止垃圾信息,此设定将自动关闭。"
|
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管理活动,为防止垃圾信息,此设定将自动关闭。"
|
||||||
deliverSuspendedSoftware: "停止投递的软件"
|
deliverSuspendedSoftware: "停止投递的软件"
|
||||||
deliverSuspendedSoftwareDescription: "可因安全漏洞之类的原因,停止向指定的服务器及服务器版本送信。版本信息由服务器提供,不保证可靠性。可使用 semver 范围来指定版本,但指定 >= 2024.3.1 将不包括如 2024.3.1-custom.0 等自定义版本,因此建议像 >= 2024.3.1-0 这样指定 prerelease 版本。"
|
deliverSuspendedSoftwareDescription: "可因安全漏洞之类的原因,停止向指定的服务器及服务器版本送信。版本信息由服务器提供,不保证可靠性。可使用 semver 范围来指定版本,但指定 >= 2024.3.1 将不包括如 2024.3.1-custom.0 等自定义版本,因此建议像 >= 2024.3.1-0 这样指定 prerelease 版本。"
|
||||||
|
@ -1641,7 +1644,7 @@ _serverSettings:
|
||||||
allowExternalApRedirect: "允许通过 ActivityPub 重定向查询"
|
allowExternalApRedirect: "允许通过 ActivityPub 重定向查询"
|
||||||
allowExternalApRedirect_description: "启用时,将允许其它服务器通过此服务器查询第三方内容,但有可能导致内容欺骗。"
|
allowExternalApRedirect_description: "启用时,将允许其它服务器通过此服务器查询第三方内容,但有可能导致内容欺骗。"
|
||||||
userGeneratedContentsVisibilityForVisitor: "用户生成内容对非用户的可见性"
|
userGeneratedContentsVisibilityForVisitor: "用户生成内容对非用户的可见性"
|
||||||
userGeneratedContentsVisibilityForVisitor_description: "这对于防止诸如难以审核的不适当的远程内容通过您自己的服务器无意中在互联网上公开等问题很有用。"
|
userGeneratedContentsVisibilityForVisitor_description: "对于防止难以审核的不适当的远程内容等,通过自己的服务器无意中在互联网上公开等问题很有用。"
|
||||||
userGeneratedContentsVisibilityForVisitor_description2: "包含服务器接收到的远程内容在内,无条件将服务器上的所有内容公开在互联网上存在风险。特别是对去中心化的特性不是很了解的访问者有可能将远程服务器上的内容误认为是在此服务器内生成的,需要特别留意。"
|
userGeneratedContentsVisibilityForVisitor_description2: "包含服务器接收到的远程内容在内,无条件将服务器上的所有内容公开在互联网上存在风险。特别是对去中心化的特性不是很了解的访问者有可能将远程服务器上的内容误认为是在此服务器内生成的,需要特别留意。"
|
||||||
_userGeneratedContentsVisibilityForVisitor:
|
_userGeneratedContentsVisibilityForVisitor:
|
||||||
all: "全部公开"
|
all: "全部公开"
|
||||||
|
@ -2140,7 +2143,7 @@ _wordMute:
|
||||||
muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
|
muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
|
||||||
muteWordsDescription2: "正则表达式用斜线包裹"
|
muteWordsDescription2: "正则表达式用斜线包裹"
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "隐藏服务器中的所有帖子和转帖,包括这些服务器上的用户回复。"
|
instanceMuteDescription: "隐藏服务器中所有的帖子和转帖,包括这些服务器上用户的回复。"
|
||||||
instanceMuteDescription2: "一行一个"
|
instanceMuteDescription2: "一行一个"
|
||||||
title: "下面实例中的帖子将被隐藏。"
|
title: "下面实例中的帖子将被隐藏。"
|
||||||
heading: "已隐藏的服务器"
|
heading: "已隐藏的服务器"
|
||||||
|
@ -2462,6 +2465,8 @@ _visibility:
|
||||||
disableFederation: "不参与联合"
|
disableFederation: "不参与联合"
|
||||||
disableFederationDescription: "不发送到其他服务器"
|
disableFederationDescription: "不发送到其他服务器"
|
||||||
_postForm:
|
_postForm:
|
||||||
|
quitInspiteOfThereAreUnuploadedFilesConfirm: "还有未上传的文件,要丢弃并关闭窗口吗?"
|
||||||
|
uploaderTip: "文件还未上传。可以在文件菜单中进行重命名、裁剪、添加水印、设置是否压缩等操作。文件将在发帖时自动上传。"
|
||||||
replyPlaceholder: "回复这个帖子..."
|
replyPlaceholder: "回复这个帖子..."
|
||||||
quotePlaceholder: "引用这个帖子..."
|
quotePlaceholder: "引用这个帖子..."
|
||||||
channelPlaceholder: "发布到频道…"
|
channelPlaceholder: "发布到频道…"
|
||||||
|
@ -2488,7 +2493,7 @@ _profile:
|
||||||
avatarDecorationMax: "最多可添加 {max} 个挂件"
|
avatarDecorationMax: "最多可添加 {max} 个挂件"
|
||||||
followedMessage: "被关注时显示的消息"
|
followedMessage: "被关注时显示的消息"
|
||||||
followedMessageDescription: "可以设置被关注时向对方显示的短消息。"
|
followedMessageDescription: "可以设置被关注时向对方显示的短消息。"
|
||||||
followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在请求被批准后显示。"
|
followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息会在请求被批准后显示。"
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "所有帖子"
|
allNotes: "所有帖子"
|
||||||
favoritedNotes: "收藏的帖子"
|
favoritedNotes: "收藏的帖子"
|
||||||
|
@ -3117,3 +3122,50 @@ _clip:
|
||||||
tip: "便签功能可以将帖子合并在一起。"
|
tip: "便签功能可以将帖子合并在一起。"
|
||||||
_userLists:
|
_userLists:
|
||||||
tip: "可创建包含任意用户的列表。已创建的列表可作为时间线查看。"
|
tip: "可创建包含任意用户的列表。已创建的列表可作为时间线查看。"
|
||||||
|
watermark: "水印"
|
||||||
|
defaultPreset: "默认预设"
|
||||||
|
_watermarkEditor:
|
||||||
|
tip: "可在图像内增加包含作者等信息的水印。"
|
||||||
|
quitWithoutSaveConfirm: "不保存就退出吗?"
|
||||||
|
driveFileTypeWarn: "不支持此文件"
|
||||||
|
driveFileTypeWarnDescription: "请选择图像文件"
|
||||||
|
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: "黑白"
|
||||||
|
colorAdjust: "色彩校正"
|
||||||
|
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 發送訊息"
|
||||||
|
@ -2462,6 +2465,8 @@ _visibility:
|
||||||
disableFederation: "停用聯邦"
|
disableFederation: "停用聯邦"
|
||||||
disableFederationDescription: "不發送到其他伺服器"
|
disableFederationDescription: "不發送到其他伺服器"
|
||||||
_postForm:
|
_postForm:
|
||||||
|
quitInspiteOfThereAreUnuploadedFilesConfirm: "尚有未上傳的檔案,確定要放棄並關閉表單嗎?"
|
||||||
|
uploaderTip: "檔案尚未上傳。您可以從檔案選單中設定重新命名、裁切圖片、加上浮水印、是否壓縮等選項。檔案會在發布貼文時自動上傳。\n"
|
||||||
replyPlaceholder: "回覆此貼文..."
|
replyPlaceholder: "回覆此貼文..."
|
||||||
quotePlaceholder: "引用此貼文..."
|
quotePlaceholder: "引用此貼文..."
|
||||||
channelPlaceholder: "發佈到頻道"
|
channelPlaceholder: "發佈到頻道"
|
||||||
|
@ -3117,3 +3122,50 @@ _clip:
|
||||||
tip: "摘錄是一項可以用來整理貼文的功能。"
|
tip: "摘錄是一項可以用來整理貼文的功能。"
|
||||||
_userLists:
|
_userLists:
|
||||||
tip: "您可以建立包含任意使用者的清單。建立後的清單可以作為時間軸顯示。\n"
|
tip: "您可以建立包含任意使用者的清單。建立後的清單可以作為時間軸顯示。\n"
|
||||||
|
watermark: "浮水印"
|
||||||
|
defaultPreset: "預設值"
|
||||||
|
_watermarkEditor:
|
||||||
|
tip: "可以在圖片中以浮水印加上出處等資訊。"
|
||||||
|
quitWithoutSaveConfirm: "不儲存就退出嗎?"
|
||||||
|
driveFileTypeWarn: "不支援此檔案"
|
||||||
|
driveFileTypeWarnDescription: "請選擇圖片檔案"
|
||||||
|
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: "黑白"
|
||||||
|
colorAdjust: "色彩校正"
|
||||||
|
colorClamp: "壓縮色彩"
|
||||||
|
colorClampAdvanced: "壓縮色彩(進階)"
|
||||||
|
distort: "變形"
|
||||||
|
threshold: "閾值"
|
||||||
|
zoomLines: "速度線"
|
||||||
|
stripe: "條紋"
|
||||||
|
polkadot: "波卡圓點"
|
||||||
|
checker: "棋盤格"
|
||||||
|
|
26
package.json
26
package.json
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2025.6.0",
|
"version": "2025.6.1",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/misskey-dev/misskey.git"
|
"url": "https://github.com/misskey-dev/misskey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.11.0",
|
"packageManager": "pnpm@10.12.1",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/frontend-shared",
|
"packages/frontend-shared",
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
|
@ -53,28 +53,28 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssnano": "7.0.7",
|
"cssnano": "7.0.7",
|
||||||
"esbuild": "0.25.4",
|
"esbuild": "0.25.5",
|
||||||
"execa": "9.5.3",
|
"execa": "9.6.0",
|
||||||
"fast-glob": "3.3.3",
|
"fast-glob": "3.3.3",
|
||||||
"glob": "11.0.2",
|
"glob": "11.0.2",
|
||||||
"ignore-walk": "7.0.0",
|
"ignore-walk": "7.0.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"postcss": "8.5.3",
|
"postcss": "8.5.4",
|
||||||
"tar": "7.4.3",
|
"tar": "7.4.3",
|
||||||
"terser": "5.39.2",
|
"terser": "5.42.0",
|
||||||
"typescript": "5.8.3"
|
"typescript": "5.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/eslint-plugin": "2.1.0",
|
"@misskey-dev/eslint-plugin": "2.1.0",
|
||||||
"@types/node": "22.15.21",
|
"@types/node": "22.15.31",
|
||||||
"@typescript-eslint/eslint-plugin": "8.32.1",
|
"@typescript-eslint/eslint-plugin": "8.34.0",
|
||||||
"@typescript-eslint/parser": "8.32.1",
|
"@typescript-eslint/parser": "8.34.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "14.4.0",
|
"cypress": "14.4.1",
|
||||||
"eslint": "9.27.0",
|
"eslint": "9.28.0",
|
||||||
"globals": "16.1.0",
|
"globals": "16.2.0",
|
||||||
"ncp": "2.0.0",
|
"ncp": "2.0.0",
|
||||||
"pnpm": "10.11.0",
|
"pnpm": "10.12.1",
|
||||||
"start-server-and-test": "2.0.12"
|
"start-server-and-test": "2.0.12"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|
|
@ -37,17 +37,17 @@
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-android-arm64": "1.3.11",
|
"@swc/core-android-arm64": "1.3.11",
|
||||||
"@swc/core-darwin-arm64": "1.11.29",
|
"@swc/core-darwin-arm64": "1.12.0",
|
||||||
"@swc/core-darwin-x64": "1.11.29",
|
"@swc/core-darwin-x64": "1.12.0",
|
||||||
"@swc/core-freebsd-x64": "1.3.11",
|
"@swc/core-freebsd-x64": "1.3.11",
|
||||||
"@swc/core-linux-arm-gnueabihf": "1.11.29",
|
"@swc/core-linux-arm-gnueabihf": "1.12.0",
|
||||||
"@swc/core-linux-arm64-gnu": "1.11.29",
|
"@swc/core-linux-arm64-gnu": "1.12.0",
|
||||||
"@swc/core-linux-arm64-musl": "1.11.29",
|
"@swc/core-linux-arm64-musl": "1.12.0",
|
||||||
"@swc/core-linux-x64-gnu": "1.11.29",
|
"@swc/core-linux-x64-gnu": "1.12.0",
|
||||||
"@swc/core-linux-x64-musl": "1.11.29",
|
"@swc/core-linux-x64-musl": "1.12.0",
|
||||||
"@swc/core-win32-arm64-msvc": "1.11.29",
|
"@swc/core-win32-arm64-msvc": "1.12.0",
|
||||||
"@swc/core-win32-ia32-msvc": "1.11.29",
|
"@swc/core-win32-ia32-msvc": "1.12.0",
|
||||||
"@swc/core-win32-x64-msvc": "1.11.29",
|
"@swc/core-win32-x64-msvc": "1.12.0",
|
||||||
"@tensorflow/tfjs": "4.22.0",
|
"@tensorflow/tfjs": "4.22.0",
|
||||||
"@tensorflow/tfjs-node": "4.22.0",
|
"@tensorflow/tfjs-node": "4.22.0",
|
||||||
"bufferutil": "4.0.9",
|
"bufferutil": "4.0.9",
|
||||||
|
@ -67,8 +67,8 @@
|
||||||
"utf-8-validate": "6.0.5"
|
"utf-8-validate": "6.0.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.817.0",
|
"@aws-sdk/client-s3": "3.826.0",
|
||||||
"@aws-sdk/lib-storage": "3.817.0",
|
"@aws-sdk/lib-storage": "3.826.0",
|
||||||
"@discordapp/twemoji": "15.1.0",
|
"@discordapp/twemoji": "15.1.0",
|
||||||
"@fastify/accepts": "5.0.2",
|
"@fastify/accepts": "5.0.2",
|
||||||
"@fastify/cookie": "11.0.2",
|
"@fastify/cookie": "11.0.2",
|
||||||
|
@ -80,10 +80,10 @@
|
||||||
"@fastify/view": "10.0.2",
|
"@fastify/view": "10.0.2",
|
||||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||||
"@misskey-dev/summaly": "5.2.1",
|
"@misskey-dev/summaly": "5.2.1",
|
||||||
"@napi-rs/canvas": "0.1.70",
|
"@napi-rs/canvas": "0.1.71",
|
||||||
"@nestjs/common": "11.1.2",
|
"@nestjs/common": "11.1.3",
|
||||||
"@nestjs/core": "11.1.2",
|
"@nestjs/core": "11.1.3",
|
||||||
"@nestjs/testing": "11.1.2",
|
"@nestjs/testing": "11.1.3",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sentry/node": "8.55.0",
|
"@sentry/node": "8.55.0",
|
||||||
"@sentry/profiling-node": "8.55.0",
|
"@sentry/profiling-node": "8.55.0",
|
||||||
|
@ -91,7 +91,7 @@
|
||||||
"@sinonjs/fake-timers": "11.3.1",
|
"@sinonjs/fake-timers": "11.3.1",
|
||||||
"@smithy/node-http-handler": "2.5.0",
|
"@smithy/node-http-handler": "2.5.0",
|
||||||
"@swc/cli": "0.7.7",
|
"@swc/cli": "0.7.7",
|
||||||
"@swc/core": "1.11.29",
|
"@swc/core": "1.12.0",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"@types/redis-info": "3.0.3",
|
"@types/redis-info": "3.0.3",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
|
@ -101,7 +101,7 @@
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"body-parser": "1.20.3",
|
"body-parser": "1.20.3",
|
||||||
"bullmq": "5.53.0",
|
"bullmq": "5.53.2",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.2",
|
"cbor": "9.0.2",
|
||||||
"chalk": "5.4.1",
|
"chalk": "5.4.1",
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "19.6.0",
|
"file-type": "19.6.0",
|
||||||
"fluent-ffmpeg": "2.1.3",
|
"fluent-ffmpeg": "2.1.3",
|
||||||
"form-data": "4.0.2",
|
"form-data": "4.0.3",
|
||||||
"got": "14.4.7",
|
"got": "14.4.7",
|
||||||
"happy-dom": "16.8.1",
|
"happy-dom": "16.8.1",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
|
@ -133,9 +133,9 @@
|
||||||
"jsonld": "8.3.3",
|
"jsonld": "8.3.3",
|
||||||
"jsrsasign": "11.1.0",
|
"jsrsasign": "11.1.0",
|
||||||
"juice": "11.0.1",
|
"juice": "11.0.1",
|
||||||
"meilisearch": "0.50.0",
|
"meilisearch": "0.51.0",
|
||||||
"mfm-js": "0.24.0",
|
"mfm-js": "0.24.0",
|
||||||
"microformats-parser": "2.0.2",
|
"microformats-parser": "2.0.3",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"misskey-reversi": "workspace:*",
|
"misskey-reversi": "workspace:*",
|
||||||
|
@ -188,16 +188,16 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.7.0",
|
"@jest/globals": "29.7.0",
|
||||||
"@nestjs/platform-express": "10.4.18",
|
"@nestjs/platform-express": "10.4.19",
|
||||||
"@sentry/vue": "9.22.0",
|
"@sentry/vue": "9.28.0",
|
||||||
"@simplewebauthn/types": "12.0.0",
|
"@simplewebauthn/types": "12.0.0",
|
||||||
"@swc/jest": "0.2.38",
|
"@swc/jest": "0.2.38",
|
||||||
"@types/accepts": "1.3.7",
|
"@types/accepts": "1.3.7",
|
||||||
"@types/archiver": "6.0.3",
|
"@types/archiver": "6.0.3",
|
||||||
"@types/bcryptjs": "2.4.6",
|
"@types/bcryptjs": "2.4.6",
|
||||||
"@types/body-parser": "1.19.5",
|
"@types/body-parser": "1.19.6",
|
||||||
"@types/color-convert": "2.0.4",
|
"@types/color-convert": "2.0.4",
|
||||||
"@types/content-disposition": "0.5.8",
|
"@types/content-disposition": "0.5.9",
|
||||||
"@types/fluent-ffmpeg": "2.1.27",
|
"@types/fluent-ffmpeg": "2.1.27",
|
||||||
"@types/htmlescape": "1.1.3",
|
"@types/htmlescape": "1.1.3",
|
||||||
"@types/http-link-header": "1.0.7",
|
"@types/http-link-header": "1.0.7",
|
||||||
|
@ -208,12 +208,12 @@
|
||||||
"@types/jsrsasign": "10.5.15",
|
"@types/jsrsasign": "10.5.15",
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/ms": "0.7.34",
|
"@types/ms": "0.7.34",
|
||||||
"@types/node": "22.15.21",
|
"@types/node": "22.15.31",
|
||||||
"@types/nodemailer": "6.4.17",
|
"@types/nodemailer": "6.4.17",
|
||||||
"@types/oauth": "0.9.6",
|
"@types/oauth": "0.9.6",
|
||||||
"@types/oauth2orize": "1.11.5",
|
"@types/oauth2orize": "1.11.5",
|
||||||
"@types/oauth2orize-pkce": "0.1.2",
|
"@types/oauth2orize-pkce": "0.1.2",
|
||||||
"@types/pg": "8.15.2",
|
"@types/pg": "8.15.4",
|
||||||
"@types/pug": "2.0.10",
|
"@types/pug": "2.0.10",
|
||||||
"@types/qrcode": "1.5.5",
|
"@types/qrcode": "1.5.5",
|
||||||
"@types/random-seed": "0.3.5",
|
"@types/random-seed": "0.3.5",
|
||||||
|
@ -229,8 +229,8 @@
|
||||||
"@types/vary": "1.1.3",
|
"@types/vary": "1.1.3",
|
||||||
"@types/web-push": "3.6.4",
|
"@types/web-push": "3.6.4",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "8.32.1",
|
"@typescript-eslint/eslint-plugin": "8.34.0",
|
||||||
"@typescript-eslint/parser": "8.32.1",
|
"@typescript-eslint/parser": "8.34.0",
|
||||||
"aws-sdk-client-mock": "4.1.0",
|
"aws-sdk-client-mock": "4.1.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
|
|
|
@ -331,6 +331,16 @@ export class ChatService {
|
||||||
await redisPipeline.exec();
|
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
|
@bindThis
|
||||||
public findMessageById(messageId: MiChatMessage['id']) {
|
public findMessageById(messageId: MiChatMessage['id']) {
|
||||||
return this.chatMessagesRepository.findOneBy({ id: messageId });
|
return this.chatMessagesRepository.findOneBy({ id: messageId });
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -482,9 +482,19 @@ export class ActivityPubServerService {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
dbFallback: async (untilId, sinceId, limit) => {
|
dbFallback: async (untilId, sinceId, limit) => {
|
||||||
return await this.getUserNotesFromDb(sinceId, untilId, limit, user.id);
|
return await this.getUserNotesFromDb({
|
||||||
|
untilId,
|
||||||
|
sinceId,
|
||||||
|
limit,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}) : await this.getUserNotesFromDb(sinceId ?? null, untilId ?? null, limit, user.id);
|
}) : await this.getUserNotesFromDb({
|
||||||
|
untilId: untilId ?? null,
|
||||||
|
sinceId: sinceId ?? null,
|
||||||
|
limit,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
if (sinceId) notes.reverse();
|
if (sinceId) notes.reverse();
|
||||||
|
|
||||||
|
@ -523,16 +533,21 @@ export class ActivityPubServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async getUserNotesFromDb(untilId: string | null, sinceId: string | null, limit: number, userId: MiUser['id']) {
|
private async getUserNotesFromDb(ps: {
|
||||||
return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
|
untilId: string | null,
|
||||||
.andWhere('note.userId = :userId', { userId })
|
sinceId: string | null,
|
||||||
|
limit: number,
|
||||||
|
userId: MiUser['id'],
|
||||||
|
}) {
|
||||||
|
return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||||
|
.andWhere('note.userId = :userId', { userId: ps.userId })
|
||||||
.andWhere(new Brackets(qb => {
|
.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
.where('note.visibility = \'public\'')
|
.where('note.visibility = \'public\'')
|
||||||
.orWhere('note.visibility = \'home\'');
|
.orWhere('note.visibility = \'home\'');
|
||||||
}))
|
}))
|
||||||
.andWhere('note.localOnly = FALSE')
|
.andWhere('note.localOnly = FALSE')
|
||||||
.limit(limit)
|
.limit(ps.limit)
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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/inbox' from './endpoints/chat/rooms/invitations/inbox.js';
|
||||||
export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.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/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';
|
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
|
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
|
||||||
|
|
|
@ -26,9 +26,9 @@
|
||||||
"mfm-js": "0.24.0",
|
"mfm-js": "0.24.0",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"punycode.js": "2.3.1",
|
"punycode.js": "2.3.1",
|
||||||
"rollup": "4.41.1",
|
"rollup": "4.42.0",
|
||||||
"sass": "1.89.0",
|
"sass": "1.89.2",
|
||||||
"shiki": "3.4.2",
|
"shiki": "3.6.0",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
|
@ -39,27 +39,27 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/summaly": "5.2.1",
|
"@misskey-dev/summaly": "5.2.1",
|
||||||
"@tabler/icons-webfont": "3.33.0",
|
"@tabler/icons-webfont": "3.34.0",
|
||||||
"@testing-library/vue": "8.1.0",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/estree": "1.0.7",
|
"@types/estree": "1.0.8",
|
||||||
"@types/micromatch": "4.0.9",
|
"@types/micromatch": "4.0.9",
|
||||||
"@types/node": "22.15.28",
|
"@types/node": "22.15.31",
|
||||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "8.33.0",
|
"@typescript-eslint/eslint-plugin": "8.34.0",
|
||||||
"@typescript-eslint/parser": "8.33.0",
|
"@typescript-eslint/parser": "8.34.0",
|
||||||
"@vitest/coverage-v8": "3.1.4",
|
"@vitest/coverage-v8": "3.2.3",
|
||||||
"@vue/runtime-core": "3.5.16",
|
"@vue/runtime-core": "3.5.16",
|
||||||
"acorn": "8.14.1",
|
"acorn": "8.15.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-vue": "10.1.0",
|
"eslint-plugin-vue": "10.2.0",
|
||||||
"fast-glob": "3.3.3",
|
"fast-glob": "3.3.3",
|
||||||
"happy-dom": "17.5.6",
|
"happy-dom": "17.6.3",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"msw": "2.8.6",
|
"msw": "2.10.2",
|
||||||
"nodemon": "3.1.10",
|
"nodemon": "3.1.10",
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"start-server-and-test": "2.0.12",
|
"start-server-and-test": "2.0.12",
|
||||||
|
|
|
@ -48,6 +48,10 @@ export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string {
|
||||||
?? char;
|
?? char;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSupportedEmoji(char: string): boolean {
|
||||||
|
return unicodeEmojisMap.has(colorizeEmoji(char)) || unicodeEmojisMap.has(char);
|
||||||
|
}
|
||||||
|
|
||||||
export function getEmojiName(char: string): string {
|
export function getEmojiName(char: string): string {
|
||||||
// Colorize it because emojilist.json assumes that
|
// Colorize it because emojilist.json assumes that
|
||||||
const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char);
|
const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char);
|
||||||
|
|
|
@ -21,11 +21,11 @@
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "22.15.28",
|
"@types/node": "22.15.31",
|
||||||
"@typescript-eslint/eslint-plugin": "8.33.0",
|
"@typescript-eslint/eslint-plugin": "8.34.0",
|
||||||
"@typescript-eslint/parser": "8.33.0",
|
"@typescript-eslint/parser": "8.34.0",
|
||||||
"esbuild": "0.25.5",
|
"esbuild": "0.25.5",
|
||||||
"eslint-plugin-vue": "10.1.0",
|
"eslint-plugin-vue": "10.2.0",
|
||||||
"nodemon": "3.1.10",
|
"nodemon": "3.1.10",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"vue-eslint-parser": "10.1.3"
|
"vue-eslint-parser": "10.1.3"
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 299 KiB |
Binary file not shown.
After Width: | Height: | Size: 410 KiB |
|
@ -24,7 +24,7 @@
|
||||||
"@rollup/plugin-json": "6.1.0",
|
"@rollup/plugin-json": "6.1.0",
|
||||||
"@rollup/plugin-replace": "6.0.2",
|
"@rollup/plugin-replace": "6.0.2",
|
||||||
"@rollup/pluginutils": "5.1.4",
|
"@rollup/pluginutils": "5.1.4",
|
||||||
"@sentry/vue": "9.24.0",
|
"@sentry/vue": "9.27.0",
|
||||||
"@syuilo/aiscript": "0.19.0",
|
"@syuilo/aiscript": "0.19.0",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"@vitejs/plugin-vue": "5.2.4",
|
"@vitejs/plugin-vue": "5.2.4",
|
||||||
|
@ -60,10 +60,10 @@
|
||||||
"misskey-reversi": "workspace:*",
|
"misskey-reversi": "workspace:*",
|
||||||
"photoswipe": "5.4.4",
|
"photoswipe": "5.4.4",
|
||||||
"punycode.js": "2.3.1",
|
"punycode.js": "2.3.1",
|
||||||
"rollup": "4.41.1",
|
"rollup": "4.42.0",
|
||||||
"sanitize-html": "2.17.0",
|
"sanitize-html": "2.17.0",
|
||||||
"sass": "1.89.0",
|
"sass": "1.89.2",
|
||||||
"shiki": "3.4.2",
|
"shiki": "3.6.0",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.177.0",
|
"three": "0.177.0",
|
||||||
|
@ -72,7 +72,6 @@
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"uuid": "11.1.0",
|
|
||||||
"v-code-diff": "1.13.1",
|
"v-code-diff": "1.13.1",
|
||||||
"vite": "6.3.5",
|
"vite": "6.3.5",
|
||||||
"vue": "3.5.16",
|
"vue": "3.5.16",
|
||||||
|
@ -99,36 +98,36 @@
|
||||||
"@storybook/types": "8.6.14",
|
"@storybook/types": "8.6.14",
|
||||||
"@storybook/vue3": "8.6.14",
|
"@storybook/vue3": "8.6.14",
|
||||||
"@storybook/vue3-vite": "8.6.14",
|
"@storybook/vue3-vite": "8.6.14",
|
||||||
"@tabler/icons-webfont": "3.33.0",
|
"@tabler/icons-webfont": "3.34.0",
|
||||||
"@testing-library/vue": "8.1.0",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/canvas-confetti": "1.9.0",
|
"@types/canvas-confetti": "1.9.0",
|
||||||
"@types/estree": "1.0.7",
|
"@types/estree": "1.0.8",
|
||||||
"@types/matter-js": "0.19.8",
|
"@types/matter-js": "0.19.8",
|
||||||
"@types/micromatch": "4.0.9",
|
"@types/micromatch": "4.0.9",
|
||||||
"@types/node": "22.15.28",
|
"@types/node": "22.15.31",
|
||||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||||
"@types/sanitize-html": "2.16.0",
|
"@types/sanitize-html": "2.16.0",
|
||||||
"@types/seedrandom": "3.0.8",
|
"@types/seedrandom": "3.0.8",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
"@types/throttle-debounce": "5.0.2",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "8.33.0",
|
"@typescript-eslint/eslint-plugin": "8.34.0",
|
||||||
"@typescript-eslint/parser": "8.33.0",
|
"@typescript-eslint/parser": "8.34.0",
|
||||||
"@vitest/coverage-v8": "3.1.4",
|
"@vitest/coverage-v8": "3.2.3",
|
||||||
"@vue/compiler-core": "3.5.16",
|
"@vue/compiler-core": "3.5.16",
|
||||||
"@vue/runtime-core": "3.5.16",
|
"@vue/runtime-core": "3.5.16",
|
||||||
"acorn": "8.14.1",
|
"acorn": "8.15.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "14.4.0",
|
"cypress": "14.4.1",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-vue": "10.1.0",
|
"eslint-plugin-vue": "10.2.0",
|
||||||
"fast-glob": "3.3.3",
|
"fast-glob": "3.3.3",
|
||||||
"happy-dom": "17.5.6",
|
"happy-dom": "17.6.3",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"minimatch": "10.0.1",
|
"minimatch": "10.0.1",
|
||||||
"msw": "2.8.6",
|
"msw": "2.10.2",
|
||||||
"msw-storybook-addon": "2.0.4",
|
"msw-storybook-addon": "2.0.5",
|
||||||
"nodemon": "3.1.10",
|
"nodemon": "3.1.10",
|
||||||
"prettier": "3.5.3",
|
"prettier": "3.5.3",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
@ -138,7 +137,7 @@
|
||||||
"storybook": "8.6.14",
|
"storybook": "8.6.14",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vitest": "3.1.4",
|
"vitest": "3.2.3",
|
||||||
"vitest-fetch-mock": "0.4.5",
|
"vitest-fetch-mock": "0.4.5",
|
||||||
"vue-component-type-helpers": "2.2.10",
|
"vue-component-type-helpers": "2.2.10",
|
||||||
"vue-eslint-parser": "10.1.3",
|
"vue-eslint-parser": "10.1.3",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { utils, values } from '@syuilo/aiscript';
|
import { utils, values } from '@syuilo/aiscript';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { genId } from '@/utility/id.js';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
@ -543,7 +543,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
|
||||||
call: C,
|
call: C,
|
||||||
) {
|
) {
|
||||||
if (id) utils.assertString(id);
|
if (id) utils.assertString(id);
|
||||||
const _id = id?.value ?? uuid();
|
const _id = id?.value ?? genId();
|
||||||
const component = ref({
|
const component = ref({
|
||||||
...getOptions(def, call),
|
...getOptions(def, call),
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -185,7 +185,7 @@ const isRootSelected = ref(false);
|
||||||
|
|
||||||
watch(selectedFiles, () => {
|
watch(selectedFiles, () => {
|
||||||
emit('changeSelectedFiles', selectedFiles.value);
|
emit('changeSelectedFiles', selectedFiles.value);
|
||||||
});
|
}, { deep: true });
|
||||||
|
|
||||||
watch([selectedFolders, isRootSelected], () => {
|
watch([selectedFolders, isRootSelected], () => {
|
||||||
emit('changeSelectedFolders', isRootSelected.value ? [null, ...selectedFolders.value] : selectedFolders.value);
|
emit('changeSelectedFolders', isRootSelected.value ? [null, ...selectedFolders.value] : selectedFolders.value);
|
||||||
|
|
|
@ -64,6 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkCustomEmoji v-if="!emoji.hasOwnProperty('char')" class="emoji" :name="getKey(emoji)" :normal="true"/>
|
<MkCustomEmoji v-if="!emoji.hasOwnProperty('char')" class="emoji" :name="getKey(emoji)" :normal="true"/>
|
||||||
<MkEmoji v-else class="emoji" :emoji="getKey(emoji)" :normal="true"/>
|
<MkEmoji v-else class="emoji" :emoji="getKey(emoji)" :normal="true"/>
|
||||||
</button>
|
</button>
|
||||||
|
<button v-tooltip="i18n.ts.settings" class="_button config" @click="settings"><i class="ti ti-settings"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -139,6 +140,9 @@ import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-e
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
|
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
import { useRouter } from '@/router.js';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
|
@ -489,6 +493,11 @@ function done(query?: string): boolean | void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function settings() {
|
||||||
|
emit('esc');
|
||||||
|
router.push('settings/emoji-palette');
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
|
@ -720,6 +729,15 @@ defineExpose({
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: $pad;
|
padding: $pad;
|
||||||
|
|
||||||
|
> .config {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 3px;
|
||||||
|
width: var(--eachSize);
|
||||||
|
height: var(--eachSize);
|
||||||
|
contain: strict;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
> .item {
|
> .item {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 3px;
|
padding: 0 3px;
|
||||||
|
|
|
@ -5,33 +5,35 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
<MkPagination v-slot="{ items }" :pagination="pagination">
|
||||||
<MkA
|
<div :class="[$style.fileList, { [$style.grid]: viewMode === 'grid', [$style.list]: viewMode === 'list', '_gaps_s': viewMode === 'list' }]">
|
||||||
v-for="file in (items as Misskey.entities.DriveFile[])"
|
<MkA
|
||||||
:key="file.id"
|
v-for="file in items"
|
||||||
v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${dateString(file.createdAt)}\nby ${file.user ? '@' + Misskey.acct.toString(file.user) : 'system'}`"
|
:key="file.id"
|
||||||
:to="`/admin/file/${file.id}`"
|
v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${dateString(file.createdAt)}\nby ${file.user ? '@' + Misskey.acct.toString(file.user) : 'system'}`"
|
||||||
class="file _button"
|
:to="`/admin/file/${file.id}`"
|
||||||
>
|
:class="[$style.file, '_button']"
|
||||||
<div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div>
|
>
|
||||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain" :highlightWhenSensitive="true"/>
|
<div v-if="file.isSensitive" :class="$style.sensitiveLabel">{{ i18n.ts.sensitive }}</div>
|
||||||
<div v-if="viewMode === 'list'" class="body">
|
<MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain" :highlightWhenSensitive="true"/>
|
||||||
<div>
|
<div v-if="viewMode === 'list'" :class="$style.body">
|
||||||
<small style="opacity: 0.7;">{{ file.name }}</small>
|
<div>
|
||||||
|
<small style="opacity: 0.7;">{{ file.name }}</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<MkAcct v-if="file.user" :user="file.user"/>
|
||||||
|
<div v-else>{{ i18n.ts.system }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style="margin-right: 1em;">{{ file.type }}</span>
|
||||||
|
<span>{{ bytes(file.size) }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{{ i18n.ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</MkA>
|
||||||
<MkAcct v-if="file.user" :user="file.user"/>
|
</div>
|
||||||
<div v-else>{{ i18n.ts.system }}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span style="margin-right: 1em;">{{ file.type }}</span>
|
|
||||||
<span>{{ bytes(file.size) }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>{{ i18n.ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</MkA>
|
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -43,76 +45,76 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||||
import bytes from '@/filters/bytes.js';
|
import bytes from '@/filters/bytes.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { dateString } from '@/filters/date.js';
|
import { dateString } from '@/filters/date.js';
|
||||||
|
import type { PagingCtx } from '@/composables/use-pagination.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
pagination: any;
|
pagination: PagingCtx<'admin/drive/files'>;
|
||||||
viewMode: 'grid' | 'list';
|
viewMode: 'grid' | 'list';
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
@keyframes sensitive-blink {
|
@keyframes sensitive-blink {
|
||||||
0% { opacity: 1; }
|
0% { opacity: 1; }
|
||||||
50% { opacity: 0; }
|
50% { opacity: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.urempief {
|
.list {
|
||||||
&.list {
|
> .file {
|
||||||
> .file {
|
display: flex;
|
||||||
display: flex;
|
width: 100%;
|
||||||
width: 100%;
|
height: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--MI_THEME-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .thumbnail {
|
|
||||||
width: 128px;
|
|
||||||
height: 128px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .body {
|
|
||||||
margin-left: 0.3em;
|
|
||||||
padding: 8px;
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.grid {
|
> .file:hover {
|
||||||
display: grid;
|
color: var(--MI_THEME-accent);
|
||||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
}
|
||||||
grid-gap: 12px;
|
|
||||||
|
|
||||||
> .file {
|
> .file > .thumbnail {
|
||||||
position: relative;
|
width: 128px;
|
||||||
aspect-ratio: 1;
|
height: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
> .thumbnail {
|
> .file > .body {
|
||||||
width: 100%;
|
margin-left: 0.3em;
|
||||||
height: 100%;
|
padding: 8px;
|
||||||
}
|
flex: 1;
|
||||||
|
|
||||||
> .sensitive-label {
|
@media (max-width: 500px) {
|
||||||
position: absolute;
|
font-size: 14px;
|
||||||
z-index: 10;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
background: #ff0000bf;
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 85%;
|
|
||||||
animation: sensitive-blink 1s infinite;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||||
|
grid-gap: 12px;
|
||||||
|
|
||||||
|
> .file {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensitiveLabel {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
background: #ff0000bf;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 85%;
|
||||||
|
animation: sensitive-blink 1s infinite;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkFolder :defaultOpen="true" :canPage="false">
|
||||||
|
<template #label>{{ fx.name }}</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="_buttons">
|
||||||
|
<MkButton iconOnly @click="emit('del')"><i class="ti ti-trash"></i></MkButton>
|
||||||
|
<MkButton iconOnly @click="emit('swapUp')"><i class="ti ti-arrow-up"></i></MkButton>
|
||||||
|
<MkButton iconOnly @click="emit('swapDown')"><i class="ti ti-arrow-down"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<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]"
|
||||||
|
>
|
||||||
|
<template #label>{{ fx.params[k].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"
|
||||||
|
:textConverter="fx.params[k].toViewValue"
|
||||||
|
@thumbDoubleClicked="() => {
|
||||||
|
if (fx.params[k].default != null) {
|
||||||
|
layer.params[k] = fx.params[k].default;
|
||||||
|
} else {
|
||||||
|
layer.params[k] = v.min;
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #label>{{ fx.params[k].label ?? k }}</template>
|
||||||
|
</MkRange>
|
||||||
|
<MkRadios
|
||||||
|
v-else-if="v.type === 'number:enum'"
|
||||||
|
v-model="layer.params[k]"
|
||||||
|
>
|
||||||
|
<template #label>{{ fx.params[k].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"
|
||||||
|
>
|
||||||
|
<template #label>{{ fx.params[k].label ?? k }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
<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>{{ fx.params[k].label ?? k }}</template>
|
||||||
|
</MkInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
|
||||||
|
import { i18n } from '@/i18n.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 { FXS } from '@/utility/image-effector/fxs.js';
|
||||||
|
|
||||||
|
const layer = defineModel<ImageEffectorLayer>('layer', { required: true });
|
||||||
|
const fx = FXS.find((fx) => fx.id === layer.value.fxId);
|
||||||
|
if (fx == null) {
|
||||||
|
throw new Error(`Unrecognized effect: ${layer.value.fxId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'del'): void;
|
||||||
|
(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>
|
||||||
|
.root {
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,303 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkModalWindow
|
||||||
|
ref="dialog"
|
||||||
|
:width="1000"
|
||||||
|
:height="600"
|
||||||
|
:scroll="false"
|
||||||
|
:withOkButton="true"
|
||||||
|
@close="cancel()"
|
||||||
|
@ok="save()"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<template #header><i class="ti ti-sparkles"></i> {{ i18n.ts._imageEffector.title }}</template>
|
||||||
|
|
||||||
|
<div :class="$style.root">
|
||||||
|
<div :class="$style.container">
|
||||||
|
<div :class="$style.preview">
|
||||||
|
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
|
||||||
|
<div :class="$style.previewContainer">
|
||||||
|
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
|
||||||
|
<div class="_acrylic" :class="$style.previewControls">
|
||||||
|
<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
|
||||||
|
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.controls">
|
||||||
|
<div class="_spacer _gaps">
|
||||||
|
<XLayer
|
||||||
|
v-for="(layer, i) in layers"
|
||||||
|
:key="layer.id"
|
||||||
|
v-model:layer="layers[i]"
|
||||||
|
@del="onLayerDelete(layer)"
|
||||||
|
@swapUp="onLayerSwapUp(layer)"
|
||||||
|
@swapDown="onLayerSwapDown(layer)"
|
||||||
|
></XLayer>
|
||||||
|
|
||||||
|
<MkButton rounded primary style="margin: 0 auto;" @click="addEffect"><i class="ti ti-plus"></i> {{ i18n.ts._imageEffector.addEffect }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkModalWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick } 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 MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
import XLayer from '@/components/MkImageEffectorDialog.Layer.vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { deepClone } from '@/utility/clone.js';
|
||||||
|
import { FXS } from '@/utility/image-effector/fxs.js';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
image: File;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'ok', image: File): void;
|
||||||
|
(ev: 'cancel'): void;
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('cancel');
|
||||||
|
dialog.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const layers = reactive<ImageEffectorLayer[]>([]);
|
||||||
|
|
||||||
|
watch(layers, async () => {
|
||||||
|
if (renderer != null) {
|
||||||
|
renderer.setLayers(layers);
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
function addEffect(ev: MouseEvent) {
|
||||||
|
os.popupMenu(FXS.map((fx) => ({
|
||||||
|
text: fx.name,
|
||||||
|
action: () => {
|
||||||
|
layers.push({
|
||||||
|
id: genId(),
|
||||||
|
fxId: fx.id,
|
||||||
|
params: Object.fromEntries(Object.entries(fx.params).map(([k, v]) => [k, v.default])),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})), ev.currentTarget ?? ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLayerSwapUp(layer: ImageEffectorLayer) {
|
||||||
|
const index = layers.indexOf(layer);
|
||||||
|
if (index > 0) {
|
||||||
|
layers.splice(index, 1);
|
||||||
|
layers.splice(index - 1, 0, layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLayerSwapDown(layer: ImageEffectorLayer) {
|
||||||
|
const index = layers.indexOf(layer);
|
||||||
|
if (index < layers.length - 1) {
|
||||||
|
layers.splice(index, 1);
|
||||||
|
layers.splice(index + 1, 0, layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLayerDelete(layer: ImageEffectorLayer) {
|
||||||
|
const index = layers.indexOf(layer);
|
||||||
|
if (index !== -1) {
|
||||||
|
layers.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvasEl = useTemplateRef('canvasEl');
|
||||||
|
|
||||||
|
let renderer: ImageEffector<typeof FXS> | null = null;
|
||||||
|
let imageBitmap: ImageBitmap | null = null;
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (canvasEl.value == null) return;
|
||||||
|
|
||||||
|
const closeWaiting = os.waiting();
|
||||||
|
|
||||||
|
await nextTick(); // waitingがレンダリングされるまで待つ
|
||||||
|
|
||||||
|
imageBitmap = await window.createImageBitmap(props.image);
|
||||||
|
|
||||||
|
const MAX_W = 1000;
|
||||||
|
const MAX_H = 1000;
|
||||||
|
let w = imageBitmap.width;
|
||||||
|
let h = imageBitmap.height;
|
||||||
|
|
||||||
|
if (w > MAX_W || h > MAX_H) {
|
||||||
|
const scale = Math.min(MAX_W / w, MAX_H / h);
|
||||||
|
w *= scale;
|
||||||
|
h *= scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer = new ImageEffector({
|
||||||
|
canvas: canvasEl.value,
|
||||||
|
renderWidth: w,
|
||||||
|
renderHeight: h,
|
||||||
|
image: imageBitmap,
|
||||||
|
fxs: FXS,
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderer.setLayers(layers);
|
||||||
|
|
||||||
|
renderer.render();
|
||||||
|
|
||||||
|
closeWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (renderer != null) {
|
||||||
|
renderer.destroy();
|
||||||
|
renderer = null;
|
||||||
|
}
|
||||||
|
if (imageBitmap != null) {
|
||||||
|
imageBitmap.close();
|
||||||
|
imageBitmap = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (layers.length === 0 || renderer == null || imageBitmap == null || canvasEl.value == null) {
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeWaiting = os.waiting();
|
||||||
|
|
||||||
|
await nextTick(); // waitingがレンダリングされるまで待つ
|
||||||
|
|
||||||
|
renderer.changeResolution(imageBitmap.width, imageBitmap.height); // 本番レンダリングのためオリジナル画質に戻す
|
||||||
|
renderer.render(); // toBlobの直前にレンダリングしないと何故か壊れる
|
||||||
|
canvasEl.value.toBlob((blob) => {
|
||||||
|
emit('ok', new File([blob!], `image-${Date.now()}.png`, { type: 'image/png' }));
|
||||||
|
dialog.value?.close();
|
||||||
|
closeWaiting();
|
||||||
|
}, 'image/png');
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabled = ref(true);
|
||||||
|
watch(enabled, () => {
|
||||||
|
if (renderer != null) {
|
||||||
|
if (enabled.value) {
|
||||||
|
renderer.setLayers(layers);
|
||||||
|
} else {
|
||||||
|
renderer.setLayers([]);
|
||||||
|
}
|
||||||
|
renderer.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.root {
|
||||||
|
container-type: inline-size;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--MI_THEME-bg);
|
||||||
|
background-size: auto auto;
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewTitle {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewControls {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewControlsButton {
|
||||||
|
&.active {
|
||||||
|
color: var(--MI_THEME-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewSpinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewCanvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 800px) {
|
||||||
|
.container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -82,7 +82,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, nextTick, onMounted, onUnmounted, useTemplateRef, watch, ref } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, useTemplateRef, watch, ref } from 'vue';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { genId } from '@/utility/id.js';
|
||||||
import { render } from 'buraha';
|
import { render } from 'buraha';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ const props = withDefaults(defineProps<{
|
||||||
onlyAvgColor: false,
|
onlyAvgColor: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const viewId = uuid();
|
const viewId = genId();
|
||||||
const canvas = useTemplateRef('canvas');
|
const canvas = useTemplateRef('canvas');
|
||||||
const root = useTemplateRef('root');
|
const root = useTemplateRef('root');
|
||||||
const img = useTemplateRef('img');
|
const img = useTemplateRef('img');
|
||||||
|
|
|
@ -52,6 +52,7 @@ import type { SuggestionType } from '@/utility/autocomplete.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { Autocomplete } from '@/utility/autocomplete.js';
|
import { Autocomplete } from '@/utility/autocomplete.js';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string | number | null;
|
modelValue: string | number | null;
|
||||||
|
@ -87,7 +88,7 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const { modelValue, type, autofocus } = toRefs(props);
|
const { modelValue, type, autofocus } = toRefs(props);
|
||||||
const v = ref(modelValue.value);
|
const v = ref(modelValue.value);
|
||||||
const id = Math.random().toString(); // TODO: uuid?
|
const id = genId();
|
||||||
const focused = ref(false);
|
const focused = ref(false);
|
||||||
const changed = ref(false);
|
const changed = ref(false);
|
||||||
const invalid = ref(false);
|
const invalid = ref(false);
|
||||||
|
|
|
@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { watch, ref } from 'vue';
|
import { watch, ref } from 'vue';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { genId } from '@/utility/id.js';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { useInterval } from '@@/js/use-interval.js';
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
const viewBoxX = 50;
|
const viewBoxX = 50;
|
||||||
const viewBoxY = 50;
|
const viewBoxY = 50;
|
||||||
const gradientId = uuid();
|
const gradientId = genId();
|
||||||
const polylinePoints = ref('');
|
const polylinePoints = ref('');
|
||||||
const polygonPoints = ref('');
|
const polygonPoints = ref('');
|
||||||
const headX = ref<number | null>(null);
|
const headX = ref<number | null>(null);
|
||||||
|
|
|
@ -4,15 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')">
|
<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" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }">
|
<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">
|
<div :class="$style.header">
|
||||||
<button v-if="withOkButton && withCloseButton" :class="$style.headerButton" class="_button" @click="emit('close')"><i class="ti ti-x"></i></button>
|
<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">
|
<span :class="$style.title">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
</span>
|
</span>
|
||||||
<button v-if="!withOkButton && withCloseButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="emit('close')"><i class="ti ti-x"></i></button>
|
<div v-if="withOkButton" style="padding: 0 16px; place-content: center;">
|
||||||
<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="emit('ok')"><i class="ti ti-check"></i></button>
|
<MkButton primary gradate small rounded :disabled="okButtonDisabled" @click="emit('ok')">{{ i18n.ts.done }} <i class="ti ti-check"></i></MkButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
@ -26,7 +27,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, useTemplateRef, ref } from 'vue';
|
import { onMounted, onUnmounted, useTemplateRef, ref } from 'vue';
|
||||||
import MkModal from './MkModal.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<{
|
const props = withDefaults(defineProps<{
|
||||||
withOkButton?: boolean;
|
withOkButton?: boolean;
|
||||||
|
@ -82,6 +86,19 @@ defineExpose({
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
--root-margin: 16px;
|
--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 {
|
.header {
|
||||||
|
|
|
@ -265,21 +265,21 @@ const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', nul
|
||||||
|
|
||||||
let note = deepClone(props.note);
|
let note = deepClone(props.note);
|
||||||
|
|
||||||
// plugin
|
// コンポーネント初期化に非同期的な処理を行うとTransitionのレンダリングがバグるため同期的に実行できるメソッドが実装されるのを待つ必要がある
|
||||||
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
// https://github.com/aiscript-dev/aiscript/issues/937
|
||||||
if (noteViewInterruptors.length > 0) {
|
//// plugin
|
||||||
onMounted(async () => {
|
//const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
||||||
let result: Misskey.entities.Note | null = deepClone(note);
|
//if (noteViewInterruptors.length > 0) {
|
||||||
for (const interruptor of noteViewInterruptors) {
|
// let result: Misskey.entities.Note | null = deepClone(note);
|
||||||
try {
|
// for (const interruptor of noteViewInterruptors) {
|
||||||
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
// try {
|
||||||
} catch (err) {
|
// result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
console.error(err);
|
// } catch (err) {
|
||||||
}
|
// console.error(err);
|
||||||
}
|
// }
|
||||||
note = result as Misskey.entities.Note;
|
// }
|
||||||
});
|
// note = result as Misskey.entities.Note;
|
||||||
}
|
//}
|
||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
const appearNote = getAppearNote(note);
|
const appearNote = getAppearNote(note);
|
||||||
|
@ -321,20 +321,27 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||||
url: `https://${host}/notes/${appearNote.id}`,
|
url: `https://${host}/notes/${appearNote.id}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
/* eslint-disable no-redeclare */
|
||||||
|
/** checkOnlyでは純粋なワードミュート結果をbooleanで返却する */
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): Array<string | string[]> | false | 'sensitiveMute';
|
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly?: false): Array<string | string[]> | false | 'sensitiveMute';
|
||||||
*/
|
|
||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
|
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | boolean | 'sensitiveMute' {
|
||||||
if (mutedWords != null) {
|
if (mutedWords != null) {
|
||||||
const result = checkWordMute(noteToCheck, $i, mutedWords);
|
const result = checkWordMute(noteToCheck, $i, mutedWords);
|
||||||
if (Array.isArray(result)) return result;
|
if (Array.isArray(result)) {
|
||||||
|
return checkOnly ? (result.length > 0) : result;
|
||||||
|
}
|
||||||
|
|
||||||
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
|
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
|
||||||
if (Array.isArray(replyResult)) return replyResult;
|
if (Array.isArray(replyResult)) {
|
||||||
|
return checkOnly ? (replyResult.length > 0) : replyResult;
|
||||||
|
}
|
||||||
|
|
||||||
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
|
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
|
||||||
if (Array.isArray(renoteResult)) return renoteResult;
|
if (Array.isArray(renoteResult)) {
|
||||||
|
return checkOnly ? (renoteResult.length > 0) : renoteResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkOnly) return false;
|
if (checkOnly) return false;
|
||||||
|
@ -345,6 +352,7 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
/* eslint-enable no-redeclare */
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => {
|
'r': () => {
|
||||||
|
@ -417,7 +425,7 @@ if (!props.mock) {
|
||||||
|
|
||||||
const users = renotes.map(x => x.user);
|
const users = renotes.map(x => x.user);
|
||||||
|
|
||||||
if (users.length < 1) return;
|
if (users.length < 1 || renoteButton.value == null) return;
|
||||||
|
|
||||||
const { dispose } = os.popup(MkUsersTooltip, {
|
const { dispose } = os.popup(MkUsersTooltip, {
|
||||||
showing,
|
showing,
|
||||||
|
|
|
@ -286,21 +286,20 @@ const inChannel = inject('inChannel', null);
|
||||||
|
|
||||||
let note = deepClone(props.note);
|
let note = deepClone(props.note);
|
||||||
|
|
||||||
// plugin
|
// コンポーネント初期化に非同期的な処理を行うとTransitionのレンダリングがバグるため同期的に実行できるメソッドが実装されるのを待つ必要がある
|
||||||
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
//// plugin
|
||||||
if (noteViewInterruptors.length > 0) {
|
//const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
||||||
onMounted(async () => {
|
//if (noteViewInterruptors.length > 0) {
|
||||||
let result: Misskey.entities.Note | null = deepClone(note);
|
// let result: Misskey.entities.Note | null = deepClone(note);
|
||||||
for (const interruptor of noteViewInterruptors) {
|
// for (const interruptor of noteViewInterruptors) {
|
||||||
try {
|
// try {
|
||||||
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
// result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
console.error(err);
|
// console.error(err);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
note = result as Misskey.entities.Note;
|
// note = result as Misskey.entities.Note;
|
||||||
});
|
//}
|
||||||
}
|
|
||||||
|
|
||||||
const isRenote = Misskey.note.isPureRenote(note);
|
const isRenote = Misskey.note.isPureRenote(note);
|
||||||
const appearNote = getAppearNote(note);
|
const appearNote = getAppearNote(note);
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="[$style.root]">
|
||||||
|
<div :class="$style.items">
|
||||||
|
<button class="_button" :class="[$style.item, x === 'left' && y === 'top' ? $style.active : null]" @click="() => { x = 'left'; y = 'top'; }"><i class="ti ti-align-box-left-top"></i></button>
|
||||||
|
<button class="_button" :class="[$style.item, x === 'center' && y === 'top' ? $style.active : null]" @click="() => { x = 'center'; y = 'top'; }"><i class="ti ti-align-box-center-top"></i></button>
|
||||||
|
<button class="_button" :class="[$style.item, x === 'right' && y === 'top' ? $style.active : null]" @click="() => { x = 'right'; y = 'top'; }"><i class="ti ti-align-box-right-top"></i></button>
|
||||||
|
<button class="_button" :class="[$style.item, x === 'left' && y === 'center' ? $style.active : null]" @click="() => { x = 'left'; y = 'center'; }"><i class="ti ti-align-box-left-middle"></i></button>
|
||||||
|
<button class="_button" :class="[$style.item, x === 'center' && y === 'center' ? $style.active : null]" @click="() => { x = 'center'; y = 'center'; }"><i class="ti ti-align-box-center-middle"></i></button>
|
||||||
|
<button class="_button" :class="[$style.item, x === 'right' && y === 'center' ? $style.active : null]" @click="() => { x = 'right'; y = 'center'; }"><i class="ti ti-align-box-right-middle"></i></button>
|
||||||
|
<button class="_button" :class="[$style.item, x === 'left' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'left'; y = 'bottom'; }"><i class="ti ti-align-box-left-bottom"></i></button>
|
||||||
|
<button class="_button" :class="[$style.item, x === 'center' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'center'; y = 'bottom'; }"><i class="ti ti-align-box-center-bottom"></i></button>
|
||||||
|
<button class="_button" :class="[$style.item, x === 'right' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'right'; y = 'bottom'; }"><i class="ti ti-align-box-right-bottom"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { } from 'vue';
|
||||||
|
|
||||||
|
const x = defineModel<string>('x', { default: 'center' });
|
||||||
|
const y = defineModel<string>('y', { default: 'center' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-template-rows: repeat(3, 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
height: 32px;
|
||||||
|
background: var(--MI_THEME-panel);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--MI_THEME-accentedBg);
|
||||||
|
color: var(--MI_THEME-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -72,24 +72,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||||
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
|
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
|
||||||
|
<div v-if="uploader.items.value.length > 0" style="padding: 12px;">
|
||||||
|
<MkTip k="postFormUploader">
|
||||||
|
{{ i18n.ts._postForm.uploaderTip }}
|
||||||
|
</MkTip>
|
||||||
|
<MkUploaderItems :items="uploader.items.value" @showMenu="(item, ev) => showPerUploadItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerUploadItemMenuViaContextmenu(item, ev)"/>
|
||||||
|
</div>
|
||||||
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
|
||||||
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
<MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
|
||||||
<div v-if="showingOptions" style="padding: 8px 16px;">
|
<div v-if="showingOptions" style="padding: 8px 16px;">
|
||||||
</div>
|
</div>
|
||||||
<footer :class="$style.footer">
|
<footer :class="$style.footer">
|
||||||
<div :class="$style.footerLeft">
|
<div :class="$style.footerLeft">
|
||||||
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
|
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.upload + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromPc"><i class="ti ti-photo-plus"></i></button>
|
||||||
|
<button v-tooltip="i18n.ts.attachFile + ' (' + i18n.ts.fromDrive + ')'" class="_button" :class="$style.footerButton" @click="chooseFileFromDrive"><i class="ti ti-cloud-download"></i></button>
|
||||||
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
||||||
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
||||||
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
|
||||||
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
||||||
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
||||||
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
|
||||||
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
|
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
|
||||||
|
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.footerRight">
|
<div :class="$style.footerRight">
|
||||||
<button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button>
|
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
||||||
<!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button>-->
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<datalist id="hashtags">
|
<datalist id="hashtags">
|
||||||
|
@ -105,10 +110,12 @@ import * as Misskey from 'misskey-js';
|
||||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||||
import { toASCII } from 'punycode.js';
|
import { toASCII } from 'punycode.js';
|
||||||
import { host, url } from '@@/js/config.js';
|
import { host, url } from '@@/js/config.js';
|
||||||
|
import MkUploaderItems from './MkUploaderItems.vue';
|
||||||
import type { ShallowRef } from 'vue';
|
import type { ShallowRef } from 'vue';
|
||||||
import type { PostFormProps } from '@/types/post-form.js';
|
import type { PostFormProps } from '@/types/post-form.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||||
|
import type { UploaderItem } from '@/composables/use-uploader.js';
|
||||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||||
import XTextCounter from '@/components/MkPostForm.TextCounter.vue';
|
import XTextCounter from '@/components/MkPostForm.TextCounter.vue';
|
||||||
|
@ -120,7 +127,7 @@ import { formatTimeString } from '@/utility/format-time-string.js';
|
||||||
import { Autocomplete } from '@/utility/autocomplete.js';
|
import { Autocomplete } from '@/utility/autocomplete.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { selectFiles } from '@/utility/drive.js';
|
import { chooseDriveFile } from '@/utility/drive.js';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -138,6 +145,7 @@ import { getPluginHandlers } from '@/plugin.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
import { globalEvents } from '@/events.js';
|
import { globalEvents } from '@/events.js';
|
||||||
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
|
import { checkDragDataType, getDragData } from '@/drag-and-drop.js';
|
||||||
|
import { useUploader } from '@/composables/use-uploader.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
@ -201,6 +209,15 @@ const justEndedComposition = ref(false);
|
||||||
const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote);
|
const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote);
|
||||||
const postFormActions = getPluginHandlers('post_form_action');
|
const postFormActions = getPluginHandlers('post_form_action');
|
||||||
|
|
||||||
|
const uploader = useUploader({
|
||||||
|
multiple: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
uploader.events.on('itemUploaded', ctx => {
|
||||||
|
files.value.push(ctx.item.uploaded!);
|
||||||
|
uploader.removeItem(ctx.item);
|
||||||
|
});
|
||||||
|
|
||||||
const draftKey = computed((): string => {
|
const draftKey = computed((): string => {
|
||||||
let key = props.channel ? `channel:${props.channel.id}` : '';
|
let key = props.channel ? `channel:${props.channel.id}` : '';
|
||||||
|
|
||||||
|
@ -258,10 +275,11 @@ const cwTextLength = computed((): number => {
|
||||||
const maxCwTextLength = 100;
|
const maxCwTextLength = 100;
|
||||||
|
|
||||||
const canPost = computed((): boolean => {
|
const canPost = computed((): boolean => {
|
||||||
return !props.mock && !posting.value && !posted.value &&
|
return !props.mock && !posting.value && !posted.value && !uploader.uploading.value && (uploader.items.value.length === 0 || uploader.readyForUpload.value) &&
|
||||||
(
|
(
|
||||||
1 <= textLength.value ||
|
1 <= textLength.value ||
|
||||||
1 <= files.value.length ||
|
1 <= files.value.length ||
|
||||||
|
1 <= uploader.items.value.length ||
|
||||||
poll.value != null ||
|
poll.value != null ||
|
||||||
renoteTargetNote.value != null ||
|
renoteTargetNote.value != null ||
|
||||||
quoteId.value != null
|
quoteId.value != null
|
||||||
|
@ -434,13 +452,20 @@ function focus() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseFileFrom(ev) {
|
function chooseFileFromPc(ev: MouseEvent) {
|
||||||
if (props.mock) return;
|
if (props.mock) return;
|
||||||
|
|
||||||
selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
|
os.chooseFileFromPc({ multiple: true }).then(files => {
|
||||||
for (const file of files_) {
|
if (files.length === 0) return;
|
||||||
files.value.push(file);
|
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();
|
toggleReactionAcceptance();
|
||||||
},
|
},
|
||||||
}, { type: 'divider' }, {
|
}, { type: 'divider' }, {
|
||||||
|
type: 'switch',
|
||||||
|
icon: 'ti ti-eye',
|
||||||
|
text: i18n.ts.preview,
|
||||||
|
ref: showPreview,
|
||||||
|
}, {
|
||||||
icon: 'ti ti-trash',
|
icon: 'ti ti-trash',
|
||||||
text: i18n.ts.reset,
|
text: i18n.ts.reset,
|
||||||
danger: true,
|
danger: true,
|
||||||
|
@ -793,6 +823,15 @@ function isAnnoying(text: string): boolean {
|
||||||
text.includes('$[position');
|
text.includes('$[position');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function uploadFiles() {
|
||||||
|
await uploader.upload();
|
||||||
|
|
||||||
|
for (const uploadedItem of uploader.items.value.filter(x => x.uploaded != null)) {
|
||||||
|
files.value.push(uploadedItem.uploaded!);
|
||||||
|
uploader.removeItem(uploadedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function post(ev?: MouseEvent) {
|
async function post(ev?: MouseEvent) {
|
||||||
if (ev) {
|
if (ev) {
|
||||||
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
|
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
|
||||||
|
@ -836,6 +875,10 @@ async function post(ev?: MouseEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uploader.items.value.some(x => x.uploaded == null)) {
|
||||||
|
await uploadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
let postData = {
|
let postData = {
|
||||||
text: text.value === '' ? null : text.value,
|
text: text.value === '' ? null : text.value,
|
||||||
fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
|
fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
|
||||||
|
@ -1039,6 +1082,16 @@ function openAccountMenu(ev: MouseEvent) {
|
||||||
}, ev);
|
}, ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showPerUploadItemMenu(item: UploaderItem, ev: MouseEvent) {
|
||||||
|
const menu = uploader.getMenu(item);
|
||||||
|
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
|
||||||
|
const menu = uploader.getMenu(item);
|
||||||
|
os.contextMenu(menu, ev);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.autofocus) {
|
if (props.autofocus) {
|
||||||
focus();
|
focus();
|
||||||
|
@ -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({
|
defineExpose({
|
||||||
clear,
|
clear,
|
||||||
|
canClose,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkModal
|
<MkModal
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:preferType="'dialog'"
|
:preferType="'dialog'"
|
||||||
@click="modal?.close()"
|
@click="_close()"
|
||||||
@closed="onModalClosed()"
|
@closed="onModalClosed()"
|
||||||
@esc="modal?.close()"
|
@esc="_close()"
|
||||||
>
|
>
|
||||||
<MkPostForm
|
<MkPostForm
|
||||||
ref="form"
|
ref="form"
|
||||||
|
@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
autofocus
|
autofocus
|
||||||
freezeAfterPosted
|
freezeAfterPosted
|
||||||
@posted="onPosted"
|
@posted="onPosted"
|
||||||
@cancel="modal?.close()"
|
@cancel="_close()"
|
||||||
@esc="modal?.close()"
|
@esc="_close()"
|
||||||
/>
|
/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -43,6 +43,7 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = useTemplateRef('modal');
|
const modal = useTemplateRef('modal');
|
||||||
|
const form = useTemplateRef('form');
|
||||||
|
|
||||||
function onPosted() {
|
function onPosted() {
|
||||||
modal.value?.close({
|
modal.value?.close({
|
||||||
|
@ -50,6 +51,12 @@ function onPosted() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _close() {
|
||||||
|
const canClose = await form.value?.canClose();
|
||||||
|
if (!canClose) return;
|
||||||
|
modal.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
function onModalClosed() {
|
function onModalClosed() {
|
||||||
emit('closed');
|
emit('closed');
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<slot name="prefix"></slot>
|
<slot name="prefix"></slot>
|
||||||
<div ref="containerEl" class="container">
|
<div ref="containerEl" class="container">
|
||||||
<div class="track">
|
<div class="track">
|
||||||
<div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div>
|
<div class="highlight right" :style="{ width: rightTrackWidth, left: rightTrackPosition }">
|
||||||
|
<div class="shine right"></div>
|
||||||
|
</div>
|
||||||
|
<div class="highlight left" :style="{ width: leftTrackWidth, left: leftTrackPosition }">
|
||||||
|
<div class="shine left"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="steps && showTicks" class="ticks">
|
<div v-if="steps && showTicks" class="ticks">
|
||||||
<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
|
<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div>
|
||||||
|
@ -24,7 +29,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
@mouseenter.passive="onMouseenter"
|
@mouseenter.passive="onMouseenter"
|
||||||
@mousedown="onMousedown"
|
@mousedown="onMousedown"
|
||||||
@touchstart="onMousedown"
|
@touchstart="onMousedown"
|
||||||
></div>
|
>
|
||||||
|
<div class="thumbInner"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot name="suffix"></slot>
|
<slot name="suffix"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, onUnmounted, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue';
|
||||||
import { isTouchUsing } from '@/utility/touch.js';
|
import { isTouchUsing } from '@/utility/touch.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
|
@ -51,19 +58,40 @@ const props = withDefaults(defineProps<{
|
||||||
continuousUpdate?: boolean;
|
continuousUpdate?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
step: 1,
|
step: 1,
|
||||||
textConverter: (v) => v.toString(),
|
textConverter: (v: number) => (Math.round(v * 1000) / 1000).toString(),
|
||||||
easing: false,
|
easing: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', value: number): void;
|
(ev: 'update:modelValue', value: number): void;
|
||||||
(ev: 'dragEnded', value: number): void;
|
(ev: 'dragEnded', value: number): void;
|
||||||
|
(ev: 'thumbDoubleClicked'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const containerEl = useTemplateRef('containerEl');
|
const containerEl = useTemplateRef('containerEl');
|
||||||
const thumbEl = useTemplateRef('thumbEl');
|
const thumbEl = useTemplateRef('thumbEl');
|
||||||
|
|
||||||
const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
|
const maxRatio = computed(() => Math.abs(props.max) / (props.max + Math.abs(Math.min(0, props.min))));
|
||||||
|
const minRatio = computed(() => Math.abs(Math.min(0, props.min)) / (props.max + Math.abs(Math.min(0, props.min))));
|
||||||
|
|
||||||
|
const rightTrackWidth = computed(() => {
|
||||||
|
return Math.max(0, (steppedRawValue.value - minRatio.value) * 100) + '%';
|
||||||
|
});
|
||||||
|
const leftTrackWidth = computed(() => {
|
||||||
|
return Math.max(0, (minRatio.value - steppedRawValue.value) * 100) + '%';
|
||||||
|
});
|
||||||
|
const rightTrackPosition = computed(() => {
|
||||||
|
return (Math.abs(Math.min(0, props.min)) / (props.max + Math.abs(Math.min(0, props.min)))) * 100 + '%';
|
||||||
|
});
|
||||||
|
const leftTrackPosition = computed(() => {
|
||||||
|
return (Math.min(minRatio.value, steppedRawValue.value) * 100) + '%';
|
||||||
|
});
|
||||||
|
|
||||||
|
const calcRawValue = (value: number) => {
|
||||||
|
return (value - props.min) / (props.max - props.min);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawValue = ref(calcRawValue(props.modelValue));
|
||||||
const steppedRawValue = computed(() => {
|
const steppedRawValue = computed(() => {
|
||||||
if (props.step) {
|
if (props.step) {
|
||||||
const step = props.step / (props.max - props.min);
|
const step = props.step / (props.max - props.min);
|
||||||
|
@ -93,6 +121,11 @@ const calcThumbPosition = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
watch([steppedRawValue, containerEl], calcThumbPosition);
|
watch([steppedRawValue, containerEl], calcThumbPosition);
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
const newRawValue = calcRawValue(newVal);
|
||||||
|
if (rawValue.value === newRawValue) return;
|
||||||
|
rawValue.value = newRawValue;
|
||||||
|
});
|
||||||
|
|
||||||
let ro: ResizeObserver | undefined;
|
let ro: ResizeObserver | undefined;
|
||||||
|
|
||||||
|
@ -118,6 +151,12 @@ const steps = computed(() => {
|
||||||
const tooltipForDragShowing = ref(false);
|
const tooltipForDragShowing = ref(false);
|
||||||
const tooltipForHoverShowing = ref(false);
|
const tooltipForHoverShowing = ref(false);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// 何らかの問題で表示されっぱなしでもコンポーネントを離れたら消えるように
|
||||||
|
tooltipForDragShowing.value = false;
|
||||||
|
tooltipForHoverShowing.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
function onMouseenter() {
|
function onMouseenter() {
|
||||||
if (isTouchUsing) return;
|
if (isTouchUsing) return;
|
||||||
|
|
||||||
|
@ -128,7 +167,7 @@ function onMouseenter() {
|
||||||
text: computed(() => {
|
text: computed(() => {
|
||||||
return props.textConverter(finalValue.value);
|
return props.textConverter(finalValue.value);
|
||||||
}),
|
}),
|
||||||
targetElement: thumbEl,
|
targetElement: thumbEl.value ?? undefined,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
|
@ -138,6 +177,8 @@ function onMouseenter() {
|
||||||
}, { once: true, passive: true });
|
}, { once: true, passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastClickTime: number | null = null;
|
||||||
|
|
||||||
function onMousedown(ev: MouseEvent | TouchEvent) {
|
function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
|
@ -148,7 +189,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
text: computed(() => {
|
text: computed(() => {
|
||||||
return props.textConverter(finalValue.value);
|
return props.textConverter(finalValue.value);
|
||||||
}),
|
}),
|
||||||
targetElement: thumbEl,
|
targetElement: thumbEl.value ?? undefined,
|
||||||
}, {
|
}, {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
|
@ -193,6 +234,20 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
window.addEventListener('touchmove', onDrag);
|
window.addEventListener('touchmove', onDrag);
|
||||||
window.addEventListener('mouseup', onMouseup, { once: true });
|
window.addEventListener('mouseup', onMouseup, { once: true });
|
||||||
window.addEventListener('touchend', onMouseup, { once: true });
|
window.addEventListener('touchend', onMouseup, { once: true });
|
||||||
|
|
||||||
|
if (lastClickTime == null) {
|
||||||
|
lastClickTime = Date.now();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastClickTime < 300) { // 300ms以内のクリックはダブルクリックとみなす
|
||||||
|
lastClickTime = null;
|
||||||
|
emit('thumbDoubleClicked');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
lastClickTime = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -222,15 +277,17 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$thumbHeight: 20px;
|
$thumbHeight: 32px;
|
||||||
$thumbWidth: 20px;
|
$thumbWidth: 32px;
|
||||||
|
$thumbInnerHeight: 19px;
|
||||||
|
$thumbInnerWidth: 19px;
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 7px 12px;
|
padding: 0px 4px;
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
border: solid 1px var(--MI_THEME-panel);
|
border: solid 1px var(--MI_THEME-panel);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
@ -256,10 +313,30 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
> .highlight {
|
> .highlight {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--MI_THEME-accent);
|
background: color(from var(--MI_THEME-buttonGradateA) srgb r g b / 0.5);
|
||||||
opacity: 0.5;
|
overflow: clip;
|
||||||
|
|
||||||
|
> .shine {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 64px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .highlight.right {
|
||||||
|
> .shine.right {
|
||||||
|
right: calc(#{$thumbInnerWidth} / 2);
|
||||||
|
background: linear-gradient(-90deg, var(--MI_THEME-buttonGradateB), color(from var(--MI_THEME-buttonGradateA) srgb r g b / 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .highlight.left {
|
||||||
|
> .shine.left {
|
||||||
|
left: calc(#{$thumbInnerWidth} / 2);
|
||||||
|
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateB), color(from var(--MI_THEME-buttonGradateA) srgb r g b / 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,11 +367,25 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
|
||||||
width: $thumbWidth;
|
width: $thumbWidth;
|
||||||
height: $thumbHeight;
|
height: $thumbHeight;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
background: var(--MI_THEME-accent);
|
|
||||||
border-radius: 999px;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
> .thumbInner {
|
||||||
|
background: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .thumbInner {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: $thumbInnerWidth;
|
||||||
|
height: $thumbInnerHeight;
|
||||||
|
background: var(--MI_THEME-accent);
|
||||||
|
border-radius: 999px;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,10 @@ import * as Misskey from 'misskey-js';
|
||||||
import { inject, watch, ref } from 'vue';
|
import { inject, watch, ref } from 'vue';
|
||||||
import { TransitionGroup } from 'vue';
|
import { TransitionGroup } from 'vue';
|
||||||
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
|
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
|
||||||
|
import { $i } from '@/i.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
import { customEmojisMap } from '@/custom-emojis.js';
|
||||||
|
import { isSupportedEmoji } from '@@/js/emojilist.js';
|
||||||
import { DI } from '@/di.js';
|
import { DI } from '@/di.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
|
@ -70,6 +73,12 @@ function onMockToggleReaction(emoji: string, count: number) {
|
||||||
emit('mockUpdateMyReaction', emoji, (count - _reactions.value[i][1]));
|
emit('mockUpdateMyReaction', emoji, (count - _reactions.value[i][1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canReact(reaction: string) {
|
||||||
|
if (!$i) return false;
|
||||||
|
// TODO: CheckPermissions
|
||||||
|
return !reaction.match(/@\w/) && (customEmojisMap.has(reaction) || isSupportedEmoji(reaction));
|
||||||
|
}
|
||||||
|
|
||||||
watch([() => props.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
|
watch([() => props.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
|
||||||
let newReactions: [string, number][] = [];
|
let newReactions: [string, number][] = [];
|
||||||
hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
|
hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
|
||||||
|
@ -86,7 +95,15 @@ watch([() => props.reactions, () => props.maxNumber], ([newSource, maxNumber]) =
|
||||||
newReactions = [
|
newReactions = [
|
||||||
...newReactions,
|
...newReactions,
|
||||||
...Object.entries(newSource)
|
...Object.entries(newSource)
|
||||||
.sort(([, a], [, b]) => b - a)
|
.sort(([emojiA, countA], [emojiB, countB]) => {
|
||||||
|
if (prefer.s.showAvailableReactionsFirstInNote) {
|
||||||
|
if (!canReact(emojiA) && canReact(emojiB)) return 1;
|
||||||
|
if (canReact(emojiA) && !canReact(emojiB)) return -1;
|
||||||
|
return countB - countA;
|
||||||
|
} else {
|
||||||
|
return countB - countA;
|
||||||
|
}
|
||||||
|
})
|
||||||
.filter(([y], i) => i < maxNumber && !newReactionsNames.includes(y)),
|
.filter(([y], i) => i < maxNumber && !newReactionsNames.includes(y)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
|
import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const particles = ref<{
|
const particles = ref<{
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -86,7 +87,7 @@ onMounted(() => {
|
||||||
const y = (Math.random() * (height.value - 64));
|
const y = (Math.random() * (height.value - 64));
|
||||||
const sizeFactor = Math.random();
|
const sizeFactor = Math.random();
|
||||||
const particle = {
|
const particle = {
|
||||||
id: Math.random().toString(),
|
id: genId(),
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
size: 0.2 + ((sizeFactor / 10) * 3),
|
size: 0.2 + ((sizeFactor / 10) * 3),
|
||||||
|
|
|
@ -62,6 +62,7 @@ import { useInterval } from '@@/js/use-interval.js';
|
||||||
import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
|
import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
|
||||||
import type { BasicTimelineType } from '@/timelines.js';
|
import type { BasicTimelineType } from '@/timelines.js';
|
||||||
import type { PagingCtx } from '@/composables/use-pagination.js';
|
import type { PagingCtx } from '@/composables/use-pagination.js';
|
||||||
|
import type { SoundStore } from '@/preferences/def.js';
|
||||||
import { usePagination } from '@/composables/use-pagination.js';
|
import { usePagination } from '@/composables/use-pagination.js';
|
||||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
|
@ -83,6 +84,7 @@ const props = withDefaults(defineProps<{
|
||||||
channel?: string;
|
channel?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
sound?: boolean;
|
sound?: boolean;
|
||||||
|
customSound?: SoundStore | null;
|
||||||
withRenotes?: boolean;
|
withRenotes?: boolean;
|
||||||
withReplies?: boolean;
|
withReplies?: boolean;
|
||||||
withSensitive?: boolean;
|
withSensitive?: boolean;
|
||||||
|
@ -92,6 +94,8 @@ const props = withDefaults(defineProps<{
|
||||||
withReplies: false,
|
withReplies: false,
|
||||||
withSensitive: true,
|
withSensitive: true,
|
||||||
onlyFiles: false,
|
onlyFiles: false,
|
||||||
|
sound: false,
|
||||||
|
customSound: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
provide('inTimeline', true);
|
provide('inTimeline', true);
|
||||||
|
@ -190,7 +194,11 @@ function prepend(note: Misskey.entities.Note) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.sound) {
|
if (props.sound) {
|
||||||
sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note');
|
if (props.customSound) {
|
||||||
|
sound.playMisskeySfxFile(props.customSound);
|
||||||
|
} else {
|
||||||
|
sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,7 +428,7 @@ defineExpose({
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note {
|
.note:not(:empty) {
|
||||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { i18n } from '@/i18n.js';
|
||||||
import { globalEvents } from '@/events.js';
|
import { globalEvents } from '@/events.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
import MkNote from '@/components/MkNote.vue';
|
import MkNote from '@/components/MkNote.vue';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
phase: 'aboutNote' | 'howToReact';
|
phase: 'aboutNote' | 'howToReact';
|
||||||
|
@ -83,7 +84,7 @@ function doNotification(emoji: string): void {
|
||||||
if (!$i || !emoji) return;
|
if (!$i || !emoji) return;
|
||||||
|
|
||||||
const notification: Misskey.entities.Notification = {
|
const notification: Misskey.entities.Notification = {
|
||||||
id: Math.random().toString(),
|
id: genId(),
|
||||||
createdAt: new Date().toUTCString(),
|
createdAt: new Date().toUTCString(),
|
||||||
type: 'reaction',
|
type: 'reaction',
|
||||||
reaction: emoji,
|
reaction: emoji,
|
||||||
|
|
|
@ -23,55 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
{{ i18n.ts._uploader.tip }}
|
{{ i18n.ts._uploader.tip }}
|
||||||
</MkTip>
|
</MkTip>
|
||||||
|
|
||||||
<div class="_gaps_s">
|
<MkUploaderItems :items="items" @showMenu="(item, ev) => showPerItemMenu(item, ev)" @showMenuViaContextmenu="(item, ev) => showPerItemMenuViaContextmenu(item, ev)"/>
|
||||||
<div
|
|
||||||
v-for="ctx in items"
|
|
||||||
:key="ctx.id"
|
|
||||||
v-panel
|
|
||||||
:class="[$style.item, ctx.waiting ? $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>{{ bytes(ctx.file.size) }}</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>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div :class="$style.itemIconWrapper">
|
|
||||||
<MkSystemIcon v-if="ctx.uploading" :class="$style.itemIcon" type="waiting"/>
|
|
||||||
<MkSystemIcon v-else-if="ctx.uploaded" :class="$style.itemIcon" type="success"/>
|
|
||||||
<MkSystemIcon v-else-if="ctx.uploadFailed" :class="$style.itemIcon" type="error"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="props.multiple">
|
<div v-if="props.multiple">
|
||||||
<MkButton style="margin: auto;" :iconOnly="true" rounded @click="chooseFile($event)"><i class="ti ti-plus"></i></MkButton>
|
<MkButton style="margin: auto;" :iconOnly="true" rounded @click="chooseFile($event)"><i class="ti ti-plus"></i></MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkSelect
|
|
||||||
v-if="items.length > 0"
|
|
||||||
v-model="compressionLevel"
|
|
||||||
:items="[
|
|
||||||
{ value: 0, label: i18n.ts.none },
|
|
||||||
{ value: 1, label: i18n.ts.low },
|
|
||||||
{ value: 2, label: i18n.ts.middle },
|
|
||||||
{ value: 3, label: i18n.ts.high },
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<template #label>{{ i18n.ts.compress }}</template>
|
|
||||||
</MkSelect>
|
|
||||||
|
|
||||||
<div>{{ i18n.tsx._uploader.maxFileSizeIsX({ x: $i.policies.maxFileSizeMb + 'MB' }) }}</div>
|
<div>{{ i18n.tsx._uploader.maxFileSizeIsX({ x: $i.policies.maxFileSizeMb + 'MB' }) }}</div>
|
||||||
|
|
||||||
<!-- クライアントで検出するMIME typeとサーバーで検出するMIME typeが異なる場合があり、混乱の元になるのでとりあえず隠しとく -->
|
<!-- クライアントで検出するMIME typeとサーバーで検出するMIME typeが異なる場合があり、混乱の元になるのでとりあえず隠しとく -->
|
||||||
|
@ -82,8 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="_buttonsCenter">
|
<div class="_buttonsCenter">
|
||||||
<MkButton v-if="isUploading" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
|
<MkButton v-if="uploader.uploading.value" rounded @click="abortWithConfirm()"><i class="ti ti-x"></i> {{ i18n.ts.abort }}</MkButton>
|
||||||
<MkButton v-else-if="!firstUploadAttempted" primary rounded @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
|
<MkButton v-else-if="!firstUploadAttempted" primary rounded :disabled="!uploader.readyForUpload.value" @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
|
||||||
|
|
||||||
<MkButton v-if="canRetry" rounded @click="upload()"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
|
<MkButton v-if="canRetry" rounded @click="upload()"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
|
||||||
<MkButton v-if="canDone" rounded @click="done()"><i class="ti ti-arrow-right"></i> {{ i18n.ts.done }}</MkButton>
|
<MkButton v-if="canDone" rounded @click="done()"><i class="ti ti-arrow-right"></i> {{ i18n.ts.done }}</MkButton>
|
||||||
|
@ -93,48 +50,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, markRaw, onMounted, ref, useTemplateRef, watch } from 'vue';
|
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import type { UploaderFeatures, UploaderItem } from '@/composables/use-uploader.js';
|
||||||
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
|
||||||
import isAnimated from 'is-file-animated';
|
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { prefer } from '@/preferences.js';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import bytes from '@/filters/bytes.js';
|
|
||||||
import 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 * as os from '@/os.js';
|
||||||
import { ensureSignin } from '@/i.js';
|
import { ensureSignin } from '@/i.js';
|
||||||
|
import { useUploader } from '@/composables/use-uploader.js';
|
||||||
|
import MkUploaderItems from '@/components/MkUploaderItems.vue';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
const COMPRESSION_SUPPORTED_TYPES = [
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/webp',
|
|
||||||
'image/svg+xml',
|
|
||||||
];
|
|
||||||
|
|
||||||
const CROPPING_SUPPORTED_TYPES = [
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/webp',
|
|
||||||
];
|
|
||||||
|
|
||||||
const mimeTypeMap = {
|
|
||||||
'image/webp': 'webp',
|
|
||||||
'image/jpeg': 'jpg',
|
|
||||||
'image/png': 'png',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
files: File[];
|
files: File[];
|
||||||
folderId?: string | null;
|
folderId?: string | null;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
features?: UploaderFeatures;
|
||||||
}>(), {
|
}>(), {
|
||||||
multiple: true,
|
multiple: true,
|
||||||
});
|
});
|
||||||
|
@ -145,27 +78,22 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const items = ref<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
progress: { max: number; value: number } | null;
|
|
||||||
thumbnail: string;
|
|
||||||
waiting: boolean;
|
|
||||||
uploading: boolean;
|
|
||||||
uploaded: Misskey.entities.DriveFile | null;
|
|
||||||
uploadFailed: boolean;
|
|
||||||
aborted: boolean;
|
|
||||||
compressedSize?: number | null;
|
|
||||||
compressedImage?: Blob | null;
|
|
||||||
file: File;
|
|
||||||
abort?: (() => void) | null;
|
|
||||||
}[]>([]);
|
|
||||||
|
|
||||||
const dialog = useTemplateRef('dialog');
|
const dialog = useTemplateRef('dialog');
|
||||||
|
|
||||||
|
const uploader = useUploader({
|
||||||
|
multiple: props.multiple,
|
||||||
|
folderId: props.folderId,
|
||||||
|
features: props.features,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
uploader.addFiles(props.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = uploader.items;
|
||||||
|
|
||||||
const firstUploadAttempted = ref(false);
|
const firstUploadAttempted = ref(false);
|
||||||
const isUploading = computed(() => items.value.some(item => item.uploading));
|
const canRetry = computed(() => firstUploadAttempted.value && uploader.readyForUpload.value);
|
||||||
const canRetry = computed(() => firstUploadAttempted.value && !items.value.some(item => item.uploading || item.waiting) && items.value.some(item => item.uploaded == null));
|
|
||||||
const canDone = computed(() => items.value.some(item => item.uploaded != null));
|
const canDone = computed(() => items.value.some(item => item.uploaded != null));
|
||||||
const overallProgress = computed(() => {
|
const overallProgress = computed(() => {
|
||||||
const max = items.value.length;
|
const max = items.value.length;
|
||||||
|
@ -178,28 +106,6 @@ const overallProgress = computed(() => {
|
||||||
return Math.round((v / max) * 100);
|
return Math.round((v / max) * 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
const compressionLevel = ref<0 | 1 | 2 | 3>(2);
|
|
||||||
const compressionSettings = computed(() => {
|
|
||||||
if (compressionLevel.value === 1) {
|
|
||||||
return {
|
|
||||||
maxWidth: 2000,
|
|
||||||
maxHeight: 2000,
|
|
||||||
};
|
|
||||||
} else if (compressionLevel.value === 2) {
|
|
||||||
return {
|
|
||||||
maxWidth: 2000 * 0.75, // =1500
|
|
||||||
maxHeight: 2000 * 0.75, // =1500
|
|
||||||
};
|
|
||||||
} else if (compressionLevel.value === 3) {
|
|
||||||
return {
|
|
||||||
maxWidth: 2000 * 0.75 * 0.75, // =1125
|
|
||||||
maxHeight: 2000 * 0.75 * 0.75, // =1125
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(items, () => {
|
watch(items, () => {
|
||||||
if (items.value.length === 0) {
|
if (items.value.length === 0) {
|
||||||
emit('canceled');
|
emit('canceled');
|
||||||
|
@ -222,11 +128,16 @@ async function cancel() {
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
abortAll();
|
uploader.abortAll();
|
||||||
emit('canceled');
|
emit('canceled');
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function upload() {
|
||||||
|
firstUploadAttempted.value = true;
|
||||||
|
uploader.upload();
|
||||||
|
}
|
||||||
|
|
||||||
async function abortWithConfirm() {
|
async function abortWithConfirm() {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
|
@ -236,11 +147,11 @@ async function abortWithConfirm() {
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
abortAll();
|
uploader.abortAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function done() {
|
async function done() {
|
||||||
if (items.value.some(item => item.uploaded == null)) {
|
if (!uploader.allItemsUploaded.value) {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
text: i18n.ts._uploader.doneConfirm,
|
text: i18n.ts._uploader.doneConfirm,
|
||||||
|
@ -254,194 +165,20 @@ async function done() {
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
|
async function chooseFile(ev: MouseEvent) {
|
||||||
const menu: MenuItem[] = [];
|
const newFiles = await os.chooseFileFromPc({ multiple: true });
|
||||||
|
uploader.addFiles(newFiles);
|
||||||
menu.push({
|
}
|
||||||
icon: 'ti ti-cursor-text',
|
|
||||||
text: i18n.ts.rename,
|
|
||||||
action: async () => {
|
|
||||||
const { result, canceled } = await os.inputText({
|
|
||||||
type: 'text',
|
|
||||||
title: i18n.ts.rename,
|
|
||||||
placeholder: item.name,
|
|
||||||
default: item.name,
|
|
||||||
});
|
|
||||||
if (canceled) return;
|
|
||||||
if (result.trim() === '') return;
|
|
||||||
|
|
||||||
item.name = result;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (CROPPING_SUPPORTED_TYPES.includes(item.file.type) && !item.waiting && !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 });
|
|
||||||
items.value.splice(items.value.indexOf(item), 1, {
|
|
||||||
...item,
|
|
||||||
file: markRaw(cropped),
|
|
||||||
thumbnail: window.URL.createObjectURL(cropped),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.waiting && !item.uploading && !item.uploaded) {
|
|
||||||
menu.push({
|
|
||||||
icon: 'ti ti-x',
|
|
||||||
text: i18n.ts.remove,
|
|
||||||
action: () => {
|
|
||||||
items.value.splice(items.value.indexOf(item), 1);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else if (item.uploading) {
|
|
||||||
menu.push({
|
|
||||||
icon: 'ti ti-cloud-pause',
|
|
||||||
text: i18n.ts.abort,
|
|
||||||
danger: true,
|
|
||||||
action: () => {
|
|
||||||
if (item.abort != null) {
|
|
||||||
item.abort();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
function showPerItemMenu(item: UploaderItem, ev: MouseEvent) {
|
||||||
|
const menu = uploader.getMenu(item);
|
||||||
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
os.popupMenu(menu, ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upload() { // エラーハンドリングなどを考慮してシーケンシャルにやる
|
function showPerItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent) {
|
||||||
firstUploadAttempted.value = true;
|
const menu = uploader.getMenu(item);
|
||||||
|
os.contextMenu(menu, ev);
|
||||||
items.value = items.value.map(item => ({
|
|
||||||
...item,
|
|
||||||
aborted: false,
|
|
||||||
uploadFailed: false,
|
|
||||||
waiting: false,
|
|
||||||
uploading: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
for (const item of items.value.filter(item => item.uploaded == null)) {
|
|
||||||
// アップロード処理途中で値が変わる場合(途中で全キャンセルされたりなど)もあるので、Array filterではなくここでチェック
|
|
||||||
if (item.aborted) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.waiting = true;
|
|
||||||
item.uploadFailed = false;
|
|
||||||
|
|
||||||
const shouldCompress = item.compressedImage == null && compressionLevel.value !== 0 && compressionSettings.value && COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) && !(await isAnimated(item.file));
|
|
||||||
|
|
||||||
if (shouldCompress) {
|
|
||||||
const config = {
|
|
||||||
mimeType: isWebpSupported() ? 'image/webp' : 'image/jpeg',
|
|
||||||
maxWidth: compressionSettings.value.maxWidth,
|
|
||||||
maxHeight: compressionSettings.value.maxHeight,
|
|
||||||
quality: isWebpSupported() ? 0.85 : 0.8,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await readAndCompressImage(item.file, config);
|
|
||||||
if (result.size < item.file.size || item.file.type === 'image/webp') {
|
|
||||||
// The compression may not always reduce the file size
|
|
||||||
// (and WebP is not browser safe yet)
|
|
||||||
item.compressedImage = markRaw(result);
|
|
||||||
item.compressedSize = result.size;
|
|
||||||
item.name = item.file.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to resize image', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item.uploading = true;
|
|
||||||
|
|
||||||
const { filePromise, abort } = uploadFile(item.compressedImage ?? item.file, {
|
|
||||||
name: item.name,
|
|
||||||
folderId: props.folderId,
|
|
||||||
onProgress: (progress) => {
|
|
||||||
item.waiting = false;
|
|
||||||
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.waiting = 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;
|
|
||||||
item.waiting = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function abortAll() {
|
|
||||||
for (const item of items.value) {
|
|
||||||
if (item.uploaded != null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.abort != null) {
|
|
||||||
item.abort();
|
|
||||||
}
|
|
||||||
item.aborted = true;
|
|
||||||
item.uploadFailed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function chooseFile(ev: MouseEvent) {
|
|
||||||
const newFiles = await os.chooseFileFromPc({ multiple: true });
|
|
||||||
|
|
||||||
for (const file of newFiles) {
|
|
||||||
initializeFile(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeFile(file: File) {
|
|
||||||
const id = uuid();
|
|
||||||
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: window.URL.createObjectURL(file),
|
|
||||||
waiting: false,
|
|
||||||
uploading: false,
|
|
||||||
aborted: false,
|
|
||||||
uploaded: null,
|
|
||||||
uploadFailed: false,
|
|
||||||
file: markRaw(file),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
for (const file of props.files) {
|
|
||||||
initializeFile(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
@ -463,127 +200,4 @@ onMounted(() => {
|
||||||
background: var(--MI_THEME-warn);
|
background: var(--MI_THEME-warn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
|
||||||
position: relative;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: clip;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: var(--p);
|
|
||||||
height: 100%;
|
|
||||||
background: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
|
|
||||||
transition: width 0.2s ease, left 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.itemWaiting {
|
|
||||||
&::after {
|
|
||||||
--c: color(from var(--MI_THEME-accent) srgb r g b / 0.25);
|
|
||||||
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(-45deg, transparent 25%, var(--c) 25%,var(--c) 50%, transparent 50%, transparent 75%, var(--c) 75%, var(--c));
|
|
||||||
background-size: 25px 25px;
|
|
||||||
animation: stripe .8s infinite linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.itemCompleted {
|
|
||||||
&::before {
|
|
||||||
left: 100%;
|
|
||||||
width: var(--p);
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemBody {
|
|
||||||
color: var(--MI_THEME-accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.itemFailed {
|
|
||||||
.itemBody {
|
|
||||||
color: var(--MI_THEME-error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes stripe {
|
|
||||||
0% { background-position-x: 0; }
|
|
||||||
100% { background-position-x: -25px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemInner {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
padding: 8px 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemThumbnail {
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
background-color: var(--MI_THEME-bg);
|
|
||||||
background-size: contain;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemBody {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemInfo {
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 90%;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemIcon {
|
|
||||||
width: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@container (max-width: 500px) {
|
|
||||||
.itemInner {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemBody {
|
|
||||||
font-size: 90%;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemActionWrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemInfo {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemIconWrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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>
|
|
@ -0,0 +1,325 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="$style.root" class="_gaps">
|
||||||
|
<template v-if="layer.type === 'text'">
|
||||||
|
<MkInput v-model="layer.text">
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.text }}</template>
|
||||||
|
</MkInput>
|
||||||
|
|
||||||
|
<FormSlot>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.position }}</template>
|
||||||
|
<MkPositionSelector
|
||||||
|
v-model:x="layer.align.x"
|
||||||
|
v-model:y="layer.align.y"
|
||||||
|
></MkPositionSelector>
|
||||||
|
</FormSlot>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.scale"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.angle"
|
||||||
|
:min="-1"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.angle }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.opacity"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkSwitch v-model="layer.repeat">
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.repeat }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="layer.type === 'image'">
|
||||||
|
<MkButton inline rounded primary @click="chooseFile">{{ i18n.ts.selectFile }}</MkButton>
|
||||||
|
|
||||||
|
<FormSlot>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.position }}</template>
|
||||||
|
<MkPositionSelector
|
||||||
|
v-model:x="layer.align.x"
|
||||||
|
v-model:y="layer.align.y"
|
||||||
|
></MkPositionSelector>
|
||||||
|
</FormSlot>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.scale"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.angle"
|
||||||
|
:min="-1"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.angle }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.opacity"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkSwitch v-model="layer.repeat">
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.repeat }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
|
||||||
|
<MkSwitch v-model="layer.cover">
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.cover }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="layer.type === 'stripe'">
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.frequency"
|
||||||
|
:min="1"
|
||||||
|
:max="30"
|
||||||
|
:step="0.01"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.stripeFrequency }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.threshold"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.stripeWidth }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.angle"
|
||||||
|
:min="-1"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.angle }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.opacity"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="layer.type === 'polkadot'">
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.angle"
|
||||||
|
:min="-1"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.angle }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.scale"
|
||||||
|
:min="0"
|
||||||
|
:max="10"
|
||||||
|
:step="0.01"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.majorRadius"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.polkadotMainDotRadius }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.majorOpacity"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.polkadotMainDotOpacity }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.minorDivisions"
|
||||||
|
:min="0"
|
||||||
|
:max="16"
|
||||||
|
:step="1"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.polkadotSubDotDivisions }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.minorRadius"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.polkadotSubDotRadius }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.minorOpacity"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.polkadotSubDotOpacity }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="layer.type === 'checker'">
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.angle"
|
||||||
|
:min="-1"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.angle }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.scale"
|
||||||
|
:min="0"
|
||||||
|
:max="10"
|
||||||
|
:step="0.01"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.scale }}</template>
|
||||||
|
</MkRange>
|
||||||
|
|
||||||
|
<MkRange
|
||||||
|
v-model="layer.opacity"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
:textConverter="(v) => (v * 100).toFixed(1) + '%'"
|
||||||
|
continuousUpdate
|
||||||
|
>
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.opacity }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import type { WatermarkPreset } from '@/utility/watermark.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkInput from '@/components/MkInput.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';
|
||||||
|
|
||||||
|
const layer = defineModel<WatermarkPreset['layers'][number]>('layer', { required: true });
|
||||||
|
|
||||||
|
const driveFile = ref<Misskey.entities.DriveFile | null>(null);
|
||||||
|
const driveFileError = ref(false);
|
||||||
|
onMounted(async () => {
|
||||||
|
if (layer.value.type === 'image' && layer.value.imageId != null) {
|
||||||
|
await misskeyApi('drive/files/show', {
|
||||||
|
fileId: layer.value.imageId,
|
||||||
|
}).then((res) => {
|
||||||
|
driveFile.value = res;
|
||||||
|
}).catch((err) => {
|
||||||
|
driveFileError.value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function chooseFile(ev: MouseEvent) {
|
||||||
|
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',
|
||||||
|
title: i18n.ts._watermarkEditor.driveFileTypeWarn,
|
||||||
|
text: i18n.ts._watermarkEditor.driveFileTypeWarnDescription,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.value.imageId = file.id;
|
||||||
|
layer.value.imageUrl = file.url;
|
||||||
|
driveFileError.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.root {
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,456 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MkModalWindow
|
||||||
|
ref="dialog"
|
||||||
|
:width="1000"
|
||||||
|
:height="600"
|
||||||
|
:scroll="false"
|
||||||
|
:withOkButton="true"
|
||||||
|
@close="cancel()"
|
||||||
|
@ok="save()"
|
||||||
|
@closed="emit('closed')"
|
||||||
|
>
|
||||||
|
<template #header><i class="ti ti-copyright"></i> {{ i18n.ts._watermarkEditor.title }}</template>
|
||||||
|
|
||||||
|
<div :class="$style.root">
|
||||||
|
<div :class="$style.container">
|
||||||
|
<div :class="$style.preview">
|
||||||
|
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
|
||||||
|
<div :class="$style.previewContainer">
|
||||||
|
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
|
||||||
|
<div v-if="props.image == null" class="_acrylic" :class="$style.previewControls">
|
||||||
|
<button class="_button" :class="[$style.previewControlsButton, sampleImageType === '3_2' ? $style.active : null]" @click="sampleImageType = '3_2'"><i class="ti ti-crop-landscape"></i></button>
|
||||||
|
<button class="_button" :class="[$style.previewControlsButton, sampleImageType === '2_3' ? $style.active : null]" @click="sampleImageType = '2_3'"><i class="ti ti-crop-portrait"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.controls">
|
||||||
|
<div class="_spacer _gaps">
|
||||||
|
<MkSelect v-model="type" :items="[{ label: i18n.ts._watermarkEditor.text, value: 'text' }, { label: i18n.ts._watermarkEditor.image, value: 'image' }, { label: i18n.ts._watermarkEditor.advanced, value: 'advanced' }]">
|
||||||
|
<template #label>{{ i18n.ts._watermarkEditor.type }}</template>
|
||||||
|
</MkSelect>
|
||||||
|
|
||||||
|
<div v-if="type === 'text' || type === 'image'">
|
||||||
|
<XLayer
|
||||||
|
v-for="(layer, i) in preset.layers"
|
||||||
|
:key="layer.id"
|
||||||
|
v-model:layer="preset.layers[i]"
|
||||||
|
></XLayer>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="type === 'advanced'" class="_gaps_s">
|
||||||
|
<MkFolder v-for="(layer, i) in preset.layers" :key="layer.id" :defaultOpen="false" :canPage="false">
|
||||||
|
<template #label>
|
||||||
|
<div v-if="layer.type === 'text'">{{ i18n.ts._watermarkEditor.text }}</div>
|
||||||
|
<div v-if="layer.type === 'image'">{{ i18n.ts._watermarkEditor.image }}</div>
|
||||||
|
<div v-if="layer.type === 'stripe'">{{ i18n.ts._watermarkEditor.stripe }}</div>
|
||||||
|
<div v-if="layer.type === 'polkadot'">{{ i18n.ts._watermarkEditor.polkadot }}</div>
|
||||||
|
<div v-if="layer.type === 'checker'">{{ i18n.ts._watermarkEditor.checker }}</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="_buttons">
|
||||||
|
<MkButton iconOnly @click="removeLayer(layer)"><i class="ti ti-trash"></i></MkButton>
|
||||||
|
<MkButton iconOnly @click="swapUpLayer(layer)"><i class="ti ti-arrow-up"></i></MkButton>
|
||||||
|
<MkButton iconOnly @click="swapDownLayer(layer)"><i class="ti ti-arrow-down"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<XLayer
|
||||||
|
v-model:layer="preset.layers[i]"
|
||||||
|
></XLayer>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkButton rounded primary style="margin: 0 auto;" @click="addLayer"><i class="ti ti-plus"></i></MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkModalWindow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick } from 'vue';
|
||||||
|
import type { WatermarkPreset } from '@/utility/watermark.js';
|
||||||
|
import { WatermarkRenderer } from '@/utility/watermark.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import XLayer from '@/components/MkWatermarkEditorDialog.Layer.vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { deepClone } from '@/utility/clone.js';
|
||||||
|
import { ensureSignin } from '@/i.js';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
function createTextLayer(): WatermarkPreset['layers'][number] {
|
||||||
|
return {
|
||||||
|
id: genId(),
|
||||||
|
type: 'text',
|
||||||
|
text: `(c) @${$i.username}`,
|
||||||
|
align: { x: 'right', y: 'bottom' },
|
||||||
|
scale: 0.3,
|
||||||
|
angle: 0,
|
||||||
|
opacity: 0.75,
|
||||||
|
repeat: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createImageLayer(): WatermarkPreset['layers'][number] {
|
||||||
|
return {
|
||||||
|
id: genId(),
|
||||||
|
type: 'image',
|
||||||
|
imageId: null,
|
||||||
|
imageUrl: null,
|
||||||
|
align: { x: 'right', y: 'bottom' },
|
||||||
|
scale: 0.3,
|
||||||
|
angle: 0,
|
||||||
|
opacity: 0.75,
|
||||||
|
repeat: false,
|
||||||
|
cover: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStripeLayer(): WatermarkPreset['layers'][number] {
|
||||||
|
return {
|
||||||
|
id: genId(),
|
||||||
|
type: 'stripe',
|
||||||
|
angle: 0.5,
|
||||||
|
frequency: 10,
|
||||||
|
threshold: 0.1,
|
||||||
|
color: [1, 1, 1],
|
||||||
|
opacity: 0.75,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPolkadotLayer(): WatermarkPreset['layers'][number] {
|
||||||
|
return {
|
||||||
|
id: genId(),
|
||||||
|
type: 'polkadot',
|
||||||
|
angle: 0.5,
|
||||||
|
scale: 3,
|
||||||
|
majorRadius: 0.1,
|
||||||
|
minorRadius: 0.25,
|
||||||
|
majorOpacity: 0.75,
|
||||||
|
minorOpacity: 0.5,
|
||||||
|
minorDivisions: 4,
|
||||||
|
color: [1, 1, 1],
|
||||||
|
opacity: 0.75,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCheckerLayer(): WatermarkPreset['layers'][number] {
|
||||||
|
return {
|
||||||
|
id: genId(),
|
||||||
|
type: 'checker',
|
||||||
|
angle: 0.5,
|
||||||
|
scale: 3,
|
||||||
|
color: [1, 1, 1],
|
||||||
|
opacity: 0.75,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
preset?: WatermarkPreset | null;
|
||||||
|
image?: File | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const preset = reactive<WatermarkPreset>(deepClone(props.preset) ?? {
|
||||||
|
id: genId(),
|
||||||
|
name: '',
|
||||||
|
layers: [createTextLayer()],
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'ok', preset: WatermarkPreset): void;
|
||||||
|
(ev: 'cancel'): void;
|
||||||
|
(ev: 'closed'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dialog = useTemplateRef('dialog');
|
||||||
|
|
||||||
|
async function cancel() {
|
||||||
|
const { canceled } = await os.confirm({
|
||||||
|
type: 'question',
|
||||||
|
text: i18n.ts._watermarkEditor.quitWithoutSaveConfirm,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
emit('cancel');
|
||||||
|
dialog.value?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = ref(preset.layers.length > 1 ? 'advanced' : preset.layers[0].type);
|
||||||
|
watch(type, () => {
|
||||||
|
if (type.value === 'text') {
|
||||||
|
preset.layers = [createTextLayer()];
|
||||||
|
} else if (type.value === 'image') {
|
||||||
|
preset.layers = [createImageLayer()];
|
||||||
|
} else if (type.value === 'advanced') {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(preset, async (newValue, oldValue) => {
|
||||||
|
if (renderer != null) {
|
||||||
|
renderer.setLayers(preset.layers);
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
const canvasEl = useTemplateRef('canvasEl');
|
||||||
|
|
||||||
|
const sampleImage_3_2 = new Image();
|
||||||
|
sampleImage_3_2.src = '/client-assets/sample/3-2.jpg';
|
||||||
|
const sampleImage_3_2_loading = new Promise<void>(resolve => {
|
||||||
|
sampleImage_3_2.onload = () => resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const sampleImage_2_3 = new Image();
|
||||||
|
sampleImage_2_3.src = '/client-assets/sample/2-3.jpg';
|
||||||
|
const sampleImage_2_3_loading = new Promise<void>(resolve => {
|
||||||
|
sampleImage_2_3.onload = () => resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const sampleImageType = ref(props.image != null ? 'provided' : '3_2');
|
||||||
|
watch(sampleImageType, async () => {
|
||||||
|
if (renderer != null) {
|
||||||
|
renderer.destroy(false);
|
||||||
|
renderer = null;
|
||||||
|
initRenderer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let renderer: WatermarkRenderer | null = null;
|
||||||
|
let imageBitmap: ImageBitmap | null = null;
|
||||||
|
|
||||||
|
async function initRenderer() {
|
||||||
|
if (canvasEl.value == null) return;
|
||||||
|
|
||||||
|
if (sampleImageType.value === '3_2') {
|
||||||
|
renderer = new WatermarkRenderer({
|
||||||
|
canvas: canvasEl.value,
|
||||||
|
renderWidth: 1500,
|
||||||
|
renderHeight: 1000,
|
||||||
|
image: sampleImage_3_2,
|
||||||
|
});
|
||||||
|
} else if (sampleImageType.value === '2_3') {
|
||||||
|
renderer = new WatermarkRenderer({
|
||||||
|
canvas: canvasEl.value,
|
||||||
|
renderWidth: 1000,
|
||||||
|
renderHeight: 1500,
|
||||||
|
image: sampleImage_2_3,
|
||||||
|
});
|
||||||
|
} else if (props.image != null) {
|
||||||
|
imageBitmap = await window.createImageBitmap(props.image);
|
||||||
|
|
||||||
|
const MAX_W = 1000;
|
||||||
|
const MAX_H = 1000;
|
||||||
|
let w = imageBitmap.width;
|
||||||
|
let h = imageBitmap.height;
|
||||||
|
|
||||||
|
if (w > MAX_W || h > MAX_H) {
|
||||||
|
const scale = Math.min(MAX_W / w, MAX_H / h);
|
||||||
|
w *= scale;
|
||||||
|
h *= scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer = new WatermarkRenderer({
|
||||||
|
canvas: canvasEl.value,
|
||||||
|
renderWidth: w,
|
||||||
|
renderHeight: h,
|
||||||
|
image: imageBitmap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderer!.setLayers(preset.layers);
|
||||||
|
|
||||||
|
renderer!.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const closeWaiting = os.waiting();
|
||||||
|
|
||||||
|
await nextTick(); // waitingがレンダリングされるまで待つ
|
||||||
|
|
||||||
|
await sampleImage_3_2_loading;
|
||||||
|
await sampleImage_2_3_loading;
|
||||||
|
|
||||||
|
await initRenderer();
|
||||||
|
|
||||||
|
closeWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (renderer != null) {
|
||||||
|
renderer.destroy();
|
||||||
|
renderer = null;
|
||||||
|
}
|
||||||
|
if (imageBitmap != null) {
|
||||||
|
imageBitmap.close();
|
||||||
|
imageBitmap = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const { canceled, result: name } = await os.inputText({
|
||||||
|
title: i18n.ts.name,
|
||||||
|
default: preset.name,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
preset.name = name || '';
|
||||||
|
|
||||||
|
dialog.value?.close();
|
||||||
|
if (renderer != null) {
|
||||||
|
renderer.destroy();
|
||||||
|
renderer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('ok', preset);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLayer(ev: MouseEvent) {
|
||||||
|
os.popupMenu([{
|
||||||
|
text: i18n.ts._watermarkEditor.text,
|
||||||
|
action: () => {
|
||||||
|
preset.layers.push(createTextLayer());
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts._watermarkEditor.image,
|
||||||
|
action: () => {
|
||||||
|
preset.layers.push(createImageLayer());
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts._watermarkEditor.stripe,
|
||||||
|
action: () => {
|
||||||
|
preset.layers.push(createStripeLayer());
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts._watermarkEditor.polkadot,
|
||||||
|
action: () => {
|
||||||
|
preset.layers.push(createPolkadotLayer());
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
text: i18n.ts._watermarkEditor.checker,
|
||||||
|
action: () => {
|
||||||
|
preset.layers.push(createCheckerLayer());
|
||||||
|
},
|
||||||
|
}], ev.currentTarget ?? ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function swapUpLayer(layer: WatermarkPreset['layers'][number]) {
|
||||||
|
const index = preset.layers.findIndex(l => l.id === layer.id);
|
||||||
|
if (index > 0) {
|
||||||
|
const tmp = preset.layers[index - 1];
|
||||||
|
preset.layers[index - 1] = preset.layers[index];
|
||||||
|
preset.layers[index] = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function swapDownLayer(layer: WatermarkPreset['layers'][number]) {
|
||||||
|
const index = preset.layers.findIndex(l => l.id === layer.id);
|
||||||
|
if (index < preset.layers.length - 1) {
|
||||||
|
const tmp = preset.layers[index + 1];
|
||||||
|
preset.layers[index + 1] = preset.layers[index];
|
||||||
|
preset.layers[index] = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLayer(layer: WatermarkPreset['layers'][number]) {
|
||||||
|
preset.layers = preset.layers.filter(l => l.id !== layer.id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.root {
|
||||||
|
container-type: inline-size;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--MI_THEME-bg);
|
||||||
|
background-size: auto auto;
|
||||||
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewTitle {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewControls {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewControlsButton {
|
||||||
|
&.active {
|
||||||
|
color: var(--MI_THEME-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewSpinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewCanvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 800px) {
|
||||||
|
.container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -51,7 +51,7 @@ export type DefaultStoredWidget = {
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, ref, computed } from 'vue';
|
import { defineAsyncComponent, ref, computed } from 'vue';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { genId } from '@/utility/id.js';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
|
import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
|
||||||
|
@ -95,7 +95,7 @@ const addWidget = () => {
|
||||||
|
|
||||||
emit('addWidget', {
|
emit('addWidget', {
|
||||||
name: widgetAdderSelected.value,
|
name: widgetAdderSelected.value,
|
||||||
id: uuid(),
|
id: genId(),
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="!store.r.tips.value[props.k]" :class="[$style.root, { [$style.warn]: warn }]" class="_selectable _gaps_s">
|
<div v-if="!store.r.tips.value[props.k]" :class="[$style.root, { [$style.warn]: warn }]" class="_selectable _gaps_s">
|
||||||
<div style="font-weight: bold;"><i class="ti ti-bulb"></i> {{ i18n.ts.tip }}:</div>
|
<div style="font-weight: bold;"><i class="ti ti-bulb"></i> {{ i18n.ts.tip }}:</div>
|
||||||
<div><slot></slot></div>
|
<div><slot></slot></div>
|
||||||
<MkButton primary rounded small @click="closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
<div>
|
||||||
|
<MkButton inline primary rounded small @click="_closeTip()"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||||
|
<button class="_button" style="padding: 8px; margin-left: 4px;" @click="showMenu"><i class="ti ti-dots"></i></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -15,19 +18,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { store } from '@/store.js';
|
import { store } from '@/store.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
import { TIPS, hideAllTips, closeTip } from '@/tips.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
k: keyof (typeof store['s']['tips']);
|
k: typeof TIPS[number];
|
||||||
warn?: boolean;
|
warn?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
warn: false,
|
warn: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
function closeTip() {
|
function _closeTip() {
|
||||||
store.set('tips', {
|
closeTip(props.k);
|
||||||
...store.r.tips.value,
|
}
|
||||||
[props.k]: true,
|
|
||||||
});
|
function showMenu(ev: MouseEvent) {
|
||||||
|
os.popupMenu([{
|
||||||
|
icon: 'ti ti-bulb-off',
|
||||||
|
text: i18n.ts.hideAllTips,
|
||||||
|
danger: true,
|
||||||
|
action: () => {
|
||||||
|
hideAllTips();
|
||||||
|
os.success();
|
||||||
|
},
|
||||||
|
}], ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
|
|
||||||
import { notificationTypes } from 'misskey-js';
|
import { notificationTypes } from 'misskey-js';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { i18n } from './i18n.js';
|
import { i18n } from './i18n.js';
|
||||||
import type { BasicTimelineType } from '@/timelines.js';
|
import type { BasicTimelineType } from '@/timelines.js';
|
||||||
import type { SoundStore } from '@/preferences/def.js';
|
import type { SoundStore } from '@/preferences/def.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
@ -103,7 +103,7 @@ function addProfile(name: string) {
|
||||||
if (prefer.s['deck.profiles'].find(p => p.name === name)) return;
|
if (prefer.s['deck.profiles'].find(p => p.name === name)) return;
|
||||||
|
|
||||||
const newProfile: DeckProfile = {
|
const newProfile: DeckProfile = {
|
||||||
id: uuid(),
|
id: genId(),
|
||||||
name,
|
name,
|
||||||
columns: [],
|
columns: [],
|
||||||
layout: [],
|
layout: [],
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { genId } from '@/utility/id.js';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -104,7 +104,7 @@ const type = computed({
|
||||||
set: (t) => {
|
set: (t) => {
|
||||||
if (t === 'and') v.value.values = [];
|
if (t === 'and') v.value.values = [];
|
||||||
if (t === 'or') v.value.values = [];
|
if (t === 'or') v.value.values = [];
|
||||||
if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' };
|
if (t === 'not') v.value.value = { id: genId(), type: 'isRemote' };
|
||||||
if (t === 'roleAssignedTo') v.value.roleId = '';
|
if (t === 'roleAssignedTo') v.value.roleId = '';
|
||||||
if (t === 'createdLessThan') v.value.sec = 86400;
|
if (t === 'createdLessThan') v.value.sec = 86400;
|
||||||
if (t === 'createdMoreThan') v.value.sec = 86400;
|
if (t === 'createdMoreThan') v.value.sec = 86400;
|
||||||
|
@ -119,7 +119,7 @@ const type = computed({
|
||||||
});
|
});
|
||||||
|
|
||||||
function addValue() {
|
function addValue() {
|
||||||
v.value.values.push({ id: uuid(), type: 'isRemote' });
|
v.value.values.push({ id: genId(), type: 'isRemote' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function valuesItemUpdated(item) {
|
function valuesItemUpdated(item) {
|
||||||
|
|
|
@ -97,6 +97,7 @@ import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const announcementsStatus = ref<'active' | 'archived'>('active');
|
const announcementsStatus = ref<'active' | 'archived'>('active');
|
||||||
|
|
||||||
|
@ -117,7 +118,7 @@ watch(announcementsStatus, (to) => {
|
||||||
|
|
||||||
function add() {
|
function add() {
|
||||||
announcements.value.unshift({
|
announcements.value.unshift({
|
||||||
_id: Math.random().toString(36),
|
_id: genId(),
|
||||||
id: null,
|
id: null,
|
||||||
title: 'New announcement',
|
title: 'New announcement',
|
||||||
text: '',
|
text: '',
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const connection = markRaw(useStream().useChannel('queueStats'));
|
const connection = markRaw(useStream().useChannel('queueStats'));
|
||||||
|
|
||||||
|
@ -113,7 +114,7 @@ onMounted(() => {
|
||||||
connection.on('stats', onStats);
|
connection.on('stats', onStats);
|
||||||
connection.on('statsLog', onStatsLog);
|
connection.on('statsLog', onStatsLog);
|
||||||
connection.send('requestLog', {
|
connection.send('requestLog', {
|
||||||
id: Math.random().toString().substring(2, 10),
|
id: genId(),
|
||||||
length: 200,
|
length: 200,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,6 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
||||||
|
@ -41,12 +42,13 @@ import * as os from '@/os.js';
|
||||||
import { lookupFile } from '@/utility/admin-lookup.js';
|
import { lookupFile } from '@/utility/admin-lookup.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
import type { PagingCtx } from '@/composables/use-pagination.js';
|
||||||
|
|
||||||
const origin = ref('local');
|
const origin = ref<Misskey.entities.AdminDriveFilesRequest['origin']>('local');
|
||||||
const type = ref<string | null>(null);
|
const type = ref<string | null>(null);
|
||||||
const searchHost = ref('');
|
const searchHost = ref('');
|
||||||
const userId = ref('');
|
const userId = ref('');
|
||||||
const viewMode = ref('grid');
|
const viewMode = ref<'grid' | 'list'>('grid');
|
||||||
const pagination = {
|
const pagination = {
|
||||||
endpoint: 'admin/drive/files' as const,
|
endpoint: 'admin/drive/files' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
@ -56,7 +58,7 @@ const pagination = {
|
||||||
origin: origin.value,
|
origin: origin.value,
|
||||||
hostname: (searchHost.value && searchHost.value !== '') ? searchHost.value : null,
|
hostname: (searchHost.value && searchHost.value !== '') ? searchHost.value : null,
|
||||||
})),
|
})),
|
||||||
};
|
} satisfies PagingCtx<'admin/drive/files'>;
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
os.confirm({
|
os.confirm({
|
||||||
|
|
|
@ -41,6 +41,7 @@ import XChart from './overview.queue.chart.vue';
|
||||||
import type { ApQueueDomain } from '@/pages/admin/queue.vue';
|
import type { ApQueueDomain } from '@/pages/admin/queue.vue';
|
||||||
import number from '@/filters/number.js';
|
import number from '@/filters/number.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const connection = markRaw(useStream().useChannel('queueStats'));
|
const connection = markRaw(useStream().useChannel('queueStats'));
|
||||||
|
|
||||||
|
@ -92,7 +93,7 @@ onMounted(() => {
|
||||||
connection.on('stats', onStats);
|
connection.on('stats', onStats);
|
||||||
connection.on('statsLog', onStatsLog);
|
connection.on('statsLog', onStatsLog);
|
||||||
connection.send('requestLog', {
|
connection.send('requestLog', {
|
||||||
id: Math.random().toString().substring(2, 10),
|
id: genId(),
|
||||||
length: 100,
|
length: 100,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -84,6 +84,7 @@ import { useStream } from '@/stream.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const rootEl = useTemplateRef('rootEl');
|
const rootEl = useTemplateRef('rootEl');
|
||||||
const serverInfo = ref<Misskey.entities.ServerInfoResponse | null>(null);
|
const serverInfo = ref<Misskey.entities.ServerInfoResponse | null>(null);
|
||||||
|
@ -170,7 +171,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
queueStatsConnection.send('requestLog', {
|
queueStatsConnection.send('requestLog', {
|
||||||
id: Math.random().toString().substring(2, 10),
|
id: genId(),
|
||||||
length: 100,
|
length: 100,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { genId } from '@/utility/id.js';
|
||||||
import XEditor from './roles.editor.vue';
|
import XEditor from './roles.editor.vue';
|
||||||
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';
|
||||||
|
@ -55,7 +55,7 @@ if (props.id) {
|
||||||
color: null,
|
color: null,
|
||||||
iconUrl: null,
|
iconUrl: null,
|
||||||
target: 'manual',
|
target: 'manual',
|
||||||
condFormula: { id: uuid(), type: 'isRemote' },
|
condFormula: { id: genId(), type: 'isRemote' },
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
isExplorable: false,
|
isExplorable: false,
|
||||||
asBadge: false,
|
asBadge: false,
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
|
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :swipable="true">
|
||||||
<div v-if="instance" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
|
<div v-if="instance" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
|
||||||
<div v-if="tab === 'overview'" class="_gaps_m">
|
<div v-if="tab === 'overview'" class="_gaps_m">
|
||||||
<div class="fnfelxur">
|
<div :class="$style.faviconAndName">
|
||||||
<img :src="faviconUrl" alt="" class="icon"/>
|
<img :src="faviconUrl" alt="" :class="$style.icon"/>
|
||||||
<span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
|
<span :class="$style.name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||||
<MkKeyValue :copy="host" oneline>
|
<MkKeyValue :copy="host" oneline>
|
||||||
|
@ -90,8 +90,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'chart'" class="_gaps_m">
|
<div v-else-if="tab === 'chart'" class="_gaps_m">
|
||||||
<div class="cmhjzshl">
|
<div>
|
||||||
<div class="selects">
|
<div :class="$style.selects">
|
||||||
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
|
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
|
||||||
<option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option>
|
<option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option>
|
||||||
<option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option>
|
<option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option>
|
||||||
|
@ -106,19 +106,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option>
|
<option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
<div class="charts">
|
<div>
|
||||||
<div class="label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div>
|
<div :class="$style.label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div>
|
||||||
<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
|
<MkChart :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
|
||||||
<div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div>
|
<div :class="$style.label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div>
|
||||||
<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
|
<MkChart :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'users'" class="_gaps_m">
|
<div v-else-if="tab === 'users'" class="_gaps_m">
|
||||||
<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
|
<MkPagination v-slot="{ items }" :pagination="usersPagination">
|
||||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/admin/user/${user.id}`">
|
<div :class="$style.users">
|
||||||
<MkUserCardMini :user="user"/>
|
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${user.updatedAt ? dateString(user.updatedAt) : 'unknown'}`" :to="`/admin/user/${user.id}`">
|
||||||
</MkA>
|
<MkUserCardMini :user="user"/>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'raw'" class="_gaps_m">
|
<div v-else-if="tab === 'raw'" class="_gaps_m">
|
||||||
|
@ -180,7 +182,7 @@ const usersPagination = {
|
||||||
hostname: props.host,
|
hostname: props.host,
|
||||||
},
|
},
|
||||||
offsetMode: true,
|
offsetMode: true,
|
||||||
} satisfies PagingCtx;
|
} satisfies PagingCtx<'admin/show-users' | 'users'>;
|
||||||
|
|
||||||
if (iAmModerator) {
|
if (iAmModerator) {
|
||||||
watch(moderationNote, async () => {
|
watch(moderationNote, async () => {
|
||||||
|
@ -281,7 +283,7 @@ const headerTabs = computed(() => [{
|
||||||
key: 'overview',
|
key: 'overview',
|
||||||
title: i18n.ts.overview,
|
title: i18n.ts.overview,
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-info-circle',
|
||||||
}, {
|
}, ...(iAmModerator ? [{
|
||||||
key: 'chart',
|
key: 'chart',
|
||||||
title: i18n.ts.charts,
|
title: i18n.ts.charts,
|
||||||
icon: 'ti ti-chart-line',
|
icon: 'ti ti-chart-line',
|
||||||
|
@ -289,7 +291,7 @@ const headerTabs = computed(() => [{
|
||||||
key: 'users',
|
key: 'users',
|
||||||
title: i18n.ts.users,
|
title: i18n.ts.users,
|
||||||
icon: 'ti ti-users',
|
icon: 'ti ti-users',
|
||||||
}, {
|
}] : []), {
|
||||||
key: 'raw',
|
key: 'raw',
|
||||||
title: 'Raw',
|
title: 'Raw',
|
||||||
icon: 'ti ti-code',
|
icon: 'ti ti-code',
|
||||||
|
@ -301,34 +303,31 @@ definePage(() => ({
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.fnfelxur {
|
.faviconAndName {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
> .icon {
|
|
||||||
display: block;
|
|
||||||
margin: 0 16px 0 0;
|
|
||||||
height: 64px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .name {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.icon {
|
||||||
.cmhjzshl {
|
display: block;
|
||||||
> .selects {
|
margin: 0 16px 0 0;
|
||||||
display: flex;
|
height: 64px;
|
||||||
margin: 0 0 16px 0;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
.name {
|
||||||
> .charts {
|
word-break: break-all;
|
||||||
> .label {
|
}
|
||||||
margin-bottom: 12px;
|
.selects {
|
||||||
font-weight: bold;
|
display: flex;
|
||||||
}
|
margin: 0 0 16px 0;
|
||||||
}
|
}
|
||||||
|
.label {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.users {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill,minmax(270px,1fr));
|
||||||
|
grid-gap: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
|
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { genId } from '@/utility/id.js';
|
||||||
import XContainer from '../page-editor.container.vue';
|
import XContainer from '../page-editor.container.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -73,7 +73,7 @@ async function add() {
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
const id = uuid();
|
const id = genId();
|
||||||
children.value.push({ id, type });
|
children.value.push({ id, type });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, provide, watch, ref } from 'vue';
|
import { computed, provide, watch, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { genId } from '@/utility/id.js';
|
||||||
import { url } from '@@/js/config.js';
|
import { url } from '@@/js/config.js';
|
||||||
import XBlocks from './page-editor.blocks.vue';
|
import XBlocks from './page-editor.blocks.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -200,12 +200,15 @@ async function add() {
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
const id = uuid();
|
const id = genId();
|
||||||
content.value.push({ id, type });
|
content.value.push({ id, type });
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -240,7 +243,7 @@ async function init() {
|
||||||
content.value = page.value.content;
|
content.value = page.value.content;
|
||||||
eyeCatchingImageId.value = page.value.eyeCatchingImageId;
|
eyeCatchingImageId.value = page.value.eyeCatchingImageId;
|
||||||
} else {
|
} else {
|
||||||
const id = uuid();
|
const id = genId();
|
||||||
content.value = [{
|
content.value = [{
|
||||||
id,
|
id,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|
|
@ -158,6 +158,7 @@ import { userPage } from '@/filters/user.js';
|
||||||
import * as sound from '@/utility/sound.js';
|
import * as sound from '@/utility/sound.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { confetti } from '@/utility/confetti.js';
|
import { confetti } from '@/utility/confetti.js';
|
||||||
|
import { genId } from '@/utility/id.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
@ -273,7 +274,7 @@ function putStone(pos: number) {
|
||||||
playbackRate: 1,
|
playbackRate: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const id = Math.random().toString(36).slice(2);
|
const id = genId();
|
||||||
props.connection!.send('putStone', {
|
props.connection!.send('putStone', {
|
||||||
pos: pos,
|
pos: pos,
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -40,8 +40,8 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/router.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
query?: string,
|
query?: string,
|
||||||
origin?: Endpoints['users/search']['req']['origin'],
|
origin?: Endpoints['users/search']['req']['origin'],
|
||||||
}>(), {
|
}>(), {
|
||||||
query: '',
|
query: '',
|
||||||
origin: 'combined',
|
origin: 'combined',
|
||||||
|
@ -115,6 +115,7 @@ async function search() {
|
||||||
userPagination.value = {
|
userPagination.value = {
|
||||||
endpoint: 'users/search',
|
endpoint: 'users/search',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
offsetMode: true,
|
||||||
params: {
|
params: {
|
||||||
query: query,
|
query: query,
|
||||||
origin: instance.federation === 'none' ? 'local' : searchOrigin.value,
|
origin: instance.federation === 'none' ? 'local' : searchOrigin.value,
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue