Merge branch 'develop'

This commit is contained in:
syuilo 2019-07-08 16:17:47 +09:00
commit 0f71d9f47a
88 changed files with 1023 additions and 165 deletions

View file

@ -17,6 +17,24 @@ npm i -g ts-node
npm run migrate npm run migrate
``` ```
11.25.0 (2019/07/08)
--------------------
### ✨Improvements
* パスワード無しログイン機能
* MisskeyPagesにイベント送信ボタンを追加
* MisskeyPagesでボタンを目立たせることができるように
* ページをピン留めできるように
* デッキのカラム内でページを見れるように
* 既定のアップロード先フォルダを設定できるように
* ファイルをペーストしてアップロードするときのファイル名をカスタマイズできるように
* 公開以外へのリプライ時元の公開範囲で指定したユーザー情報を引き継ぐように
* デスクトップ版を使うかモバイル版を使うか手動で設定できるように
* フォルダーを削除できないときダイアログボックスで知らせるように
### 🐛Fixes
* postgres redis cache の option が適用されない問題を修正
* 一部MFMテキストが折り返されず突き抜ける問題を修正
11.24.2 (2019/07/05) 11.24.2 (2019/07/05)
-------------------- --------------------
### 🐛Fixes ### 🐛Fixes
@ -36,7 +54,10 @@ npm run migrate
11.24.0 (2019/07/05) 11.24.0 (2019/07/05)
-------------------- --------------------
注意: このアップデート後に、`node built/tools/accept-migration Init 1000000000000`してください。 ### 注意
- このアップデート後に、`node built/tools/accept-migration Init 1000000000000`してください。
- プロセスを起動(もしくは再起動)する前に[マイグレーション](#migration)の手順を実行してください
### ✨Improvements ### ✨Improvements
* WebAuthnサポート * WebAuthnサポート

View file

@ -220,8 +220,7 @@ const user = await Users.findOne(userId).then(ensure);
### Migration作成方法 ### Migration作成方法
``` ```
npm i -g ts-node npx ts-node ./node_modules/typeorm/cli.js migration:generate -n 変更の名前
ts-node ./node_modules/typeorm/cli.js migration:generate -n 変更の名前
``` ```
作成されたスクリプトは不必要な変更を含むため除去してください。 作成されたスクリプトは不必要な変更を含むため除去してください。

View file

@ -101,7 +101,8 @@ common:
submit: "Odeslat" submit: "Odeslat"
reply: "Odpovědět" reply: "Odpovědět"
renote: "Renotovat" renote: "Renotovat"
attach-media-from-local: "Uplodovat soubor z vašeho zařízení" posting: "Posílání"
attach-media-from-local: "Nahrát soubor z vašeho zařízení"
insert-a-kao: "v('ω')v" insert-a-kao: "v('ω')v"
create-poll: "Vytvořit anketu" create-poll: "Vytvořit anketu"
text-remain: "zbývá ještě {} znaků" text-remain: "zbývá ještě {} znaků"
@ -537,6 +538,8 @@ common/views/components/emoji-picker.vue:
objects: "Objekty" objects: "Objekty"
symbols: "Symboly" symbols: "Symboly"
flags: "Vlajky" flags: "Vlajky"
common/views/components/settings/app-type.vue:
info: "Pro aktivování změn musíte znovu načíst stránky."
common/views/components/signin.vue: common/views/components/signin.vue:
username: "Přezdívka" username: "Přezdívka"
password: "Heslo" password: "Heslo"
@ -764,7 +767,7 @@ desktop/views/components/drive.file.vue:
rename: "Přejmenovat" rename: "Přejmenovat"
copy-url: "Kopírovat URL" copy-url: "Kopírovat URL"
download: "Stáhnout" download: "Stáhnout"
else-files: "Více..." else-files: "Ostatní"
set-as-avatar: "Nastavit jako avatar" set-as-avatar: "Nastavit jako avatar"
set-as-banner: "Nastavit jako baner" set-as-banner: "Nastavit jako baner"
open-in-app: "Otevřít v aplikaci" open-in-app: "Otevřít v aplikaci"
@ -782,6 +785,7 @@ desktop/views/components/drive.folder.vue:
rename: "Přejmenovat" rename: "Přejmenovat"
rename-folder: "Přejmenovat složku" rename-folder: "Přejmenovat složku"
input-new-folder-name: "Zadejte nové jméno" input-new-folder-name: "Zadejte nové jméno"
else-folders: "Ostatní"
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
empty-drive-description: "Klikněte pravým tlačítkem myši pro otevření menu, nebo sem přetáhněte soubor pro nahrání." empty-drive-description: "Klikněte pravým tlačítkem myši pro otevření menu, nebo sem přetáhněte soubor pro nahrání."
empty-folder: "Tato složka je prázdná" empty-folder: "Tato složka je prázdná"
@ -1265,6 +1269,8 @@ deck/deck.user-column.vue:
dev/views/new-app.vue: dev/views/new-app.vue:
app-name-desc: "Jméno vaší aplikace" app-name-desc: "Jméno vaší aplikace"
pages: pages:
pin-this-page: "Připnout"
unpin-this-page: "Odepnout"
like: "Lajk" like: "Lajk"
title: "Titulek" title: "Titulek"
blocks: blocks:

View file

@ -568,6 +568,8 @@ common/views/components/emoji-picker.vue:
objects: "Objekt" objects: "Objekt"
symbols: "Symboler" symbols: "Symboler"
flags: "Flag" flags: "Flag"
common/views/components/settings/app-type.vue:
info: "Du er nødt til at genindlæse siden, før ændringerne slår igennem."
common/views/components/signin.vue: common/views/components/signin.vue:
username: "Brugernavn" username: "Brugernavn"
password: "Adgangskode" password: "Adgangskode"
@ -847,7 +849,7 @@ desktop/views/components/drive.file.vue:
unmark-as-sensitive: "Fjern markering som 'følsom'" unmark-as-sensitive: "Fjern markering som 'følsom'"
copy-url: "Kopier webadresse" copy-url: "Kopier webadresse"
download: "Download" download: "Download"
else-files: "Andre" else-files: "Avanceret"
set-as-avatar: "Vælg som avatar" set-as-avatar: "Vælg som avatar"
set-as-banner: "Vælg som banner" set-as-banner: "Vælg som banner"
open-in-app: "Åbn i app" open-in-app: "Åbn i app"
@ -866,6 +868,7 @@ desktop/views/components/drive.folder.vue:
rename: "Omdøb" rename: "Omdøb"
rename-folder: "Omdøb mappe" rename-folder: "Omdøb mappe"
input-new-folder-name: "Angiv nyt navn" input-new-folder-name: "Angiv nyt navn"
else-folders: "Avanceret"
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
search: "Søg" search: "Søg"
empty-draghover: "Smid det her! Fordi du ved, at jeg er meget sød, ikke?" empty-draghover: "Smid det her! Fordi du ved, at jeg er meget sød, ikke?"
@ -995,6 +998,7 @@ common/views/components/drive-settings.vue:
max: "Kapacitet" max: "Kapacitet"
in-use: "I brug" in-use: "I brug"
stats: "Statistik" stats: "Statistik"
default-upload-folder-name: "Mappe(r)"
common/views/components/mute-and-block.vue: common/views/components/mute-and-block.vue:
mute-and-block: "Annuller / Bloker" mute-and-block: "Annuller / Bloker"
mute: "Annuller" mute: "Annuller"
@ -1664,6 +1668,8 @@ dev/views/new-app.vue:
app-name: "Navn på app" app-name: "Navn på app"
app-name-placeholder: "F.eks. Misskey for iOS" app-name-placeholder: "F.eks. Misskey for iOS"
pages: pages:
pin-this-page: "Tilknyt til din profil"
unpin-this-page: "Fjern tilknytning til din profil"
like: "Synes om" like: "Synes om"
title: "Titel" title: "Titel"
blocks: blocks:

View file

@ -632,7 +632,6 @@ desktop/views/components/drive.file.vue:
rename: "Umbenennen" rename: "Umbenennen"
copy-url: "URL kopieren" copy-url: "URL kopieren"
download: "Download" download: "Download"
else-files: "Anderes…"
set-as-avatar: "Als Avatar festlegen" set-as-avatar: "Als Avatar festlegen"
set-as-banner: "Setze als Banner" set-as-banner: "Setze als Banner"
open-in-app: "In der App öffnen" open-in-app: "In der App öffnen"
@ -911,6 +910,8 @@ dev/views/new-app.vue:
authority-desc: "Nur die hier eingetragenen Berechtigungen, werden per API zur Verfügung stehen." authority-desc: "Nur die hier eingetragenen Berechtigungen, werden per API zur Verfügung stehen."
authority-warning: "Dies kann auch nach dem erstellen der Anwendung geändert werden, allerdings werden dann alle bisher generierten Token ungültig." authority-warning: "Dies kann auch nach dem erstellen der Anwendung geändert werden, allerdings werden dann alle bisher generierten Token ungültig."
pages: pages:
pin-this-page: "An die Profilseite pinnen"
unpin-this-page: "Lösen"
like: "Gefällt mir" like: "Gefällt mir"
blocks: blocks:
post: "\"Neuer Beitrag\"-Formular" post: "\"Neuer Beitrag\"-Formular"

View file

@ -35,6 +35,7 @@ common:
signout: "Logout" signout: "Logout"
reload-to-apply-the-setting: "You'll need to reload the page to reflect this setting. Do you want to reload it now?" reload-to-apply-the-setting: "You'll need to reload the page to reflect this setting. Do you want to reload it now?"
fetching-as-ap-object: "Inquiring to union" fetching-as-ap-object: "Inquiring to union"
unfollow-confirm: "Do you want to unfollow {name}?"
got-it: "Got it!" got-it: "Got it!"
customization-tips: customization-tips:
title: "Customization tips" title: "Customization tips"
@ -122,6 +123,7 @@ common:
add-visible-user: "Add a user" add-visible-user: "Add a user"
cw-placeholder: "Comments for the post (optional)" cw-placeholder: "Comments for the post (optional)"
username-prompt: "Please enter username" username-prompt: "Please enter username"
enter-file-name: "Edit file name"
weekday-short: weekday-short:
sunday: "S" sunday: "S"
monday: "M" monday: "M"
@ -188,6 +190,8 @@ common:
remember-note-visibility: "Remember post visibility" remember-note-visibility: "Remember post visibility"
web-search-engine: "Web search engine" web-search-engine: "Web search engine"
web-search-engine-desc: "Example: https://www.google.com/?#q={{query}}" web-search-engine-desc: "Example: https://www.google.com/?#q={{query}}"
paste: "Paste"
paste-dialog: "Edit the pasted file name"
keep-cw: "Preserve content warning" keep-cw: "Preserve content warning"
keep-cw-desc: "When replying to a post, the same content warning is set by default to the reply, as has been set by the original post." keep-cw-desc: "When replying to a post, the same content warning is set by default to the reply, as has been set by the original post."
i-like-sushi: "I prefer sushi rather than pudding" i-like-sushi: "I prefer sushi rather than pudding"
@ -592,6 +596,14 @@ common/views/components/emoji-picker.vue:
objects: "Objects" objects: "Objects"
symbols: "Symbols" symbols: "Symbols"
flags: "Flags" flags: "Flags"
common/views/components/settings/app-type.vue:
title: "Mode"
intro: "You can specify whether you want to use the desktop, or the mobile layout."
choices:
auto: "Choose layout automatically"
desktop: "Always use the desktop layout"
mobile: "Always use the mobile layout"
info: "You need to reload the page for the changes to take effect."
common/views/components/signin.vue: common/views/components/signin.vue:
username: "Username" username: "Username"
password: "Password" password: "Password"
@ -876,7 +888,7 @@ desktop/views/components/drive.file.vue:
unmark-as-sensitive: "Unmark as 'sensitive'" unmark-as-sensitive: "Unmark as 'sensitive'"
copy-url: "Copy URL" copy-url: "Copy URL"
download: "Download" download: "Download"
else-files: "Others" else-files: "Other"
set-as-avatar: "Set as an avatar" set-as-avatar: "Set as an avatar"
set-as-banner: "Set as a banner" set-as-banner: "Set as a banner"
open-in-app: "Open in app" open-in-app: "Open in app"
@ -886,15 +898,20 @@ desktop/views/components/drive.file.vue:
copied: "Copied" copied: "Copied"
copied-url-to-clipboard: "URL has been copied to clipboard" copied-url-to-clipboard: "URL has been copied to clipboard"
desktop/views/components/drive.folder.vue: desktop/views/components/drive.folder.vue:
upload-folder: "Default Upload location"
unable-to-process: "The operation could not be completed." unable-to-process: "The operation could not be completed."
circular-reference-detected: "The destination folder is a subfolder of the folder you wish to move." circular-reference-detected: "The destination folder is a subfolder of the folder you wish to move."
unhandled-error: "Unknown error" unhandled-error: "Unknown error"
unable-to-delete: "Unable to delete"
has-child-files-or-folders: "Since this folder is not empty, it can not be deleted."
contextmenu: contextmenu:
move-to-this-folder: "Move to this folder" move-to-this-folder: "Move to this folder"
show-in-new-window: "Open in new window" show-in-new-window: "Open in new window"
rename: "Rename" rename: "Rename"
rename-folder: "Rename folder" rename-folder: "Rename folder"
input-new-folder-name: "Enter new name" input-new-folder-name: "Enter new name"
else-folders: "Other"
set-as-upload-folder: "Set as default upload folder"
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
search: "Search" search: "Search"
empty-draghover: "Drop it here! Yep, cuz you know I'm cute, right?" empty-draghover: "Drop it here! Yep, cuz you know I'm cute, right?"
@ -987,7 +1004,7 @@ desktop/views/components/settings.2fa.vue:
url: "https://www.google.com/landing/2step/" url: "https://www.google.com/landing/2step/"
caution: "If you lose access to your registered device, you won't be able to connect to Misskey anymore!" caution: "If you lose access to your registered device, you won't be able to connect to Misskey anymore!"
register: "Register a device" register: "Register a device"
already-registered: "Your account is currently registered to an authenticator application" already-registered: "This device is already registered"
unregister: "Unregister" unregister: "Unregister"
unregistered: "Two-factor authentication has been disabled." unregistered: "Two-factor authentication has been disabled."
enter-password: "Enter the password" enter-password: "Enter the password"
@ -1009,6 +1026,7 @@ desktop/views/components/settings.2fa.vue:
register-security-key: "Complete Key registration" register-security-key: "Complete Key registration"
something-went-wrong: "Wow! There was a problem registering the Key:" something-went-wrong: "Wow! There was a problem registering the Key:"
key-unregistered: "The Key has been deleted" key-unregistered: "The Key has been deleted"
use-password-less-login: "Use Password-less login"
common/views/components/media-image.vue: common/views/components/media-image.vue:
sensitive: "NSFW" sensitive: "NSFW"
click-to-show: "Click to show" click-to-show: "Click to show"
@ -1033,6 +1051,9 @@ common/views/components/drive-settings.vue:
max: "Max" max: "Max"
in-use: "In use" in-use: "In use"
stats: "Statistics" stats: "Statistics"
default-upload-folder: "Default upload folder location"
default-upload-folder-name: "Folder(s)"
change-default-upload-folder: "Change folder"
common/views/components/mute-and-block.vue: common/views/components/mute-and-block.vue:
mute-and-block: "Mute / Block" mute-and-block: "Mute / Block"
mute: "Mute" mute: "Mute"
@ -1715,6 +1736,7 @@ deck/deck.user-column.vue:
activity: "Activity" activity: "Activity"
timeline: "Timeline" timeline: "Timeline"
pinned-notes: "Pinned posts" pinned-notes: "Pinned posts"
pinned-page: "Pinned page"
docs: docs:
edit-this-page-on-github: "Found an error, or do you want to contribute to the documentation?" edit-this-page-on-github: "Found an error, or do you want to contribute to the documentation?"
edit-this-page-on-github-link: "Edit this page at GitHub!" edit-this-page-on-github-link: "Edit this page at GitHub!"
@ -1749,6 +1771,8 @@ pages:
are-you-sure-delete: "Do you want to delete this page?" are-you-sure-delete: "Do you want to delete this page?"
page-deleted: "The page has been deleted" page-deleted: "The page has been deleted"
edit-this-page: "Edit this page" edit-this-page: "Edit this page"
pin-this-page: "Pin to your profile"
unpin-this-page: "Unpin"
view-source: "View Source" view-source: "View Source"
view-page: "View page" view-page: "View page"
like: "Like" like: "Like"
@ -1767,6 +1791,7 @@ pages:
url: "Page URL" url: "Page URL"
summary: "Summary of page" summary: "Summary of page"
align-center: "Center align" align-center: "Center align"
hide-title-when-pinned: "Hide page title when pinned to profile"
font: "Font" font: "Font"
fontSerif: "Serif" fontSerif: "Serif"
fontSansSerif: "Sans Serif" fontSansSerif: "Sans Serif"
@ -1820,12 +1845,19 @@ pages:
inc: "Increase number" inc: "Increase number"
_button: _button:
text: "Title" text: "Title"
colored: "Color"
action: "Operation when the button pressed" action: "Operation when the button pressed"
_action: _action:
dialog: "Show a dialog" dialog: "Show a dialog"
_dialog: _dialog:
content: "Content" content: "Content"
resetRandom: "Reset a random number" resetRandom: "Reset a random number"
pushEvent: "Send an event"
_pushEvent:
event: "Name of the event"
message: "Message to display when pressed"
variable: "Variable to send"
no-variable: "None"
script: script:
categories: categories:
flow: "Control" flow: "Control"

View file

@ -458,6 +458,8 @@ common/views/components/emoji-picker.vue:
objects: "Objetos" objects: "Objetos"
symbols: "Símbolos" symbols: "Símbolos"
flags: "Países" flags: "Países"
common/views/components/settings/app-type.vue:
info: "Necesitas recargar la página para que los cambios tengan efecto."
common/views/components/signin.vue: common/views/components/signin.vue:
username: "Usuario" username: "Usuario"
password: "Contraseña" password: "Contraseña"
@ -725,6 +727,7 @@ desktop/views/components/drive.folder.vue:
rename: "Renombrar" rename: "Renombrar"
rename-folder: "Renombrar carpeta" rename-folder: "Renombrar carpeta"
input-new-folder-name: "Escribe el nombre nuevo" input-new-folder-name: "Escribe el nombre nuevo"
else-folders: "Otros"
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
search: "Buscar" search: "Buscar"
empty-draghover: "¡Saluda!" empty-draghover: "¡Saluda!"
@ -1091,6 +1094,7 @@ deck:
deck/deck.user-column.vue: deck/deck.user-column.vue:
activity: "Actividad" activity: "Actividad"
pages: pages:
pin-this-page: "Fijar en el perfil"
like: "Me gusta" like: "Me gusta"
blocks: blocks:
post: "Formulario" post: "Formulario"

View file

@ -34,6 +34,7 @@ common:
signup: "S'enregistrer" signup: "S'enregistrer"
signout: "Se déconnecter" signout: "Se déconnecter"
reload-to-apply-the-setting: "Le rechargement de la page est nécessaire pour appliquer ces paramètres. Désirez-vous la recharger maintenant ?" reload-to-apply-the-setting: "Le rechargement de la page est nécessaire pour appliquer ces paramètres. Désirez-vous la recharger maintenant ?"
unfollow-confirm: "Désirez-vous vous désabonner de {name} ?"
got-it: "Jai compris !" got-it: "Jai compris !"
customization-tips: customization-tips:
title: "Conseils de personnalisation" title: "Conseils de personnalisation"
@ -532,8 +533,10 @@ common/views/components/user-menu.vue:
report-abuse-reported: "Transmit à ladministrateur. Merci de votre collaboration." report-abuse-reported: "Transmit à ladministrateur. Merci de votre collaboration."
silence: "Mettre en sourdine" silence: "Mettre en sourdine"
unsilence: "Enlever la sourdine" unsilence: "Enlever la sourdine"
silence-confirm: "Êtes-vous surs de vouloir mettre cet·te utilisateur·rice en sourdine ?"
suspend: "Suspendre" suspend: "Suspendre"
unsuspend: "Ne plus suspendre" unsuspend: "Ne plus suspendre"
suspend-confirm: "Êtes-vous surs de vouloir suspendre cet·te utilisateur·rice ?"
common/views/components/poll.vue: common/views/components/poll.vue:
vote-to: "Voter pour '{}'" vote-to: "Voter pour '{}'"
vote-count: "{} votes" vote-count: "{} votes"
@ -577,6 +580,8 @@ common/views/components/emoji-picker.vue:
objects: "Objets" objects: "Objets"
symbols: "Symboles" symbols: "Symboles"
flags: "Drapeaux" flags: "Drapeaux"
common/views/components/settings/app-type.vue:
info: "Le rechargement de la page est requis afin d'appliquer les modifications."
common/views/components/signin.vue: common/views/components/signin.vue:
username: "Nom d'utilisateur·rice" username: "Nom d'utilisateur·rice"
password: "Mot de passe" password: "Mot de passe"
@ -587,6 +592,8 @@ common/views/components/signin.vue:
signin-with-github: "Se connecter avec GitHub" signin-with-github: "Se connecter avec GitHub"
signin-with-discord: "Se connecter avec Discord" signin-with-discord: "Se connecter avec Discord"
login-failed: "Échec dauthentification. Veuillez vérifier que votre nom dutilisateur et mot de passe sont corrects." login-failed: "Échec dauthentification. Veuillez vérifier que votre nom dutilisateur et mot de passe sont corrects."
tap-key: "Cliquez sur la clé de sécurité pour vous connecter"
enter-2fa-code: "Entrez votre code de vérification"
common/views/components/signup.vue: common/views/components/signup.vue:
invitation-code: "Code dinvitation" invitation-code: "Code dinvitation"
invitation-info: "Si vous navez pas de code dinvitation, contactez un <a href=\"{}\">administrateur</a>." invitation-info: "Si vous navez pas de code dinvitation, contactez un <a href=\"{}\">administrateur</a>."
@ -723,6 +730,7 @@ common/views/components/user-group-editor.vue:
rename: "Renommer le groupe" rename: "Renommer le groupe"
delete: "Supprimer le groupe" delete: "Supprimer le groupe"
transfer: "Transférer de groupe" transfer: "Transférer de groupe"
transfer-are-you-sure: "Êtes vous surs de vouloir ajouter @$2 au groupe $1 ?"
transferred: "Groupe transféré" transferred: "Groupe transféré"
remove-user: "Enlever un utilisateur de ce groupe" remove-user: "Enlever un utilisateur de ce groupe"
delete-are-you-sure: "Désirez-vous vraiment supprimer le groupe $1 ?" delete-are-you-sure: "Désirez-vous vraiment supprimer le groupe $1 ?"
@ -857,7 +865,7 @@ desktop/views/components/drive.file.vue:
unmark-as-sensitive: "Ne pas marquer comme sensible" unmark-as-sensitive: "Ne pas marquer comme sensible"
copy-url: "Copier lURL" copy-url: "Copier lURL"
download: "Télécharger" download: "Télécharger"
else-files: "Autres..." else-files: "Avancé"
set-as-avatar: "Utiliser en tant qu'avatar" set-as-avatar: "Utiliser en tant qu'avatar"
set-as-banner: "Utiliser en tant que bannière" set-as-banner: "Utiliser en tant que bannière"
open-in-app: "Ouvrir dans l'application" open-in-app: "Ouvrir dans l'application"
@ -876,6 +884,7 @@ desktop/views/components/drive.folder.vue:
rename: "Renommer" rename: "Renommer"
rename-folder: "Renommer le dossier" rename-folder: "Renommer le dossier"
input-new-folder-name: "Entrer un nouveau nom" input-new-folder-name: "Entrer un nouveau nom"
else-folders: "Avancé"
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
search: "Rechercher" search: "Rechercher"
empty-draghover: "Drop Welcome!" empty-draghover: "Drop Welcome!"
@ -981,6 +990,12 @@ desktop/views/components/settings.2fa.vue:
success: "Sauvegarde des paramètres avec succès !" success: "Sauvegarde des paramètres avec succès !"
failed: "Lopération a échoué. Veuillez vous assurer que le jeton a été saisi correctement." failed: "Lopération a échoué. Veuillez vous assurer que le jeton a été saisi correctement."
info: "À partir de maintenant, à chaque fois que vous vous connectez entrez votre mot de passe ainsi que le jeton généré sur votre appareil." info: "À partir de maintenant, à chaque fois que vous vous connectez entrez votre mot de passe ainsi que le jeton généré sur votre appareil."
security-key-header: "Clé de sécurité"
last-used: "Dernière utilisation :"
activate-key: "Cliquez pour activer la clé de sécurité"
security-key-name: "Nom de la clé"
key-unregistered: "La clé a été supprimée"
use-password-less-login: "Utiliser une connexion sans mot de passe"
common/views/components/media-image.vue: common/views/components/media-image.vue:
sensitive: "Contenu sensible" sensitive: "Contenu sensible"
click-to-show: "Cliquer pour afficher" click-to-show: "Cliquer pour afficher"
@ -1005,6 +1020,7 @@ common/views/components/drive-settings.vue:
max: "Maximale" max: "Maximale"
in-use: "utilisé" in-use: "utilisé"
stats: "Statistiques" stats: "Statistiques"
default-upload-folder-name: "Dossier·s"
common/views/components/mute-and-block.vue: common/views/components/mute-and-block.vue:
mute-and-block: "Silencés / Bloqués" mute-and-block: "Silencés / Bloqués"
mute: "Mettre en sourdine" mute: "Mettre en sourdine"
@ -1294,6 +1310,7 @@ admin/views/users.vue:
update-remote-user: "Mettre à jour les informations de lutilisateur·rice distant·e" update-remote-user: "Mettre à jour les informations de lutilisateur·rice distant·e"
remote-user-updated: "Les informations de lutilisateur·rice distant·e ont étés mis à jour" remote-user-updated: "Les informations de lutilisateur·rice distant·e ont étés mis à jour"
delete-all-files: "Supprimer tous les fichiers" delete-all-files: "Supprimer tous les fichiers"
delete-all-files-confirm: "Êtes vous surs de vouloir supprimer tous les fichiers ?"
users: users:
title: "Utilisateur·rice·s" title: "Utilisateur·rice·s"
sort: sort:
@ -1692,6 +1709,8 @@ pages:
are-you-sure-delete: "Confirmez-vous la suppression de cette page ?" are-you-sure-delete: "Confirmez-vous la suppression de cette page ?"
page-deleted: "La page a bien été supprimée." page-deleted: "La page a bien été supprimée."
edit-this-page: "Éditer cette page" edit-this-page: "Éditer cette page"
pin-this-page: "Épingler sur votre profil"
unpin-this-page: "Désépingler"
view-source: "Afficher la source" view-source: "Afficher la source"
view-page: "Afficher la page" view-page: "Afficher la page"
like: "Bien" like: "Bien"
@ -1705,6 +1724,7 @@ pages:
url: "URL de page" url: "URL de page"
summary: "Résumé de page" summary: "Résumé de page"
align-center: "Centrée" align-center: "Centrée"
hide-title-when-pinned: "Masquer le titre de la page lorsque celle-ci est épinglée au profil"
font: "Police de caractères" font: "Police de caractères"
fontSerif: "Serif" fontSerif: "Serif"
fontSansSerif: "Sans Serif" fontSansSerif: "Sans Serif"
@ -1757,12 +1777,19 @@ pages:
inc: "Augmenter le chiffre" inc: "Augmenter le chiffre"
_button: _button:
text: "Titre" text: "Titre"
colored: "Couleur"
action: "L'opération lorsque le bouton sera pressé" action: "L'opération lorsque le bouton sera pressé"
_action: _action:
dialog: "Afficher une fenêtre de dialogue" dialog: "Afficher une fenêtre de dialogue"
_dialog: _dialog:
content: "Contenu" content: "Contenu"
resetRandom: "Réinitialiser le nombre aléatoire" resetRandom: "Réinitialiser le nombre aléatoire"
pushEvent: "Envoyer un évènement"
_pushEvent:
event: "Nom de l'évènement"
message: "Message à afficher lorsque appuyé"
variable: "Variable à envoyer"
no-variable: "Aucune"
script: script:
categories: categories:
flow: "Contrôle" flow: "Contrôle"

View file

@ -129,6 +129,7 @@ common:
add-visible-user: "ユーザーを追加" add-visible-user: "ユーザーを追加"
cw-placeholder: "内容への注釈 (オプション)" cw-placeholder: "内容への注釈 (オプション)"
username-prompt: "ユーザー名を入力してください" username-prompt: "ユーザー名を入力してください"
enter-file-name: "ファイル名を編集"
weekday-short: weekday-short:
sunday: "日" sunday: "日"
@ -201,6 +202,11 @@ common:
remember-note-visibility: "投稿の公開範囲を記憶する" remember-note-visibility: "投稿の公開範囲を記憶する"
web-search-engine: "ウェブ検索エンジン" web-search-engine: "ウェブ検索エンジン"
web-search-engine-desc: "例: https://www.google.com/?#q={{query}}" web-search-engine-desc: "例: https://www.google.com/?#q={{query}}"
paste: "ペースト"
pasted-file-name: "ペーストされたファイル名のテンプレート"
pasted-file-name-desc: "例: \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\""
paste-dialog: "ペースト時にファイル名を編集"
paste-dialog-desc: "ペースト時にファイル名を編集するダイアログを表示するようにします。"
keep-cw: "CW保持" keep-cw: "CW保持"
keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。" keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。"
i-like-sushi: "私は(プリンよりむしろ)寿司が好き" i-like-sushi: "私は(プリンよりむしろ)寿司が好き"
@ -637,6 +643,15 @@ common/views/components/emoji-picker.vue:
symbols: "記号" symbols: "記号"
flags: "旗" flags: "旗"
common/views/components/settings/app-type.vue:
title: "モード"
intro: "デスクトップ版とモバイル版のどちらを使うかを指定できます。"
choices:
auto: "自動で選択"
desktop: "デスクトップ版に固定"
mobile: "モバイル版に固定"
info: "変更はページの再度読み込み後に反映されます。"
common/views/components/signin.vue: common/views/components/signin.vue:
username: "ユーザー名" username: "ユーザー名"
password: "パスワード" password: "パスワード"
@ -958,7 +973,7 @@ desktop/views/components/drive.file.vue:
unmark-as-sensitive: "閲覧注意を解除" unmark-as-sensitive: "閲覧注意を解除"
copy-url: "URLをコピー" copy-url: "URLをコピー"
download: "ダウンロード" download: "ダウンロード"
else-files: "その他..." else-files: "その他"
set-as-avatar: "アイコンに設定" set-as-avatar: "アイコンに設定"
set-as-banner: "バナーに設定" set-as-banner: "バナーに設定"
open-in-app: "アプリで開く" open-in-app: "アプリで開く"
@ -969,15 +984,20 @@ desktop/views/components/drive.file.vue:
copied-url-to-clipboard: "URLをクリップボードにコピーしました" copied-url-to-clipboard: "URLをクリップボードにコピーしました"
desktop/views/components/drive.folder.vue: desktop/views/components/drive.folder.vue:
upload-folder: "既定アップロード先"
unable-to-process: "操作を完了できません" unable-to-process: "操作を完了できません"
circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダーです。" circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダーです。"
unhandled-error: "不明なエラー" unhandled-error: "不明なエラー"
unable-to-delete: "削除できません"
has-child-files-or-folders: "このフォルダは空でないため、削除できません。"
contextmenu: contextmenu:
move-to-this-folder: "このフォルダへ移動" move-to-this-folder: "このフォルダへ移動"
show-in-new-window: "新しいウィンドウで表示" show-in-new-window: "新しいウィンドウで表示"
rename: "名前を変更" rename: "名前を変更"
rename-folder: "フォルダ名の変更" rename-folder: "フォルダ名の変更"
input-new-folder-name: "新しいフォルダ名を入力してください" input-new-folder-name: "新しいフォルダ名を入力してください"
else-folders: "その他"
set-as-upload-folder: "既定アップロード先に設定"
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
search: "検索" search: "検索"
@ -1112,6 +1132,7 @@ desktop/views/components/settings.2fa.vue:
register-security-key: "キーの登録を完了" register-security-key: "キーの登録を完了"
something-went-wrong: "わー! キーを登録する際に問題が発生しました:" something-went-wrong: "わー! キーを登録する際に問題が発生しました:"
key-unregistered: "キーが削除されました" key-unregistered: "キーが削除されました"
use-password-less-login: "パスワードなしのログインを使用"
common/views/components/media-image.vue: common/views/components/media-image.vue:
sensitive: "閲覧注意" sensitive: "閲覧注意"
@ -1140,6 +1161,9 @@ common/views/components/drive-settings.vue:
max: "容量" max: "容量"
in-use: "使用中" in-use: "使用中"
stats: "統計" stats: "統計"
default-upload-folder: "既定のアップロード先フォルダ"
default-upload-folder-name: "フォルダ"
change-default-upload-folder: "フォルダを変更"
common/views/components/mute-and-block.vue: common/views/components/mute-and-block.vue:
mute-and-block: "ミュートとブロック" mute-and-block: "ミュートとブロック"
@ -1906,6 +1930,7 @@ deck/deck.user-column.vue:
activity: "アクティビティ" activity: "アクティビティ"
timeline: "タイムライン" timeline: "タイムライン"
pinned-notes: "ピン留めされた投稿" pinned-notes: "ピン留めされた投稿"
pinned-page: "ピン留めされたページ"
docs: docs:
edit-this-page-on-github: "間違いや改善点を見つけましたか?" edit-this-page-on-github: "間違いや改善点を見つけましたか?"
@ -1945,6 +1970,8 @@ pages:
are-you-sure-delete: "このページを削除しますか?" are-you-sure-delete: "このページを削除しますか?"
page-deleted: "ページを削除しました" page-deleted: "ページを削除しました"
edit-this-page: "このページを編集" edit-this-page: "このページを編集"
pin-this-page: "ピン留め"
unpin-this-page: "ピン留め解除"
view-source: "ソースを表示" view-source: "ソースを表示"
view-page: "ページを見る" view-page: "ページを見る"
like: "いいね" like: "いいね"
@ -1963,6 +1990,7 @@ pages:
url: "ページURL" url: "ページURL"
summary: "ページの要約" summary: "ページの要約"
align-center: "中央寄せ" align-center: "中央寄せ"
hide-title-when-pinned: "ピン留めされているときにタイトルを非表示"
font: "フォント" font: "フォント"
fontSerif: "セリフ" fontSerif: "セリフ"
fontSansSerif: "サンセリフ" fontSansSerif: "サンセリフ"
@ -2024,12 +2052,19 @@ pages:
_button: _button:
text: "タイトル" text: "タイトル"
colored: "色付き"
action: "ボタンを押したときの動作" action: "ボタンを押したときの動作"
_action: _action:
dialog: "ダイアログを表示する" dialog: "ダイアログを表示する"
_dialog: _dialog:
content: "内容" content: "内容"
resetRandom: "乱数をリセット" resetRandom: "乱数をリセット"
pushEvent: "イベントを送信させる"
_pushEvent:
event: "イベント名"
message: "押したときに表示するメッセージ"
variable: "送信する変数"
no-variable: "なし"
script: script:
categories: categories:

View file

@ -363,6 +363,8 @@ common/views/components/emoji-picker.vue:
objects: "物" objects: "物"
symbols: "記号" symbols: "記号"
flags: "旗" flags: "旗"
common/views/components/settings/app-type.vue:
info: "ページもっぺん読み込んだら反映したるで。"
common/views/components/signin.vue: common/views/components/signin.vue:
username: "ユーザー名" username: "ユーザー名"
password: "パスワード" password: "パスワード"
@ -601,7 +603,7 @@ desktop/views/components/drive.file.vue:
unmark-as-sensitive: "やっぱ見せたるわ" unmark-as-sensitive: "やっぱ見せたるわ"
copy-url: "URLをコピー" copy-url: "URLをコピー"
download: "ダウンロード" download: "ダウンロード"
else-files: "まだあんで..." else-files: "その他"
set-as-avatar: "アイコンにする" set-as-avatar: "アイコンにする"
set-as-banner: "バナーにする" set-as-banner: "バナーにする"
open-in-app: "アプリで開く" open-in-app: "アプリで開く"
@ -620,6 +622,7 @@ desktop/views/components/drive.folder.vue:
rename: "名前を変えるで" rename: "名前を変えるで"
rename-folder: "フォルダ名を変えるで" rename-folder: "フォルダ名を変えるで"
input-new-folder-name: "新しいフォルダ名を入力してや" input-new-folder-name: "新しいフォルダ名を入力してや"
else-folders: "その他"
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
search: "検索" search: "検索"
empty-draghover: "ドロップするにゃ!お魚以外なら何でもいいにゃ!" empty-draghover: "ドロップするにゃ!お魚以外なら何でもいいにゃ!"
@ -747,6 +750,7 @@ common/views/components/drive-settings.vue:
max: "容量" max: "容量"
in-use: "使うとる" in-use: "使うとる"
stats: "統計" stats: "統計"
default-upload-folder-name: "フォルダ"
common/views/components/mute-and-block.vue: common/views/components/mute-and-block.vue:
mute-and-block: "ミュートとブロック" mute-and-block: "ミュートとブロック"
mute: "ミュート" mute: "ミュート"
@ -1234,6 +1238,8 @@ dev/views/new-app.vue:
authority-desc: "ここにチェックした機能しかAPIからアクセスできひんから気ぃつけてな" authority-desc: "ここにチェックした機能しかAPIからアクセスできひんから気ぃつけてな"
authority-warning: "アプリ作った後でも変えれるけど、新しいやつ追加したらそん時関連付いてるユーザーキーは全部ほかされるで。" authority-warning: "アプリ作った後でも変えれるけど、新しいやつ追加したらそん時関連付いてるユーザーキーは全部ほかされるで。"
pages: pages:
pin-this-page: "ピン留め"
unpin-this-page: "ピン留めやめる"
like: "ええやん" like: "ええやん"
blocks: blocks:
image: "画像" image: "画像"

View file

@ -290,7 +290,7 @@ common:
is-remote-user: "이 사용자 정보는 정확하지 않을 수 있습니다." is-remote-user: "이 사용자 정보는 정확하지 않을 수 있습니다."
is-remote-post: "이 글 정보는 복사본입니다." is-remote-post: "이 글 정보는 복사본입니다."
view-on-remote: "정확한 정보 보기" view-on-remote: "정확한 정보 보기"
renoted-by: "{user}님이 리노트" renoted-by: "{user} 님이 리노트"
no-notes: "글이 없습니다" no-notes: "글이 없습니다"
turn-on-darkmode: "어둠에 삼켜져라" turn-on-darkmode: "어둠에 삼켜져라"
turn-off-darkmode: "빛이 있으라" turn-off-darkmode: "빛이 있으라"
@ -593,6 +593,14 @@ common/views/components/emoji-picker.vue:
objects: "사물" objects: "사물"
symbols: "기호" symbols: "기호"
flags: "깃발" flags: "깃발"
common/views/components/settings/app-type.vue:
title: "모드"
intro: "데스크톱과 모바일 중 어떤 레이아웃을 사용할 지 지정할 수 있습니다."
choices:
auto: "자동으로 선택"
desktop: "데스크톱 레이아웃으로 고정"
mobile: "모바일 레이아웃으로 고정"
info: "변경사항은 페이지를 새로고침한 뒤에 반영됩니다."
common/views/components/signin.vue: common/views/components/signin.vue:
username: "사용자명" username: "사용자명"
password: "비밀번호" password: "비밀번호"
@ -877,7 +885,7 @@ desktop/views/components/drive.file.vue:
unmark-as-sensitive: "열람주의 해제" unmark-as-sensitive: "열람주의 해제"
copy-url: "URL 복사" copy-url: "URL 복사"
download: "다운로드" download: "다운로드"
else-files: "기타..." else-files: "기타"
set-as-avatar: "아이콘으로 설정" set-as-avatar: "아이콘으로 설정"
set-as-banner: "배너로 설정" set-as-banner: "배너로 설정"
open-in-app: "앱에서 열기" open-in-app: "앱에서 열기"
@ -887,15 +895,20 @@ desktop/views/components/drive.file.vue:
copied: "복사 완료" copied: "복사 완료"
copied-url-to-clipboard: "URL을 클립보드에 복사하였습니다" copied-url-to-clipboard: "URL을 클립보드에 복사하였습니다"
desktop/views/components/drive.folder.vue: desktop/views/components/drive.folder.vue:
upload-folder: "기본 업로드 위치"
unable-to-process: "작업을 완료할 수 없습니다" unable-to-process: "작업을 완료할 수 없습니다"
circular-reference-detected: "대상 폴더가 이동할 폴더의 하위 폴더입니다." circular-reference-detected: "대상 폴더가 이동할 폴더의 하위 폴더입니다."
unhandled-error: "알 수 없는 오류" unhandled-error: "알 수 없는 오류"
unable-to-delete: "삭제할 수 없습니다"
has-child-files-or-folders: "이 폴더는 비어있지 않기 때문에 삭제할 수 없습니다."
contextmenu: contextmenu:
move-to-this-folder: "이 폴더로 이동" move-to-this-folder: "이 폴더로 이동"
show-in-new-window: "새 창으로 보기" show-in-new-window: "새 창으로 보기"
rename: "이름 변경" rename: "이름 변경"
rename-folder: "폴더 이름 변경" rename-folder: "폴더 이름 변경"
input-new-folder-name: "새 폴더 이름을 입력하여 주십시오" input-new-folder-name: "새 폴더 이름을 입력하여 주십시오"
else-folders: "기타"
set-as-upload-folder: "기본 업로드 위치로 설정"
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
search: "검색" search: "검색"
empty-draghover: "끌어놓으신 거 맞나요? 괜찮아요, 저는 귀여우니까요" empty-draghover: "끌어놓으신 거 맞나요? 괜찮아요, 저는 귀여우니까요"
@ -1010,6 +1023,7 @@ desktop/views/components/settings.2fa.vue:
register-security-key: "키 등록 완료" register-security-key: "키 등록 완료"
something-went-wrong: "으악! 키를 등록하는 도중 문제가 발생하였습니다:" something-went-wrong: "으악! 키를 등록하는 도중 문제가 발생하였습니다:"
key-unregistered: "키가 등록되어 있지 않습니다" key-unregistered: "키가 등록되어 있지 않습니다"
use-password-less-login: "비밀번호 없는 로그인 사용"
common/views/components/media-image.vue: common/views/components/media-image.vue:
sensitive: "열람주의" sensitive: "열람주의"
click-to-show: "클릭하여 보기" click-to-show: "클릭하여 보기"
@ -1034,6 +1048,9 @@ common/views/components/drive-settings.vue:
max: "최대 용량" max: "최대 용량"
in-use: "사용중" in-use: "사용중"
stats: "통계" stats: "통계"
default-upload-folder: "기본 업로드 폴더 위치"
default-upload-folder-name: "폴더"
change-default-upload-folder: "폴더 변경"
common/views/components/mute-and-block.vue: common/views/components/mute-and-block.vue:
mute-and-block: "뮤트 및 차단" mute-and-block: "뮤트 및 차단"
mute: "뮤트" mute: "뮤트"
@ -1716,6 +1733,7 @@ deck/deck.user-column.vue:
activity: "활동" activity: "활동"
timeline: "타임라인" timeline: "타임라인"
pinned-notes: "고정해놓은 글" pinned-notes: "고정해놓은 글"
pinned-page: "고정해놓은 페이지"
docs: docs:
edit-this-page-on-github: "틀린 점이나 개선할 점을 찾으셨나요?" edit-this-page-on-github: "틀린 점이나 개선할 점을 찾으셨나요?"
edit-this-page-on-github-link: "이 페이지를 GitHub에서 편집" edit-this-page-on-github-link: "이 페이지를 GitHub에서 편집"
@ -1750,6 +1768,8 @@ pages:
are-you-sure-delete: "이 페이지를 삭제하시겠습니까?" are-you-sure-delete: "이 페이지를 삭제하시겠습니까?"
page-deleted: "페이지가 삭제되었습니다" page-deleted: "페이지가 삭제되었습니다"
edit-this-page: "이 페이지를 편집" edit-this-page: "이 페이지를 편집"
pin-this-page: "프로필에 고정"
unpin-this-page: "프로필에서 고정 해제"
view-source: "소스 보기" view-source: "소스 보기"
view-page: "페이지 보기" view-page: "페이지 보기"
like: "좋아요" like: "좋아요"
@ -1768,6 +1788,7 @@ pages:
url: "페이지 URL" url: "페이지 URL"
summary: "페이지 요약" summary: "페이지 요약"
align-center: "가운데 정렬" align-center: "가운데 정렬"
hide-title-when-pinned: "프로필에 고정해놓은 경우 타이틀을 표시하지 않음"
font: "글꼴" font: "글꼴"
fontSerif: "세리프" fontSerif: "세리프"
fontSansSerif: "산 세리프" fontSansSerif: "산 세리프"
@ -1821,12 +1842,19 @@ pages:
inc: "증가치" inc: "증가치"
_button: _button:
text: "제목" text: "제목"
colored: "색상"
action: "버튼을 눌렀을 때의 동작" action: "버튼을 눌렀을 때의 동작"
_action: _action:
dialog: "대화상자를 표시" dialog: "대화상자를 표시"
_dialog: _dialog:
content: "내용" content: "내용"
resetRandom: "난수를 초기화" resetRandom: "난수를 초기화"
pushEvent: "이벤트 보내기"
_pushEvent:
event: "이벤트 이름"
message: "눌렀을 때 표시할 메시지"
variable: "보낼 변수"
no-variable: "없음"
script: script:
categories: categories:
flow: "흐름 제어" flow: "흐름 제어"

View file

@ -260,7 +260,6 @@ desktop/views/components/drive.file.vue:
rename: "Naam wijzigen" rename: "Naam wijzigen"
copy-url: "URL kopiëren" copy-url: "URL kopiëren"
download: "Downloaden" download: "Downloaden"
else-files: "Overig"
set-as-avatar: "Instellen als gebruikersafbeelding" set-as-avatar: "Instellen als gebruikersafbeelding"
set-as-banner: "Instellen als omslagfoto" set-as-banner: "Instellen als omslagfoto"
open-in-app: "Openen in app" open-in-app: "Openen in app"
@ -377,6 +376,7 @@ common/views/components/api-settings.vue:
common/views/components/drive-settings.vue: common/views/components/drive-settings.vue:
in-use: "gebruikt" in-use: "gebruikt"
stats: "Statistieken" stats: "Statistieken"
default-upload-folder-name: "Map(pen)"
desktop/views/components/sub-note-content.vue: desktop/views/components/sub-note-content.vue:
private: "(dit bericht is privé)" private: "(dit bericht is privé)"
poll: "Peilingen" poll: "Peilingen"
@ -606,6 +606,7 @@ docs:
edit-this-page-on-github: "Heb je een fout ontdekt of wil je bijdragen aan de documentatie? " edit-this-page-on-github: "Heb je een fout ontdekt of wil je bijdragen aan de documentatie? "
edit-this-page-on-github-link: "Bewerk deze pagina op GitHub!" edit-this-page-on-github-link: "Bewerk deze pagina op GitHub!"
pages: pages:
pin-this-page: "Vastmaken aan profielpagina"
like: "Leuk" like: "Leuk"
blocks: blocks:
image: "Afbeeldingen" image: "Afbeeldingen"

View file

@ -286,6 +286,7 @@ common/views/components/api-settings.vue:
common/views/components/drive-settings.vue: common/views/components/drive-settings.vue:
in-use: "brukt" in-use: "brukt"
stats: "Statistikk" stats: "Statistikk"
default-upload-folder-name: "Mappe(r)"
common/views/components/mute-and-block.vue: common/views/components/mute-and-block.vue:
save: "Lagre" save: "Lagre"
desktop/views/components/settings.tags.vue: desktop/views/components/settings.tags.vue:
@ -484,6 +485,7 @@ deck/deck.user-column.vue:
followers: "Følgere" followers: "Følgere"
images: "Bilder" images: "Bilder"
pages: pages:
pin-this-page: "Fest til profilen din"
like: "Lik" like: "Lik"
blocks: blocks:
image: "Bilder" image: "Bilder"

View file

@ -414,6 +414,8 @@ common/views/components/emoji-picker.vue:
objects: "Rzeczy" objects: "Rzeczy"
symbols: "Symbole" symbols: "Symbole"
flags: "Flagi" flags: "Flagi"
common/views/components/settings/app-type.vue:
info: "Musisz odświeżyć stronę, aby zmiany zostały uwzględnione."
common/views/components/signin.vue: common/views/components/signin.vue:
username: "Nazwa użytkownika" username: "Nazwa użytkownika"
password: "Hasło" password: "Hasło"
@ -666,6 +668,7 @@ desktop/views/components/drive.folder.vue:
rename: "Zmień nazwę" rename: "Zmień nazwę"
rename-folder: "Zmień nazwę katalogu" rename-folder: "Zmień nazwę katalogu"
input-new-folder-name: "Wprowadź nową nazwę" input-new-folder-name: "Wprowadź nową nazwę"
else-folders: "Inne"
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
search: "Szukaj" search: "Szukaj"
empty-draghover: "Przeciągnij tutaj!" empty-draghover: "Przeciągnij tutaj!"
@ -785,6 +788,7 @@ common/views/components/drive-settings.vue:
max: "Max" max: "Max"
in-use: "użyto" in-use: "użyto"
stats: "Statystyki" stats: "Statystyki"
default-upload-folder-name: "Katalog(i)"
common/views/components/mute-and-block.vue: common/views/components/mute-and-block.vue:
mute-and-block: "Wycisz / Zablokuj" mute-and-block: "Wycisz / Zablokuj"
mute: "Wycisz" mute: "Wycisz"
@ -1189,6 +1193,8 @@ dev/views/new-app.vue:
app-name: "Nazwa Aplikacji" app-name: "Nazwa Aplikacji"
authority: "Uprawnienia" authority: "Uprawnienia"
pages: pages:
pin-this-page: "Przypnij do profilu"
unpin-this-page: "Odepnij"
like: "Lubię" like: "Lubię"
title: "Tytuł" title: "Tytuł"
blocks: blocks:

View file

@ -28,6 +28,7 @@ common:
enter-password: "Пожалуйста, введите ваш пароль" enter-password: "Пожалуйста, введите ваш пароль"
2fa: "Двухфакторная аутентификация" 2fa: "Двухфакторная аутентификация"
customize-home: "Настройка домашней страницы" customize-home: "Настройка домашней страницы"
featured-notes: "Рекомендуемые"
dark-mode: "Тёмная тема" dark-mode: "Тёмная тема"
signin: "Войти" signin: "Войти"
signup: "Регистрация" signup: "Регистрация"

View file

@ -591,6 +591,8 @@ common/views/components/emoji-picker.vue:
objects: "物品" objects: "物品"
symbols: "符号" symbols: "符号"
flags: "旗帜" flags: "旗帜"
common/views/components/settings/app-type.vue:
info: "更改将在刷新页面后生效。"
common/views/components/signin.vue: common/views/components/signin.vue:
username: "用户名" username: "用户名"
password: "密码" password: "密码"
@ -873,7 +875,7 @@ desktop/views/components/drive.file.vue:
unmark-as-sensitive: "取消标记为“敏感”" unmark-as-sensitive: "取消标记为“敏感”"
copy-url: "复制链接" copy-url: "复制链接"
download: "下载" download: "下载"
else-files: "其他..." else-files: "其他"
set-as-avatar: "设置为头像" set-as-avatar: "设置为头像"
set-as-banner: "设置为背景" set-as-banner: "设置为背景"
open-in-app: "在应用程序中打开" open-in-app: "在应用程序中打开"
@ -892,6 +894,7 @@ desktop/views/components/drive.folder.vue:
rename: "重命名" rename: "重命名"
rename-folder: "重命名文件夹" rename-folder: "重命名文件夹"
input-new-folder-name: "请输入新文件名" input-new-folder-name: "请输入新文件名"
else-folders: "其他"
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
search: "搜索" search: "搜索"
empty-draghover: "放在这里!因为你知道我很可爱,对吗?" empty-draghover: "放在这里!因为你知道我很可爱,对吗?"
@ -1021,6 +1024,7 @@ common/views/components/drive-settings.vue:
max: "容量" max: "容量"
in-use: "已使用" in-use: "已使用"
stats: "统计" stats: "统计"
default-upload-folder-name: "文件夹"
common/views/components/mute-and-block.vue: common/views/components/mute-and-block.vue:
mute-and-block: "屏蔽/拉黑" mute-and-block: "屏蔽/拉黑"
mute: "屏蔽" mute: "屏蔽"
@ -1734,6 +1738,8 @@ pages:
are-you-sure-delete: "是否删除此页面?" are-you-sure-delete: "是否删除此页面?"
page-deleted: "该页面已被删除。" page-deleted: "该页面已被删除。"
edit-this-page: "编辑此页面" edit-this-page: "编辑此页面"
pin-this-page: "置顶"
unpin-this-page: "取消置顶"
view-source: "查看源代码" view-source: "查看源代码"
view-page: "查看页面" view-page: "查看页面"
like: "赞" like: "赞"

View file

@ -0,0 +1,13 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class PasswordLessLogin1562422242907 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "user_profile" ADD COLUMN "usePasswordLessLogin" boolean DEFAULT false NOT NULL`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "usePasswordLessLogin"`);
}
}

