mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-26 15:03:08 +02:00
merge: upstream
This commit is contained in:
parent
34b4646b9f
commit
6a94a52131
60 changed files with 741 additions and 190 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -49,6 +49,12 @@
|
||||||
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
|
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
|
||||||
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
|
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
|
||||||
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
|
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
|
||||||
|
- Enhance: リモートへの引用リノートと同一のリンクにはリンクプレビューを表示しないように
|
||||||
|
- Enhance: コードのシンタックスハイライトにテーマを適用できるように
|
||||||
|
- Enhance: リアクション権限がない場合、ハートにフォールバックするのではなくリアクションピッカーなどから打てないように
|
||||||
|
- リモートのユーザーにローカルのみのカスタム絵文字をリアクションしようとした場合
|
||||||
|
- センシティブなリアクションを認めていないユーザーにセンシティブなカスタム絵文字をリアクションしようとした場合
|
||||||
|
- ロールが必要な絵文字をリアクションしようとした場合
|
||||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
||||||
|
@ -62,6 +68,11 @@
|
||||||
- Enhance: ページ遷移時にPlayerを閉じるように
|
- Enhance: ページ遷移時にPlayerを閉じるように
|
||||||
- Fix: iOSで大きな画像を変換してアップロードできない問題を修正
|
- Fix: iOSで大きな画像を変換してアップロードできない問題を修正
|
||||||
- Fix: 「アニメーション画像を再生しない」もしくは「データセーバー(アイコン)」を有効にしていても、アイコンデコレーションのアニメーションが停止されない問題を修正
|
- Fix: 「アニメーション画像を再生しない」もしくは「データセーバー(アイコン)」を有効にしていても、アイコンデコレーションのアニメーションが停止されない問題を修正
|
||||||
|
- Fix: 画像をクロップするとクロップ後の解像度が異様に低くなる問題の修正
|
||||||
|
- Fix: 画像をクロップ時、正常に完了できない問題の修正
|
||||||
|
- Fix: キャプションが空の画像をクロップするとキャプションにnullという文字列が入ってしまう問題の修正
|
||||||
|
- Fix: プロフィールを編集してもリロードするまで反映されない問題を修正
|
||||||
|
- Fix: エラー画像URLを設定した後解除すると,デフォルトの画像が表示されない問題の修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||||
|
|
|
@ -1157,6 +1157,12 @@ edited: "Editat"
|
||||||
notificationRecieveConfig: "Paràmetres de notificacions"
|
notificationRecieveConfig: "Paràmetres de notificacions"
|
||||||
mutualFollow: "Seguidor mutu"
|
mutualFollow: "Seguidor mutu"
|
||||||
fileAttachedOnly: "Només notes amb adjunts"
|
fileAttachedOnly: "Només notes amb adjunts"
|
||||||
|
showRepliesToOthersInTimeline: "Mostrar les respostes a altres a la línia de temps"
|
||||||
|
hideRepliesToOthersInTimeline: "Amagar les respostes a altres a la línia de temps"
|
||||||
|
showRepliesToOthersInTimelineAll: "Mostrar les respostes a altres a usuaris que segueixes a la línia de temps"
|
||||||
|
hideRepliesToOthersInTimelineAll: "Ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps"
|
||||||
|
confirmShowRepliesAll: "Aquesta opció no té marxa enrere. Vols mostrar les teves respostes a tots els que segueixes a la teva línia de temps?"
|
||||||
|
confirmHideRepliesAll: "Aquesta opció no té marxa enrere. Vols ocultar les teves respostes a tots els usuaris que segueixes a la línia de temps?"
|
||||||
externalServices: "Serveis externs"
|
externalServices: "Serveis externs"
|
||||||
impressum: "Impressum"
|
impressum: "Impressum"
|
||||||
impressumUrl: "Adreça URL impressum"
|
impressumUrl: "Adreça URL impressum"
|
||||||
|
@ -1187,7 +1193,25 @@ seasonalScreenEffect: "Efectes de pantalla segons les estacions"
|
||||||
decorate: "Decorar"
|
decorate: "Decorar"
|
||||||
addMfmFunction: "Afegeix funcions MFM"
|
addMfmFunction: "Afegeix funcions MFM"
|
||||||
enableQuickAddMfmFunction: "Activar accés ràpid per afegir funcions MFM"
|
enableQuickAddMfmFunction: "Activar accés ràpid per afegir funcions MFM"
|
||||||
|
bubbleGame: "Bubble Game"
|
||||||
|
sfx: "Efectes de so"
|
||||||
|
soundWillBePlayed: "Es reproduiran efectes de so"
|
||||||
|
showReplay: "Veure reproducció"
|
||||||
|
replay: "Reproduir"
|
||||||
|
replaying: "Reproduint"
|
||||||
|
ranking: "Classificació"
|
||||||
lastNDays: "Últims {n} dies"
|
lastNDays: "Últims {n} dies"
|
||||||
|
backToTitle: "Torna al títol"
|
||||||
|
hemisphere: "Geolocalització"
|
||||||
|
withSensitive: "Incloure notes amb fitxers sensibles"
|
||||||
|
userSaysSomethingSensitive: "La publicació de {name} conte material sensible"
|
||||||
|
enableHorizontalSwipe: "Lliscar per canviar de pestanya"
|
||||||
|
_bubbleGame:
|
||||||
|
howToPlay: "Com es juga"
|
||||||
|
_howToPlay:
|
||||||
|
section1: "Ajusta la posició i deixa caure l'objecte dintre la caixa."
|
||||||
|
section2: "Quan dos objectes del mateix tipus es toquen, canviaran en un objecte diferent i guanyares punts."
|
||||||
|
section3: "El joc s'acabarà quan els objectes sobresurtin de la caixa. Intenta aconseguir la puntuació més gran possible fusionant objectes mentre impedeixes que sobresurtin de la caixa!"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "Anunci per usuaris registrats"
|
forExistingUsers: "Anunci per usuaris registrats"
|
||||||
forExistingUsersDescription: "Aquest avís només es mostrarà als usuaris existents fins al moment de la publicació. Si no també es mostrarà als usuaris que es registrin després de la publicació."
|
forExistingUsersDescription: "Aquest avís només es mostrarà als usuaris existents fins al moment de la publicació. Si no també es mostrarà als usuaris que es registrin després de la publicació."
|
||||||
|
@ -1209,8 +1233,32 @@ _initialAccountSetting:
|
||||||
privacySetting: "Configuració de seguretat"
|
privacySetting: "Configuració de seguretat"
|
||||||
theseSettingsCanEditLater: "Aquests ajustos es poden canviar més tard."
|
theseSettingsCanEditLater: "Aquests ajustos es poden canviar més tard."
|
||||||
youCanEditMoreSettingsInSettingsPageLater: "A més d'això, es poden fer diferents configuracions a través de la pàgina de configuració. Assegureu-vos de comprovar-ho més tard."
|
youCanEditMoreSettingsInSettingsPageLater: "A més d'això, es poden fer diferents configuracions a través de la pàgina de configuració. Assegureu-vos de comprovar-ho més tard."
|
||||||
|
followUsers: "Prova de seguir usuaris que t'interessin per construir la teva línia de temps."
|
||||||
|
pushNotificationDescription: "Activant les notificacions emergents et permetrà rebre notificacions de {name} directament al teu dispositiu."
|
||||||
|
initialAccountSettingCompleted: "Configuració del perfil completada!"
|
||||||
|
haveFun: "Disfruta {name}!"
|
||||||
|
youCanContinueTutorial: "Pots continuar amb un tutorial per aprendre a Fer servir {name} (MissKey) o tu pots estalviar i començar a fer-lo servir ja."
|
||||||
|
startTutorial: "Començar el tutorial"
|
||||||
|
skipAreYouSure: "Et vols saltar la configuració del perfil?"
|
||||||
|
laterAreYouSure: "Vols continuar la configuració del perfil més tard?"
|
||||||
_initialTutorial:
|
_initialTutorial:
|
||||||
|
launchTutorial: "Començar tutorial"
|
||||||
|
title: "Tutorial"
|
||||||
|
wellDone: "Ben fet!"
|
||||||
|
skipAreYouSure: "Sortir del tutorial?"
|
||||||
|
_landing:
|
||||||
|
title: "Benvingut al tutorial"
|
||||||
|
description: "Aquí aprendràs el bàsic per poder fer servir Misskey i les seves característiques."
|
||||||
|
_note:
|
||||||
|
title: "Què és una Nota?"
|
||||||
|
description: "Les publicacions a Misskey es diuen 'Notes'. Les Notes s'ordenen cronològicament a la línia de temps i s'actualitzen de forma automàtica."
|
||||||
|
reply: "Fes clic en aquest botó per contestar a un missatge. També és possible contestar a una contestació, continuant la conversació en forma de fil."
|
||||||
|
renote: "Pots compartir una Nota a la teva pròpia línia de temps. Inclús pots citar-les amb els teus comentaris."
|
||||||
|
reaction: "Pots afegir reaccions a les Notes. Entrarem més en detall a la pròxima pàgina."
|
||||||
|
menu: "Pots veure els detalls de les Notes, copiar enllaços i fer diferents accions."
|
||||||
_reaction:
|
_reaction:
|
||||||
|
title: "Què són les Reaccions?"
|
||||||
|
description: "Es poden reaccionar a les Notes amb diferents emoticones. Les reaccions et permeten expressar matisos que hi són més enllà d'un simple m'agrada."
|
||||||
letsTryReacting: "Es poden afegir reaccions fent clic al botó '+'. Prova reaccionant a aquesta nota!"
|
letsTryReacting: "Es poden afegir reaccions fent clic al botó '+'. Prova reaccionant a aquesta nota!"
|
||||||
reactToContinue: "Afegeix una reacció per continuar."
|
reactToContinue: "Afegeix una reacció per continuar."
|
||||||
reactNotification: "Rebràs notificacions en temps real quan un usuari reaccioni a les teves notes."
|
reactNotification: "Rebràs notificacions en temps real quan un usuari reaccioni a les teves notes."
|
||||||
|
@ -1272,9 +1320,75 @@ _serverSettings:
|
||||||
shortName: "Nom curt"
|
shortName: "Nom curt"
|
||||||
shortNameDescription: "Una abreviatura del nom de la instància que es poguí mostrar en cas que el nom oficial sigui massa llarg"
|
shortNameDescription: "Una abreviatura del nom de la instància que es poguí mostrar en cas que el nom oficial sigui massa llarg"
|
||||||
fanoutTimelineDescription: "Quan es troba activat millora bastant el rendiment quan es recuperen les línies de temps i redueix la carrega de la base de dades. Com a contrapunt, l'ús de memòria de Redis es veurà incrementada. Considera d'estabilitat aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes de inestabilitat."
|
fanoutTimelineDescription: "Quan es troba activat millora bastant el rendiment quan es recuperen les línies de temps i redueix la carrega de la base de dades. Com a contrapunt, l'ús de memòria de Redis es veurà incrementada. Considera d'estabilitat aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes de inestabilitat."
|
||||||
|
fanoutTimelineDbFallback: "Carregar de la base de dades"
|
||||||
|
fanoutTimelineDbFallbackDescription: "Quan s'activa, la línia de temps fa servir la base de dades per consultes adicionals si la línia de temps no es troba a la memòria cau. Si és desactiva la càrrega del servidor és veure reduïda, però també és reduirà el nombre de línies de temps que és poden obtenir."
|
||||||
|
_accountMigration:
|
||||||
|
moveFrom: "Migrar un altre compte a aquest"
|
||||||
|
moveFromSub: "Crear un àlies per un altre compte"
|
||||||
|
moveFromLabel: "Compte original #{n}"
|
||||||
|
moveFromDescription: "Has de crear un àlies del compte que vols migrar en aquest compte.\nFes servir aquest format per posar el compte que vols migrar: @nomusuari@servidor.exemple.com\nPer esborrar l'àlies deixa el camp en blanc (no és recomanable de fer)"
|
||||||
|
moveTo: "Migrar aquest compte a un altre"
|
||||||
|
moveToLabel: "Compte al qual es vol migrar:"
|
||||||
|
moveCannotBeUndone: "Les migracions dels comptes no es poden desfer."
|
||||||
|
moveAccountDescription: "Això migrarà la teva compte a un altre diferent.\n ・Els seguidors d'aquest compte és passaran al compte nou de forma automàtica\n ・Es deixaran de seguir a tots els usuaris que es segueixen actualment en aquest compte\n ・No es poden crear notes noves, etc. en aquest compte\n\nSi bé la migració de seguidors es automàtica, has de preparar alguns pasos manualment per migrar la llista d'usuaris que segueixes. Per fer això has d'exportar els seguidors que després importaraes al compte nou mitjançant el menú de configuració. El mateix procediment s'ha de seguir per less teves llistes i els teus usuaris silenciats i bloquejats.\n\n(Aquesta explicació s'aplica a Misskey v13.12.0 i posteriors. Altres aplicacions, com Mastodon, poden funcionar diferent.)"
|
||||||
|
moveAccountHowTo: "Per fer la migració, primer has de crear un àlies per aquest compte al compte al qual vols migrar.\nDesprés de crear l'àlies, introdueix el compte al qual vols migrar amb el format següent: @nomusuari@servidor.exemple.com"
|
||||||
|
startMigration: "Migrar"
|
||||||
|
migrationConfirm: "Vols migrar aquest compte a {account}? Una vegada comenci la migració no es podrà parar O fer marxa enrere i no podràs tornar a fer servir aquest compte mai més."
|
||||||
|
movedAndCannotBeUndone: "Aquest compte ha migrat.\nLes migracions no es poden desfer."
|
||||||
|
postMigrationNote: "Aquest compte deixarà de seguir tots els comptes que segueix 24 hores després de germinar la migració.\nEl nombre de seguidors i seguits passarà a ser de zero. Per evitar que els teus seguidors no puguin veure les publicacions marcades com a només seguidors continuaren seguint aquest compte."
|
||||||
|
movedTo: "Nou compte:"
|
||||||
_achievements:
|
_achievements:
|
||||||
|
earnedAt: "Desbloquejat el"
|
||||||
_types:
|
_types:
|
||||||
|
_notes1:
|
||||||
|
title: "Aquí, configurant el meu msky"
|
||||||
|
description: "Publica la teva primera Nota"
|
||||||
|
flavor: "Passa-t'ho bé fent servir Miskey!"
|
||||||
|
_notes10:
|
||||||
|
title: "Algunes notes"
|
||||||
|
description: "Publica 10 notes"
|
||||||
|
_notes100:
|
||||||
|
title: "Un piló de notes"
|
||||||
|
description: "Publica 100 notes"
|
||||||
|
_notes500:
|
||||||
|
title: "Cobert de notes"
|
||||||
|
description: "Publica 500 notes"
|
||||||
|
_notes1000:
|
||||||
|
title: "Un piló de notes"
|
||||||
|
description: "1 000 notes publicades"
|
||||||
|
_notes5000:
|
||||||
|
title: "Desbordament de notes"
|
||||||
|
description: "5 000 notes publicades"
|
||||||
|
_notes10000:
|
||||||
|
title: "Supernota"
|
||||||
|
description: "10 000 notes publicades"
|
||||||
|
_notes20000:
|
||||||
|
title: "Necessito... Més... Notes!"
|
||||||
|
description: "20 000 notes publicades"
|
||||||
|
_notes30000:
|
||||||
|
title: "Notes notes notes!"
|
||||||
|
description: "30 000 notes publicades"
|
||||||
|
_notes40000:
|
||||||
|
title: "Fàbrica de notes"
|
||||||
|
description: "40 000 notes publicades"
|
||||||
|
_notes50000:
|
||||||
|
title: "Planeta de notes"
|
||||||
|
description: "50 000 notes publicades"
|
||||||
|
_notes60000:
|
||||||
|
title: "Quàsar de notes"
|
||||||
|
description: "60 000 notes publicades"
|
||||||
|
_notes70000:
|
||||||
|
title: "Forat negre de notes"
|
||||||
|
description: "70 000 notes publicades"
|
||||||
|
_notes80000:
|
||||||
|
title: "Galàxia de notes"
|
||||||
|
description: "80 000 notes publicades"
|
||||||
|
_notes90000:
|
||||||
|
title: "Univers de notes"
|
||||||
|
description: "90 000 notes publicades"
|
||||||
_notes100000:
|
_notes100000:
|
||||||
|
title: "ALL YOUR NOTE ARE BELONG TO US"
|
||||||
|
description: "100 000 notes publicades"
|
||||||
flavor: "Segur que tens moltes coses a dir?"
|
flavor: "Segur que tens moltes coses a dir?"
|
||||||
_login3:
|
_login3:
|
||||||
title: "Principiant I"
|
title: "Principiant I"
|
||||||
|
@ -1347,13 +1461,90 @@ _achievements:
|
||||||
description: "És la primera vegada que et segueixo"
|
description: "És la primera vegada que et segueixo"
|
||||||
_following10:
|
_following10:
|
||||||
title: "Segueix-me... Segueix-me..."
|
title: "Segueix-me... Segueix-me..."
|
||||||
|
description: "Seguir 10 usuaris"
|
||||||
|
_following50:
|
||||||
|
title: "Molts amics"
|
||||||
|
description: "Seguir 50 comptes"
|
||||||
|
_following100:
|
||||||
|
title: "100 amics"
|
||||||
|
description: "Segueixes 100 comptes"
|
||||||
|
_following300:
|
||||||
|
title: "Sobrecàrrega d'amics"
|
||||||
|
description: "Segueixes 300 comptes"
|
||||||
|
_followers1:
|
||||||
|
title: "Primer seguidor"
|
||||||
|
description: "1 seguidor guanyat"
|
||||||
|
_followers10:
|
||||||
|
title: "Segueix-me!"
|
||||||
|
description: "10 seguidors guanyats"
|
||||||
|
_followers50:
|
||||||
|
title: "Venen en manada"
|
||||||
|
description: "50 seguidors guanyats"
|
||||||
|
_followers100:
|
||||||
|
title: "Popular"
|
||||||
|
description: "100 seguidors guanyats"
|
||||||
|
_followers300:
|
||||||
|
title: "Si us plau, d'un en un!"
|
||||||
|
description: "300 seguidors guanyats"
|
||||||
|
_followers500:
|
||||||
|
title: "Torre de ràdio"
|
||||||
|
description: "500 seguidors guanyats"
|
||||||
|
_followers1000:
|
||||||
|
title: "Influenciador"
|
||||||
|
description: "1 000 seguidors guanyats"
|
||||||
|
_collectAchievements30:
|
||||||
|
title: "Col·leccionista d'èxits "
|
||||||
|
description: "Desbloqueja 30 assoliments"
|
||||||
|
_viewAchievements3min:
|
||||||
|
title: "M'agraden els èxits "
|
||||||
|
description: "Mira la teva llista d'assoliments durant més de 3 minuts"
|
||||||
|
_iLoveMisskey:
|
||||||
|
title: "Estimo Misskey"
|
||||||
|
description: "Publica \"I ❤ #Misskey\""
|
||||||
|
flavor: "L'equip de desenvolupament de Misskey agraeix el vostre suport!"
|
||||||
|
_foundTreasure:
|
||||||
|
title: "A la Recerca del Tresor"
|
||||||
|
description: "Has trobat el tresor amagat"
|
||||||
|
_client30min:
|
||||||
|
title: "Parem una estona"
|
||||||
|
description: "Mantingues obert Misskey per 30 minuts"
|
||||||
|
_client60min:
|
||||||
|
title: "A totes amb Misskey"
|
||||||
|
description: "Mantingues Misskey obert per 60 minuts"
|
||||||
|
_noteDeletedWithin1min:
|
||||||
|
title: "No et preocupis"
|
||||||
|
description: "Esborra una nota al minut de publicar-la"
|
||||||
|
_postedAtLateNight:
|
||||||
|
title: "Nocturn"
|
||||||
|
description: "Publica una nota a altes hores de la nit "
|
||||||
|
flavor: "És hora d'anar a dormir."
|
||||||
_open3windows:
|
_open3windows:
|
||||||
title: "Multi finestres"
|
title: "Multi finestres"
|
||||||
description: "I va obrir més de tres finestres"
|
description: "I va obrir més de tres finestres"
|
||||||
_driveFolderCircularReference:
|
_driveFolderCircularReference:
|
||||||
title: "Consulteu la secció de bucle"
|
title: "Consulteu la secció de bucle"
|
||||||
_role:
|
_role:
|
||||||
|
permission: "Permisos de rol"
|
||||||
|
descriptionOfPermission: "Els <b>Moderadors</b> poden fer operacions bàsiques de moderació.\nEls <b>Administradors</b> poden canviar tots els ajustos del servidor."
|
||||||
assignTarget: "Assignar "
|
assignTarget: "Assignar "
|
||||||
|
descriptionOfAssignTarget: "<b>Manual</b> per canviar manualment qui és part d'aquest rol i qui no.\n<b>Condicional</b> per afegir o eliminar de manera automàtica els usuaris d'aquest rol basat en una determinada condició."
|
||||||
|
manual: "Manual"
|
||||||
|
manualRoles: "Rols manuals"
|
||||||
|
conditional: "Condicional"
|
||||||
|
conditionalRoles: "Rols condicionals"
|
||||||
|
condition: "Condició"
|
||||||
|
isConditionalRole: "Aquest és un rol condicional"
|
||||||
|
isPublic: "Rol públic"
|
||||||
|
descriptionOfIsPublic: "Aquest rol es mostrarà al perfil dels usuaris al que se'ls assigni."
|
||||||
|
options: "Opcions"
|
||||||
|
policies: "Polítiques"
|
||||||
|
baseRole: "Plantilla de rols"
|
||||||
|
useBaseValue: "Fer servir els valors de la plantilla de rols"
|
||||||
|
chooseRoleToAssign: "Selecciona els rols a assignar"
|
||||||
|
iconUrl: "URL de la icona "
|
||||||
|
asBadge: "Mostrar com a insígnia "
|
||||||
|
descriptionOfAsBadge: "La icona d'aquest rol es mostrarà al costat dels noms d'usuaris que tinguin assignats aquest rol."
|
||||||
|
isExplorable: "Fer el rol explorable"
|
||||||
priority: "Prioritat"
|
priority: "Prioritat"
|
||||||
_priority:
|
_priority:
|
||||||
low: "Baixa"
|
low: "Baixa"
|
||||||
|
|
|
@ -103,7 +103,7 @@ defaultNoteVisibility: "Privacy predefinita delle note"
|
||||||
follow: "Segui"
|
follow: "Segui"
|
||||||
followRequest: "Richiesta di follow"
|
followRequest: "Richiesta di follow"
|
||||||
followRequests: "Richieste di follow"
|
followRequests: "Richieste di follow"
|
||||||
unfollow: "Interrompi following"
|
unfollow: "Smetti di seguire"
|
||||||
followRequestPending: "Richiesta in approvazione"
|
followRequestPending: "Richiesta in approvazione"
|
||||||
enterEmoji: "Inserisci emoji"
|
enterEmoji: "Inserisci emoji"
|
||||||
renote: "Rinota"
|
renote: "Rinota"
|
||||||
|
@ -381,9 +381,11 @@ hcaptcha: "hCaptcha"
|
||||||
enableHcaptcha: "Abilita hCaptcha"
|
enableHcaptcha: "Abilita hCaptcha"
|
||||||
hcaptchaSiteKey: "Chiave del sito"
|
hcaptchaSiteKey: "Chiave del sito"
|
||||||
hcaptchaSecretKey: "Chiave segreta"
|
hcaptchaSecretKey: "Chiave segreta"
|
||||||
|
mcaptcha: "mCaptcha"
|
||||||
enableMcaptcha: "Abilita hCaptcha"
|
enableMcaptcha: "Abilita hCaptcha"
|
||||||
mcaptchaSiteKey: "Chiave del sito"
|
mcaptchaSiteKey: "Chiave del sito"
|
||||||
mcaptchaSecretKey: "Chiave segreta"
|
mcaptchaSecretKey: "Chiave segreta"
|
||||||
|
mcaptchaInstanceUrl: "URL della istanza mCaptcha"
|
||||||
recaptcha: "reCAPTCHA"
|
recaptcha: "reCAPTCHA"
|
||||||
enableRecaptcha: "Abilita reCAPTCHA"
|
enableRecaptcha: "Abilita reCAPTCHA"
|
||||||
recaptchaSiteKey: "Chiave del sito"
|
recaptchaSiteKey: "Chiave del sito"
|
||||||
|
@ -631,6 +633,7 @@ medium: "Medio"
|
||||||
small: "Piccolo"
|
small: "Piccolo"
|
||||||
generateAccessToken: "Genera token di accesso"
|
generateAccessToken: "Genera token di accesso"
|
||||||
permission: "Autorizzazioni "
|
permission: "Autorizzazioni "
|
||||||
|
adminPermission: "Privilegi amministrativi"
|
||||||
enableAll: "Abilita tutto"
|
enableAll: "Abilita tutto"
|
||||||
disableAll: "Disabilita tutto"
|
disableAll: "Disabilita tutto"
|
||||||
tokenRequested: "Autorizza accesso al profilo"
|
tokenRequested: "Autorizza accesso al profilo"
|
||||||
|
@ -674,6 +677,7 @@ useGlobalSettingDesc: "Quando attiva, verranno utilizzate le impostazioni notifi
|
||||||
other: "Ulteriori"
|
other: "Ulteriori"
|
||||||
regenerateLoginToken: "Genera di nuovo un token di connessione"
|
regenerateLoginToken: "Genera di nuovo un token di connessione"
|
||||||
regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi."
|
regenerateLoginTokenDescription: "Genera un nuovo token di autenticazione. Solitamente questa operazione non è necessaria: quando si genera un nuovo token, tutti i dispositivi vanno disconnessi."
|
||||||
|
theKeywordWhenSearchingForCustomEmoji: "Questa sarà la parola chiave durante la ricerca di emoji personalizzate"
|
||||||
setMultipleBySeparatingWithSpace: "È possibile creare multiple voci separate da spazi."
|
setMultipleBySeparatingWithSpace: "È possibile creare multiple voci separate da spazi."
|
||||||
fileIdOrUrl: "ID o URL del file"
|
fileIdOrUrl: "ID o URL del file"
|
||||||
behavior: "Comportamento"
|
behavior: "Comportamento"
|
||||||
|
@ -872,7 +876,7 @@ pubSub: "Publish/Subscribe del profilo"
|
||||||
lastCommunication: "La comunicazione più recente"
|
lastCommunication: "La comunicazione più recente"
|
||||||
resolved: "Risolto"
|
resolved: "Risolto"
|
||||||
unresolved: "Non risolto"
|
unresolved: "Non risolto"
|
||||||
breakFollow: "Interrompi follow"
|
breakFollow: "Impedire di seguirmi"
|
||||||
breakFollowConfirm: "Vuoi davvero che questo profilo smetta di seguirti?"
|
breakFollowConfirm: "Vuoi davvero che questo profilo smetta di seguirti?"
|
||||||
itsOn: "Abilitato"
|
itsOn: "Abilitato"
|
||||||
itsOff: "Disabilitato"
|
itsOff: "Disabilitato"
|
||||||
|
@ -888,6 +892,8 @@ makeReactionsPublicDescription: "La lista delle reazioni che avete fatto è a di
|
||||||
classic: "Classico"
|
classic: "Classico"
|
||||||
muteThread: "Silenzia conversazione"
|
muteThread: "Silenzia conversazione"
|
||||||
unmuteThread: "Riattiva la conversazione"
|
unmuteThread: "Riattiva la conversazione"
|
||||||
|
followingVisibility: "Visibilità dei profili seguiti"
|
||||||
|
followersVisibility: "Visibilità dei profili che ti seguono"
|
||||||
continueThread: "Altre conversazioni"
|
continueThread: "Altre conversazioni"
|
||||||
deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?"
|
deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?"
|
||||||
incorrectPassword: "La password è errata."
|
incorrectPassword: "La password è errata."
|
||||||
|
@ -1057,6 +1063,8 @@ limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale"
|
||||||
noteIdOrUrl: "ID della Nota o URL"
|
noteIdOrUrl: "ID della Nota o URL"
|
||||||
video: "Video"
|
video: "Video"
|
||||||
videos: "Video"
|
videos: "Video"
|
||||||
|
audio: "Audio"
|
||||||
|
audioFiles: "Audio"
|
||||||
dataSaver: "Risparmia dati"
|
dataSaver: "Risparmia dati"
|
||||||
accountMigration: "Migrazione del profilo"
|
accountMigration: "Migrazione del profilo"
|
||||||
accountMoved: "Questo profilo ha migrato altrove:"
|
accountMoved: "Questo profilo ha migrato altrove:"
|
||||||
|
@ -1187,7 +1195,27 @@ remainingN: "Rimangono: {n}"
|
||||||
overwriteContentConfirm: "Vuoi davvero sostituire l'attuale contenuto?"
|
overwriteContentConfirm: "Vuoi davvero sostituire l'attuale contenuto?"
|
||||||
seasonalScreenEffect: "Schermate in base alla stagione"
|
seasonalScreenEffect: "Schermate in base alla stagione"
|
||||||
decorate: "Decora"
|
decorate: "Decora"
|
||||||
|
addMfmFunction: "Aggiungi decorazioni"
|
||||||
|
enableQuickAddMfmFunction: "Attiva il selettore di funzioni MFM"
|
||||||
|
bubbleGame: "Bubble Game"
|
||||||
|
sfx: "Effetti sonori"
|
||||||
|
soundWillBePlayed: "Verrà riprodotto il suono"
|
||||||
|
showReplay: "Vedi i replay"
|
||||||
|
replay: "Replay"
|
||||||
|
replaying: "Replay in corso"
|
||||||
|
ranking: "Classifica"
|
||||||
lastNDays: "Ultimi {n} giorni"
|
lastNDays: "Ultimi {n} giorni"
|
||||||
|
backToTitle: "Torna al titolo"
|
||||||
|
hemisphere: "Geolocalizzazione"
|
||||||
|
withSensitive: "Mostra le Note con allegati espliciti"
|
||||||
|
userSaysSomethingSensitive: "Note da {name} con allegati espliciti"
|
||||||
|
enableHorizontalSwipe: "Trascina per invertire i tab"
|
||||||
|
_bubbleGame:
|
||||||
|
howToPlay: "Come giocare"
|
||||||
|
_howToPlay:
|
||||||
|
section1: "Regola la posizione e rilascia l'oggetto nella casella."
|
||||||
|
section2: "Ottieni un punteggio, quando due oggetti dello stesso tipo si toccano e si trasformano in un oggetto diverso."
|
||||||
|
section3: "Se gli oggetti traboccano dalla scatola, il gioco finisce. Cerca di ottenere un punteggio elevato fondendo gli oggetti, evitando che escano dalla scatola!"
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "Solo ai profili attuali"
|
forExistingUsers: "Solo ai profili attuali"
|
||||||
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
|
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
|
||||||
|
@ -1558,6 +1586,13 @@ _achievements:
|
||||||
_tutorialCompleted:
|
_tutorialCompleted:
|
||||||
title: "Attestato di partecipazione al corso per principianti di Misskey"
|
title: "Attestato di partecipazione al corso per principianti di Misskey"
|
||||||
description: "Ha completato il tutorial"
|
description: "Ha completato il tutorial"
|
||||||
|
_bubbleGameExplodingHead:
|
||||||
|
title: "🤯"
|
||||||
|
description: "Estrai l'oggetto più grande dal Bubble Game"
|
||||||
|
_bubbleGameDoubleExplodingHead:
|
||||||
|
title: "Doppio 🤯"
|
||||||
|
description: "Due oggetti più grossi contemporaneamente nel Bubble Game"
|
||||||
|
flavor: "Ha le dimensioni di una bento-box 🤯 🤯"
|
||||||
_role:
|
_role:
|
||||||
new: "Nuovo ruolo"
|
new: "Nuovo ruolo"
|
||||||
edit: "Modifica ruolo"
|
edit: "Modifica ruolo"
|
||||||
|
@ -1648,6 +1683,7 @@ _emailUnavailable:
|
||||||
disposable: "Indirizzo email non utilizzabile"
|
disposable: "Indirizzo email non utilizzabile"
|
||||||
mx: "Server email non corretto"
|
mx: "Server email non corretto"
|
||||||
smtp: "Il server email non risponde"
|
smtp: "Il server email non risponde"
|
||||||
|
banned: "Non puoi registrarti con questo indirizzo email"
|
||||||
_ffVisibility:
|
_ffVisibility:
|
||||||
public: "Pubblica"
|
public: "Pubblica"
|
||||||
followers: "Mostra solo ai follower"
|
followers: "Mostra solo ai follower"
|
||||||
|
@ -1940,6 +1976,26 @@ _permissions:
|
||||||
"write:flash": "Modifica Play"
|
"write:flash": "Modifica Play"
|
||||||
"read:flash-likes": "Visualizza lista di Play piaciuti"
|
"read:flash-likes": "Visualizza lista di Play piaciuti"
|
||||||
"write:flash-likes": "Modifica lista di Play piaciuti"
|
"write:flash-likes": "Modifica lista di Play piaciuti"
|
||||||
|
"read:admin:abuse-user-reports": "Mostra i report dai profili utente"
|
||||||
|
"write:admin:delete-account": "Elimina l'account utente"
|
||||||
|
"write:admin:delete-all-files-of-a-user": "Elimina i file dell'account utente"
|
||||||
|
"read:admin:index-stats": "Visualizza informazioni sugli indici del database"
|
||||||
|
"read:admin:table-stats": "Visualizza informazioni sulle tabelle del database"
|
||||||
|
"read:admin:user-ips": "Visualizza indirizzi IP degli account"
|
||||||
|
"read:admin:meta": "Visualizza i metadati dell'istanza"
|
||||||
|
"write:admin:reset-password": "Ripristina la password dell'account utente"
|
||||||
|
"write:admin:resolve-abuse-user-report": "Risolvere le segnalazioni dagli account utente"
|
||||||
|
"write:admin:send-email": "Spedire email"
|
||||||
|
"read:admin:server-info": "Vedere le informazioni sul server"
|
||||||
|
"read:admin:show-moderation-log": "Vedere lo storico di moderazione"
|
||||||
|
"read:admin:show-user": "Vedere le informazioni private degli account utente"
|
||||||
|
"read:admin:show-users": "Vedere le informazioni private degli account utente"
|
||||||
|
"write:admin:suspend-user": "Sospendere i profili"
|
||||||
|
"write:admin:unset-user-avatar": "Rimuovere la foto profilo dai profili"
|
||||||
|
"write:admin:unset-user-banner": "Rimuovere l'immagine testata dai profili"
|
||||||
|
"write:admin:unsuspend-user": "Togliere la sospensione ai profili"
|
||||||
|
"write:admin:meta": "Modificare i metadati dell'istanza"
|
||||||
|
"write:admin:user-note": "Scrivere annotazioni di moderazione"
|
||||||
_auth:
|
_auth:
|
||||||
shareAccessTitle: "Permessi dell'applicazione"
|
shareAccessTitle: "Permessi dell'applicazione"
|
||||||
shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?"
|
shareAccess: "Vuoi autorizzare {name} ad accedere al tuo profilo?"
|
||||||
|
|
|
@ -40,7 +40,7 @@ favorites: "질겨찾기"
|
||||||
unfavorite: "질겨찾기서 어ᇝ애기"
|
unfavorite: "질겨찾기서 어ᇝ애기"
|
||||||
favorited: "질겨찾기에 담앗십니다."
|
favorited: "질겨찾기에 담앗십니다."
|
||||||
alreadyFavorited: "벌시로 질겨찾기에 담기 잇십니다."
|
alreadyFavorited: "벌시로 질겨찾기에 담기 잇십니다."
|
||||||
cantFavorite: "질겨찾기에 몬 담았십니다."
|
cantFavorite: "질겨찾기에 몬 담앗십니다."
|
||||||
pin: "프로필에 붙이기"
|
pin: "프로필에 붙이기"
|
||||||
unpin: "프로필서 띠기"
|
unpin: "프로필서 띠기"
|
||||||
copyContent: "내용 복사하기"
|
copyContent: "내용 복사하기"
|
||||||
|
@ -124,6 +124,7 @@ reactions: "반엉"
|
||||||
reactionSettingDescription2: "꺼시서 두고, 누질라서 뭉캐고, ‘+’럴 누질라서 옇십니다."
|
reactionSettingDescription2: "꺼시서 두고, 누질라서 뭉캐고, ‘+’럴 누질라서 옇십니다."
|
||||||
rememberNoteVisibility: "공개 범위럴 기억하기"
|
rememberNoteVisibility: "공개 범위럴 기억하기"
|
||||||
attachCancel: "붙임 빼기"
|
attachCancel: "붙임 빼기"
|
||||||
|
deleteFile: "파일 뭉캐기"
|
||||||
markAsSensitive: "수ᇚ힘 설정"
|
markAsSensitive: "수ᇚ힘 설정"
|
||||||
unmarkAsSensitive: "수ᇚ힘 무루기"
|
unmarkAsSensitive: "수ᇚ힘 무루기"
|
||||||
enterFileName: "파일 이럼 서기"
|
enterFileName: "파일 이럼 서기"
|
||||||
|
@ -463,6 +464,8 @@ onlyOneFileCanBeAttached: "메시지엔 파일 하나까제밖에 몬 넣십니
|
||||||
invitations: "초대하기"
|
invitations: "초대하기"
|
||||||
invitationCode: "초대장"
|
invitationCode: "초대장"
|
||||||
checking: "학인하고 잇십니다"
|
checking: "학인하고 잇십니다"
|
||||||
|
tooShort: "억수로 짜립니다"
|
||||||
|
tooLong: "억수로 집니다"
|
||||||
passwordMatched: "맞십니다"
|
passwordMatched: "맞십니다"
|
||||||
passwordNotMatched: "안 맞십니다"
|
passwordNotMatched: "안 맞십니다"
|
||||||
signinFailed: "로그인 몬 했십니다. 고 이름이랑 비밀번호 제대로 썼는가 확인해 주이소."
|
signinFailed: "로그인 몬 했십니다. 고 이름이랑 비밀번호 제대로 썼는가 확인해 주이소."
|
||||||
|
@ -571,7 +574,11 @@ userSilenced: "요 게정은... 수ᇚ혀 있십니다."
|
||||||
relays: "릴레이"
|
relays: "릴레이"
|
||||||
addRelay: "릴레이 옇기"
|
addRelay: "릴레이 옇기"
|
||||||
addedRelays: "옇은 릴레이"
|
addedRelays: "옇은 릴레이"
|
||||||
|
deletedNote: "뭉캔 걸"
|
||||||
enableInfiniteScroll: "알아서 더 보기"
|
enableInfiniteScroll: "알아서 더 보기"
|
||||||
|
useCw: "내용 수ᇚ후기"
|
||||||
|
description: "설멩"
|
||||||
|
describeFile: "캡션 옇기"
|
||||||
author: "맨던 사람"
|
author: "맨던 사람"
|
||||||
manage: "간리"
|
manage: "간리"
|
||||||
emailServer: "전자우펜 서버"
|
emailServer: "전자우펜 서버"
|
||||||
|
@ -600,6 +607,7 @@ renotesCount: "리노트한 수"
|
||||||
renotedCount: "리노트덴 수"
|
renotedCount: "리노트덴 수"
|
||||||
followingCount: "팔로우 수"
|
followingCount: "팔로우 수"
|
||||||
followersCount: "팔로워 수"
|
followersCount: "팔로워 수"
|
||||||
|
noteFavoritesCount: "질겨찾기한 노트 수"
|
||||||
clips: "클립 맨걸기"
|
clips: "클립 맨걸기"
|
||||||
clearCache: "캐시 비우기"
|
clearCache: "캐시 비우기"
|
||||||
unlikeConfirm: "좋네예럴 무룹니꺼?"
|
unlikeConfirm: "좋네예럴 무룹니꺼?"
|
||||||
|
@ -608,6 +616,7 @@ user: "사용자"
|
||||||
administration: "간리"
|
administration: "간리"
|
||||||
on: "킴"
|
on: "킴"
|
||||||
off: "껌"
|
off: "껌"
|
||||||
|
hide: "수ᇚ후기"
|
||||||
clickToFinishEmailVerification: "[{ok}]럴 누질라서 전자우펜 정멩얼 껕내이소."
|
clickToFinishEmailVerification: "[{ok}]럴 누질라서 전자우펜 정멩얼 껕내이소."
|
||||||
searchByGoogle: "찾기"
|
searchByGoogle: "찾기"
|
||||||
tenMinutes: "십 분"
|
tenMinutes: "십 분"
|
||||||
|
@ -626,9 +635,11 @@ role: "옉할"
|
||||||
noRole: "옉할이 없십니다"
|
noRole: "옉할이 없십니다"
|
||||||
thisPostMayBeAnnoyingCancel: "아이예"
|
thisPostMayBeAnnoyingCancel: "아이예"
|
||||||
likeOnly: "좋네예마"
|
likeOnly: "좋네예마"
|
||||||
|
myClips: "내 클립"
|
||||||
icon: "아바타"
|
icon: "아바타"
|
||||||
replies: "답하기"
|
replies: "답하기"
|
||||||
renotes: "리노트"
|
renotes: "리노트"
|
||||||
|
attach: "옇기"
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
startTutorial: "길라잡이 하기"
|
startTutorial: "길라잡이 하기"
|
||||||
_initialTutorial:
|
_initialTutorial:
|
||||||
|
@ -641,9 +652,52 @@ _initialTutorial:
|
||||||
title: "길라잡이가 껕낫십니다!🎉"
|
title: "길라잡이가 껕낫십니다!🎉"
|
||||||
_achievements:
|
_achievements:
|
||||||
_types:
|
_types:
|
||||||
|
_notes1:
|
||||||
|
description: "첫 노트럴 섯어예"
|
||||||
|
_notes10:
|
||||||
|
description: "노트럴 10번 섰어예"
|
||||||
|
_notes100:
|
||||||
|
description: "노트럴 100번 섰어예"
|
||||||
|
_notes500:
|
||||||
|
description: "노트럴 500번 섰어예"
|
||||||
|
_notes1000:
|
||||||
|
description: "노트럴 1,000번 섰어예"
|
||||||
|
_notes5000:
|
||||||
|
description: "노트럴 5,000번 섰어예"
|
||||||
|
_notes10000:
|
||||||
|
description: "노트럴 10,000번 섰어예"
|
||||||
|
_notes20000:
|
||||||
|
description: "노트럴 20,000번 섰어예"
|
||||||
|
_notes30000:
|
||||||
|
description: "노트럴 30,000번 섰어예"
|
||||||
|
_notes40000:
|
||||||
|
description: "노트럴 40,000번 섰어예"
|
||||||
|
_notes50000:
|
||||||
|
description: "노트럴 50,000번 섰어예"
|
||||||
|
_notes60000:
|
||||||
|
description: "노트럴 60,000번 섰어예"
|
||||||
|
_notes70000:
|
||||||
|
description: "노트럴 70,000번 섰어예"
|
||||||
|
_notes80000:
|
||||||
|
description: "노트럴 80,000번 섰어예"
|
||||||
|
_notes90000:
|
||||||
|
description: "노트럴 90,000번 섰어예"
|
||||||
|
_notes100000:
|
||||||
|
description: "노트럴 100,000번 섰어예"
|
||||||
|
_noteClipped1:
|
||||||
|
description: "첫 노트럴 클립햇어예"
|
||||||
|
_noteFavorited1:
|
||||||
|
description: "첫 노트럴 질겨찾기에 담앗어예"
|
||||||
|
_myNoteFavorited1:
|
||||||
|
description: "다런 사람이 내 노트럴 질겨찾기에 담앗십니다"
|
||||||
|
_iLoveMisskey:
|
||||||
|
description: "“I ❤ #Misskey”럴 섰어예"
|
||||||
|
_postedAt0min0sec:
|
||||||
|
description: "0분 0초에 노트를 섰어예"
|
||||||
_tutorialCompleted:
|
_tutorialCompleted:
|
||||||
description: "길라잡이럴 껕냇십니다"
|
description: "길라잡이럴 껕냇십니다"
|
||||||
_gallery:
|
_gallery:
|
||||||
|
my: "내 걸"
|
||||||
liked: "좋네예한 걸"
|
liked: "좋네예한 걸"
|
||||||
like: "좋네예!"
|
like: "좋네예!"
|
||||||
unlike: "좋네예 무루기"
|
unlike: "좋네예 무루기"
|
||||||
|
@ -654,7 +708,12 @@ _serverDisconnectedBehavior:
|
||||||
reload: "알아서 새로곤침"
|
reload: "알아서 새로곤침"
|
||||||
_channel:
|
_channel:
|
||||||
removeBanner: "배너 뭉캐기"
|
removeBanner: "배너 뭉캐기"
|
||||||
|
usersCount: "{n}명 참여"
|
||||||
|
notesCount: "노트 {n}개"
|
||||||
|
_menuDisplay:
|
||||||
|
hide: "수ᇚ후기"
|
||||||
_theme:
|
_theme:
|
||||||
|
description: "설멩"
|
||||||
keys:
|
keys:
|
||||||
mention: "멘션"
|
mention: "멘션"
|
||||||
_sfx:
|
_sfx:
|
||||||
|
@ -663,6 +722,9 @@ _sfx:
|
||||||
_2fa:
|
_2fa:
|
||||||
step3Title: "학인 기호럴 서기"
|
step3Title: "학인 기호럴 서기"
|
||||||
renewTOTPCancel: "뎃어예"
|
renewTOTPCancel: "뎃어예"
|
||||||
|
_permissions:
|
||||||
|
"read:favorites": "질겨찾기 보기"
|
||||||
|
"write:favorites": "질겨찾기 곤치기"
|
||||||
_widgets:
|
_widgets:
|
||||||
profile: "프로필"
|
profile: "프로필"
|
||||||
instanceInfo: "서버 정보"
|
instanceInfo: "서버 정보"
|
||||||
|
@ -674,7 +736,10 @@ _widgets:
|
||||||
_userList:
|
_userList:
|
||||||
chooseList: "리스트 개리기"
|
chooseList: "리스트 개리기"
|
||||||
_cw:
|
_cw:
|
||||||
|
hide: "수ᇚ후기"
|
||||||
show: "더 볼래예"
|
show: "더 볼래예"
|
||||||
|
chars: "걸자 {count}개"
|
||||||
|
files: "파일 {count}개"
|
||||||
_visibility:
|
_visibility:
|
||||||
home: "덜머리"
|
home: "덜머리"
|
||||||
followers: "팔로워"
|
followers: "팔로워"
|
||||||
|
@ -682,6 +747,7 @@ _profile:
|
||||||
name: "이럼"
|
name: "이럼"
|
||||||
username: "사용자 이럼"
|
username: "사용자 이럼"
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
|
favoritedNotes: "질겨찾기한 노트"
|
||||||
clips: "클립 맨걸기"
|
clips: "클립 맨걸기"
|
||||||
followingList: "팔로잉"
|
followingList: "팔로잉"
|
||||||
muteList: "수ᇚ후기"
|
muteList: "수ᇚ후기"
|
||||||
|
@ -692,16 +758,20 @@ _charts:
|
||||||
_timelines:
|
_timelines:
|
||||||
home: "덜머리"
|
home: "덜머리"
|
||||||
_play:
|
_play:
|
||||||
|
my: "내 플레이"
|
||||||
script: "스크립트"
|
script: "스크립트"
|
||||||
|
summary: "설멩"
|
||||||
_pages:
|
_pages:
|
||||||
like: "좋네예"
|
like: "좋네예"
|
||||||
unlike: "좋네예 무루기"
|
unlike: "좋네예 무루기"
|
||||||
|
my: "내 페이지"
|
||||||
blocks:
|
blocks:
|
||||||
image: "이미지"
|
image: "이미지"
|
||||||
_note:
|
_note:
|
||||||
id: "노트 아이디"
|
id: "노트 아이디"
|
||||||
_notification:
|
_notification:
|
||||||
youWereFollowed: "새 팔로워가 잇십니다"
|
youWereFollowed: "새 팔로워가 잇십니다"
|
||||||
|
newNote: "새 걸"
|
||||||
_types:
|
_types:
|
||||||
follow: "팔로잉"
|
follow: "팔로잉"
|
||||||
mention: "멘션"
|
mention: "멘션"
|
||||||
|
|
|
@ -279,7 +279,7 @@ uploadFromUrl: "URL 업로드"
|
||||||
uploadFromUrlDescription: "업로드하려는 파일의 URL"
|
uploadFromUrlDescription: "업로드하려는 파일의 URL"
|
||||||
uploadFromUrlRequested: "업로드를 요청했습니다"
|
uploadFromUrlRequested: "업로드를 요청했습니다"
|
||||||
uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 시간이 소요될 수 있습니다."
|
uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 시간이 소요될 수 있습니다."
|
||||||
explore: "발견하기"
|
explore: "둘러보기"
|
||||||
messageRead: "읽음"
|
messageRead: "읽음"
|
||||||
noMoreHistory: "이것보다 과거의 기록이 없습니다"
|
noMoreHistory: "이것보다 과거의 기록이 없습니다"
|
||||||
startMessaging: "대화 시작하기"
|
startMessaging: "대화 시작하기"
|
||||||
|
@ -1022,7 +1022,7 @@ internalServerError: "내부 서버 오류"
|
||||||
internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다."
|
internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했습니다."
|
||||||
copyErrorInfo: "오류 정보 복사"
|
copyErrorInfo: "오류 정보 복사"
|
||||||
joinThisServer: "이 서버에 가입"
|
joinThisServer: "이 서버에 가입"
|
||||||
exploreOtherServers: "다른 서버 둘러보기"
|
exploreOtherServers: "다른 서버 찾기"
|
||||||
letsLookAtTimeline: "타임라인 구경하기"
|
letsLookAtTimeline: "타임라인 구경하기"
|
||||||
disableFederationConfirm: "정말로 연합을 끄시겠습니까?"
|
disableFederationConfirm: "정말로 연합을 끄시겠습니까?"
|
||||||
disableFederationConfirmWarn: "연합을 끄더라도 게시물이 비공개로 전환되는 것은 아닙니다. 대부분의 경우 연합을 비활성화할 필요가 없습니다."
|
disableFederationConfirmWarn: "연합을 끄더라도 게시물이 비공개로 전환되는 것은 아닙니다. 대부분의 경우 연합을 비활성화할 필요가 없습니다."
|
||||||
|
@ -1797,7 +1797,7 @@ _instanceMute:
|
||||||
title: "지정한 서버의 노트를 숨깁니다."
|
title: "지정한 서버의 노트를 숨깁니다."
|
||||||
heading: "뮤트할 서버"
|
heading: "뮤트할 서버"
|
||||||
_theme:
|
_theme:
|
||||||
explore: "테마 찾아보기"
|
explore: "테마 둘러보기"
|
||||||
install: "테마 설치"
|
install: "테마 설치"
|
||||||
manage: "테마 관리"
|
manage: "테마 관리"
|
||||||
code: "테마 코드"
|
code: "테마 코드"
|
||||||
|
@ -2022,7 +2022,7 @@ _permissions:
|
||||||
"write:report-abuse": "위반 내용 신고하기"
|
"write:report-abuse": "위반 내용 신고하기"
|
||||||
_auth:
|
_auth:
|
||||||
shareAccessTitle: "어플리케이션의 접근 허가"
|
shareAccessTitle: "어플리케이션의 접근 허가"
|
||||||
shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?"
|
shareAccess: "‘{name}’에서 계정에 접근하는 것을 허용하시겠습니까?"
|
||||||
shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용하시겠습니까?"
|
shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용하시겠습니까?"
|
||||||
permission: "{name}에서 다음 권한을 요청하였습니다"
|
permission: "{name}에서 다음 권한을 요청하였습니다"
|
||||||
permissionAsk: "이 앱은 다음의 권한을 요청합니다"
|
permissionAsk: "이 앱은 다음의 권한을 요청합니다"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sharkey",
|
"name": "sharkey",
|
||||||
"version": "2024.2.0-beta.9",
|
"version": "2024.2.0-beta.10",
|
||||||
"codename": "shonk",
|
"codename": "shonk",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -40,6 +40,8 @@ export class EmailService {
|
||||||
public async sendEmail(to: string, subject: string, html: string, text: string) {
|
public async sendEmail(to: string, subject: string, html: string, text: string) {
|
||||||
const meta = await this.metaService.fetch(true);
|
const meta = await this.metaService.fetch(true);
|
||||||
|
|
||||||
|
if (!meta.enableEmail) return;
|
||||||
|
|
||||||
const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
|
const iconUrl = `${this.config.url}/static-assets/mi-white.png`;
|
||||||
const emailSettingUrl = `${this.config.url}/settings/email`;
|
const emailSettingUrl = `${this.config.url}/settings/email`;
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull, Not } from 'typeorm';
|
||||||
import type { MiLocalUser } from '@/models/User.js';
|
import type { MiLocalUser } from '@/models/User.js';
|
||||||
import type { UsersRepository } from '@/models/_.js';
|
import type { UsersRepository } from '@/models/_.js';
|
||||||
import { MemorySingleCache } from '@/misc/cache.js';
|
import { MemorySingleCache } from '@/misc/cache.js';
|
||||||
|
@ -27,6 +27,14 @@ export class InstanceActorService {
|
||||||
this.cache = new MemorySingleCache<MiLocalUser>(Infinity);
|
this.cache = new MemorySingleCache<MiLocalUser>(Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async realLocalUsersPresent(): Promise<boolean> {
|
||||||
|
return await this.usersRepository.existsBy({
|
||||||
|
host: IsNull(),
|
||||||
|
username: Not(ACTOR_USERNAME),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getInstanceActor(): Promise<MiLocalUser> {
|
public async getInstanceActor(): Promise<MiLocalUser> {
|
||||||
const cached = this.cache.get();
|
const cached = this.cache.get();
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||||
import { MiUsedUsername } from '@/models/UsedUsername.js';
|
import { MiUsedUsername } from '@/models/UsedUsername.js';
|
||||||
import generateUserToken from '@/misc/generate-native-user-token.js';
|
import generateUserToken from '@/misc/generate-native-user-token.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import UsersChart from '@/core/chart/charts/users.js';
|
import UsersChart from '@/core/chart/charts/users.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
@ -38,6 +39,7 @@ export class SignupService {
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
private instanceActorService: InstanceActorService,
|
||||||
private usersChart: UsersChart,
|
private usersChart: UsersChart,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -84,7 +86,7 @@ export class SignupService {
|
||||||
throw new Error('USED_USERNAME');
|
throw new Error('USED_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTheFirstUser = (await this.usersRepository.countBy({ host: IsNull() })) === 0;
|
const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent();
|
||||||
|
|
||||||
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
|
if (!opts.ignorePreservedUsernames && !isTheFirstUser) {
|
||||||
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
|
const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
|
||||||
|
|
|
@ -796,6 +796,7 @@ export class ApRendererService {
|
||||||
'https://www.w3.org/ns/activitystreams',
|
'https://www.w3.org/ns/activitystreams',
|
||||||
'https://w3id.org/security/v1',
|
'https://w3id.org/security/v1',
|
||||||
{
|
{
|
||||||
|
Key: 'sec:Key',
|
||||||
// as non-standards
|
// as non-standards
|
||||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||||
sensitive: 'as:sensitive',
|
sensitive: 'as:sensitive',
|
||||||
|
|
|
@ -225,23 +225,42 @@ export class ApPersonService implements OnModuleInit {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any, bgimg: any): Promise<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>> {
|
private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any, bgimg: any): Promise<Partial<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'backgroundId' | 'avatarUrl' | 'bannerUrl' | 'backgroundUrl' | 'avatarBlurhash' | 'bannerBlurhash' | 'backgroundBlurhash'>>> {
|
||||||
|
if (user == null) throw new Error('failed to create user: user is null');
|
||||||
|
|
||||||
const [avatar, banner, background] = await Promise.all([icon, image, bgimg].map(img => {
|
const [avatar, banner, background] = await Promise.all([icon, image, bgimg].map(img => {
|
||||||
if (img == null) return null;
|
// if we have an explicitly missing image, return an
|
||||||
if (user == null) throw new Error('failed to create user: user is null');
|
// explicitly-null set of values
|
||||||
|
if ((img == null) || (typeof img === 'object' && img.url == null)) {
|
||||||
|
return { id: null, url: null, blurhash: null };
|
||||||
|
}
|
||||||
|
|
||||||
return this.apImageService.resolveImage(user, img).catch(() => null);
|
return this.apImageService.resolveImage(user, img).catch(() => null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/*
|
||||||
|
we don't want to return nulls on errors! if the database fields
|
||||||
|
are already null, nothing changes; if the database has old
|
||||||
|
values, we should keep those. The exception is if the remote has
|
||||||
|
actually removed the images: in that case, the block above
|
||||||
|
returns the special {id:null}&c value, and we return those
|
||||||
|
*/
|
||||||
return {
|
return {
|
||||||
avatarId: avatar?.id ?? null,
|
...( avatar ? {
|
||||||
bannerId: banner?.id ?? null,
|
avatarId: avatar.id,
|
||||||
backgroundId: background?.id ?? null,
|
avatarUrl: avatar.url ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
|
||||||
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
|
avatarBlurhash: avatar.blurhash,
|
||||||
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
} : {}),
|
||||||
backgroundUrl: background ? this.driveFileEntityService.getPublicUrl(background) : null,
|
...( banner ? {
|
||||||
avatarBlurhash: avatar?.blurhash ?? null,
|
bannerId: banner.id,
|
||||||
bannerBlurhash: banner?.blurhash ?? null,
|
bannerUrl: banner.url ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
||||||
backgroundBlurhash: background?.blurhash ?? null
|
bannerBlurhash: banner.blurhash,
|
||||||
|
} : {}),
|
||||||
|
...( background ? {
|
||||||
|
backgroundId: background.id,
|
||||||
|
backgroundUrl: background.url ? this.driveFileEntityService.getPublicUrl(background) : null,
|
||||||
|
backgroundBlurhash: background.blurhash,
|
||||||
|
} : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ export class EmojiEntityService {
|
||||||
category: emoji.category,
|
category: emoji.category,
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: emoji.publicUrl || emoji.originalUrl,
|
||||||
|
localOnly: emoji.localOnly ? true : undefined,
|
||||||
isSensitive: emoji.isSensitive ? true : undefined,
|
isSensitive: emoji.isSensitive ? true : undefined,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,10 @@ export const packedEmojiSimpleSchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
localOnly: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
isSensitive: {
|
isSensitive: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: true, nullable: false,
|
optional: true, nullable: false,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { UsersRepository } from '@/models/_.js';
|
import type { UsersRepository } from '@/models/_.js';
|
||||||
import { SignupService } from '@/core/SignupService.js';
|
import { SignupService } from '@/core/SignupService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
|
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { Packed } from '@/misc/json-schema.js';
|
import { Packed } from '@/misc/json-schema.js';
|
||||||
|
@ -46,13 +47,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private signupService: SignupService,
|
private signupService: SignupService,
|
||||||
|
private instanceActorService: InstanceActorService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, _me, token) => {
|
super(meta, paramDef, async (ps, _me, token) => {
|
||||||
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
|
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
|
||||||
const noUsers = (await this.usersRepository.countBy({
|
const realUsers = await this.instanceActorService.realLocalUsersPresent();
|
||||||
host: IsNull(),
|
if ((realUsers && !me?.isRoot) || token !== null) throw new Error('access denied');
|
||||||
})) === 0;
|
|
||||||
if ((!noUsers && !me?.isRoot) || token !== null) throw new Error('access denied');
|
|
||||||
|
|
||||||
const { account, secret } = await this.signupService.signup({
|
const { account, secret } = await this.signupService.signup({
|
||||||
username: ps.username,
|
username: ps.username,
|
||||||
|
|
|
@ -6,10 +6,11 @@
|
||||||
import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm';
|
import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import JSON5 from 'json5';
|
import JSON5 from 'json5';
|
||||||
import type { AdsRepository, UsersRepository } from '@/models/_.js';
|
import type { AdsRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||||
|
@ -337,14 +338,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.adsRepository)
|
@Inject(DI.adsRepository)
|
||||||
private adsRepository: AdsRepository,
|
private adsRepository: AdsRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
private instanceActorService: InstanceActorService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const instance = await this.metaService.fetch(true);
|
const instance = await this.metaService.fetch(true);
|
||||||
|
@ -427,9 +426,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
...(ps.detail ? {
|
...(ps.detail ? {
|
||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
|
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
|
||||||
requireSetup: (await this.usersRepository.countBy({
|
requireSetup: !await this.instanceActorService.realLocalUsersPresent(),
|
||||||
host: IsNull(),
|
|
||||||
})) === 0,
|
|
||||||
} : {}),
|
} : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }]" v-html="html"></div>
|
<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { bundledLanguagesInfo } from 'shiki';
|
import { bundledLanguagesInfo } from 'shiki';
|
||||||
import type { BuiltinLanguage } from 'shiki';
|
import type { BuiltinLanguage } from 'shiki';
|
||||||
import { getHighlighter } from '@/scripts/code-highlighter.js';
|
import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
code: string;
|
code: string;
|
||||||
|
@ -21,11 +22,23 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const highlighter = await getHighlighter();
|
const highlighter = await getHighlighter();
|
||||||
|
const darkMode = defaultStore.reactiveState.darkMode;
|
||||||
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
|
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
|
||||||
|
|
||||||
|
const [lightThemeName, darkThemeName] = await Promise.all([
|
||||||
|
getTheme('light', true),
|
||||||
|
getTheme('dark', true),
|
||||||
|
]);
|
||||||
|
|
||||||
const html = computed(() => highlighter.codeToHtml(props.code, {
|
const html = computed(() => highlighter.codeToHtml(props.code, {
|
||||||
lang: codeLang.value,
|
lang: codeLang.value,
|
||||||
theme: 'dark-plus',
|
themes: {
|
||||||
|
fallback: 'dark-plus',
|
||||||
|
light: lightThemeName,
|
||||||
|
dark: darkThemeName,
|
||||||
|
},
|
||||||
|
defaultColor: false,
|
||||||
|
cssVariablePrefix: '--shiki-',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function fetchLanguage(to: string): Promise<void> {
|
async function fetchLanguage(to: string): Promise<void> {
|
||||||
|
@ -79,6 +92,15 @@ watch(() => props.lang, (to) => {
|
||||||
margin: .5em 0;
|
margin: .5em 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--divider);
|
||||||
|
|
||||||
|
color: var(--shiki-fallback);
|
||||||
|
background-color: var(--shiki-fallback-bg);
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: var(--shiki-fallback);
|
||||||
|
background-color: var(--shiki-fallback-bg);
|
||||||
|
}
|
||||||
|
|
||||||
& pre,
|
& pre,
|
||||||
& code {
|
& code {
|
||||||
|
@ -86,6 +108,26 @@ watch(() => props.lang, (to) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.light.codeBlockRoot :global(.shiki) {
|
||||||
|
color: var(--shiki-light);
|
||||||
|
background-color: var(--shiki-light-bg);
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: var(--shiki-light);
|
||||||
|
background-color: var(--shiki-light-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark.codeBlockRoot :global(.shiki) {
|
||||||
|
color: var(--shiki-dark);
|
||||||
|
background-color: var(--shiki-dark-bg);
|
||||||
|
|
||||||
|
& span {
|
||||||
|
color: var(--shiki-dark);
|
||||||
|
background-color: var(--shiki-dark-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.codeBlockRoot.codeEditor {
|
.codeBlockRoot.codeEditor {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -94,6 +136,7 @@ watch(() => props.lang, (to) => {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
border: none;
|
||||||
min-height: 130px;
|
min-height: 130px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
min-width: calc(100% - 24px);
|
min-width: calc(100% - 24px);
|
||||||
|
@ -105,6 +148,11 @@ watch(() => props.lang, (to) => {
|
||||||
text-rendering: inherit;
|
text-rendering: inherit;
|
||||||
text-transform: inherit;
|
text-transform: inherit;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
|
||||||
|
& span {
|
||||||
|
display: inline-block;
|
||||||
|
min-height: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -53,7 +53,6 @@ function copy() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.codeBlockCopyButton {
|
.codeBlockCopyButton {
|
||||||
color: #D4D4D4;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
|
@ -67,8 +66,7 @@ function copy() {
|
||||||
.codeBlockFallbackRoot {
|
.codeBlockFallbackRoot {
|
||||||
display: block;
|
display: block;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
color: #D4D4D4;
|
background: var(--bg);
|
||||||
background: #1E1E1E;
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin: .5em 0;
|
margin: .5em 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
@ -93,8 +91,8 @@ function copy() {
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #D4D4D4;
|
color: var(--fg);
|
||||||
background: #1E1E1E;
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.codePlaceholderContainer {
|
.codePlaceholderContainer {
|
||||||
|
|
|
@ -196,10 +196,11 @@ watch(v, newValue => {
|
||||||
resize: none;
|
resize: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
caret-color: rgb(225, 228, 232);
|
caret-color: var(--fg);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
box-sizing: border-box;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
min-width: calc(100% - 24px);
|
min-width: calc(100% - 24px);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -212,6 +213,6 @@ watch(v, newValue => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea::selection {
|
.textarea::selection {
|
||||||
color: #fff;
|
color: var(--bg);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,8 +18,7 @@ const props = defineProps<{
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
color: #D4D4D4;
|
background: var(--bg);
|
||||||
background: #1E1E1E;
|
|
||||||
padding: .1em;
|
padding: .1em;
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,18 +63,25 @@ const loading = ref(true);
|
||||||
|
|
||||||
const ok = async () => {
|
const ok = async () => {
|
||||||
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => {
|
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => {
|
||||||
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
|
const croppedImage = await cropper?.getCropperImage();
|
||||||
|
const croppedSection = await cropper?.getCropperSelection();
|
||||||
|
|
||||||
|
// 拡大率を計算し、(ほぼ)元の大きさに戻す
|
||||||
|
const zoomedRate = croppedImage.getBoundingClientRect().width / croppedImage.clientWidth;
|
||||||
|
const widthToRender = croppedSection.getBoundingClientRect().width / zoomedRate;
|
||||||
|
|
||||||
|
const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender });
|
||||||
croppedCanvas?.toBlob(blob => {
|
croppedCanvas?.toBlob(blob => {
|
||||||
if (!blob) return;
|
if (!blob) return;
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', blob);
|
formData.append('file', blob);
|
||||||
formData.append('name', `cropped_${props.file.name}`);
|
formData.append('name', `cropped_${props.file.name}`);
|
||||||
formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false');
|
formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false');
|
||||||
formData.append('comment', props.file.comment ?? 'null');
|
if (props.file.comment) { formData.append('comment', props.file.comment);}
|
||||||
formData.append('i', $i!.token);
|
formData.append('i', $i!.token);
|
||||||
if (props.uploadFolder || props.uploadFolder === null) {
|
if (props.uploadFolder) {
|
||||||
formData.append('folderId', props.uploadFolder ?? 'null');
|
formData.append('folderId', props.uploadFolder);
|
||||||
} else if (defaultStore.state.uploadFolder) {
|
} else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) {
|
||||||
formData.append('folderId', defaultStore.state.uploadFolder);
|
formData.append('folderId', defaultStore.state.uploadFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,7 @@ import { i18n } from '@/i18n.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
|
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
|
@ -126,6 +127,7 @@ const props = withDefaults(defineProps<{
|
||||||
asDrawer?: boolean;
|
asDrawer?: boolean;
|
||||||
asWindow?: boolean;
|
asWindow?: boolean;
|
||||||
asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
|
asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
|
||||||
|
targetNote?: Misskey.entities.Note;
|
||||||
}>(), {
|
}>(), {
|
||||||
showPinned: true,
|
showPinned: true,
|
||||||
});
|
});
|
||||||
|
@ -340,7 +342,7 @@ watch(q, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
|
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
|
||||||
return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) ?? false;
|
return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
|
|
|
@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:showPinned="showPinned"
|
:showPinned="showPinned"
|
||||||
:pinnedEmojis="pinnedEmojis"
|
:pinnedEmojis="pinnedEmojis"
|
||||||
:asReactionPicker="asReactionPicker"
|
:asReactionPicker="asReactionPicker"
|
||||||
|
:targetNote="targetNote"
|
||||||
:asDrawer="type === 'drawer'"
|
:asDrawer="type === 'drawer'"
|
||||||
:max-height="maxHeight"
|
:max-height="maxHeight"
|
||||||
@chosen="chosen"
|
@chosen="chosen"
|
||||||
|
@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { shallowRef } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||||
|
@ -43,6 +45,7 @@ const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
pinnedEmojis?: string[],
|
pinnedEmojis?: string[],
|
||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
|
targetNote?: Misskey.entities.Note;
|
||||||
choseAndClose?: boolean;
|
choseAndClose?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
manualShowing: null,
|
manualShowing: null,
|
||||||
|
|
|
@ -13,12 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:front="true"
|
:front="true"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/>
|
<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" :targetNote="targetNote" asWindow :class="$style.picker" @chosen="chosen"/>
|
||||||
</MkWindow>
|
</MkWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import MkWindow from '@/components/MkWindow.vue';
|
import MkWindow from '@/components/MkWindow.vue';
|
||||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ withDefaults(defineProps<{
|
||||||
src?: HTMLElement;
|
src?: HTMLElement;
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
|
targetNote?: Misskey.entities.Note
|
||||||
}>(), {
|
}>(), {
|
||||||
showPinned: true,
|
showPinned: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -119,6 +119,7 @@ function close() {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .indicatorWithValue {
|
> .indicatorWithValue {
|
||||||
|
|
|
@ -285,13 +285,11 @@ const quoteButton = shallowRef<HTMLElement>();
|
||||||
const clipButton = shallowRef<HTMLElement>();
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
const likeButton = shallowRef<HTMLElement>();
|
const likeButton = shallowRef<HTMLElement>();
|
||||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||||
const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
|
|
||||||
const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
|
|
||||||
|
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||||
const showContent = ref(defaultStore.state.uncollapseCW);
|
const showContent = ref(defaultStore.state.uncollapseCW);
|
||||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||||
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter(u => u !== renoteUrl && u !== renoteUri) : null);
|
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
|
||||||
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
||||||
const collapsed = ref(defaultStore.state.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
const collapsed = ref(defaultStore.state.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
|
@ -624,7 +622,7 @@ function react(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
|
|
|
@ -303,8 +303,6 @@ const quoteButton = shallowRef<HTMLElement>();
|
||||||
const clipButton = shallowRef<HTMLElement>();
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
const likeButton = shallowRef<HTMLElement>();
|
const likeButton = shallowRef<HTMLElement>();
|
||||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||||
const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
|
|
||||||
const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
|
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||||
const showContent = ref(defaultStore.state.uncollapseCW);
|
const showContent = ref(defaultStore.state.uncollapseCW);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
|
@ -313,7 +311,7 @@ const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : fals
|
||||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
||||||
const urls = parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null;
|
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
|
||||||
const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
|
const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
|
||||||
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
|
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||||
|
@ -612,7 +610,7 @@ function react(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value ?? null, reaction => {
|
reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
|
|
|
@ -76,7 +76,7 @@ const buttonsLeft = computed(() => {
|
||||||
});
|
});
|
||||||
const buttonsRight = computed(() => {
|
const buttonsRight = computed(() => {
|
||||||
const buttons = [{
|
const buttons = [{
|
||||||
icon: 'ph-arrow-clockwise ph-bold ph-lg',
|
icon: 'ph-arrows-clockwise ph-bold ph-lg',
|
||||||
title: i18n.ts.reload,
|
title: i18n.ts.reload,
|
||||||
onClick: reload,
|
onClick: reload,
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -32,6 +32,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
|
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
|
||||||
|
import { customEmojis } from '@/custom-emojis.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
reaction: string;
|
reaction: string;
|
||||||
|
@ -48,13 +50,19 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const buttonEl = shallowRef<HTMLElement>();
|
const buttonEl = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
const isCustomEmoji = computed(() => props.reaction.includes(':'));
|
||||||
|
const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null);
|
||||||
|
|
||||||
|
const canToggle = computed(() => {
|
||||||
|
return !props.reaction.match(/@\w/) && $i
|
||||||
|
&& (emoji.value && checkReactionPermissions($i, props.note, emoji.value))
|
||||||
|
|| !isCustomEmoji.value;
|
||||||
|
});
|
||||||
|
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
|
||||||
|
|
||||||
async function toggleReaction() {
|
async function toggleReaction() {
|
||||||
if (!canToggle.value) return;
|
if (!canToggle.value) return;
|
||||||
|
|
||||||
// TODO: その絵文字を使う権限があるかどうか確認
|
|
||||||
|
|
||||||
const oldReaction = props.note.myReaction;
|
const oldReaction = props.note.myReaction;
|
||||||
if (oldReaction) {
|
if (oldReaction) {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
|
@ -101,8 +109,8 @@ async function toggleReaction() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function menu(ev) {
|
async function menu(ev) {
|
||||||
if (!canToggle.value) return;
|
if (!canGetInfo.value) return;
|
||||||
if (!props.reaction.includes(':')) return;
|
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.info,
|
text: i18n.ts.info,
|
||||||
icon: 'ph-info ph-bold ph-lg',
|
icon: 'ph-info ph-bold ph-lg',
|
||||||
|
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.caption"><slot name="caption"></slot></div>
|
<div :class="$style.caption"><slot name="caption"></slot></div>
|
||||||
|
|
||||||
<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -138,6 +138,7 @@ function show() {
|
||||||
active: computed(() => v.value === option.props?.value),
|
active: computed(() => v.value === option.props?.value),
|
||||||
action: () => {
|
action: () => {
|
||||||
v.value = option.props?.value;
|
v.value = option.props?.value;
|
||||||
|
changed.value = true;
|
||||||
emit('changeByUser', v.value);
|
emit('changeByUser', v.value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -288,6 +289,10 @@ function show() {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save {
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
transition: transform 0.1s ease-out;
|
transition: transform 0.1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
|
@ -286,13 +286,11 @@ const quoteButton = shallowRef<HTMLElement>();
|
||||||
const clipButton = shallowRef<HTMLElement>();
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
const likeButton = shallowRef<HTMLElement>();
|
const likeButton = shallowRef<HTMLElement>();
|
||||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||||
const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
|
|
||||||
const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
|
|
||||||
|
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||||
const showContent = ref(defaultStore.state.uncollapseCW);
|
const showContent = ref(defaultStore.state.uncollapseCW);
|
||||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||||
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter(u => u !== renoteUrl && u !== renoteUri) : null);
|
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
|
||||||
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
||||||
const collapsed = ref(defaultStore.state.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
const collapsed = ref(defaultStore.state.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
|
|
|
@ -312,8 +312,6 @@ const quoteButton = shallowRef<HTMLElement>();
|
||||||
const clipButton = shallowRef<HTMLElement>();
|
const clipButton = shallowRef<HTMLElement>();
|
||||||
const likeButton = shallowRef<HTMLElement>();
|
const likeButton = shallowRef<HTMLElement>();
|
||||||
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
|
||||||
const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
|
|
||||||
const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
|
|
||||||
const isMyRenote = $i && ($i.id === note.value.userId);
|
const isMyRenote = $i && ($i.id === note.value.userId);
|
||||||
const showContent = ref(defaultStore.state.uncollapseCW);
|
const showContent = ref(defaultStore.state.uncollapseCW);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
|
@ -322,7 +320,7 @@ const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : fals
|
||||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
|
||||||
const urls = parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null;
|
const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
|
||||||
const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
|
const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
|
||||||
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
|
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||||
|
|
|
@ -4,7 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<KeepAlive :max="defaultStore.state.numberOfPageCache">
|
<KeepAlive
|
||||||
|
:max="defaultStore.state.numberOfPageCache"
|
||||||
|
:exclude="pageCacheController"
|
||||||
|
>
|
||||||
<Suspense :timeout="0">
|
<Suspense :timeout="0">
|
||||||
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
||||||
|
|
||||||
|
@ -16,9 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue';
|
import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue';
|
||||||
import { IRouter, Resolved } from '@/nirax.js';
|
import { IRouter, Resolved, RouteDef } from '@/nirax.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { globalEvents } from '@/events.js';
|
||||||
|
import MkLoadingPage from '@/pages/_loading_.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
router?: IRouter;
|
router?: IRouter;
|
||||||
|
@ -46,20 +51,47 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = resolveNested(router.current)!;
|
const current = resolveNested(router.current)!;
|
||||||
const currentPageComponent = shallowRef(current.route.component);
|
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
|
||||||
const currentPageProps = ref(current.props);
|
const currentPageProps = ref(current.props);
|
||||||
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
|
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
|
||||||
|
|
||||||
function onChange({ resolved, key: newKey }) {
|
function onChange({ resolved, key: newKey }) {
|
||||||
const current = resolveNested(resolved);
|
const current = resolveNested(resolved);
|
||||||
if (current == null) return;
|
if (current == null || 'redirect' in current.route) return;
|
||||||
currentPageComponent.value = current.route.component;
|
currentPageComponent.value = current.route.component;
|
||||||
currentPageProps.value = current.props;
|
currentPageProps.value = current.props;
|
||||||
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
|
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
// ページ遷移完了後に再びキャッシュを有効化
|
||||||
|
if (clearCacheRequested.value) {
|
||||||
|
clearCacheRequested.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
router.addListener('change', onChange);
|
router.addListener('change', onChange);
|
||||||
|
|
||||||
|
// #region キャッシュ制御
|
||||||
|
|
||||||
|
/**
|
||||||
|
* キャッシュクリアが有効になったら、全キャッシュをクリアする
|
||||||
|
*
|
||||||
|
* keepAlive側にwatcherがあるのですぐ消えるとはおもうけど、念のためページ遷移完了まではキャッシュを無効化しておく。
|
||||||
|
* キャッシュ有効時向けにexcludeを使いたい場合は、pageCacheControllerに並列に突っ込むのではなく、下に追記すること
|
||||||
|
*/
|
||||||
|
const pageCacheController = computed(() => clearCacheRequested.value ? /.*/ : undefined);
|
||||||
|
const clearCacheRequested = ref(false);
|
||||||
|
|
||||||
|
globalEvents.on('requestClearPageCache', () => {
|
||||||
|
if (_DEV_) console.log('clear page cache requested');
|
||||||
|
if (!clearCacheRequested.value) {
|
||||||
|
clearCacheRequested.value = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
router.removeListener('change', onChange);
|
router.removeListener('change', onChange);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
// TODO: 型付け
|
export const globalEvents = new EventEmitter<{
|
||||||
export const globalEvents = new EventEmitter();
|
themeChanged: () => void;
|
||||||
|
clientNotification: (notification: Misskey.entities.Notification) => void;
|
||||||
|
requestClearPageCache: () => void;
|
||||||
|
}>();
|
||||||
|
|
|
@ -128,9 +128,10 @@ export function promiseDialog<T extends Promise<any>>(
|
||||||
|
|
||||||
let popupIdCount = 0;
|
let popupIdCount = 0;
|
||||||
export const popups = ref([]) as Ref<{
|
export const popups = ref([]) as Ref<{
|
||||||
id: any;
|
id: number;
|
||||||
component: any;
|
component: Component;
|
||||||
props: Record<string, any>;
|
props: Record<string, any>;
|
||||||
|
events: Record<string, any>;
|
||||||
}[]>;
|
}[]>;
|
||||||
|
|
||||||
const zIndexes = {
|
const zIndexes = {
|
||||||
|
@ -144,7 +145,18 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
|
||||||
return zIndexes[priority];
|
return zIndexes[priority];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function popup<T extends Component>(component: T, props: ComponentProps<T>, events = {}, disposeEvent?: string) {
|
// InstanceType<typeof Component>['$emit'] だとインターセクション型が返ってきて
|
||||||
|
// 使い物にならないので、代わりに ['$props'] から色々省くことで emit の型を生成する
|
||||||
|
// FIXME: 何故か *.ts ファイルからだと型がうまく取れない?ことがあるのをなんとかしたい
|
||||||
|
type ComponentEmit<T> = T extends new () => { $props: infer Props }
|
||||||
|
? EmitsExtractor<Props>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type EmitsExtractor<T> = {
|
||||||
|
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function popup<T extends Component>(component: T, props: ComponentProps<T>, events: ComponentEmit<T> = {} as ComponentEmit<T>, disposeEvent?: keyof ComponentEmit<T>) {
|
||||||
markRaw(component);
|
markRaw(component);
|
||||||
|
|
||||||
const id = ++popupIdCount;
|
const id = ++popupIdCount;
|
||||||
|
|
|
@ -31,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="provider === 'mcaptcha'">
|
<template v-else-if="provider === 'mcaptcha'">
|
||||||
<MkInput v-model="mcaptchaSiteKey">
|
<MkInput v-model="mcaptchaSiteKey">
|
||||||
<template #prefix><i class="ti ti-key"></i></template>
|
<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
|
||||||
<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
|
<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="mcaptchaSecretKey">
|
<MkInput v-model="mcaptchaSecretKey">
|
||||||
<template #prefix><i class="ti ti-key"></i></template>
|
<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
|
||||||
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
|
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="mcaptchaInstanceUrl">
|
<MkInput v-model="mcaptchaInstanceUrl">
|
||||||
<template #prefix><i class="ti ti-link"></i></template>
|
<template #prefix><i class="ph-globe-simple ph-bold ph-lg"></i></template>
|
||||||
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
|
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<FormSlot v-if="mcaptchaSiteKey && mcaptchaInstanceUrl">
|
<FormSlot v-if="mcaptchaSiteKey && mcaptchaInstanceUrl">
|
||||||
|
|
|
@ -158,9 +158,9 @@ function save() {
|
||||||
themeColor: themeColor.value === '' ? null : themeColor.value,
|
themeColor: themeColor.value === '' ? null : themeColor.value,
|
||||||
defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value,
|
defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value,
|
||||||
defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value,
|
defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value,
|
||||||
infoImageUrl: infoImageUrl.value,
|
infoImageUrl: infoImageUrl.value === '' ? null : infoImageUrl.value,
|
||||||
notFoundImageUrl: notFoundImageUrl.value,
|
notFoundImageUrl: notFoundImageUrl.value === '' ? null : notFoundImageUrl.value,
|
||||||
serverErrorImageUrl: serverErrorImageUrl.value,
|
serverErrorImageUrl: serverErrorImageUrl.value === '' ? null : serverErrorImageUrl.value,
|
||||||
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
|
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
|
|
|
@ -42,14 +42,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>Use TrueMail API</template>
|
<template #label>Use TrueMail API</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkInput v-model="truemailInstance">
|
<MkInput v-model="truemailInstance">
|
||||||
<template #prefix><i class="ti ti-key"></i></template>
|
<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
|
||||||
<template #label>TrueMail API Instance</template>
|
<template #label>TrueMail API Instance</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="truemailAuthKey">
|
<MkInput v-model="truemailAuthKey">
|
||||||
<template #prefix><i class="ti ti-key"></i></template>
|
<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
|
||||||
<template #label>TrueMail API Auth Key</template>
|
<template #label>TrueMail API Auth Key</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
|
|
@ -893,7 +893,6 @@ function getGameImageDriveFile() {
|
||||||
formData.append('file', blob);
|
formData.append('file', blob);
|
||||||
formData.append('name', `bubble-game-${Date.now()}.png`);
|
formData.append('name', `bubble-game-${Date.now()}.png`);
|
||||||
formData.append('isSensitive', 'false');
|
formData.append('isSensitive', 'false');
|
||||||
formData.append('comment', 'null');
|
|
||||||
formData.append('i', $i.token);
|
formData.append('i', $i.token);
|
||||||
if (defaultStore.state.uploadFolder) {
|
if (defaultStore.state.uploadFolder) {
|
||||||
formData.append('folderId', defaultStore.state.uploadFolder);
|
formData.append('folderId', defaultStore.state.uploadFolder);
|
||||||
|
|
|
@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
||||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
||||||
<MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">Mark as NSFW</MkSwitch>
|
<MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">Mark as NSFW</MkSwitch>
|
||||||
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
<MkButton @click="refreshMetadata"><i class="ph-arrows-clockwise ph-bold ph-lg"></i> Refresh metadata</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
|
|
@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.gamePreviews">
|
<div :class="$style.gamePreviews">
|
||||||
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
||||||
<div :class="$style.gamePreviewPlayers">
|
<div :class="$style.gamePreviewPlayers">
|
||||||
<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ph-trophy ph-bold ph-lg"></i></span>
|
||||||
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
|
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ph-x ph-bold ph-lg"></i></span>
|
||||||
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
|
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
|
||||||
<span style="margin: 0 1em;">vs</span>
|
<span style="margin: 0 1em;">vs</span>
|
||||||
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
|
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
|
||||||
<span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
|
<span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ph-x ph-bold ph-lg"></i></span>
|
||||||
<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ph-trophy ph-bold ph-lg"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.gamePreviewFooter">
|
<div :class="$style.gamePreviewFooter">
|
||||||
<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
||||||
|
@ -63,13 +63,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.gamePreviews">
|
<div :class="$style.gamePreviews">
|
||||||
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
|
||||||
<div :class="$style.gamePreviewPlayers">
|
<div :class="$style.gamePreviewPlayers">
|
||||||
<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
<span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ph-trophy ph-bold ph-lg"></i></span>
|
||||||
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
|
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ph-x ph-bold ph-lg"></i></span>
|
||||||
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
|
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
|
||||||
<span style="margin: 0 1em;">vs</span>
|
<span style="margin: 0 1em;">vs</span>
|
||||||
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
|
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
|
||||||
<span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
|
<span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ph-x ph-bold ph-lg"></i></span>
|
||||||
<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
|
<span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ph-trophy ph-bold ph-lg"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.gamePreviewFooter">
|
<div :class="$style.gamePreviewFooter">
|
||||||
<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
|
||||||
|
|
|
@ -172,7 +172,7 @@ const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
|
||||||
const setDefaultEmoji = () => setDefault(pinnedEmojis);
|
const setDefaultEmoji = () => setDefault(pinnedEmojis);
|
||||||
|
|
||||||
function previewReaction(ev: MouseEvent) {
|
function previewReaction(ev: MouseEvent) {
|
||||||
reactionPicker.show(getHTMLElement(ev));
|
reactionPicker.show(getHTMLElement(ev), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewEmoji(ev: MouseEvent) {
|
function previewEmoji(ev: MouseEvent) {
|
||||||
|
|
|
@ -132,6 +132,7 @@ import { langmap } from '@/scripts/langmap.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
import { globalEvents } from '@/events.js';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
|
|
||||||
|
@ -158,7 +159,7 @@ const profile = reactive({
|
||||||
lang: $i.lang,
|
lang: $i.lang,
|
||||||
isBot: $i.isBot ?? false,
|
isBot: $i.isBot ?? false,
|
||||||
isCat: $i.isCat ?? false,
|
isCat: $i.isCat ?? false,
|
||||||
speakAsCat: $i.speakAsCat,
|
speakAsCat: $i.speakAsCat ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => profile, () => {
|
watch(() => profile, () => {
|
||||||
|
@ -190,6 +191,7 @@ function saveFields() {
|
||||||
os.apiWithDialog('i/update', {
|
os.apiWithDialog('i/update', {
|
||||||
fields: fields.value.filter(field => field.name !== '' && field.value !== '').map(field => ({ name: field.name, value: field.value })),
|
fields: fields.value.filter(field => field.name !== '' && field.value !== '').map(field => ({ name: field.name, value: field.value })),
|
||||||
});
|
});
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
@ -217,6 +219,7 @@ function save() {
|
||||||
isCat: !!profile.isCat,
|
isCat: !!profile.isCat,
|
||||||
speakAsCat: !!profile.speakAsCat,
|
speakAsCat: !!profile.speakAsCat,
|
||||||
});
|
});
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
claimAchievement('profileFilled');
|
claimAchievement('profileFilled');
|
||||||
if (profile.name === 'syuilo' || profile.name === 'しゅいろ') {
|
if (profile.name === 'syuilo' || profile.name === 'しゅいろ') {
|
||||||
claimAchievement('setNameToSyuilo');
|
claimAchievement('setNameToSyuilo');
|
||||||
|
@ -248,6 +251,7 @@ function changeAvatar(ev) {
|
||||||
});
|
});
|
||||||
$i.avatarId = i.avatarId;
|
$i.avatarId = i.avatarId;
|
||||||
$i.avatarUrl = i.avatarUrl;
|
$i.avatarUrl = i.avatarUrl;
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
claimAchievement('profileFilled');
|
claimAchievement('profileFilled');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -278,6 +282,7 @@ function changeBanner(ev) {
|
||||||
});
|
});
|
||||||
$i.bannerId = i.bannerId;
|
$i.bannerId = i.bannerId;
|
||||||
$i.bannerUrl = i.bannerUrl;
|
$i.bannerUrl = i.bannerUrl;
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
@ -288,6 +293,7 @@ function changeBanner(ev) {
|
||||||
});
|
});
|
||||||
$i.bannerId = i.bannerId;
|
$i.bannerId = i.bannerId;
|
||||||
$i.bannerUrl = i.bannerUrl;
|
$i.bannerUrl = i.bannerUrl;
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
},
|
},
|
||||||
}], ev.currentTarget ?? ev.target);
|
}], ev.currentTarget ?? ev.target);
|
||||||
} else {
|
} else {
|
||||||
|
@ -312,6 +318,7 @@ function changeBanner(ev) {
|
||||||
});
|
});
|
||||||
$i.bannerId = i.bannerId;
|
$i.bannerId = i.bannerId;
|
||||||
$i.bannerUrl = i.bannerUrl;
|
$i.bannerUrl = i.bannerUrl;
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -342,6 +349,7 @@ function changeBackground(ev) {
|
||||||
});
|
});
|
||||||
$i.backgroundId = i.backgroundId;
|
$i.backgroundId = i.backgroundId;
|
||||||
$i.backgroundUrl = i.backgroundUrl;
|
$i.backgroundUrl = i.backgroundUrl;
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
@ -352,6 +360,7 @@ function changeBackground(ev) {
|
||||||
});
|
});
|
||||||
$i.backgroundId = i.backgroundId;
|
$i.backgroundId = i.backgroundId;
|
||||||
$i.backgroundUrl = i.backgroundUrl;
|
$i.backgroundUrl = i.backgroundUrl;
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
},
|
},
|
||||||
}], ev.currentTarget ?? ev.target);
|
}], ev.currentTarget ?? ev.target);
|
||||||
} else {
|
} else {
|
||||||
|
@ -376,6 +385,7 @@ function changeBackground(ev) {
|
||||||
});
|
});
|
||||||
$i.backgroundId = i.backgroundId;
|
$i.backgroundId = i.backgroundId;
|
||||||
$i.backgroundUrl = i.backgroundUrl;
|
$i.backgroundUrl = i.backgroundUrl;
|
||||||
|
globalEvents.emit('requestClearPageCache');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,18 @@ import { uniqueBy } from '@/scripts/array.js';
|
||||||
import { fetchThemes, getThemes } from '@/theme-store.js';
|
import { fetchThemes, getThemes } from '@/theme-store.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
|
|
||||||
|
async function reloadAsk() {
|
||||||
|
const { canceled } = await os.confirm({
|
||||||
|
type: 'info',
|
||||||
|
text: i18n.ts.reloadToApplySetting,
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
unisonReload();
|
||||||
|
}
|
||||||
|
|
||||||
const installedThemes = ref(getThemes());
|
const installedThemes = ref(getThemes());
|
||||||
const builtinThemes = getBuiltinThemesRef();
|
const builtinThemes = getBuiltinThemesRef();
|
||||||
|
@ -124,6 +136,7 @@ const lightThemeId = computed({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
|
||||||
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
|
||||||
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
|
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
|
||||||
|
@ -141,7 +154,7 @@ watch(wallpaper, () => {
|
||||||
} else {
|
} else {
|
||||||
miLocalStorage.setItem('wallpaper', wallpaper.value);
|
miLocalStorage.setItem('wallpaper', wallpaper.value);
|
||||||
}
|
}
|
||||||
location.reload();
|
reloadAsk();
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { get, set } from '@/scripts/idb-proxy.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
|
import { deepMerge } from '@/scripts/merge.js';
|
||||||
|
|
||||||
type StateDef = Record<string, {
|
type StateDef = Record<string, {
|
||||||
where: 'account' | 'device' | 'deviceAccount';
|
where: 'account' | 'device' | 'deviceAccount';
|
||||||
|
@ -84,29 +85,9 @@ export class Storage<T extends StateDef> {
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* valueにないキーをdefからもらう(再帰的)\
|
|
||||||
* nullはそのまま、undefinedはdefの値
|
|
||||||
**/
|
|
||||||
private mergeObject<X>(value: X, def: X): X {
|
|
||||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
|
||||||
const result = structuredClone(value) as X;
|
|
||||||
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
|
|
||||||
result[k] = v;
|
|
||||||
} else if (this.isPureObject(v) && this.isPureObject(result[k])) {
|
|
||||||
const child = structuredClone(result[k]) as X[keyof X] & Record<string | number | symbol, unknown>;
|
|
||||||
result[k] = this.mergeObject<typeof v>(child, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private mergeState<X>(value: X, def: X): X {
|
private mergeState<X>(value: X, def: X): X {
|
||||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||||
const merged = this.mergeObject(value, def);
|
const merged = deepMerge(value, def);
|
||||||
|
|
||||||
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
|
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
|
||||||
|
|
||||||
|
@ -258,7 +239,7 @@ export class Storage<T extends StateDef> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 特定のキーの、簡易的なgetter/setterを作ります
|
* 特定のキーの、簡易的なgetter/setterを作ります
|
||||||
* 主にvue場で設定コントロールのmodelとして使う用
|
* 主にvue上で設定コントロールのmodelとして使う用
|
||||||
*/
|
*/
|
||||||
public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): {
|
public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): {
|
||||||
get: () => T[K]['default'];
|
get: () => T[K]['default'];
|
||||||
|
|
|
@ -80,6 +80,10 @@ class MainRouterProxy implements IRouter {
|
||||||
return this.supplier().resolve(path);
|
return this.supplier().resolve(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.supplier().init();
|
||||||
|
}
|
||||||
|
|
||||||
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
|
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
|
||||||
return this.supplier().eventNames();
|
return this.supplier().eventNames();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
|
export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple): boolean {
|
||||||
|
const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
|
||||||
|
return !(emoji.localOnly && note.user.host !== me.host)
|
||||||
|
&& !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
|
||||||
|
&& (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)));
|
||||||
|
}
|
|
@ -8,13 +8,13 @@
|
||||||
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
|
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
|
||||||
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
|
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
|
||||||
|
|
||||||
type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[];
|
export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[];
|
||||||
|
|
||||||
export function deepClone<T extends Cloneable>(x: T): T {
|
export function deepClone<T extends Cloneable>(x: T): T {
|
||||||
if (typeof x === 'object') {
|
if (typeof x === 'object') {
|
||||||
if (x === null) return x;
|
if (x === null) return x;
|
||||||
if (Array.isArray(x)) return x.map(deepClone) as T;
|
if (Array.isArray(x)) return x.map(deepClone) as T;
|
||||||
const obj = {} as Record<string, Cloneable>;
|
const obj = {} as Record<string | number | symbol, Cloneable>;
|
||||||
for (const [k, v] of Object.entries(x)) {
|
for (const [k, v] of Object.entries(x)) {
|
||||||
obj[k] = v === undefined ? undefined : deepClone(v);
|
obj[k] = v === undefined ? undefined : deepClone(v);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,51 @@
|
||||||
|
import { bundledThemesInfo } from 'shiki';
|
||||||
import { getHighlighterCore, loadWasm } from 'shiki/core';
|
import { getHighlighterCore, loadWasm } from 'shiki/core';
|
||||||
import darkPlus from 'shiki/themes/dark-plus.mjs';
|
import darkPlus from 'shiki/themes/dark-plus.mjs';
|
||||||
import type { Highlighter, LanguageRegistration } from 'shiki';
|
import { unique } from './array.js';
|
||||||
|
import { deepClone } from './clone.js';
|
||||||
|
import { deepMerge } from './merge.js';
|
||||||
|
import type { Highlighter, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki';
|
||||||
|
import { ColdDeviceStorage } from '@/store.js';
|
||||||
|
import lightTheme from '@/themes/_light.json5';
|
||||||
|
import darkTheme from '@/themes/_dark.json5';
|
||||||
|
|
||||||
let _highlighter: Highlighter | null = null;
|
let _highlighter: Highlighter | null = null;
|
||||||
|
|
||||||
|
export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>;
|
||||||
|
export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>;
|
||||||
|
export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> {
|
||||||
|
const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme'));
|
||||||
|
|
||||||
|
if (theme.base) {
|
||||||
|
const base = [lightTheme, darkTheme].find(x => x.id === theme.base);
|
||||||
|
if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theme.codeHighlighter) {
|
||||||
|
let _res: ThemeRegistration = {};
|
||||||
|
if (theme.codeHighlighter.base === '_none_') {
|
||||||
|
_res = deepClone(theme.codeHighlighter.overrides);
|
||||||
|
} else {
|
||||||
|
const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus;
|
||||||
|
_res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base);
|
||||||
|
}
|
||||||
|
if (_res.name == null) {
|
||||||
|
_res.name = theme.id;
|
||||||
|
}
|
||||||
|
_res.type = mode;
|
||||||
|
|
||||||
|
if (getName) {
|
||||||
|
return _res.name;
|
||||||
|
}
|
||||||
|
return _res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getName) {
|
||||||
|
return 'dark-plus';
|
||||||
|
}
|
||||||
|
return darkPlus;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getHighlighter(): Promise<Highlighter> {
|
export async function getHighlighter(): Promise<Highlighter> {
|
||||||
if (!_highlighter) {
|
if (!_highlighter) {
|
||||||
return await initHighlighter();
|
return await initHighlighter();
|
||||||
|
@ -13,11 +55,17 @@ export async function getHighlighter(): Promise<Highlighter> {
|
||||||
|
|
||||||
export async function initHighlighter() {
|
export async function initHighlighter() {
|
||||||
const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json');
|
const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json');
|
||||||
|
|
||||||
await loadWasm(import('shiki/onig.wasm?init'));
|
await loadWasm(import('shiki/onig.wasm?init'));
|
||||||
|
|
||||||
|
// テーマの重複を消す
|
||||||
|
const themes = unique([
|
||||||
|
darkPlus,
|
||||||
|
...(await Promise.all([getTheme('light'), getTheme('dark')])),
|
||||||
|
]);
|
||||||
|
|
||||||
const highlighter = await getHighlighterCore({
|
const highlighter = await getHighlighterCore({
|
||||||
themes: [darkPlus],
|
themes,
|
||||||
langs: [
|
langs: [
|
||||||
import('shiki/langs/javascript.mjs'),
|
import('shiki/langs/javascript.mjs'),
|
||||||
{
|
{
|
||||||
|
@ -27,6 +75,20 @@ export async function initHighlighter() {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ColdDeviceStorage.watch('lightTheme', async () => {
|
||||||
|
const newTheme = await getTheme('light');
|
||||||
|
if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
|
||||||
|
highlighter.loadTheme(newTheme);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ColdDeviceStorage.watch('darkTheme', async () => {
|
||||||
|
const newTheme = await getTheme('dark');
|
||||||
|
if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
|
||||||
|
highlighter.loadTheme(newTheme);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
_highlighter = highlighter;
|
_highlighter = highlighter;
|
||||||
|
|
||||||
return highlighter;
|
return highlighter;
|
||||||
|
|
31
packages/frontend/src/scripts/merge.ts
Normal file
31
packages/frontend/src/scripts/merge.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { deepClone } from './clone.js';
|
||||||
|
import type { Cloneable } from './clone.js';
|
||||||
|
|
||||||
|
function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* valueにないキーをdefからもらう(再帰的)\
|
||||||
|
* nullはそのまま、undefinedはdefの値
|
||||||
|
**/
|
||||||
|
export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: X, def: X): X {
|
||||||
|
if (isPureObject(value) && isPureObject(def)) {
|
||||||
|
const result = deepClone(value as Cloneable) as X;
|
||||||
|
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
|
||||||
|
result[k] = v;
|
||||||
|
} else if (isPureObject(v) && isPureObject(result[k])) {
|
||||||
|
const child = deepClone(result[k] as Cloneable) as X[keyof X] & Record<string | number | symbol, unknown>;
|
||||||
|
result[k] = deepMerge<typeof v>(child, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import { defineAsyncComponent, Ref, ref } from 'vue';
|
import { defineAsyncComponent, Ref, ref } from 'vue';
|
||||||
import { popup } from '@/os.js';
|
import { popup } from '@/os.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
@ -10,6 +11,7 @@ import { defaultStore } from '@/store.js';
|
||||||
class ReactionPicker {
|
class ReactionPicker {
|
||||||
private src: Ref<HTMLElement | null> = ref(null);
|
private src: Ref<HTMLElement | null> = ref(null);
|
||||||
private manualShowing = ref(false);
|
private manualShowing = ref(false);
|
||||||
|
private targetNote: Ref<Misskey.entities.Note | null> = ref(null);
|
||||||
private onChosen?: (reaction: string) => void;
|
private onChosen?: (reaction: string) => void;
|
||||||
private onClosed?: () => void;
|
private onClosed?: () => void;
|
||||||
|
|
||||||
|
@ -23,6 +25,7 @@ class ReactionPicker {
|
||||||
src: this.src,
|
src: this.src,
|
||||||
pinnedEmojis: reactionsRef,
|
pinnedEmojis: reactionsRef,
|
||||||
asReactionPicker: true,
|
asReactionPicker: true,
|
||||||
|
targetNote: this.targetNote,
|
||||||
manualShowing: this.manualShowing,
|
manualShowing: this.manualShowing,
|
||||||
}, {
|
}, {
|
||||||
done: reaction => {
|
done: reaction => {
|
||||||
|
@ -38,8 +41,9 @@ class ReactionPicker {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public show(src: HTMLElement | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
|
public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
|
||||||
this.src.value = src;
|
this.src.value = src;
|
||||||
|
this.targetNote.value = targetNote;
|
||||||
this.manualShowing.value = true;
|
this.manualShowing.value = true;
|
||||||
this.onChosen = onChosen;
|
this.onChosen = onChosen;
|
||||||
this.onClosed = onClosed;
|
this.onClosed = onClosed;
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { deepClone } from './clone.js';
|
import { deepClone } from './clone.js';
|
||||||
|
import type { BuiltinTheme } from 'shiki';
|
||||||
import { globalEvents } from '@/events.js';
|
import { globalEvents } from '@/events.js';
|
||||||
import lightTheme from '@/themes/_light.json5';
|
import lightTheme from '@/themes/_light.json5';
|
||||||
import darkTheme from '@/themes/_dark.json5';
|
import darkTheme from '@/themes/_dark.json5';
|
||||||
|
@ -18,6 +19,13 @@ export type Theme = {
|
||||||
desc?: string;
|
desc?: string;
|
||||||
base?: 'dark' | 'light';
|
base?: 'dark' | 'light';
|
||||||
props: Record<string, string>;
|
props: Record<string, string>;
|
||||||
|
codeHighlighter?: {
|
||||||
|
base: BuiltinTheme;
|
||||||
|
overrides?: Record<string, any>;
|
||||||
|
} | {
|
||||||
|
base: '_none_';
|
||||||
|
overrides: Record<string, any>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
||||||
|
@ -57,7 +65,7 @@ export const getBuiltinThemesRef = () => {
|
||||||
|
|
||||||
const themeFontFaceName = 'sharkey-theme-font-face';
|
const themeFontFaceName = 'sharkey-theme-font-face';
|
||||||
|
|
||||||
let timeout = null;
|
let timeout: number | null = null;
|
||||||
|
|
||||||
export function applyTheme(theme: Theme, persist = true) {
|
export function applyTheme(theme: Theme, persist = true) {
|
||||||
if (timeout) window.clearTimeout(timeout);
|
if (timeout) window.clearTimeout(timeout);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { markRaw, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { miLocalStorage } from './local-storage.js';
|
import { miLocalStorage } from './local-storage.js';
|
||||||
import type { SoundType } from '@/scripts/sound.js';
|
import type { SoundType } from '@/scripts/sound.js';
|
||||||
|
import type { BuiltinTheme as ShikiBuiltinTheme } from 'shiki';
|
||||||
import { Storage } from '@/pizzax.js';
|
import { Storage } from '@/pizzax.js';
|
||||||
import { hemisphere } from '@/scripts/intl-const.js';
|
import { hemisphere } from '@/scripts/intl-const.js';
|
||||||
|
|
||||||
|
|
|
@ -95,4 +95,8 @@
|
||||||
X16: ':alpha<0.7<@panel',
|
X16: ':alpha<0.7<@panel',
|
||||||
X17: ':alpha<0.8<@bg',
|
X17: ':alpha<0.8<@bg',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
codeHighlighter: {
|
||||||
|
base: 'one-dark-pro',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,4 +95,8 @@
|
||||||
X16: ':alpha<0.7<@panel',
|
X16: ':alpha<0.7<@panel',
|
||||||
X17: ':alpha<0.8<@bg',
|
X17: ':alpha<0.8<@bg',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
codeHighlighter: {
|
||||||
|
base: 'catppuccin-latte',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,22 +4,6 @@ import { toPascal } from 'ts-case-convert';
|
||||||
import OpenAPIParser from '@readme/openapi-parser';
|
import OpenAPIParser from '@readme/openapi-parser';
|
||||||
import openapiTS from 'openapi-typescript';
|
import openapiTS from 'openapi-typescript';
|
||||||
|
|
||||||
function generateVersionHeaderComment(openApiDocs: OpenAPIV3_1.Document): string {
|
|
||||||
const contents = {
|
|
||||||
version: openApiDocs.info.version,
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const lines: string[] = [];
|
|
||||||
lines.push('/*');
|
|
||||||
for (const [key, value] of Object.entries(contents)) {
|
|
||||||
lines.push(` * ${key}: ${value}`);
|
|
||||||
}
|
|
||||||
lines.push(' */');
|
|
||||||
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateBaseTypes(
|
async function generateBaseTypes(
|
||||||
openApiDocs: OpenAPIV3_1.Document,
|
openApiDocs: OpenAPIV3_1.Document,
|
||||||
openApiJsonPath: string,
|
openApiJsonPath: string,
|
||||||
|
@ -36,9 +20,6 @@ async function generateBaseTypes(
|
||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
lines.push(generateVersionHeaderComment(openApiDocs));
|
|
||||||
lines.push('');
|
|
||||||
|
|
||||||
const generatedTypes = await openapiTS(openApiJsonPath, { exportType: true });
|
const generatedTypes = await openapiTS(openApiJsonPath, { exportType: true });
|
||||||
lines.push(generatedTypes);
|
lines.push(generatedTypes);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
@ -59,8 +40,6 @@ async function generateSchemaEntities(
|
||||||
const schemaNames = Object.keys(schemas);
|
const schemaNames = Object.keys(schemas);
|
||||||
const typeAliasLines: string[] = [];
|
const typeAliasLines: string[] = [];
|
||||||
|
|
||||||
typeAliasLines.push(generateVersionHeaderComment(openApiDocs));
|
|
||||||
typeAliasLines.push('');
|
|
||||||
typeAliasLines.push(`import { components } from '${toImportPath(typeFileName)}';`);
|
typeAliasLines.push(`import { components } from '${toImportPath(typeFileName)}';`);
|
||||||
typeAliasLines.push(
|
typeAliasLines.push(
|
||||||
...schemaNames.map(it => `export type ${it} = components['schemas']['${it}'];`),
|
...schemaNames.map(it => `export type ${it} = components['schemas']['${it}'];`),
|
||||||
|
@ -119,9 +98,6 @@ async function generateEndpoints(
|
||||||
|
|
||||||
const entitiesOutputLine: string[] = [];
|
const entitiesOutputLine: string[] = [];
|
||||||
|
|
||||||
entitiesOutputLine.push(generateVersionHeaderComment(openApiDocs));
|
|
||||||
entitiesOutputLine.push('');
|
|
||||||
|
|
||||||
entitiesOutputLine.push(`import { operations } from '${toImportPath(typeFileName)}';`);
|
entitiesOutputLine.push(`import { operations } from '${toImportPath(typeFileName)}';`);
|
||||||
entitiesOutputLine.push('');
|
entitiesOutputLine.push('');
|
||||||
|
|
||||||
|
@ -139,9 +115,6 @@ async function generateEndpoints(
|
||||||
|
|
||||||
const endpointOutputLine: string[] = [];
|
const endpointOutputLine: string[] = [];
|
||||||
|
|
||||||
endpointOutputLine.push(generateVersionHeaderComment(openApiDocs));
|
|
||||||
endpointOutputLine.push('');
|
|
||||||
|
|
||||||
endpointOutputLine.push('import type {');
|
endpointOutputLine.push('import type {');
|
||||||
endpointOutputLine.push(
|
endpointOutputLine.push(
|
||||||
...[emptyRequest, emptyResponse, ...entities].map(it => '\t' + it.generateName() + ','),
|
...[emptyRequest, emptyResponse, ...entities].map(it => '\t' + it.generateName() + ','),
|
||||||
|
@ -187,9 +160,6 @@ async function generateApiClientJSDoc(
|
||||||
|
|
||||||
const endpointOutputLine: string[] = [];
|
const endpointOutputLine: string[] = [];
|
||||||
|
|
||||||
endpointOutputLine.push(generateVersionHeaderComment(openApiDocs));
|
|
||||||
endpointOutputLine.push('');
|
|
||||||
|
|
||||||
endpointOutputLine.push(`import type { SwitchCaseResponseType } from '${toImportPath(apiClientFileName)}';`);
|
endpointOutputLine.push(`import type { SwitchCaseResponseType } from '${toImportPath(apiClientFileName)}';`);
|
||||||
endpointOutputLine.push(`import type { Endpoints } from '${toImportPath(endpointsFileName)}';`);
|
endpointOutputLine.push(`import type { Endpoints } from '${toImportPath(endpointsFileName)}';`);
|
||||||
endpointOutputLine.push('');
|
endpointOutputLine.push('');
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
/*
|
|
||||||
* version: 2024.2.0-beta2
|
|
||||||
* generatedAt: 2024-02-03T19:17:05.681Z
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { SwitchCaseResponseType } from '../api.js';
|
import type { SwitchCaseResponseType } from '../api.js';
|
||||||
import type { Endpoints } from './endpoint.js';
|
import type { Endpoints } from './endpoint.js';
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
/*
|
|
||||||
* version: 2024.2.0-beta2
|
|
||||||
* generatedAt: 2024-02-03T19:17:05.679Z
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
EmptyRequest,
|
EmptyRequest,
|
||||||
EmptyResponse,
|
EmptyResponse,
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
/*
|
|
||||||
* version: 2024.2.0-beta2
|
|
||||||
* generatedAt: 2024-02-03T19:17:05.678Z
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { operations } from './types.js';
|
import { operations } from './types.js';
|
||||||
|
|
||||||
export type EmptyRequest = Record<string, unknown> | undefined;
|
export type EmptyRequest = Record<string, unknown> | undefined;
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
/*
|
|
||||||
* version: 2024.2.0-beta2
|
|
||||||
* generatedAt: 2024-02-03T19:17:05.676Z
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { components } from './types.js';
|
import { components } from './types.js';
|
||||||
export type Error = components['schemas']['Error'];
|
export type Error = components['schemas']['Error'];
|
||||||
export type UserLite = components['schemas']['UserLite'];
|
export type UserLite = components['schemas']['UserLite'];
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
/* eslint @typescript-eslint/naming-convention: 0 */
|
/* eslint @typescript-eslint/naming-convention: 0 */
|
||||||
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
||||||
|
|
||||||
/*
|
|
||||||
* version: 2024.2.0-beta2
|
|
||||||
* generatedAt: 2024-02-03T19:17:05.578Z
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file was auto-generated by openapi-typescript.
|
* This file was auto-generated by openapi-typescript.
|
||||||
* Do not make direct changes to the file.
|
* Do not make direct changes to the file.
|
||||||
|
@ -4556,6 +4551,7 @@ export type components = {
|
||||||
name: string;
|
name: string;
|
||||||
category: string | null;
|
category: string | null;
|
||||||
url: string;
|
url: string;
|
||||||
|
localOnly?: boolean;
|
||||||
isSensitive?: boolean;
|
isSensitive?: boolean;
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction?: string[];
|
roleIdsThatCanBeUsedThisEmojiAsReaction?: string[];
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue