Merge pull request #16629 from misskey-dev/develop

Release: 2025.10.1
This commit is contained in:
misskey-release-bot[bot] 2025-10-24 06:31:35 +00:00 committed by GitHub
commit b4e16c83e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
145 changed files with 4784 additions and 4379 deletions

View File

@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.3.0
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Setup Node.js
uses: actions/setup-node@v4.4.0

View File

@ -30,7 +30,7 @@ jobs:
ref: ${{ matrix.ref }}
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:

View File

@ -41,7 +41,7 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'
@ -74,7 +74,7 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'
@ -104,7 +104,7 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'

View File

@ -19,7 +19,7 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'

View File

@ -20,7 +20,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:

View File

@ -0,0 +1,50 @@
name: Request release review
on:
issue_comment:
types: [created]
jobs:
reply:
if: github.event.comment.body == '/request-release-review'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Reply
uses: actions/github-script@v6
with:
script: |
const body = `To dev team (@misskey-dev/dev):
リリースが提案されています :rocket:
GOの場合はapprove、NO GOの場合はその旨コメントをお願いいたします。
判断にあたって考慮すべき観点は、
- やり残したことはないか?
- CHANGELOGは過不足ないか
- バージョンに問題はないか?(月跨いでいるのに更新忘れているなど)
- 再考すべき仕様・実装はないか?
- ベータ版を検証したサーバーから不具合の報告等は上がってないか?
- (セキュリティの修正や重要なバグ修正などのため)リリースを急いだ方が良いか?そうではないか?
- Actionsが落ちていないか
などが挙げられます。
ご協力ありがとうございます :sparkles:
`
const issue_number = context.payload.issue ? context.payload.issue.number : (context.payload.pull_request && context.payload.pull_request.number)
if (!issue_number) {
console.log('No issue or PR number found in payload; skipping')
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
body,
})
}

View File

@ -37,7 +37,7 @@ jobs:
if: github.event_name == 'pull_request_target'
run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)"
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:

View File

@ -54,7 +54,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
@ -133,7 +133,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
@ -177,7 +177,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT

View File

@ -36,7 +36,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT

View File

@ -32,7 +32,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
@ -86,7 +86,7 @@ jobs:
#- uses: browser-actions/setup-firefox@latest
# if: ${{ matrix.browser == 'firefox' }}
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:

View File

@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@v4.3.0
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Setup Node.js
uses: actions/setup-node@v4.4.0

View File

@ -20,7 +20,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:

View File

@ -21,7 +21,7 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
uses: pnpm/action-setup@v4.2.0
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:

View File

@ -1,3 +1,31 @@
## 2025.10.1
### General
- Enhance: リモートユーザーに付与したロールバッジを表示できるように(オプトイン)
パフォーマンス上の問題からデフォルトで無効化されています。「コントロールパネル > パフォーマンス」から有効化できます。
- 依存関係の更新
### Client
- Enhance: デッキのメインカラムのヘッダをクリックしてページ上部/下部にスクロールできるように
- Enhance: 下書き/予約投稿一覧は投稿フォームのアカウントメニュー内に移動し、下書き保存は「...」メニュー内に移動されました
- Fix: カスタム絵文字画面(beta)のaliasesで使用される区切り文字が一致していないのを修正 #15614
- Fix: バナー画像の幅が表示領域と一致していない問題を修正
- Fix: 一部のブラウザでバナー画像が上下中央に表示されない問題を修正
- Fix: ナビゲーションバーの設定で削除した項目をその場で再追加できない問題を修正
- Fix: ロールポリシーによりダイレクトメッセージが無効化されている際のデッキのダイレクトメッセージカラムの挙動を改善
- Fix: 画像のマスクでタッチ操作が不安定な問題を修正
- Fix: ウォーターマークの各種挙動修正
- ウォーターマークを回転させると歪む問題を修正
- ウォーターマークを敷き詰めると上下左右反転した画像/文字が表示される問題を修正
- ウォーターマークを回転させた際に画面からはみ出た部分を考慮できるように
- Fix: 投票が終了した後に投票結果が正しく表示されない問題を修正
- Fix: ダークモードの同期が機能しない場合がある問題を修正
- Fix: iOSで動画の圧縮を行うと音声トラックが失われる問題を修正
### Server
- Enhance: 管理者/モデレーターはファイルのアップロード制限をバイパスするように
- Enhance: セキュリティの向上
## 2025.10.0
### NOTE

View File

@ -651,7 +651,7 @@ disablePlayer: "Tanca el reproductor de vídeo"
expandTweet: "Expandir post"
themeEditor: "Editor de temes"
description: "Descripció"
describeFile: "Afegeix una descripció "
describeFile: "Afegir text alternatiu"
enterFileDescription: "Escriu un peu de foto"
author: "Autor"
leaveConfirm: "Hi ha canvis sense guardar. Els vols descartar?"
@ -775,6 +775,7 @@ lockedAccountInfo: "Tret que establiu la visibilitat de la nota a \"Només segui
alwaysMarkSensitive: "Marcar com a sensible per defecte"
loadRawImages: "Carregar les imatges originals en comptes de miniatures "
disableShowingAnimatedImages: "No reproduir imatges animades"
disableShowingAnimatedImages_caption: "Si les imatges animades no es reprodueixen, independentment d'aquesta configuració, és possible que la configuració d'accessibilitat del navegador i el sistema operatiu, els modes d'estalvi d'energia i similars estiguin interferint."
highlightSensitiveMedia: "Ressalta els medis marcats com a sensibles"
verificationEmailSent: "S'ha enviat un correu electrònic de verificació. Fes clic a l'enllaç per completar la verificació."
notSet: "Sense definir"
@ -1171,6 +1172,7 @@ installed: "Instal·lats "
branding: "Marca"
enableServerMachineStats: "Publicar estadístiques del maquinari del servidor"
enableIdenticonGeneration: "Activar la generació d'icones d'identificació "
showRoleBadgesOfRemoteUsers: "Mostrar insígnies de rols d'instàncies remotes "
turnOffToImprovePerformance: "Desactivant aquesta opció es pot millorar el rendiment."
createInviteCode: "Crear codi d'invitació "
createWithOptions: "Crear invitació amb opcions"
@ -3199,6 +3201,7 @@ _watermarkEditor:
title: "Editar la marca d'aigua "
cover: "Cobrir-ho tot"
repeat: "Repetir"
preserveBoundingRect: "Ajusta'l per evitar que sobresortir en fer la rotació "
opacity: "Opacitat"
scale: "Mida"
text: "Text"
@ -3268,7 +3271,7 @@ _imageEffector:
frequency: "Freqüència "
strength: "Intensitat"
glitchChannelShift: "Canvi de canal "
seed: "Llindar"
seed: "Llavors"
redComponent: "Component vermell"
greenComponent: "Component verd"
blueComponent: "Component blau"

View File

@ -334,6 +334,7 @@ fileName: "Filename"
selectFile: "Select a file"
selectFiles: "Select files"
selectFolder: "Select a folder"
unselectFolder: "Deselect folder"
selectFolders: "Select folders"
fileNotSelected: "No file selected"
renameFile: "Rename file"
@ -346,6 +347,7 @@ addFile: "Add a file"
showFile: "Show files"
emptyDrive: "Your Drive is empty"
emptyFolder: "This folder is empty"
dropHereToUpload: "Drop files here to upload"
unableToDelete: "Unable to delete"
inputNewFileName: "Enter a new filename"
inputNewDescription: "Enter new alt text"
@ -773,6 +775,7 @@ lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", yo
alwaysMarkSensitive: "Mark as sensitive by default"
loadRawImages: "Load original images instead of showing thumbnails"
disableShowingAnimatedImages: "Don't play animated images"
disableShowingAnimatedImages_caption: "If animated images do not play even if this setting is disabled, it may be due to browser or OS accessibility settings, power-saving settings, or similar factors."
highlightSensitiveMedia: "Highlight sensitive media"
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
notSet: "Not set"
@ -1169,6 +1172,7 @@ installed: "Installed"
branding: "Branding"
enableServerMachineStats: "Publish server hardware stats"
enableIdenticonGeneration: "Enable user identicon generation"
showRoleBadgesOfRemoteUsers: "Display the role badges assigned to remote users"
turnOffToImprovePerformance: "Turning this off can increase performance."
createInviteCode: "Generate invite"
createWithOptions: "Generate with options"
@ -1390,6 +1394,8 @@ scheduledToPostOnX: "Note is scheduled for {x}"
schedule: "Schedule"
scheduled: "Scheduled"
widgets: "Widgets"
deviceInfo: "Device information"
deviceInfoDescription: "When making technical inquiries, including the following information may help resolve the issue."
_compression:
_quality:
high: "High quality"
@ -2014,6 +2020,7 @@ _role:
canManageAvatarDecorations: "Manage avatar decorations"
driveCapacity: "Drive capacity"
maxFileSize: "Upload-able max file size"
maxFileSize_caption: "Reverse proxies, CDNs, and other front-end components may have their own configuration settings."
alwaysMarkNsfw: "Always mark files as NSFW"
canUpdateBioMedia: "Can edit an icon or a banner image"
pinMax: "Maximum number of pinned notes"
@ -2432,6 +2439,7 @@ _auth:
scopeUser: "Operate as the following user"
pleaseLogin: "Please log in to authorize applications."
byClickingYouWillBeRedirectedToThisUrl: "When access is granted, you will automatically be redirected to the following URL"
alreadyAuthorized: "This application already has access permission."
_antennaSources:
all: "All notes"
homeTimeline: "Notes from followed users"
@ -2698,6 +2706,8 @@ _notification:
quote: "Quotes"
reaction: "Reactions"
pollEnded: "Polls ending"
scheduledNotePosted: "Scheduled note was successful"
scheduledNotePostFailed: "Scheduled note failed"
receiveFollowRequest: "Received follow requests"
followRequestAccepted: "Accepted follow requests"
roleAssigned: "Role given"
@ -3191,6 +3201,7 @@ _watermarkEditor:
title: "Edit Watermark"
cover: "Cover everything"
repeat: "spread all over"
preserveBoundingRect: "Adjust to prevent overflow when rotating"
opacity: "Opacity"
scale: "Size"
text: "Text"

View File

@ -9,7 +9,7 @@ reset: "Reiniciar"
notifications: "Notificaciones"
username: "Nombre de usuario"
password: "Contraseña"
initialPasswordForSetup: "Contraseña para iniciar la inicialización"
initialPasswordForSetup: "Contraseña de configuración inicial"
initialPasswordIsIncorrect: "La contraseña para iniciar la configuración inicial es incorrecta."
initialPasswordForSetupDescription: "Si ha instalado Misskey usted mismo, utilice la contraseña introducida en el archivo de configuración.\nSi utiliza un servicio de alojamiento de Misskey o similar, utilice la contraseña proporcionada.\nSi no ha establecido una contraseña, déjela en blanco para continuar."
forgotPassword: "Olvidé mi contraseña"
@ -651,7 +651,7 @@ disablePlayer: "Cerrar reproductor"
expandTweet: "Expandir tweet"
themeEditor: "Editor de temas"
description: "Descripción"
describeFile: "Añade una descripción"
describeFile: "Añadir texto alternativo"
enterFileDescription: "Introducir un título"
author: "Autor"
leaveConfirm: "Hay modificaciones sin guardar. ¿Desea descartarlas?"
@ -775,6 +775,7 @@ lockedAccountInfo: "A menos que configures la visibilidad de tus notas como \"S
alwaysMarkSensitive: "Marcar los medios de comunicación como contenido sensible por defecto"
loadRawImages: "Cargar las imágenes originales en lugar de mostrar las miniaturas"
disableShowingAnimatedImages: "No reproducir imágenes animadas"
disableShowingAnimatedImages_caption: "Si las imágenes animadas no se reproducen independientemente de esta configuración, es posible que la configuración de accesibilidad del navegador o del sistema operativo, los modos de ahorro de energía o funciones similares estén interfiriendo."
highlightSensitiveMedia: "Resaltar medios marcados como sensibles"
verificationEmailSent: "Se le ha enviado un correo electrónico de confirmación. Por favor, acceda al enlace proporcionado en el correo electrónico para completar la configuración."
notSet: "Sin especificar"
@ -965,7 +966,7 @@ threeDays: "Tres días"
reflectMayTakeTime: "Puede pasar un tiempo hasta que se reflejen los cambios"
failedToFetchAccountInformation: "No se pudo obtener información de la cuenta"
rateLimitExceeded: "Se excedió el límite de peticiones"
cropImage: "Recortar imágen"
cropImage: "Recortar Imagen"
cropImageAsk: "¿Desea recortar la imagen?"
cropYes: "Recortar"
cropNo: "Usar como está"
@ -1024,7 +1025,7 @@ sendPushNotificationReadMessageCaption: "La notificación \"{emptyPushNotificati
windowMaximize: "Maximizar"
windowMinimize: "Minimizar"
windowRestore: "Regresar"
caption: "Pie de foto"
caption: "Texto alternativo"
loggedInAsBot: "Inicio sesión como cuenta bot."
tools: "Utilidades"
cannotLoad: "No se puede cargar."
@ -1171,6 +1172,7 @@ installed: "Instalado"
branding: "Marca"
enableServerMachineStats: "Publicar estadísticas de hardware del servidor"
enableIdenticonGeneration: "Activar generación de identicon por usuario"
showRoleBadgesOfRemoteUsers: "Mostrar la insignia de rol asignada a los usuarios remotos."
turnOffToImprovePerformance: "Desactivar esto puede aumentar el rendimiento."
createInviteCode: "Generar invitación"
createWithOptions: "Generar con opciones"
@ -1351,7 +1353,7 @@ directMessage: "Chatear"
directMessage_short: "Mensaje"
migrateOldSettings: "Migrar la configuración anterior"
migrateOldSettings_description: "Esto debería hacerse automáticamente, pero si por alguna razón la migración no ha tenido éxito, puede activar usted mismo el proceso de migración manualmente. Se sobrescribirá la información de configuración actual."
compress: "Comprimir"
compress: "Compresión de la imagen"
right: "Derecha"
bottom: "Abajo"
top: "Arriba"
@ -1385,7 +1387,7 @@ pluginsAreDisabledBecauseSafeMode: "El modo seguro está activado, por lo que to
customCssIsDisabledBecauseSafeMode: "El modo seguro está activado, por lo que no se aplica el CSS personalizado."
themeIsDefaultBecauseSafeMode: "Mientras el modo seguro esté activado, se utilizará el tema predeterminado. Cuando se desactive el modo seguro, se volverá al tema original."
thankYouForTestingBeta: "¡Gracias por tu colaboración en la prueba de la versión beta!"
createUserSpecifiedNote: "Crear notas especificadas por el usuario"
createUserSpecifiedNote: "Mencionar al usuario (Nota Directa)"
schedulePost: "Programar una nota"
scheduleToPostOnX: "Programar una nota para {x}"
scheduledToPostOnX: "La nota está programada para {x}."
@ -2524,7 +2526,7 @@ _visibility:
disableFederationDescription: "No enviar a otras instancias"
_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."
uploaderTip: "El archivo aún no se ha cargado. Desde el menú de archivos, puedes cambiar el nombre, recortar la imagen, añadir una marca de agua y configurar la compresión, entre otras opciones. Los archivos se suben automáticamente al publicar una nota."
replyPlaceholder: "Responder a esta nota"
quotePlaceholder: "Citar esta nota"
channelPlaceholder: "Publicar en el canal"
@ -3199,6 +3201,7 @@ _watermarkEditor:
title: "Editar la marca de agua"
cover: "Cubrir todo"
repeat: "Repetir"
preserveBoundingRect: "Ajuste para evitar que se desborde al rotar."
opacity: "Opacidad"
scale: "Tamaño"
text: "Texto"
@ -3212,8 +3215,8 @@ _watermarkEditor:
stripe: "Rayas"
stripeWidth: "Anchura de línea"
stripeFrequency: "Número de líneas."
polkadot: "Lunares"
checker: "verificador"
polkadot: "Patrón de Lunares"
checker: "Patrón de Damas / Tablero de Ajedrez"
polkadotMainDotOpacity: "Opacidad del círculo principal"
polkadotMainDotRadius: "Tamaño del círculo principal."
polkadotSubDotOpacity: "Opacidad del círculo secundario"
@ -3234,15 +3237,15 @@ _imageEffector:
blur: "Difuminar"
pixelate: "Pixelar"
colorAdjust: "Corrección de Color"
colorClamp: "Compresión cromática"
colorClampAdvanced: "Compresión cromática avanzada"
colorClamp: "Ajuste de Tono"
colorClampAdvanced: "Ajuste de Tono avanzado"
distort: "Distorsión"
threshold: "umbral"
zoomLines: "Saturación de Líneas"
threshold: "Binarización"
zoomLines: "Líneas de Impacto"
stripe: "Rayas"
polkadot: "Lunares"
checker: "Corrector"
blockNoise: "Bloquear Ruido"
polkadot: "Patrón de Lunares"
checker: "Patrón de Damas / Tablero de Ajedrez"
blockNoise: "Ruido de Bloque"
tearing: "Rasgado de Imagen (Tearing)"
fill: "Relleno de color"
_fxProps:
@ -3259,7 +3262,7 @@ _imageEffector:
lightness: "Brillo"
contrast: "Contraste"
hue: "Tonalidad"
brightness: "Brillo"
brightness: "Luminancia"
saturation: "Saturación"
max: "Valor máximo"
min: "Valor mínimo"
@ -3267,11 +3270,11 @@ _imageEffector:
phase: "Fase"
frequency: "Frecuencia"
strength: "Intensidad"
glitchChannelShift: "cambio de canal de imagen"
glitchChannelShift: "Desfase"
seed: "Valor de la semilla"
redComponent: "Componente rojo"
greenComponent: "Componente Verde"
blueComponent: "Componente Azul"
redComponent: "Canal Rojo"
greenComponent: "Canal Verde"
blueComponent: "Canal Azul"
threshold: "Umbral"
centerX: "Centrar X"
centerY: "Centrar Y"
@ -3279,7 +3282,7 @@ _imageEffector:
zoomLinesSmoothingDescription: "El suavizado y el ancho de línea de zoom no se pueden utilizar juntos."
zoomLinesThreshold: "Ancho de línea del zoom"
zoomLinesMaskSize: "Diámetro del centro"
zoomLinesBlack: "Hacer oscuro"
zoomLinesBlack: "Cambiar color de las líneas de impacto a negro."
circle: "Círculo"
drafts: "Borrador"
_drafts:

View File

@ -298,6 +298,7 @@ uploadFromUrlMayTakeTime: "Membutuhkan beberapa waktu hingga pengunggahan selesa
explore: "Jelajahi"
messageRead: "Telah dibaca"
noMoreHistory: "Tidak ada sejarah lagi"
startChat: "Kirim pesan"
nUsersRead: "Dibaca oleh {n}"
agreeTo: "Saya setuju kepada {0}"
agree: "Setuju"
@ -510,6 +511,7 @@ emojiStyle: "Gaya emoji"
native: "Native"
menuStyle: "Gaya menu"
style: "Gaya"
popup: "Pemunculan"
showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk"
showReactionsCount: "Lihat jumlah reaksi dalam catatan"
noHistory: "Tidak ada riwayat"
@ -566,6 +568,7 @@ showFixedPostForm: "Tampilkan form posting di atas lini masa"
showFixedPostFormInChannel: "Tampilkan form posting di atas lini masa (Kanal)"
withRepliesByDefaultForNewlyFollowed: "Termasuk balasan dari pengguna baru yang diikuti pada lini masa secara bawaan"
newNoteRecived: "Kamu mendapat catatan baru"
newNote: "Catatan baru"
sounds: "Bunyi"
sound: "Bunyi"
listen: "Dengarkan"
@ -1028,6 +1031,7 @@ permissionDeniedError: "Operasi ditolak"
permissionDeniedErrorDescription: "Akun ini tidak memiliki izin untuk melakukan aksi ini."
preset: "Prasetel"
selectFromPresets: "Pilih dari prasetel"
custom: "Penyesuaian"
achievements: "Pencapaian"
gotInvalidResponseError: "Respon peladen tidak valid"
gotInvalidResponseErrorDescription: "Peladen tidak dapat dijangkau atau sedang dalam perawatan. Mohon coba lagi nanti."
@ -1110,6 +1114,7 @@ preservedUsernamesDescription: "Daftar nama pengguna yang dicadangkan dipisah de
createNoteFromTheFile: "Buat catatan dari berkas ini"
archive: "Arsipkan"
archived: "Diarsipkan"
unarchive: "Batalkan pengarsipan"
channelArchiveConfirmTitle: "Yakin untuk mengarsipkan {name}?"
channelArchiveConfirmDescription: "Kanal yang diarsipkan tidak akan muncul pada daftar kanal atau hasil pencarian. Postingan baru juga tidak dapat ditambahkan lagi."
thisChannelArchived: "Kanal ini telah diarsipkan."
@ -1251,6 +1256,7 @@ noDescription: "Tidak ada deskripsi"
alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti"
inquiry: "Hubungi kami"
tryAgain: "Silahkan coba lagi."
sensitiveMediaRevealConfirm: "Media sensitif. Apakah ingin melihat?"
createdLists: "Senarai yang dibuat"
createdAntennas: "Antena yang dibuat"
fromX: "Dari {x}"
@ -1258,21 +1264,43 @@ noteOfThisUser: "Catatan oleh pengguna ini"
clipNoteLimitExceeded: "Klip ini tak bisa ditambahi lagi catatan."
performance: "Kinerja"
modified: "Diubah"
discard: "Buang"
thereAreNChanges: "Ada {n} perubahan"
signinWithPasskey: "Masuk dengan kunci sandi"
unknownWebAuthnKey: "Kunci sandi tidak terdaftar."
passkeyVerificationFailed: "Verifikasi kunci sandi gagal."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Verifikasi kunci sandi berhasil, namun pemasukan tanpa sandi dinonaktifkan."
messageToFollower: "Pesan kepada pengikut"
prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna"
lockdown: "Kuncitara"
noName: "Tidak ada nama"
skip: "Lewati"
paste: "Tempel"
emojiPalette: "Palet emoji"
postForm: "Buat catatan"
information: "Informasi"
chat: "Obrolan"
directMessage: "Obrolan pengguna"
right: "Kanan"
bottom: "Bawah"
top: "Atas"
advice: "Saran"
inMinutes: "menit"
inDays: "hari"
widgets: "Widget"
_chat:
invitations: "Undang"
history: "Riwayat obrolan"
noHistory: "Tidak ada riwayat"
members: "Anggota"
home: "Beranda"
send: "Kirim"
chatWithThisUser: "Obrolan pengguna"
_settings:
webhook: "Webhook"
contentsUpdateFrequency: "Frekuensi pembaruan konten"
_preferencesProfile:
profileName: "Nama profil"
_abuseUserReport:
accept: "Setuju"
reject: "Tolak"
@ -1966,6 +1994,7 @@ _sfx:
noteMy: "Catatan (Saya)"
notification: "Notifikasi"
reaction: "Ketika memilih reaksi"
chatMessage: "Obrolan pengguna"
_soundSettings:
driveFile: "Menggunakan berkas audio dalam Drive"
driveFileWarn: "Pilih berkas audio dari Drive"
@ -2168,6 +2197,7 @@ _widgets:
chooseList: "Pilih daftar"
clicker: "Pengeklik"
birthdayFollowings: "Pengguna yang merayakan hari ulang tahunnya hari ini"
chat: "Obrolan pengguna"
_cw:
hide: "Sembunyikan"
show: "Lihat konten"
@ -2416,6 +2446,7 @@ _deck:
mentions: "Sebutan"
direct: "Langsung"
roleTimeline: "Lini masa peran"
chat: "Obrolan pengguna"
_dialog:
charactersExceeded: "Kamu telah melebihi batas karakter maksimum! Saat ini pada {current} dari {max}."
charactersBelow: "Kamu berada di bawah batas minimum karakter! Saat ini pada {current} dari {min}."

12
locales/index.d.ts vendored
View File

@ -3118,6 +3118,10 @@ export interface Locale extends ILocale {
*
*/
"disableShowingAnimatedImages": string;
/**
* OSのアクセシビリティ設定や省電力設定等が干渉している場合があります
*/
"disableShowingAnimatedImages_caption": string;
/**
*
*/
@ -4702,6 +4706,10 @@ export interface Locale extends ILocale {
* Identicon生成を有効にする
*/
"enableIdenticonGeneration": string;
/**
*
*/
"showRoleBadgesOfRemoteUsers": string;
/**
*
*/
@ -12337,6 +12345,10 @@ export interface Locale extends ILocale {
*
*/
"repeat": string;
/**
* 調
*/
"preserveBoundingRect": string;
/**
*
*/

View File

@ -334,6 +334,7 @@ fileName: "Nome dell'allegato"
selectFile: "Scelta allegato"
selectFiles: "Scelta allegato"
selectFolder: "Seleziona cartella"
unselectFolder: "Deseleziona la cartella"
selectFolders: "Seleziona cartella"
fileNotSelected: "Nessun file selezionato"
renameFile: "Rinomina file"
@ -346,6 +347,7 @@ addFile: "Allega"
showFile: "Visualizza file"
emptyDrive: "Il Drive è vuoto"
emptyFolder: "La cartella è vuota"
dropHereToUpload: "Trascina qui il tuo file per caricarlo"
unableToDelete: "Eliminazione impossibile"
inputNewFileName: "Inserisci nome del nuovo file"
inputNewDescription: "Inserisci una nuova descrizione"
@ -456,7 +458,7 @@ setupOf2fa: "Impostare l'autenticazione a due fattori"
totp: "App di autenticazione a due fattori (2FA/MFA)"
totpDescription: "Puoi autenticarti inserendo un codice OTP tramite la tua App di autenticazione a due fattori (2FA/MFA)"
moderator: "Moderatore"
moderation: "moderazione"
moderation: "Moderazione"
moderationNote: "Promemoria di moderazione"
moderationNoteDescription: "Puoi scrivere promemoria condivisi solo tra moderatori."
addModerationNote: "Aggiungi promemoria di moderazione"
@ -497,7 +499,7 @@ attachAsFileQuestion: "Il testo copiato eccede le dimensioni, vuoi allegarlo?"
onlyOneFileCanBeAttached: "È possibile allegare al messaggio soltanto uno file"
signinRequired: "Occorre avere un profilo registrato su questa istanza"
signinOrContinueOnRemote: "Per continuare, devi accedere alla tua istanza o registrarti su questa e poi accedere"
invitations: "Invita"
invitations: "Inviti"
invitationCode: "Codice di invito"
checking: "Confermando"
available: "Disponibile"
@ -773,6 +775,7 @@ lockedAccountInfo: "A meno che non imposti la visibilità delle tue note su \"So
alwaysMarkSensitive: "Segnare automaticamente come espliciti gli allegati"
loadRawImages: "Visualizza le intere immagini allegate invece delle miniature."
disableShowingAnimatedImages: "Disabilitare le immagini animate"
disableShowingAnimatedImages_caption: "L'attivazione delle animazioni immagini potrebbe interferire sull'accessibilità e sul risparmio energetico nel dispositivo."
highlightSensitiveMedia: "Evidenzia i media espliciti"
verificationEmailSent: "Una mail di verifica è stata inviata. Si prega di accedere al collegamento per compiere la verifica."
notSet: "Non impostato"
@ -900,7 +903,7 @@ useBlurEffect: "Utilizza effetto sfocatura"
learnMore: "Per saperne di più"
misskeyUpdated: "Misskey è stato aggiornato!"
whatIsNew: "Informazioni sull'aggiornamento"
translate: "Traduci"
translate: "Traduzione"
translatedFrom: "Traduzione da {x}"
accountDeletionInProgress: "È in corso l'eliminazione del profilo"
usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito."
@ -911,7 +914,7 @@ pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente"
resolved: "Risolto"
unresolved: "Non risolto"
breakFollow: "Rimuovi Follower"
breakFollow: "Rimuovere Follower"
breakFollowConfirm: "Vuoi davvero togliere questo Follower?"
itsOn: "Abilitato"
itsOff: "Disabilitato"
@ -1169,6 +1172,7 @@ installed: "Installazione avvenuta"
branding: "Branding"
enableServerMachineStats: "Pubblicare le informazioni sul server"
enableIdenticonGeneration: "Generazione automatica delle Identicon"
showRoleBadgesOfRemoteUsers: "Visualizza i badge per i ruoli concessi ai profili remoti"
turnOffToImprovePerformance: "Disattiva, per migliorare le prestazioni"
createInviteCode: "Genera codice di invito"
createWithOptions: "Genera con opzioni"
@ -1291,7 +1295,7 @@ sensitiveMediaRevealConfirm: "Questo allegato è esplicito, vuoi vederlo?"
createdLists: "Liste create"
createdAntennas: "Antenne create"
fromX: "Da {x}"
genEmbedCode: "Ottieni il codice di incorporamento"
genEmbedCode: "Ottieni il codice per incorporare"
noteOfThisUser: "Elenco di Note di questo profilo"
clipNoteLimitExceeded: "Non è possibile aggiungere ulteriori Note a questa Clip."
performance: "Prestazioni"
@ -1345,7 +1349,7 @@ postForm: "Finestra di pubblicazione"
textCount: "Il numero di caratteri"
information: "Informazioni"
chat: "Chat"
directMessage: "Chatta con questa persona"
directMessage: "Chattare insieme"
directMessage_short: "Messaggio"
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."
@ -1383,13 +1387,15 @@ pluginsAreDisabledBecauseSafeMode: "Tutti i plugin sono disattivati, poiché la
customCssIsDisabledBecauseSafeMode: "Il CSS personalizzato non è stato applicato, poiché la modalità sicura è attiva."
themeIsDefaultBecauseSafeMode: "Quando la modalità sicura è attiva, viene utilizzato il tema predefinito. Quando la modalità sicura viene disattivata, il tema torna a essere quello precedente."
thankYouForTestingBeta: "Grazie per la tua collaborazione nella verifica delle versioni beta!"
createUserSpecifiedNote: "Creare Nota personalizzata"
createUserSpecifiedNote: "Crea Nota privata"
schedulePost: "Pianificare la pubblicazione"
scheduleToPostOnX: "Pianificare la pubblicazione {x}"
scheduledToPostOnX: "Pubblicazione pianificata {x}"
schedule: "Pianificare"
scheduled: "Pianificata"
widgets: "Riquadri"
deviceInfo: "Informazioni sul dispositivo"
deviceInfoDescription: "Se ci contatti per ricevere supporto tecnico, ti preghiamo di includere le seguenti informazioni per aiutarci a risolvere il tuo problema."
_compression:
_quality:
high: "Alta qualità"
@ -1414,12 +1420,12 @@ _chat:
inviteUserToChat: "Invita a chattare altre persone"
yourRooms: "Le tue stanze"
joiningRooms: "Stanze a cui partecipi"
invitations: "Invita"
invitations: "Inviti"
noInvitations: "Nessun invito"
history: "Cronologia"
noHistory: "Nessuna cronologia"
noRooms: "Nessuna stanza"
inviteUser: "Invita"
inviteUser: "Invita persona"
sentInvitations: "Inviti spediti"
join: "Entra"
ignore: "Ignora"
@ -2014,6 +2020,7 @@ _role:
canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo"
driveCapacity: "Capienza del Drive"
maxFileSize: "Dimensione massima del file caricabile"
maxFileSize_caption: "Potrebbero esserci altre impostazioni nella fase precedente, come reverse proxy o CDN."
alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)"
canUpdateBioMedia: "Può aggiornare foto profilo e di testata"
pinMax: "Quantità massima di Note in primo piano"
@ -2432,6 +2439,7 @@ _auth:
scopeUser: "Sto funzionando per il seguente profilo"
pleaseLogin: "Per favore accedi al tuo account per cambiare i permessi dell'applicazione"
byClickingYouWillBeRedirectedToThisUrl: "Consentendo l'accesso, si verrà reindirizzati presso questo indirizzo URL"
alreadyAuthorized: "Questa applicazione è già autorizzata ad accedere."
_antennaSources:
all: "Tutte le note"
homeTimeline: "Note dai tuoi Following"
@ -2698,6 +2706,8 @@ _notification:
quote: "Cita"
reaction: "Reazioni"
pollEnded: "Sondaggio terminato"
scheduledNotePosted: "Nota pianificata correttamente"
scheduledNotePostFailed: "La pianificazione della Nota è fallita"
receiveFollowRequest: "Richieste di follow in arrivo"
followRequestAccepted: "Richieste di follow accettate"
roleAssigned: "Ruolo concesso"
@ -3044,7 +3054,7 @@ _customEmojisManager:
confirmClearEmojisDescription: "Annullare le modifiche e cancella le emoji nell'elenco. Confermi?"
confirmUploadEmojisDescription: "Caricamento sul Drive di {count} file locali. Vuoi davvero procedere?"
_embedCodeGen:
title: "Personalizza il codice di incorporamento"
title: "Personalizza il codice per incorporare"
header: "Mostra la testata"
autoload: "Carica automaticamente di più (sconsigliato)"
maxHeight: "Altezza massima"
@ -3053,8 +3063,8 @@ _embedCodeGen:
previewIsNotActual: "Poiché supera l'intervallo che può essere visualizzato in anteprima, la visualizzazione vera e propria sarà diversa quando effettivamente incorporata."
rounded: "Bordo arrotondato"
border: "Aggiungi un bordo al contenitore"
applyToPreview: "Applica all'anteprima"
generateCode: "Crea il codice di incorporamento"
applyToPreview: "Aggiorna l'anteprima"
generateCode: "Crea il codice per incorporare"
codeGenerated: "Codice generato"
codeGeneratedDescription: "Incolla il codice appena generato sul tuo sito web."
_selfXssPrevention:

View File

@ -775,6 +775,7 @@ lockedAccountInfo: "フォローを承認制にしても、ノートの公開範
alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする"
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
disableShowingAnimatedImages: "アニメーション画像を再生しない"
disableShowingAnimatedImages_caption: "この設定に関わらずアニメーション画像が再生されないときは、ブラウザ・OSのアクセシビリティ設定や省電力設定等が干渉している場合があります。"
highlightSensitiveMedia: "メディアがセンシティブであることを分かりやすく表示"
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
notSet: "未設定"
@ -1171,6 +1172,7 @@ installed: "インストール済み"
branding: "ブランディング"
enableServerMachineStats: "サーバーのマシン情報を公開する"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
showRoleBadgesOfRemoteUsers: "リモートユーザーに付与したロールバッジを表示する"
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
createInviteCode: "招待コードを作成"
createWithOptions: "オプションを指定して作成"
@ -3303,6 +3305,7 @@ _watermarkEditor:
title: "ウォーターマークの編集"
cover: "全体に被せる"
repeat: "敷き詰める"
preserveBoundingRect: "回転時はみ出ないように調整する"
opacity: "不透明度"
scale: "サイズ"
text: "テキスト"

View File

@ -775,6 +775,7 @@ lockedAccountInfo: "팔로우를 승인으로 승인받더라도 노트의 공
alwaysMarkSensitive: "미디어를 항상 열람 주의로 설정"
loadRawImages: "첨부한 이미지의 썸네일을 원본화질로 표시"
disableShowingAnimatedImages: "움직이는 이미지를 자동으로 재생하지 않음"
disableShowingAnimatedImages_caption: "이 설정에 상관없이 애니메이션 이미지가 재생되지 않을 때는 브라우저·OS의 액티비티 설정이나 절전 모드 설정 등이 간섭하고 있는 경우가 있습니다."
highlightSensitiveMedia: "미디어가 민감한 내용이라는 것을 알기 쉽게 표시"
verificationEmailSent: "확인 메일을 발송하였습니다. 설정을 완료하려면 메일에 첨부된 링크를 확인해 주세요."
notSet: "설정되지 않음"
@ -1171,6 +1172,7 @@ installed: "설치됨"
branding: "브랜딩"
enableServerMachineStats: "서버의 머신 사양을 공개하기"
enableIdenticonGeneration: "유저마다의 Identicon 생성 유효화"
showRoleBadgesOfRemoteUsers: "리모트 유저의 역할 배지 표시"
turnOffToImprovePerformance: "이 기능을 끄면 성능이 향상될 수 있습니다."
createInviteCode: "초대 코드 생성"
createWithOptions: "옵션을 지정하여 생성"
@ -3199,6 +3201,7 @@ _watermarkEditor:
title: "워터마크 편집"
cover: "전체에 붙이기"
repeat: "전면에 깔기"
preserveBoundingRect: "회전 시 빠져나오지 않도록 조정"
opacity: "불투명도"
scale: "크기"
text: "텍스트"

View File

@ -106,8 +106,8 @@ privacy: "隐私"
makeFollowManuallyApprove: "关注请求需要批准"
defaultNoteVisibility: "默认可见性"
follow: "关注"
followRequest: "关注申请"
followRequests: "关注请"
followRequest: "申请关注"
followRequests: "关注"
unfollow: "取消关注"
followRequestPending: "关注请求待批准"
enterEmoji: "输入表情符号"
@ -540,7 +540,7 @@ regenerate: "重新生成"
fontSize: "字体大小"
mediaListWithOneImageAppearance: "仅一张图片的媒体列表高度"
limitTo: "上限为 {x}"
noFollowRequests: "没有关注请"
noFollowRequests: "没有关注"
openImageInNewTab: "在新标签页中打开图片"
dashboard: "管理面板"
local: "本地"
@ -775,6 +775,7 @@ lockedAccountInfo: "即使启用该功能,只要帖子可见范围不是「仅
alwaysMarkSensitive: "默认将媒体文件标记为敏感内容"
loadRawImages: "添加附件图像的缩略图时使用原始图像质量"
disableShowingAnimatedImages: "不播放动画"
disableShowingAnimatedImages_caption: "如果即使关闭了此设置但动画仍无法播放,则可能是浏览器或操作系统的辅助功能设置,又或者是省电设置等产生了干扰。"
highlightSensitiveMedia: "高亮显示敏感媒体"
verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。"
notSet: "未设置"
@ -872,12 +873,12 @@ noMaintainerInformationWarning: "尚未设置管理员信息。"
noInquiryUrlWarning: "尚未设置联络地址。"
noBotProtectionWarning: "尚未设置 Bot 防御。"
configure: "设置"
postToGallery: "创建新相册"
postToGallery: "创建新图集"
postToHashtag: "投稿到这个标签"
gallery: "相册"
gallery: "图集"
recentPosts: "最新发布"
popularPosts: "热门投稿"
shareWithNote: "分享到文"
shareWithNote: "分享到文"
ads: "广告"
expiration: "截止时间"
startingperiod: "开始时间"
@ -1030,7 +1031,7 @@ tools: "工具"
cannotLoad: "无法加载"
numberOfProfileView: "个人资料展示次数"
like: "点赞!"
unlike: "取消"
unlike: "取消喜欢"
numberOfLikes: "点赞数"
show: "显示"
neverShow: "不再显示"
@ -1066,7 +1067,7 @@ thisPostMayBeAnnoyingHome: "发到首页"
thisPostMayBeAnnoyingCancel: "取消"
thisPostMayBeAnnoyingIgnore: "就这样发布"
collapseRenotes: "省略显示已经看过的转发内容"
collapseRenotesDescription: "将回应过或转贴过的贴子折叠表示。"
collapseRenotesDescription: "折叠显示回应或转发过的帖文。"
internalServerError: "内部服务器错误"
internalServerErrorDescription: "内部服务器发生了预期外的错误"
copyErrorInfo: "复制错误信息"
@ -1107,7 +1108,7 @@ retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
enableChartsForRemoteUser: "生成远程用户的图表"
enableChartsForFederatedInstances: "生成远程服务器的图表"
enableStatsForFederatedInstances: "获取远程服务器的信息"
showClipButtonInNoteFooter: "在文下方显示便签按钮"
showClipButtonInNoteFooter: "在文下方显示便签按钮"
reactionsDisplaySize: "回应显示大小"
limitWidthOfReaction: "限制回应的最大宽度,并将其缩小显示"
noteIdOrUrl: "帖子 ID 或 URL"
@ -1145,7 +1146,7 @@ archive: "归档"
archived: "已归档"
unarchive: "取消归档"
channelArchiveConfirmTitle: "要将 {name} 归档吗?"
channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。"
channelArchiveConfirmDescription: "归档后,不会在频道列表与搜索结果中显示,也无法发布新的帖文。"
thisChannelArchived: "该频道已被归档。"
displayOfNote: "显示帖子"
initialAccountSetting: "初始设定"
@ -1171,6 +1172,7 @@ installed: "已安装"
branding: "品牌"
enableServerMachineStats: "公开服务器硬件统计信息"
enableIdenticonGeneration: "启用生成用户 Identicon"
showRoleBadgesOfRemoteUsers: "显示远程用户的角色徽章"
turnOffToImprovePerformance: "关闭该选项可以提高性能。"
createInviteCode: "生成邀请码"
createWithOptions: "使用选项来创建"
@ -1801,7 +1803,7 @@ _achievements:
_login500:
title: "老熟人Ⅰ"
description: "累计登录 500 天"
flavor: "诸君,我喜欢文"
flavor: "诸君,我喜欢文"
_login600:
title: "老熟人Ⅱ"
description: "累计登录 600 天"
@ -1820,7 +1822,7 @@ _achievements:
flavor: "感谢您使用 Misskey"
_noteClipped1:
title: "忍不住要收藏到便签"
description: "第一次将贴文贴进便签"
description: "第一次将帖子加入便签"
_noteFavorited1:
title: "观星者"
description: "第一次将帖子加入收藏"
@ -2026,7 +2028,7 @@ _role:
wordMuteMax: "屏蔽词的字数限制"
webhookMax: "Webhook 创建数量限制"
clipMax: "便签创建数量限制"
noteEachClipsMax: "单个便签内贴文数量限制"
noteEachClipsMax: "便签内贴文的最大数量"
userListMax: "用户列表创建数量限制"
userEachUserListsMax: "单个用户列表内用户数量限制"
rateLimitFactor: "速率限制"
@ -2112,8 +2114,8 @@ _forgotPassword:
ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。"
contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。"
_gallery:
my: "我的相册"
liked: "喜欢的相册"
my: "我的图集"
liked: "喜欢的图集"
like: "喜欢!"
unlike: "取消喜欢"
_email:
@ -2366,10 +2368,10 @@ _permissions:
"write:user-groups": "编辑用户组"
"read:channels": "查看频道"
"write:channels": "管理频道"
"read:gallery": "浏览相册"
"write:gallery": "编辑相册"
"read:gallery-likes": "浏览喜欢的相册"
"write:gallery-likes": "管理喜欢的相册"
"read:gallery": "浏览图集"
"write:gallery": "编辑图集"
"read:gallery-likes": "浏览喜欢的图集"
"write:gallery-likes": "管理喜欢的图集"
"read:flash": "查看 Play"
"write:flash": "编辑 Play"
"read:flash-likes": "查看 Play 的点赞"
@ -2776,7 +2778,7 @@ _webhookSettings:
_events:
follow: "关注时"
followed: "被关注时"
note: "发布文时"
note: "发布文时"
reply: "收到回复时"
renote: "被转发时"
reaction: "被回应时"
@ -2852,7 +2854,7 @@ _moderationLogTypes:
deleteAccount: "删除了账户"
deletePage: "删除了页面"
deleteFlash: "删除了 Play"
deleteGalleryPost: "删除相册内容"
deleteGalleryPost: "删除图集内容"
deleteChatRoom: "删除聊天室"
updateProxyAccountDescription: "更新代理账户的简介"
_fileViewer:
@ -3072,8 +3074,8 @@ _selfXssPrevention:
description2: "如果不能完全理解将要粘贴的内容,%c 请立即停止操作并关闭这个窗口。"
description3: "详情请看这里。{link}"
_followRequest:
recieved: "已收到申请"
sent: "已发送申请"
recieved: "收到的请求"
sent: "已发送的请求"
_remoteLookupErrors:
_federationNotAllowed:
title: "无法与此服务器通信"
@ -3199,6 +3201,7 @@ _watermarkEditor:
title: "编辑水印"
cover: "覆盖全体"
repeat: "平铺"
preserveBoundingRect: "调整为旋转时不超出范围"
opacity: "不透明度"
scale: "大小"
text: "文本"

View File

@ -775,6 +775,7 @@ lockedAccountInfo: "即使追隨需要核准,除非你將貼文的可見性設
alwaysMarkSensitive: "預設標記檔案為敏感內容"
loadRawImages: "以原始圖檔顯示附件圖檔的縮圖"
disableShowingAnimatedImages: "不播放動態圖檔"
disableShowingAnimatedImages_caption: "無論這個設定如何,如果動畫圖片無法播放,可能是因為瀏覽器或作業系統的無障礙設定、省電設定等產生了干擾。"
highlightSensitiveMedia: "強調敏感標記"
verificationEmailSent: "已發送驗證電子郵件。請點擊進入電子郵件中的連結以完成驗證。"
notSet: "未設定"
@ -3175,7 +3176,7 @@ _uploader:
abortConfirm: "有些檔案尚未上傳,您要中止嗎?"
doneConfirm: "有些檔案尚未上傳,是否要完成上傳?"
maxFileSizeIsX: "可上傳的最大檔案大小為 {x}。"
allowedTypes: "可上傳的檔案類型"
allowedTypes: "可上傳的檔案類型"
tip: "檔案尚未上傳。您可以在此對話框中進行上傳前的確認、重新命名、壓縮、裁切等操作。準備完成後,請點選「上傳」按鈕開始上傳。\n"
_clientPerformanceIssueTip:
title: "如果覺得電池消耗過快的話"
@ -3199,6 +3200,7 @@ _watermarkEditor:
title: "編輯浮水印"
cover: "覆蓋整體"
repeat: "佈局"
preserveBoundingRect: "調整使其在旋轉時不會突出"
opacity: "透明度"
scale: "大小"
text: "文字"

View File

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2025.10.0",
"version": "2025.10.1",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@10.17.1",
"packageManager": "pnpm@10.18.2",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
@ -58,25 +58,25 @@
"execa": "9.6.0",
"fast-glob": "3.3.3",
"glob": "11.0.3",
"ignore-walk": "7.0.0",
"ignore-walk": "8.0.0",
"js-yaml": "4.1.0",
"postcss": "8.5.6",
"tar": "7.5.1",
"terser": "5.44.0",
"typescript": "5.9.2"
"typescript": "5.9.3"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/js-yaml": "4.0.9",
"@types/node": "22.18.6",
"@typescript-eslint/eslint-plugin": "8.44.1",
"@typescript-eslint/parser": "8.44.1",
"cross-env": "7.0.3",
"cypress": "14.5.4",
"eslint": "9.36.0",
"@types/node": "22.18.10",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"cross-env": "10.1.0",
"cypress": "15.4.0",
"eslint": "9.37.0",
"globals": "16.4.0",
"ncp": "2.0.0",
"pnpm": "10.17.1",
"pnpm": "10.18.2",
"start-server-and-test": "2.1.2"
},
"optionalDependencies": {
@ -85,9 +85,6 @@
"pnpm": {
"overrides": {
"@aiscript-dev/aiscript-languageserver": "-"
},
"patchedDependencies": {
"typeorm": "patches/typeorm.patch"
}
}
}

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class RoleBadgesRemoteUsers1760607435831 {
name = 'RoleBadgesRemoteUsers1760607435831'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "showRoleBadgesOfRemoteUsers" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "showRoleBadgesOfRemoteUsers"`);
}
}

View File

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class UnnecessaryNullDefault1760790899857 {
name = 'UnnecessaryNullDefault1760790899857'
/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "abuse_report_notification_recipient" ALTER COLUMN "userId" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "abuse_report_notification_recipient" ALTER COLUMN "systemWebhookId" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "urlPreviewUserAgent" DROP DEFAULT`);
}
/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "urlPreviewUserAgent" SET DEFAULT NULL`);
await queryRunner.query(`ALTER TABLE "abuse_report_notification_recipient" ALTER COLUMN "systemWebhookId" SET DEFAULT NULL`);
await queryRunner.query(`ALTER TABLE "abuse_report_notification_recipient" ALTER COLUMN "userId" SET DEFAULT NULL`);
}
}

View File

@ -39,17 +39,17 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.13.19",
"@swc/core-darwin-x64": "1.13.19",
"@swc/core-darwin-arm64": "1.13.20",
"@swc/core-darwin-x64": "1.13.20",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.13.19",
"@swc/core-linux-arm64-gnu": "1.13.19",
"@swc/core-linux-arm64-musl": "1.13.19",
"@swc/core-linux-x64-gnu": "1.13.19",
"@swc/core-linux-x64-musl": "1.13.19",
"@swc/core-win32-arm64-msvc": "1.13.19",
"@swc/core-win32-ia32-msvc": "1.13.19",
"@swc/core-win32-x64-msvc": "1.13.19",
"@swc/core-linux-arm-gnueabihf": "1.13.20",
"@swc/core-linux-arm64-gnu": "1.13.20",
"@swc/core-linux-arm64-musl": "1.13.20",
"@swc/core-linux-x64-gnu": "1.13.20",
"@swc/core-linux-x64-musl": "1.13.20",
"@swc/core-win32-arm64-msvc": "1.13.20",
"@swc/core-win32-ia32-msvc": "1.13.20",
"@swc/core-win32-x64-msvc": "1.13.20",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9",
@ -69,10 +69,10 @@
"utf-8-validate": "6.0.5"
},
"dependencies": {
"@aws-sdk/client-s3": "3.896.0",
"@aws-sdk/lib-storage": "3.895.0",
"@aws-sdk/client-s3": "3.908.0",
"@aws-sdk/lib-storage": "3.908.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.2",
"@fastify/accepts": "5.0.3",
"@fastify/cookie": "11.0.2",
"@fastify/cors": "10.1.0",
"@fastify/express": "4.0.2",
@ -81,7 +81,7 @@
"@fastify/static": "8.2.0",
"@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.3",
"@misskey-dev/summaly": "5.2.4",
"@napi-rs/canvas": "0.1.80",
"@nestjs/common": "11.1.6",
"@nestjs/core": "11.1.6",
@ -103,7 +103,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.58.8",
"bullmq": "5.61.0",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.6.2",
@ -120,12 +120,12 @@
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.4",
"got": "14.4.9",
"happy-dom": "16.8.1",
"got": "14.5.0",
"happy-dom": "20.0.7",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",
"ioredis": "5.8.0",
"ioredis": "5.8.1",
"ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0",
"is-svg": "5.1.0",
@ -134,7 +134,7 @@
"json5": "2.2.3",
"jsonld": "8.3.3",
"jsrsasign": "11.1.0",
"juice": "11.0.1",
"juice": "11.0.3",
"meilisearch": "0.53.0",
"mfm-js": "0.25.0",
"microformats-parser": "2.0.4",
@ -145,7 +145,7 @@
"nanoid": "5.1.6",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"nodemailer": "6.10.1",
"nodemailer": "7.0.9",
"nsfwjs": "4.2.0",
"oauth": "0.10.2",
"oauth2orize": "1.12.0",
@ -171,17 +171,17 @@
"sanitize-html": "2.17.0",
"secure-json-parse": "3.0.2",
"sharp": "0.33.5",
"semver": "7.7.2",
"semver": "7.7.3",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.27.10",
"systeminformation": "5.27.11",
"tinycolor2": "1.6.0",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.27",
"typescript": "5.9.2",
"typescript": "5.9.3",
"ulid": "2.4.0",
"vary": "1.1.2",
"web-push": "3.6.7",
@ -210,8 +210,8 @@
"@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "22.18.6",
"@types/nodemailer": "6.4.19",
"@types/node": "22.18.10",
"@types/nodemailer": "6.4.20",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
@ -231,8 +231,8 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.44.1",
"@typescript-eslint/parser": "8.44.1",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.32.0",

View File

@ -517,8 +517,10 @@ export class DriveService {
this.registerLogger.debug(`ADD DRIVE FILE: user ${user?.id ?? 'not set'}, name ${detectedName}, tmp ${path}`);
//#region Check drive usage and mime type
if (user && !isLink) {
if (user != null && !isLink) {
const isLocalUser = this.userEntityService.isLocalUser(user);
const isModerator = isLocalUser ? await this.roleService.isModerator(user) : false;
if (!isModerator) {
const policies = await this.roleService.getUserPolicies(user.id);
const allowedMimeTypes = policies.uploadableFileTypes;
@ -553,6 +555,7 @@ export class DriveService {
await this.expireOldFile(await this.usersRepository.findOneByOrFail({ id: user.id }) as MiRemoteUser, driveCapacity - info.size);
}
}
}
//#endregion
const fetchFolder = async () => {

View File

@ -512,8 +512,8 @@ export class UserEntityService implements OnModuleInit {
} : undefined) : undefined,
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),
// パフォーマンス上の理由でローカルユーザーのみ
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
// パフォーマンス上の理由で、明示的に設定しない場合はローカルユーザーのみ取得
badgeRoles: (this.meta.showRoleBadgesOfRemoteUsers || user.host == null) ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
.filter((r) => r.isPublic || iAmModerator)
.sort((a, b) => b.displayOrder - a.displayOrder)
.map((r) => ({

View File

@ -717,6 +717,11 @@ export class MiMeta {
})
public remoteNotesCleaningExpiryDaysForEachNotes: number;
@Column('boolean', {
default: false,
})
public showRoleBadgesOfRemoteUsers: boolean;
@Column('jsonb', {
default: { },
})

View File

@ -593,6 +593,10 @@ export const meta = {
type: 'number',
optional: false, nullable: false,
},
showRoleBadgesOfRemoteUsers: {
type: 'boolean',
optional: false, nullable: false,
},
},
},
} as const;
@ -748,6 +752,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableRemoteNotesCleaning: instance.enableRemoteNotesCleaning,
remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes,
remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes,
showRoleBadgesOfRemoteUsers: instance.showRoleBadgesOfRemoteUsers,
};
});
}

View File

@ -209,6 +209,7 @@ export const paramDef = {
enableRemoteNotesCleaning: { type: 'boolean' },
remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' },
remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' },
showRoleBadgesOfRemoteUsers: { type: 'boolean' },
},
required: [],
} as const;
@ -743,6 +744,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.remoteNotesCleaningMaxProcessingDurationInMinutes = ps.remoteNotesCleaningMaxProcessingDurationInMinutes;
}
if (ps.showRoleBadgesOfRemoteUsers !== undefined) {
set.showRoleBadgesOfRemoteUsers = ps.showRoleBadgesOfRemoteUsers;
}
const before = await this.metaService.fetch(true);
await this.metaService.update(set);

View File

@ -11,15 +11,15 @@
},
"devDependencies": {
"@types/estree": "1.0.8",
"@types/node": "22.18.6",
"@typescript-eslint/eslint-plugin": "8.44.1",
"@typescript-eslint/parser": "8.44.1",
"rollup": "4.52.2",
"typescript": "5.9.2"
"@types/node": "22.18.10",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"rollup": "4.52.4",
"typescript": "5.9.3"
},
"dependencies": {
"estree-walker": "3.0.3",
"magic-string": "0.30.19",
"vite": "7.1.7"
"vite": "7.1.9"
}
}

View File

@ -26,47 +26,47 @@
"mfm-js": "0.25.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.52.2",
"rollup": "4.52.4",
"sass": "1.93.2",
"shiki": "3.13.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"uuid": "11.1.0",
"vite": "7.1.7",
"typescript": "5.9.3",
"uuid": "13.0.0",
"vite": "7.1.9",
"vue": "3.5.22"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.3",
"@misskey-dev/summaly": "5.2.4",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.18.6",
"@types/node": "22.18.10",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.44.1",
"@typescript-eslint/parser": "8.44.1",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"@vitest/coverage-v8": "3.2.4",
"@vue/runtime-core": "3.5.22",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.5.0",
"fast-glob": "3.3.3",
"happy-dom": "18.0.1",
"happy-dom": "20.0.7",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.11.3",
"msw": "2.11.5",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"start-server-and-test": "2.1.2",
"tsx": "4.20.6",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.0.8",
"vue-component-type-helpers": "3.1.1",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.8"
"vue-tsc": "3.1.1"
}
}

View File

@ -39,13 +39,18 @@ for (let i = 0; i < emojilist.length; i++) {
export const emojiCharByCategory = _charGroupByCategory;
export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string {
export function getUnicodeEmojiOrNull(char: string): UnicodeEmojiDef | null {
// Colorize it because emojilist.json assumes that
return unicodeEmojisMap.get(colorizeEmoji(char))
// カラースタイル絵文字がjsonに無い場合はテキストスタイル絵文字にフォールバックする
?? unicodeEmojisMap.get(char)
// それでも見つからない場合はそのまま返す絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する
?? char;
// それでも見つからない場合はnullを返す
?? null;
}
export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string {
// 絵文字が見つからない場合はそのまま返す絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する
return getUnicodeEmojiOrNull(char) ?? char;
}
export function isSupportedEmoji(char: string): boolean {

View File

@ -21,13 +21,13 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.18.6",
"@typescript-eslint/eslint-plugin": "8.44.1",
"@typescript-eslint/parser": "8.44.1",
"@types/node": "22.18.10",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"esbuild": "0.25.10",
"eslint-plugin-vue": "10.5.0",
"nodemon": "3.1.10",
"typescript": "5.9.2",
"typescript": "5.9.3",
"vue-eslint-parser": "10.2.0"
},
"files": [

View File

@ -24,7 +24,7 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.15.0",
"@sentry/vue": "10.19.0",
"@syuilo/aiscript": "1.1.2",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
@ -36,12 +36,12 @@
"broadcast-channel": "7.1.0",
"buraha": "0.0.1",
"canvas-confetti": "1.9.3",
"chart.js": "4.5.0",
"chart.js": "4.5.1",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "13.2.1",
"chromatic": "13.3.0",
"compare-versions": "6.1.1",
"cropperjs": "2.0.1",
"date-fns": "4.1.0",
@ -57,7 +57,7 @@
"json5": "2.2.3",
"magic-string": "0.30.19",
"matter-js": "0.20.0",
"mediabunny": "1.21.0",
"mediabunny": "1.23.0",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@ -66,7 +66,7 @@
"punycode.js": "2.3.1",
"qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2",
"rollup": "4.52.2",
"rollup": "4.52.4",
"sanitize-html": "2.17.0",
"sass": "1.93.2",
"shiki": "3.13.0",
@ -77,18 +77,18 @@
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"typescript": "5.9.3",
"v-code-diff": "1.13.1",
"vite": "7.1.7",
"vite": "7.1.9",
"vue": "3.5.22",
"vuedraggable": "next",
"wanakana": "5.3.1"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.3",
"@misskey-dev/summaly": "5.2.4",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
"@storybook/addon-links": "9.1.8",
"@storybook/addon-links": "9.1.10",
"@storybook/addon-mdx-gfm": "8.6.14",
"@storybook/addon-storysource": "8.6.14",
"@storybook/blocks": "8.6.14",
@ -96,57 +96,58 @@
"@storybook/core-events": "8.6.14",
"@storybook/manager-api": "8.6.14",
"@storybook/preview-api": "8.6.14",
"@storybook/react": "9.1.8",
"@storybook/react-vite": "9.1.8",
"@storybook/react": "9.1.10",
"@storybook/react-vite": "9.1.10",
"@storybook/test": "8.6.14",
"@storybook/theming": "8.6.14",
"@storybook/types": "8.6.14",
"@storybook/vue3": "9.1.8",
"@storybook/vue3-vite": "9.1.8",
"@storybook/vue3": "9.1.10",
"@storybook/vue3-vite": "9.1.10",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8",
"@types/matter-js": "0.20.2",
"@types/micromatch": "4.0.9",
"@types/node": "22.18.6",
"@types/node": "22.18.10",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.44.1",
"@typescript-eslint/parser": "8.44.1",
"@typescript-eslint/eslint-plugin": "8.46.1",
"@typescript-eslint/parser": "8.46.1",
"@vitest/coverage-v8": "3.2.4",
"@vue/compiler-core": "3.5.22",
"@vue/runtime-core": "3.5.22",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"cypress": "14.5.4",
"cross-env": "10.1.0",
"cypress": "15.4.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.5.0",
"fast-glob": "3.3.3",
"happy-dom": "18.0.1",
"happy-dom": "20.0.7",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.0.3",
"msw": "2.11.3",
"msw-storybook-addon": "2.0.5",
"msw": "2.11.5",
"msw-storybook-addon": "2.0.6",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"seedrandom": "3.0.5",
"start-server-and-test": "2.1.2",
"storybook": "9.1.8",
"storybook": "9.1.10",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.20.6",
"vite-plugin-glsl": "1.5.4",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.0.8",
"vue-component-type-helpers": "3.1.1",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.8"
"vue-tsc": "3.1.1"
}
}

View File

@ -211,13 +211,13 @@ export async function switchAccount(host: string, id: string) {
}
}
export async function openAccountMenu(opts: {
export async function getAccountMenu(opts: {
includeCurrentAccount?: boolean;
withExtraOperation: boolean;
active?: Misskey.entities.User['id'];
onChoose?: (account: Misskey.entities.MeDetailed) => void;
}, ev: MouseEvent) {
if (!$i) return;
}) {
if ($i == null) throw new Error('No current account');
const me = $i;
const callback = opts.onChoose;
@ -338,9 +338,7 @@ export async function openAccountMenu(opts: {
menuItems.push(...accountItems);
}
popupMenu(menuItems, ev.currentTarget ?? ev.target, {
align: 'left',
});
return menuItems;
}
export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {

View File

@ -66,7 +66,7 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string })
});
return confirm.canceled ? values.FALSE : values.TRUE;
}),
'Mk:toast': values.FN_NATIVE(async ([text]) => {
'Mk:toast': values.FN_NATIVE(([text]) => {
utils.assertString(text);
os.toast(text.value);
return values.NULL;

View File

@ -69,9 +69,6 @@ export async function common(createVue: () => Promise<App<Element>>) {
if (lastVersion !== version) {
miLocalStorage.setItem('lastVersion', version);
// テーマリビルドするため
miLocalStorage.removeItem('theme');
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
isClientUpdated = true;
@ -176,7 +173,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
})();
applyTheme(theme);
}, { immediate: isSafeMode || miLocalStorage.getItem('theme') == null });
}, { immediate: true });
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
@ -195,14 +192,6 @@ export async function common(createVue: () => Promise<App<Element>>) {
applyTheme(theme ?? defaultLightTheme);
}
});
}
if (!isSafeMode) {
if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
} else if (prefer.s.lightTheme && !store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
}
fetchInstanceMetaPromise.then(() => {
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア

View File

@ -0,0 +1,111 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec2 mod289(vec2 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec3 permute(vec3 x) {
return mod289(((x*34.0)+1.0)*x);
}
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
m = m*m;
m = m*m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
in vec2 in_uv;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_spread;
uniform float u_speed;
uniform float u_warp;
uniform float u_focus;
uniform float u_itensity;
out vec4 out_color;
float circle(in vec2 _pos, in vec2 _origin, in float _radius) {
float SPREAD = 0.7 * u_spread;
float SPEED = 0.00055 * u_speed;
float WARP = 1.5 * u_warp;
float FOCUS = 1.15 * u_focus;
vec2 dist = _pos - _origin;
float distortion = snoise(vec2(
_pos.x * 1.587 * WARP + u_time * SPEED * 0.5,
_pos.y * 1.192 * WARP + u_time * SPEED * 0.3
)) * 0.5 + 0.5;
float feather = 0.01 + SPREAD * pow(distortion, FOCUS);
return 1.0 - smoothstep(
_radius - (_radius * feather),
_radius + (_radius * feather),
dot( dist, dist ) * 4.0
);
}
void main() {
vec3 green = vec3(1.0) - vec3(153.0 / 255.0, 211.0 / 255.0, 221.0 / 255.0);
vec3 purple = vec3(1.0) - vec3(195.0 / 255.0, 165.0 / 255.0, 242.0 / 255.0);
vec3 orange = vec3(1.0) - vec3(255.0 / 255.0, 156.0 / 255.0, 136.0 / 255.0);
float ratio = u_resolution.x / u_resolution.y;
vec2 uv = vec2(in_uv.x, in_uv.y / ratio) * 0.5 + 0.5;
vec3 color = vec3(0.0);
float greenMix = snoise(in_uv * 1.31 + u_time * 0.8 * 0.00017) * 0.5 + 0.5;
float purpleMix = snoise(in_uv * 1.26 + u_time * 0.8 * -0.0001) * 0.5 + 0.5;
float orangeMix = snoise(in_uv * 1.34 + u_time * 0.8 * 0.00015) * 0.5 + 0.5;
float alphaOne = 0.35 + 0.65 * pow(snoise(vec2(u_time * 0.00012, uv.x)) * 0.5 + 0.5, 1.2);
float alphaTwo = 0.35 + 0.65 * pow(snoise(vec2((u_time + 1561.0) * 0.00014, uv.x )) * 0.5 + 0.5, 1.2);
float alphaThree = 0.35 + 0.65 * pow(snoise(vec2((u_time + 3917.0) * 0.00013, uv.x )) * 0.5 + 0.5, 1.2);
color += vec3(circle(uv, vec2(0.22 + sin(u_time * 0.000201) * 0.06, 0.80 + cos(u_time * 0.000151) * 0.06), 0.15)) * alphaOne * (purple * purpleMix + orange * orangeMix);
color += vec3(circle(uv, vec2(0.90 + cos(u_time * 0.000166) * 0.06, 0.42 + sin(u_time * 0.000138) * 0.06), 0.18)) * alphaTwo * (green * greenMix + purple * purpleMix);
color += vec3(circle(uv, vec2(0.19 + sin(u_time * 0.000112) * 0.06, 0.25 + sin(u_time * 0.000192) * 0.06), 0.09)) * alphaThree * (orange * orangeMix);
color *= u_itensity + 1.0 * pow(snoise(vec2(in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009)) * 0.5 + 0.5, 2.0);
vec3 inverted = vec3(1.0) - color;
out_color = vec4(color, max(max(color.x, color.y), color.z));
}

View File

@ -0,0 +1,15 @@
#version 300 es
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 position;
uniform vec2 u_scale;
out vec2 in_uv;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
in_uv = position / u_scale;
}

View File

@ -10,6 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
import isChromatic from 'chromatic/isChromatic';
import vertexShaderSource from './MkAnimBg.vertex.glsl';
import fragmentShaderSource from './MkAnimBg.fragment.glsl';
import { initShaderProgram } from '@/utility/webgl.js';
const canvasEl = useTemplateRef('canvasEl');
@ -42,126 +44,7 @@ onMounted(() => {
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const shaderProgram = initShaderProgram(gl, `#version 300 es
in vec2 position;
uniform vec2 u_scale;
out vec2 in_uv;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
in_uv = position / u_scale;
}
`, `#version 300 es
precision mediump float;
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec2 mod289(vec2 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec3 permute(vec3 x) {
return mod289(((x*34.0)+1.0)*x);
}
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187,
0.366025403784439,
-0.577350269189626,
0.024390243902439);
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m ;
m = m*m ;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
in vec2 in_uv;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_spread;
uniform float u_speed;
uniform float u_warp;
uniform float u_focus;
uniform float u_itensity;
out vec4 out_color;
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
float SPREAD = 0.7 * u_spread;
float SPEED = 0.00055 * u_speed;
float WARP = 1.5 * u_warp;
float FOCUS = 1.15 * u_focus;
vec2 dist = _pos - _origin;
float distortion = snoise( vec2(
_pos.x * 1.587 * WARP + u_time * SPEED * 0.5,
_pos.y * 1.192 * WARP + u_time * SPEED * 0.3
) ) * 0.5 + 0.5;
float feather = 0.01 + SPREAD * pow( distortion, FOCUS );
return 1.0 - smoothstep(
_radius - ( _radius * feather ),
_radius + ( _radius * feather ),
dot( dist, dist ) * 4.0
);
}
void main() {
vec3 green = vec3( 1.0 ) - vec3( 153.0 / 255.0, 211.0 / 255.0, 221.0 / 255.0 );
vec3 purple = vec3( 1.0 ) - vec3( 195.0 / 255.0, 165.0 / 255.0, 242.0 / 255.0 );
vec3 orange = vec3( 1.0 ) - vec3( 255.0 / 255.0, 156.0 / 255.0, 136.0 / 255.0 );
float ratio = u_resolution.x / u_resolution.y;
vec2 uv = vec2( in_uv.x, in_uv.y / ratio ) * 0.5 + 0.5;
vec3 color = vec3( 0.0 );
float greenMix = snoise( in_uv * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
float purpleMix = snoise( in_uv * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
float orangeMix = snoise( in_uv * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
float alphaThree = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 3917.0 ) * 0.00013, uv.x ) ) * 0.5 + 0.5, 1.2 );
color += vec3( circle( uv, vec2( 0.22 + sin( u_time * 0.000201 ) * 0.06, 0.80 + cos( u_time * 0.000151 ) * 0.06 ), 0.15 ) ) * alphaOne * ( purple * purpleMix + orange * orangeMix );
color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
color *= u_itensity + 1.0 * pow( snoise( vec2( in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
vec3 inverted = vec3( 1.0 ) - color;
out_color = vec4(color, max(max(color.x, color.y), color.z));
}
`);
const shaderProgram = initShaderProgram(gl, vertexShaderSource, fragmentShaderSource);
if (shaderProgram == null) return;
gl.useProgram(shaderProgram);

View File

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div :class="$style.container">
<div :class="$style.preview">
<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas>
<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown.prevent.stop="onImagePointerdown"></canvas>
<div :class="$style.previewContainer">
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
<div class="_acrylic" :class="$style.editControls">
@ -257,8 +257,12 @@ function onImagePointerdown(ev: PointerEvent) {
xOffset /= 2;
yOffset /= 2;
let startX = ev.offsetX - xOffset;
let startY = ev.offsetY - yOffset;
const rect = canvasEl.value.getBoundingClientRect();
const pointerOffsetX = ev.clientX - rect.left;
const pointerOffsetY = ev.clientY - rect.top;
let startX = pointerOffsetX - xOffset;
let startY = pointerOffsetY - yOffset;
if (AW / AH < BW / BH) { //
startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1));
@ -311,9 +315,11 @@ function onImagePointerdown(ev: PointerEvent) {
});
}
_move(ev.offsetX, ev.offsetY);
_move(ev.clientX, ev.clientY);
function _move(pointerX: number, pointerY: number) {
function _move(pointerClientX: number, pointerClientY: number) {
const pointerX = pointerClientX - rect.left;
const pointerY = pointerClientY - rect.top;
let x = pointerX - xOffset;
let y = pointerY - yOffset;
@ -340,7 +346,7 @@ function onImagePointerdown(ev: PointerEvent) {
}
function move(ev: PointerEvent) {
_move(ev.offsetX, ev.offsetY);
_move(ev.clientX, ev.clientY);
}
function up() {
@ -448,6 +454,7 @@ function onImagePointerdown(ev: PointerEvent) {
margin: 20px;
box-sizing: border-box;
object-fit: contain;
touch-action: none;
}
.controls {

View File

@ -57,15 +57,8 @@ const remaining = computed(() => {
return Math.floor(Math.max(expiresAtTime.value - now.value, 0) / 1000);
});
const remainingWatchStop = watch(remaining, (to) => {
if (to <= 0) {
showResult.value = true;
remainingWatchStop();
}
}, { immediate: true });
const total = computed(() => sum(props.choices.map(x => x.votes)));
const closed = computed(() => remaining.value === 0);
const closed = computed(() => remaining.value <= 0);
const isVoted = computed(() => !props.multiple && props.choices.some(c => c.isVoted));
const timer = computed(() => i18n.tsx._poll[
remaining.value >= 86400 ? 'remainingDays' :
@ -78,7 +71,16 @@ const timer = computed(() => i18n.tsx._poll[
d: Math.floor(remaining.value / 86400),
}));
const showResult = ref(props.readOnly || isVoted.value);
const showResult = ref(props.readOnly || isVoted.value || closed.value);
if (!closed.value) {
const closedWatchStop = watch(closed, (isNowClosed) => {
if (isNowClosed) {
showResult.value = true;
closedWatchStop();
}
});
}
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
type: 'lookup',

View File

@ -17,7 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu">
<img :class="$style.avatar" :src="(postAccount ?? $i).avatarUrl" style="border-radius: 100%;"/>
</button>
<button v-if="$i.policies.noteDraftLimit > 0" v-tooltip="(postAccount != null && postAccount.id !== $i.id) ? null : i18n.ts.draftsAndScheduledNotes" class="_button" :class="$style.draftButton" :disabled="postAccount != null && postAccount.id !== $i.id" @click="showDraftMenu"><i class="ti ti-list"></i></button>
</div>
<div :class="$style.headerRight">
<template v-if="!(targetChannel != null && fixed)">
@ -141,7 +140,7 @@ import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { ensureSignin, notesCount, incNotesCount } from '@/i.js';
import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { getAccounts, getAccountMenu } from '@/accounts.js';
import { deepClone } from '@/utility/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';
@ -620,6 +619,19 @@ function showOtherSettings() {
action: () => {
toggleReactionAcceptance();
},
}, { type: 'divider' }, {
type: 'button',
text: i18n.ts._drafts.saveToDraft,
icon: 'ti ti-cloud-upload',
action: async () => {
if (!canSaveAsServerDraft.value) {
return os.alert({
type: 'error',
text: i18n.ts._drafts.cannotCreateDraft,
});
}
saveServerDraft();
},
}, ...($i.policies.scheduledNoteLimit > 0 ? [{
icon: 'ti ti-calendar-time',
text: i18n.ts.schedulePost + '...',
@ -1159,34 +1171,9 @@ function showActions(ev: MouseEvent) {
const postAccount = ref<Misskey.entities.UserDetailed | null>(null);
function openAccountMenu(ev: MouseEvent) {
async function openAccountMenu(ev: MouseEvent) {
if (props.mock) return;
openAccountMenu_({
withExtraOperation: false,
includeCurrentAccount: true,
active: postAccount.value != null ? postAccount.value.id : $i.id,
onChoose: (account) => {
if (account.id === $i.id) {
postAccount.value = null;
} else {
postAccount.value = account;
}
},
}, 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);
}
function showDraftMenu(ev: MouseEvent) {
function showDraftsDialog(scheduled: boolean) {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNoteDraftsDialog.vue')), {
scheduled,
@ -1244,34 +1231,44 @@ function showDraftMenu(ev: MouseEvent) {
});
}
os.popupMenu([{
type: 'button',
text: i18n.ts._drafts.saveToDraft,
icon: 'ti ti-cloud-upload',
action: async () => {
if (!canSaveAsServerDraft.value) {
return os.alert({
type: 'error',
text: i18n.ts._drafts.cannotCreateDraft,
});
const items = await getAccountMenu({
withExtraOperation: false,
includeCurrentAccount: true,
active: postAccount.value != null ? postAccount.value.id : $i.id,
onChoose: (account) => {
if (account.id === $i.id) {
postAccount.value = null;
} else {
postAccount.value = account;
}
saveServerDraft();
},
}, {
});
os.popupMenu([{
type: 'button',
text: i18n.ts._drafts.listDrafts,
icon: 'ti ti-cloud-download',
action: () => {
showDraftsDialog(false);
},
}, { type: 'divider' }, {
}, {
type: 'button',
text: i18n.ts._drafts.listScheduledNotes,
icon: 'ti ti-clock-down',
action: () => {
showDraftsDialog(true);
},
}], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}, { type: 'divider' }, ...items], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
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);
}
async function schedule() {
@ -1422,20 +1419,6 @@ defineExpose({
margin: auto;
}
.draftButton {
padding: 8px;
font-size: 90%;
border-radius: 6px;
&:hover {
background: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
}
&:disabled {
background: none;
}
}
.headerRight {
display: flex;
min-height: 48px;

View File

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, inject, onMounted, useTemplateRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { getUnicodeEmoji } from '@@/js/emojilist.js';
import { getUnicodeEmojiOrNull } from '@@/js/emojilist.js';
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
import type { MenuItem } from '@/types/menu';
import XDetails from '@/components/MkReactionsViewer.details.vue';
@ -60,11 +60,11 @@ const buttonEl = useTemplateRef('buttonEl');
const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, ''));
const canToggle = computed(() => {
const emoji = customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction);
const emoji = customEmojisMap.get(emojiName.value) ?? getUnicodeEmojiOrNull(props.reaction);
// TODO
//return !props.reaction.match(/@\w/) && $i && emoji && checkReactionPermissions($i, props.note, emoji);
return !props.reaction.match(/@\w/) && $i && emoji;
return props.reaction.match(/@\w/) == null && $i != null && emoji != null;
});
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
const isLocalCustomEmoji = props.reaction[0] === ':' && props.reaction.includes('@.');

View File

@ -64,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
import type { Awaitable } from '@/types/misc.js';
export type SuperMenuDef = {
title?: string;
items: ({
@ -80,7 +82,7 @@ export type SuperMenuDef = {
text: string;
danger?: boolean;
active?: boolean;
action: (ev: MouseEvent) => void | Promise<void>;
action: (ev: MouseEvent) => Awaitable<void>;
} | {
type?: 'link';
to: string;

View File

@ -33,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- クライアントで検出するMIME typeとサーバーで検出するMIME typeが異なる場合があり混乱の元になるのでとりあえず隠しとく -->
<!-- https://github.com/misskey-dev/misskey/issues/16091 -->
<!-- https://github.com/misskey-dev/misskey/issues/16663 -->
<!--<div>{{ i18n.ts._uploader.allowedTypes }}: {{ $i.policies.uploadableFileTypes.join(', ') }}</div>-->
</div>
</div>

View File

@ -65,6 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="layer.repeat">
<template #label>{{ i18n.ts._watermarkEditor.repeat }}</template>
</MkSwitch>
<MkSwitch v-model="layerPreserveBoundingRect">
<template #label>{{ i18n.ts._watermarkEditor.preserveBoundingRect }}</template>
</MkSwitch>
</template>
<template v-else-if="layer.type === 'image'">
@ -129,6 +133,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="layer.cover">
<template #label>{{ i18n.ts._watermarkEditor.cover }}</template>
</MkSwitch>
<MkSwitch v-model="layerPreserveBoundingRect">
<template #label>{{ i18n.ts._watermarkEditor.preserveBoundingRect }}</template>
</MkSwitch>
</template>
<template v-else-if="layer.type === 'qr'">
@ -335,7 +343,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed } from 'vue';
import * as Misskey from 'misskey-js';
import type { WatermarkPreset } from '@/utility/watermark.js';
import { i18n } from '@/i18n.js';
@ -351,6 +359,20 @@ import { misskeyApi } from '@/utility/misskey-api.js';
const layer = defineModel<WatermarkPreset['layers'][number]>('layer', { required: true });
const layerPreserveBoundingRect = computed({
get: () => {
if (layer.value.type === 'text' || layer.value.type === 'image') {
return !layer.value.noBoundingBoxExpansion;
}
return false;
},
set: (v: boolean) => {
if (layer.value.type === 'text' || layer.value.type === 'image') {
layer.value.noBoundingBoxExpansion = !v;
}
},
});
const driveFile = ref<Misskey.entities.DriveFile | null>(null);
const driveFileError = ref(false);
onMounted(async () => {

View File

@ -90,6 +90,7 @@ function createTextLayer(): WatermarkPreset['layers'][number] {
angle: 0,
opacity: 0.75,
repeat: false,
noBoundingBoxExpansion: false,
};
}
@ -104,6 +105,7 @@ function createImageLayer(): WatermarkPreset['layers'][number] {
angle: 0,
opacity: 0.75,
repeat: false,
noBoundingBoxExpansion: false,
cover: false,
};
}

View File

@ -62,9 +62,10 @@ import { onMounted, onUnmounted, ref, inject, useTemplateRef, computed } from 'v
import { scrollToTop } from '@@/js/scroll.js';
import XTabs from './MkPageHeader.tabs.vue';
import { globalEvents } from '@/events.js';
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { getAccountMenu } from '@/accounts.js';
import { $i } from '@/i.js';
import { DI } from '@/di.js';
import * as os from '@/os.js';
const props = withDefaults(defineProps<PageHeaderProps>(), {
tabs: () => ([] as Tab[]),
@ -99,10 +100,12 @@ const top = () => {
}
};
function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
async function openAccountMenu(ev: MouseEvent) {
const menuItems = await getAccountMenu({
withExtraOperation: true,
}, ev);
});
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
function onTabClick(): void {

View File

@ -71,7 +71,7 @@ import {
import * as os from '@/os.js';
import { createColumn } from '@/components/grid/column.js';
import { createRow, defaultGridRowSetting, resetRow } from '@/components/grid/row.js';
import { handleKeyEvent } from '@/utility/key-event.js';
import { makeHotkey } from '@/utility/hotkey.js';
type RowHolder = {
row: GridRow,
@ -289,9 +289,8 @@ function onKeyDown(ev: KeyboardEvent) {
const max = availableBounds.value;
const bounds = rangedBounds.value;
handleKeyEvent(ev, [
{
code: 'Delete', handler: () => {
makeHotkey({
'delete': () => {
if (rangedRows.value.length > 0) {
if (rowSetting.events.delete) {
rowSetting.events.delete(rangedRows.value);
@ -303,56 +302,54 @@ function onKeyDown(ev: KeyboardEvent) {
});
}
},
},
{
code: 'KeyC', modifiers: ['Control'], handler: () => {
'ctrl+c|meta+c': () => {
const context = createContext();
copyGridDataToClipboard(data.value, context);
},
},
{
code: 'KeyV', modifiers: ['Control'], handler: async () => {
'ctrl+v|meta+v': async () => {
const _cells = cells.value;
const context = createContext();
await pasteToGridFromClipboard(context, (row, col, parsedValue) => {
emitCellValue(_cells[row.index].cells[col.index], parsedValue);
});
},
},
{
code: 'ArrowRight', modifiers: ['Control', 'Shift'], handler: () => {
'ctrl+shift+right|meta+shift+right': () => {
updateSelectionRange({
leftTop: { col: selectedCellAddress.col, row: bounds.leftTop.row },
rightBottom: { col: max.rightBottom.col, row: bounds.rightBottom.row },
});
},
},
{
code: 'ArrowLeft', modifiers: ['Control', 'Shift'], handler: () => {
'ctrl+shift+left|meta+shift+left': () => {
updateSelectionRange({
leftTop: { col: max.leftTop.col, row: bounds.leftTop.row },
rightBottom: { col: selectedCellAddress.col, row: bounds.rightBottom.row },
});
},
},
{
code: 'ArrowUp', modifiers: ['Control', 'Shift'], handler: () => {
'ctrl+shift+up|meta+shift+up': () => {
updateSelectionRange({
leftTop: { col: bounds.leftTop.col, row: max.leftTop.row },
rightBottom: { col: bounds.rightBottom.col, row: selectedCellAddress.row },
});
},
},
{
code: 'ArrowDown', modifiers: ['Control', 'Shift'], handler: () => {
'ctrl+shift+down|meta+shift+down': () => {
updateSelectionRange({
leftTop: { col: bounds.leftTop.col, row: selectedCellAddress.row },
rightBottom: { col: bounds.rightBottom.col, row: max.rightBottom.row },
});
},
'ctrl+right|meta+right': () => {
selectionCell({ col: max.rightBottom.col, row: selectedCellAddress.row });
},
{
code: 'ArrowRight', modifiers: ['Shift'], handler: () => {
'ctrl+left|meta+left': () => {
selectionCell({ col: max.leftTop.col, row: selectedCellAddress.row });
},
'ctrl+up|meta+up': () => {
selectionCell({ col: selectedCellAddress.col, row: max.leftTop.row });
},
'ctrl+down|meta+down': () => {
selectionCell({ col: selectedCellAddress.col, row: max.rightBottom.row });
},
'shift+right': () => {
updateSelectionRange({
leftTop: {
col: bounds.leftTop.col < selectedCellAddress.col
@ -368,9 +365,7 @@ function onKeyDown(ev: KeyboardEvent) {
},
});
},
},
{
code: 'ArrowLeft', modifiers: ['Shift'], handler: () => {
'shift+left': () => {
updateSelectionRange({
leftTop: {
col: (bounds.leftTop.col < selectedCellAddress.col || bounds.rightBottom.col === selectedCellAddress.col)
@ -386,9 +381,7 @@ function onKeyDown(ev: KeyboardEvent) {
},
});
},
},
{
code: 'ArrowUp', modifiers: ['Shift'], handler: () => {
'shift+up': () => {
updateSelectionRange({
leftTop: {
col: bounds.leftTop.col,
@ -404,9 +397,7 @@ function onKeyDown(ev: KeyboardEvent) {
},
});
},
},
{
code: 'ArrowDown', modifiers: ['Shift'], handler: () => {
'shift+down': () => {
updateSelectionRange({
leftTop: {
col: bounds.leftTop.col,
@ -422,28 +413,19 @@ function onKeyDown(ev: KeyboardEvent) {
},
});
},
},
{
code: 'ArrowDown', handler: () => {
'down': () => {
selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row + 1 });
},
},
{
code: 'ArrowUp', handler: () => {
'up': () => {
selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row - 1 });
},
},
{
code: 'ArrowRight', handler: () => {
'right': () => {
selectionCell({ col: selectedCellAddress.col + 1, row: selectedCellAddress.row });
},
},
{
code: 'ArrowLeft', handler: () => {
'left': () => {
selectionCell({ col: selectedCellAddress.col - 1, row: selectedCellAddress.row });
},
},
]);
}, [])(ev);
break;
}

View File

@ -634,7 +634,9 @@ export function useUploader(options: {
bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW,
},
audio: {
bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW,
// Explicitly keep audio (don't discard) and copy it if possible
// without re-encoding to avoid WebCodecs limitations on iOS Safari
discard: false,
},
});

View File

@ -6,8 +6,8 @@
import type { Directive } from 'vue';
import { getBgColor } from '@/utility/get-bg-color.js';
export default {
mounted(src, binding, vn) {
export const adaptiveBgDirective = {
mounted(src) {
const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const myBg = window.getComputedStyle(src).backgroundColor;
@ -18,4 +18,4 @@ export default {
src.style.backgroundColor = myBg;
}
},
} as Directive;
} as Directive<HTMLElement>;

View File

@ -9,8 +9,8 @@ import { globalEvents } from '@/events.js';
const handlerMap = new WeakMap<any, any>();
export default {
mounted(src, binding, vn) {
export const adaptiveBorderDirective = {
mounted(src) {
function calc() {
const parentBg = getBgColor(src.parentElement) ?? 'transparent';
@ -30,7 +30,7 @@ export default {
globalEvents.on('themeChanged', calc);
},
unmounted(src, binding, vn) {
unmounted(src) {
globalEvents.off('themeChanged', handlerMap.get(src));
},
} as Directive;
} as Directive<HTMLElement>;

View File

@ -5,8 +5,8 @@
import type { Directive } from 'vue';
export default {
beforeMount(src, binding, vn) {
export const animDirective = {
beforeMount(src) {
src.style.opacity = '0';
src.style.transform = 'scale(0.9)';
// ページネーションと相性が悪いので
@ -14,10 +14,10 @@ export default {
src.classList.add('_zoom');
},
mounted(src, binding, vn) {
mounted(src) {
window.setTimeout(() => {
src.style.opacity = '1';
src.style.transform = 'none';
}, 1);
},
} as Directive;
} as Directive<HTMLElement>;

View File

@ -5,13 +5,18 @@
import { throttle } from 'throttle-debounce';
import type { Directive } from 'vue';
import type { Awaitable } from '@/types/misc.js';
export default {
mounted(src, binding, vn) {
interface HTMLElementWithObserver extends HTMLElement {
_observer_?: IntersectionObserver;
}
export const appearDirective = {
mounted(src, binding) {
const fn = binding.value;
if (fn == null) return;
const check = throttle(1000, (entries) => {
const check = throttle<IntersectionObserverCallback>(1000, (entries) => {
if (entries.some(entry => entry.isIntersecting)) {
fn();
}
@ -24,7 +29,7 @@ export default {
src._observer_ = observer;
},
unmounted(src, binding, vn) {
unmounted(src) {
if (src._observer_) src._observer_.disconnect();
},
} as Directive;
} as Directive<HTMLElementWithObserver, (() => Awaitable<void>) | null | undefined>;

View File

@ -6,8 +6,8 @@
import type { Directive } from 'vue';
import { prefer } from '@/preferences.js';
export default {
mounted(el: HTMLElement, binding, vn) {
export const clickAnimeDirective = {
mounted(el) {
if (!prefer.s.animation) return;
const target = el.children[0];
@ -37,4 +37,4 @@ export default {
target.classList.add('_anime_bounce_standBy');
});
},
} as Directive;
} as Directive<HTMLElement>;

View File

@ -6,8 +6,12 @@
import type { Directive } from 'vue';
import { getScrollContainer, getScrollPosition } from '@@/js/scroll.js';
export default {
mounted(src, binding, vn) {
interface HTMLElementWithRO extends HTMLElement {
_ro_?: ResizeObserver;
}
export const followAppendDirective = {
mounted(src, binding) {
if (binding.value === false) return;
let isBottom = true;
@ -34,7 +38,7 @@ export default {
src._ro_ = ro;
},
unmounted(src, binding, vn) {
unmounted(src) {
if (src._ro_) src._ro_.unobserve(src);
},
} as Directive;
} as Directive<HTMLElementWithRO, boolean>;

View File

@ -37,8 +37,10 @@ function calc(src: Element) {
info.fn(width, height);
}
export default {
mounted(src, binding, vn) {
type SizeCallback = (w: number, h: number) => void;
export const getSizeDirective = {
mounted(src, binding) {
const resize = new ResizeObserver((entries, observer) => {
calc(src);
});
@ -48,7 +50,7 @@ export default {
calc(src);
},
unmounted(src, binding, vn) {
unmounted(src, binding) {
binding.value(0, 0);
const info = mountings.get(src);
if (!info) return;
@ -56,4 +58,4 @@ export default {
if (info.intersection) info.intersection.disconnect();
mountings.delete(src);
},
} as Directive<Element, (w: number, h: number) => void>;
} as Directive<Element, SizeCallback>;

View File

@ -5,8 +5,14 @@
import type { Directive } from 'vue';
import { makeHotkey } from '@/utility/hotkey.js';
import type { Keymap } from '@/utility/hotkey.js';
export default {
interface HTMLElementWithHotkey extends HTMLElement {
_hotkey_global?: boolean;
_keyHandler?: (ev: KeyboardEvent) => void;
}
export const hotkeyDirective = {
mounted(el, binding) {
el._hotkey_global = binding.modifiers.global === true;
@ -20,10 +26,11 @@ export default {
},
unmounted(el) {
if (el._keyHandler == null) return;
if (el._hotkey_global) {
window.document.removeEventListener('keydown', el._keyHandler);
} else {
el.removeEventListener('keydown', el._keyHandler);
}
},
} as Directive;
} as Directive<HTMLElementWithHotkey, Keymap>;

View File

@ -3,19 +3,19 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { App } from 'vue';
import type { App, Directive } from 'vue';
import userPreview from './user-preview.js';
import getSize from './get-size.js';
import ripple from './ripple.js';
import tooltip from './tooltip.js';
import hotkey from './hotkey.js';
import appear from './appear.js';
import anim from './anim.js';
import clickAnime from './click-anime.js';
import panel from './panel.js';
import adaptiveBorder from './adaptive-border.js';
import adaptiveBg from './adaptive-bg.js';
import { userPreviewDirective } from './user-preview.js';
import { getSizeDirective } from './get-size.js';
import { rippleDirective } from './ripple.js';
import { tooltipDirective } from './tooltip.js';
import { hotkeyDirective } from './hotkey.js';
import { appearDirective } from './appear.js';
import { animDirective } from './anim.js';
import { clickAnimeDirective } from './click-anime.js';
import { panelDirective } from './panel.js';
import { adaptiveBorderDirective } from './adaptive-border.js';
import { adaptiveBgDirective } from './adaptive-bg.js';
export default function(app: App) {
for (const [key, value] of Object.entries(directives)) {
@ -24,16 +24,32 @@ export default function(app: App) {
}
export const directives = {
'userPreview': userPreview,
'user-preview': userPreview,
'get-size': getSize,
'ripple': ripple,
'tooltip': tooltip,
'hotkey': hotkey,
'appear': appear,
'anim': anim,
'click-anime': clickAnime,
'panel': panel,
'adaptive-border': adaptiveBorder,
'adaptive-bg': adaptiveBg,
};
'userPreview': userPreviewDirective,
'user-preview': userPreviewDirective,
'get-size': getSizeDirective,
'ripple': rippleDirective,
'tooltip': tooltipDirective,
'hotkey': hotkeyDirective,
'appear': appearDirective,
'anim': animDirective,
'click-anime': clickAnimeDirective,
'panel': panelDirective,
'adaptive-border': adaptiveBorderDirective,
'adaptive-bg': adaptiveBgDirective,
} as Record<string, Directive>;
declare module 'vue' {
export interface ComponentCustomProperties {
vUserPreview: typeof userPreviewDirective;
vGetSize: typeof getSizeDirective;
vRipple: typeof rippleDirective;
vTooltip: typeof tooltipDirective;
vHotkey: typeof hotkeyDirective;
vAppear: typeof appearDirective;
vAnim: typeof animDirective;
vClickAnime: typeof clickAnimeDirective;
vPanel: typeof panelDirective;
vAdaptiveBorder: typeof adaptiveBorderDirective;
vAdaptiveBg: typeof adaptiveBgDirective;
}
}

View File

@ -6,8 +6,8 @@
import type { Directive } from 'vue';
import { getBgColor } from '@/utility/get-bg-color.js';
export default {
mounted(src, binding, vn) {
export const panelDirective = {
mounted(src) {
const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const myBg = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel');
@ -18,4 +18,4 @@ export default {
src.style.backgroundColor = 'var(--MI_THEME-panel)';
}
},
} as Directive;
} as Directive<HTMLElement>;

View File

@ -3,12 +3,13 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { Directive } from 'vue';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { prefer } from '@/preferences.js';
import { popup } from '@/os.js';
export default {
mounted(el, binding, vn) {
export const rippleDirective = {
mounted(el, binding) {
// 明示的に false であればバインドしない
if (binding.value === false) return;
if (!prefer.s.animation) return;
@ -24,4 +25,4 @@ export default {
});
});
},
};
} as Directive<HTMLElement, boolean | undefined>;

View File

@ -14,13 +14,30 @@ import { popup, alert } from '@/os.js';
const start = isTouchUsing ? 'touchstart' : 'mouseenter';
const end = isTouchUsing ? 'touchend' : 'mouseleave';
export default {
mounted(el: HTMLElement, binding, vn) {
type TooltipDirectiveState = {
text: string | null | undefined;
_close: null | (() => void);
showTimer: number | null;
hideTimer: number | null;
checkTimer: number | null;
show: () => void;
close: () => void;
};
interface TooltipDirectiveElement extends HTMLElement {
_tooltipDirective_?: TooltipDirectiveState;
}
type TooltipDirectiveModifiers = 'left' | 'right' | 'top' | 'bottom' | 'mfm' | 'noDelay';
type TooltipDirectiveArg = 'dialog';
export const tooltipDirective = {
mounted(el, binding) {
const delay = binding.modifiers.noDelay ? 0 : 100;
const self = (el as any)._tooltipDirective_ = {} as any;
const self = el._tooltipDirective_ = {} as TooltipDirectiveState;
self.text = binding.value as string;
self.text = binding.value;
self._close = null;
self.showTimer = null;
self.hideTimer = null;
@ -28,7 +45,7 @@ export default {
self.close = () => {
if (self._close) {
window.clearInterval(self.checkTimer);
if (self.checkTimer) window.clearInterval(self.checkTimer);
self._close();
self._close = null;
}
@ -36,6 +53,7 @@ export default {
if (binding.arg === 'dialog') {
el.addEventListener('click', (ev) => {
if (binding.value == null) return;
ev.preventDefault();
ev.stopPropagation();
alert({
@ -72,8 +90,8 @@ export default {
});
el.addEventListener(start, (ev) => {
window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer);
if (self.showTimer) window.clearTimeout(self.showTimer);
if (self.hideTimer) window.clearTimeout(self.hideTimer);
if (delay === 0) {
self.show();
} else {
@ -82,8 +100,8 @@ export default {
}, { passive: true });
el.addEventListener(end, () => {
window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer);
if (self.showTimer) window.clearTimeout(self.showTimer);
if (self.hideTimer) window.clearTimeout(self.hideTimer);
if (delay === 0) {
self.close();
} else {
@ -92,18 +110,23 @@ export default {
}, { passive: true });
el.addEventListener('click', () => {
window.clearTimeout(self.showTimer);
if (self.showTimer) window.clearTimeout(self.showTimer);
self.close();
});
},
updated(el, binding) {
const self = el._tooltipDirective_;
if (self == null) return;
self.text = binding.value as string;
},
unmounted(el, binding, vn) {
unmounted(el) {
const self = el._tooltipDirective_;
window.clearInterval(self.checkTimer);
if (self == null) return;
if (self.showTimer) window.clearTimeout(self.showTimer);
if (self.hideTimer) window.clearTimeout(self.hideTimer);
if (self.checkTimer) window.clearTimeout(self.checkTimer);
self.close();
},
} as Directive;
} as Directive<TooltipDirectiveElement, string | null | undefined, TooltipDirectiveModifiers, TooltipDirectiveArg>;

View File

@ -5,18 +5,19 @@
import { defineAsyncComponent, ref } from 'vue';
import type { Directive } from 'vue';
import * as Misskey from 'misskey-js';
import { popup } from '@/os.js';
import { isTouchUsing } from '@/utility/touch.js';
export class UserPreview {
private el;
private user;
private showTimer;
private hideTimer;
private checkTimer;
private promise;
private el: HTMLElement;
private user: string | Misskey.entities.UserDetailed;
private showTimer: number | null = null;
private hideTimer: number | null = null;
private checkTimer: number | null = null;
private promise: null | { cancel: () => void } = null;
constructor(el, user) {
constructor(el: HTMLElement, user: string | Misskey.entities.UserDetailed) {
this.el = el;
this.user = user;
@ -43,10 +44,10 @@ export class UserPreview {
source: this.el,
}, {
mouseover: () => {
window.clearTimeout(this.hideTimer);
if (this.hideTimer) window.clearTimeout(this.hideTimer);
},
mouseleave: () => {
window.clearTimeout(this.showTimer);
if (this.showTimer) window.clearTimeout(this.showTimer);
this.hideTimer = window.setTimeout(this.close, 500);
},
closed: () => dispose(),
@ -60,8 +61,8 @@ export class UserPreview {
this.checkTimer = window.setInterval(() => {
if (!window.document.body.contains(this.el)) {
window.clearTimeout(this.showTimer);
window.clearTimeout(this.hideTimer);
if (this.showTimer) window.clearTimeout(this.showTimer);
if (this.hideTimer) window.clearTimeout(this.hideTimer);
this.close();
}
}, 1000);
@ -69,26 +70,26 @@ export class UserPreview {
private close() {
if (this.promise) {
window.clearInterval(this.checkTimer);
if (this.checkTimer) window.clearInterval(this.checkTimer);
this.promise.cancel();
this.promise = null;
}
}
private onMouseover() {
window.clearTimeout(this.showTimer);
window.clearTimeout(this.hideTimer);
if (this.showTimer) window.clearTimeout(this.showTimer);
if (this.hideTimer) window.clearTimeout(this.hideTimer);
this.showTimer = window.setTimeout(this.show, 500);
}
private onMouseleave() {
window.clearTimeout(this.showTimer);
window.clearTimeout(this.hideTimer);
if (this.showTimer) window.clearTimeout(this.showTimer);
if (this.hideTimer) window.clearTimeout(this.hideTimer);
this.hideTimer = window.setTimeout(this.close, 500);
}
private onClick() {
window.clearTimeout(this.showTimer);
if (this.showTimer) window.clearTimeout(this.showTimer);
this.close();
}
@ -105,8 +106,14 @@ export class UserPreview {
}
}
export default {
mounted(el: HTMLElement, binding, vn) {
interface UserPreviewDirectiveElement extends HTMLElement {
_userPreviewDirective_?: {
preview: UserPreview;
};
}
export const userPreviewDirective = {
mounted(el, binding) {
if (binding.value == null) return;
if (isTouchUsing) return;
@ -117,10 +124,11 @@ export default {
self.preview = new UserPreview(el, binding.value);
},
unmounted(el, binding, vn) {
unmounted(el, binding) {
if (binding.value == null) return;
const self = el._userPreviewDirective_;
if (self == null) return;
self.preview.detach();
},
} as Directive;
} as Directive<UserPreviewDirectiveElement, string | Misskey.entities.UserDetailed | null | undefined>;

View File

@ -25,6 +25,7 @@ export type Keys = (
'bootloaderLocales' |
'theme' |
'themeId' |
'themeCachedVersion' |
'customCss' |
'chatMessageDrafts' |
'scratchpad' |

View File

@ -295,6 +295,9 @@ const patronsWithIcon = [{
}, {
name: 'しゃどかの',
icon: 'https://assets.misskey-hub.net/patrons/5bec3c6b402942619e03f7a2ae76d69e.jpg',
}, {
name: '大賀愛一郎',
icon: 'https://assets.misskey-hub.net/patrons/c701a797d1df4125970f25d3052250ac.jpg',
}];
const patrons = [

View File

@ -503,7 +503,7 @@ function refreshGridItems() {
name: it.name,
host: it.host ?? '',
category: it.category ?? '',
aliases: it.aliases.join(','),
aliases: it.aliases.join(' '),
license: it.license ?? '',
isSensitive: it.isSensitive,
localOnly: it.localOnly,

View File

@ -53,6 +53,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</SearchMarker>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="showRoleBadgesOfRemoteUsers" @change="onChange_showRoleBadgesOfRemoteUsers">
<template #label><SearchLabel>{{ i18n.ts.showRoleBadgesOfRemoteUsers }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template>
@ -188,6 +197,7 @@ const enableIdenticonGeneration = ref(meta.enableIdenticonGeneration);
const enableChartsForRemoteUser = ref(meta.enableChartsForRemoteUser);
const enableStatsForFederatedInstances = ref(meta.enableStatsForFederatedInstances);
const enableChartsForFederatedInstances = ref(meta.enableChartsForFederatedInstances);
const showRoleBadgesOfRemoteUsers = ref(meta.showRoleBadgesOfRemoteUsers);
function onChange_enableServerMachineStats(value: boolean) {
os.apiWithDialog('admin/update-meta', {
@ -229,6 +239,14 @@ function onChange_enableChartsForFederatedInstances(value: boolean) {
});
}
function onChange_showRoleBadgesOfRemoteUsers(value: boolean) {
os.apiWithDialog('admin/update-meta', {
showRoleBadgesOfRemoteUsers: value,
}).then(() => {
fetchInstance(true);
});
}
const fttForm = useForm({
enableFanoutTimeline: meta.enableFanoutTimeline,
enableFanoutTimelineDbFallback: meta.enableFanoutTimelineDbFallback,

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder v-for="token in items" :key="token.id" :defaultOpen="true">
<template #icon>
<img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/>
<i v-else class="ti ti-plug"/>
<i v-else class="ti ti-plug"></i>
</template>
<template #label>{{ token.name }}</template>
<template #caption>{{ token.description }}</template>
@ -86,6 +86,7 @@ definePage(() => ({
<style lang="scss" module>
.appIcon {
display: block;
width: 20px;
height: 20px;
border-radius: 4px;

View File

@ -67,7 +67,6 @@ import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { prefer } from '@/preferences.js';
import { PREF_DEF } from '@/preferences/def.js';
import { getInitialPrefValue } from '@/preferences/manager.js';
import { genId } from '@/utility/id.js';
@ -77,12 +76,13 @@ const items = ref(prefer.s.menu.map(x => ({
id: genId(),
type: x,
})));
const itemTypeValues = computed(() => items.value.map(x => x.type));
const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
const showNavbarSubButtons = prefer.model('showNavbarSubButtons');
async function addItem() {
const menu = Object.keys(navbarItemDef).filter(k => !prefer.s.menu.includes(k));
const menu = Object.keys(navbarItemDef).filter(k => !itemTypeValues.value.includes(k));
const { canceled, result: item } = await os.select({
title: i18n.ts.addItem,
items: [...menu.map(k => ({
@ -102,8 +102,9 @@ function removeItem(index: number) {
items.value.splice(index, 1);
}
async function save() {
prefer.commit('menu', items.value.map(x => x.type));
function save() {
prefer.commit('menu', itemTypeValues.value);
os.success();
}
function reset() {

View File

@ -475,6 +475,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="disableShowingAnimatedImages">
<MkSwitch v-model="disableShowingAnimatedImages">
<template #label><SearchLabel>{{ i18n.ts.disableShowingAnimatedImages }}</SearchLabel></template>
<template #caption>{{ i18n.ts.disableShowingAnimatedImages_caption }}</template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>

View File

@ -368,27 +368,19 @@ onDeactivated(disposeBannerParallaxResizeObserver);
> .banner-container {
position: relative;
height: 250px;
--bannerHeight: 250px;
height: var(--bannerHeight);
overflow: clip;
background-size: cover;
background-position: center;
view-timeline-name: --bannerParallax;
view-timeline-inset: var(--bannerParallaxInset, auto);
view-timeline-axis: block;
> .banner {
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 300%;
height: 100%;
background-size: cover;
background-color: #4c5e6d;
background-repeat: repeat-y;
background-position: center;
will-change: transform;
animation: bannerParallaxKeyframes linear both;
animation-timeline: --bannerParallax;
animation-range: cover;
background-position-x: center;
background-position-y: 50%;
will-change: background-position-y;
}
> .fade {
@ -681,7 +673,8 @@ onDeactivated(disposeBannerParallaxResizeObserver);
> .main {
> .profile > .main {
> .banner-container {
height: 140px;
--bannerHeight: 140px;
height: var(--bannerHeight);
> .fade {
display: none;
@ -745,12 +738,32 @@ onDeactivated(disposeBannerParallaxResizeObserver);
}
}
@supports (view-timeline-name: --name) {
.ftskorzw {
> .main {
> .profile > .main {
> .banner-container {
view-timeline-name: --bannerParallax;
view-timeline-inset: var(--bannerParallaxInset, auto);
view-timeline-axis: block;
> .banner {
animation: bannerParallaxKeyframes linear both;
animation-timeline: --bannerParallax;
animation-range: cover;
}
}
}
}
}
}
@keyframes bannerParallaxKeyframes {
from {
transform: translateY(-50%);
background-position-y: 50%;
}
to {
transform: translateY(-30%);
background-position-y: calc(50% + var(--bannerHeight, 250px) / 3);
}
}
</style>

View File

@ -0,0 +1,85 @@
// Description : Array and textureless GLSL 2D/3D/4D simplex
// noise functions.
// Author : Ian McEwan, Ashima Arts.
// Maintainer : stegu
// Lastmod : 20201014 (stegu)
// License : Copyright (C) 2011 Ashima Arts. All rights reserved.
// Distributed under the MIT License. See LICENSE file.
// https://github.com/ashima/webgl-noise
// https://github.com/stegu/webgl-noise
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 mod289(vec4 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec4 permute(vec4 x) {
return mod289(((x * 34.0) + 10.0) * x);
}
vec4 taylorInvSqrt(vec4 r) {
return 1.79284291400159 - 0.85373472095314 * r;
}
float snoise(vec3 v) {
const vec2 C = vec2(1.0/6.0, 1.0/3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i);
vec4 p = permute(permute(permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.5 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
m = m * m;
return 105.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}

View File

@ -10,6 +10,7 @@ import tinycolor from 'tinycolor2';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import JSON5 from 'json5';
import { version } from '@@/js/config.js';
import type { Ref } from 'vue';
import type { BundledTheme } from 'shiki/themes';
import { deepClone } from '@/utility/clone.js';
@ -123,6 +124,7 @@ function applyThemeInternal(theme: Theme, persist: boolean) {
if (persist) {
miLocalStorage.setItem('theme', JSON.stringify(props));
miLocalStorage.setItem('themeId', theme.id);
miLocalStorage.setItem('themeCachedVersion', version);
miLocalStorage.setItem('colorScheme', colorScheme);
}
@ -131,7 +133,7 @@ function applyThemeInternal(theme: Theme, persist: boolean) {
}
let timeout: number | null = null;
let currentTheme: Theme | null = null;
let currentThemeId = miLocalStorage.getItem('themeId');
export function applyTheme(theme: Theme, persist = true) {
if (timeout) {
@ -139,9 +141,8 @@ export function applyTheme(theme: Theme, persist = true) {
timeout = null;
}
if (deepEqual(currentTheme, theme)) return;
// リアクティビティ解除
currentTheme = deepClone(theme);
if (theme.id === currentThemeId && miLocalStorage.getItem('themeCachedVersion') === version) return;
currentThemeId = theme.id;
if (window.document.startViewTransition != null) {
window.document.documentElement.classList.add('_themeChanging_');

View File

@ -1,8 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
export type WithNonNullable<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]> };

View File

@ -0,0 +1,6 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export type Awaitable <T> = T | Promise<T>;

View File

@ -55,7 +55,7 @@ import MkButton from '@/components/MkButton.vue';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { getAccountMenu } from '@/accounts.js';
import { $i } from '@/i.js';
import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
@ -84,10 +84,12 @@ async function more(ev: MouseEvent) {
});
}
function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
async function openAccountMenu(ev: MouseEvent) {
const menuItems = await getAccountMenu({
withExtraOperation: true,
}, ev);
});
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
onMounted(() => {

View File

@ -109,7 +109,7 @@ import { instance } from '@/instance.js';
import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
import { useRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { getAccountMenu } from '@/accounts.js';
import { $i } from '@/i.js';
const router = useRouter();
@ -170,10 +170,12 @@ function toggleRealtimeMode(ev: MouseEvent) {
}], ev.currentTarget ?? ev.target);
}
function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
async function openAccountMenu(ev: MouseEvent) {
const menuItems = await getAccountMenu({
withExtraOperation: true,
}, ev);
});
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}
async function more(ev: MouseEvent) {

View File

@ -167,7 +167,7 @@ const columnsEl = useTemplateRef('columnsEl');
const addColumn = async (ev) => {
const { canceled, result: column } = await os.select({
title: i18n.ts._deck.addColumn,
items: columnTypes.map(column => ({
items: columnTypes.filter(column => column !== 'chat' || $i == null || $i.policies.chatAvailability !== 'unavailable').map(column => ({
value: column, label: i18n.ts._deck._columns[column],
})),
});

View File

@ -7,21 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<XColumn :column="column" :isStacked="isStacked">
<template #header><i class="ti ti-messages" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.chat }}</template>
<div style="padding: 8px;">
<MkChatHistories/>
<div style="padding: 8px;" class="_gaps">
<MkInfo v-if="$i.policies.chatAvailability === 'readonly'">{{ i18n.ts._chat.chatIsReadOnlyForThisAccountOrServer }}</MkInfo>
<MkInfo v-else-if="$i.policies.chatAvailability === 'unavailable'" warn>{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo>
<MkChatHistories v-if="$i.policies.chatAvailability !== 'unavailable'"/>
</div>
</XColumn>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ensureSignin } from '@/i.js';
import { i18n } from '../../i18n.js';
import XColumn from './column.vue';
import type { Column } from '@/deck.js';
import MkInfo from '@/components/MkInfo.vue';
import MkChatHistories from '@/components/MkChatHistories.vue';
defineProps<{
column: Column;
isStacked: boolean;
}>();
const $i = ensureSignin();
</script>

View File

@ -62,15 +62,18 @@ const props = withDefaults(defineProps<{
column: Column;
isStacked?: boolean;
naked?: boolean;
handleScrollToTop?: boolean;
menu?: MenuItem[];
refresher?: () => Promise<void>;
}>(), {
isStacked: false,
naked: false,
handleScrollToTop: true,
});
const emit = defineEmits<{
(ev: 'headerWheel', ctx: WheelEvent): void;
(ev: 'headerClick', ctx: MouseEvent): void;
}>();
const body = useTemplateRef('body');
@ -252,7 +255,10 @@ function onContextmenu(ev: MouseEvent) {
os.contextMenu(getMenu(), ev);
}
function goTop() {
function goTop(ev: MouseEvent) {
emit('headerClick', ev);
if (!props.handleScrollToTop) return;
if (body.value) {
body.value.scrollTo({
top: 0,

View File

@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<XColumn v-if="prefer.s['deck.alwaysShowMainColumn'] || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked">
<XColumn
v-if="prefer.s['deck.alwaysShowMainColumn'] || mainRouter.currentRoute.value.name !== 'index'"
:column="column"
:isStacked="isStacked"
:handleScrollToTop="false"
@headerClick="onHeaderClick"
>
<template #header>
<template v-if="pageMetadata">
<i :class="pageMetadata.icon"></i>
@ -12,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</template>
<div style="height: 100%;">
<div ref="rootEl" style="height: 100%;">
<StackingRouterView v-if="prefer.s['experimental.stackingRouterView']" @contextmenu.stop="onContextmenu"/>
<RouterView v-else @contextmenu.stop="onContextmenu"/>
</div>
@ -20,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { provide, shallowRef, ref } from 'vue';
import { provide, useTemplateRef, ref } from 'vue';
import { isLink } from '@@/js/is-link.js';
import XColumn from './column.vue';
import type { Column } from '@/deck.js';
@ -38,6 +44,7 @@ defineProps<{
}>();
const pageMetadata = ref<null | PageMetadata>(null);
const rootEl = useTemplateRef('rootEl');
provide(DI.router, mainRouter);
provideMetadataReceiver((metadataGetter) => {
@ -69,4 +76,15 @@ function onContextmenu(ev: MouseEvent) {
},
}], ev);
}
function onHeaderClick() {
if (!rootEl.value) return;
const scrollEl = rootEl.value.querySelector<HTMLElement>('._pageScrollable,._pageScrollableReversed');
if (scrollEl) {
scrollEl.scrollTo({
top: 0,
behavior: 'smooth',
});
}
}
</script>

View File

@ -50,12 +50,12 @@ let latestHotkey: Pattern & { callback: CallbackFunction } | null = null;
//#endregion
//#region impl
export const makeHotkey = (keymap: Keymap) => {
export const makeHotkey = (keymap: Keymap, ignoreElements = IGNORE_ELEMENTS) => {
const actions = parseKeymap(keymap);
return (ev: KeyboardEvent) => {
if ('pswp' in window && window.pswp != null) return;
if (window.document.activeElement != null) {
if (IGNORE_ELEMENTS.includes(window.document.activeElement.tagName.toLowerCase())) return;
if (ignoreElements.includes(window.document.activeElement.tagName.toLowerCase())) return;
if (getHTMLElementOrNull(window.document.activeElement)?.isContentEditable) return;
}
for (const action of actions) {

View File

@ -0,0 +1,43 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform int u_amount;
uniform float u_shiftStrengths[128];
uniform vec2 u_shiftOrigins[128];
uniform vec2 u_shiftSizes[128];
uniform float u_channelShift;
out vec4 out_color;
void main() {
// TODO: ピクセル毎に計算する必要はないのでuniformにする
float aspect_ratio = min(in_resolution.x, in_resolution.y) / max(in_resolution.x, in_resolution.y);
float aspect_ratio_x = in_resolution.x > in_resolution.y ? 1.0 : aspect_ratio;
float aspect_ratio_y = in_resolution.x < in_resolution.y ? 1.0 : aspect_ratio;
float v = 0.0;
for (int i = 0; i < u_amount; i++) {
if (
in_uv.x * aspect_ratio_x > ((u_shiftOrigins[i].x * aspect_ratio_x) - u_shiftSizes[i].x) &&
in_uv.x * aspect_ratio_x < ((u_shiftOrigins[i].x * aspect_ratio_x) + u_shiftSizes[i].x) &&
in_uv.y * aspect_ratio_y > ((u_shiftOrigins[i].y * aspect_ratio_y) - u_shiftSizes[i].y) &&
in_uv.y * aspect_ratio_y < ((u_shiftOrigins[i].y * aspect_ratio_y) + u_shiftSizes[i].y)
) {
v += u_shiftStrengths[i];
}
}
float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r;
float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g;
float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b;
float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a;
out_color = vec4(r, g, b, a);
}

View File

@ -4,49 +4,10 @@
*/
import seedrandom from 'seedrandom';
import shader from './blockNoise.glsl';
import { defineImageEffectorFx } from '../ImageEffector.js';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform int u_amount;
uniform float u_shiftStrengths[128];
uniform vec2 u_shiftOrigins[128];
uniform vec2 u_shiftSizes[128];
uniform float u_channelShift;
out vec4 out_color;
void main() {
// TODO: ピクセル毎に計算する必要はないのでuniformにする
float aspect_ratio = min(in_resolution.x, in_resolution.y) / max(in_resolution.x, in_resolution.y);
float aspect_ratio_x = in_resolution.x > in_resolution.y ? 1.0 : aspect_ratio;
float aspect_ratio_y = in_resolution.x < in_resolution.y ? 1.0 : aspect_ratio;
float v = 0.0;
for (int i = 0; i < u_amount; i++) {
if (
in_uv.x * aspect_ratio_x > ((u_shiftOrigins[i].x * aspect_ratio_x) - u_shiftSizes[i].x) &&
in_uv.x * aspect_ratio_x < ((u_shiftOrigins[i].x * aspect_ratio_x) + u_shiftSizes[i].x) &&
in_uv.y * aspect_ratio_y > ((u_shiftOrigins[i].y * aspect_ratio_y) - u_shiftSizes[i].y) &&
in_uv.y * aspect_ratio_y < ((u_shiftOrigins[i].y * aspect_ratio_y) + u_shiftSizes[i].y)
) {
v += u_shiftStrengths[i];
}
}
float r = texture(in_texture, vec2(in_uv.x + (v * (1.0 + u_channelShift)), in_uv.y)).r;
float g = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).g;
float b = texture(in_texture, vec2(in_uv.x + (v * (1.0 + (u_channelShift / 2.0))), in_uv.y)).b;
float a = texture(in_texture, vec2(in_uv.x + v, in_uv.y)).a;
out_color = vec4(r, g, b, a);
}
`;
export const FX_blockNoise = defineImageEffectorFx({
id: 'blockNoise',
name: i18n.ts._imageEffector._fxs.glitch + ': ' + i18n.ts._imageEffector._fxs.blockNoise,

View File

@ -0,0 +1,78 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
const float PI = 3.141592653589793;
const float TWO_PI = 6.283185307179586;
const float HALF_PI = 1.5707963267948966;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform vec2 u_offset;
uniform vec2 u_scale;
uniform bool u_ellipse;
uniform float u_angle;
uniform float u_radius;
uniform int u_samples;
out vec4 out_color;
void main() {
float angle = -(u_angle * PI);
vec2 centeredUv = in_uv - vec2(0.5, 0.5) - u_offset;
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
) + u_offset;
bool isInside = false;
if (u_ellipse) {
vec2 norm = (rotatedUV - u_offset) / u_scale;
isInside = dot(norm, norm) <= 1.0;
} else {
isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y;
}
if (!isInside) {
out_color = texture(in_texture, in_uv);
return;
}
vec4 result = vec4(0.0);
float totalSamples = 0.0;
// Make blur radius resolution-independent by using a percentage of image size
// This ensures consistent visual blur regardless of image resolution
float referenceSize = min(in_resolution.x, in_resolution.y);
float normalizedRadius = u_radius / 100.0; // Convert radius to percentage (0-15 -> 0-0.15)
vec2 blurOffset = vec2(normalizedRadius) / in_resolution * referenceSize;
// Calculate how many samples to take in each direction
// This determines the grid density, not the blur extent
int sampleRadius = int(sqrt(float(u_samples)) / 2.0);
// Sample in a grid pattern within the specified radius
for (int x = -sampleRadius; x <= sampleRadius; x++) {
for (int y = -sampleRadius; y <= sampleRadius; y++) {
// Normalize the grid position to [-1, 1] range
float normalizedX = float(x) / float(sampleRadius);
float normalizedY = float(y) / float(sampleRadius);
// Scale by radius to get the actual sampling offset
vec2 offset = vec2(normalizedX, normalizedY) * blurOffset;
vec2 sampleUV = in_uv + offset;
// Only sample if within texture bounds
if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 && sampleUV.y >= 0.0 && sampleUV.y <= 1.0) {
result += texture(in_texture, sampleUV);
totalSamples += 1.0;
}
}
}
out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv);
}

View File

@ -4,83 +4,9 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './blur.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
const float PI = 3.141592653589793;
const float TWO_PI = 6.283185307179586;
const float HALF_PI = 1.5707963267948966;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform vec2 u_offset;
uniform vec2 u_scale;
uniform bool u_ellipse;
uniform float u_angle;
uniform float u_radius;
uniform int u_samples;
out vec4 out_color;
void main() {
float angle = -(u_angle * PI);
vec2 centeredUv = in_uv - vec2(0.5, 0.5) - u_offset;
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
) + u_offset;
bool isInside = false;
if (u_ellipse) {
vec2 norm = (rotatedUV - u_offset) / u_scale;
isInside = dot(norm, norm) <= 1.0;
} else {
isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y;
}
if (!isInside) {
out_color = texture(in_texture, in_uv);
return;
}
vec4 result = vec4(0.0);
float totalSamples = 0.0;
// Make blur radius resolution-independent by using a percentage of image size
// This ensures consistent visual blur regardless of image resolution
float referenceSize = min(in_resolution.x, in_resolution.y);
float normalizedRadius = u_radius / 100.0; // Convert radius to percentage (0-15 -> 0-0.15)
vec2 blurOffset = vec2(normalizedRadius) / in_resolution * referenceSize;
// Calculate how many samples to take in each direction
// This determines the grid density, not the blur extent
int sampleRadius = int(sqrt(float(u_samples)) / 2.0);
// Sample in a grid pattern within the specified radius
for (int x = -sampleRadius; x <= sampleRadius; x++) {
for (int y = -sampleRadius; y <= sampleRadius; y++) {
// Normalize the grid position to [-1, 1] range
float normalizedX = float(x) / float(sampleRadius);
float normalizedY = float(y) / float(sampleRadius);
// Scale by radius to get the actual sampling offset
vec2 offset = vec2(normalizedX, normalizedY) * blurOffset;
vec2 sampleUV = in_uv + offset;
// Only sample if within texture bounds
if (sampleUV.x >= 0.0 && sampleUV.x <= 1.0 && sampleUV.y >= 0.0 && sampleUV.y <= 1.0) {
result += texture(in_texture, sampleUV);
totalSamples += 1.0;
}
}
}
out_color = totalSamples > 0.0 ? result / totalSamples : texture(in_texture, in_uv);
}
`;
export const FX_blur = defineImageEffectorFx({
id: 'blur',
name: i18n.ts._imageEffector._fxs.blur,

View File

@ -0,0 +1,43 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
const float PI = 3.141592653589793;
const float TWO_PI = 6.283185307179586;
const float HALF_PI = 1.5707963267948966;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_angle;
uniform float u_scale;
uniform vec3 u_color;
uniform float u_opacity;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
float x_ratio = max(in_resolution.x / in_resolution.y, 1.0);
float y_ratio = max(in_resolution.y / in_resolution.x, 1.0);
float angle = -(u_angle * PI);
vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio);
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
);
float fmodResult = mod(floor(u_scale * rotatedUV.x) + floor(u_scale * rotatedUV.y), 2.0);
float fin = max(sign(fmodResult), 0.0);
out_color = vec4(
mix(in_color.r, u_color.r, fin * u_opacity),
mix(in_color.g, u_color.g, fin * u_opacity),
mix(in_color.b, u_color.b, fin * u_opacity),
in_color.a
);
}

View File

@ -4,48 +4,9 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './checker.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
const float PI = 3.141592653589793;
const float TWO_PI = 6.283185307179586;
const float HALF_PI = 1.5707963267948966;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_angle;
uniform float u_scale;
uniform vec3 u_color;
uniform float u_opacity;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
float x_ratio = max(in_resolution.x / in_resolution.y, 1.0);
float y_ratio = max(in_resolution.y / in_resolution.x, 1.0);
float angle = -(u_angle * PI);
vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio);
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
);
float fmodResult = mod(floor(u_scale * rotatedUV.x) + floor(u_scale * rotatedUV.y), 2.0);
float fin = max(sign(fmodResult), 0.0);
out_color = vec4(
mix(in_color.r, u_color.r, fin * u_opacity),
mix(in_color.g, u_color.g, fin * u_opacity),
mix(in_color.b, u_color.b, fin * u_opacity),
in_color.a
);
}
`;
export const FX_checker = defineImageEffectorFx({
id: 'checker',
name: i18n.ts._imageEffector._fxs.checker,

View File

@ -0,0 +1,49 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
out vec4 out_color;
uniform float u_amount;
uniform float u_start;
uniform bool u_normalize;
void main() {
int samples = 64;
float r_strength = 1.0;
float g_strength = 1.5;
float b_strength = 2.0;
vec2 size = vec2(in_resolution.x, in_resolution.y);
vec4 accumulator = vec4(0.0);
float normalisedValue = length((in_uv - 0.5) * 2.0);
float strength = clamp((normalisedValue - u_start) * (1.0 / (1.0 - u_start)), 0.0, 1.0);
vec2 vector = (u_normalize ? normalize(in_uv - vec2(0.5)) : in_uv - vec2(0.5));
vec2 velocity = vector * strength * u_amount;
vec2 rOffset = -vector * strength * (u_amount * r_strength);
vec2 gOffset = -vector * strength * (u_amount * g_strength);
vec2 bOffset = -vector * strength * (u_amount * b_strength);
for (int i = 0; i < samples; i++) {
accumulator.r += texture(in_texture, in_uv + rOffset).r;
rOffset -= velocity / float(samples);
accumulator.g += texture(in_texture, in_uv + gOffset).g;
gOffset -= velocity / float(samples);
accumulator.b += texture(in_texture, in_uv + bOffset).b;
bOffset -= velocity / float(samples);
}
out_color = vec4(vec3(accumulator / float(samples)), 1.0);
}

View File

@ -4,53 +4,9 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './chromaticAberration.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
out vec4 out_color;
uniform float u_amount;
uniform float u_start;
uniform bool u_normalize;
void main() {
int samples = 64;
float r_strength = 1.0;
float g_strength = 1.5;
float b_strength = 2.0;
vec2 size = vec2(in_resolution.x, in_resolution.y);
vec4 accumulator = vec4(0.0);
float normalisedValue = length((in_uv - 0.5) * 2.0);
float strength = clamp((normalisedValue - u_start) * (1.0 / (1.0 - u_start)), 0.0, 1.0);
vec2 vector = (u_normalize ? normalize(in_uv - vec2(0.5)) : in_uv - vec2(0.5));
vec2 velocity = vector * strength * u_amount;
vec2 rOffset = -vector * strength * (u_amount * r_strength);
vec2 gOffset = -vector * strength * (u_amount * g_strength);
vec2 bOffset = -vector * strength * (u_amount * b_strength);
for (int i = 0; i < samples; i++) {
accumulator.r += texture(in_texture, in_uv + rOffset).r;
rOffset -= velocity / float(samples);
accumulator.g += texture(in_texture, in_uv + gOffset).g;
gOffset -= velocity / float(samples);
accumulator.b += texture(in_texture, in_uv + bOffset).b;
bOffset -= velocity / float(samples);
}
out_color = vec4(vec3(accumulator / float(samples)), 1.0);
}
`;
export const FX_chromaticAberration = defineImageEffectorFx({
id: 'chromaticAberration',
name: i18n.ts._imageEffector._fxs.chromaticAberration,

View File

@ -0,0 +1,82 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_brightness;
uniform float u_contrast;
uniform float u_hue;
uniform float u_lightness;
uniform float u_saturation;
out vec4 out_color;
// RGB to HSL
vec3 rgb2hsl(vec3 c) {
float maxc = max(max(c.r, c.g), c.b);
float minc = min(min(c.r, c.g), c.b);
float l = (maxc + minc) * 0.5;
float s = 0.0;
float h = 0.0;
if (maxc != minc) {
float d = maxc - minc;
s = l > 0.5 ? d / (2.0 - maxc - minc) : d / (maxc + minc);
if (maxc == c.r) {
h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
} else if (maxc == c.g) {
h = (c.b - c.r) / d + 2.0;
} else {
h = (c.r - c.g) / d + 4.0;
}
h /= 6.0;
}
return vec3(h, s, l);
}
// HSL to RGB
float hue2rgb(float p, float q, float t) {
if (t < 0.0) t += 1.0;
if (t > 1.0) t -= 1.0;
if (t < 1.0/6.0) return p + (q - p) * 6.0 * t;
if (t < 1.0/2.0) return q;
if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
return p;
}
vec3 hsl2rgb(vec3 hsl) {
float r, g, b;
float h = hsl.x;
float s = hsl.y;
float l = hsl.z;
if (s == 0.0) {
r = g = b = l;
} else {
float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
float p = 2.0 * l - q;
r = hue2rgb(p, q, h + 1.0/3.0);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1.0/3.0);
}
return vec3(r, g, b);
}
void main() {
vec4 in_color = texture(in_texture, in_uv);
vec3 color = in_color.rgb;
color = color * u_brightness;
color += vec3(u_lightness);
color = (color - 0.5) * u_contrast + 0.5;
vec3 hsl = rgb2hsl(color);
hsl.x = mod(hsl.x + u_hue, 1.0);
hsl.y = clamp(hsl.y * u_saturation, 0.0, 1.0);
color = hsl2rgb(hsl);
out_color = vec4(color, in_color.a);
}

View File

@ -4,86 +4,9 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './colorAdjust.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_brightness;
uniform float u_contrast;
uniform float u_hue;
uniform float u_lightness;
uniform float u_saturation;
out vec4 out_color;
// RGB to HSL
vec3 rgb2hsl(vec3 c) {
float maxc = max(max(c.r, c.g), c.b);
float minc = min(min(c.r, c.g), c.b);
float l = (maxc + minc) * 0.5;
float s = 0.0;
float h = 0.0;
if (maxc != minc) {
float d = maxc - minc;
s = l > 0.5 ? d / (2.0 - maxc - minc) : d / (maxc + minc);
if (maxc == c.r) {
h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0);
} else if (maxc == c.g) {
h = (c.b - c.r) / d + 2.0;
} else {
h = (c.r - c.g) / d + 4.0;
}
h /= 6.0;
}
return vec3(h, s, l);
}
// HSL to RGB
float hue2rgb(float p, float q, float t) {
if (t < 0.0) t += 1.0;
if (t > 1.0) t -= 1.0;
if (t < 1.0/6.0) return p + (q - p) * 6.0 * t;
if (t < 1.0/2.0) return q;
if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
return p;
}
vec3 hsl2rgb(vec3 hsl) {
float r, g, b;
float h = hsl.x;
float s = hsl.y;
float l = hsl.z;
if (s == 0.0) {
r = g = b = l;
} else {
float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
float p = 2.0 * l - q;
r = hue2rgb(p, q, h + 1.0/3.0);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1.0/3.0);
}
return vec3(r, g, b);
}
void main() {
vec4 in_color = texture(in_texture, in_uv);
vec3 color = in_color.rgb;
color = color * u_brightness;
color += vec3(u_lightness);
color = (color - 0.5) * u_contrast + 0.5;
vec3 hsl = rgb2hsl(color);
hsl.x = mod(hsl.x + u_hue, 1.0);
hsl.y = clamp(hsl.y * u_saturation, 0.0, 1.0);
color = hsl2rgb(hsl);
out_color = vec4(color, in_color.a);
}
`;
export const FX_colorAdjust = defineImageEffectorFx({
id: 'colorAdjust',
name: i18n.ts._imageEffector._fxs.colorAdjust,

View File

@ -0,0 +1,29 @@
#version 300 es
precision mediump float;
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// colorClamp, colorClampAdvanced共通
// colorClampではmax, minがすべて同じ値となる
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_rMax;
uniform float u_rMin;
uniform float u_gMax;
uniform float u_gMin;
uniform float u_bMax;
uniform float u_bMin;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
float r = min(max(in_color.r, u_rMin), u_rMax);
float g = min(max(in_color.g, u_gMin), u_gMax);
float b = min(max(in_color.b, u_bMin), u_bMax);
out_color = vec4(r, g, b, in_color.a);
}

View File

@ -4,32 +4,14 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './colorClamp.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_max;
uniform float u_min;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
float r = min(max(in_color.r, u_min), u_max);
float g = min(max(in_color.g, u_min), u_max);
float b = min(max(in_color.b, u_min), u_max);
out_color = vec4(r, g, b, in_color.a);
}
`;
export const FX_colorClamp = defineImageEffectorFx({
id: 'colorClamp',
name: i18n.ts._imageEffector._fxs.colorClamp,
shader,
uniforms: ['max', 'min'] as const,
uniforms: ['rMax', 'rMin', 'gMax', 'gMin', 'bMax', 'bMin'] as const,
params: {
max: {
label: i18n.ts._imageEffector._fxProps.max,
@ -51,7 +33,11 @@ export const FX_colorClamp = defineImageEffectorFx({
},
},
main: ({ gl, u, params }) => {
gl.uniform1f(u.max, params.max);
gl.uniform1f(u.min, 1.0 + params.min);
gl.uniform1f(u.rMax, params.max);
gl.uniform1f(u.rMin, 1.0 + params.min);
gl.uniform1f(u.gMax, params.max);
gl.uniform1f(u.gMin, 1.0 + params.min);
gl.uniform1f(u.bMax, params.max);
gl.uniform1f(u.bMin, 1.0 + params.min);
},
});

View File

@ -4,31 +4,9 @@
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import shader from './colorClamp.glsl';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform float u_rMax;
uniform float u_rMin;
uniform float u_gMax;
uniform float u_gMin;
uniform float u_bMax;
uniform float u_bMin;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
float r = min(max(in_color.r, u_rMin), u_rMax);
float g = min(max(in_color.g, u_gMin), u_gMax);
float b = min(max(in_color.b, u_bMin), u_bMax);
out_color = vec4(r, g, b, in_color.a);
}
`;
export const FX_colorClampAdvanced = defineImageEffectorFx({
id: 'colorClampAdvanced',
name: i18n.ts._imageEffector._fxs.colorClampAdvanced,

Some files were not shown because too many files have changed in this diff Show More