View file

@ -0,0 +1,17 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class PinnedPage1562444565093 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "pinnedPageId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_6dc44f1ceb65b1e72bacef2ca27" UNIQUE ("pinnedPageId")`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_6dc44f1ceb65b1e72bacef2ca27" FOREIGN KEY ("pinnedPageId") REFERENCES "page"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_6dc44f1ceb65b1e72bacef2ca27"`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_6dc44f1ceb65b1e72bacef2ca27"`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "pinnedPageId"`);
}
}

View file

@ -0,0 +1,13 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class PageTitleHideOption1562448332510 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "page" ADD "hideTitleWhenPinned" boolean NOT NULL DEFAULT false`);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "hideTitleWhenPinned"`);
}
}

View file

@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "11.24.2", "version": "11.25.0",
"codename": "daybreak", "codename": "daybreak",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -84,7 +84,14 @@
// Detect the user agent // Detect the user agent
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
const isMobile = /mobile|iphone|ipad|android/.test(ua); let isMobile = /mobile|iphone|ipad|android/.test(ua) || window.innerWidth < 576;
if (settings && settings.device.appTypeForce) {
if (settings.device.appTypeForce === 'mobile') {
isMobile = true;
} else if (settings.device.appTypeForce === 'desktop') {
isMobile = false;
}
}
// Get the <head> element // Get the <head> element
const head = document.getElementsByTagName('head')[0]; const head = document.getElementsByTagName('head')[0];

