Compare commits
122 Commits
Author | SHA1 | Date | |
---|---|---|---|
50d1500dfc | |||
94441f93a5 | |||
5f712fbf3c | |||
1c757f10e0 | |||
0508d5f643 | |||
d9986b7a2f | |||
3d79e7a136 | |||
52fb1237ec | |||
8a7197726e | |||
b7f5458684 | |||
52710f3810 | |||
a54de07260 | |||
aa2c8d101e | |||
1441fd93b9 | |||
4a585e8920 | |||
8c4245a09d | |||
e4af16989a | |||
5dc0944fe8 | |||
b4d24f4377 | |||
67be47b8db | |||
e382982d32 | |||
b09b74b5da | |||
c628bdb7a6 | |||
2fcf6fb0fd | |||
4f3fc9ffd0 | |||
15839a7399 | |||
26b3a14a63 | |||
f2f0799df1 | |||
6c99c32100 | |||
93d25a2a34 | |||
88f5ec59d7 | |||
586d3c4db7 | |||
f45fb56e15 | |||
8fe153c7c1 | |||
36a8720fbb | |||
9cbfdc94d9 | |||
091923764d | |||
dc39caed1e | |||
bcd7d1f007 | |||
40d4dc0474 | |||
02ac30c0d0 | |||
518bc92673 | |||
a5b92e316c | |||
828c7b66a0 | |||
93474eaa06 | |||
237f366aa2 | |||
714bcf28d5 | |||
420eeb4d68 | |||
bc6daf4a2e | |||
6f7832c09b | |||
bef67fa275 | |||
05d7198667 | |||
df0bfc14e5 | |||
3f28f7451f | |||
dbb9199d6f | |||
72cb3b03af | |||
d0085f00ed | |||
43734f027b | |||
bb903cab40 | |||
92f765bc47 | |||
742889a035 | |||
24453ebcc3 | |||
8b8ab1bf5c | |||
e9bc9b8675 | |||
eeaa27c7ca | |||
ccea1755fc | |||
c32a5d602b | |||
2a04f2ca4d | |||
37c80e8ef5 | |||
1dce62e42a | |||
ec222378c4 | |||
ac930a1c6a | |||
5ccd5ad56e | |||
67da90530b | |||
b7f3753615 | |||
d21d38509c | |||
a59e1c0345 | |||
7ab613b394 | |||
c00ab0fbe2 | |||
87451b1223 | |||
d2b61229a3 | |||
980584020a | |||
a43d0dafa5 | |||
d5c1e7e579 | |||
55bdf0d618 | |||
44f7c13ad4 | |||
7bd1a3c8ac | |||
4f1981df03 | |||
8689a998aa | |||
079bb8d722 | |||
65c0b6c7da | |||
84958af4ce | |||
c53b59914b | |||
8ffd9ab2d9 | |||
0305caf504 | |||
3012e4ffe0 | |||
585f3c3d3e | |||
f1d07dfbed | |||
cddfc55110 | |||
a2ce072ae7 | |||
439563c5d6 | |||
962617b4f4 | |||
4a202f0f7e | |||
6e6b12519a | |||
f5f7654f4b | |||
5ac4c48ad1 | |||
7e18fd18b0 | |||
fb30c479ea | |||
f40dcbfe13 | |||
8755b5f353 | |||
691482bb28 | |||
4248bb8ce0 | |||
a5653e33d3 | |||
072dc1c7e6 | |||
d76fceae85 | |||
86107b2710 | |||
a473768bef | |||
f7fe13a177 | |||
acd29d22eb | |||
c228155514 | |||
b601b98d5c | |||
1cd3419688 |
68
CHANGELOG.md
68
CHANGELOG.md
@ -1,6 +1,74 @@
|
||||
ChangeLog
|
||||
=========
|
||||
|
||||
12.13.0 (2020/02/18)
|
||||
-------------------
|
||||
### ✨Improvements
|
||||
* プロモーションノート機能を実装
|
||||
* インスタンス管理者が、重要なお知らせやユーザーにやってもらいたいアンケートなどをタイムラインの途中に挿入する機能
|
||||
* プロモーションされる期限を設定できる
|
||||
* 複数のプロモーションがある場合はランダムに選択されて表示される
|
||||
* ユーザーがプロモーションを個別に非表示にすることもできる
|
||||
* ハイライトインジェクション機能を実装
|
||||
* タイムラインの途中におすすめのノートを表示できる機能
|
||||
* 設定で有効/無効を切り替えられる
|
||||
* アクティビティウィジェットを実装
|
||||
* フォトウィジェットを実装
|
||||
* タイムラインの一番上までスクロールできるように
|
||||
* 管理者はモデレーターに変更できないように
|
||||
|
||||
### 🐛Fixes
|
||||
* admin/show-users APIがadminかつmoderator設定されているとき使えない問題を修正
|
||||
|
||||
12.12.0 (2020/02/17)
|
||||
-------------------
|
||||
### ✨Improvements
|
||||
* インスタンス情報ページを強化
|
||||
* インスタンス設定ページを強化
|
||||
* 設定ページをアカウント設定ページとクライアント設定ページに分離
|
||||
* UIの調整
|
||||
|
||||
12.11.0 (2020/02/16)
|
||||
-------------------
|
||||
### ✨Improvements
|
||||
* 投稿詳細ページで前後の投稿を見れるように
|
||||
* 自分のfollowersノートはRenoteできるように
|
||||
* 画像ダイアログを実装
|
||||
* フォロー申請ページの調整
|
||||
* 壁紙設定の強化
|
||||
* 画面が狭い状態でMisskeyを起動した場合でも、画面幅が広がったときにウィジェットを表示するように
|
||||
* 「もっと読み込む」したときの読み込み量を増量
|
||||
|
||||
### 🐛Fixes
|
||||
* 認証なしでグローバルTLにアクセスすると妙なエラーが返る問題を修正
|
||||
* API docが見れない問題を修正
|
||||
* 画面右上に当たり判定があるのを修正
|
||||
|
||||
12.10.0 (2020/02/15)
|
||||
-------------------
|
||||
### ✨Improvements
|
||||
* アンテナの受信ソースにグループを指定できるように
|
||||
* 時計ウィジェットを追加
|
||||
* ログアウトせずに新しいアカウントを追加できるように
|
||||
* フォントサイズを設定できるように
|
||||
* APIキー設定を実装
|
||||
|
||||
### 🐛Fixes
|
||||
* v12アップデート後にトップページアクセスでOops!になっちゃうのを修正
|
||||
* drive/files APIのパフォーマンスを改善
|
||||
|
||||
12.9.0 (2020/02/14)
|
||||
-------------------
|
||||
### ✨Improvements
|
||||
* カスタム絵文字の管理を強化
|
||||
* 動きのあるMFMを無効にするオプションを追加
|
||||
|
||||
### 🐛Fixes
|
||||
* タイムラインに自分の返信と自分への返信と投稿者自身への返信以外の返信が含まれている問題を修正
|
||||
* グループがない状態でグループチャットを開始しようとするとフリーズする問題を修正
|
||||
* 通知インジケーターがずれる問題を修正
|
||||
* AP: 投稿を削除したときに関係する投稿の削除アクティビティが連合されない問題を修正
|
||||
|
||||
12.8.0 (2020/02/13)
|
||||
--------------------
|
||||
### ✨Improvements
|
||||
|
@ -1,2 +1,39 @@
|
||||
---
|
||||
_lang_: "Deutsch"
|
||||
monthAndDay: "{day}/{month}"
|
||||
search: "Suchen"
|
||||
notifications: "Benachrichtigungen"
|
||||
username: "Benutzername"
|
||||
password: "Passwort"
|
||||
fetchingAsApObject: "Aus Fediverse holen"
|
||||
ok: "OK"
|
||||
gotIt: "Verstanden!"
|
||||
cancel: "Abbrechen"
|
||||
enterUsername: "Benutzername eingeben"
|
||||
renotedBy: "Renote von {user}"
|
||||
noNotes: "Keine Notizen"
|
||||
noNotifications: "Keine Benachrichtigungen"
|
||||
instance: "Instanz"
|
||||
settings: "Einstellungen"
|
||||
profile: "Profil"
|
||||
timeline: "Zeitleiste"
|
||||
noAccountDescription: "Keine Selbsteinführung"
|
||||
login: "Einloggen"
|
||||
loggingIn: "Einloggen in bearbeitung"
|
||||
logout: "Ausloggen"
|
||||
signup: "Registrieren"
|
||||
uploading: "Upload läuft"
|
||||
save: "Speichern"
|
||||
users: "Benutzer"
|
||||
addUser: "Benutzer hinzufügen"
|
||||
copyUsername: "Benutzernamen kopieren"
|
||||
selectUser: "Benutzer wählen"
|
||||
instances: "Instanz"
|
||||
mutedUsers: "Stummgestellte Benutzer"
|
||||
blockedUsers: "Blockierte Benutzer"
|
||||
noUsers: "Keine Benutzer"
|
||||
_widgets:
|
||||
notifications: "Benachrichtigungen"
|
||||
timeline: "Zeitleiste"
|
||||
_profile:
|
||||
username: "Benutzername"
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
_lang_: "English"
|
||||
introMisskey: "Welcome! Misskey is an open source distributed microblogging service.\nCreate \"notes\" to share what's happening or to tell everyone about you📡\nThen send \"reactions\" to respond quickly to everyone's notes👍\nLet's explore a new world🚀"
|
||||
introMisskey: "Welcome! Misskey is an open source and also decentralized microblogging service.\nWrite the \"notes\" to share what is happening now, or send out your own words to everyone 📡\nWith the \"reactions\", you can add your feelings to everyone's notes faster than anyone 👍\nLet's explore the new world 🚀"
|
||||
monthAndDay: "{month}/{day}"
|
||||
search: "Search"
|
||||
notifications: "Notifications"
|
||||
@ -252,9 +252,9 @@ tosUrl: "Terms of Service URL"
|
||||
thisYear: "Year"
|
||||
thisMonth: "Month"
|
||||
today: "Today"
|
||||
dayX: "{day} days"
|
||||
monthX: "{month} months"
|
||||
yearX: "{year} years"
|
||||
dayX: "{day}"
|
||||
monthX: "{month}"
|
||||
yearX: "{year} /"
|
||||
pages: "Pages"
|
||||
integration: "Integration"
|
||||
connectSerice: "Connect"
|
||||
@ -311,6 +311,7 @@ aboutMisskey: "About Misskey"
|
||||
aboutMisskeyText: "Misskey is an open-source software developed by syuilo since 2014."
|
||||
misskeyMembers: "It is currently developed an maintained by the members listed below:"
|
||||
misskeySource: "Source code is available here:"
|
||||
misskeyTranslation: "Help us with your contribution to translate Misskey:"
|
||||
misskeyDonate: "Help us to keep improving the software by donating here:"
|
||||
morePatrons: "We really appreciate the support of many other helpers not listed here. Thank you! 🥰"
|
||||
patrons: "Backers"
|
||||
@ -385,6 +386,32 @@ signinWith: "Sign in with {x}"
|
||||
tapSecurityKey: "Tap your security key"
|
||||
or: "Or"
|
||||
uiLanguage: "UI display language"
|
||||
groupInvited: "Invited to group"
|
||||
aboutX: "About {x}"
|
||||
useOsNativeEmojis: "Use the OS native Emojis"
|
||||
noGroups: "No groups"
|
||||
joinOrCreateGroup: "Get invited to join the groups or you can create your own group."
|
||||
noHistory: "No history items"
|
||||
disableAnimatedMfm: "Disable MFM which has animations"
|
||||
doing: "On my way"
|
||||
category: "Category"
|
||||
tags: "Tags"
|
||||
docSource: "Source of this document"
|
||||
createAccount: "Create account"
|
||||
existingAcount: "Existing accounts"
|
||||
regenerate: "Regenerate"
|
||||
fontSize: "Font size"
|
||||
noFollowRequests: "You don't have any pending follow requests"
|
||||
openImageInNewTab: "Open image in new tab"
|
||||
dashboard: "Dashboard"
|
||||
local: "Local"
|
||||
remote: "Remote"
|
||||
total: "Total"
|
||||
weekOverWeekChanges: "Weekly"
|
||||
dayOverDayChanges: "Daily"
|
||||
accessibility: "Accessibility"
|
||||
clinetSettings: "Client Settings"
|
||||
accountSettings: "Account Settings"
|
||||
_ago:
|
||||
unknown: "Unknown"
|
||||
future: "Future"
|
||||
@ -411,7 +438,7 @@ _tutorial:
|
||||
step3_1: "Finished setting up your profile?"
|
||||
step3_2: "The next step is to post a note. You can do this by pressing a pencil icon on the screen."
|
||||
step3_3: "Fill in the modal and press the button on the right top to post."
|
||||
step3_4: "Have nothing to say? Try \"I just started Misskey!\""
|
||||
step3_4: "Have nothing to say? Try \"just setting up my msky\"!"
|
||||
step4_1: "Finished posting your first note?"
|
||||
step4_2: "Hurray! Now your first note is displayed on your timeline."
|
||||
step5_1: "Now, let's try making your timeline more lively by following other people."
|
||||
@ -468,6 +495,7 @@ _antennaSources:
|
||||
homeTimeline: "Notes from following users"
|
||||
users: "Notes from specific users"
|
||||
userList: "Notes from specific list"
|
||||
userGroup: "Notes from users in the specified group"
|
||||
_weekday:
|
||||
sunday: "Sunday"
|
||||
monday: "Monday"
|
||||
@ -484,6 +512,7 @@ _widgets:
|
||||
trends: "Trending"
|
||||
clock: "Clock"
|
||||
rss: "RSS reader"
|
||||
activity: "Activity"
|
||||
_cw:
|
||||
hide: "Hide"
|
||||
show: "Load more"
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
_lang_: "Español"
|
||||
introMisskey: "¡Bienvenido/a! Misskey es un servicio de microblogging descentralizado de código abierto. Escribe \"notas\" para compartir lo que te ocurre ahora o para contar sobre ti a todos. 📡\nCon la función de \"reacciones\", puedes también añadir una reacción rápida a las notas de todos.👍\nExplora un nuevo mundo.🚀"
|
||||
introMisskey: "¡Bienvenido/a! Misskey es un servicio de microblogging descentralizado de código abierto.\nEscribe \"notas\" para compartir lo que te ocurre ahora o para contar sobre ti a todos 📡\nCon la función de \"reacciones\", puedes también añadir una reacción rápida a las notas de todos 👍\nExplora un nuevo mundo 🚀"
|
||||
monthAndDay: "{day}/{month}"
|
||||
search: "Buscar"
|
||||
notifications: "Notificaciones"
|
||||
@ -311,6 +311,7 @@ aboutMisskey: "Sobre Misskey"
|
||||
aboutMisskeyText: "Misskey es un software de código abierto, desarrollado por syuilo desde el 2014"
|
||||
misskeyMembers: "Es creado y mantenido por los miembros aquí listados:"
|
||||
misskeySource: "El código fuente está disponible aquí:"
|
||||
misskeyTranslation: "Ayúdanos con tu contribución para traducir Misskey:"
|
||||
misskeyDonate: "Puedes contribuir al desarrollo de Misskey donando aquí:"
|
||||
morePatrons: "Muchas más personas nos apoyan. Muchas gracias🥰"
|
||||
patrons: "Patrocinadores"
|
||||
@ -385,6 +386,32 @@ signinWith: "Inicie sesión con {x}"
|
||||
tapSecurityKey: "Toque la clave de seguridad"
|
||||
or: "O"
|
||||
uiLanguage: "Idioma de visualización de la interfaz"
|
||||
groupInvited: "Invitado al grupo"
|
||||
aboutX: "Acerca de {x}"
|
||||
useOsNativeEmojis: "Usa los emojis nativos de la plataforma"
|
||||
noGroups: "Sin grupos"
|
||||
joinOrCreateGroup: "Obtenga una invitación para unirse al grupos o puede crear su propio grupo."
|
||||
noHistory: "No hay datos en el historial"
|
||||
disableAnimatedMfm: "Deshabilitar MFM que tiene animaciones"
|
||||
doing: "Voy en camino"
|
||||
category: "Categoría"
|
||||
tags: "Etiqueta"
|
||||
docSource: "Fuente de este documento"
|
||||
createAccount: "Crear cuenta"
|
||||
existingAcount: "Cuentas existentes"
|
||||
regenerate: "Regenerar"
|
||||
fontSize: "Tamaño de la letra"
|
||||
noFollowRequests: "No hay solicitudes de seguimiento"
|
||||
openImageInNewTab: "Abrir imagen en nueva pestaña"
|
||||
dashboard: "Panel de control"
|
||||
local: "Local"
|
||||
remote: "Remoto"
|
||||
total: "Total"
|
||||
weekOverWeekChanges: "Dif semanal"
|
||||
dayOverDayChanges: "Dif diaria"
|
||||
accessibility: "Accesibilidad"
|
||||
clinetSettings: "Ajustes del cliente"
|
||||
accountSettings: "Ajustes de cuenta"
|
||||
_ago:
|
||||
unknown: "Desconocido"
|
||||
future: "Futuro"
|
||||
@ -468,6 +495,7 @@ _antennaSources:
|
||||
homeTimeline: "Notas de los usuarios que sigues"
|
||||
users: "Notas de un usuario o varios"
|
||||
userList: "Notas de los usuarios de una lista"
|
||||
userGroup: "Notas de los usuarios de una grupo"
|
||||
_weekday:
|
||||
sunday: "Domingo"
|
||||
monday: "Lunes"
|
||||
@ -484,6 +512,7 @@ _widgets:
|
||||
trends: "Tendencias"
|
||||
clock: "Reloj"
|
||||
rss: "Lector RSS"
|
||||
activity: "Actividad"
|
||||
_cw:
|
||||
hide: "Ocultar"
|
||||
show: "Ver más"
|
||||
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
_lang_: "Français"
|
||||
introMisskey: "Bienvenue! Misskey est un service de microblogage décentralisé open source.\nÉcrivez des «notes» pour partager ce qui vous arrive maintenant ou pour parler de vous à tout le monde 📡\nAvec la fonction «réactions», vous pouvez également ajouter une réaction rapide aux notes de chacun 👍\nExplorez un nouveau monde 🚀"
|
||||
monthAndDay: "{day}/{month}"
|
||||
search: "Rechercher"
|
||||
notifications: "Notifications"
|
||||
@ -163,6 +164,7 @@ noUsers: "Il n'y a aucun utilisateur·rice"
|
||||
editProfile: "Modifier votre profil"
|
||||
noteDeleteConfirm: "Confirmez-vous la suppression de cette note ?"
|
||||
pinLimitExceeded: "Je ne peux plus épingler"
|
||||
intro: "L'installation de Misskey est terminée! Créons le compte administrateur."
|
||||
done: "Terminé"
|
||||
processing: "Traitement en cours"
|
||||
preview: "Prévisualisation"
|
||||
@ -264,6 +266,7 @@ registration: "S'inscrire"
|
||||
enableRegistration: "Autoriser n’importe qui à s’enregistrés"
|
||||
invite: "Inviter"
|
||||
proxyRemoteFiles: "Proxy fichiers distants"
|
||||
proxyRemoteFilesDescription: "Si vous activez ce paramètre, les fichiers distants non stockés ou supprimés en raison d'une capacité excédentaire seront affichés via un proxy local et généreront une miniature. Cela n'affectera pas le stockage du serveur."
|
||||
driveCapacityPerLocalAccount: "Volume du Drive par utilisateur local"
|
||||
driveCapacityPerRemoteAccount: "Volume du Drive par utilisateur distant"
|
||||
inMb: "en mégaoctets"
|
||||
@ -271,6 +274,7 @@ iconUrl: "URL de l'image de l'icône"
|
||||
bannerUrl: "URL de l'image de la bannière"
|
||||
basicInfo: "Informations basiques"
|
||||
pinnedUsers: "Utilisateur·rice épinglé·e"
|
||||
pinnedUsersDescription: "Décrivez les utilisateurs que vous souhaitez définir sur la page \"Découvrir\" séparés par une nouvelle ligne"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "Activation de reCAPTCHA"
|
||||
recaptchaSiteKey: "Clé du site"
|
||||
@ -304,6 +308,12 @@ popularTags: "Mots-clés populaires"
|
||||
userList: "Listes"
|
||||
about: "Informations"
|
||||
aboutMisskey: "À propos de Misskey"
|
||||
aboutMisskeyText: "Misskey est un logiciel open source, développé par syuilo depuis 2014."
|
||||
misskeyMembers: "Il est développé et maintenu par les membres répertoriés ici:"
|
||||
misskeySource: "Le code source est disponible ici:"
|
||||
misskeyTranslation: "Aidez-nous avec votre contribution à traduire Misskey:"
|
||||
misskeyDonate: "Vous pouvez contribuer au développement de Misskey en faisant un don ici:"
|
||||
morePatrons: "Nous apprécions vraiment le soutien de nombreux autres les soutiens non répertoriés ici. Merci beaucoup à tous! 🥰"
|
||||
patrons: "Supporteurs"
|
||||
administrator: "Administrateur"
|
||||
token: "Jeton"
|
||||
@ -322,6 +332,7 @@ post: "Notes"
|
||||
posted: "Publié !"
|
||||
autoReloadWhenDisconnected: "Rechargement automatique lorsque le serveur se déconnecte"
|
||||
autoNoteWatch: "Surveiller automatique pour les notes"
|
||||
autoNoteWatchDescription: "Soyez informé des notes auxquelles vous avez réagi ou répondu."
|
||||
reduceUiAnimation: "Réduire l'animation de l'interface"
|
||||
share: "Partager"
|
||||
notFound: "Non trouvé"
|
||||
@ -375,6 +386,32 @@ signinWith: "Connectez-vous avec {x}"
|
||||
tapSecurityKey: "Touchez la clé de sécurité"
|
||||
or: "OU"
|
||||
uiLanguage: "Langue d'affichage de l'interface"
|
||||
groupInvited: "Invité au groupe"
|
||||
aboutX: "À propos de {x}"
|
||||
useOsNativeEmojis: "Utilisez les emojis natifs de la plateforme"
|
||||
noGroups: "Pas de groupes"
|
||||
joinOrCreateGroup: "Soyez invité à rejoindre les groupes ou vous pouvez créer votre propre groupe."
|
||||
noHistory: "Pas d'historique"
|
||||
disableAnimatedMfm: "Désactiver MFM qui a des animations"
|
||||
doing: "Attends une seconde"
|
||||
category: "Catégories"
|
||||
tags: "Étiquettes"
|
||||
docSource: "Source de ce document"
|
||||
createAccount: "Créer compte"
|
||||
existingAcount: "Comptes existants"
|
||||
regenerate: "Régénérer"
|
||||
fontSize: "Taille de la police"
|
||||
noFollowRequests: "Vous n'avez aucune demandes d'abonnement en attente"
|
||||
openImageInNewTab: "Ouvrir l'image dans un nouvel onglet"
|
||||
dashboard: "Tableau de bord"
|
||||
local: "Local"
|
||||
remote: "Distant"
|
||||
total: "Total"
|
||||
weekOverWeekChanges: "Diff hebdo"
|
||||
dayOverDayChanges: "Diff quotidien"
|
||||
accessibility: "Accessibilité"
|
||||
clinetSettings: "Paramètres du client"
|
||||
accountSettings: "Paramètres du compte"
|
||||
_ago:
|
||||
unknown: "Inconnu"
|
||||
future: "Futur"
|
||||
@ -393,6 +430,7 @@ _time:
|
||||
day: "j"
|
||||
_tutorial:
|
||||
title: "Comment utiliser Misskey"
|
||||
step1_1: "Bienvenue,"
|
||||
_2fa:
|
||||
alreadyRegistered: "Cette étape à déjà été complétée"
|
||||
registerDevice: "S’inscrire l'appareil"
|
||||
@ -437,6 +475,7 @@ _antennaSources:
|
||||
homeTimeline: "Notes de l'utilisateur auquel je m'abonne"
|
||||
users: "Notes des un ou plusieurs utilisateurs spécifiés"
|
||||
userList: "Notes pour les utilisateurs de la liste spécifiée"
|
||||
userGroup: "Notes pour les utilisateurs de la groupe spécifiée"
|
||||
_weekday:
|
||||
sunday: "Dimanche"
|
||||
monday: "Lundi"
|
||||
@ -453,6 +492,7 @@ _widgets:
|
||||
trends: "Tendances"
|
||||
clock: "Horloge"
|
||||
rss: "Lecteur de flux RSS"
|
||||
activity: "Activités"
|
||||
_cw:
|
||||
hide: "Masquer"
|
||||
show: "Voir plus"
|
||||
@ -565,6 +605,10 @@ _pages:
|
||||
inspector: "Inspecteur"
|
||||
content: "Bloc de page"
|
||||
variables: "Variables"
|
||||
variables-info: "Vous pouvez créer une page dynamique à l'aide de variables. En tapant le <b>{nom de variable}</b> dans le texte, vous pouvez y incorporer la valeur de la variable. Par exemple, si dans le texte <b>Bonjour {chose} monde!</b> la valeur de la variable (chose) est <b>ai</b>, le texte devient est <b>Bonjour ai monde!</b>."
|
||||
variables-info2: "L'évaluation des variables (le calcul des valeurs) se fait de haut en bas, donc l'variable ne peut pas se référer à une autre qui est en dessous. Par exemple, lorsque les variables <b>A、B、C</b> sont définies, <b>C</b> peut faire référence à <b>A</b> ou <b>B</b>, mais <b>A</b> ne peut pas faire référence à <b>B</b> ou <b>C</b>."
|
||||
variables-info3: "Pour recevoir une entrée utilisateur, ajoutez un bloc \"Entrée\" sur la page et définissez le nom des variables que vous souhaitez stocker dans le champ \"Nom de la variable\" (les variables seront créées automatiquement). Les actions seront exécutées en fonction de l'entrée utilisateur de ces variables."
|
||||
variables-info4: "Les fonctions vous permettent d'organiser le processus de calcul des valeurs sous une forme réutilisable. Pour créer une fonction, créez une variable de type \"fonction\". Une fonction peut avoir un slot (argument) et sa valeur peut être utilisée comme variable dans la fonction. Il existe également une fonction qui prend une fonction comme argument dans la norme AiScript (appelée fonction d'ordre supérieur). En plus des fonctions prédéfinies, elles peuvent être définies instantanément dans ces emplacements de fonction d'ordre supérieur."
|
||||
more-details: "Description"
|
||||
title: "Titre"
|
||||
url: "URL de page"
|
||||
|
@ -389,6 +389,34 @@ uiLanguage: "UIの表示言語"
|
||||
groupInvited: "グループに招待されました"
|
||||
aboutX: "{x}について"
|
||||
useOsNativeEmojis: "OSネイティブの絵文字を使用"
|
||||
noGroups: "グループがありません"
|
||||
joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。"
|
||||
noHistory: "履歴はありません"
|
||||
disableAnimatedMfm: "動きのあるMFMを無効にする"
|
||||
doing: "やっています"
|
||||
category: "カテゴリ"
|
||||
tags: "タグ"
|
||||
docSource: "このドキュメントのソース"
|
||||
createAccount: "アカウントを作成"
|
||||
existingAcount: "既存のアカウント"
|
||||
regenerate: "再生成"
|
||||
fontSize: "フォントサイズ"
|
||||
noFollowRequests: "フォロー申請はありません"
|
||||
openImageInNewTab: "画像を新しいタブで開く"
|
||||
dashboard: "ダッシュボード"
|
||||
local: "ローカル"
|
||||
remote: "リモート"
|
||||
total: "合計"
|
||||
weekOverWeekChanges: "前週比"
|
||||
dayOverDayChanges: "前日比"
|
||||
accessibility: "アクセシビリティ"
|
||||
clinetSettings: "クライアント設定"
|
||||
accountSettings: "アカウント設定"
|
||||
promotion: "プロモーション"
|
||||
promote: "プロモート"
|
||||
numberOfDays: "日数"
|
||||
hideThisNote: "このノートを非表示"
|
||||
showFeaturedNotesInTimeline: "タイムラインにおすすめのノートを表示する"
|
||||
|
||||
_ago:
|
||||
unknown: "謎"
|
||||
@ -479,6 +507,7 @@ _antennaSources:
|
||||
homeTimeline: "フォローしているユーザーのノート"
|
||||
users: "指定した一人または複数のユーザーのノート"
|
||||
userList: "指定したリストのユーザーのノート"
|
||||
userGroup: "指定したグループのユーザーのノート"
|
||||
|
||||
_weekday:
|
||||
sunday: "日曜日"
|
||||
@ -497,6 +526,8 @@ _widgets:
|
||||
trends: "トレンド"
|
||||
clock: "時計"
|
||||
rss: "RSSリーダー"
|
||||
activity: "アクティビティ"
|
||||
photos: "フォト"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
|
@ -1,2 +1,31 @@
|
||||
---
|
||||
_lang_: "ಕನ್ನಡ"
|
||||
introMisskey: "ಸ್ವಾಗತ! Misskey ಓಪನ್ ಸೋರ್ಸ್ ಒಕ್ಕೂಟ ಮೈಕ್ರೋಬ್ಲಾಗಿಂಗ್ ಸೇವೆಯಾಗಿದೆ.\n ಏನಾಗುತ್ತಿದೆ ಎಂಬುದನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ಅಥವಾ ನಿಮ್ಮ ಬಗ್ಗೆ ಎಲ್ಲರಿಗೂ ಹೇಳಲು \"ಟಿಪ್ಪಣಿ\"ಗಳನ್ನು ರಚಿಸಿ📡\n \"ಸ್ಪಂದನೆ\" ಕ್ರಿಯೆಯೊಂದಿಗೆ, ನೀವು ಎಲ್ಲರ ಟಿಪ್ಪಣಿಗಳಿಗೆ ತ್ವರಿತವಾಗಿ ಸ್ಪಂದನೆಗಳನ್ನು ಕೂಡ ಸೇರಿಸಬಹುದು.👍\n ಹೊಸ ಜಗತ್ತನ್ನು ಅನ್ವೇಷಿಸಿ🚀"
|
||||
monthAndDay: "{month}ನೇ ತಿಂಗಳ {day}ನೇ ದಿನ"
|
||||
search: "ಹುಡುಕು"
|
||||
notifications: "ಅಧಿಸೂಚನೆಗಳು"
|
||||
username: "ಬಳಕೆಹೆಸರು"
|
||||
password: "ಗುಪ್ತಪದ"
|
||||
fetchingAsApObject: "ಒಕ್ಕೂಟದಿಂದ ಪಡೆಯಲಾಗುತ್ತಿದೆ..."
|
||||
ok: "ಸರಿ"
|
||||
gotIt: "ಅರ್ಥವಾಯಿತು!"
|
||||
cancel: "ರದ್ದು"
|
||||
enterUsername: "ಬಳಕೆಹೆಸರನ್ನು ಭರ್ತಿ ಮಾಡಿ"
|
||||
renotedBy: "{user} ಪುನರಾವರ್ತಿಸಿದರು"
|
||||
noNotes: "ಟಿಪ್ಪಣಿಗಳಿಲ್ಲ"
|
||||
noNotifications: "ಅಧಿಸೂಚನೆಗಳಿಲ್ಲ"
|
||||
instance: "ನಿದರ್ಶನ"
|
||||
settings: "ಸಿದ್ಧತೆಗಳು"
|
||||
profile: "ಪ್ರೊಫೈಲು"
|
||||
timeline: "ಸಮಯಸಾಲು"
|
||||
noAccountDescription: "ಇವರು ಸ್ವಯಂ ಪರಿಚಯ ರಚಿಸಿಲ್ಲ"
|
||||
login: "ಪ್ರವೇಶ"
|
||||
loggingIn: "ಪ್ರವೇಶಿಸುತ್ತಾ..."
|
||||
logout: "ಆಚೆಗೆ"
|
||||
signup: "ನೋಂದಣಿ"
|
||||
instances: "ನಿದರ್ಶನ"
|
||||
_widgets:
|
||||
notifications: "ಅಧಿಸೂಚನೆಗಳು"
|
||||
timeline: "ಸಮಯಸಾಲು"
|
||||
_profile:
|
||||
username: "ಬಳಕೆಹೆಸರು"
|
||||
|
@ -106,8 +106,8 @@ customEmojis: "커스텀 이모지"
|
||||
emojiName: "이모지 이름"
|
||||
emojiUrl: "이모지 URL"
|
||||
addEmoji: "이모지 추가"
|
||||
cacheRemoteFiles: "원격 파일을 캐시"
|
||||
cacheRemoteFilesDescription: "이 설정을 해지하면 원격 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다."
|
||||
cacheRemoteFiles: "리모트 파일을 캐시"
|
||||
cacheRemoteFilesDescription: "이 설정을 해지하면 리모트 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다."
|
||||
flagAsBot: "나는 봇입니다"
|
||||
flagAsCat: "나는 고양이다냥"
|
||||
autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락"
|
||||
@ -154,7 +154,7 @@ clearQueue: "대기열 비우기"
|
||||
clearQueueConfirmTitle: "대기열을 비우시겠습니까?"
|
||||
clearQueueConfirmText: "대기열에 남아 있는 노트는 더이상 연합되지 않습니다. 보통의 경우 이 작업은 필요하지 않습니다."
|
||||
clearCachedFiles: "캐시 비우기"
|
||||
clearCachedFilesConfirm: "캐시된 원격 파일을 모두 삭제하시겠습니까?"
|
||||
clearCachedFilesConfirm: "캐시된 리모트 파일을 모두 삭제하시겠습니까?"
|
||||
blockedInstances: "차단된 인스턴스"
|
||||
blockedInstancesDescription: "차단하려는 인스턴스의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다."
|
||||
muteAndBlock: "뮤트 및 차단"
|
||||
@ -253,7 +253,7 @@ thisYear: "올해"
|
||||
thisMonth: "이번 달"
|
||||
today: "오늘"
|
||||
dayX: "{day}일"
|
||||
monthX: "{month}개월"
|
||||
monthX: "{month}월"
|
||||
yearX: "{year}년"
|
||||
pages: "페이지"
|
||||
integration: "연동"
|
||||
@ -265,8 +265,8 @@ disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리
|
||||
registration: "등록"
|
||||
enableRegistration: "신규 회원가입을 활성화"
|
||||
invite: "초대"
|
||||
proxyRemoteFiles: "원격 파일 프록시"
|
||||
proxyRemoteFilesDescription: "이 설정을 활성화할 경우, 저장되지 않았거나 저장용량 초과로 삭제된 원격 파일을 로컬에서 프록시하여 썸네일을 생성하게 됩니다. 서버의 스토리지에는 영향을 주지 않습니다."
|
||||
proxyRemoteFiles: "리모트 파일 프록시"
|
||||
proxyRemoteFilesDescription: "이 설정을 활성화할 경우, 저장되지 않았거나 저장용량 초과로 삭제된 리모트 파일을 로컬에서 프록시하여 썸네일을 생성하게 됩니다. 서버의 스토리지에는 영향을 주지 않습니다."
|
||||
driveCapacityPerLocalAccount: "로컬 유저 한 명당 드라이브 용량"
|
||||
driveCapacityPerRemoteAccount: "리모트 유저 한 명당 드라이브 용량"
|
||||
inMb: "메가바이트 단위"
|
||||
@ -308,9 +308,10 @@ popularTags: "인기 태그"
|
||||
userList: "리스트"
|
||||
about: "정보"
|
||||
aboutMisskey: "Misskey에 대하여"
|
||||
aboutMisskeyText: "Misskey는 syuilo에 의해 2014년부터 개발된 오픈 소스 소프트웨어입니다."
|
||||
misskeyMembers: "현재는 아래 멤버들에 의해 개발 및 유지보수 되고 있습니다."
|
||||
misskeySource: "소스 코드는 여기에서 보실 수 있습니다:"
|
||||
aboutMisskeyText: "Misskey는 syuilo에 의해서 2014년부터 개발되어 온 오픈소스 소프트웨어 입니다."
|
||||
misskeyMembers: "현재는 아래 멤버들에 의해 개발 및 유지보수 되고 있습니다:"
|
||||
misskeySource: "소스코드는 여기에 공개되어 있습니다:"
|
||||
misskeyTranslation: "Misskey의 번역을 함께해 주시길 부탁드립니다:"
|
||||
misskeyDonate: "Misskey에 기부하심으로써 개발에 도움을 주실 수 있습니다:"
|
||||
morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰"
|
||||
patrons: "후원자들"
|
||||
@ -385,6 +386,32 @@ signinWith: "{x}로 로그인"
|
||||
tapSecurityKey: "보안 키를 터치"
|
||||
or: "혹은"
|
||||
uiLanguage: "UI 표시 언어"
|
||||
groupInvited: "그룹에 초대되었습니다"
|
||||
aboutX: "{x}에 대하여"
|
||||
useOsNativeEmojis: "OS 기본 이모지를 사용"
|
||||
noGroups: "그룹이 없습니다"
|
||||
joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들어 보세요."
|
||||
noHistory: "기록이 없습니다"
|
||||
disableAnimatedMfm: "움직임이 있는 MFM을 비활성화"
|
||||
doing: "잠시만요"
|
||||
category: "카테고리"
|
||||
tags: "태그"
|
||||
docSource: "이 문서의 소스"
|
||||
createAccount: "계정 만들기"
|
||||
existingAcount: "기존 계정"
|
||||
regenerate: "다시 생성"
|
||||
fontSize: "글자 크기"
|
||||
noFollowRequests: "처리되지 않은 팔로우 요청이 없습니다"
|
||||
openImageInNewTab: "새 탭에서 이미지 열기"
|
||||
dashboard: "대시보드"
|
||||
local: "로컬"
|
||||
remote: "리모트"
|
||||
total: "합계"
|
||||
weekOverWeekChanges: "지난주보다"
|
||||
dayOverDayChanges: "어제보다"
|
||||
accessibility: "접근성"
|
||||
clinetSettings: "클라이언트 설정"
|
||||
accountSettings: "계정 설정"
|
||||
_ago:
|
||||
unknown: "알 수 없음"
|
||||
future: "미래"
|
||||
@ -468,6 +495,7 @@ _antennaSources:
|
||||
homeTimeline: "팔로우중인 유저의 노트"
|
||||
users: "지정한 한 명 혹은 여러 명의 유저의 노트"
|
||||
userList: "지정한 리스트에 속한 유저의 노트"
|
||||
userGroup: "지정한 그룹에 속한 유저의 노트"
|
||||
_weekday:
|
||||
sunday: "일요일"
|
||||
monday: "월요일"
|
||||
@ -484,6 +512,7 @@ _widgets:
|
||||
trends: "트렌드"
|
||||
clock: "시계"
|
||||
rss: "RSS 리더"
|
||||
activity: "활동"
|
||||
_cw:
|
||||
hide: "숨기기"
|
||||
show: "더 보기"
|
||||
|
@ -121,18 +121,23 @@ searchWith: "搜索:{q}"
|
||||
youHaveNoLists: "列表为空"
|
||||
followConfirm: "你确定要关注{name}吗?"
|
||||
proxyAccount: "代理账户"
|
||||
proxyAccountDescription: "代理帐户是在某些情况下充当用户的远程关注者的帐户。 例如,当一个用户列出一个远程用户时,如果没有人跟随该列出的用户,则该活动将不会传递到该实例,因此将代之以代理帐户。"
|
||||
host: "主机名"
|
||||
selectUser: "选择用户"
|
||||
recipient: "收件人"
|
||||
annotation: "注解"
|
||||
federation: "联合"
|
||||
instances: "实例"
|
||||
registeredAt: "初次观察"
|
||||
latestRequestSentAt: "上次发送的请求"
|
||||
latestRequestReceivedAt: "上次收到的请求"
|
||||
latestStatus: "最后状态"
|
||||
storageUsage: "已用存储"
|
||||
charts: "图表"
|
||||
perHour: "每小时"
|
||||
perDay: "每天"
|
||||
stopActivityDelivery: "停止发送活动"
|
||||
blockThisInstance: "阻止此实例"
|
||||
operations: "操作"
|
||||
software: "软件"
|
||||
version: "版本"
|
||||
@ -147,6 +152,7 @@ instanceInfo: "实例情报"
|
||||
statistics: "统计"
|
||||
clearQueue: "清除队列"
|
||||
clearQueueConfirmTitle: "确定清除队列?"
|
||||
clearQueueConfirmText: "未送达的帖子将不会送达。 通常,您不需要这样做。"
|
||||
clearCachedFiles: "清除缓存"
|
||||
clearCachedFilesConfirm: "确定要清除缓存文件?"
|
||||
blockedInstances: "被阻拦的实例"
|
||||
@ -273,6 +279,7 @@ recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
|
||||
recaptchaSiteKey: "网站密钥"
|
||||
recaptchaSecretKey: "reCAPTCHA 密钥"
|
||||
antennas: "天线"
|
||||
name: "名称"
|
||||
antennaKeywordsDescription: "使用空格分隔会产生AND规范,并且使用换行符分隔会产生OR规范"
|
||||
serviceworker: "ServiceWorker"
|
||||
@ -297,6 +304,7 @@ aboutMisskey: "关于 Misskey"
|
||||
aboutMisskeyText: "Misskey是由syuilo于2014年开发的开放源代码软件。"
|
||||
misskeyMembers: "现在由以下成员进行开发和维护:"
|
||||
misskeySource: "源代码在这里公开:"
|
||||
misskeyTranslation: "与我们一同进行Misskey的翻译工作:"
|
||||
misskeyDonate: "可以向 Misskey 进行捐款以支持开发:"
|
||||
morePatrons: "还有很多其他的人也在支持我们,非常感谢🥰"
|
||||
patrons: "支持者"
|
||||
@ -343,6 +351,50 @@ retype: "重新输入"
|
||||
noteOf: "{user}的帖子"
|
||||
inviteToGroup: "群组邀请"
|
||||
maxNoteTextLength: "帖子的字数限制"
|
||||
quoteAttached: "已引用"
|
||||
quoteQuestion: "是否将其作为引用附上?"
|
||||
newMessageExists: "新信息"
|
||||
onlyOneFileCanBeAttached: "只能添加一个附件"
|
||||
signinRequired: "请先登录"
|
||||
invitationCode: "邀请码"
|
||||
checking: "正在确认"
|
||||
available: "可用"
|
||||
unavailable: "不可用"
|
||||
usernameInvalidFormat: "可使用大小写英文字母、数字和下划线。"
|
||||
tooShort: "过短"
|
||||
tooLong: "过长"
|
||||
weakPassword: "密码强度:弱"
|
||||
normalPassword: "密码强度:中等"
|
||||
strongPassword: "密码强度:强"
|
||||
passwordMatched: "密码一致"
|
||||
passwordNotMatched: "密码不一致"
|
||||
or: "或者"
|
||||
uiLanguage: "显示语言"
|
||||
groupInvited: "群组招待"
|
||||
aboutX: "关于 {x}"
|
||||
useOsNativeEmojis: "使用OS原生Emoji"
|
||||
noGroups: "没有组"
|
||||
joinOrCreateGroup: "加入或者创建群组"
|
||||
noHistory: "没有历史记录"
|
||||
disableAnimatedMfm: "禁用MFM动画"
|
||||
doing: "正在进行"
|
||||
category: "类别"
|
||||
tags: "标签"
|
||||
createAccount: "注册账户"
|
||||
existingAcount: "现有的帐户"
|
||||
regenerate: "重新生成"
|
||||
fontSize: "字体大小"
|
||||
noFollowRequests: "没有关注申请"
|
||||
openImageInNewTab: "在新标签页中打开图片"
|
||||
dashboard: "Dashboard"
|
||||
local: "本地"
|
||||
remote: "远程"
|
||||
total: "总计"
|
||||
weekOverWeekChanges: "与前一周相比"
|
||||
dayOverDayChanges: "与前一日相比"
|
||||
accessibility: "辅助功能"
|
||||
clinetSettings: "客户端设置"
|
||||
accountSettings: "账户设置"
|
||||
_ago:
|
||||
unknown: "未知"
|
||||
future: "未来"
|
||||
@ -362,6 +414,7 @@ _time:
|
||||
_tutorial:
|
||||
title: "Misskey的使用方法"
|
||||
step1_1: "欢迎!"
|
||||
step7_3: "接下来,享受Misskey带来的乐趣吧🚀"
|
||||
_2fa:
|
||||
alreadyRegistered: "此设备已被注册"
|
||||
registerDevice: "注册设备"
|
||||
@ -392,6 +445,8 @@ _permissions:
|
||||
"write:user-groups": "操作用户组"
|
||||
_auth:
|
||||
permissionAsk: "这个应用程序需要以下权限"
|
||||
_antennaSources:
|
||||
all: "所有帖子"
|
||||
_weekday:
|
||||
sunday: "星期日"
|
||||
monday: "星期一"
|
||||
@ -408,6 +463,7 @@ _widgets:
|
||||
trends: "趋势"
|
||||
clock: "时钟"
|
||||
rss: "RSS阅读器"
|
||||
activity: "活动"
|
||||
_cw:
|
||||
hide: "隐藏"
|
||||
show: "查看更多"
|
||||
@ -439,13 +495,27 @@ _poll:
|
||||
_visibility:
|
||||
public: "公开"
|
||||
home: "首页"
|
||||
homeDescription: "仅发送至首页的时间线"
|
||||
followers: "关注者"
|
||||
followersDescription: "仅发送至关注者"
|
||||
specified: "指定用户"
|
||||
specifiedDescription: "仅发送至指定用户"
|
||||
localOnly: "仅限本地"
|
||||
_postForm:
|
||||
replyPlaceholder: "回复这个帖子..."
|
||||
quotePlaceholder: "引用这个帖子..."
|
||||
_placeholders:
|
||||
a: "现在如何?"
|
||||
b: "发生了什么?"
|
||||
c: "你有什么想法?"
|
||||
d: "你想要发布些什么吗?"
|
||||
e: "请写下来吧"
|
||||
f: "等待您的发布..."
|
||||
_profile:
|
||||
name: "名称"
|
||||
username: "用户名"
|
||||
description: "个人简介"
|
||||
youCanIncludeHashtags: "您可以包含一个哈希标签。"
|
||||
metadata: "额外信息"
|
||||
metadataLabel: "标签"
|
||||
metadataContent: "内容"
|
||||
@ -467,6 +537,7 @@ _instanceCharts:
|
||||
users: "用户数量:增加/减少"
|
||||
usersTotal: "用户总数"
|
||||
notes: "帖子:增加/减少"
|
||||
notesTotal: "帖子:总数"
|
||||
ff: "关注/被关注:数量变化"
|
||||
ffTotal: "关注/被关注:总数"
|
||||
cacheSize: "缓存大小:增加/减少"
|
||||
|
28
migration/1581695816408-user-group-antenna.ts
Normal file
28
migration/1581695816408-user-group-antenna.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class userGroupAntenna1581695816408 implements MigrationInterface {
|
||||
name = 'userGroupAntenna1581695816408'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ADD "userGroupJoiningId" character varying(32)`, undefined);
|
||||
await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`, undefined);
|
||||
await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'users', 'list', 'group')`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum" USING "src"::"text"::"antenna_src_enum"`, undefined);
|
||||
await queryRunner.query(`DROP TYPE "antenna_src_enum_old"`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[]`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb" FOREIGN KEY ("userGroupJoiningId") REFERENCES "user_group_joining"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb"`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying array NOT NULL DEFAULT '{}'`, undefined);
|
||||
await queryRunner.query(`CREATE TYPE "antenna_src_enum_old" AS ENUM('home', 'all', 'users', 'list')`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum_old" USING "src"::"text"::"antenna_src_enum_old"`, undefined);
|
||||
await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined);
|
||||
await queryRunner.query(`ALTER TYPE "antenna_src_enum_old" RENAME TO "antenna_src_enum"`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "userGroupJoiningId"`, undefined);
|
||||
}
|
||||
|
||||
}
|
14
migration/1581708415836-drive-user-folder-id-index.ts
Normal file
14
migration/1581708415836-drive-user-folder-id-index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class driveUserFolderIdIndex1581708415836 implements MigrationInterface {
|
||||
name = 'driveUserFolderIdIndex1581708415836'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`CREATE INDEX "IDX_55720b33a61a7c806a8215b825" ON "drive_file" ("userId", "folderId", "id") `, undefined);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_55720b33a61a7c806a8215b825"`, undefined);
|
||||
}
|
||||
|
||||
}
|
28
migration/1581979837262-promo.ts
Normal file
28
migration/1581979837262-promo.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class promo1581979837262 implements MigrationInterface {
|
||||
name = 'promo1581979837262'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`CREATE TABLE "promo_note" ("noteId" character varying(32) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "REL_e263909ca4fe5d57f8d4230dd5" UNIQUE ("noteId"), CONSTRAINT "PK_e263909ca4fe5d57f8d4230dd5c" PRIMARY KEY ("noteId"))`, undefined);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_83f0862e9bae44af52ced7099e" ON "promo_note" ("userId") `, undefined);
|
||||
await queryRunner.query(`CREATE TABLE "promo_read" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_61917c1541002422b703318b7c9" PRIMARY KEY ("id"))`, undefined);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_9657d55550c3d37bfafaf7d4b0" ON "promo_read" ("userId") `, undefined);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2882b8a1a07c7d281a98b6db16" ON "promo_read" ("userId", "noteId") `, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4"`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05"`, undefined);
|
||||
await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`, undefined);
|
||||
await queryRunner.query(`DROP INDEX "IDX_2882b8a1a07c7d281a98b6db16"`, undefined);
|
||||
await queryRunner.query(`DROP INDEX "IDX_9657d55550c3d37bfafaf7d4b0"`, undefined);
|
||||
await queryRunner.query(`DROP TABLE "promo_read"`, undefined);
|
||||
await queryRunner.query(`DROP INDEX "IDX_83f0862e9bae44af52ced7099e"`, undefined);
|
||||
await queryRunner.query(`DROP TABLE "promo_note"`, undefined);
|
||||
}
|
||||
|
||||
}
|
14
migration/1582019042083-featured-injecttion.ts
Normal file
14
migration/1582019042083-featured-injecttion.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class featuredInjecttion1582019042083 implements MigrationInterface {
|
||||
name = 'featuredInjecttion1582019042083'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "injectFeaturedNote" boolean NOT NULL DEFAULT true`, undefined);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "injectFeaturedNote"`, undefined);
|
||||
}
|
||||
|
||||
}
|
54
package.json
54
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
||||
"version": "12.8.0",
|
||||
"version": "12.13.0",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -30,16 +30,16 @@
|
||||
"lodash": "^4.17.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "7.5.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.26",
|
||||
"@fortawesome/free-brands-svg-icons": "5.12.0",
|
||||
"@fortawesome/free-regular-svg-icons": "5.12.0",
|
||||
"@fortawesome/free-solid-svg-icons": "5.12.0",
|
||||
"@elastic/elasticsearch": "7.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.27",
|
||||
"@fortawesome/free-brands-svg-icons": "5.12.1",
|
||||
"@fortawesome/free-regular-svg-icons": "5.12.1",
|
||||
"@fortawesome/free-solid-svg-icons": "5.12.1",
|
||||
"@fortawesome/vue-fontawesome": "0.1.9",
|
||||
"@juggle/resize-observer": "3.0.2",
|
||||
"@koa/cors": "3.0.0",
|
||||
"@koa/multer": "2.0.2",
|
||||
"@koa/router": "8.0.6",
|
||||
"@koa/router": "8.0.8",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.12.0",
|
||||
"@types/cbor": "5.0.0",
|
||||
@ -54,7 +54,7 @@
|
||||
"@types/js-yaml": "3.12.2",
|
||||
"@types/jsdom": "12.2.4",
|
||||
"@types/katex": "0.11.0",
|
||||
"@types/koa": "2.11.0",
|
||||
"@types/koa": "2.11.1",
|
||||
"@types/koa-bodyparser": "4.3.0",
|
||||
"@types/koa-compress": "2.0.9",
|
||||
"@types/koa-cors": "0.0.0",
|
||||
@ -69,7 +69,7 @@
|
||||
"@types/lolex": "5.1.0",
|
||||
"@types/markdown-it": "0.0.9",
|
||||
"@types/mocha": "7.0.1",
|
||||
"@types/node": "13.7.0",
|
||||
"@types/node": "13.7.1",
|
||||
"@types/nodemailer": "6.4.0",
|
||||
"@types/nprogress": "0.2.0",
|
||||
"@types/oauth": "0.9.1",
|
||||
@ -80,7 +80,7 @@
|
||||
"@types/qrcode": "1.3.4",
|
||||
"@types/random-seed": "0.3.3",
|
||||
"@types/ratelimiter": "2.1.28",
|
||||
"@types/redis": "2.8.14",
|
||||
"@types/redis": "2.8.15",
|
||||
"@types/rename": "1.0.1",
|
||||
"@types/request": "2.48.4",
|
||||
"@types/request-promise-native": "1.0.17",
|
||||
@ -95,18 +95,18 @@
|
||||
"@types/tmp": "0.1.0",
|
||||
"@types/uuid": "3.4.7",
|
||||
"@types/web-push": "3.3.0",
|
||||
"@types/webpack": "4.41.3",
|
||||
"@types/webpack": "4.41.6",
|
||||
"@types/webpack-stream": "3.2.10",
|
||||
"@types/websocket": "1.0.0",
|
||||
"@types/ws": "7.2.1",
|
||||
"@typescript-eslint/parser": "2.18.0",
|
||||
"@typescript-eslint/parser": "2.19.2",
|
||||
"agentkeepalive": "4.1.0",
|
||||
"animejs": "3.1.0",
|
||||
"apexcharts": "3.15.3",
|
||||
"apexcharts": "3.15.6",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "4.0.2",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.610.0",
|
||||
"aws-sdk": "2.617.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bull": "3.12.1",
|
||||
"cafy": "15.2.1",
|
||||
@ -115,7 +115,7 @@
|
||||
"chalk": "3.0.0",
|
||||
"chart.js": "2.9.3",
|
||||
"cli-highlight": "2.1.4",
|
||||
"commander": "4.1.0",
|
||||
"commander": "4.1.1",
|
||||
"content-disposition": "0.5.3",
|
||||
"crc-32": "1.2.0",
|
||||
"css-loader": "3.4.2",
|
||||
@ -128,7 +128,7 @@
|
||||
"eventemitter3": "4.0.0",
|
||||
"feed": "4.1.0",
|
||||
"fibers": "4.0.2",
|
||||
"file-type": "13.1.2",
|
||||
"file-type": "14.1.2",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"glob": "7.1.6",
|
||||
"gulp": "4.0.2",
|
||||
@ -144,12 +144,12 @@
|
||||
"hard-source-webpack-plugin": "0.13.1",
|
||||
"html-minifier": "4.0.0",
|
||||
"http-signature": "1.3.1",
|
||||
"https-proxy-agent": "4.0.0",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"is-root": "2.1.0",
|
||||
"is-svg": "4.2.1",
|
||||
"js-yaml": "3.13.1",
|
||||
"jsdom": "16.0.1",
|
||||
"jsdom": "16.1.0",
|
||||
"json5": "2.1.1",
|
||||
"json5-loader": "3.0.0",
|
||||
"jsrsasign": "8.0.12",
|
||||
@ -198,16 +198,16 @@
|
||||
"randomcolor": "0.5.4",
|
||||
"ratelimiter": "3.4.0",
|
||||
"recaptcha-promise": "0.1.3",
|
||||
"reconnecting-websocket": "4.3.0",
|
||||
"redis": "2.8.0",
|
||||
"reconnecting-websocket": "4.4.0",
|
||||
"redis": "3.0.2",
|
||||
"redis-lock": "0.1.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rename": "1.0.4",
|
||||
"request": "2.88.0",
|
||||
"request": "2.88.2",
|
||||
"request-promise-native": "1.0.8",
|
||||
"request-stats": "3.0.0",
|
||||
"require-all": "3.0.0",
|
||||
"rimraf": "3.0.1",
|
||||
"rimraf": "3.0.2",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.25.0",
|
||||
@ -221,7 +221,7 @@
|
||||
"style-loader": "1.1.3",
|
||||
"summaly": "2.3.1",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "4.21.1",
|
||||
"systeminformation": "4.21.2",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"terser-webpack-plugin": "2.3.4",
|
||||
"textarea-caret": "3.1.0",
|
||||
@ -245,7 +245,7 @@
|
||||
"vue-cropperjs": "4.0.1",
|
||||
"vue-i18n": "8.15.3",
|
||||
"vue-json-pretty": "1.6.3",
|
||||
"vue-loader": "15.8.3",
|
||||
"vue-loader": "15.9.0",
|
||||
"vue-marquee-text-component": "1.1.1",
|
||||
"vue-meta": "2.3.2",
|
||||
"vue-prism-component": "1.1.1",
|
||||
@ -256,10 +256,10 @@
|
||||
"vue-template-compiler": "2.6.11",
|
||||
"vuedraggable": "2.23.2",
|
||||
"vuex": "3.1.2",
|
||||
"vuex-persistedstate": "2.7.0",
|
||||
"vuex-persistedstate": "2.7.1",
|
||||
"web-push": "3.4.3",
|
||||
"webpack": "4.41.5",
|
||||
"webpack-cli": "3.3.10",
|
||||
"webpack": "4.41.6",
|
||||
"webpack-cli": "3.3.11",
|
||||
"websocket": "1.0.31",
|
||||
"ws": "7.2.1",
|
||||
"xev": "2.0.1"
|
||||
|
@ -25,6 +25,7 @@
|
||||
<input type="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/>
|
||||
</div>
|
||||
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
|
||||
<x-clock v-if="isDesktop" class="clock"/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -43,11 +44,11 @@
|
||||
<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
|
||||
</button>
|
||||
<div class="divider"></div>
|
||||
<router-link class="item index" active-class="active" to="/" exact v-if="$store.getters.isSignedIn">
|
||||
<fa :icon="faHome" fixed-width/><span class="text">{{ $t('timeline') }}</span>
|
||||
</router-link>
|
||||
<button class="item _button index active" @click="top()" v-if="$route.name === 'index'">
|
||||
<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
|
||||
</button>
|
||||
<router-link class="item index" active-class="active" to="/" exact v-else>
|
||||
<fa :icon="faHome" fixed-width/><span class="text">{{ $t('home') }}</span>
|
||||
<fa :icon="faHome" fixed-width/><span class="text">{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}</span>
|
||||
</router-link>
|
||||
<button class="item _button notifications" @click="notificationsOpen = !notificationsOpen" ref="notificationButton" v-if="$store.getters.isSignedIn">
|
||||
<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
|
||||
@ -57,13 +58,13 @@
|
||||
<fa :icon="faComments" fixed-width/><span class="text">{{ $t('messaging') }}</span>
|
||||
<i v-if="$store.state.i.hasUnreadMessagingMessage"><fa :icon="faCircle"/></i>
|
||||
</router-link>
|
||||
<router-link class="item" active-class="active" to="/my/follow-requests" v-if="$store.getters.isSignedIn && $store.state.i.isLocked">
|
||||
<fa :icon="faUserClock" fixed-width/><span class="text">{{ $t('followRequests') }}</span>
|
||||
<i v-if="$store.state.i.pendingReceivedFollowRequestsCount"><fa :icon="faCircle"/></i>
|
||||
</router-link>
|
||||
<router-link class="item" active-class="active" to="/my/drive" v-if="$store.getters.isSignedIn">
|
||||
<fa :icon="faCloud" fixed-width/><span class="text">{{ $t('drive') }}</span>
|
||||
</router-link>
|
||||
<router-link class="item" active-class="active" to="/my/follow-requests" v-if="$store.getters.isSignedIn && $store.state.i.isLocked">
|
||||
<fa :icon="faUserClock" fixed-width/><span class="text">{{ $t('followRequests') }}</span>
|
||||
<i v-if="$store.state.i.hasPendingReceivedFollowRequest"><fa :icon="faCircle"/></i>
|
||||
</router-link>
|
||||
<div class="divider"></div>
|
||||
<router-link class="item" active-class="active" to="/featured">
|
||||
<fa :icon="faFireAlt" fixed-width/><span class="text">{{ $t('featured') }}</span>
|
||||
@ -86,11 +87,14 @@
|
||||
<fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span>
|
||||
<i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes)"><fa :icon="faCircle"/></i>
|
||||
</button>
|
||||
<router-link class="item" active-class="active" to="/settings">
|
||||
<fa :icon="faCog" fixed-width/><span class="text">{{ $t('settings') }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</nav>
|
||||
</transition>
|
||||
|
||||
<div class="contents" ref="contents">
|
||||
<div class="contents" ref="contents" :class="{ wallpaper }">
|
||||
<main ref="main">
|
||||
<div class="content">
|
||||
<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
|
||||
@ -107,7 +111,7 @@
|
||||
|
||||
<div class="widgets">
|
||||
<div ref="widgets" :class="{ edit: widgetsEditMode }">
|
||||
<template v-if="enableWidgets && $store.getters.isSignedIn">
|
||||
<template v-if="isDesktop && $store.getters.isSignedIn">
|
||||
<template v-if="widgetsEditMode">
|
||||
<mk-button primary @click="addWidget" class="add"><fa :icon="faPlus"/></mk-button>
|
||||
<x-draggable
|
||||
@ -136,8 +140,9 @@
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button v-if="$store.getters.isSignedIn" class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="$store.state.i.hasUnreadSpecifiedNotes || $store.state.i.pendingReceivedFollowRequestsCount || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement"><fa :icon="faCircle"/></i></button>
|
||||
<button v-if="$store.getters.isSignedIn" class="button home _button" :disabled="$route.path === '/'" @click="$router.push('/')"><fa :icon="faHome"/></button>
|
||||
<button class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadSpecifiedNotes || $store.state.i.hasPendingReceivedFollowRequest || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement)"><fa :icon="faCircle"/></i></button>
|
||||
<button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button>
|
||||
<button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button>
|
||||
<button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton2"><fa :icon="notificationsOpen ? faTimes : faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
|
||||
<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
|
||||
</div>
|
||||
@ -162,10 +167,13 @@ import { search } from './scripts/search';
|
||||
import contains from './scripts/contains';
|
||||
import MkToast from './components/toast.vue';
|
||||
|
||||
const DESKTOP_THRESHOLD = 1100;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
XClock: () => import('./components/header-clock.vue').then(m => m.default),
|
||||
XNotifications: () => import('./components/notifications.vue').then(m => m.default),
|
||||
MkButton: () => import('./components/ui/button.vue').then(m => m.default),
|
||||
XDraggable: () => import('vuedraggable'),
|
||||
@ -184,9 +192,10 @@ export default Vue.extend({
|
||||
searchQuery: '',
|
||||
searchWait: false,
|
||||
widgetsEditMode: false,
|
||||
enableWidgets: window.innerWidth >= 1100,
|
||||
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
|
||||
canBack: false,
|
||||
disconnectedDialog: null as Promise<void> | null,
|
||||
wallpaper: localStorage.getItem('wallpaper') != null,
|
||||
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer
|
||||
};
|
||||
},
|
||||
@ -224,6 +233,10 @@ export default Vue.extend({
|
||||
el.removeEventListener('mousedown', this.onMousedown);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isDesktop() {
|
||||
if (this.isDesktop) this.adjustWidgetsWidth();
|
||||
}
|
||||
},
|
||||
|
||||
@ -272,17 +285,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// https://stackoverflow.com/questions/33891709/when-flexbox-items-wrap-in-column-mode-container-does-not-grow-its-width
|
||||
if (this.enableWidgets) {
|
||||
const adjustWidgetsWidth = () => {
|
||||
const lastChild = this.$refs.widgets.children[this.$refs.widgets.children.length - 1];
|
||||
if (lastChild == null) return;
|
||||
|
||||
const width = lastChild.offsetLeft + 300 + 16;
|
||||
this.$refs.widgets.style.width = width + 'px';
|
||||
};
|
||||
setInterval(adjustWidgetsWidth, 1000);
|
||||
}
|
||||
if (this.isDesktop) this.adjustWidgetsWidth();
|
||||
|
||||
const adjustTitlePosition = () => {
|
||||
this.$refs.title.style.left = (this.$refs.main.getBoundingClientRect().left - this.$refs.nav.offsetWidth) + 'px';
|
||||
@ -293,13 +296,36 @@ export default Vue.extend({
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
adjustTitlePosition();
|
||||
});
|
||||
|
||||
|
||||
ro.observe(this.$refs.contents);
|
||||
|
||||
window.addEventListener('resize', adjustTitlePosition);
|
||||
window.addEventListener('resize', adjustTitlePosition, { passive: true });
|
||||
|
||||
if (!this.isDesktop) {
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
|
||||
}, { passive: true });
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
adjustWidgetsWidth() {
|
||||
// https://stackoverflow.com/questions/33891709/when-flexbox-items-wrap-in-column-mode-container-does-not-grow-its-width
|
||||
const adjust = () => {
|
||||
const lastChild = this.$refs.widgets.children[this.$refs.widgets.children.length - 1];
|
||||
if (lastChild == null) return;
|
||||
|
||||
const width = lastChild.offsetLeft + 300 + 16;
|
||||
this.$refs.widgets.style.width = width + 'px';
|
||||
};
|
||||
setInterval(adjust, 1000);
|
||||
setTimeout(adjust, 100);
|
||||
},
|
||||
|
||||
top() {
|
||||
window.scroll({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
|
||||
help() {
|
||||
this.$router.push('/docs/keyboard-shortcut');
|
||||
},
|
||||
@ -348,7 +374,7 @@ export default Vue.extend({
|
||||
const accountItems = accounts.map(account => ({
|
||||
type: 'user',
|
||||
user: account,
|
||||
action: () => { this.switchAccount(account) }
|
||||
action: () => { this.switchAccount(account); }
|
||||
}));
|
||||
|
||||
this.$root.menu({
|
||||
@ -359,15 +385,28 @@ export default Vue.extend({
|
||||
avatar: this.$store.state.i,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('settings'),
|
||||
text: this.$t('accountSettings'),
|
||||
to: '/my/settings',
|
||||
icon: faCog,
|
||||
}, null, {
|
||||
type: 'item',
|
||||
text: this.$t('addAcount'),
|
||||
}, null, ...accountItems, {
|
||||
icon: faPlus,
|
||||
action: () => { this.addAcount() },
|
||||
}], ...accountItems],
|
||||
text: this.$t('addAcount'),
|
||||
action: () => {
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
text: this.$t('existingAcount'),
|
||||
action: () => { this.addAcount(); },
|
||||
}, {
|
||||
text: this.$t('createAccount'),
|
||||
action: () => { this.createAccount(); },
|
||||
}],
|
||||
align: 'left',
|
||||
fixed: true,
|
||||
width: 240,
|
||||
source: ev.currentTarget || ev.target,
|
||||
});
|
||||
},
|
||||
}]],
|
||||
align: 'left',
|
||||
fixed: true,
|
||||
width: 240,
|
||||
@ -379,9 +418,14 @@ export default Vue.extend({
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
type: 'link',
|
||||
text: this.$t('statistics'),
|
||||
to: '/instance/stats',
|
||||
icon: faChartBar,
|
||||
text: this.$t('dashboard'),
|
||||
to: '/instance',
|
||||
icon: faTachometerAlt,
|
||||
}, null, {
|
||||
type: 'link',
|
||||
text: this.$t('settings'),
|
||||
to: '/instance/settings',
|
||||
icon: faCog,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('customEmojis'),
|
||||
@ -397,11 +441,6 @@ export default Vue.extend({
|
||||
text: this.$t('files'),
|
||||
to: '/instance/files',
|
||||
icon: faCloud,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('monitor'),
|
||||
to: '/instance/monitor',
|
||||
icon: faTachometerAlt,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('jobQueue'),
|
||||
@ -417,11 +456,6 @@ export default Vue.extend({
|
||||
text: this.$t('announcements'),
|
||||
to: '/instance/announcements',
|
||||
icon: faBroadcastTower,
|
||||
}, null, {
|
||||
type: 'link',
|
||||
text: this.$t('general'),
|
||||
to: '/instance',
|
||||
icon: faCog,
|
||||
}],
|
||||
align: 'left',
|
||||
fixed: true,
|
||||
@ -507,9 +541,25 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
async switchAccount(account) {
|
||||
const token = this.$store.state.device.accounts.find(x => x.id === account.id).token;
|
||||
this.$root.api('i', {}, token).then(i => {
|
||||
async createAccount() {
|
||||
this.$root.new(await import('./components/signup-dialog.vue').then(m => m.default)).$once('signup', res => {
|
||||
this.$store.dispatch('addAcount', res);
|
||||
this.switchAccountWithToken(res.i);
|
||||
});
|
||||
},
|
||||
|
||||
async switchAccount(account: any) {
|
||||
const token = this.$store.state.device.accounts.find((x: any) => x.id === account.id).token;
|
||||
this.switchAccountWithToken(token);
|
||||
},
|
||||
|
||||
switchAccountWithToken(token: string) {
|
||||
this.$root.dialog({
|
||||
type: 'waiting',
|
||||
iconOnly: true
|
||||
});
|
||||
|
||||
this.$root.api('i', {}, token).then((i: any) => {
|
||||
this.$store.dispatch('switchAccount', {
|
||||
...i,
|
||||
token: token
|
||||
@ -556,6 +606,9 @@ export default Vue.extend({
|
||||
'calendar',
|
||||
'rss',
|
||||
'trends',
|
||||
'clock',
|
||||
'activity',
|
||||
'photos',
|
||||
];
|
||||
|
||||
this.$root.menu({
|
||||
@ -753,7 +806,7 @@ export default Vue.extend({
|
||||
position: relative;
|
||||
|
||||
> input {
|
||||
width: 210px;
|
||||
width: 220px;
|
||||
box-sizing: border-box;
|
||||
margin-right: 8px;
|
||||
padding: 0 12px 0 42px;
|
||||
@ -786,6 +839,10 @@ export default Vue.extend({
|
||||
border-radius: 100%;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
> .clock {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -831,6 +888,7 @@ export default Vue.extend({
|
||||
width: $nav-width;
|
||||
height: 100vh;
|
||||
padding: 16px 0;
|
||||
padding-bottom: calc(3.7rem + 24px);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
background: var(--navBg);
|
||||
@ -844,6 +902,7 @@ export default Vue.extend({
|
||||
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
|
||||
width: $nav-icon-only-width;
|
||||
padding: 8px 0;
|
||||
padding-bottom: calc(3.7rem + 24px);
|
||||
|
||||
> .divider {
|
||||
margin: 8px auto;
|
||||
@ -891,12 +950,24 @@ export default Vue.extend({
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--navHoverFg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--navActive);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: inherit;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
background: var(--navBg);
|
||||
border-top: solid 1px var(--divider);
|
||||
border-right: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
|
||||
padding-left: 0;
|
||||
width: 100%;
|
||||
@ -933,6 +1004,10 @@ export default Vue.extend({
|
||||
margin: 0 auto;
|
||||
min-width: 0;
|
||||
|
||||
&.wallpaper {
|
||||
background: var(--wallpaperOverlay);
|
||||
}
|
||||
|
||||
> main {
|
||||
width: $main-width;
|
||||
min-width: $main-width;
|
||||
@ -1129,7 +1204,7 @@ export default Vue.extend({
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: var(--accent);
|
||||
color: var(--indicator);
|
||||
font-size: 16px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
24
src/client/assets/redoc.html
Normal file
24
src/client/assets/redoc.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Misskey API</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||
|
||||
<!--
|
||||
ReDoc doesn't change outer page styles
|
||||
-->
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url='/api.json'></redoc>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
||||
</body>
|
||||
</html>
|
BIN
src/client/assets/remove.png
Normal file
BIN
src/client/assets/remove.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 424 B |
156
src/client/components/analog-clock.vue
Normal file
156
src/client/components/analog-clock.vue
Normal file
@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<svg class="mbcofsoe" viewBox="0 0 10 10" preserveAspectRatio="none">
|
||||
<circle v-for="(angle, i) in graduations"
|
||||
:cx="5 + (Math.sin(angle) * (5 - graduationsPadding))"
|
||||
:cy="5 - (Math.cos(angle) * (5 - graduationsPadding))"
|
||||
:r="i % 5 == 0 ? 0.125 : 0.05"
|
||||
:fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor"
|
||||
:key="i"/>
|
||||
|
||||
<line
|
||||
:x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))"
|
||||
:y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))"
|
||||
:x2="5 + (Math.sin(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
|
||||
:y2="5 - (Math.cos(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
|
||||
:stroke="sHandColor"
|
||||
stroke-width="0.05"/>
|
||||
|
||||
<line
|
||||
:x1="5 - (Math.sin(mAngle) * (mHandLengthRatio * handsTailLength))"
|
||||
:y1="5 + (Math.cos(mAngle) * (mHandLengthRatio * handsTailLength))"
|
||||
:x2="5 + (Math.sin(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
|
||||
:y2="5 - (Math.cos(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
|
||||
:stroke="mHandColor"
|
||||
stroke-width="0.1"/>
|
||||
|
||||
<line
|
||||
:x1="5 - (Math.sin(hAngle) * (hHandLengthRatio * handsTailLength))"
|
||||
:y1="5 + (Math.cos(hAngle) * (hHandLengthRatio * handsTailLength))"
|
||||
:x2="5 + (Math.sin(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
|
||||
:y2="5 - (Math.cos(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
|
||||
:stroke="hHandColor"
|
||||
stroke-width="0.1"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
smooth: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
now: new Date(),
|
||||
enabled: true,
|
||||
|
||||
graduationsPadding: 0.5,
|
||||
handsPadding: 1,
|
||||
handsTailLength: 0.7,
|
||||
hHandLengthRatio: 0.75,
|
||||
mHandLengthRatio: 1,
|
||||
sHandLengthRatio: 1,
|
||||
|
||||
computedStyle: getComputedStyle(document.documentElement)
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
dark(): boolean {
|
||||
return tinycolor(this.computedStyle.getPropertyValue('--bg')).isDark();
|
||||
},
|
||||
|
||||
majorGraduationColor(): string {
|
||||
return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
|
||||
},
|
||||
minorGraduationColor(): string {
|
||||
return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
},
|
||||
|
||||
sHandColor(): string {
|
||||
return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
|
||||
},
|
||||
mHandColor(): string {
|
||||
return tinycolor(this.computedStyle.getPropertyValue('--fg')).toHexString();
|
||||
},
|
||||
hHandColor(): string {
|
||||
return tinycolor(this.computedStyle.getPropertyValue('--accent')).toHexString();
|
||||
},
|
||||
|
||||
ms(): number {
|
||||
return this.now.getMilliseconds() * (this.smooth ? 1 : 0);
|
||||
},
|
||||
s(): number {
|
||||
return this.now.getSeconds();
|
||||
},
|
||||
m(): number {
|
||||
return this.now.getMinutes();
|
||||
},
|
||||
h(): number {
|
||||
return this.now.getHours();
|
||||
},
|
||||
|
||||
hAngle(): number {
|
||||
return Math.PI * (this.h % 12 + (this.m + (this.s + this.ms / 1000) / 60) / 60) / 6;
|
||||
},
|
||||
mAngle(): number {
|
||||
return Math.PI * (this.m + (this.s + this.ms / 1000) / 60) / 30;
|
||||
},
|
||||
sAngle(): number {
|
||||
return Math.PI * (this.s + this.ms / 1000) / 30;
|
||||
},
|
||||
|
||||
graduations(): any {
|
||||
const angles = [];
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const angle = Math.PI * i / 30;
|
||||
angles.push(angle);
|
||||
}
|
||||
|
||||
return angles;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const update = () => {
|
||||
if (this.enabled) {
|
||||
this.tick();
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
};
|
||||
update();
|
||||
|
||||
this.$store.subscribe((mutation, state) => {
|
||||
if (mutation.type !== 'device/set') return;
|
||||
|
||||
if (mutation?.payload?.key !== 'theme') return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.computedStyle = getComputedStyle(document.documentElement);
|
||||
}, 250);
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.enabled = false;
|
||||
},
|
||||
|
||||
methods: {
|
||||
tick() {
|
||||
this.now = new Date();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mbcofsoe {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
@ -2,7 +2,7 @@
|
||||
<div class="swhvrteh" @contextmenu.prevent="() => {}">
|
||||
<ol class="users" ref="suggests" v-if="type === 'user'">
|
||||
<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1" class="user">
|
||||
<img class="avatar" :src="user.avatarUrl" alt=""/>
|
||||
<img class="avatar" :src="user.avatarUrl"/>
|
||||
<span class="name">
|
||||
<mk-user-name :user="user" :key="user.id"/>
|
||||
</span>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<sequential-entrance class="sqadhkmv" ref="list" :direction="direction" :reversed="reversed">
|
||||
<template v-for="(item, i) in items">
|
||||
<slot :item="item" :i="i"></slot>
|
||||
<div class="separator" :key="item.id + '_date'" v-if="i != items.length - 1 && new Date(item.createdAt).getDate() != new Date(items[i + 1].createdAt).getDate()">
|
||||
<div class="separator" :key="item.id + '_date'" v-if="showDate(i, item)">
|
||||
<p class="date">
|
||||
<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
|
||||
<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span>
|
||||
@ -52,6 +52,16 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
showDate(i, item) {
|
||||
return (
|
||||
i != this.items.length - 1 &&
|
||||
new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() &&
|
||||
!item._prId_ &&
|
||||
!this.items[i + 1]._prId_ &&
|
||||
!item._featuredId_ &&
|
||||
!this.items[i + 1]._featuredId_);
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.$refs.list.focus();
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="mk-dialog" :class="{ iconOnly }">
|
||||
<transition name="bg-fade" appear>
|
||||
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
|
||||
<div class="bg" ref="bg" @click="onBgClick" v-if="show"></div>
|
||||
</transition>
|
||||
<transition name="dialog" appear @after-leave="() => { destroyDom(); }">
|
||||
<transition :name="$store.state.device.animation ? 'dialog' : ''" appear @after-leave="() => { destroyDom(); }">
|
||||
<div class="main" ref="main" v-if="show">
|
||||
<template v-if="type == 'signin'">
|
||||
<mk-signin/>
|
||||
|
@ -12,7 +12,7 @@
|
||||
preload="metadata"
|
||||
controls
|
||||
v-else-if="detail && is === 'video'"/>
|
||||
<img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/>
|
||||
<img :src="file.thumbnailUrl" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/>
|
||||
<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
|
||||
<fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>
|
||||
|
||||
|
@ -83,17 +83,14 @@ export default Vue.extend({
|
||||
} else {
|
||||
this.$root.menu({
|
||||
items: [{
|
||||
type: 'item',
|
||||
text: this.$t('rename'),
|
||||
icon: faICursor,
|
||||
action: this.rename
|
||||
}, {
|
||||
type: 'item',
|
||||
text: this.file.isSensitive ? this.$t('unmarkAsSensitive') : this.$t('markAsSensitive'),
|
||||
icon: this.file.isSensitive ? faEye : faEyeSlash,
|
||||
action: this.toggleSensitive
|
||||
}, null, {
|
||||
type: 'item',
|
||||
text: this.$t('copyUrl'),
|
||||
icon: faLink,
|
||||
action: this.copyUrl
|
||||
@ -105,7 +102,6 @@ export default Vue.extend({
|
||||
icon: faDownload,
|
||||
download: this.file.name
|
||||
}, null, {
|
||||
type: 'item',
|
||||
text: this.$t('delete'),
|
||||
icon: faTrashAlt,
|
||||
action: this.deleteFile
|
||||
@ -113,11 +109,9 @@ export default Vue.extend({
|
||||
type: 'nest',
|
||||
text: this.$t('contextmenu.else-files'),
|
||||
menu: [{
|
||||
type: 'item',
|
||||
text: this.$t('contextmenu.set-as-avatar'),
|
||||
action: this.setAsAvatar
|
||||
}, {
|
||||
type: 'item',
|
||||
text: this.$t('contextmenu.set-as-banner'),
|
||||
action: this.setAsBanner
|
||||
}]
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="mjndxjcg _panel">
|
||||
<img src="https://xn--931a.moe/assets/error.png" alt=""/>
|
||||
<img src="https://xn--931a.moe/assets/error.png" class="_ghost"/>
|
||||
<p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p>
|
||||
<mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button>
|
||||
</div>
|
||||
@ -45,8 +45,6 @@ export default Vue.extend({
|
||||
height: 150px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
101
src/client/components/header-clock.vue
Normal file
101
src/client/components/header-clock.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="eqryymyo">
|
||||
<div class="header">
|
||||
<time ref="time" class="_ghost">
|
||||
<span class="yyyymmdd">{{ yyyy }}/{{ mm }}/{{ dd }}</span>
|
||||
<br>
|
||||
<span class="hhnn">{{ hh }}<span :style="{ visibility: now.getSeconds() % 2 == 0 ? 'visible' : 'hidden' }">:</span>{{ nn }}</span>
|
||||
</time>
|
||||
</div>
|
||||
<div class="content _panel _ghost">
|
||||
<mk-clock/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import MkClock from './analog-clock.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
MkClock
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
now: new Date(),
|
||||
clock: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
yyyy(): number {
|
||||
return this.now.getFullYear();
|
||||
},
|
||||
mm(): string {
|
||||
return ('0' + (this.now.getMonth() + 1)).slice(-2);
|
||||
},
|
||||
dd(): string {
|
||||
return ('0' + this.now.getDate()).slice(-2);
|
||||
},
|
||||
hh(): string {
|
||||
return ('0' + this.now.getHours()).slice(-2);
|
||||
},
|
||||
nn(): string {
|
||||
return ('0' + this.now.getMinutes()).slice(-2);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.tick();
|
||||
this.clock = setInterval(this.tick, 1000);
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.clock);
|
||||
},
|
||||
methods: {
|
||||
tick() {
|
||||
this.now = new Date();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.eqryymyo {
|
||||
display: inline-block;
|
||||
overflow: visible;
|
||||
|
||||
> .header {
|
||||
padding: 0 12px;
|
||||
padding-top: 4px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-family: Lucida Console, Courier, monospace;
|
||||
|
||||
&:hover + .content {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
> time {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
height: 48px;
|
||||
|
||||
> .yyyymmdd {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
opacity: 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: auto;
|
||||
right: 0;
|
||||
margin: 16px 0 0 0;
|
||||
padding: 16px;
|
||||
width: 230px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
</style>
|
54
src/client/components/image-viewer.vue
Normal file
54
src/client/components/image-viewer.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }">
|
||||
<img class="xubzgfga" ref="img" :src="image.url" :alt="image.name" :title="image.name" @click="close" tabindex="-1"/>
|
||||
</x-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../i18n';
|
||||
import XModal from './modal.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
XModal,
|
||||
},
|
||||
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.img.focus();
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.$refs.modal.close();
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.xubzgfga {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
margin: auto;
|
||||
cursor: zoom-out;
|
||||
image-orientation: from-image;
|
||||
}
|
||||
</style>
|
@ -1,8 +1,91 @@
|
||||
<template>
|
||||
<div class="mk-instance-stats">
|
||||
<div class="zbcjwnqg">
|
||||
<div class="stats" v-if="info">
|
||||
<div class="_panel">
|
||||
<div>
|
||||
<b><fa :icon="faUser"/>{{ $t('users') }}</b>
|
||||
<small>{{ $t('local') }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<dl class="total">
|
||||
<dt>{{ $t('total') }}</dt>
|
||||
<dd>{{ info.originalUsersCount | number }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: usersLocalDoD > 0 }">
|
||||
<dt>{{ $t('dayOverDayChanges') }}</dt>
|
||||
<dd>{{ usersLocalDoD | number }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: usersLocalWoW > 0 }">
|
||||
<dt>{{ $t('weekOverWeekChanges') }}</dt>
|
||||
<dd>{{ usersLocalWoW | number }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<div>
|
||||
<b><fa :icon="faUser"/>{{ $t('users') }}</b>
|
||||
<small>{{ $t('remote') }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<dl class="total">
|
||||
<dt>{{ $t('total') }}</dt>
|
||||
<dd>{{ (info.usersCount - info.originalUsersCount) | number }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: usersRemoteDoD > 0 }">
|
||||
<dt>{{ $t('dayOverDayChanges') }}</dt>
|
||||
<dd>{{ usersRemoteDoD | number }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: usersRemoteWoW > 0 }">
|
||||
<dt>{{ $t('weekOverWeekChanges') }}</dt>
|
||||
<dd>{{ usersRemoteWoW | number }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<div>
|
||||
<b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
|
||||
<small>{{ $t('local') }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<dl class="total">
|
||||
<dt>{{ $t('total') }}</dt>
|
||||
<dd>{{ info.originalNotesCount | number }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: notesLocalDoD > 0 }">
|
||||
<dt>{{ $t('dayOverDayChanges') }}</dt>
|
||||
<dd>{{ notesLocalDoD | number }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: notesLocalWoW > 0 }">
|
||||
<dt>{{ $t('weekOverWeekChanges') }}</dt>
|
||||
<dd>{{ notesLocalWoW | number }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_panel">
|
||||
<div>
|
||||
<b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b>
|
||||
<small>{{ $t('remote') }}</small>
|
||||
</div>
|
||||
<div>
|
||||
<dl class="total">
|
||||
<dt>{{ $t('total') }}</dt>
|
||||
<dd>{{ (info.notesCount - info.originalNotesCount) | number }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: notesRemoteDoD > 0 }">
|
||||
<dt>{{ $t('dayOverDayChanges') }}</dt>
|
||||
<dd>{{ notesRemoteDoD | number }}</dd>
|
||||
</dl>
|
||||
<dl class="diff" :class="{ inc: notesRemoteWoW > 0 }">
|
||||
<dt>{{ $t('weekOverWeekChanges') }}</dt>
|
||||
<dd>{{ notesRemoteWoW | number }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faChartBar"/> {{ $t('statistics') }}</div>
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<div class="_content" style="margin-top: -8px;">
|
||||
<div class="selects" style="display: flex;">
|
||||
<mk-select v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||
<optgroup :label="$t('federation')">
|
||||
@ -40,10 +123,10 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faChartBar } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faChartBar, faUser, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import Chart from 'chart.js';
|
||||
import i18n from '../../i18n';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import i18n from '../i18n';
|
||||
import MkSelect from './ui/select.vue';
|
||||
|
||||
const chartLimit = 90;
|
||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||
@ -59,24 +142,27 @@ const alpha = (hex, a) => {
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
metaInfo() {
|
||||
return {
|
||||
title: `${this.$t('statistics')} | ${this.$t('instance')}`
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
MkSelect
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
info: null,
|
||||
notesLocalWoW: 0,
|
||||
notesLocalDoD: 0,
|
||||
notesRemoteWoW: 0,
|
||||
notesRemoteDoD: 0,
|
||||
usersLocalWoW: 0,
|
||||
usersLocalDoD: 0,
|
||||
usersRemoteWoW: 0,
|
||||
usersRemoteDoD: 0,
|
||||
now: null,
|
||||
chart: null,
|
||||
chartInstance: null,
|
||||
chartSrc: 'notes',
|
||||
chartSpan: 'hour',
|
||||
faChartBar
|
||||
faChartBar, faUser, faPencilAlt
|
||||
}
|
||||
},
|
||||
|
||||
@ -121,6 +207,8 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.info = await this.$root.api('stats');
|
||||
|
||||
this.now = new Date();
|
||||
|
||||
const [perHour, perDay] = await Promise.all([Promise.all([
|
||||
@ -154,6 +242,15 @@ export default Vue.extend({
|
||||
}
|
||||
};
|
||||
|
||||
this.notesLocalWoW = this.info.originalNotesCount - chart.perDay.notes.local.total[7];
|
||||
this.notesLocalDoD = this.info.originalNotesCount - chart.perDay.notes.local.total[1];
|
||||
this.notesRemoteWoW = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[7];
|
||||
this.notesRemoteDoD = (this.info.notesCount - this.info.originalNotesCount) - chart.perDay.notes.remote.total[1];
|
||||
this.usersLocalWoW = this.info.originalUsersCount - chart.perDay.users.local.total[7];
|
||||
this.usersLocalDoD = this.info.originalUsersCount - chart.perDay.users.local.total[1];
|
||||
this.usersRemoteWoW = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[7];
|
||||
this.usersRemoteDoD = (this.info.usersCount - this.info.originalUsersCount) - chart.perDay.users.remote.total[1];
|
||||
|
||||
this.chart = chart;
|
||||
|
||||
this.renderChart();
|
||||
@ -489,3 +586,80 @@ export default Vue.extend({
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zbcjwnqg {
|
||||
> .stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
margin: calc(0px - var(--margin) / 2);
|
||||
margin-bottom: calc(var(--margin) / 2);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex: 1 0 213px;
|
||||
margin: calc(var(--margin) / 2);
|
||||
box-sizing: border-box;
|
||||
padding: 16px 20px;
|
||||
|
||||
> div {
|
||||
width: 50%;
|
||||
|
||||
&:first-child {
|
||||
> b {
|
||||
display: block;
|
||||
|
||||
> [data-icon] {
|
||||
width: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> small {
|
||||
margin-left: 16px + 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
> dl {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
line-height: 1.5em;
|
||||
|
||||
> dt,
|
||||
> dd {
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> dt {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.total {
|
||||
> dt,
|
||||
> dd {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.diff.inc {
|
||||
> dd {
|
||||
color: #82c11c;
|
||||
|
||||
&:before {
|
||||
content: "+";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -20,6 +20,7 @@ import Vue from 'vue';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import i18n from '../i18n';
|
||||
import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
||||
import ImageViewer from './image-viewer.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
@ -60,7 +61,16 @@ export default Vue.extend({
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
window.open(this.image.url, '_blank');
|
||||
if (this.$store.state.device.imageNewTab) {
|
||||
window.open(this.image.url, '_blank');
|
||||
} else {
|
||||
const viewer = this.$root.new(ImageViewer, {
|
||||
image: this.image
|
||||
});
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
viewer.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -125,11 +125,13 @@ export default Vue.extend({
|
||||
|
||||
> .item {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding: 8px 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
font-size: 0.9em;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -174,7 +176,7 @@ export default Vue.extend({
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 13px;
|
||||
color: var(--accent);
|
||||
color: var(--indicator);
|
||||
font-size: 12px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
@ -82,10 +82,10 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
attrs: {
|
||||
style: `display: inline-block; font-size: 150%;`
|
||||
},
|
||||
directives: [this.$store.state.settings.disableAnimatedMfm ? {} : {
|
||||
directives: [this.$store.state.device.animatedMfm ? {
|
||||
name: 'animate-css',
|
||||
value: { classes: 'tada', iteration: 'infinite' }
|
||||
}]
|
||||
}: {}]
|
||||
}, genEl(token.children));
|
||||
}
|
||||
|
||||
@ -110,10 +110,10 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
attrs: {
|
||||
style: 'display: inline-block;'
|
||||
},
|
||||
directives: [this.$store.state.settings.disableAnimatedMfm ? {} : {
|
||||
directives: [this.$store.state.device.animatedMfm ? {
|
||||
name: 'animate-css',
|
||||
value: { classes: 'rubberBand', iteration: 'infinite' }
|
||||
}]
|
||||
} : {}]
|
||||
}, genEl(token.children));
|
||||
}
|
||||
|
||||
@ -122,9 +122,8 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
token.node.props.attr == 'left' ? 'reverse' :
|
||||
token.node.props.attr == 'alternate' ? 'alternate' :
|
||||
'normal';
|
||||
const style = (this.$store.state.settings.disableAnimatedMfm)
|
||||
? ''
|
||||
: `animation: spin 1.5s linear infinite; animation-direction: ${direction};`;
|
||||
const style = this.$store.state.device.animatedMfm
|
||||
? `animation: spin 1.5s linear infinite; animation-direction: ${direction};` : '';
|
||||
return (createElement as any)('span', {
|
||||
attrs: {
|
||||
style: 'display: inline-block;' + style
|
||||
@ -135,7 +134,7 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||
case 'jump': {
|
||||
return (createElement as any)('span', {
|
||||
attrs: {
|
||||
style: (this.$store.state.settings.disableAnimatedMfm) ? 'display: inline-block;' : 'display: inline-block; animation: jump 0.75s linear infinite;'
|
||||
style: this.$store.state.device.animatedMfm ? 'display: inline-block; animation: jump 0.75s linear infinite;' : 'display: inline-block;'
|
||||
},
|
||||
}, genEl(token.children));
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mk-modal">
|
||||
<div class="mk-modal" v-hotkey.global="keymap">
|
||||
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
|
||||
<div class="bg" ref="bg" v-if="show" @click="close()"></div>
|
||||
</transition>
|
||||
@ -20,6 +20,13 @@ export default Vue.extend({
|
||||
show: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
keymap(): any {
|
||||
return {
|
||||
'esc': this.close,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.show = false;
|
||||
|
@ -77,23 +77,19 @@ export default Vue.extend({
|
||||
> .admin,
|
||||
> .moderator {
|
||||
margin-right: 0.5em;
|
||||
color: var(--badge);
|
||||
}
|
||||
|
||||
> .username {
|
||||
margin: 0 .5em 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--noteHeaderAcct);
|
||||
}
|
||||
|
||||
> .info {
|
||||
margin-left: auto;
|
||||
font-size: 0.9em;
|
||||
|
||||
> * {
|
||||
color: var(--noteHeaderInfo);
|
||||
}
|
||||
|
||||
> .mobile {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
@ -9,7 +9,9 @@
|
||||
>
|
||||
<x-sub v-for="note in conversation" :key="note.id" :note="note"/>
|
||||
<x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
|
||||
<div class="pinned" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div>
|
||||
<div class="info" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div>
|
||||
<div class="info" v-if="appearNote._prId_"><fa :icon="faBullhorn"/> {{ $t('promotion') }}<button class="_textButton hide" @click="readPromo()">{{ $t('hideThisNote') }} <fa :icon="faTimes"/></button></div>
|
||||
<div class="info" v-if="appearNote._featuredId_"><fa :icon="faBolt"/> {{ $t('featured') }}</div>
|
||||
<div class="renote" v-if="isRenote">
|
||||
<mk-avatar class="avatar" :user="note.user"/>
|
||||
<fa :icon="faRetweet"/>
|
||||
@ -58,7 +60,7 @@
|
||||
<template v-else><fa :icon="faReply"/></template>
|
||||
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<button v-if="['public', 'home'].includes(appearNote.visibility)" @click="renote()" class="button _button" ref="renoteButton">
|
||||
<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton">
|
||||
<fa :icon="faRetweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button v-else class="button _button">
|
||||
@ -83,7 +85,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||
import { parse } from '../../mfm/parse';
|
||||
import { sum, unique } from '../../prelude/array';
|
||||
@ -140,7 +142,7 @@ export default Vue.extend({
|
||||
replies: [],
|
||||
showContent: false,
|
||||
hideThisNote: false,
|
||||
faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan
|
||||
faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan
|
||||
};
|
||||
},
|
||||
|
||||
@ -190,16 +192,16 @@ export default Vue.extend({
|
||||
return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId);
|
||||
},
|
||||
|
||||
canRenote(): boolean {
|
||||
return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
|
||||
},
|
||||
|
||||
reactionsCount(): number {
|
||||
return this.appearNote.reactions
|
||||
? sum(Object.values(this.appearNote.reactions))
|
||||
: 0;
|
||||
},
|
||||
|
||||
title(): string {
|
||||
return '';
|
||||
},
|
||||
|
||||
urls(): string[] {
|
||||
if (this.appearNote.text) {
|
||||
const ast = parse(this.appearNote.text);
|
||||
@ -263,6 +265,13 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
methods: {
|
||||
readPromo() {
|
||||
(this as any).$root.api('promo/read', {
|
||||
noteId: this.appearNote.id
|
||||
});
|
||||
this.hideThisNote = true;
|
||||
},
|
||||
|
||||
capture(withHandler = false) {
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
|
||||
@ -522,6 +531,15 @@ export default Vue.extend({
|
||||
text: this.$t('pin'),
|
||||
action: () => this.togglePin(true)
|
||||
} : undefined,
|
||||
...(this.$store.state.i.isModerator || this.$store.state.i.isAdmin ? [
|
||||
null,
|
||||
{
|
||||
icon: faBullhorn,
|
||||
text: this.$t('promote'),
|
||||
action: this.promote
|
||||
}]
|
||||
: []
|
||||
),
|
||||
...(this.appearNote.userId == this.$store.state.i.id ? [
|
||||
null,
|
||||
{
|
||||
@ -614,6 +632,30 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
async promote() {
|
||||
const { canceled, result: days } = await this.$root.dialog({
|
||||
title: this.$t('numberOfDays'),
|
||||
input: { type: 'number' }
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
this.$root.api('admin/promo/create', {
|
||||
noteId: this.appearNote.id,
|
||||
expiresAt: Date.now() + (86400000 * days)
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
focus() {
|
||||
this.$el.focus();
|
||||
},
|
||||
@ -710,7 +752,9 @@ export default Vue.extend({
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
}
|
||||
|
||||
> .pinned {
|
||||
> .info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 32px 8px 32px;
|
||||
line-height: 24px;
|
||||
font-size: 90%;
|
||||
@ -724,9 +768,14 @@ export default Vue.extend({
|
||||
> [data-icon] {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
> .hide {
|
||||
margin-left: auto;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
> .pinned + .article {
|
||||
> .info + .article {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
@ -863,7 +912,7 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--mkykhqkw);
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
|
||||
> .count {
|
||||
|
@ -1,22 +1,29 @@
|
||||
<template>
|
||||
<div class="mk-notes" v-size="[{ max: 500 }]">
|
||||
<div class="empty" v-if="empty">
|
||||
<img src="https://xn--931a.moe/assets/info.png" alt=""/>
|
||||
<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
|
||||
<div>{{ $t('noNotes') }}</div>
|
||||
</div>
|
||||
|
||||
<mk-error v-if="error" @retry="init()"/>
|
||||
|
||||
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }">
|
||||
<x-note :note="note" :detail="detail" :key="note.id"/>
|
||||
</x-list>
|
||||
|
||||
<footer class="more" v-if="more">
|
||||
<div class="more" v-if="more && reversed" style="margin-bottom: var(--margin);">
|
||||
<mk-button class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()" primary>
|
||||
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
|
||||
<template v-if="moreFetching"><mk-loading inline/></template>
|
||||
</mk-button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
|
||||
<x-note :note="note" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
|
||||
</x-list>
|
||||
|
||||
<div class="more" v-if="more && !reversed" style="margin-top: var(--margin);">
|
||||
<mk-button class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore()" primary>
|
||||
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
|
||||
<template v-if="moreFetching"><mk-loading inline/></template>
|
||||
</mk-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -67,6 +74,10 @@ export default Vue.extend({
|
||||
notes(): any[] {
|
||||
return this.extract ? this.extract(this.items) : this.items;
|
||||
},
|
||||
|
||||
reversed(): boolean {
|
||||
return this.pagination.reversed;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -88,20 +99,18 @@ export default Vue.extend({
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .notes {
|
||||
> ::v-deep * {
|
||||
> ::v-deep *:not(:last-child) {
|
||||
margin-bottom: var(--marginFull);
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_500px {
|
||||
> .notes {
|
||||
> ::v-deep * {
|
||||
> ::v-deep *:not(:last-child) {
|
||||
margin-bottom: var(--marginHalf);
|
||||
}
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ export default Vue.extend({
|
||||
background: #36aed2;
|
||||
}
|
||||
|
||||
&.retweet {
|
||||
&.renote {
|
||||
padding: 3px;
|
||||
background: #36d298;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="lzyxtsnt">
|
||||
<img v-if="image" :src="image.url" alt=""/>
|
||||
<img v-if="image" :src="image.url"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,12 +1,10 @@
|
||||
<template>
|
||||
<transition-group v-if="$store.state.device.animation"
|
||||
name="staggered-fade"
|
||||
class="uupnnhew"
|
||||
:data-direction="direction"
|
||||
:data-reversed="reversed ? 'true' : 'false'"
|
||||
name="staggered"
|
||||
tag="div"
|
||||
:css="false"
|
||||
@before-enter="beforeEnter"
|
||||
@enter="enter"
|
||||
@leave="leave"
|
||||
mode="out-in"
|
||||
appear
|
||||
>
|
||||
<slot></slot>
|
||||
@ -37,48 +35,46 @@ export default Vue.extend({
|
||||
default: false
|
||||
}
|
||||
},
|
||||
i: 0,
|
||||
methods: {
|
||||
beforeEnter(el) {
|
||||
if (document.hidden) return;
|
||||
|
||||
el.style.opacity = 0;
|
||||
el.style.transform = this.direction === 'down' ? 'translateY(-64px)' : 'translateY(64px)';
|
||||
const delay = this.delay * this.$options.i;
|
||||
el.style.transition = [getComputedStyle(el).transition, `transform 0.7s cubic-bezier(0.23, 1, 0.32, 1) ${delay}ms`, `opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1) ${delay}ms`].filter(x => x != '').join(',');
|
||||
this.$options.i++;
|
||||
|
||||
setTimeout(() => {
|
||||
el.style.transition = null;
|
||||
el.style.transform = null;
|
||||
el.style.opacity = null;
|
||||
this.$options.i--;
|
||||
}, delay + 710);
|
||||
},
|
||||
enter(el) {
|
||||
if (document.hidden) {
|
||||
el.style.opacity = 1;
|
||||
el.style.transform = 'translateY(0px)';
|
||||
} else {
|
||||
setTimeout(() => { // 必要
|
||||
el.style.opacity = 1;
|
||||
el.style.transform = 'translateY(0px)';
|
||||
});
|
||||
}
|
||||
},
|
||||
leave(el) {
|
||||
el.style.opacity = 0;
|
||||
el.style.transform = this.direction === 'down' ? 'translateY(64px)' : 'translateY(-64px)';
|
||||
},
|
||||
focus() {
|
||||
this.$slots.default[0].elm.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.staggered-fade-move {
|
||||
transition: transform 0.7s !important;
|
||||
.staggered-move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1) !important;
|
||||
}
|
||||
|
||||
.uupnnhew[data-direction="up"] {
|
||||
.staggered-enter {
|
||||
opacity: 0;
|
||||
transform: translateY(64px);
|
||||
}
|
||||
}
|
||||
|
||||
.uupnnhew[data-direction="down"] {
|
||||
.staggered-enter {
|
||||
opacity: 0;
|
||||
transform: translateY(-64px);
|
||||
}
|
||||
}
|
||||
|
||||
.uupnnhew[data-reversed="true"] {
|
||||
@for $i from 1 through 30 {
|
||||
.staggered-enter-active:nth-last-child(#{$i}) {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1) (15ms * ($i - 1)), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1) (15ms * ($i - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uupnnhew[data-reversed="false"] {
|
||||
@for $i from 1 through 30 {
|
||||
.staggered-enter-active:nth-child(#{$i}) {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1) (15ms * ($i - 1)), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1) (15ms * ($i - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<x-window @closed="() => { $emit('closed'); destroyDom(); }">
|
||||
<x-window ref="window" @closed="() => { $emit('closed'); destroyDom(); }">
|
||||
<template #header>{{ $t('signup') }}</template>
|
||||
<x-signup/>
|
||||
<x-signup :auto-set="autoSet" @signup="onSignup"/>
|
||||
</x-window>
|
||||
</template>
|
||||
|
||||
@ -18,5 +18,20 @@ export default Vue.extend({
|
||||
XSignup,
|
||||
XWindow,
|
||||
},
|
||||
|
||||
props: {
|
||||
autoSet: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSignup(res) {
|
||||
this.$emit('signup', res);
|
||||
this.$refs.window.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -84,6 +84,14 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
autoSet: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
meta() {
|
||||
return this.$store.state.instance.meta;
|
||||
@ -97,6 +105,15 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.autoSet) {
|
||||
this.$once('signup', res => {
|
||||
localStorage.setItem('i', res.i);
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
@ -166,8 +183,7 @@ export default Vue.extend({
|
||||
username: this.username,
|
||||
password: this.password
|
||||
}).then(res => {
|
||||
localStorage.setItem('i', res.i);
|
||||
location.href = '/';
|
||||
this.$emit('signup', res);
|
||||
});
|
||||
}).catch(() => {
|
||||
this.submitting = false;
|
||||
|
@ -105,7 +105,7 @@ export default Vue.extend({
|
||||
padding: 8px 14px;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
font-size: 0.9em;
|
||||
line-height: 24px;
|
||||
box-shadow: none;
|
||||
text-decoration: none;
|
||||
|
@ -239,6 +239,14 @@ export default Vue.extend({
|
||||
position: relative;
|
||||
margin: 32px 0;
|
||||
|
||||
&:not(.inline):first-child {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&:not(.inline):last-child {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -77,6 +77,14 @@ export default Vue.extend({
|
||||
position: relative;
|
||||
margin: 32px 0;
|
||||
|
||||
&:not(.inline):first-child {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&:not(.inline):last-child {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -129,7 +129,6 @@ export default Vue.extend({
|
||||
> .label {
|
||||
margin-left: 8px;
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: inherit;
|
||||
color: var(--fg);
|
||||
|
@ -85,6 +85,10 @@ export default Vue.extend({
|
||||
margin: 42px 0 32px 0;
|
||||
position: relative;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
<template #header><mk-user-name :user="user"/></template>
|
||||
<div class="vrcsvlkm">
|
||||
<mk-button @click="resetPassword()" primary>{{ $t('resetPassword') }}</mk-button>
|
||||
<mk-switch v-if="$store.state.i.isAdmin" @change="toggleModerator()" v-model="moderator">{{ $t('moderator') }}</mk-switch>
|
||||
<mk-switch v-if="$store.state.i.isAdmin && !user.isAdmin" @change="toggleModerator()" v-model="moderator">{{ $t('moderator') }}</mk-switch>
|
||||
<mk-switch @change="toggleSilence()" v-model="silenced">{{ $t('silence') }}</mk-switch>
|
||||
<mk-switch @change="toggleSuspend()" v-model="suspended">{{ $t('suspend') }}</mk-switch>
|
||||
</div>
|
||||
@ -47,7 +47,7 @@ export default Vue.extend({
|
||||
type: 'waiting',
|
||||
iconOnly: true
|
||||
});
|
||||
|
||||
|
||||
this.$root.api('admin/reset-password', {
|
||||
userId: this.user.id,
|
||||
}).then(({ password }) => {
|
||||
|
@ -54,6 +54,8 @@ export default {
|
||||
|
||||
calc();
|
||||
|
||||
vn.context.$on('hook:activated', calc);
|
||||
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
calc();
|
||||
});
|
||||
|
@ -136,8 +136,6 @@ document.body.innerHTML = '<div id="app"></div>';
|
||||
const os = new MiOS();
|
||||
|
||||
os.init(async () => {
|
||||
if (os.store.state.settings.wallpaper) document.documentElement.style.backgroundImage = `url(${os.store.state.settings.wallpaper})`;
|
||||
|
||||
if ('Notification' in window && os.store.getters.isSignedIn) {
|
||||
// 許可を得ていなかったらリクエスト
|
||||
if (Notification.permission === 'default') {
|
||||
|
@ -42,9 +42,9 @@ export default class MiOS extends EventEmitter {
|
||||
* @param callback A function that call when initialized
|
||||
*/
|
||||
@autobind
|
||||
public async init(_callback) {
|
||||
const callback = () => {
|
||||
_callback();
|
||||
public async init(callback) {
|
||||
const finish = () => {
|
||||
callback();
|
||||
|
||||
this.store.dispatch('instance/fetch').then(() => {
|
||||
// Init service worker
|
||||
@ -59,7 +59,7 @@ export default class MiOS extends EventEmitter {
|
||||
let me = null;
|
||||
|
||||
// Return when not signed in
|
||||
if (token == null) {
|
||||
if (token == null || token === 'null') {
|
||||
return done();
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ export default class MiOS extends EventEmitter {
|
||||
this.initStream();
|
||||
|
||||
// Finish init
|
||||
callback();
|
||||
finish();
|
||||
};
|
||||
|
||||
// キャッシュがあったとき
|
||||
@ -133,7 +133,7 @@ export default class MiOS extends EventEmitter {
|
||||
this.initStream();
|
||||
|
||||
// Finish init
|
||||
callback();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -227,7 +227,6 @@ export default class MiOS extends EventEmitter {
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on('myTokenRegenerated', () => {
|
||||
alert(locale['common']['my-token-regenerated']);
|
||||
this.signout();
|
||||
});
|
||||
}
|
||||
|
@ -12,14 +12,12 @@
|
||||
<div><b>{{ $t('administrator') }}</b><span>{{ meta.maintainerName }}</span></div>
|
||||
<div><b></b><span>{{ meta.maintainerEmail }}</span></div>
|
||||
</div>
|
||||
<div class="_content table" v-if="stats">
|
||||
<div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
|
||||
<div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
|
||||
</div>
|
||||
<div class="_content table">
|
||||
<div><b>Misskey</b><span>v{{ version }}</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<mk-instance-stats style="margin-top: var(--margin);"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -28,6 +26,7 @@ import Vue from 'vue';
|
||||
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { version } from '../config';
|
||||
import i18n from '../i18n';
|
||||
import MkInstanceStats from '../components/instance-stats.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
@ -38,10 +37,13 @@ export default Vue.extend({
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
MkInstanceStats
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
version,
|
||||
stats: null,
|
||||
serverInfo: null,
|
||||
faInfoCircle
|
||||
}
|
||||
@ -52,12 +54,6 @@ export default Vue.extend({
|
||||
return this.$store.state.instance.meta;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$root.api('stats').then(res => {
|
||||
this.stats = res;
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
<div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
|
||||
<div class="_content">
|
||||
<mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl" alt=""/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
</div>
|
||||
<div class="_footer" v-if="$store.getters.isSignedIn && !announcement.isRead">
|
||||
<mk-button @click="read(announcement)" primary><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button>
|
||||
|
@ -3,9 +3,13 @@
|
||||
<portal to="icon"><fa :icon="faFileAlt"/></portal>
|
||||
<portal to="title">{{ title }}</portal>
|
||||
<main class="_card">
|
||||
<div class="_title"><fa :icon="faFileAlt"/> {{ title }}</div>
|
||||
<div class="_content">
|
||||
<div v-html="body" class="qyqbqfal"></div>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-link :url="`https://github.com/syuilo/misskey/blob/master/src/docs/${doc}.ja-JP.md`" class="at">{{ $t('docSource') }}</mk-link>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@ -14,19 +18,27 @@
|
||||
import Vue from 'vue';
|
||||
import { faFileAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import i18n from '../i18n';
|
||||
import { url, lang } from '../config';
|
||||
import MkLink from '../components/link.vue';
|
||||
|
||||
const markdown = MarkdownIt({
|
||||
html: true
|
||||
});
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.title,
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
MkLink
|
||||
},
|
||||
|
||||
props: {
|
||||
doc: {
|
||||
type: String,
|
||||
|
@ -1,27 +1,40 @@
|
||||
<template>
|
||||
<mk-pagination :pagination="pagination" #default="{items}" class="mk-follow-requests" ref="list">
|
||||
<div class="user _panel" v-for="(req, i) in items" :key="req.id">
|
||||
<mk-avatar class="avatar" :user="req.follower"/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<router-link class="name" :to="req.follower | userPage" v-user-preview="req.follower.id"><mk-user-name :user="req.follower"/></router-link>
|
||||
<p class="acct">@{{ req.follower | acct }}</p>
|
||||
<div>
|
||||
<portal to="icon"><fa :icon="faUserClock"/></portal>
|
||||
<portal to="title">{{ $t('followRequests') }}</portal>
|
||||
|
||||
<mk-pagination :pagination="pagination" class="mk-follow-requests" ref="list">
|
||||
<template #empty>
|
||||
<div class="tkdrhpxr">
|
||||
<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
|
||||
<div>{{ $t('noFollowRequests') }}</div>
|
||||
</div>
|
||||
<div class="description" v-if="req.follower.description" :title="req.follower.description">
|
||||
<mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$store.state.i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div class="user _panel" v-for="req in items" :key="req.id">
|
||||
<mk-avatar class="avatar" :user="req.follower"/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<router-link class="name" :to="req.follower | userPage" v-user-preview="req.follower.id"><mk-user-name :user="req.follower"/></router-link>
|
||||
<p class="acct">@{{ req.follower | acct }}</p>
|
||||
</div>
|
||||
<div class="description" v-if="req.follower.description" :title="req.follower.description">
|
||||
<mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$store.state.i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="_button" @click="accept(req.follower)"><fa :icon="faCheck"/></button>
|
||||
<button class="_button" @click="reject(req.follower)"><fa :icon="faTimes"/></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="_button" @click="accept(req.follower)"><fa :icon="faCheck"/></button>
|
||||
<button class="_button" @click="reject(req.follower)"><fa :icon="faTimes"/></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mk-pagination>
|
||||
</template>
|
||||
</mk-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faCheck, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faUserClock, faCheck, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkPagination from '../components/ui/pagination.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
@ -41,7 +54,7 @@ export default Vue.extend({
|
||||
endpoint: 'following/requests/list',
|
||||
limit: 10,
|
||||
},
|
||||
faCheck, faTimes
|
||||
faCheck, faTimes, faUserClock
|
||||
};
|
||||
},
|
||||
|
||||
@ -62,6 +75,18 @@ export default Vue.extend({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-follow-requests {
|
||||
.tkdrhpxr {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
|
||||
> img {
|
||||
vertical-align: bottom;
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
> .user {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
@ -184,7 +184,7 @@ export default Vue.extend({
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 8px;
|
||||
color: var(--accent);
|
||||
color: var(--indicator);
|
||||
font-size: 12px;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
@ -62,7 +62,9 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
signup() {
|
||||
this.$root.new(XSignupDialog);
|
||||
this.$root.new(XSignupDialog, {
|
||||
autoSet: true
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -5,7 +5,7 @@
|
||||
<mk-button @click="add()" primary style="margin: 0 auto 16px auto;"><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
|
||||
<section class="_card announcements">
|
||||
<div class="_content announcement" v-for="announcement in announcements">
|
||||
<mk-input v-model="announcement.title" style="margin-top: 8px;">
|
||||
<mk-input v-model="announcement.title">
|
||||
<span>{{ $t('title') }}</span>
|
||||
</mk-input>
|
||||
<mk-textarea v-model="announcement.text">
|
||||
|
@ -2,10 +2,10 @@
|
||||
<div class="mk-instance-emojis">
|
||||
<portal to="icon"><fa :icon="faLaugh"/></portal>
|
||||
<portal to="title">{{ $t('customEmojis') }}</portal>
|
||||
|
||||
<section class="_card local">
|
||||
<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojis') }}</div>
|
||||
<div class="_content">
|
||||
<input ref="file" type="file" style="display: none;" @change="onChangeFile"/>
|
||||
<mk-pagination :pagination="pagination" class="emojis" ref="emojis">
|
||||
<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
|
||||
<template #default="{items}">
|
||||
@ -13,20 +13,30 @@
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<span class="name">{{ emoji.name }}</span>
|
||||
<span class="info">
|
||||
<b class="category">{{ emoji.category }}</b>
|
||||
<span class="aliases">{{ emoji.aliases.join(' ') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</mk-pagination>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button inline primary @click="add()"><fa :icon="faPlus"/> {{ $t('addEmoji') }}</mk-button>
|
||||
<div class="_content" v-if="selected">
|
||||
<mk-input v-model="name"><span>{{ $t('name') }}</span></mk-input>
|
||||
<mk-input v-model="category" :datalist="categories"><span>{{ $t('category') }}</span></mk-input>
|
||||
<mk-input v-model="aliases"><span>{{ $t('tags') }}</span></mk-input>
|
||||
<mk-button inline primary @click="update"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
<mk-button inline :disabled="selected == null" @click="del()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</mk-button>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button inline primary @click="add"><fa :icon="faPlus"/> {{ $t('addEmoji') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_card remote">
|
||||
<div class="_title"><fa :icon="faLaugh"/> {{ $t('customEmojisOfRemote') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input v-model="host" :debounce="true" style="margin-top: 0;"><span>{{ $t('host') }}</span></mk-input>
|
||||
<mk-input v-model="host" :debounce="true"><span>{{ $t('host') }}</span></mk-input>
|
||||
<mk-pagination :pagination="remotePagination" class="emojis" ref="remoteEmojis">
|
||||
<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
|
||||
<template #default="{items}">
|
||||
@ -34,7 +44,7 @@
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<span class="name">{{ emoji.name }}</span>
|
||||
<span class="host">{{ emoji.host }}</span>
|
||||
<span class="info">{{ emoji.host }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -49,12 +59,13 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPlus, faSave } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faTrashAlt, faLaugh } from '@fortawesome/free-regular-svg-icons';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
import MkPagination from '../../components/ui/pagination.vue';
|
||||
import { apiUrl } from '../../config';
|
||||
import { selectFile } from '../../scripts/select-file';
|
||||
import { unique } from '../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
metaInfo() {
|
||||
@ -71,9 +82,11 @@ export default Vue.extend({
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
selected: null,
|
||||
selectedRemote: null,
|
||||
name: null,
|
||||
category: null,
|
||||
aliases: null,
|
||||
host: '',
|
||||
pagination: {
|
||||
endpoint: 'admin/emoji/list',
|
||||
@ -86,52 +99,48 @@ export default Vue.extend({
|
||||
host: this.host ? this.host : null
|
||||
})
|
||||
},
|
||||
faTrashAlt, faPlus, faLaugh
|
||||
faTrashAlt, faPlus, faLaugh, faSave
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
categories() {
|
||||
if (this.$store.state.instance.meta) {
|
||||
return unique(this.$store.state.instance.meta.emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
host() {
|
||||
this.$refs.remoteEmojis.reload();
|
||||
},
|
||||
|
||||
selected() {
|
||||
this.name = this.selected ? this.selected.name : null;
|
||||
this.category = this.selected ? this.selected.category : null;
|
||||
this.aliases = this.selected ? this.selected.aliases.join(' ') : null;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async add() {
|
||||
const { canceled: canceled, result: name } = await this.$root.dialog({
|
||||
title: this.$t('emojiName'),
|
||||
input: true
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
this.name = name;
|
||||
|
||||
(this.$refs.file as any).click();
|
||||
},
|
||||
|
||||
onChangeFile() {
|
||||
const [file] = Array.from((this.$refs.file as any).files);
|
||||
if (file == null) return;
|
||||
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
data.append('name', this.name);
|
||||
data.append('i', this.$store.state.i.token);
|
||||
async add(e) {
|
||||
const files = await selectFile(this, e.currentTarget || e.target, null, true);
|
||||
|
||||
const dialog = this.$root.dialog({
|
||||
type: 'waiting',
|
||||
text: this.$t('uploading') + '...',
|
||||
text: this.$t('doing') + '...',
|
||||
showOkButton: false,
|
||||
showCancelButton: false,
|
||||
cancelableByBgClick: false
|
||||
});
|
||||
|
||||
fetch(apiUrl + '/admin/emoji/add', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(f => {
|
||||
|
||||
Promise.all(files.map(file => this.$root.api('admin/emoji/add', {
|
||||
fileId: file.id,
|
||||
})))
|
||||
.then(() => {
|
||||
this.$refs.emojis.reload();
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
@ -143,6 +152,22 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
async update() {
|
||||
await this.$root.api('admin/emoji/update', {
|
||||
id: this.selected.id,
|
||||
name: this.name,
|
||||
category: this.category,
|
||||
aliases: this.aliases.split(' '),
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
|
||||
this.$refs.emojis.reload();
|
||||
},
|
||||
|
||||
async del() {
|
||||
const { canceled } = await this.$root.dialog({
|
||||
type: 'warning',
|
||||
@ -207,6 +232,18 @@ export default Vue.extend({
|
||||
> .name {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> .info {
|
||||
opacity: 0.5;
|
||||
|
||||
> .category {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> .aliases {
|
||||
font-style: oblique;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -241,7 +278,7 @@ export default Vue.extend({
|
||||
display: block;
|
||||
}
|
||||
|
||||
> .host {
|
||||
> .info {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
@ -1,165 +1,58 @@
|
||||
<template>
|
||||
<div v-if="meta" class="mk-instance-page">
|
||||
<div v-if="meta" class="xhexznfu">
|
||||
<portal to="icon"><fa :icon="faServer"/></portal>
|
||||
<portal to="title">{{ $t('instance') }}</portal>
|
||||
|
||||
<section class="_card info">
|
||||
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input v-model="name" style="margin-top: 8px;">{{ $t('instanceName') }}</mk-input>
|
||||
<mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea>
|
||||
<mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input>
|
||||
<mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input>
|
||||
<mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input>
|
||||
<mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input>
|
||||
<mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input>
|
||||
<mk-instance-stats style="margin-bottom: var(--margin);"/>
|
||||
|
||||
<section class="_card chart">
|
||||
<div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="cpumem"></canvas>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div>
|
||||
<div class="cell"><div class="label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
|
||||
<div class="cell"><div class="label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_card chart">
|
||||
<div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div>
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="disk"></canvas>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div>
|
||||
<div class="cell"><div class="label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
|
||||
<div class="cell"><div class="label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_card chart">
|
||||
<div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div>
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="net"></canvas>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card info">
|
||||
<div class="_content">
|
||||
<mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
|
||||
<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
|
||||
<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card info">
|
||||
<div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
|
||||
<mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
|
||||
<template v-if="enableRecaptcha">
|
||||
<mk-info>{{ $t('recaptchaInfo') }}</mk-info>
|
||||
<mk-info warn>{{ $t('recaptchaInfo2') }}</mk-info>
|
||||
<mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input>
|
||||
<mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
|
||||
<header>{{ $t('preview') }}</header>
|
||||
<div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
|
||||
<template v-if="enableServiceWorker">
|
||||
<mk-info>{{ $t('vapidInfo') }}<br><code>npx web-push generate-vapid-keys</code></mk-info>
|
||||
<mk-horizon-group inputs class="fit-bottom">
|
||||
<mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
|
||||
<mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
|
||||
</mk-horizon-group>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
|
||||
<div class="_content">
|
||||
<mk-textarea v-model="pinnedUsers" style="margin-top: 0;">
|
||||
<template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template>
|
||||
</mk-textarea>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
|
||||
<mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch>
|
||||
<mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
|
||||
<mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
|
||||
<mk-button primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
|
||||
<div class="_content">
|
||||
<mk-textarea v-model="blockedHosts" style="margin-top: 0;">
|
||||
<template #desc>{{ $t('blockedInstancesDescription') }}</template>
|
||||
</mk-textarea>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
|
||||
<div class="_content">
|
||||
<header><fa :icon="faTwitter"/> Twitter</header>
|
||||
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
|
||||
<template v-if="enableTwitterIntegration">
|
||||
<mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
|
||||
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
|
||||
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<header><fa :icon="faGithub"/> GitHub</header>
|
||||
<mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
|
||||
<template v-if="enableGithubIntegration">
|
||||
<mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
|
||||
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
|
||||
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<header><fa :icon="faDiscord"/> Discord</header>
|
||||
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
|
||||
<template v-if="enableDiscordIntegration">
|
||||
<mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
|
||||
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
|
||||
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card info">
|
||||
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('instanceInfo') }}</div>
|
||||
<div class="_content table" v-if="stats">
|
||||
<div><b>{{ $t('users') }}</b><span>{{ stats.originalUsersCount | number }}</span></div>
|
||||
<div><b>{{ $t('notes') }}</b><span>{{ stats.originalNotesCount | number }}</span></div>
|
||||
</div>
|
||||
<div class="_content table">
|
||||
<div><b>Misskey</b><span>v{{ version }}</span></div>
|
||||
</div>
|
||||
@ -174,18 +67,19 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
import MkTextarea from '../../components/ui/textarea.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
import MkInfo from '../../components/ui/info.vue';
|
||||
import MkUserSelect from '../../components/user-select.vue';
|
||||
import { faServer, faExchangeAlt, faMicrochip, faHdd } from '@fortawesome/free-solid-svg-icons';
|
||||
import Chart from 'chart.js';
|
||||
import MkInstanceStats from '../../components/instance-stats.vue';
|
||||
import { version, url } from '../../config';
|
||||
import i18n from '../../i18n';
|
||||
import getAcct from '../../../misc/acct/render';
|
||||
|
||||
const alpha = (hex, a) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
||||
const r = parseInt(result[1], 16);
|
||||
const g = parseInt(result[2], 16);
|
||||
const b = parseInt(result[3], 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
};
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
@ -197,11 +91,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkSwitch,
|
||||
MkInfo,
|
||||
MkInstanceStats,
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -210,41 +100,11 @@ export default Vue.extend({
|
||||
url,
|
||||
stats: null,
|
||||
serverInfo: null,
|
||||
proxyAccount: null,
|
||||
proxyAccountId: null,
|
||||
cacheRemoteFiles: false,
|
||||
proxyRemoteFiles: false,
|
||||
localDriveCapacityMb: 0,
|
||||
remoteDriveCapacityMb: 0,
|
||||
blockedHosts: '',
|
||||
pinnedUsers: '',
|
||||
maintainerName: null,
|
||||
maintainerEmail: null,
|
||||
name: null,
|
||||
description: null,
|
||||
tosUrl: null,
|
||||
bannerUrl: null,
|
||||
iconUrl: null,
|
||||
maxNoteTextLength: 0,
|
||||
enableRegistration: false,
|
||||
enableLocalTimeline: false,
|
||||
enableGlobalTimeline: false,
|
||||
enableRecaptcha: false,
|
||||
recaptchaSiteKey: null,
|
||||
recaptchaSecretKey: null,
|
||||
enableServiceWorker: false,
|
||||
swPublicKey: null,
|
||||
swPrivateKey: null,
|
||||
enableTwitterIntegration: false,
|
||||
twitterConsumerKey: null,
|
||||
twitterConsumerSecret: null,
|
||||
enableGithubIntegration: false,
|
||||
githubClientId: null,
|
||||
githubClientSecret: null,
|
||||
enableDiscordIntegration: false,
|
||||
discordClientId: null,
|
||||
discordClientSecret: null,
|
||||
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
|
||||
connection: null,
|
||||
memUsage: 0,
|
||||
chartCpuMem: null,
|
||||
chartNet: null,
|
||||
faServer, faExchangeAlt, faMicrochip, faHdd
|
||||
}
|
||||
},
|
||||
|
||||
@ -254,153 +114,313 @@ export default Vue.extend({
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.name = this.meta.name;
|
||||
this.description = this.meta.description;
|
||||
this.tosUrl = this.meta.tosUrl;
|
||||
this.bannerUrl = this.meta.bannerUrl;
|
||||
this.iconUrl = this.meta.iconUrl;
|
||||
this.maintainerName = this.meta.maintainerName;
|
||||
this.maintainerEmail = this.meta.maintainerEmail;
|
||||
this.maxNoteTextLength = this.meta.maxNoteTextLength;
|
||||
this.enableRegistration = !this.meta.disableRegistration;
|
||||
this.enableLocalTimeline = !this.meta.disableLocalTimeline;
|
||||
this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
|
||||
this.enableRecaptcha = this.meta.enableRecaptcha;
|
||||
this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
|
||||
this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
|
||||
this.proxyAccountId = this.meta.proxyAccountId;
|
||||
this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
|
||||
this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
|
||||
this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
|
||||
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
|
||||
this.blockedHosts = this.meta.blockedHosts.join('\n');
|
||||
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
|
||||
this.enableServiceWorker = this.meta.enableServiceWorker;
|
||||
this.swPublicKey = this.meta.swPublickey;
|
||||
this.swPrivateKey = this.meta.swPrivateKey;
|
||||
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
|
||||
this.twitterConsumerKey = this.meta.twitterConsumerKey;
|
||||
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
|
||||
this.enableGithubIntegration = this.meta.enableGithubIntegration;
|
||||
this.githubClientId = this.meta.githubClientId;
|
||||
this.githubClientSecret = this.meta.githubClientSecret;
|
||||
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
|
||||
this.discordClientId = this.meta.discordClientId;
|
||||
this.discordClientSecret = this.meta.discordClientSecret;
|
||||
mounted() {
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
if (this.proxyAccountId) {
|
||||
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
|
||||
this.proxyAccount = proxyAccount;
|
||||
});
|
||||
}
|
||||
|
||||
this.$root.api('admin/server-info').then(res => {
|
||||
this.serverInfo = res;
|
||||
this.chartCpuMem = new Chart(this.$refs.cpumem, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'CPU',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#86b300',
|
||||
backgroundColor: alpha('#86b300', 0.1),
|
||||
data: []
|
||||
}, {
|
||||
label: 'MEM (active)',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#935dbf',
|
||||
backgroundColor: alpha('#935dbf', 0.02),
|
||||
data: []
|
||||
}, {
|
||||
label: 'MEM (used)',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#935dbf',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: []
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 3,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 8,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 16,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
position: 'right',
|
||||
ticks: {
|
||||
display: false,
|
||||
max: 100
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$root.api('stats').then(res => {
|
||||
this.stats = res;
|
||||
this.chartNet = new Chart(this.$refs.net, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'In',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#94a029',
|
||||
backgroundColor: alpha('#94a029', 0.1),
|
||||
data: []
|
||||
}, {
|
||||
label: 'Out',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#ff9156',
|
||||
backgroundColor: alpha('#ff9156', 0.1),
|
||||
data: []
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 3,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 8,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 16,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
position: 'right',
|
||||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.chartDisk = new Chart(this.$refs.disk, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Read',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#94a029',
|
||||
backgroundColor: alpha('#94a029', 0.1),
|
||||
data: []
|
||||
}, {
|
||||
label: 'Write',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#ff9156',
|
||||
backgroundColor: alpha('#ff9156', 0.1),
|
||||
data: []
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 3,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 8,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 16,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
position: 'right',
|
||||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$root.api('admin/server-info', {}).then(res => {
|
||||
this.serverInfo = res;
|
||||
|
||||
this.connection = this.$root.stream.useSharedConnection('serverStats');
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
this.connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 150
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const renderRecaptchaPreview = () => {
|
||||
if (!(window as any).grecaptcha) return;
|
||||
if (!this.$refs.recaptcha) return;
|
||||
if (!this.recaptchaSiteKey) return;
|
||||
(window as any).grecaptcha.render(this.$refs.recaptcha, {
|
||||
sitekey: this.recaptchaSiteKey
|
||||
});
|
||||
};
|
||||
window.onRecaotchaLoad = () => {
|
||||
renderRecaptchaPreview();
|
||||
};
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad');
|
||||
head.appendChild(script);
|
||||
this.$watch('enableRecaptcha', () => {
|
||||
renderRecaptchaPreview();
|
||||
});
|
||||
this.$watch('recaptchaSiteKey', () => {
|
||||
renderRecaptchaPreview();
|
||||
});
|
||||
beforeDestroy() {
|
||||
this.connection.off('stats', this.onStats);
|
||||
this.connection.off('statsLog', this.onStatsLog);
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
addPinUser() {
|
||||
this.$root.new(MkUserSelect, {}).$once('selected', user => {
|
||||
this.pinnedUsers = this.pinnedUsers.trim();
|
||||
this.pinnedUsers += '\n@' + getAcct(user);
|
||||
this.pinnedUsers = this.pinnedUsers.trim();
|
||||
});
|
||||
onStats(stats) {
|
||||
const cpu = (stats.cpu * 100).toFixed(0);
|
||||
const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
|
||||
const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
|
||||
this.memUsage = stats.mem.active;
|
||||
|
||||
this.chartCpuMem.data.labels.push('');
|
||||
this.chartCpuMem.data.datasets[0].data.push(cpu);
|
||||
this.chartCpuMem.data.datasets[1].data.push(memActive);
|
||||
this.chartCpuMem.data.datasets[2].data.push(memUsed);
|
||||
this.chartNet.data.labels.push('');
|
||||
this.chartNet.data.datasets[0].data.push(stats.net.rx);
|
||||
this.chartNet.data.datasets[1].data.push(stats.net.tx);
|
||||
this.chartDisk.data.labels.push('');
|
||||
this.chartDisk.data.datasets[0].data.push(stats.fs.r);
|
||||
this.chartDisk.data.datasets[1].data.push(stats.fs.w);
|
||||
if (this.chartCpuMem.data.datasets[0].data.length > 150) {
|
||||
this.chartCpuMem.data.labels.shift();
|
||||
this.chartCpuMem.data.datasets[0].data.shift();
|
||||
this.chartCpuMem.data.datasets[1].data.shift();
|
||||
this.chartCpuMem.data.datasets[2].data.shift();
|
||||
this.chartNet.data.labels.shift();
|
||||
this.chartNet.data.datasets[0].data.shift();
|
||||
this.chartNet.data.datasets[1].data.shift();
|
||||
this.chartDisk.data.labels.shift();
|
||||
this.chartDisk.data.datasets[0].data.shift();
|
||||
this.chartDisk.data.datasets[1].data.shift();
|
||||
}
|
||||
this.chartCpuMem.update();
|
||||
this.chartNet.update();
|
||||
this.chartDisk.update();
|
||||
},
|
||||
|
||||
chooseProxyAccount() {
|
||||
this.$root.new(MkUserSelect, {}).$once('selected', user => {
|
||||
this.proxyAccount = user;
|
||||
this.proxyAccountId = user.id;
|
||||
this.save(true);
|
||||
});
|
||||
},
|
||||
|
||||
save(withDialog = false) {
|
||||
this.$root.api('admin/update-meta', {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
tosUrl: this.tosUrl,
|
||||
bannerUrl: this.bannerUrl,
|
||||
iconUrl: this.iconUrl,
|
||||
maintainerName: this.maintainerName,
|
||||
maintainerEmail: this.maintainerEmail,
|
||||
maxNoteTextLength: this.maxNoteTextLength,
|
||||
disableRegistration: !this.enableRegistration,
|
||||
disableLocalTimeline: !this.enableLocalTimeline,
|
||||
disableGlobalTimeline: !this.enableGlobalTimeline,
|
||||
enableRecaptcha: this.enableRecaptcha,
|
||||
recaptchaSiteKey: this.recaptchaSiteKey,
|
||||
recaptchaSecretKey: this.recaptchaSecretKey,
|
||||
proxyAccountId: this.proxyAccountId,
|
||||
cacheRemoteFiles: this.cacheRemoteFiles,
|
||||
proxyRemoteFiles: this.proxyRemoteFiles,
|
||||
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
|
||||
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
|
||||
blockedHosts: this.blockedHosts.split('\n') || [],
|
||||
pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
|
||||
enableServiceWorker: this.enableServiceWorker,
|
||||
swPublicKey: this.swPublicKey,
|
||||
swPrivateKey: this.swPrivateKey,
|
||||
enableTwitterIntegration: this.enableTwitterIntegration,
|
||||
twitterConsumerKey: this.twitterConsumerKey,
|
||||
twitterConsumerSecret: this.twitterConsumerSecret,
|
||||
enableGithubIntegration: this.enableGithubIntegration,
|
||||
githubClientId: this.githubClientId,
|
||||
githubClientSecret: this.githubClientSecret,
|
||||
enableDiscordIntegration: this.enableDiscordIntegration,
|
||||
discordClientId: this.discordClientId,
|
||||
discordClientSecret: this.discordClientSecret,
|
||||
}).then(() => {
|
||||
this.$store.dispatch('instance/fetch');
|
||||
if (withDialog) {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
}
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
onStatsLog(statsLog) {
|
||||
for (const stats of statsLog.reverse()) {
|
||||
this.onStats(stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-instance-page {
|
||||
.xhexznfu {
|
||||
> .stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
margin: calc(0px - var(--margin) / 2);
|
||||
margin-bottom: calc(var(--margin) / 2);
|
||||
|
||||
> div {
|
||||
flex: 1 0 213px;
|
||||
margin: calc(var(--margin) / 2);
|
||||
box-sizing: border-box;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
> .chart {
|
||||
> ._content {
|
||||
> .table {
|
||||
> .row {
|
||||
display: flex;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .cell {
|
||||
flex: 1;
|
||||
|
||||
> .label {
|
||||
font-size: 80%;
|
||||
opacity: 0.7;
|
||||
|
||||
> .icon {
|
||||
margin-right: 4px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .info {
|
||||
> .table {
|
||||
> div {
|
||||
|
@ -1,381 +0,0 @@
|
||||
<template>
|
||||
<div class="mk-instance-monitor">
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faMicrochip"/> {{ $t('cpuAndMemory') }}</div>
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="cpumem"></canvas>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">CPU</div>{{ serverInfo.cpu.model }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">MEM total</div>{{ serverInfo.mem.total | bytes }}</div>
|
||||
<div class="cell"><div class="label">MEM used</div>{{ memUsage | bytes }} ({{ (memUsage / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
|
||||
<div class="cell"><div class="label">MEM free</div>{{ serverInfo.mem.total - memUsage | bytes }} ({{ ((serverInfo.mem.total - memUsage) / serverInfo.mem.total * 100).toFixed(0) }}%)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faHdd"/> {{ $t('disk') }}</div>
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="disk"></canvas>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">Disk total</div>{{ serverInfo.fs.total | bytes }}</div>
|
||||
<div class="cell"><div class="label">Disk used</div>{{ serverInfo.fs.used | bytes }} ({{ (serverInfo.fs.used / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
|
||||
<div class="cell"><div class="label">Disk free</div>{{ serverInfo.fs.total - serverInfo.fs.used | bytes }} ({{ ((serverInfo.fs.total - serverInfo.fs.used) / serverInfo.fs.total * 100).toFixed(0) }}%)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faExchangeAlt"/> {{ $t('network') }}</div>
|
||||
<div class="_content" style="margin-top: -8px; margin-bottom: -12px;">
|
||||
<canvas ref="net"></canvas>
|
||||
</div>
|
||||
<div class="_content" v-if="serverInfo">
|
||||
<div class="table">
|
||||
<div class="row">
|
||||
<div class="cell"><div class="label">Interface</div>{{ serverInfo.net.interface }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faTachometerAlt, faExchangeAlt, faMicrochip, faHdd } from '@fortawesome/free-solid-svg-icons';
|
||||
import Chart from 'chart.js';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
const alpha = (hex, a) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
||||
const r = parseInt(result[1], 16);
|
||||
const g = parseInt(result[2], 16);
|
||||
const b = parseInt(result[3], 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
};
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
metaInfo() {
|
||||
return {
|
||||
title: `${this.$t('monitor')} | ${this.$t('instance')}`
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
serverInfo: null,
|
||||
memUsage: 0,
|
||||
chartCpuMem: null,
|
||||
chartNet: null,
|
||||
faTachometerAlt, faExchangeAlt, faMicrochip, faHdd
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
this.chartCpuMem = new Chart(this.$refs.cpumem, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'CPU',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#86b300',
|
||||
backgroundColor: alpha('#86b300', 0.1),
|
||||
data: []
|
||||
}, {
|
||||
label: 'MEM (active)',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#935dbf',
|
||||
backgroundColor: alpha('#935dbf', 0.02),
|
||||
data: []
|
||||
}, {
|
||||
label: 'MEM (used)',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#935dbf',
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
data: []
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 3,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 8,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 16,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
position: 'right',
|
||||
ticks: {
|
||||
display: false,
|
||||
max: 100
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.chartNet = new Chart(this.$refs.net, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'In',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#94a029',
|
||||
backgroundColor: alpha('#94a029', 0.1),
|
||||
data: []
|
||||
}, {
|
||||
label: 'Out',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#ff9156',
|
||||
backgroundColor: alpha('#ff9156', 0.1),
|
||||
data: []
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 3,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 8,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 16,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
position: 'right',
|
||||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.chartDisk = new Chart(this.$refs.disk, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Read',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#94a029',
|
||||
backgroundColor: alpha('#94a029', 0.1),
|
||||
data: []
|
||||
}, {
|
||||
label: 'Write',
|
||||
pointRadius: 0,
|
||||
lineTension: 0,
|
||||
borderWidth: 2,
|
||||
borderColor: '#ff9156',
|
||||
backgroundColor: alpha('#ff9156', 0.1),
|
||||
data: []
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 3,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 8,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 16,
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
position: 'right',
|
||||
ticks: {
|
||||
display: false,
|
||||
}
|
||||
}]
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$root.api('admin/server-info', {}).then(res => {
|
||||
this.serverInfo = res;
|
||||
|
||||
this.connection = this.$root.stream.useSharedConnection('serverStats');
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
this.connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 150
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.connection.off('stats', this.onStats);
|
||||
this.connection.off('statsLog', this.onStatsLog);
|
||||
this.connection.dispose();
|
||||
},
|
||||
|
||||
methods: {
|
||||
onStats(stats) {
|
||||
const cpu = (stats.cpu * 100).toFixed(0);
|
||||
const memActive = (stats.mem.active / this.serverInfo.mem.total * 100).toFixed(0);
|
||||
const memUsed = (stats.mem.used / this.serverInfo.mem.total * 100).toFixed(0);
|
||||
this.memUsage = stats.mem.active;
|
||||
|
||||
this.chartCpuMem.data.labels.push('');
|
||||
this.chartCpuMem.data.datasets[0].data.push(cpu);
|
||||
this.chartCpuMem.data.datasets[1].data.push(memActive);
|
||||
this.chartCpuMem.data.datasets[2].data.push(memUsed);
|
||||
this.chartNet.data.labels.push('');
|
||||
this.chartNet.data.datasets[0].data.push(stats.net.rx);
|
||||
this.chartNet.data.datasets[1].data.push(stats.net.tx);
|
||||
this.chartDisk.data.labels.push('');
|
||||
this.chartDisk.data.datasets[0].data.push(stats.fs.r);
|
||||
this.chartDisk.data.datasets[1].data.push(stats.fs.w);
|
||||
if (this.chartCpuMem.data.datasets[0].data.length > 150) {
|
||||
this.chartCpuMem.data.labels.shift();
|
||||
this.chartCpuMem.data.datasets[0].data.shift();
|
||||
this.chartCpuMem.data.datasets[1].data.shift();
|
||||
this.chartCpuMem.data.datasets[2].data.shift();
|
||||
this.chartNet.data.labels.shift();
|
||||
this.chartNet.data.datasets[0].data.shift();
|
||||
this.chartNet.data.datasets[1].data.shift();
|
||||
this.chartDisk.data.labels.shift();
|
||||
this.chartDisk.data.datasets[0].data.shift();
|
||||
this.chartDisk.data.datasets[1].data.shift();
|
||||
}
|
||||
this.chartCpuMem.update();
|
||||
this.chartNet.update();
|
||||
this.chartDisk.update();
|
||||
},
|
||||
|
||||
onStatsLog(statsLog) {
|
||||
for (const stats of statsLog.reverse()) {
|
||||
this.onStats(stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-instance-monitor {
|
||||
> section {
|
||||
> ._content {
|
||||
> .table {
|
||||
> .row {
|
||||
display: flex;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
> .cell {
|
||||
flex: 1;
|
||||
|
||||
> .label {
|
||||
font-size: 80%;
|
||||
opacity: 0.7;
|
||||
|
||||
> .icon {
|
||||
margin-right: 4px;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
370
src/client/pages/instance/settings.vue
Normal file
370
src/client/pages/instance/settings.vue
Normal file
@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div v-if="meta">
|
||||
<portal to="icon"><fa :icon="faCog"/></portal>
|
||||
<portal to="title">{{ $t('settings') }}</portal>
|
||||
|
||||
<section class="_card info">
|
||||
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input v-model="name">{{ $t('instanceName') }}</mk-input>
|
||||
<mk-textarea v-model="description">{{ $t('instanceDescription') }}</mk-textarea>
|
||||
<mk-input v-model="iconUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('iconUrl') }}</mk-input>
|
||||
<mk-input v-model="bannerUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</mk-input>
|
||||
<mk-input v-model="tosUrl"><template #icon><fa :icon="faLink"/></template>{{ $t('tosUrl') }}</mk-input>
|
||||
<mk-input v-model="maintainerName">{{ $t('maintainerName') }}</mk-input>
|
||||
<mk-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</mk-input>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card info">
|
||||
<div class="_content">
|
||||
<mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
|
||||
<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
|
||||
<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card info">
|
||||
<div class="_title"><fa :icon="faUser"/> {{ $t('registration') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableRegistration" @change="save()">{{ $t('enableRegistration') }}</mk-switch>
|
||||
<mk-button v-if="!enableRegistration" @click="invite">{{ $t('invite') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch>
|
||||
<template v-if="enableRecaptcha">
|
||||
<mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input>
|
||||
<mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_content" v-if="enableRecaptcha && recaptchaSiteKey">
|
||||
<header>{{ $t('preview') }}</header>
|
||||
<div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></mk-switch>
|
||||
<template v-if="enableServiceWorker">
|
||||
<mk-horizon-group inputs class="fit-bottom">
|
||||
<mk-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Public key</mk-input>
|
||||
<mk-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa :icon="faKey"/></template>Private key</mk-input>
|
||||
</mk-horizon-group>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
|
||||
<div class="_content">
|
||||
<mk-textarea v-model="pinnedUsers">
|
||||
<template #desc>{{ $t('pinnedUsersDescription') }} <button class="_textButton" @click="addPinUser">{{ $t('addUser') }}</button></template>
|
||||
</mk-textarea>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faCloud"/> {{ $t('files') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></mk-switch>
|
||||
<mk-switch v-model="proxyRemoteFiles">{{ $t('proxyRemoteFiles') }}<template #desc>{{ $t('proxyRemoteFilesDescription') }}</template></mk-switch>
|
||||
<mk-input v-model="localDriveCapacityMb" type="number">{{ $t('driveCapacityPerLocalAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
|
||||
<mk-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" style="margin-bottom: 0;">{{ $t('driveCapacityPerRemoteAccount') }}<template #suffix>MB</template><template #desc>{{ $t('inMb') }}</template></mk-input>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input :value="proxyAccount ? proxyAccount.username : null" style="margin: 0;" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></mk-input>
|
||||
<mk-button primary @click="chooseProxyAccount">{{ $t('chooseProxyAccount') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
|
||||
<div class="_content">
|
||||
<mk-textarea v-model="blockedHosts">
|
||||
<template #desc>{{ $t('blockedInstancesDescription') }}</template>
|
||||
</mk-textarea>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
|
||||
<div class="_content">
|
||||
<header><fa :icon="faTwitter"/> Twitter</header>
|
||||
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
|
||||
<template v-if="enableTwitterIntegration">
|
||||
<mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
|
||||
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
|
||||
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<header><fa :icon="faGithub"/> GitHub</header>
|
||||
<mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
|
||||
<template v-if="enableGithubIntegration">
|
||||
<mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
|
||||
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
|
||||
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<header><fa :icon="faDiscord"/> Discord</header>
|
||||
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
|
||||
<template v-if="enableDiscordIntegration">
|
||||
<mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
|
||||
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
|
||||
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
import MkTextarea from '../../components/ui/textarea.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
import MkInfo from '../../components/ui/info.vue';
|
||||
import MkUserSelect from '../../components/user-select.vue';
|
||||
import { url } from '../../config';
|
||||
import i18n from '../../i18n';
|
||||
import getAcct from '../../../misc/acct/render';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('instance') as string
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkSwitch,
|
||||
MkInfo,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
url,
|
||||
proxyAccount: null,
|
||||
proxyAccountId: null,
|
||||
cacheRemoteFiles: false,
|
||||
proxyRemoteFiles: false,
|
||||
localDriveCapacityMb: 0,
|
||||
remoteDriveCapacityMb: 0,
|
||||
blockedHosts: '',
|
||||
pinnedUsers: '',
|
||||
maintainerName: null,
|
||||
maintainerEmail: null,
|
||||
name: null,
|
||||
description: null,
|
||||
tosUrl: null,
|
||||
bannerUrl: null,
|
||||
iconUrl: null,
|
||||
maxNoteTextLength: 0,
|
||||
enableRegistration: false,
|
||||
enableLocalTimeline: false,
|
||||
enableGlobalTimeline: false,
|
||||
enableRecaptcha: false,
|
||||
recaptchaSiteKey: null,
|
||||
recaptchaSecretKey: null,
|
||||
enableServiceWorker: false,
|
||||
swPublicKey: null,
|
||||
swPrivateKey: null,
|
||||
enableTwitterIntegration: false,
|
||||
twitterConsumerKey: null,
|
||||
twitterConsumerSecret: null,
|
||||
enableGithubIntegration: false,
|
||||
githubClientId: null,
|
||||
githubClientSecret: null,
|
||||
enableDiscordIntegration: false,
|
||||
discordClientId: null,
|
||||
discordClientSecret: null,
|
||||
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
meta() {
|
||||
return this.$store.state.instance.meta;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.name = this.meta.name;
|
||||
this.description = this.meta.description;
|
||||
this.tosUrl = this.meta.tosUrl;
|
||||
this.bannerUrl = this.meta.bannerUrl;
|
||||
this.iconUrl = this.meta.iconUrl;
|
||||
this.maintainerName = this.meta.maintainerName;
|
||||
this.maintainerEmail = this.meta.maintainerEmail;
|
||||
this.maxNoteTextLength = this.meta.maxNoteTextLength;
|
||||
this.enableRegistration = !this.meta.disableRegistration;
|
||||
this.enableLocalTimeline = !this.meta.disableLocalTimeline;
|
||||
this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
|
||||
this.enableRecaptcha = this.meta.enableRecaptcha;
|
||||
this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
|
||||
this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
|
||||
this.proxyAccountId = this.meta.proxyAccountId;
|
||||
this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
|
||||
this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
|
||||
this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
|
||||
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
|
||||
this.blockedHosts = this.meta.blockedHosts.join('\n');
|
||||
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
|
||||
this.enableServiceWorker = this.meta.enableServiceWorker;
|
||||
this.swPublicKey = this.meta.swPublickey;
|
||||
this.swPrivateKey = this.meta.swPrivateKey;
|
||||
this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
|
||||
this.twitterConsumerKey = this.meta.twitterConsumerKey;
|
||||
this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
|
||||
this.enableGithubIntegration = this.meta.enableGithubIntegration;
|
||||
this.githubClientId = this.meta.githubClientId;
|
||||
this.githubClientSecret = this.meta.githubClientSecret;
|
||||
this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
|
||||
this.discordClientId = this.meta.discordClientId;
|
||||
this.discordClientSecret = this.meta.discordClientSecret;
|
||||
|
||||
if (this.proxyAccountId) {
|
||||
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
|
||||
this.proxyAccount = proxyAccount;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const renderRecaptchaPreview = () => {
|
||||
if (!(window as any).grecaptcha) return;
|
||||
if (!this.$refs.recaptcha) return;
|
||||
if (!this.recaptchaSiteKey) return;
|
||||
(window as any).grecaptcha.render(this.$refs.recaptcha, {
|
||||
sitekey: this.recaptchaSiteKey
|
||||
});
|
||||
};
|
||||
window.onRecaotchaLoad = () => {
|
||||
renderRecaptchaPreview();
|
||||
};
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad');
|
||||
head.appendChild(script);
|
||||
this.$watch('enableRecaptcha', () => {
|
||||
renderRecaptchaPreview();
|
||||
});
|
||||
this.$watch('recaptchaSiteKey', () => {
|
||||
renderRecaptchaPreview();
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
addPinUser() {
|
||||
this.$root.new(MkUserSelect, {}).$once('selected', user => {
|
||||
this.pinnedUsers = this.pinnedUsers.trim();
|
||||
this.pinnedUsers += '\n@' + getAcct(user);
|
||||
this.pinnedUsers = this.pinnedUsers.trim();
|
||||
});
|
||||
},
|
||||
|
||||
chooseProxyAccount() {
|
||||
this.$root.new(MkUserSelect, {}).$once('selected', user => {
|
||||
this.proxyAccount = user;
|
||||
this.proxyAccountId = user.id;
|
||||
this.save(true);
|
||||
});
|
||||
},
|
||||
|
||||
save(withDialog = false) {
|
||||
this.$root.api('admin/update-meta', {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
tosUrl: this.tosUrl,
|
||||
bannerUrl: this.bannerUrl,
|
||||
iconUrl: this.iconUrl,
|
||||
maintainerName: this.maintainerName,
|
||||
maintainerEmail: this.maintainerEmail,
|
||||
maxNoteTextLength: this.maxNoteTextLength,
|
||||
disableRegistration: !this.enableRegistration,
|
||||
disableLocalTimeline: !this.enableLocalTimeline,
|
||||
disableGlobalTimeline: !this.enableGlobalTimeline,
|
||||
enableRecaptcha: this.enableRecaptcha,
|
||||
recaptchaSiteKey: this.recaptchaSiteKey,
|
||||
recaptchaSecretKey: this.recaptchaSecretKey,
|
||||
proxyAccountId: this.proxyAccountId,
|
||||
cacheRemoteFiles: this.cacheRemoteFiles,
|
||||
proxyRemoteFiles: this.proxyRemoteFiles,
|
||||
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
|
||||
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
|
||||
blockedHosts: this.blockedHosts.split('\n') || [],
|
||||
pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
|
||||
enableServiceWorker: this.enableServiceWorker,
|
||||
swPublicKey: this.swPublicKey,
|
||||
swPrivateKey: this.swPrivateKey,
|
||||
enableTwitterIntegration: this.enableTwitterIntegration,
|
||||
twitterConsumerKey: this.twitterConsumerKey,
|
||||
twitterConsumerSecret: this.twitterConsumerSecret,
|
||||
enableGithubIntegration: this.enableGithubIntegration,
|
||||
githubClientId: this.githubClientId,
|
||||
githubClientSecret: this.githubClientSecret,
|
||||
enableDiscordIntegration: this.enableDiscordIntegration,
|
||||
discordClientId: this.discordClientId,
|
||||
discordClientSecret: this.discordClientSecret,
|
||||
}).then(() => {
|
||||
this.$store.dispatch('instance/fetch');
|
||||
if (withDialog) {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
}
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -6,7 +6,7 @@
|
||||
<section class="_card lookup">
|
||||
<div class="_title"><fa :icon="faSearch"/> {{ $t('lookup') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input class="target" v-model="target" type="text" @enter="showUser()" style="margin-top: 0;">
|
||||
<mk-input class="target" v-model="target" type="text" @enter="showUser()">
|
||||
<span>{{ $t('usernameOrUserId') }}</span>
|
||||
</mk-input>
|
||||
<mk-button @click="showUser()" primary><fa :icon="faSearch"/> {{ $t('lookup') }}</mk-button>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="content">
|
||||
<div class="balloon _panel" :data-no-text="message.text == null">
|
||||
<button class="delete-button" v-if="isMe" :title="$t('delete')" @click="del">
|
||||
<img src="/assets/desktop/remove.png" alt="Delete"/>
|
||||
<img src="/assets/remove.png" alt="Delete"/>
|
||||
</button>
|
||||
<div class="content" v-if="!message.isDeleted">
|
||||
<mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
|
||||
@ -230,7 +230,7 @@ export default Vue.extend({
|
||||
> footer {
|
||||
display: block;
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 10px;
|
||||
font-size: 0.65em;
|
||||
|
||||
> .read {
|
||||
margin: 0 8px;
|
||||
|
@ -31,7 +31,10 @@
|
||||
</div>
|
||||
</router-link>
|
||||
</sequential-entrance>
|
||||
<p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p>
|
||||
<div class="no-history" v-if="!fetching && messages.length == 0">
|
||||
<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
|
||||
<div>{{ $t('noHistory') }}</div>
|
||||
</div>
|
||||
<mk-loading v-if="fetching"/>
|
||||
</div>
|
||||
</template>
|
||||
@ -139,6 +142,14 @@ export default Vue.extend({
|
||||
async startGroup() {
|
||||
const groups1 = await this.$root.api('users/groups/owned');
|
||||
const groups2 = await this.$root.api('users/groups/joined');
|
||||
if (groups1.length === 0 && groups2.length === 0) {
|
||||
this.$root.dialog({
|
||||
type: 'warning',
|
||||
title: this.$t('noGroups'),
|
||||
text: this.$t('joinOrCreateGroup'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { canceled, result: group } = await this.$root.dialog({
|
||||
type: null,
|
||||
title: this.$t('group'),
|
||||
@ -277,24 +288,18 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
> .no-history {
|
||||
margin: 0;
|
||||
padding: 2em 1em;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
|
||||
> img {
|
||||
vertical-align: bottom;
|
||||
height: 128px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
> .search {
|
||||
> .result {
|
||||
> .users {
|
||||
> li {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .history {
|
||||
> .message {
|
||||
&:not([data-is-me]):not([data-is-read]) {
|
||||
@ -306,7 +311,7 @@ export default Vue.extend({
|
||||
|
||||
> div {
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
font-size: 0.9em;
|
||||
|
||||
> .avatar {
|
||||
margin: 0 12px 0 0;
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="shaynizk _card">
|
||||
<div class="_title" v-if="antenna.name">{{ antenna.name }}</div>
|
||||
<div class="_content body">
|
||||
<mk-input v-model="name" style="margin-top: 8px;">
|
||||
<mk-input v-model="name">
|
||||
<span>{{ $t('name') }}</span>
|
||||
</mk-input>
|
||||
<mk-select v-model="src">
|
||||
@ -11,12 +11,17 @@
|
||||
<option value="home">{{ $t('_antennaSources.homeTimeline') }}</option>
|
||||
<option value="users">{{ $t('_antennaSources.users') }}</option>
|
||||
<option value="list">{{ $t('_antennaSources.userList') }}</option>
|
||||
<option value="group">{{ $t('_antennaSources.userGroup') }}</option>
|
||||
</mk-select>
|
||||
<mk-select v-model="userListId" v-if="src === 'list'">
|
||||
<template #label>{{ $t('userList') }}</template>
|
||||
<option v-for="list in userLists" :value="list.id" :key="list.id">{{ list.name }}</option>
|
||||
</mk-select>
|
||||
<mk-textarea v-model="users" v-if="src === 'users'">
|
||||
<mk-select v-model="userGroupId" v-else-if="src === 'group'">
|
||||
<template #label>{{ $t('userGroup') }}</template>
|
||||
<option v-for="group in userGroups" :value="group.id" :key="group.id">{{ group.name }}</option>
|
||||
</mk-select>
|
||||
<mk-textarea v-model="users" v-else-if="src === 'users'">
|
||||
<span>{{ $t('users') }}</span>
|
||||
<template #desc>{{ $t('antennaUsersDescription') }} <button class="_textButton" @click="addUser">{{ $t('addUser') }}</button></template>
|
||||
</mk-textarea>
|
||||
@ -67,6 +72,7 @@ export default Vue.extend({
|
||||
name: '',
|
||||
src: '',
|
||||
userListId: null,
|
||||
userGroupId: null,
|
||||
users: '',
|
||||
keywords: '',
|
||||
caseSensitive: false,
|
||||
@ -74,6 +80,7 @@ export default Vue.extend({
|
||||
withFile: false,
|
||||
notify: false,
|
||||
userLists: null,
|
||||
userGroups: null,
|
||||
faSave, faTrash
|
||||
};
|
||||
},
|
||||
@ -83,6 +90,13 @@ export default Vue.extend({
|
||||
if (this.src === 'list' && this.userLists === null) {
|
||||
this.userLists = await this.$root.api('users/lists/list');
|
||||
}
|
||||
|
||||
if (this.src === 'group' && this.userGroups === null) {
|
||||
const groups1 = await this.$root.api('users/groups/owned');
|
||||
const groups2 = await this.$root.api('users/groups/joined');
|
||||
|
||||
this.userGroups = [...groups1, ...groups2];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -90,6 +104,7 @@ export default Vue.extend({
|
||||
this.name = this.antenna.name;
|
||||
this.src = this.antenna.src;
|
||||
this.userListId = this.antenna.userListId;
|
||||
this.userGroupId = this.antenna.userGroupId;
|
||||
this.users = this.antenna.users.join('\n');
|
||||
this.keywords = this.antenna.keywords.map(x => x.join(' ')).join('\n');
|
||||
this.caseSensitive = this.antenna.caseSensitive;
|
||||
@ -105,6 +120,7 @@ export default Vue.extend({
|
||||
name: this.name,
|
||||
src: this.src,
|
||||
userListId: this.userListId,
|
||||
userGroupId: this.userGroupId,
|
||||
withReplies: this.withReplies,
|
||||
withFile: this.withFile,
|
||||
notify: this.notify,
|
||||
@ -119,6 +135,7 @@ export default Vue.extend({
|
||||
name: this.name,
|
||||
src: this.src,
|
||||
userListId: this.userListId,
|
||||
userGroupId: this.userGroupId,
|
||||
withReplies: this.withReplies,
|
||||
withFile: this.withFile,
|
||||
notify: this.notify,
|
||||
|
@ -50,6 +50,7 @@ export default Vue.extend({
|
||||
name: '',
|
||||
src: 'all',
|
||||
userListId: null,
|
||||
userGroupId: null,
|
||||
users: [],
|
||||
keywords: [],
|
||||
withReplies: false,
|
||||
|
46
src/client/pages/my-settings/api.vue
Normal file
46
src/client/pages/my-settings/api.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faKey"/> API</div>
|
||||
<div class="_content">
|
||||
<mk-input :value="$store.state.i.token" readonly>
|
||||
<span>{{ $t('token') }}</span>
|
||||
</mk-input>
|
||||
<mk-button @click="regenerateToken"><fa :icon="faSyncAlt"/> {{ $t('regenerate') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faKey, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import i18n from '../../i18n';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
components: {
|
||||
MkButton, MkInput
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
faKey, faSyncAlt
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
regenerateToken() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('i/regenerate_token', {
|
||||
password: password
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
@ -2,8 +2,7 @@
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faBoxes"/> {{ $t('importAndExport') }}</div>
|
||||
<div class="_content">
|
||||
<input ref="file" type="file" style="display: none;" @change="onChangeFile"/>
|
||||
<mk-select v-model="exportTarget" style="margin-top: 0;">
|
||||
<mk-select v-model="exportTarget">
|
||||
<option value="notes">{{ $t('_exportOrImport.allNotes') }}</option>
|
||||
<option value="following">{{ $t('_exportOrImport.followingList') }}</option>
|
||||
<option value="user-lists">{{ $t('_exportOrImport.userLists') }}</option>
|
||||
@ -13,6 +12,7 @@
|
||||
<mk-button inline @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</mk-button>
|
||||
<mk-button inline @click="doImport()" :disabled="!['following', 'user-lists'].includes(exportTarget)"><fa :icon="faUpload"/> {{ $t('import') }}</mk-button>
|
||||
</div>
|
||||
<input ref="file" type="file" style="display: none;" @change="onChangeFile"/>
|
||||
</section>
|
||||
</template>
|
||||
|
109
src/client/pages/my-settings/index.vue
Normal file
109
src/client/pages/my-settings/index.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div>
|
||||
<portal to="icon"><fa :icon="faCog"/></portal>
|
||||
<portal to="title">{{ $t('accountSettings') }}</portal>
|
||||
|
||||
<x-profile-setting/>
|
||||
<x-privacy-setting/>
|
||||
<x-reaction-setting/>
|
||||
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
|
||||
{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
|
||||
</mk-switch>
|
||||
<mk-switch v-model="$store.state.i.injectFeaturedNote" @change="onChangeInjectFeaturedNote">
|
||||
{{ $t('showFeaturedNotesInTimeline') }}
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-button @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</mk-button>
|
||||
<mk-button @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</mk-button>
|
||||
<mk-button @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<x-import-export/>
|
||||
<x-drive/>
|
||||
<x-mute-block/>
|
||||
<x-security/>
|
||||
<x-2fa/>
|
||||
<x-integration/>
|
||||
<x-api/>
|
||||
|
||||
<mk-button @click="$root.signout()" primary style="margin: var(--margin) auto;">{{ $t('logout') }}</mk-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import XProfileSetting from './profile.vue';
|
||||
import XPrivacySetting from './privacy.vue';
|
||||
import XImportExport from './import-export.vue';
|
||||
import XDrive from './drive.vue';
|
||||
import XReactionSetting from './reaction.vue';
|
||||
import XMuteBlock from './mute-block.vue';
|
||||
import XSecurity from './security.vue';
|
||||
import X2fa from './2fa.vue';
|
||||
import XIntegration from './integration.vue';
|
||||
import XApi from './api.vue';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('settings') as string
|
||||
};
|
||||
},
|
||||
|
||||
components: {
|
||||
XProfileSetting,
|
||||
XPrivacySetting,
|
||||
XImportExport,
|
||||
XDrive,
|
||||
XReactionSetting,
|
||||
XMuteBlock,
|
||||
XSecurity,
|
||||
X2fa,
|
||||
XIntegration,
|
||||
XApi,
|
||||
MkButton,
|
||||
MkSwitch,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faCog
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onChangeAutoWatch(v) {
|
||||
this.$root.api('i/update', {
|
||||
autoWatch: v
|
||||
});
|
||||
},
|
||||
|
||||
onChangeInjectFeaturedNote(v) {
|
||||
this.$root.api('i/update', {
|
||||
injectFeaturedNote: v
|
||||
});
|
||||
},
|
||||
|
||||
readAllUnreadNotes() {
|
||||
this.$root.api('i/read_all_unread_notes');
|
||||
},
|
||||
|
||||
readAllMessagingMessages() {
|
||||
this.$root.api('i/read_all_messaging_messages');
|
||||
},
|
||||
|
||||
readAllNotifications() {
|
||||
this.$root.api('notifications/mark_all_as_read');
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
@ -2,7 +2,7 @@
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faLaugh"/> {{ $t('reaction') }}</div>
|
||||
<div class="_content">
|
||||
<mk-textarea v-model="reactions" style="margin-top: 16px;">{{ $t('reaction') }}<template #desc>{{ $t('reactionSettingDescription') }}</template></mk-textarea>
|
||||
<mk-textarea v-model="reactions">{{ $t('reaction') }}<template #desc>{{ $t('reactionSettingDescription') }}</template></mk-textarea>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
@ -5,7 +5,7 @@
|
||||
|
||||
<section class="_card">
|
||||
<div class="_content">
|
||||
<img src="https://xn--931a.moe/assets/not-found.png" alt=""/>
|
||||
<img src="https://xn--931a.moe/assets/not-found.png" class="_ghost"/>
|
||||
<div>{{ $t('notFoundDescription') }}</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -45,8 +45,6 @@ export default Vue.extend({
|
||||
height: 150px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,20 @@
|
||||
<portal to="avatar" v-if="note"><mk-avatar class="avatar" :user="note.user" :disable-preview="true"/></portal>
|
||||
<portal to="title" v-if="note">{{ $t('noteOf', { user: note.user.name }) }}</portal>
|
||||
|
||||
<transition name="zoom" mode="out-in">
|
||||
<x-note v-if="note" :note="note" :key="note.id" :detail="true"/>
|
||||
<div v-else-if="error">
|
||||
<mk-error @retry="fetch()"/>
|
||||
<transition :name="$store.state.device.animation ? 'zoom' : ''" mode="out-in">
|
||||
<div v-if="note">
|
||||
<mk-button v-if="hasNext && !showNext" @click="showNext = true" primary style="margin: 0 auto var(--margin) auto;"><fa :icon="faChevronUp"/></mk-button>
|
||||
<x-notes v-if="showNext" ref="next" :pagination="next"/>
|
||||
<hr v-if="showNext"/>
|
||||
|
||||
<x-note :note="note" :key="note.id" :detail="true"/>
|
||||
<div v-if="error">
|
||||
<mk-error @retry="fetch()"/>
|
||||
</div>
|
||||
|
||||
<mk-button v-if="hasPrev && !showPrev" @click="showPrev = true" primary style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></mk-button>
|
||||
<hr v-if="showPrev"/>
|
||||
<x-notes v-if="showPrev" ref="prev" :pagination="prev" style="margin-top: var(--margin);"/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
@ -14,9 +24,12 @@
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faChevronUp, faChevronDown } from '@fortawesome/free-solid-svg-icons';
|
||||
import i18n from '../i18n';
|
||||
import Progress from '../scripts/loading';
|
||||
import XNote from '../components/note.vue';
|
||||
import XNotes from '../components/notes.vue';
|
||||
import MkButton from '../components/ui/button.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
@ -26,12 +39,36 @@ export default Vue.extend({
|
||||
};
|
||||
},
|
||||
components: {
|
||||
XNote
|
||||
XNote,
|
||||
XNotes,
|
||||
MkButton,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
note: null,
|
||||
hasPrev: false,
|
||||
hasNext: false,
|
||||
showPrev: false,
|
||||
showNext: false,
|
||||
error: null,
|
||||
prev: {
|
||||
endpoint: 'users/notes',
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
userId: this.note.userId,
|
||||
untilId: this.note.id,
|
||||
})
|
||||
},
|
||||
next: {
|
||||
reversed: true,
|
||||
endpoint: 'users/notes',
|
||||
limit: 10,
|
||||
params: init => ({
|
||||
userId: this.note.userId,
|
||||
sinceId: this.note.id,
|
||||
})
|
||||
},
|
||||
faChevronUp, faChevronDown
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@ -46,7 +83,22 @@ export default Vue.extend({
|
||||
this.$root.api('notes/show', {
|
||||
noteId: this.$route.params.note
|
||||
}).then(note => {
|
||||
this.note = note;
|
||||
Promise.all([
|
||||
this.$root.api('users/notes', {
|
||||
userId: note.userId,
|
||||
untilId: note.id,
|
||||
limit: 1,
|
||||
}),
|
||||
this.$root.api('users/notes', {
|
||||
userId: note.userId,
|
||||
sinceId: note.id,
|
||||
limit: 1,
|
||||
}),
|
||||
]).then(([prev, next]) => {
|
||||
this.hasPrev = prev.length !== 0;
|
||||
this.hasNext = next.length !== 0;
|
||||
this.note = note;
|
||||
});
|
||||
}).catch(e => {
|
||||
this.error = e;
|
||||
}).finally(() => {
|
||||
|
@ -1,152 +0,0 @@
|
||||
<template>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
|
||||
<div class="_content">
|
||||
<mk-input type="file" @change="onWallpaperChange" style="margin-top: 0;">
|
||||
<span>{{ $t('wallpaper') }}</span>
|
||||
<template #icon><fa :icon="faImage"/></template>
|
||||
<template #desc v-if="wallpaperUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
|
||||
</mk-input>
|
||||
<mk-button primary :disabled="$store.state.settings.wallpaper == null" @click="delWallpaper()">{{ $t('removeWallpaper') }}</mk-button>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="autoReload">
|
||||
{{ $t('autoReloadWhenDisconnected') }}
|
||||
</mk-switch>
|
||||
<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
|
||||
{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-button @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</mk-button>
|
||||
<mk-button @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</mk-button>
|
||||
<mk-button @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</mk-button>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="reduceAnimation">{{ $t('reduceUiAnimation') }}</mk-switch>
|
||||
<mk-switch v-model="useOsNativeEmojis">
|
||||
{{ $t('useOsNativeEmojis') }}
|
||||
<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-select v-model="lang">
|
||||
<template #label>{{ $t('uiLanguage') }}</template>
|
||||
|
||||
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||
</mk-select>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faImage, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import i18n from '../../i18n';
|
||||
import { apiUrl, langs } from '../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
components: {
|
||||
MkInput,
|
||||
MkButton,
|
||||
MkSwitch,
|
||||
MkSelect,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
langs,
|
||||
lang: localStorage.getItem('lang'),
|
||||
wallpaperUploading: false,
|
||||
faImage, faCog
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
wallpaper: {
|
||||
get() { return this.$store.state.settings.wallpaper; },
|
||||
set(value) { this.$store.dispatch('settings/set', { key: 'wallpaper', value }); }
|
||||
},
|
||||
|
||||
autoReload: {
|
||||
get() { return this.$store.state.device.autoReload; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'autoReload', value }); }
|
||||
},
|
||||
|
||||
reduceAnimation: {
|
||||
get() { return !this.$store.state.device.animation; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'animation', value: !value }); }
|
||||
},
|
||||
|
||||
useOsNativeEmojis: {
|
||||
get() { return this.$store.state.device.useOsNativeEmojis; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'useOsNativeEmojis', value }); }
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
lang() {
|
||||
localStorage.setItem('lang', this.lang);
|
||||
localStorage.removeItem('locale');
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onWallpaperChange([file]) {
|
||||
this.wallpaperUploading = true;
|
||||
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
data.append('i', this.$store.state.i.token);
|
||||
|
||||
fetch(apiUrl + '/drive/files/create', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(f => {
|
||||
this.wallpaper = f.url;
|
||||
this.wallpaperUploading = false;
|
||||
document.documentElement.style.backgroundImage = `url(${this.$store.state.settings.wallpaper})`;
|
||||
})
|
||||
.catch(e => {
|
||||
this.wallpaperUploading = false;
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
delWallpaper() {
|
||||
this.wallpaper = null;
|
||||
document.documentElement.style.backgroundImage = 'none';
|
||||
},
|
||||
|
||||
onChangeAutoWatch(v) {
|
||||
this.$root.api('i/update', {
|
||||
autoWatch: v
|
||||
});
|
||||
},
|
||||
|
||||
readAllUnreadNotes() {
|
||||
this.$root.api('i/read_all_unread_notes');
|
||||
},
|
||||
|
||||
readAllMessagingMessages() {
|
||||
this.$root.api('i/read_all_messaging_messages');
|
||||
},
|
||||
|
||||
readAllNotifications() {
|
||||
this.$root.api('notifications/mark_all_as_read');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -1,42 +1,61 @@
|
||||
<template>
|
||||
<div class="mk-settings-page">
|
||||
<div>
|
||||
<portal to="icon"><fa :icon="faCog"/></portal>
|
||||
<portal to="title">{{ $t('settings') }}</portal>
|
||||
<portal to="title">{{ $t('clinetSettings') }}</portal>
|
||||
|
||||
<x-profile-setting/>
|
||||
<x-privacy-setting/>
|
||||
<x-reaction-setting/>
|
||||
<x-theme/>
|
||||
<x-import-export/>
|
||||
<x-drive/>
|
||||
<x-general/>
|
||||
<x-mute-block/>
|
||||
<x-security/>
|
||||
<x-2fa/>
|
||||
<x-integration/>
|
||||
|
||||
<mk-button @click="cacheClear()" primary class="cacheClear">{{ $t('cacheClear') }}</mk-button>
|
||||
<mk-button @click="$root.signout()" primary class="logout">{{ $t('logout') }}</mk-button>
|
||||
<section class="_card">
|
||||
<div class="_title"><fa :icon="faCog"/> {{ $t('accessibility') }}</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="autoReload">
|
||||
{{ $t('autoReloadWhenDisconnected') }}
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-switch v-model="imageNewTab">{{ $t('openImageInNewTab') }}</mk-switch>
|
||||
<mk-switch v-model="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</mk-switch>
|
||||
<mk-switch v-model="reduceAnimation">{{ $t('reduceUiAnimation') }}</mk-switch>
|
||||
<mk-switch v-model="useOsNativeEmojis">
|
||||
{{ $t('useOsNativeEmojis') }}
|
||||
<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
|
||||
</mk-switch>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-select v-model="lang">
|
||||
<template #label>{{ $t('uiLanguage') }}</template>
|
||||
|
||||
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||
</mk-select>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<div>{{ $t('fontSize') }}</div>
|
||||
<mk-radio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></mk-radio>
|
||||
<mk-radio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></mk-radio>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<mk-button @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</mk-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import XProfileSetting from './profile.vue';
|
||||
import XPrivacySetting from './privacy.vue';
|
||||
import XImportExport from './import-export.vue';
|
||||
import XDrive from './drive.vue';
|
||||
import XGeneral from './general.vue';
|
||||
import XReactionSetting from './reaction.vue';
|
||||
import XMuteBlock from './mute-block.vue';
|
||||
import XSecurity from './security.vue';
|
||||
import XTheme from './theme.vue';
|
||||
import X2fa from './2fa.vue';
|
||||
import XIntegration from './integration.vue';
|
||||
import { faImage, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkInput from '../../components/ui/input.vue';
|
||||
import MkButton from '../../components/ui/button.vue';
|
||||
import MkSwitch from '../../components/ui/switch.vue';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import MkRadio from '../../components/ui/radio.vue';
|
||||
import XTheme from './theme.vue';
|
||||
import i18n from '../../i18n';
|
||||
import { langs } from '../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('settings') as string
|
||||
@ -44,26 +63,67 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
components: {
|
||||
XProfileSetting,
|
||||
XPrivacySetting,
|
||||
XImportExport,
|
||||
XDrive,
|
||||
XGeneral,
|
||||
XReactionSetting,
|
||||
XMuteBlock,
|
||||
XSecurity,
|
||||
XTheme,
|
||||
X2fa,
|
||||
XIntegration,
|
||||
MkInput,
|
||||
MkButton,
|
||||
MkSwitch,
|
||||
MkSelect,
|
||||
MkRadio,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
faCog
|
||||
langs,
|
||||
lang: localStorage.getItem('lang'),
|
||||
fontSize: localStorage.getItem('fontSize'),
|
||||
faImage, faCog
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
autoReload: {
|
||||
get() { return this.$store.state.device.autoReload; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'autoReload', value }); }
|
||||
},
|
||||
|
||||
reduceAnimation: {
|
||||
get() { return !this.$store.state.device.animation; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'animation', value: !value }); }
|
||||
},
|
||||
|
||||
disableAnimatedMfm: {
|
||||
get() { return !this.$store.state.device.animatedMfm; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'animatedMfm', value: !value }); }
|
||||
},
|
||||
|
||||
useOsNativeEmojis: {
|
||||
get() { return this.$store.state.device.useOsNativeEmojis; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'useOsNativeEmojis', value }); }
|
||||
},
|
||||
|
||||
imageNewTab: {
|
||||
get() { return this.$store.state.device.imageNewTab; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'imageNewTab', value }); }
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
lang() {
|
||||
localStorage.setItem('lang', this.lang);
|
||||
localStorage.removeItem('locale');
|
||||
location.reload();
|
||||
},
|
||||
|
||||
fontSize() {
|
||||
if (this.fontSize == null) {
|
||||
localStorage.removeItem('fontSize');
|
||||
} else {
|
||||
localStorage.setItem('fontSize', this.fontSize);
|
||||
}
|
||||
location.reload();
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
cacheClear() {
|
||||
// Clear cache (service worker)
|
||||
@ -83,12 +143,3 @@ export default Vue.extend({
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mk-settings-page {
|
||||
> .logout,
|
||||
> .cacheClear {
|
||||
margin: 8px auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -12,6 +12,10 @@
|
||||
</optgroup>
|
||||
</mk-select>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<mk-button primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</mk-button>
|
||||
<mk-button primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</mk-button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@ -23,6 +27,7 @@ import MkButton from '../../components/ui/button.vue';
|
||||
import MkSelect from '../../components/ui/select.vue';
|
||||
import i18n from '../../i18n';
|
||||
import { Theme, builtinThemes, applyTheme } from '../../theme';
|
||||
import { selectFile } from '../../scripts/select-file';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
@ -35,7 +40,7 @@ export default Vue.extend({
|
||||
|
||||
data() {
|
||||
return {
|
||||
wallpaperUploading: false,
|
||||
wallpaper: localStorage.getItem('wallpaper'),
|
||||
faPalette
|
||||
}
|
||||
},
|
||||
@ -66,11 +71,25 @@ export default Vue.extend({
|
||||
watch: {
|
||||
theme() {
|
||||
applyTheme(this.themes.find(x => x.id === this.theme));
|
||||
},
|
||||
|
||||
|
||||
wallpaper() {
|
||||
if (this.wallpaper == null) {
|
||||
localStorage.removeItem('wallpaper');
|
||||
} else {
|
||||
localStorage.setItem('wallpaper', this.wallpaper);
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
setWallpaper(e) {
|
||||
selectFile(this, e.currentTarget || e.target, null, false).then(file => {
|
||||
this.wallpaper = file.url;
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -1,9 +1,15 @@
|
||||
<template>
|
||||
<x-notes ref="notes" :pagination="pagination" @before="before" @after="after"/>
|
||||
<div>
|
||||
<portal to="icon"><fa :icon="faHashtag"/></portal>
|
||||
<portal to="title">{{ $route.params.tag }}</portal>
|
||||
|
||||
<x-notes ref="notes" :pagination="pagination" @before="before" @after="after"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { faHashtag } from '@fortawesome/free-solid-svg-icons';
|
||||
import Progress from '../scripts/loading';
|
||||
import XNotes from '../components/notes.vue';
|
||||
|
||||
@ -26,7 +32,8 @@ export default Vue.extend({
|
||||
params: () => ({
|
||||
tag: this.$route.params.tag,
|
||||
})
|
||||
}
|
||||
},
|
||||
faHashtag
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -13,8 +13,8 @@
|
||||
<mk-user-name class="name" :user="user" :nowrap="true"/>
|
||||
<div class="bottom">
|
||||
<span class="username"><mk-acct :user="user" :detail="true" /></span>
|
||||
<span v-if="user.isAdmin" :title="$t('isAdmin')"><fa :icon="faBookmark"/></span>
|
||||
<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')"><fa :icon="farBookmark"/></span>
|
||||
<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><fa :icon="faBookmark"/></span>
|
||||
<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><fa :icon="farBookmark"/></span>
|
||||
<span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span>
|
||||
<span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span>
|
||||
</div>
|
||||
@ -30,8 +30,8 @@
|
||||
<mk-user-name :user="user" :nowrap="false" class="name"/>
|
||||
<div class="bottom">
|
||||
<span class="username"><mk-acct :user="user" :detail="true" /></span>
|
||||
<span v-if="user.isAdmin" :title="$t('isAdmin')"><fa :icon="faBookmark"/></span>
|
||||
<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')"><fa :icon="farBookmark"/></span>
|
||||
<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><fa :icon="faBookmark"/></span>
|
||||
<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><fa :icon="farBookmark"/></span>
|
||||
<span v-if="user.isLocked" :title="$t('isLocked')"><fa :icon="faLock"/></span>
|
||||
<span v-if="user.isBot" :title="$t('isBot')"><fa :icon="faRobot"/></span>
|
||||
</div>
|
||||
@ -250,6 +250,7 @@ export default Vue.extend({
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
|
||||
will-change: background-position;
|
||||
}
|
||||
|
||||
> .fade {
|
||||
@ -380,7 +381,7 @@ export default Vue.extend({
|
||||
|
||||
> .description {
|
||||
padding: 24px 24px 24px 154px;
|
||||
font-size: 15px;
|
||||
font-size: 0.95em;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 16px;
|
||||
@ -395,7 +396,7 @@ export default Vue.extend({
|
||||
|
||||
> .fields {
|
||||
padding: 24px;
|
||||
font-size: 14px;
|
||||
font-size: 0.9em;
|
||||
border-top: solid 1px var(--divider);
|
||||
|
||||
@media (max-width: 500px) {
|
||||
|
@ -38,20 +38,20 @@ export const router = new VueRouter({
|
||||
{ path: '/my/pages', name: 'pages', component: page('pages') },
|
||||
{ path: '/my/pages/new', component: page('page-editor/page-editor') },
|
||||
{ path: '/my/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
|
||||
{ path: '/my/settings', component: page('settings/index') },
|
||||
{ path: '/my/settings', component: page('my-settings/index') },
|
||||
{ path: '/my/follow-requests', component: page('follow-requests') },
|
||||
{ path: '/my/lists', component: page('my-lists/index') },
|
||||
{ path: '/my/lists/:list', component: page('my-lists/list') },
|
||||
{ path: '/my/groups', component: page('my-groups/index') },
|
||||
{ path: '/my/groups/:group', component: page('my-groups/group') },
|
||||
{ path: '/my/antennas', component: page('my-antennas/index') },
|
||||
{ path: '/settings', component: page('settings/index') },
|
||||
{ path: '/instance', component: page('instance/index') },
|
||||
{ path: '/instance/emojis', component: page('instance/emojis') },
|
||||
{ path: '/instance/users', component: page('instance/users') },
|
||||
{ path: '/instance/files', component: page('instance/files') },
|
||||
{ path: '/instance/monitor', component: page('instance/monitor') },
|
||||
{ path: '/instance/queue', component: page('instance/queue') },
|
||||
{ path: '/instance/stats', component: page('instance/stats') },
|
||||
{ path: '/instance/settings', component: page('instance/settings') },
|
||||
{ path: '/instance/federation', component: page('instance/federation') },
|
||||
{ path: '/instance/announcements', component: page('instance/announcements') },
|
||||
{ path: '/notes/:note', name: 'note', component: page('note') },
|
||||
|
@ -1,32 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
import { getScrollPosition, onScrollTop } from './scroll';
|
||||
|
||||
function getScrollContainer(el: Element | null): Element | null {
|
||||
if (el == null || el.tagName === 'BODY') return null;
|
||||
const style = window.getComputedStyle(el);
|
||||
if (style.getPropertyValue('overflow') === 'auto') {
|
||||
return el;
|
||||
} else {
|
||||
return getScrollContainer(el.parentElement);
|
||||
}
|
||||
}
|
||||
|
||||
function getScrollPosition(el: Element | null): number {
|
||||
const container = getScrollContainer(el);
|
||||
return container == null ? window.scrollY : container.scrollTop;
|
||||
}
|
||||
|
||||
function onScrollTop(el, cb) {
|
||||
const container = getScrollContainer(el) || window;
|
||||
const onScroll = ev => {
|
||||
if (!document.body.contains(el)) return;
|
||||
const pos = getScrollPosition(el);
|
||||
if (pos === 0) {
|
||||
cb();
|
||||
container.removeEventListener('scroll', onscroll);
|
||||
}
|
||||
};
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
}
|
||||
const SECOND_FETCH_LIMIT = 30;
|
||||
|
||||
export default (opts) => ({
|
||||
data() {
|
||||
@ -89,18 +64,18 @@ export default (opts) => ({
|
||||
if (params && params.then) params = await params;
|
||||
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
|
||||
await this.$root.api(endpoint, {
|
||||
...params,
|
||||
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
|
||||
...params
|
||||
}).then(x => {
|
||||
if (!this.pagination.noPaging && (x.length === (this.pagination.limit || 10) + 1)) {
|
||||
x.pop();
|
||||
this.items = x;
|
||||
}).then(items => {
|
||||
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
|
||||
items.pop();
|
||||
this.items = this.pagination.reversed ? [...items].reverse() : items;
|
||||
this.more = true;
|
||||
} else {
|
||||
this.items = x;
|
||||
this.items = this.pagination.reversed ? [...items].reverse() : items;
|
||||
this.more = false;
|
||||
}
|
||||
this.offset = x.length;
|
||||
this.offset = items.length;
|
||||
this.inited = true;
|
||||
this.fetching = false;
|
||||
if (opts.after) opts.after(this, null);
|
||||
@ -118,23 +93,25 @@ export default (opts) => ({
|
||||
if (params && params.then) params = await params;
|
||||
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
|
||||
await this.$root.api(endpoint, {
|
||||
limit: (this.pagination.limit || 10) + 1,
|
||||
...params,
|
||||
limit: SECOND_FETCH_LIMIT + 1,
|
||||
...(this.pagination.offsetMode ? {
|
||||
offset: this.offset,
|
||||
} : this.pagination.reversed ? {
|
||||
sinceId: this.items[0].id,
|
||||
} : {
|
||||
untilId: this.items[this.items.length - 1].id,
|
||||
}),
|
||||
...params
|
||||
}).then(x => {
|
||||
if (x.length === (this.pagination.limit || 10) + 1) {
|
||||
x.pop();
|
||||
this.items = this.items.concat(x);
|
||||
}).then(items => {
|
||||
if (items.length > SECOND_FETCH_LIMIT) {
|
||||
items.pop();
|
||||
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
|
||||
this.more = true;
|
||||
} else {
|
||||
this.items = this.items.concat(x);
|
||||
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
|
||||
this.more = false;
|
||||
}
|
||||
this.offset += x.length;
|
||||
this.offset += items.length;
|
||||
this.moreFetching = false;
|
||||
}, e => {
|
||||
this.moreFetching = false;
|
||||
|
27
src/client/scripts/scroll.ts
Normal file
27
src/client/scripts/scroll.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export function getScrollContainer(el: Element | null): Element | null {
|
||||
if (el == null || el.tagName === 'BODY') return null;
|
||||
const style = window.getComputedStyle(el);
|
||||
if (style.getPropertyValue('overflow') === 'auto') {
|
||||
return el;
|
||||
} else {
|
||||
return getScrollContainer(el.parentElement);
|
||||
}
|
||||
}
|
||||
|
||||
export function getScrollPosition(el: Element | null): number {
|
||||
const container = getScrollContainer(el);
|
||||
return container == null ? window.scrollY : container.scrollTop;
|
||||
}
|
||||
|
||||
export function onScrollTop(el: Element, cb) {
|
||||
const container = getScrollContainer(el) || window;
|
||||
const onScroll = ev => {
|
||||
if (!document.body.contains(el)) return;
|
||||
const pos = getScrollPosition(el);
|
||||
if (pos === 0) {
|
||||
cb();
|
||||
container.removeEventListener('scroll', onscroll);
|
||||
}
|
||||
};
|
||||
container.addEventListener('scroll', onScroll, { passive: true });
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { faUpload, faCloud, faLink } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faUpload, faCloud } from '@fortawesome/free-solid-svg-icons';
|
||||
import { selectDriveFile } from './select-drive-file';
|
||||
import { apiUrl } from '../config';
|
||||
|
||||
export function selectFile(component: any, src: any, label: string, multiple = false) {
|
||||
export function selectFile(component: any, src: any, label: string | null, multiple = false) {
|
||||
return new Promise((res, rej) => {
|
||||
const chooseFileFromPc = () => {
|
||||
const input = document.createElement('input');
|
||||
@ -56,10 +56,10 @@ export function selectFile(component: any, src: any, label: string, multiple = f
|
||||
};
|
||||
|
||||
component.$root.menu({
|
||||
items: [{
|
||||
items: [label ? {
|
||||
text: label,
|
||||
type: 'label'
|
||||
}, {
|
||||
} : undefined, {
|
||||
text: component.$t('upload'),
|
||||
icon: faUpload,
|
||||
action: chooseFileFromPc
|
||||
|
@ -13,7 +13,6 @@ const defaultSettings = {
|
||||
defaultNoteLocalOnly: false,
|
||||
uploadFolder: null,
|
||||
pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
|
||||
wallpaper: null,
|
||||
memo: null,
|
||||
reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
|
||||
};
|
||||
@ -38,6 +37,8 @@ const defaultDeviceSettings = {
|
||||
themes: [],
|
||||
theme: 'light',
|
||||
animation: true,
|
||||
animatedMfm: true,
|
||||
imageNewTab: false,
|
||||
userData: {},
|
||||
};
|
||||
|
||||
|
@ -58,6 +58,18 @@ html {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.f-small {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.f-large {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
&.f-veryLarge {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
html.changing-theme {
|
||||
@ -116,6 +128,13 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: var(--margin) 0 var(--margin) 0;
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: var(--divider);
|
||||
}
|
||||
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
@ -164,6 +183,19 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
._noSelect {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
._ghost {
|
||||
&, * {
|
||||
@extend ._noSelect;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
._button {
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
@ -175,9 +207,7 @@ a {
|
||||
font-size: 1em;
|
||||
|
||||
&, * {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
@extend ._noSelect;
|
||||
}
|
||||
|
||||
* {
|
||||
|
@ -9,8 +9,8 @@ export type Theme = {
|
||||
props: { [key: string]: string };
|
||||
};
|
||||
|
||||
export const lightTheme: Theme = require('./themes/light.json5');
|
||||
export const darkTheme: Theme = require('./themes/dark.json5');
|
||||
export const lightTheme: Theme = require('./themes/_light.json5');
|
||||
export const darkTheme: Theme = require('./themes/_dark.json5');
|
||||
|
||||
export const builtinThemes = [
|
||||
lightTheme,
|
||||
@ -52,7 +52,7 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||
|
||||
for (const tag of document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', props['accent']);
|
||||
tag.setAttribute('content', props['html']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,15 @@
|
||||
focus: ':alpha<0.3<@accent',
|
||||
bg: '#000',
|
||||
fg: '#c7d1d8',
|
||||
fgHighlighted: ':lighten<3<@fg',
|
||||
html: '@bg',
|
||||
indicator: '@accent',
|
||||
panel: '#111213',
|
||||
shadow: 'rgba(0, 0, 0, 0.1)',
|
||||
header: 'rgba(20, 20, 20, 0.75)',
|
||||
navBg: '@panel',
|
||||
navFg: '@fg',
|
||||
navHoverFg: ':lighten<17<@fg',
|
||||
navActive: '@accent',
|
||||
navIndicator: '@accent',
|
||||
link: '#44a4c1',
|
||||
@ -45,11 +49,12 @@
|
||||
inputBorder: '#959da2',
|
||||
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
|
||||
badge: '#31b1ce',
|
||||
bonzsgfz: ':alpha<0<@bg',
|
||||
pcncwizz: ':darken<2<@panel',
|
||||
vocsgcxy: 'rgba(0, 0, 0, 0.5)',
|
||||
yrnqrguo: 'rgba(255, 255, 255, 0.05)',
|
||||
mkykhqkw: ':lighten<3<@fg',
|
||||
nwjktjjq: 'rgba(255, 255, 255, 0.1)',
|
||||
geavgsxy: 'rgba(255, 255, 255, 0.05)',
|
||||
nhzhphzx: 'rgba(255, 255, 255, 0.15)',
|
@ -14,11 +14,15 @@
|
||||
focus: ':alpha<0.3<@accent',
|
||||
bg: '#fafafa',
|
||||
fg: '#5c6a73',
|
||||
fgHighlighted: ':darken<3<@fg',
|
||||
html: '@bg',
|
||||
indicator: '@accent',
|
||||
panel: '#fff',
|
||||
shadow: 'rgba(0, 0, 0, 0.1)',
|
||||
header: 'rgba(255, 255, 255, 0.75)',
|
||||
navBg: '@panel',
|
||||
navFg: '@fg',
|
||||
navHoverFg: ':darken<17<@fg',
|
||||
navActive: '@accent',
|
||||
navIndicator: '@accent',
|
||||
link: '#44a4c1',
|
||||
@ -45,11 +49,12 @@
|
||||
inputBorder: '#dae0e4',
|
||||
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
|
||||
driveFolderBg: ':alpha<0.3<@accent',
|
||||
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
|
||||
badge: '#31b1ce',
|
||||
bonzsgfz: ':alpha<0<@bg',
|
||||
pcncwizz: ':darken<2<@panel',
|
||||
vocsgcxy: 'rgba(255, 255, 255, 0.5)',
|
||||
yrnqrguo: 'rgba(0, 0, 0, 0.05)',
|
||||
mkykhqkw: ':darken<3<@fg',
|
||||
nwjktjjq: 'rgba(0, 0, 0, 0.1)',
|
||||
geavgsxy: 'rgba(0, 0, 0, 0.05)',
|
||||
nhzhphzx: 'rgba(0, 0, 0, 0.25)',
|
@ -12,6 +12,7 @@
|
||||
panel: '#1f1d30',
|
||||
bg: '#0f0e17',
|
||||
fg: '#b1bee3',
|
||||
html: '@accent',
|
||||
renote: '@accent',
|
||||
},
|
||||
}
|
||||
|
84
src/client/widgets/activity.calendar.vue
Normal file
84
src/client/widgets/activity.calendar.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 21 7">
|
||||
<rect v-for="record in data" class="day"
|
||||
width="1" height="1"
|
||||
:x="record.x" :y="record.date.weekday"
|
||||
rx="1" ry="1"
|
||||
fill="transparent">
|
||||
<title>{{ record.date.year }}/{{ record.date.month + 1 }}/{{ record.date.day }}</title>
|
||||
</rect>
|
||||
<rect v-for="record in data" class="day"
|
||||
:width="record.v" :height="record.v"
|
||||
:x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)"
|
||||
rx="1" ry="1"
|
||||
:fill="record.color"
|
||||
style="pointer-events: none;"/>
|
||||
<rect class="today"
|
||||
width="1" height="1"
|
||||
:x="data[0].x" :y="data[0].date.weekday"
|
||||
rx="1" ry="1"
|
||||
fill="none"
|
||||
stroke-width="0.1"
|
||||
stroke="#f73520"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['data'],
|
||||
created() {
|
||||
for (const d of this.data) {
|
||||
d.total = d.notes + d.replies + d.renotes;
|
||||
}
|
||||
const peak = Math.max.apply(null, this.data.map(d => d.total));
|
||||
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth();
|
||||
const day = now.getDate();
|
||||
|
||||
let x = 20;
|
||||
this.data.slice().forEach((d, i) => {
|
||||
d.x = x;
|
||||
|
||||
const date = new Date(year, month, day - i);
|
||||
d.date = {
|
||||
year: date.getFullYear(),
|
||||
month: date.getMonth(),
|
||||
day: date.getDate(),
|
||||
weekday: date.getDay()
|
||||
};
|
||||
|
||||
d.v = peak == 0 ? 0 : d.total / (peak / 2);
|
||||
if (d.v > 1) d.v = 1;
|
||||
const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
|
||||
const cs = d.v * 100;
|
||||
const cl = 15 + ((1 - d.v) * 80);
|
||||
d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
|
||||
|
||||
if (d.date.weekday == 0) x--;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
svg {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
> rect {
|
||||
transform-origin: center;
|
||||
|
||||
&.day {
|
||||
&:hover {
|
||||
fill: rgba(#000, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
108
src/client/widgets/activity.chart.vue
Normal file
108
src/client/widgets/activity.chart.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" @mousedown.prevent="onMousedown">
|
||||
<polyline
|
||||
:points="pointsNote"
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#41ddde"/>
|
||||
<polyline
|
||||
:points="pointsReply"
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#f7796c"/>
|
||||
<polyline
|
||||
:points="pointsRenote"
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#a1de41"/>
|
||||
<polyline
|
||||
:points="pointsTotal"
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#555"
|
||||
stroke-dasharray="2 2"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../i18n';
|
||||
|
||||
function dragListen(fn) {
|
||||
window.addEventListener('mousemove', fn);
|
||||
window.addEventListener('mouseleave', dragClear.bind(null, fn));
|
||||
window.addEventListener('mouseup', dragClear.bind(null, fn));
|
||||
}
|
||||
|
||||
function dragClear(fn) {
|
||||
window.removeEventListener('mousemove', fn);
|
||||
window.removeEventListener('mouseleave', dragClear);
|
||||
window.removeEventListener('mouseup', dragClear);
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
i18n,
|
||||
props: ['data'],
|
||||
data() {
|
||||
return {
|
||||
viewBoxX: 147,
|
||||
viewBoxY: 60,
|
||||
zoom: 1,
|
||||
pos: 0,
|
||||
pointsNote: null,
|
||||
pointsReply: null,
|
||||
pointsRenote: null,
|
||||
pointsTotal: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
for (const d of this.data) {
|
||||
d.total = d.notes + d.replies + d.renotes;
|
||||
}
|
||||
|
||||
this.render();
|
||||
},
|
||||
methods: {
|
||||
render() {
|
||||
const peak = Math.max.apply(null, this.data.map(d => d.total));
|
||||
if (peak != 0) {
|
||||
const data = this.data.slice().reverse();
|
||||
this.pointsNote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
|
||||
this.pointsReply = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
|
||||
this.pointsRenote = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
|
||||
this.pointsTotal = data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
|
||||
}
|
||||
},
|
||||
onMousedown(e) {
|
||||
const clickX = e.clientX;
|
||||
const clickY = e.clientY;
|
||||
const baseZoom = this.zoom;
|
||||
const basePos = this.pos;
|
||||
|
||||
// 動かした時
|
||||
dragListen(me => {
|
||||
let moveLeft = me.clientX - clickX;
|
||||
let moveTop = me.clientY - clickY;
|
||||
|
||||
this.zoom = baseZoom + (-moveTop / 20);
|
||||
this.pos = basePos + moveLeft;
|
||||
if (this.zoom < 1) this.zoom = 1;
|
||||
if (this.pos > 0) this.pos = 0;
|
||||
if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
|
||||
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
svg {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
cursor: all-scroll;
|
||||
}
|
||||
</style>
|
80
src/client/widgets/activity.vue
Normal file
80
src/client/widgets/activity.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-container :show-header="props.design === 0" :naked="props.design === 2">
|
||||
<template #header><fa :icon="faChartBar"/>{{ $t('_widgets.activity') }}</template>
|
||||
<template #func><button @click="toggleView()" class="_button"><fa :icon="faSort"/></button></template>
|
||||
|
||||
<div>
|
||||
<mk-loading v-if="fetching"/>
|
||||
<template v-else>
|
||||
<x-calendar v-show="props.view === 0" :data="[].concat(activity)"/>
|
||||
<x-chart v-show="props.view === 1" :data="[].concat(activity)"/>
|
||||
</template>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { faChartBar, faSort } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkContainer from '../components/ui/container.vue';
|
||||
import define from './define';
|
||||
import i18n from '../i18n';
|
||||
import XCalendar from './activity.calendar.vue';
|
||||
import XChart from './activity.chart.vue';
|
||||
|
||||
export default define({
|
||||
name: 'activity',
|
||||
props: () => ({
|
||||
design: 0,
|
||||
view: 0
|
||||
})
|
||||
}).extend({
|
||||
i18n,
|
||||
components: {
|
||||
MkContainer,
|
||||
XCalendar,
|
||||
XChart,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetching: true,
|
||||
activity: null,
|
||||
faChartBar, faSort
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$root.api('charts/user/notes', {
|
||||
userId: this.$store.state.i.id,
|
||||
span: 'day',
|
||||
limit: 7 * 21
|
||||
}).then(activity => {
|
||||
this.activity = activity.diffs.normal.map((_, i) => ({
|
||||
total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i],
|
||||
notes: activity.diffs.normal[i],
|
||||
replies: activity.diffs.reply[i],
|
||||
renotes: activity.diffs.renote[i]
|
||||
}));
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
if (this.props.design == 2) {
|
||||
this.props.design = 0;
|
||||
} else {
|
||||
this.props.design++;
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
toggleView() {
|
||||
if (this.props.view == 1) {
|
||||
this.props.view = 0;
|
||||
} else {
|
||||
this.props.view++;
|
||||
}
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -132,7 +132,7 @@ export default define({
|
||||
> p {
|
||||
margin: 0;
|
||||
line-height: 18px;
|
||||
font-size: 14px;
|
||||
font-size: 0.9em;
|
||||
|
||||
> span {
|
||||
margin: 0 4px;
|
||||
@ -142,7 +142,7 @@ export default define({
|
||||
> .day {
|
||||
margin: 10px 0;
|
||||
line-height: 32px;
|
||||
font-size: 28px;
|
||||
font-size: 1.75em;
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,7 +162,7 @@ export default define({
|
||||
|
||||
> p {
|
||||
margin: 0 0 2px 0;
|
||||
font-size: 12px;
|
||||
font-size: 0.75em;
|
||||
line-height: 18px;
|
||||
opacity: 0.8;
|
||||
|
||||
|
44
src/client/widgets/clock.vue
Normal file
44
src/client/widgets/clock.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-container :naked="props.style % 2 === 0" :show-header="false">
|
||||
<div class="vubelbmv">
|
||||
<mk-analog-clock class="clock" :smooth="props.style < 2"/>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import define from './define';
|
||||
import MkContainer from '../components/ui/container.vue';
|
||||
import MkAnalogClock from '../components/analog-clock.vue';
|
||||
|
||||
export default define({
|
||||
name: 'clock',
|
||||
props: () => ({
|
||||
style: 0
|
||||
})
|
||||
}).extend({
|
||||
components: {
|
||||
MkContainer,
|
||||
MkAnalogClock
|
||||
},
|
||||
methods: {
|
||||
func() {
|
||||
this.props.style = (this.props.style + 1) % 4;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vubelbmv {
|
||||
padding: 8px;
|
||||
|
||||
> .clock {
|
||||
height: 150px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -6,3 +6,6 @@ Vue.component('mkw-timeline', () => import('./timeline.vue').then(m => m.default
|
||||
Vue.component('mkw-calendar', () => import('./calendar.vue').then(m => m.default));
|
||||
Vue.component('mkw-rss', () => import('./rss.vue').then(m => m.default));
|
||||
Vue.component('mkw-trends', () => import('./trends.vue').then(m => m.default));
|
||||
Vue.component('mkw-clock', () => import('./clock.vue').then(m => m.default));
|
||||
Vue.component('mkw-activity', () => import('./activity.vue').then(m => m.default));
|
||||
Vue.component('mkw-photos', () => import('./photos.vue').then(m => m.default));
|
||||
|
@ -83,7 +83,6 @@ export default define({
|
||||
}
|
||||
|
||||
.tl {
|
||||
height: 100%;
|
||||
background: var(--bg);
|
||||
}
|
||||
}
|
||||
|
116
src/client/widgets/photos.vue
Normal file
116
src/client/widgets/photos.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div>
|
||||
<mk-container :show-header="props.design === 0" :naked="props.design === 2" :class="$style.root" :data-melt="props.design == 2">
|
||||
<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template>
|
||||
|
||||
<div class="">
|
||||
<mk-loading v-if="fetching"/>
|
||||
<div v-else :class="$style.stream">
|
||||
<div v-for="(image, i) in images" :key="i"
|
||||
:class="$style.img"
|
||||
:style="`background-image: url(${thumbnail(image)})`"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</mk-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { faCamera } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkContainer from '../components/ui/container.vue';
|
||||
import define from './define';
|
||||
import i18n from '../i18n';
|
||||
import { getStaticImageUrl } from '../scripts/get-static-image-url';
|
||||
|
||||
export default define({
|
||||
name: 'photos',
|
||||
props: () => ({
|
||||
design: 0,
|
||||
})
|
||||
}).extend({
|
||||
i18n,
|
||||
components: {
|
||||
MkContainer,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
images: [],
|
||||
fetching: true,
|
||||
connection: null,
|
||||
faCamera
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.connection = this.$root.stream.useSharedConnection('main');
|
||||
|
||||
this.connection.on('driveFileCreated', this.onDriveFileCreated);
|
||||
|
||||
this.$root.api('drive/stream', {
|
||||
type: 'image/*',
|
||||
limit: 9
|
||||
}).then(images => {
|
||||
this.images = images;
|
||||
this.fetching = false;
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
methods: {
|
||||
onDriveFileCreated(file) {
|
||||
if (/^image\/.+$/.test(file.type)) {
|
||||
this.images.unshift(file);
|
||||
if (this.images.length > 9) this.images.pop();
|
||||
}
|
||||
},
|
||||
|
||||
func() {
|
||||
if (this.props.design == 2) {
|
||||
this.props.design = 0;
|
||||
} else {
|
||||
this.props.design++;
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
|
||||
thumbnail(image: any): string {
|
||||
return this.$store.state.device.disableShowingAnimatedImages
|
||||
? getStaticImageUrl(image.thumbnailUrl)
|
||||
: image.thumbnailUrl;
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root[data-melt] {
|
||||
.stream {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.img {
|
||||
border: solid 4px transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.stream {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px;
|
||||
|
||||
.img {
|
||||
flex: 1 1 33%;
|
||||
width: 33%;
|
||||
height: 80px;
|
||||
box-sizing: border-box;
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
background-clip: content-box;
|
||||
border: solid 2px transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user