View file

@ -8,6 +8,7 @@ import { host, url } from '../../config';
import i18n from '../../i18n'; import i18n from '../../i18n';
import { erase, unique } from '../../../../prelude/array'; import { erase, unique } from '../../../../prelude/array';
import extractMentions from '../../../../misc/extract-mentions'; import extractMentions from '../../../../misc/extract-mentions';
import { formatTimeString } from '../../../../misc/format-time-string';
export default (opts) => ({ export default (opts) => ({
i18n: i18n(), i18n: i18n(),
@ -151,9 +152,16 @@ export default (opts) => ({
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ // 公開以外へのリプライ時は元の公開範囲を引き継ぐ
if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) { if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
this.visibility = this.reply.visibility; this.visibility = this.reply.visibility;
if (this.reply.visibility === 'specified') {
this.$root.api('users/show', {
userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$store.state.i.id && uid !== this.reply.userId)
}).then(users => {
this.visibleUsers.push(...users);
});
}
} }
if (this.reply) { if (this.reply && this.reply.userId !== this.$store.state.i.id) {
this.$root.api('users/show', { userId: this.reply.userId }).then(user => { this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
this.visibleUsers.push(user); this.visibleUsers.push(user);
}); });
@ -237,8 +245,8 @@ export default (opts) => ({
for (const x of Array.from((this.$refs.file as any).files)) this.upload(x); for (const x of Array.from((this.$refs.file as any).files)) this.upload(x);
}, },
upload(file) { upload(file: File, name?: string) {
(this.$refs.uploader as any).upload(file); (this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
}, },
onChangeUploadings(uploads) { onChangeUploadings(uploads) {
@ -327,10 +335,23 @@ export default (opts) => ({
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
}, },
async onPaste(e) { async onPaste(e: ClipboardEvent) {
for (const item of Array.from(e.clipboardData.items)) { for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
if (item.kind == 'file') { if (item.kind == 'file') {
this.upload(item.getAsFile()); const file = item.getAsFile();
const lio = file.name.lastIndexOf('.');
const ext = lio >= 0 ? file.name.slice(lio) : '';
const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
const name = this.$store.state.settings.pasteDialog
? await this.$root.dialog({
title: this.$t('@.post-form.enter-file-name'),
input: {
default: formatted
},
allowEmpty: false
}).then(({ canceled, result }) => canceled ? false : result)
: formatted;
if (name) this.upload(file, name);
} }
} }

View file

@ -5,17 +5,17 @@
<div style="overflow: hidden; line-height: 28px;"> <div style="overflow: hidden; line-height: 28px;">
<p class="turn" v-if="!iAmPlayer && !game.isEnded"> <p class="turn" v-if="!iAmPlayer && !game.isEnded">
<mfm :key="'turn:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/> <mfm :key="'turn:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) })" :plain="true" :custom-emojis="turnUser.emojis"/>
<mk-ellipsis/> <mk-ellipsis/>
</p> </p>
<p class="turn" v-if="logPos != logs.length"> <p class="turn" v-if="logPos != logs.length">
<mfm :key="'past-turn-of:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/> <mfm :key="'past-turn-of:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) })" :plain="true" :custom-emojis="turnUser.emojis"/>
</p> </p>
<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ $t('@.reversi.opponent-turn') }}<mk-ellipsis/></p> <p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ $t('@.reversi.opponent-turn') }}<mk-ellipsis/></p>
<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">{{ $t('@.reversi.my-turn') }}</p> <p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">{{ $t('@.reversi.my-turn') }}</p>
<p class="result" v-if="game.isEnded && logPos == logs.length"> <p class="result" v-if="game.isEnded && logPos == logs.length">
<template v-if="game.winner"> <template v-if="game.winner">
<mfm :key="'won'" :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :should-break="false" :plain-text="true" :custom-emojis="game.winner.emojis"/> <mfm :key="'won'" :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :plain="true" :custom-emojis="game.winner.emojis"/>
<span v-if="game.surrendered != null"> ({{ $t('surrendered') }})</span> <span v-if="game.surrendered != null"> ({{ $t('surrendered') }})</span>
</template> </template>
<template v-else>{{ $t('@.reversi.drawn') }}</template> <template v-else>{{ $t('@.reversi.drawn') }}</template>

View file

@ -30,6 +30,7 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import * as autosize from 'autosize'; import * as autosize from 'autosize';
import { formatTimeString } from '../../../../../misc/format-time-string';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('common/views/components/messaging-room.form.vue'), i18n: i18n('common/views/components/messaging-room.form.vue'),
@ -84,13 +85,26 @@ export default Vue.extend({
} }
}, },
methods: { methods: {
onPaste(e) { async onPaste(e: ClipboardEvent) {
const data = e.clipboardData; const data = e.clipboardData;
const items = data.items; const items = data.items;
if (items.length == 1) { if (items.length == 1) {
if (items[0].kind == 'file') { if (items[0].kind == 'file') {
this.upload(items[0].getAsFile()); const file = items[0].getAsFile();
const lio = file.name.lastIndexOf('.');
const ext = lio >= 0 ? file.name.slice(lio) : '';
const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
const name = this.$store.state.settings.pasteDialog
? await this.$root.dialog({
title: this.$t('@.post-form.enter-file-name'),
input: {
default: formatted
},
allowEmpty: false
}).then(({ canceled, result }) => canceled ? false : result)
: formatted;
if (name) this.upload(file, name);
} }
} else { } else {
if (items[0].kind == 'file') { if (items[0].kind == 'file') {
@ -157,8 +171,8 @@ export default Vue.extend({
this.upload((this.$refs.file as any).files[0]); this.upload((this.$refs.file as any).files[0]);
}, },
upload(file) { upload(file: File, name?: string) {
(this.$refs.uploader as any).upload(file); (this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
}, },
onUploaded(file) { onUploaded(file) {

View file

@ -22,11 +22,11 @@ export default Vue.component('misskey-flavored-markdown', {
type: String, type: String,
required: true required: true
}, },
shouldBreak: { plain: {
type: Boolean, type: Boolean,
default: true default: false
}, },
plainText: { nowrap: {
type: Boolean, type: Boolean,
default: false default: false
}, },
@ -50,7 +50,7 @@ export default Vue.component('misskey-flavored-markdown', {
render(createElement) { render(createElement) {
if (this.text == null || this.text == '') return; if (this.text == null || this.text == '') return;
const ast = (this.plainText ? parsePlain : parse)(this.text); const ast = (this.plain ? parsePlain : parse)(this.text);
let bigCount = 0; let bigCount = 0;
let motionCount = 0; let motionCount = 0;
@ -60,7 +60,7 @@ export default Vue.component('misskey-flavored-markdown', {
case 'text': { case 'text': {
const text = token.node.props.text.replace(/(\r\n|\n|\r)/g, '\n'); const text = token.node.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (this.shouldBreak) { if (!this.plain) {
const x = text.split('\n') const x = text.split('\n')
.map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]); .map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]);
x[x.length - 1].pop(); x[x.length - 1].pop();
@ -270,7 +270,7 @@ export default Vue.component('misskey-flavored-markdown', {
}, },
props: { props: {
customEmojis: this.customEmojis || customEmojis, customEmojis: this.customEmojis || customEmojis,
normal: this.plainText normal: this.plain
} }
})]; })];
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ plain: $attrs['plain-text'] }" v-once/> <mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }" v-once/>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -17,7 +17,7 @@ export default Vue.extend({
.havbbuyv .havbbuyv
white-space pre-wrap white-space pre-wrap
&.plain &.nowrap
white-space pre white-space pre
>>> .title >>> .title

View file

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<ui-button class="kudkigyw" @click="click()">{{ script.interpolate(value.text) }}</ui-button> <ui-button class="kudkigyw" @click="click()" :primary="value.primary">{{ script.interpolate(value.text) }}</ui-button>
</div> </div>
</template> </template>
@ -27,6 +27,19 @@ export default Vue.extend({
} else if (this.value.action === 'resetRandom') { } else if (this.value.action === 'resetRandom') {
this.script.aiScript.updateRandomSeed(Math.random()); this.script.aiScript.updateRandomSeed(Math.random());
this.script.eval(); this.script.eval();
} else if (this.value.action === 'pushEvent') {
this.$root.api('page-push', {
pageId: this.script.page.id,
event: this.value.event,
...(this.value.var ? {
var: this.script.vars[this.value.var]
} : {})
});
this.$root.dialog({
type: 'success',
text: this.script.interpolate(this.value.message)
});
} }
} }
} }
@ -36,7 +49,7 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
.kudkigyw .kudkigyw
display inline-block display inline-block
min-width 300px min-width 200px
max-width 450px max-width 450px
margin 8px 0 margin 8px 0
</style> </style>

View file

@ -1,6 +1,6 @@
<template> <template>
<div v-if="page" class="iroscrza" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners, center: page.alignCenter }" :style="{ fontFamily: page.font }" :key="path"> <div class="iroscrza" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners, center: page.alignCenter }" :style="{ fontFamily: page.font }">
<header> <header v-if="showTitle">
<div class="title">{{ page.title }}</div> <div class="title">{{ page.title }}</div>
</header> </header>
@ -8,9 +8,13 @@
<x-block v-for="child in page.content" :value="child" @input="v => updateBlock(v)" :page="page" :script="script" :key="child.id" :h="2"/> <x-block v-for="child in page.content" :value="child" @input="v => updateBlock(v)" :page="page" :script="script" :key="child.id" :h="2"/>
</div> </div>
<footer> <footer v-if="showFooter">
<small>@{{ page.user.username }}</small> <small>@{{ page.user.username }}</small>
<router-link v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId" :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link> <template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId">
<router-link :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link>
<a v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)">{{ $t('unpin-this-page') }}</a>
<a v-else @click="pin(true)">{{ $t('pin-this-page') }}</a>
</template>
<router-link :to="`./${page.name}/view-source`">{{ $t('view-source') }}</router-link> <router-link :to="`./${page.name}/view-source`">{{ $t('view-source') }}</router-link>
<div class="like"> <div class="like">
<button @click="unlike()" v-if="page.isLiked" :title="$t('unlike')"><fa :icon="faHeartS"/></button> <button @click="unlike()" v-if="page.isLiked" :title="$t('unlike')"><fa :icon="faHeartS"/></button>
@ -25,7 +29,7 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../../i18n'; import i18n from '../../../../i18n';
import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons'; import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons';
import { faHeart, faStickyNote } from '@fortawesome/free-regular-svg-icons'; import { faHeart } from '@fortawesome/free-regular-svg-icons';
import XBlock from './page.block.vue'; import XBlock from './page.block.vue';
import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator'; import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator';
import { collectPageVars } from '../../../scripts/collect-page-vars'; import { collectPageVars } from '../../../scripts/collect-page-vars';
@ -35,8 +39,10 @@ class Script {
public aiScript: ASEvaluator; public aiScript: ASEvaluator;
private onError: any; private onError: any;
public vars: Record<string, any>; public vars: Record<string, any>;
public page: Record<string, any>;
constructor(aiScript, onError) { constructor(page, aiScript, onError) {
this.page = page;
this.aiScript = aiScript; this.aiScript = aiScript;
this.onError = onError; this.onError = onError;
this.eval(); this.eval();
@ -67,64 +73,43 @@ export default Vue.extend({
}, },
props: { props: {
pageName: { page: {
type: String, type: Object,
required: true required: true
}, },
username: { showTitle: {
type: String, type: Boolean,
required: true required: false,
default: true
},
showFooter: {
type: Boolean,
required: false,
default: false
}, },
}, },
data() { data() {
return { return {
page: null,
script: null, script: null,
faHeartS, faHeart faHeartS, faHeart
}; };
}, },
computed: {
path(): string {
return this.username + '/' + this.pageName;
}
},
watch: {
path() {
this.fetch();
}
},
created() { created() {
this.fetch();
},
methods: {
fetch() {
this.$root.api('pages/show', {
name: this.pageName,
username: this.username,
}).then(page => {
this.page = page;
this.$emit('init', {
title: this.page.title,
icon: faStickyNote
});
const pageVars = this.getPageVars(); const pageVars = this.getPageVars();
this.script = new Script(new ASEvaluator(this.page.variables, pageVars, { this.script = new Script(this.page, new ASEvaluator(this.page.variables, pageVars, {
randomSeed: Math.random(), randomSeed: Math.random(),
user: page.user, user: this.page.user,
visitor: this.$store.state.i, visitor: this.$store.state.i,
page: page, page: this.page,
url: url url: url
}), e => { }), e => {
console.dir(e); console.dir(e);
}); });
});
}, },
methods: {
getPageVars() { getPageVars() {
return collectPageVars(this.page.content); return collectPageVars(this.page.content);
}, },
@ -145,6 +130,17 @@ export default Vue.extend({
this.page.isLiked = false; this.page.isLiked = false;
this.page.likedCount--; this.page.likedCount--;
}); });
},
pin(pin) {
this.$root.api('i/update', {
pinnedPageId: pin ? this.page.id : null,
}).then(() => {
this.$root.dialog({
type: 'success',
splash: true
});
});
} }
} }
}); });

View file

@ -5,7 +5,7 @@
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span> <span>
<template v-if="choice.isVoted"><fa icon="check"/></template> <template v-if="choice.isVoted"><fa icon="check"/></template>
<mfm :text="choice.text" :should-break="false" :plain-text="true" :custom-emojis="note.emojis"/> <mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
<span class="votes" v-if="showResult">({{ $t('vote-count').replace('{}', choice.votes) }})</span> <span class="votes" v-if="showResult">({{ $t('vote-count').replace('{}', choice.votes) }})</span>
</span> </span>
</li> </li>

View file

@ -28,6 +28,10 @@
</div> </div>
</div> </div>
<ui-switch v-model="usePasswordLessLogin" @change="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">
{{ $t('use-password-less-login') }}
</ui-switch>
<ui-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</ui-info> <ui-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</ui-info>
<ui-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</ui-button> <ui-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</ui-button>
@ -80,6 +84,7 @@ export default Vue.extend({
return { return {
data: null, data: null,
supportsCredentials: !!navigator.credentials, supportsCredentials: !!navigator.credentials,
usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
registration: null, registration: null,
keyName: '', keyName: '',
token: null token: null
@ -112,6 +117,9 @@ export default Vue.extend({
if (canceled) return; if (canceled) return;
this.$root.api('i/2fa/unregister', { this.$root.api('i/2fa/unregister', {
password: password password: password
}).then(() => {
this.usePasswordLessLogin = false;
this.updatePasswordLessLogin();
}).then(() => { }).then(() => {
this.$notify(this.$t('unregistered')); this.$notify(this.$t('unregistered'));
this.$store.state.i.twoFactorEnabled = false; this.$store.state.i.twoFactorEnabled = false;
@ -157,6 +165,9 @@ export default Vue.extend({
return this.$root.api('i/2fa/remove-key', { return this.$root.api('i/2fa/remove-key', {
password, password,
credentialId: key.id credentialId: key.id
}).then(() => {
this.usePasswordLessLogin = false;
this.updatePasswordLessLogin();
}).then(() => { }).then(() => {
this.$notify(this.$t('key-unregistered')); this.$notify(this.$t('key-unregistered'));
}); });
@ -213,6 +224,11 @@ export default Vue.extend({
this.registration.stage = -1; this.registration.stage = -1;
}); });
}); });
},
updatePasswordLessLogin() {
this.$root.api('i/2fa/password-less', {
value: !!this.usePasswordLessLogin
});
} }
} }
}); });

View file

@ -0,0 +1,36 @@
<template>
<ui-card>
<template #title><fa :icon="faMobileAlt"/> {{ $t('title') }}</template>
<section class="fit-top">
<p>{{ $t('intro') }}</p>
<ui-select v-model="appTypeForce" :placeholder="$t('intro')">
<option v-for="x in ['auto', 'desktop', 'mobile']" :value="x" :key="x">{{ $t(`choices.${x}`) }}</option>
</ui-select>
<ui-info warn>{{ $t('info') }}</ui-info>
</section>
</ui-card>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import { faMobileAlt } from '@fortawesome/free-solid-svg-icons'
export default Vue.extend({
i18n: i18n('common/views/components/settings/app-type.vue'),
data() {
return {
faMobileAlt
};
},
computed: {
appTypeForce: {
get() { return this.$store.state.device.appTypeForce; },
set(value) { this.$store.commit('device/set', { key: 'appTypeForce', value }); }
},
},
});
</script>

View file

@ -11,6 +11,12 @@
<header>{{ $t('stats') }}</header> <header>{{ $t('stats') }}</header>
<div ref="chart" style="margin-bottom: -16px; margin-left: -8px; color: #000;"></div> <div ref="chart" style="margin-bottom: -16px; margin-left: -8px; color: #000;"></div>
</section> </section>
<section>
<header>{{ $t('default-upload-folder') }}</header>
<ui-input v-model="uploadFolderName" readonly>{{ $t('default-upload-folder-name') }}</ui-input>
<ui-button @click="chooseUploadFolder()">{{ $t('change-default-upload-folder') }}</ui-button>
</section>
</ui-card> </ui-card>
</template> </template>
@ -26,7 +32,8 @@ export default Vue.extend({
return { return {
fetching: true, fetching: true,
usage: null, usage: null,
capacity: null capacity: null,
uploadFolderName: null
}; };
}, },
@ -40,10 +47,25 @@ export default Vue.extend({
l: 0.5 l: 0.5
}) })
}; };
} },
uploadFolder: {
get() { return this.$store.state.settings.uploadFolder; },
set(value) { this.$store.dispatch('settings/set', { key: 'uploadFolder', value }); }
},
}, },
mounted() { mounted() {
if (this.uploadFolder == null) {
this.uploadFolderName = this.$t('@._settings.root');
} else {
this.$root.api('drive/folders/show', {
folderId: this.uploadFolder
}).then(folder => {
this.uploadFolderName = folder.name;
});
}
this.$root.api('drive').then(info => { this.$root.api('drive').then(info => {
this.capacity = info.capacity; this.capacity = info.capacity;
this.usage = info.usage; this.usage = info.usage;
@ -89,6 +111,9 @@ export default Vue.extend({
height: 150, height: 150,
zoom: { zoom: {
enabled: false enabled: false
},
toolbar: {
show: false
} }
}, },
plotOptions: { plotOptions: {
@ -152,6 +177,13 @@ export default Vue.extend({
chart.render(); chart.render();
}); });
},
chooseUploadFolder() {
this.$chooseDriveFolder().then(folder => {
this.uploadFolder = folder ? folder.id : null;
this.uploadFolderName = folder ? folder.name : this.$t('@._settings.root');
})
} }
} }
}); });

View file

@ -140,7 +140,19 @@
<section> <section>
<header>{{ $t('@._settings.web-search-engine') }}</header> <header>{{ $t('@._settings.web-search-engine') }}</header>
<ui-input v-model="webSearchEngine">{{ $t('@._settings.web-search-engine') }}<template #desc>{{ $t('@._settings.web-search-engine-desc') }}</template></ui-input> <ui-input v-model="webSearchEngine">{{ $t('@._settings.web-search-engine') }}
<template #desc>{{ $t('@._settings.web-search-engine-desc') }}</template>
</ui-input>
</section>
<section v-if="!$root.isMobile">
<header>{{ $t('@._settings.paste') }}</header>
<ui-input v-model="pastedFileName">{{ $t('@._settings.pasted-file-name') }}
<template #desc>{{ $t('@._settings.pasted-file-name-desc') }}</template>
</ui-input>
<ui-switch v-model="pasteDialog">{{ $t('@._settings.paste-dialog') }}
<template #desc>{{ $t('@._settings.paste-dialog-desc') }}</template>
</ui-switch>
</section> </section>
</ui-card> </ui-card>
@ -163,6 +175,7 @@
</ui-card> </ui-card>
<x-language/> <x-language/>
<x-app-type/>
</template> </template>
<template v-if="page == null || page == 'notification'"> <template v-if="page == null || page == 'notification'">
@ -271,6 +284,7 @@ import XPassword from './password.vue';
import XProfile from './profile.vue'; import XProfile from './profile.vue';
import XApi from './api.vue'; import XApi from './api.vue';
import XLanguage from './language.vue'; import XLanguage from './language.vue';
import XAppType from './app-type.vue';
import XNotification from './notification.vue'; import XNotification from './notification.vue';
import { url, version } from '../../../../config'; import { url, version } from '../../../../config';
@ -291,6 +305,7 @@ export default Vue.extend({
XProfile, XProfile,
XApi, XApi,
XLanguage, XLanguage,
XAppType,
XNotification, XNotification,
}, },
props: { props: {
@ -409,6 +424,16 @@ export default Vue.extend({
set(value) { this.$store.dispatch('settings/set', { key: 'webSearchEngine', value }); } set(value) { this.$store.dispatch('settings/set', { key: 'webSearchEngine', value }); }
}, },
pastedFileName: {
get() { return this.$store.state.settings.pastedFileName; },
set(value) { this.$store.dispatch('settings/set', { key: 'pastedFileName', value }); }
},
pasteDialog: {
get() { return this.$store.state.settings.pasteDialog; },
set(value) { this.$store.dispatch('settings/set', { key: 'pasteDialog', value }); }
},
showReplyTarget: { showReplyTarget: {
get() { return this.$store.state.settings.showReplyTarget; }, get() { return this.$store.state.settings.showReplyTarget; },
set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); } set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); }

View file

@ -7,7 +7,7 @@
<template #prefix>@</template> <template #prefix>@</template>
<template #suffix>@{{ host }}</template> <template #suffix>@{{ host }}</template>
</ui-input> </ui-input>
<ui-input v-model="password" type="password" :with-password-toggle="true" required> <ui-input v-model="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required>
<span>{{ $t('password') }}</span> <span>{{ $t('password') }}</span>
<template #prefix><fa icon="lock"/></template> <template #prefix><fa icon="lock"/></template>
</ui-input> </ui-input>
@ -28,6 +28,10 @@
</div> </div>
<div class="twofa-group totp-group"> <div class="twofa-group totp-group">
<p style="margin-bottom:0;">{{ $t('enter-2fa-code') }}</p> <p style="margin-bottom:0;">{{ $t('enter-2fa-code') }}</p>
<ui-input v-model="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required>
<span>{{ $t('password') }}</span>
<template #prefix><fa icon="lock"/></template>
</ui-input>
<ui-input v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required> <ui-input v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
<span>{{ $t('@.2fa') }}</span> <span>{{ $t('@.2fa') }}</span>
<template #prefix><fa icon="gavel"/></template> <template #prefix><fa icon="gavel"/></template>

View file

@ -46,7 +46,7 @@ export default Vue.extend({
}); });
}, },
upload(file: File, folder: any) { upload(file: File, folder: any, name?: string) {
if (folder && typeof folder == 'object') folder = folder.id; if (folder && typeof folder == 'object') folder = folder.id;
const id = Math.random(); const id = Math.random();
@ -61,7 +61,7 @@ export default Vue.extend({
const ctx = { const ctx = {
id: id, id: id,
name: file.name || 'untitled', name: name || file.name || 'untitled',
progress: undefined, progress: undefined,
img: window.URL.createObjectURL(file) img: window.URL.createObjectURL(file)
}; };
@ -75,6 +75,7 @@ export default Vue.extend({
data.append('file', file); data.append('file', file);
if (folder) data.append('folderId', folder); if (folder) data.append('folderId', folder);
if (name) data.append('name', name);
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + '/drive/files/create', true); xhr.open('POST', apiUrl + '/drive/files/create', true);

View file

@ -63,6 +63,7 @@ export default Vue.extend({
data() { data() {
const isSelf = this.url.startsWith(local); const isSelf = this.url.startsWith(local);
const hasRoute = const hasRoute =
(this.url.substr(local.length) === '/') ||
this.url.substr(local.length).startsWith('/@') || this.url.substr(local.length).startsWith('/@') ||
this.url.substr(local.length).startsWith('/notes/') || this.url.substr(local.length).startsWith('/notes/') ||
this.url.substr(local.length).startsWith('/pages/'); this.url.substr(local.length).startsWith('/pages/');

View file

@ -16,7 +16,7 @@
<p class="username">@{{ user | acct }}</p> <p class="username">@{{ user | acct }}</p>
</div> </div>
<div class="description" v-if="user.description" :title="user.description"> <div class="description" v-if="user.description" :title="user.description">
<mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :should-break="false" :plain-text="true"/> <mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :plain="true" :nowrap="true"/>
</div> </div>
<mk-follow-button class="follow-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/> <mk-follow-button class="follow-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
</div> </div>

View file

@ -1,5 +1,5 @@
<template> <template>
<mfm :text="user.name || user.username" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/> <mfm :text="user.name || user.username" :plain="true" :nowrap="true" :custom-emojis="user.emojis"/>
</template> </template>
<script lang="ts"> <script lang="ts">

View file

@ -12,7 +12,7 @@
</header> </header>
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<fa icon="quote-left"/> <fa icon="quote-left"/>
<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/> <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
<fa icon="quote-right"/> <fa icon="quote-right"/>
</router-link> </router-link>
</div> </div>
@ -30,7 +30,7 @@
</header> </header>
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
<fa icon="quote-left"/> <fa icon="quote-left"/>
<mfm :text="getNoteSummary(notification.note.renote)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.renote.emojis"/> <mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/>
<fa icon="quote-right"/> <fa icon="quote-right"/>
</router-link> </router-link>
</div> </div>
@ -74,7 +74,7 @@
</header> </header>
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<fa icon="quote-left"/> <fa icon="quote-left"/>
<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/> <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
<fa icon="quote-right"/> <fa icon="quote-right"/>
</router-link> </router-link>
</div> </div>

View file

@ -0,0 +1,69 @@
<template>
<x-column>
<template #header>
<fa :icon="faStickyNote"/>{{ page ? page.name : '' }}
</template>
<div v-if="page">
<x-page :page="page" :key="page.id"/>
</div>
</x-column>
</template>
<script lang="ts">
import Vue from 'vue';
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
import i18n from '../../../i18n';
import XColumn from './deck.column.vue';
import XPage from '../../../common/views/components/page/page.vue';
export default Vue.extend({
i18n: i18n(),
components: {
XColumn,
XPage
},
props: {
pageName: {
type: String,
required: true
},
username: {
type: String,
required: true
},
},
data() {
return {
page: null,
faStickyNote
};
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
methods: {
fetch() {
this.$root.api('pages/show', {
name: this.pageName,
username: this.username,
}).then(page => {
this.page = page;
this.$emit('init', {
title: this.page.title,
icon: faStickyNote
});
});
}
}
});
</script>

View file

@ -1,5 +1,11 @@
<template> <template>
<div> <div>
<ui-container v-if="user.pinnedPage" :body-togglable="true">
<template #header><fa icon="thumbtack"/> {{ $t('pinned-page') }}</template>
<div>
<x-page :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/>
</div>
</ui-container>
<ui-container v-if="user.pinnedNotes && user.pinnedNotes.length > 0" :body-togglable="true"> <ui-container v-if="user.pinnedNotes && user.pinnedNotes.length > 0" :body-togglable="true">
<template #header><fa icon="thumbtack"/> {{ $t('pinned-notes') }}</template> <template #header><fa icon="thumbtack"/> {{ $t('pinned-notes') }}</template>
<div> <div>
@ -48,6 +54,7 @@ export default Vue.extend({
components: { components: {
XNotes, XNotes,
XPage: () => import('../../../common/views/components/page/page.vue').then(m => m.default),
}, },
props: { props: {

View file

@ -30,7 +30,7 @@
<div class="fields" v-if="user.fields"> <div class="fields" v-if="user.fields">
<dl class="field" v-for="(field, i) in user.fields" :key="i"> <dl class="field" v-for="(field, i) in user.fields" :key="i">
<dt class="name"> <dt class="name">
<mfm :text="field.name" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/> <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/>
</dt> </dt>
<dd class="value"> <dd class="value">
<mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching"> <div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching">
<div class="signed-in-as"> <div class="signed-in-as">
<mfm :text="$t('signed-in-as').replace('{}', myName)" :should-break="false" :plain-text="true" :custom-emojis="$store.state.i.emojis"/> <mfm :text="$t('signed-in-as').replace('{}', myName)" :plain="true" :custom-emojis="$store.state.i.emojis"/>
</div> </div>
<main> <main>
<div class="banner" :style="bannerStyle"></div> <div class="banner" :style="bannerStyle"></div>

View file

@ -4,12 +4,31 @@
<section class="xfhsjczc"> <section class="xfhsjczc">
<ui-input v-model="value.text"><span>{{ $t('blocks._button.text') }}</span></ui-input> <ui-input v-model="value.text"><span>{{ $t('blocks._button.text') }}</span></ui-input>
<ui-switch v-model="value.primary"><span>{{ $t('blocks._button.colored') }}</span></ui-switch>
<ui-select v-model="value.action"> <ui-select v-model="value.action">
<template #label>{{ $t('blocks._button.action') }}</template> <template #label>{{ $t('blocks._button.action') }}</template>
<option value="dialog">{{ $t('blocks._button._action.dialog') }}</option> <option value="dialog">{{ $t('blocks._button._action.dialog') }}</option>
<option value="resetRandom">{{ $t('blocks._button._action.resetRandom') }}</option> <option value="resetRandom">{{ $t('blocks._button._action.resetRandom') }}</option>
<option value="pushEvent">{{ $t('blocks._button._action.pushEvent') }}</option>
</ui-select> </ui-select>
<ui-input v-if="value.action === 'dialog'" v-model="value.content"><span>{{ $t('blocks._button._action._dialog.content') }}</span></ui-input> <template v-if="value.action === 'dialog'">
<ui-input v-model="value.content"><span>{{ $t('blocks._button._action._dialog.content') }}</span></ui-input>
</template>
<template v-else-if="value.action === 'pushEvent'">
<ui-input v-model="value.event"><span>{{ $t('blocks._button._action._pushEvent.event') }}</span></ui-input>
<ui-input v-model="value.message"><span>{{ $t('blocks._button._action._pushEvent.message') }}</span></ui-input>
<ui-select v-model="value.var">
<template #label>{{ $t('blocks._button._action._pushEvent.variable') }}</template>
<option :value="null">{{ $t('blocks._button._action._pushEvent.no-variable') }}</option>
<option v-for="v in aiScript.getVarsByType()" :value="v.name">{{ v.name }}</option>
<optgroup :label="$t('script.pageVariables')">
<option v-for="v in aiScript.getPageVarsByType()" :value="v">{{ v }}</option>
</optgroup>
<optgroup :label="$t('script.enviromentVariables')">
<option v-for="v in aiScript.getEnvVarsByType()" :value="v">{{ v }}</option>
</optgroup>
</ui-select>
</template>
</section> </section>
</x-container> </x-container>
</template> </template>
@ -31,6 +50,9 @@ export default Vue.extend({
value: { value: {
required: true required: true
}, },
aiScript: {
required: true,
},
}, },
data() { data() {
@ -43,6 +65,10 @@ export default Vue.extend({
if (this.value.text == null) Vue.set(this.value, 'text', ''); if (this.value.text == null) Vue.set(this.value, 'text', '');
if (this.value.action == null) Vue.set(this.value, 'action', 'dialog'); if (this.value.action == null) Vue.set(this.value, 'action', 'dialog');
if (this.value.content == null) Vue.set(this.value, 'content', null); if (this.value.content == null) Vue.set(this.value, 'content', null);
if (this.value.event == null) Vue.set(this.value, 'event', null);
if (this.value.message == null) Vue.set(this.value, 'message', null);
if (this.value.primary == null) Vue.set(this.value, 'primary', false);
if (this.value.var == null) Vue.set(this.value, 'var', null);
}, },
}); });
</script> </script>

View file

@ -35,6 +35,8 @@
<option value="sans-serif">{{ $t('fontSansSerif') }}</option> <option value="sans-serif">{{ $t('fontSansSerif') }}</option>
</ui-select> </ui-select>
<ui-switch v-model="hideTitleWhenPinned">{{ $t('hide-title-when-pinned') }}</ui-switch>
<div class="eyeCatch"> <div class="eyeCatch">
<ui-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catching-image') }}</ui-button> <ui-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catching-image') }}</ui-button>
<div v-else-if="eyeCatchingImage"> <div v-else-if="eyeCatchingImage">
@ -140,6 +142,7 @@ export default Vue.extend({
font: 'sans-serif', font: 'sans-serif',
content: [], content: [],
alignCenter: false, alignCenter: false,
hideTitleWhenPinned: false,
variables: [], variables: [],
aiScript: null, aiScript: null,
showOptions: false, showOptions: false,
@ -192,6 +195,7 @@ export default Vue.extend({
this.currentName = this.page.name; this.currentName = this.page.name;
this.summary = this.page.summary; this.summary = this.page.summary;
this.font = this.page.font; this.font = this.page.font;
this.hideTitleWhenPinned = this.page.hideTitleWhenPinned;
this.alignCenter = this.page.alignCenter; this.alignCenter = this.page.alignCenter;
this.content = this.page.content; this.content = this.page.content;
this.variables = this.page.variables; this.variables = this.page.variables;
@ -223,6 +227,7 @@ export default Vue.extend({
name: this.name.trim(), name: this.name.trim(),
summary: this.summary, summary: this.summary,
font: this.font, font: this.font,
hideTitleWhenPinned: this.hideTitleWhenPinned,
alignCenter: this.alignCenter, alignCenter: this.alignCenter,
content: this.content, content: this.content,
variables: this.variables, variables: this.variables,
@ -240,6 +245,7 @@ export default Vue.extend({
name: this.name.trim(), name: this.name.trim(),
summary: this.summary, summary: this.summary,
font: this.font, font: this.font,
hideTitleWhenPinned: this.hideTitleWhenPinned,
alignCenter: this.alignCenter, alignCenter: this.alignCenter,
content: this.content, content: this.content,
variables: this.variables, variables: this.variables,

View file

@ -0,0 +1,63 @@
<template>
<x-page v-if="page" :page="page" :key="page.id" :show-footer="true"/>
</template>
<script lang="ts">
import Vue from 'vue';
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
import XPage from '../components/page/page.vue';
export default Vue.extend({
components: {
XPage
},
props: {
pageName: {
type: String,
required: true
},
username: {
type: String,
required: true
},
},
data() {
return {
page: null,
};
},
computed: {
path(): string {
return this.username + '/' + this.pageName;
}
},
watch: {
path() {
this.fetch();
}
},
created() {
this.fetch();
},
methods: {
fetch() {
this.$root.api('pages/show', {
name: this.pageName,
username: this.username,
}).then(page => {
this.page = page;
this.$emit('init', {
title: this.page.title,
icon: faStickyNote
});
});
},
}
});
</script>

View file

@ -38,6 +38,7 @@
import define from '../../../common/define-widget'; import define from '../../../common/define-widget';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import insertTextAtCursor from 'insert-text-at-cursor'; import insertTextAtCursor from 'insert-text-at-cursor';
import { formatTimeString } from '../../../../../misc/format-time-string';
export default define({ export default define({
name: 'post-form', name: 'post-form',
@ -109,10 +110,23 @@ export default define({
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && !this.posting && this.text) this.post(); if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && !this.posting && this.text) this.post();
}, },
onPaste(e) { async onPaste(e: ClipboardEvent) {
for (const item of Array.from(e.clipboardData.items)) { for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
if (item.kind == 'file') { if (item.kind == 'file') {
this.upload(item.getAsFile()); const file = item.getAsFile();
const lio = file.name.lastIndexOf('.');
const ext = lio >= 0 ? file.name.slice(lio) : '';
const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
const name = this.$store.state.settings.pasteDialog
? await this.$root.dialog({
title: this.$t('@.post-form.enter-file-name'),
input: {
default: formatted
},
allowEmpty: false
}).then(({ canceled, result }) => canceled ? false : result)
: formatted;
if (name) this.upload(file, name);
} }
} }
}, },
@ -121,8 +135,8 @@ export default define({
for (const x of Array.from((this.$refs.file as any).files)) this.upload(x); for (const x of Array.from((this.$refs.file as any).files)) this.upload(x);
}, },
upload(file) { upload(file: File, name?: string) {
(this.$refs.uploader as any).upload(file); (this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
}, },
onDragover(e) { onDragover(e) {

View file

@ -148,6 +148,7 @@ init(async (launch, os) => {
{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, { path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) }, { path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) },
{ path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, { path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) },
{ path: '/@:username/pages/:pageName', name: 'page', props: true, component: () => import('../common/views/deck/deck.page-column.vue').then(m => m.default) },
]} ]}
: { path: '/', component: MkHome, children: [ : { path: '/', component: MkHome, children: [
{ path: '', name: 'index', component: MkHomeTimeline }, { path: '', name: 'index', component: MkHomeTimeline },
@ -171,12 +172,11 @@ init(async (launch, os) => {
{ path: '/i/follow-requests', component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }, { path: '/i/follow-requests', component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) },
{ path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) }, { path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) },
{ path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) }, { path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) },
{ path: '/@:user/pages/:page', component: () => import('../common/views/pages/page/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) }, { path: '/@:user/pages/:page', component: () => import('../common/views/pages/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) },
{ path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, { path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
]}, ]},
{ path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) }, { path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) },
{ path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) }, { path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) },
{ path: '/@:user/pages/:page', component: () => import('../common/views/pages/page/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) },
{ path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, { path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
{ path: '/i/messaging/group/:group', component: MkMessagingRoom }, { path: '/i/messaging/group/:group', component: MkMessagingRoom },
{ path: '/i/messaging/:user', component: MkMessagingRoom }, { path: '/i/messaging/:user', component: MkMessagingRoom },

View file

@ -20,6 +20,9 @@
<template v-if="!hover"><fa :icon="['far', 'folder']" fixed-width/></template> <template v-if="!hover"><fa :icon="['far', 'folder']" fixed-width/></template>
{{ folder.name }} {{ folder.name }}
</p> </p>
<p class="upload" v-if="$store.state.settings.uploadFolder == folder.id">
{{ $t('upload-folder') }}
</p>
</div> </div>
</template> </template>
@ -73,6 +76,14 @@ export default Vue.extend({
text: this.$t('@.delete'), text: this.$t('@.delete'),
icon: ['far', 'trash-alt'], icon: ['far', 'trash-alt'],
action: this.deleteFolder action: this.deleteFolder
}, null, {
type: 'nest',
text: this.$t('contextmenu.else-folders'),
menu: [{
type: 'item',
text: this.$t('contextmenu.set-as-upload-folder'),
action: this.setAsUploadFolder
}]
}], { }], {
closed: () => { closed: () => {
this.isContextmenuShowing = false; this.isContextmenuShowing = false;
@ -213,8 +224,37 @@ export default Vue.extend({
deleteFolder() { deleteFolder() {
this.$root.api('drive/folders/delete', { this.$root.api('drive/folders/delete', {
folderId: this.folder.id folderId: this.folder.id
}).then(() => {
if (this.$store.state.settings.uploadFolder === this.folder.id) {
this.$store.dispatch('settings/set', {
key: 'uploadFolder',
value: null
}); });
} }
}).catch(err => {
switch(err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
this.$root.dialog({
type: 'error',
title: this.$t('unable-to-delete'),
text: this.$t('has-child-files-or-folders')
});
break;
default:
this.$root.dialog({
type: 'error',
text: this.$t('unable-to-delete')
});
}
});
},
setAsUploadFolder() {
this.$store.dispatch('settings/set', {
key: 'uploadFolder',
value: this.folder.id
});
},
} }
}); });
</script> </script>
@ -264,4 +304,10 @@ export default Vue.extend({
margin-left 2px margin-left 2px
text-align left text-align left
> .upload
margin 4px 4px
font-size 0.8em
text-align right
color var(--desktopDriveFolderFg)
</style> </style>

View file

@ -24,7 +24,7 @@
</p> </p>
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<fa icon="quote-left"/> <fa icon="quote-left"/>
<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/> <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
<fa icon="quote-right"/> <fa icon="quote-right"/>
</router-link> </router-link>
</div> </div>
@ -40,7 +40,7 @@
</p> </p>
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
<fa icon="quote-left"/> <fa icon="quote-left"/>
<mfm :text="getNoteSummary(notification.note.renote)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.renote.emojis"/> <mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/>
<fa icon="quote-right"/> <fa icon="quote-right"/>
</router-link> </router-link>
</div> </div>
@ -55,7 +55,7 @@
</router-link> </router-link>
</p> </p>
<router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> <router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/> <mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
</router-link> </router-link>
</div> </div>
</template> </template>
@ -91,7 +91,7 @@
</router-link> </router-link>
</p> </p>
<router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> <router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/> <mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
</router-link> </router-link>
</div> </div>
</template> </template>
@ -105,7 +105,7 @@
</router-link> </router-link>
</p> </p>
<router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> <router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/> <mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
</router-link> </router-link>
</div> </div>
</template> </template>
@ -118,7 +118,7 @@
</router-link></p> </router-link></p>
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<fa icon="quote-left"/> <fa icon="quote-left"/>
<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/> <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
<fa icon="quote-right"/> <fa icon="quote-right"/>
</router-link> </router-link>
</div> </div>

View file

@ -62,7 +62,7 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div v-for="place in ['left', 'right']" :class="place"> <div v-for="place in ['left', 'right']" :class="place" :key="place">
<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="desktop"/> <component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="desktop"/>
</div> </div>
<div class="main"> <div class="main">
@ -392,7 +392,7 @@ export default Vue.extend({
margin 0 auto margin 0 auto
&:not(.side) &:not(.side)
@media (max-width 1200px) @media (max-width 1100px)
> *:not(.main) > *:not(.main)
display none display none

View file

@ -4,12 +4,12 @@
<div class="main"> <div class="main">
<component :is="src == 'list' ? 'mk-user-list-timeline' : 'x-core'" ref="tl" v-bind="options"> <component :is="src == 'list' ? 'mk-user-list-timeline' : 'x-core'" ref="tl" v-bind="options">
<header class="zahtxcqi"> <header class="zahtxcqi">
<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> <div :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</div>
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> <div :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</div>
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> <div :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</div>
<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> <div :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</div>
<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span> <div :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</div>
<span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</span> <div :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</div>
<div class="buttons"> <div class="buttons">
<button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="indicator" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button> <button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="indicator" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button>
<button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="indicator" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button> <button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="indicator" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button>
@ -200,18 +200,19 @@ export default Vue.extend({
&.shadow &.shadow
box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
.zahtxcqi header.zahtxcqi
display flex
flex-wrap wrap
padding 0 8px padding 0 8px
z-index 10 z-index 10
background var(--faceHeader) background var(--faceHeader)
box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow) box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow)
> *
flex-shrink 0
> .buttons > .buttons
position absolute margin-left auto
z-index 2
top 0
right 0
padding-right 8px
> button > button
padding 0 8px padding 0 8px
@ -244,8 +245,7 @@ export default Vue.extend({
height 2px height 2px
background var(--primary) background var(--primary)
> span > div:not(.buttons)
display inline-block
padding 0 10px padding 0 10px
line-height 42px line-height 42px
font-size 12px font-size 12px

View file

@ -28,7 +28,7 @@
<div class="fields" v-if="user.fields"> <div class="fields" v-if="user.fields">
<dl class="field" v-for="(field, i) in user.fields" :key="i"> <dl class="field" v-for="(field, i) in user.fields" :key="i">
<dt class="name"> <dt class="name">
<mfm :text="field.name" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/> <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/>
</dt> </dt>
<dd class="value"> <dd class="value">
<mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>

View file

@ -1,5 +1,6 @@
<template> <template>
<div class="lnctpgve"> <div class="lnctpgve">
<x-page v-if="user.pinnedPage" :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/>
<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/> <mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
<!--<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>--> <!--<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>-->
<div class="activity"> <div class="activity">
@ -21,13 +22,15 @@ import i18n from '../../../../i18n';
import XTimeline from './user.timeline.vue'; import XTimeline from './user.timeline.vue';
import XPhotos from './user.photos.vue'; import XPhotos from './user.photos.vue';
import XActivity from '../../../../common/views/components/activity.vue'; import XActivity from '../../../../common/views/components/activity.vue';
import XPage from '../../../../common/views/components/page/page.vue';
export default Vue.extend({ export default Vue.extend({
i18n: i18n(), i18n: i18n(),
components: { components: {
XTimeline, XTimeline,
XPhotos, XPhotos,
XActivity XActivity,
XPage,
}, },
props: { props: {
user: { user: {

View file

@ -118,6 +118,8 @@ export default define({
line-height 16px line-height 16px
font-weight bold font-weight bold
color var(--text) color var(--text)
overflow hidden
text-overflow ellipsis
> .username > .username
display block display block

View file

@ -129,6 +129,7 @@ init((launch, os) => {
{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) }, { path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) }, { path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) },
{ path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) }, { path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) },
{ path: '/@:username/pages/:pageName', name: 'page', props: true, component: () => import('../common/views/deck/deck.page-column.vue').then(m => m.default) },
]}] ]}]
: [ : [
{ path: '/', name: 'index', component: MkIndex }, { path: '/', name: 'index', component: MkIndex },
@ -163,7 +164,7 @@ init((launch, os) => {
{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) }, { path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) }, { path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
]}, ]},
{ path: '/@:user/pages/:page', component: UI, props: route => ({ component: () => import('../common/views/pages/page/page.vue').then(m => m.default), pageName: route.params.page, username: route.params.user }) }, { path: '/@:user/pages/:page', component: UI, props: route => ({ component: () => import('../common/views/pages/page.vue').then(m => m.default), pageName: route.params.page, username: route.params.user }) },
{ path: '/@:user/pages/:pageName/view-source', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initUser: route.params.user, initPageName: route.params.pageName }) }, { path: '/@:user/pages/:pageName/view-source', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initUser: route.params.user, initPageName: route.params.pageName }) },
{ path: '/notes/:note', component: MkNote }, { path: '/notes/:note', component: MkNote },
{ path: '/authorize-follow', component: MkFollow }, { path: '/authorize-follow', component: MkFollow },

View file

@ -10,7 +10,7 @@
</header> </header>
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<fa icon="quote-left"/> <fa icon="quote-left"/>
<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/> <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
<fa icon="quote-right"/> <fa icon="quote-right"/>
</router-link> </router-link>
</div> </div>
@ -26,7 +26,7 @@
</header> </header>
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
<fa icon="quote-left"/> <fa icon="quote-left"/>
<mfm :text="getNoteSummary(notification.note.renote)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.renote.emojis"/> <mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/>
<fa icon="quote-right"/> <fa icon="quote-right"/>
</router-link> </router-link>
</div> </div>
@ -64,7 +64,7 @@
</header> </header>
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> <router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
<fa icon="quote-left"/> <fa icon="quote-left"/>
<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/> <mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
<fa icon="quote-right"/> <fa icon="quote-right"/>
</router-link> </router-link>
</div> </div>

View file

@ -3,7 +3,7 @@
<template #header><span style="margin-right:4px;"><fa icon="cog"/></span>{{ $t('@.settings') }}</template> <template #header><span style="margin-right:4px;"><fa icon="cog"/></span>{{ $t('@.settings') }}</template>
<main> <main>
<div class="signed-in-as" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> <div class="signed-in-as" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
<mfm :text="$t('signed-in-as').replace('{}', name)" :should-break="false" :plain-text="true" :custom-emojis="$store.state.i.emojis"/> <mfm :text="$t('signed-in-as').replace('{}', name)" :plain="true" :custom-emojis="$store.state.i.emojis"/>
</div> </div>
<x-settings/> <x-settings/>

View file

@ -1,5 +1,6 @@
<template> <template>
<div class="wojmldye"> <div class="wojmldye">
<x-page class="page" v-if="user.pinnedPage" :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/>
<mk-note-detail class="note" v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/> <mk-note-detail class="note" v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
<ui-container :body-togglable="true"> <ui-container :body-togglable="true">
<template #header><fa :icon="['far', 'comments']"/>{{ $t('recent-notes') }}</template> <template #header><fa :icon="['far', 'comments']"/>{{ $t('recent-notes') }}</template>
@ -33,6 +34,7 @@ export default Vue.extend({
components: { components: {
XNotes, XNotes,
XPhotos, XPhotos,
XPage: () => import('../../../../common/views/components/page/page.vue').then(m => m.default),
XActivity: () => import('../../../../common/views/components/activity.vue').then(m => m.default) XActivity: () => import('../../../../common/views/components/activity.vue').then(m => m.default)
}, },
props: ['user'], props: ['user'],
@ -53,6 +55,12 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
.wojmldye .wojmldye
> .page
margin 0 0 8px 0
@media (min-width 500px)
margin 0 0 16px 0
> .note > .note
margin 0 0 8px 0 margin 0 0 8px 0

View file

@ -28,7 +28,7 @@
<div class="fields" v-if="user.fields"> <div class="fields" v-if="user.fields">
<dl class="field" v-for="(field, i) in user.fields" :key="i"> <dl class="field" v-for="(field, i) in user.fields" :key="i">
<dt class="name"> <dt class="name">
<mfm :text="field.name" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/> <mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/>
</dt> </dt>
<dd class="value"> <dd class="value">
<mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> <mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>

View file

@ -38,6 +38,9 @@ const defaultSettings = {
homeProfiles: {}, homeProfiles: {},
mobileHomeProfiles: {}, mobileHomeProfiles: {},
deckProfiles: {}, deckProfiles: {},
uploadFolder: null,
pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
pasteDialog: false,
}; };
const defaultDeviceSettings = { const defaultDeviceSettings = {
@ -60,6 +63,7 @@ const defaultDeviceSettings = {
soundVolume: 0.5, soundVolume: 0.5,
mediaVolume: 0.5, mediaVolume: 0.5,
lang: null, lang: null,
appTypeForce: 'auto',
debug: false, debug: false,
lightmode: false, lightmode: false,
loadRawImages: false, loadRawImages: false,

View file

@ -150,12 +150,10 @@ export function initDb(justBorrow = false, sync = false, log = false) {
options: { options: {
host: config.redis.host, host: config.redis.host,
port: config.redis.port, port: config.redis.port,
options: {
password: config.redis.pass, password: config.redis.pass,
prefix: config.redis.prefix, prefix: config.redis.prefix,
db: config.redis.db || 0 db: config.redis.db || 0
} }
}
} : false, } : false,
logging: log, logging: log,
logger: log ? new MyCustomLogger() : undefined, logger: log ? new MyCustomLogger() : undefined,

View file

@ -0,0 +1,50 @@
const defaultLocaleStringFormats: {[index: string]: string} = {
'weekday': 'narrow',
'era': 'narrow',
'year': 'numeric',
'month': 'numeric',
'day': 'numeric',
'hour': 'numeric',
'minute': 'numeric',
'second': 'numeric',
'timeZoneName': 'short'
};
function formatLocaleString(date: Date, format: string): string {
return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => {
if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) {
return date.toLocaleString(window.navigator.language, {[kind]: option ? option : defaultLocaleStringFormats[kind]});
} else {
return match;
}
});
}
function formatDateTimeString(date: Date, format: string): string {
return format
.replace(/yyyy/g, date.getFullYear().toString())
.replace(/yy/g, date.getFullYear().toString().slice(-2))
.replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long'}))
.replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short'}))
.replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2))
.replace(/M/g, (date.getMonth() + 1).toString())
.replace(/dd/g, (`0${date.getDate()}`).slice(-2))
.replace(/d/g, date.getDate().toString())
.replace(/HH/g, (`0${date.getHours()}`).slice(-2))
.replace(/H/g, date.getHours().toString())
.replace(/hh/g, (`0${(date.getHours() % 12) || 12}`).slice(-2))
.replace(/h/g, ((date.getHours() % 12) || 12).toString())
.replace(/mm/g, (`0${date.getMinutes()}`).slice(-2))
.replace(/m/g, date.getMinutes().toString())
.replace(/ss/g, (`0${date.getSeconds()}`).slice(-2))
.replace(/s/g, date.getSeconds().toString())
.replace(/tt/g, date.getHours() >= 12 ? 'PM' : 'AM');
}
export function formatTimeString(date: Date, format: string): string {
return format.replace(/\[(([^\[]|\[\])*)\]|(([yMdHhmst])\4{0,3})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => {
if (localeformat) return formatLocaleString(date, localeformat);
if (datetimeformat) return formatDateTimeString(date, datetimeformat);
return match;
});
}

View file

@ -40,6 +40,11 @@ export class Page {
@Column('boolean') @Column('boolean')
public alignCenter: boolean; public alignCenter: boolean;
@Column('boolean', {
default: false
})
public hideTitleWhenPinned: boolean;
@Column('varchar', { @Column('varchar', {
length: 32, length: 32,
}) })

View file

@ -1,6 +1,7 @@
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { id } from '../id'; import { id } from '../id';
import { User } from './user'; import { User } from './user';
import { Page } from './page';
@Entity() @Entity()
export class UserProfile { export class UserProfile {
@ -81,6 +82,11 @@ export class UserProfile {
}) })
public securityKeysAvailable: boolean; public securityKeysAvailable: boolean;
@Column('boolean', {
default: false,
})
public usePasswordLessLogin: boolean;
@Column('varchar', { @Column('varchar', {
length: 128, nullable: true, length: 128, nullable: true,
comment: 'The password hash of the User. It will be null if the origin of the user is local.' comment: 'The password hash of the User. It will be null if the origin of the user is local.'
@ -113,6 +119,18 @@ export class UserProfile {
}) })
public carefulBot: boolean; public carefulBot: boolean;
@Column({
...id(),
nullable: true
})
public pinnedPageId: Page['id'] | null;
@OneToOne(type => Page, {
onDelete: 'SET NULL'
})
@JoinColumn()
public pinnedPage: Page | null;
//#region Linking //#region Linking
@Column('boolean', { @Column('boolean', {
default: false, default: false,

View file

@ -71,6 +71,7 @@ export class PageRepository extends Repository<Page> {
title: page.title, title: page.title,
name: page.name, name: page.name,
summary: page.summary, summary: page.summary,
hideTitleWhenPinned: page.hideTitleWhenPinned,
alignCenter: page.alignCenter, alignCenter: page.alignCenter,
font: page.font, font: page.font,
eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImageId: page.eyeCatchingImageId,

View file

@ -1,7 +1,7 @@
import $ from 'cafy'; import $ from 'cafy';
import { EntityRepository, Repository, In } from 'typeorm'; import { EntityRepository, Repository, In } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '../entities/user'; import { User, ILocalUser, IRemoteUser } from '../entities/user';
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings } from '..'; import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages } from '..';
import { ensure } from '../../prelude/ensure'; import { ensure } from '../../prelude/ensure';
import config from '../../config'; import config from '../../config';
import { SchemaType } from '../../misc/schema'; import { SchemaType } from '../../misc/schema';
@ -155,7 +155,10 @@ export class UserRepository extends Repository<User> {
pinnedNotes: Notes.packMany(pins.map(pin => pin.noteId), meId, { pinnedNotes: Notes.packMany(pins.map(pin => pin.noteId), meId, {
detail: true detail: true
}), }),
pinnedPageId: profile!.pinnedPageId,
pinnedPage: profile!.pinnedPageId ? Pages.pack(profile!.pinnedPageId, meId) : null,
twoFactorEnabled: profile!.twoFactorEnabled, twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled securityKeys: profile!.twoFactorEnabled
? UserSecurityKeys.count({ ? UserSecurityKeys.count({
userId: user.id userId: user.id
@ -208,7 +211,6 @@ export class UserRepository extends Repository<User> {
select: ['id', 'name', 'lastUsed'] select: ['id', 'name', 'lastUsed']
}) })
: [] : []
} : {}), } : {}),
...(relation ? { ...(relation ? {

View file

@ -35,6 +35,14 @@ export const meta = {
} }
}, },
name: {
validator: $.optional.nullable.str,
default: null as any,
desc: {
'ja-JP': 'ファイル名(拡張子があるなら含めて)'
}
},
isSensitive: { isSensitive: {
validator: $.optional.either($.bool, $.str), validator: $.optional.either($.bool, $.str),
default: false, default: false,
@ -72,7 +80,7 @@ export const meta = {
export default define(meta, async (ps, user, app, file, cleanup) => { export default define(meta, async (ps, user, app, file, cleanup) => {
// Get 'name' parameter // Get 'name' parameter
let name = file.originalname; let name = ps.name || file.originalname;
if (name !== undefined && name !== null) { if (name !== undefined && name !== null) {
name = name.trim(); name = name.trim();
if (name.length === 0) { if (name.length === 0) {

View file

@ -0,0 +1,21 @@
import $ from 'cafy';
import define from '../../../define';
import { UserProfiles } from '../../../../../models';
export const meta = {
requireCredential: true,
secure: true,
params: {
value: {
validator: $.boolean
}
}
};
export default define(meta, async (ps, user) => {
await UserProfiles.update(user.id, {
usePasswordLessLogin: ps.value
});
});

View file

@ -10,7 +10,7 @@ import extractHashtags from '../../../../misc/extract-hashtags';
import * as langmap from 'langmap'; import * as langmap from 'langmap';
import { updateHashtag } from '../../../../services/update-hashtag'; import { updateHashtag } from '../../../../services/update-hashtag';
import { ApiError } from '../../error'; import { ApiError } from '../../error';
import { Users, DriveFiles, UserProfiles } from '../../../../models'; import { Users, DriveFiles, UserProfiles, Pages } from '../../../../models';
import { User } from '../../../../models/entities/user'; import { User } from '../../../../models/entities/user';
import { UserProfile } from '../../../../models/entities/user-profile'; import { UserProfile } from '../../../../models/entities/user-profile';
import { ensure } from '../../../../prelude/ensure'; import { ensure } from '../../../../prelude/ensure';
@ -125,6 +125,13 @@ export const meta = {
'ja-JP': 'アップロードするメディアをデフォルトで「閲覧注意」として設定するか' 'ja-JP': 'アップロードするメディアをデフォルトで「閲覧注意」として設定するか'
} }
}, },
pinnedPageId: {
validator: $.optional.nullable.type(ID),
desc: {
'ja-JP': 'ピン留めするページID'
}
}
}, },
errors: { errors: {
@ -150,7 +157,13 @@ export const meta = {
message: 'The file specified as a banner is not an image.', message: 'The file specified as a banner is not an image.',
code: 'BANNER_NOT_AN_IMAGE', code: 'BANNER_NOT_AN_IMAGE',
id: '75aedb19-2afd-4e6d-87fc-67941256fa60' id: '75aedb19-2afd-4e6d-87fc-67941256fa60'
} },
noSuchPage: {
message: 'No such page.',
code: 'NO_SUCH_PAGE',
id: '8e01b590-7eb9-431b-a239-860e086c408e'
},
} }
}; };
@ -203,6 +216,16 @@ export default define(meta, async (ps, user, app) => {
} }
} }
if (ps.pinnedPageId) {
const page = await Pages.findOne(ps.pinnedPageId);
if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage);
profileUpdates.pinnedPageId = page.id;
} else if (ps.pinnedPageId === null) {
profileUpdates.pinnedPageId = null;
}
//#region emojis/tags //#region emojis/tags
let emojis = [] as string[]; let emojis = [] as string[];

View file

@ -0,0 +1,49 @@
import $ from 'cafy';
import define from '../define';
import { ID } from '../../../misc/cafy-id';
import { publishMainStream } from '../../../services/stream';
import { Users, Pages } from '../../../models';
import { ApiError } from '../error';
export const meta = {
requireCredential: true,
secure: true,
params: {
pageId: {
validator: $.type(ID)
},
event: {
validator: $.str
},
var: {
validator: $.optional.nullable.any
}
},
errors: {
noSuchPage: {
message: 'No such page.',
code: 'NO_SUCH_PAGE',
id: '4a13ad31-6729-46b4-b9af-e86b265c2e74'
}
}
};
export default define(meta, async (ps, user) => {
const page = await Pages.findOne(ps.pageId);
if (page == null) {
throw new ApiError(meta.errors.noSuchPage);
}
publishMainStream(user.id, 'pageEvent', {
pageId: ps.pageId,
event: ps.event,
var: ps.var,
user: await Users.pack(user, page.userId, {
detail: true
})
});
});

View file

@ -57,6 +57,11 @@ export const meta = {
validator: $.optional.bool, validator: $.optional.bool,
default: false default: false
}, },
hideTitleWhenPinned: {
validator: $.optional.bool,
default: false
},
}, },
res: { res: {
@ -100,6 +105,7 @@ export default define(meta, async (ps, user) => {
userId: user.id, userId: user.id,
visibility: 'public', visibility: 'public',
alignCenter: ps.alignCenter, alignCenter: ps.alignCenter,
hideTitleWhenPinned: ps.hideTitleWhenPinned,
font: ps.font font: ps.font
})); }));

View file

@ -61,6 +61,10 @@ export const meta = {
alignCenter: { alignCenter: {
validator: $.optional.bool, validator: $.optional.bool,
}, },
hideTitleWhenPinned: {
validator: $.optional.bool,
},
}, },
errors: { errors: {
@ -113,6 +117,7 @@ export default define(meta, async (ps, user) => {
content: ps.content, content: ps.content,
variables: ps.variables, variables: ps.variables,
alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
font: ps.font === undefined ? page.font : ps.font, font: ps.font === undefined ? page.font : ps.font,
eyeCatchingImageId: ps.eyeCatchingImageId === null eyeCatchingImageId: ps.eyeCatchingImageId === null
? null ? null

View file

@ -72,6 +72,18 @@ export default async (ctx: Koa.BaseContext) => {
} }
} }
if (!profile.twoFactorEnabled) {
if (same) {
signin(ctx, user);
} else {
await fail(403, {
error: 'incorrect password'
});
}
return;
}
if (token) {
if (!same) { if (!same) {
await fail(403, { await fail(403, {
error: 'incorrect password' error: 'incorrect password'
@ -79,12 +91,6 @@ export default async (ctx: Koa.BaseContext) => {
return; return;
} }
if (!profile.twoFactorEnabled) {
signin(ctx, user);
return;
}
if (token) {
const verified = (speakeasy as any).totp.verify({ const verified = (speakeasy as any).totp.verify({
secret: profile.twoFactorSecret, secret: profile.twoFactorSecret,
encoding: 'base32', encoding: 'base32',
@ -101,6 +107,13 @@ export default async (ctx: Koa.BaseContext) => {
return; return;
} }
} else if (body.credentialId) { } else if (body.credentialId) {
if (!same && !profile.usePasswordLessLogin) {
await fail(403, {
error: 'incorrect password'
});
return;
}
const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex'); const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex');
const clientData = JSON.parse(clientDataJSON.toString('utf-8')); const clientData = JSON.parse(clientDataJSON.toString('utf-8'));
const challenge = await AttestationChallenges.findOne({ const challenge = await AttestationChallenges.findOne({
@ -163,6 +176,13 @@ export default async (ctx: Koa.BaseContext) => {
return; return;
} }
} else { } else {
if (!same && !profile.usePasswordLessLogin) {
await fail(403, {
error: 'incorrect password'
});
return;
}
const keys = await UserSecurityKeys.find({ const keys = await UserSecurityKeys.find({
userId: user.id userId: user.id
}); });
@ -201,5 +221,4 @@ export default async (ctx: Koa.BaseContext) => {
} }
await fail(); await fail();
return;
}; };

View file

@ -71,7 +71,7 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
]; ];
if (alts.webpublic) { if (alts.webpublic) {
webpublicKey = `${meta.objectStoragePrefix}/${uuid.v4()}.${alts.webpublic.ext}`; webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid.v4()}.${alts.webpublic.ext}`;
webpublicUrl = `${ baseUrl }/${ webpublicKey }`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
logger.info(`uploading webpublic: ${webpublicKey}`); logger.info(`uploading webpublic: ${webpublicKey}`);
@ -79,7 +79,7 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
} }
if (alts.thumbnail) { if (alts.thumbnail) {
thumbnailKey = `${meta.objectStoragePrefix}/${uuid.v4()}.${alts.thumbnail.ext}`; thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid.v4()}.${alts.thumbnail.ext}`;
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
logger.info(`uploading thumbnail: ${thumbnailKey}`); logger.info(`uploading thumbnail: ${thumbnailKey}`);
@ -104,8 +104,8 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
return await DriveFiles.save(file); return await DriveFiles.save(file);
} else { // use internal storage } else { // use internal storage
const accessKey = uuid.v4(); const accessKey = uuid.v4();
const thumbnailAccessKey = uuid.v4(); const thumbnailAccessKey = 'thumbnail-' + uuid.v4();
const webpublicAccessKey = uuid.v4(); const webpublicAccessKey = 'webpublic-' + uuid.v4();
const url = InternalStorage.saveFromPath(accessKey, path); const url = InternalStorage.saveFromPath(accessKey, path);

View file

@ -474,6 +474,20 @@ describe('API', () => {
assert.strictEqual(res.body.name, 'Lenna.png'); assert.strictEqual(res.body.name, 'Lenna.png');
})); }));
it('ファイルに名前を付けられる', async(async () => {
const alice = await signup({ username: 'alice' });
const res = await assert.request(server)
.post('/drive/files/create')
.field('i', alice.token)
.field('name', 'Belmond.png')
.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
expect(res).have.status(200);
expect(res.body).be.a('object');
expect(res.body).have.property('name').eql('Belmond.png');
}));
it('ファイル無しで怒られる', async(async () => { it('ファイル無しで怒られる', async(async () => {
const alice = await signup({ username: 'alice' }); const alice = await signup({ username: 'alice' });