Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
d153d3b761 | |||
60561578d8 | |||
b8bc96c181 | |||
005d6b4ccf | |||
f972fb6920 | |||
77078f2ce2 | |||
dae6d60163 | |||
264d6afe5d | |||
52c1d87fa2 | |||
dccafca58d | |||
52c12f96d5 | |||
114a9fbdb2 | |||
20e67e7edd | |||
889b52e813 | |||
6c975275f8 | |||
1286dee1ab | |||
fee953b0df | |||
26e7454f0e | |||
edbaa07867 | |||
f7ebf14501 | |||
d00928cf3f | |||
b3a198cae6 | |||
8b87d56a5a | |||
62ac3400da | |||
d54f71774e | |||
a47358f590 | |||
8f2cf066b6 | |||
e3e5a4272b | |||
2dcb3af70f | |||
3a19e9f80e | |||
c6fe798092 | |||
e74a47916d | |||
761071e1ce | |||
9e3610d513 | |||
393ac6c203 | |||
b7a79d25b0 | |||
486f045751 | |||
d6e3ec3218 | |||
078b043cae | |||
296cbc2e5a | |||
a74beaac36 | |||
79f8eb910f |
@ -62,6 +62,8 @@ Organize and store your files! Want to post a picture you have already uploaded?
|
||||
|
||||
...and more! Experience Misskey with your own eyes at [misskey.io](https://misskey.io/) or join one of the [other instances](https://joinmisskey.github.io/) that are available.
|
||||
|
||||
To recive updates of this repo, follow [@repo@misskey.io](https://misskey.io/@repo) on fediverse.
|
||||
|
||||
Screen shots
|
||||
----------------------------------------------------------------
|
||||
### Profile page
|
||||
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 9.1 KiB |
@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(0.413372,0,0,0.469741,64.564,40.5821)">
|
||||
<rect x="-156.189" y="-86.393" width="619.297" height="544.981" style="fill:rgb(27,30,31);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.898356,0,0,0.898356,-130.722,-120.968)">
|
||||
<g transform="matrix(0.5,0.866025,-0.866025,0.5,288,-166.277)">
|
||||
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-96,166.277)">
|
||||
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.5,-0.866025,0.866025,0.5,-96,498.831)">
|
||||
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-95.9902,55.4086)">
|
||||
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:rgb(150,208,74);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.5,-0.866025,0.866025,0.5,-2.64322e-11,554.256)">
|
||||
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:rgb(150,208,74);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.5,0.866025,-0.866025,0.5,192,-110.851)">
|
||||
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:rgb(150,208,74);"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
BIN
assets/mi-white.afdesign
Normal file
BIN
assets/mi.afdesign
Normal file
@ -45,6 +45,7 @@ copyUsername: "انسخ اسم المستخدم"
|
||||
searchUser: "ابحث عن مستخدمين"
|
||||
reply: "رد"
|
||||
loadMore: "عرض المزيد"
|
||||
showMore: "عرض المزيد"
|
||||
youGotNewFollower: "يتابعك"
|
||||
receiveFollowRequest: "تلقيت طلب متابعة"
|
||||
followRequestAccepted: "قُبل طلب المتابعة"
|
||||
@ -175,16 +176,23 @@ remove: "حذف"
|
||||
removed: "تم حذفه بنجاح"
|
||||
removeAreYouSure: "متأكد من أنك تريد حذف {x}؟"
|
||||
deleteAreYouSure: "متأكد من أنك تريد حذف {x}؟"
|
||||
resetAreYouSure: "هل تريد إعادة التعيين؟"
|
||||
saved: "تم حفظه"
|
||||
messaging: "الدردشة"
|
||||
upload: "تحميل"
|
||||
fromDrive: "من المخزن"
|
||||
fromUrl: "من عنوان URL"
|
||||
uploadFromUrl: "التحميل عبر URL"
|
||||
uploadFromUrlDescription: "رابط الملف المراد تحميله "
|
||||
uploadFromUrlRequested: "الرفع مطلوب"
|
||||
uploadFromUrlMayTakeTime: "سيستغرق بعض الوقت لاتمام الرفع "
|
||||
explore: "استكشاف"
|
||||
games: "ألعاب Misskey"
|
||||
messageRead: "مقروءة"
|
||||
noMoreHistory: "لا يوجد المزيد من التاريخ"
|
||||
startMessaging: "ابدأ الدردشة"
|
||||
nUsersRead: "تمت القراءة من {n}"
|
||||
agreeTo: "اوافق على {0}"
|
||||
tos: "شروط الخدمة"
|
||||
start: "البداية"
|
||||
home: "الرئيسي"
|
||||
@ -201,6 +209,7 @@ light: "فاتح"
|
||||
dark: "داكن"
|
||||
lightThemes: "الحلة الفاتحة"
|
||||
darkThemes: "الحلة الداكنة"
|
||||
syncDeviceDarkMode: "مطابقة الوضع المضلمومع اعدادات الجهاز"
|
||||
drive: "قرص التخرين"
|
||||
fileName: "اسم الملف"
|
||||
selectFile: "اختر ملفًا"
|
||||
@ -218,14 +227,17 @@ emptyFolder: "هذا المجلد فارغ"
|
||||
unableToDelete: "لا يمكن حذفه"
|
||||
inputNewFileName: "ادخل الإسم الجديد للملف"
|
||||
inputNewFolderName: "ادخل الإسم الجديد للمجلد"
|
||||
hasChildFilesOrFolders: "الان الملف غير فارغ. لا يمكن حذفه"
|
||||
copyUrl: "انسخ عنوان URL"
|
||||
rename: "إعادة التسمية"
|
||||
avatar: "الصورة الرمزية"
|
||||
banner: "الصورة الرأسية"
|
||||
nsfw: "محتوى حساس"
|
||||
whenServerDisconnected: "عند فقدان الاتصال بالخادم"
|
||||
disconnectedFromServer: "قُطِع الإتصال بالخادم"
|
||||
reload: "انعش"
|
||||
doNothing: "تجاهل"
|
||||
reloadConfirm: "هل ترغب في تحديث الجدول الزمني؟"
|
||||
watch: "راقب"
|
||||
unwatch: "إلغاء المراقبة"
|
||||
accept: "السماح"
|
||||
@ -243,10 +255,12 @@ dayX: "{day}"
|
||||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "الصفحات"
|
||||
integration: "دمج"
|
||||
connectSerice: "أوصل"
|
||||
disconnectSerice: "قطع الاتصال"
|
||||
enableLocalTimeline: "تفعيل الخيط المحلي"
|
||||
enableGlobalTimeline: "تفعيل الخيط الزمني الشامل"
|
||||
disablingTimelinesInfo: "سيتمكن المسؤولون ومن تعديل دائمًا و من الوصول إلى جميع المخططات الزمنية ، حتى إذا لم يتم تمكينها."
|
||||
registration: "إنشاء حساب"
|
||||
enableRegistration: "تفعيل إنشاء الحسابات الجديدة"
|
||||
invite: "دعوة"
|
||||
@ -288,6 +302,8 @@ resetPassword: "أعد تعيين كلمتك السرية"
|
||||
newPasswordIs: "كلمتك السرية الجديدة هي {password}"
|
||||
share: "شارِك"
|
||||
notFound: "غير موجود"
|
||||
cacheClear: "مسح ذاكرة التخزين المؤقت"
|
||||
markAsReadAllNotifications: "وضع جميع الإشعارات كأنها مقروءة"
|
||||
help: "المساعدة"
|
||||
inputMessageHere: "اكتب رسالتك هنا"
|
||||
close: "اغلق"
|
||||
@ -328,6 +344,7 @@ aboutX: "عن {x}"
|
||||
useOsNativeEmojis: "استخدم الإيموجيات الخاصة بنظام التشغيل"
|
||||
youHaveNoGroups: "لا تمتلك أية فِرَق"
|
||||
noHistory: "السجل فارغ"
|
||||
signinHistory: "تاريخ تسجيل الدخول"
|
||||
doing: "انتظر لحظة"
|
||||
category: "الفئات"
|
||||
tags: "الوسوم"
|
||||
@ -336,6 +353,7 @@ createAccount: "أنشئ حسابًا"
|
||||
existingAcount: "الحسابات الموجودة"
|
||||
regenerate: "أعِد التوليد"
|
||||
fontSize: "حجم الخط"
|
||||
openImageInNewTab: "إفتح الصورة بصفحة جديدة"
|
||||
dashboard: "لوحة التحكم"
|
||||
local: "المحلي"
|
||||
remote: "بُعدي"
|
||||
@ -364,6 +382,9 @@ state: "الحالة"
|
||||
sort: "ترتيب حسب"
|
||||
output: "الخارجة"
|
||||
updateRemoteUser: "تحديث المعلومات عن المستخدم البعيد"
|
||||
deleteAllFiles: "حذف كافة الملفات"
|
||||
userSuspended: "تم تعليق هذا المستخدم."
|
||||
userSilenced: "تم إسكات هذا المستخدم."
|
||||
sidebar: "الشريط الجانبي"
|
||||
addItem: "إضافة عنصر"
|
||||
rooms: "الغرفة"
|
||||
@ -373,13 +394,32 @@ addedRelays: "المرحلات التي تم إضافتها"
|
||||
deletedNote: "ملاحظة محذوفة"
|
||||
invisibleNote: "ملاحظة مخفية"
|
||||
poll: "استطلاع رأي"
|
||||
useCw: "إخفاء المحتوى"
|
||||
themeEditor: "مصمم القوالب"
|
||||
manage: "إدارة "
|
||||
plugins: "الإضافات"
|
||||
pluginInstallWarn: "يرجى تنصيب إضافات ذات مصدر موثوق منه فقط."
|
||||
width: "العرض"
|
||||
height: "الإرتفاع"
|
||||
large: "كبير"
|
||||
medium: "متوسط"
|
||||
small: "صغير"
|
||||
permission: "أذونات"
|
||||
enableAll: "تشغيل الكل"
|
||||
disableAll: "تعطيل الكل"
|
||||
tokenRequested: "منح حق الوصول إلى الحساب"
|
||||
notificationType: "أنواع الإشعارات"
|
||||
edit: "التعديل"
|
||||
email: "البريد الإلكتروني "
|
||||
emailAddress: "عنوان البريد الالكتروني"
|
||||
smtpHost: "المضيف"
|
||||
smtpPort: "المنفذ"
|
||||
smtpUser: "اسم المستخدم"
|
||||
smtpPass: "الكلمة السرية"
|
||||
makeActive: "تفعيل"
|
||||
display: "المظهر"
|
||||
copy: "نسخ"
|
||||
metrics: "المقاييس"
|
||||
public: "للعامة"
|
||||
_mfm:
|
||||
mention: "أشر الى"
|
||||
@ -453,6 +493,7 @@ _widgets:
|
||||
activity: "النشاط"
|
||||
photos: "الصور"
|
||||
federation: "الفديرالية"
|
||||
jobQueue: "قائمة الانتظار"
|
||||
_cw:
|
||||
hide: "إخفاء"
|
||||
show: "عرض المزيد"
|
||||
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
_lang_: "Deutsch"
|
||||
headlineMisskey: "Durch Notizen verbundenes Netzwerk"
|
||||
introMisskey: "Willkommen! Misskey ist eine dezentralisierte Open-Source Microblogging-Platform.\nVerfasse \"Notizen\" um mitzuteilen, was gerade passiert oder um Ereignisse mit Anderen zu teilen. 📡\nMit \"Reaktionen\" kannst du außerdem schnell deine Gefühle über Notizen anderer Benutzer zum Ausdruck bringen. 👍\nLass uns eine neue Welt erforschen! 🚀"
|
||||
monthAndDay: "{day}/{month}"
|
||||
search: "Suchen"
|
||||
@ -49,6 +50,7 @@ copyUsername: "Benutzernamen kopieren"
|
||||
searchUser: "Benutzersuche"
|
||||
reply: "Antworten"
|
||||
loadMore: "Mehr anzeigen"
|
||||
showMore: "Mehr anzeigen"
|
||||
youGotNewFollower: "Du hast einen neuen Follower"
|
||||
receiveFollowRequest: "Follow-Anfrage erhalten"
|
||||
followRequestAccepted: "Follow-Anfrage akzeptiert"
|
||||
@ -182,7 +184,7 @@ clearCachedFiles: "Cache leeren"
|
||||
clearCachedFilesConfirm: "Sollen alle im Cache gespeicherten Dateien von anderen Instanzen wirklich gelöscht werden?"
|
||||
blockedInstances: "Blockierte Instanzen"
|
||||
blockedInstancesDescription: "Gib den Hostnamen der Instanz an, die blockiert werden soll. Blockierte Instanzen können nicht mehr mit dieser kommunizieren."
|
||||
muteAndBlock: "Stummgeschaltet / Blockiert"
|
||||
muteAndBlock: "Stummschaltungen / Blockierungen"
|
||||
mutedUsers: "Stummgeschaltete Benutzer"
|
||||
blockedUsers: "Blockierte Benutzer"
|
||||
noUsers: "Keine Benutzer"
|
||||
@ -243,7 +245,7 @@ nUsersRead: "Von {n} gelesen"
|
||||
agreeTo: "Ich stimme {0} zu"
|
||||
tos: "Nutzungsbedingungen"
|
||||
start: "Anfangen"
|
||||
home: "Home"
|
||||
home: "Startseite"
|
||||
remoteUserCaution: "Diese Informationen sind möglicherweise veraltet, da der Benutzer von einer anderen Instanz stammt."
|
||||
activity: "Aktivität"
|
||||
images: "Bilder"
|
||||
@ -316,7 +318,7 @@ enableRegistration: "Registration neuer Benutzer erlauben"
|
||||
invite: "Einladen"
|
||||
proxyRemoteFiles: "Dateien anderer Instanzen durch Proxy leiten"
|
||||
proxyRemoteFilesDescription: "Wenn diese Einstellung aktiviert ist, dann werden Dateien von anderen Instanzen, welche entweder nicht lokal gespeichert sind oder durch Überschreiten des Speicherlimits gelöscht wurden, durch einen Proxy geleitet. Hierbei wird auch ein Vorschaubild generiert. \n Dies hat keinen Effekt auf den Speicherplatz des Servers."
|
||||
driveCapacityPerLocalAccount: "Drivekapazität pro lokales Benutzerkonto"
|
||||
driveCapacityPerLocalAccount: "Drive-Kapazität pro lokales Benutzerkonto"
|
||||
driveCapacityPerRemoteAccount: "Drive-Kapazität pro Benutzer anderer Instanzen"
|
||||
inMb: "In Megabytes"
|
||||
iconUrl: "Icon-URL"
|
||||
@ -491,7 +493,7 @@ showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik an
|
||||
newNoteRecived: "Es gibt neue Notizen"
|
||||
sounds: "Töne"
|
||||
listen: "Anhören"
|
||||
none: "Keine"
|
||||
none: "Nichts"
|
||||
showInPage: "In Seite anzeigen"
|
||||
popout: "Pop-Up"
|
||||
volume: "Lautstärke"
|
||||
@ -536,7 +538,7 @@ invisibleNote: "Private Notiz"
|
||||
enableInfiniteScroll: "Automatisch mehr Notizen laden"
|
||||
visibility: "Sichtbarkeit"
|
||||
poll: "Umfrage"
|
||||
useCw: "Inhalt verstecken"
|
||||
useCw: "Inhalt verdecken"
|
||||
enablePlayer: "Video-Player öffnen"
|
||||
disablePlayer: "Video-Player schließen"
|
||||
expandTweet: "Tweet ausklappen"
|
||||
@ -562,7 +564,7 @@ enableAll: "Alle aktivieren"
|
||||
disableAll: "Alle deaktivieren"
|
||||
tokenRequested: "Benutzerkontozugriff gewähren"
|
||||
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
|
||||
notificationType: "Benachrichtigungstyp"
|
||||
notificationType: "Benachrichtigungsart"
|
||||
edit: "Bearbeiten"
|
||||
useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
|
||||
emailConfig: "Email-Server Konfiguration"
|
||||
@ -671,6 +673,31 @@ wide: "Breit"
|
||||
narrow: "Schmal"
|
||||
reloadToApplySetting: "Diese Einstellung tritt nach einer Aktualisierung der Seite in Kraft. Jetzt aktualisieren?"
|
||||
showTitlebar: "Titelleiste anzeigen"
|
||||
clearCache: "Cache leeren"
|
||||
onlineUsersCount: "{n} Benutzer sind online"
|
||||
nUsers: "{n} Benutzer"
|
||||
nNotes: "{n} Notizen"
|
||||
sendErrorReports: "Fehlerberichte senden"
|
||||
sendErrorReportsDescription: "Ist diese Option aktiviert, so werden beim Auftreten von Fehlern detaillierte Fehlerinformationen an Misskey weitergegeben, was zur Verbesserung der Qualität von Misskey beiträgt."
|
||||
myTheme: "Mein Farbthema"
|
||||
backgroundColor: "Hintergrund"
|
||||
accentColor: "Akzentfarbe"
|
||||
textColor: "Text"
|
||||
saveAs: "Speichern als…"
|
||||
advanced: "Fortgeschritten"
|
||||
value: "Wert"
|
||||
updatedAt: "Zuletzt geändert am"
|
||||
saveConfirm: "Änderungen speichern?"
|
||||
deleteConfirm: "Wirklich löschen?"
|
||||
invalidValue: "Ungültiger Wert."
|
||||
registry: "Registry"
|
||||
closeAccount: "Benutzerkonto schließen"
|
||||
_registry:
|
||||
scope: "Scope"
|
||||
key: "Schlüssel"
|
||||
keys: "Schlüssel"
|
||||
domain: "Domain"
|
||||
createKey: "Schlüssel erstellen"
|
||||
_aboutMisskey:
|
||||
about: "Misskey ist Open-Source-Software die von syuilo seit 2014 entwickelt wird."
|
||||
contributors: "Hauptmitwirkende"
|
||||
@ -693,7 +720,7 @@ _mfm:
|
||||
hashtag: "Hashtag"
|
||||
hashtagDescription: "Mit einer Raute und Text kann ein Hashtag angegeben werden."
|
||||
url: "URL"
|
||||
urlDescription: "URLs können angezeigt werden."
|
||||
urlDescription: "Zeigt URLs an."
|
||||
link: "Link"
|
||||
linkDescription: "Ein spezifizierter Textabschnitt kann als URL angezeigt werden."
|
||||
bold: "Fett"
|
||||
@ -732,6 +759,16 @@ _mfm:
|
||||
twitchDescription: "Verleiht eine sehr stark zuckende Animation."
|
||||
spin: "Animation (Rotieren)"
|
||||
spinDescription: "Verleiht eine rotierende Animation."
|
||||
x2: "Groß"
|
||||
x2Description: "Lässt Inhalte größer angezeigt werden."
|
||||
x3: "Sehr groß"
|
||||
x3Description: "Lässt Inhalte noch größer angezeigt werden."
|
||||
x4: "Am größten"
|
||||
x4Description: "Lässt Inhalte noch größer als größer als groß angezeigt werden."
|
||||
blur: "Weichzeichner"
|
||||
blurDescription: "Inhalte durch Weihzeichnung verschwimmen lassen. Durch das Bewegen des Mauszeigers auf den Inhalt wird er klar angezeigt."
|
||||
font: "Schriftart"
|
||||
fontDescription: "Setzt die Schriftart des Inhaltes fest."
|
||||
_reversi:
|
||||
reversi: "Reversi"
|
||||
gameSettings: "Spieleinstellungen"
|
||||
@ -816,7 +853,7 @@ _theme:
|
||||
refConst: "Konstante referenzieren"
|
||||
key: "Schlüssel"
|
||||
func: "Funktionen"
|
||||
funcKind: "Funktionstyp"
|
||||
funcKind: "Funktionsart"
|
||||
argument: "Parameter"
|
||||
basedProp: "Referenzierte Eigenschaft"
|
||||
alpha: "Transparenz"
|
||||
@ -853,9 +890,9 @@ _theme:
|
||||
infoFg: "Text von Informationen"
|
||||
infoWarnBg: "Hintergrund von Warnungen"
|
||||
infoWarnFg: "Text von Informationen"
|
||||
cwBg: "Hintergrund von versteckten Inhalten"
|
||||
cwFg: "Text von versteckten Inhalten"
|
||||
cwHoverBg: "Hintergrund von versteckten Inhalten (Mouseover)"
|
||||
cwBg: "Hintergrund von verdeckten Inhalten"
|
||||
cwFg: "Text von verdeckten Inhalten"
|
||||
cwHoverBg: "Hintergrund von verdeckten Inhalten (Mouseover)"
|
||||
toastBg: "Hintergrund von Benachrichtigungen"
|
||||
toastFg: "Text von Benachrichtigungen"
|
||||
buttonBg: "Hintergrund von Schaltflächen"
|
||||
@ -992,6 +1029,9 @@ _widgets:
|
||||
postForm: "Neue Notiz anfertigen"
|
||||
slideshow: "Diashow"
|
||||
button: "Knopf"
|
||||
onlineUsers: "Benutzer Online"
|
||||
jobQueue: "Job-Warteschlange"
|
||||
serverMetric: "Servermetriken"
|
||||
_cw:
|
||||
hide: "Ausblenden"
|
||||
show: "Mehr anzeigen"
|
||||
@ -1101,7 +1141,7 @@ _rooms:
|
||||
clearConfirm: "Möchtest du wirklich alle Möbel entfernen?"
|
||||
leaveConfirm: "Es gibt ungespeicherte Änderungen. Möchtest du wirklich gehen?"
|
||||
chooseImage: "Bild auswählen"
|
||||
roomType: "Raumtyp"
|
||||
roomType: "Raumart"
|
||||
carpetColor: "Teppichfarbe"
|
||||
_roomType:
|
||||
default: "Standard"
|
||||
@ -1481,8 +1521,9 @@ _deck:
|
||||
swapRight: "Nach rechts verschieben"
|
||||
swapUp: "Nach oben verschieben"
|
||||
swapDown: "Nach unten verschieben"
|
||||
stackLeft: "Nach links stapeln"
|
||||
stackLeft: "Auf linke Spalte stapeln"
|
||||
popRight: "Nach rechts vom Stapel nehmen"
|
||||
profile: "Profil"
|
||||
_columns:
|
||||
main: "Hauptspalte"
|
||||
widgets: "Widgets"
|
||||
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
_lang_: "English"
|
||||
headlineMisskey: "Network connected by notes"
|
||||
introMisskey: "Welcome! Misskey is an open source, and also a decentralized microblogging service.\nCreate \"notes\" to share what is happening now, or to share it with everyone around you 📡\nWith \"reactions\", you can also quickly express your feelings about everyone's notes 👍\nLet's explore a new world 🚀"
|
||||
monthAndDay: "{month}/{day}"
|
||||
search: "Search"
|
||||
@ -49,6 +50,7 @@ copyUsername: "Copy username"
|
||||
searchUser: "User search"
|
||||
reply: "Reply"
|
||||
loadMore: "Load more"
|
||||
showMore: "Load more"
|
||||
youGotNewFollower: "Followed you"
|
||||
receiveFollowRequest: "Follow request received"
|
||||
followRequestAccepted: "Follow request accepted"
|
||||
@ -77,7 +79,7 @@ error: "Error"
|
||||
somethingHappened: "An error occurred"
|
||||
retry: "Retry"
|
||||
pageLoadError: "Failed to load page"
|
||||
pageLoadErrorDescription: "This is normally caused by network errors or the browser's cache. Try clearung the cache and then try again after waiting a little while."
|
||||
pageLoadErrorDescription: "This is normally caused by network errors or the browser's cache. Try clearing the cache and then try again after waiting a little while."
|
||||
enterListName: "List name"
|
||||
privacy: "Privacy"
|
||||
makeFollowManuallyApprove: "Follow requests require approval"
|
||||
@ -312,7 +314,7 @@ enableLocalTimeline: "Enable local timeline"
|
||||
enableGlobalTimeline: "Enable global timeline"
|
||||
disablingTimelinesInfo: "Admins and Mods will always have access to all timelines, even if they are not enabled."
|
||||
registration: "Register"
|
||||
enableRegistration: "Enable new user registeration"
|
||||
enableRegistration: "Enable new user registration"
|
||||
invite: "Invite"
|
||||
proxyRemoteFiles: "Proxy remote files"
|
||||
proxyRemoteFilesDescription: "If enabled, remote files that (1) are not stored locally or (2) got deleted from exceeding storage limit will be locally proxied (with thumbnails). This does not affect the server's storage."
|
||||
@ -671,6 +673,31 @@ wide: "Wide"
|
||||
narrow: "Narrow"
|
||||
reloadToApplySetting: "This setting will be applied upon page reload. Reload now?"
|
||||
showTitlebar: "Show title bar"
|
||||
clearCache: "Clear cache"
|
||||
onlineUsersCount: "{n} people are online"
|
||||
nUsers: "{n} Users"
|
||||
nNotes: "{n} Notes"
|
||||
sendErrorReports: "Send error reports"
|
||||
sendErrorReportsDescription: "When turned on, detailed error information will be shared with Misskey when a problem occurs, helping to improve the quality of Misskey."
|
||||
myTheme: "My theme"
|
||||
backgroundColor: "Background"
|
||||
accentColor: "Accent"
|
||||
textColor: "Text"
|
||||
saveAs: "Save as..."
|
||||
advanced: "Advanced"
|
||||
value: "Value"
|
||||
updatedAt: "Updated at"
|
||||
saveConfirm: "Save changes?"
|
||||
deleteConfirm: "Really delete?"
|
||||
invalidValue: "Invalid value."
|
||||
registry: "Registry"
|
||||
closeAccount: "Close account"
|
||||
_registry:
|
||||
scope: "Scope"
|
||||
key: "Key"
|
||||
keys: "Keys"
|
||||
domain: "Domain"
|
||||
createKey: "Create key"
|
||||
_aboutMisskey:
|
||||
about: "Misskey is open-source software being developed by syuilo since 2014."
|
||||
contributors: "Main contributors"
|
||||
@ -732,6 +759,16 @@ _mfm:
|
||||
twitchDescription: "Infuses a strongly twitching animation."
|
||||
spin: "Animation (Spin)"
|
||||
spinDescription: "Infuses a spinning animation."
|
||||
x2: "Big"
|
||||
x2Description: "Displays content bigger."
|
||||
x3: "Very big"
|
||||
x3Description: "Displays content even bigger."
|
||||
x4: "Extremely big"
|
||||
x4Description: "Displays content even bigger than bigger than big."
|
||||
blur: "Blur"
|
||||
blurDescription: "Content can be blurred via this effect. It will be clearly displayed by hovering your cursor above it."
|
||||
font: "Font"
|
||||
fontDescription: "Sets the font to display contents in."
|
||||
_reversi:
|
||||
reversi: "Reversi"
|
||||
gameSettings: "Game settings"
|
||||
@ -992,6 +1029,9 @@ _widgets:
|
||||
postForm: "Compose a note"
|
||||
slideshow: "Slideshow"
|
||||
button: "Button"
|
||||
onlineUsers: "Online users"
|
||||
jobQueue: "Job Queue"
|
||||
serverMetric: "Server metrics"
|
||||
_cw:
|
||||
hide: "Hide"
|
||||
show: "Load more"
|
||||
@ -1481,8 +1521,9 @@ _deck:
|
||||
swapRight: "Swap to right"
|
||||
swapUp: "Swap with above"
|
||||
swapDown: "Swap with below"
|
||||
stackLeft: "Stack on the left"
|
||||
stackLeft: "Stack on left column"
|
||||
popRight: "Pop to the right"
|
||||
profile: "Profile"
|
||||
_columns:
|
||||
main: "Main"
|
||||
widgets: "Widgets"
|
||||
|
@ -49,6 +49,7 @@ copyUsername: "Copiar nombre de usuario"
|
||||
searchUser: "Búsqueda de usuarios"
|
||||
reply: "Responder"
|
||||
loadMore: "Ver más"
|
||||
showMore: "Ver más"
|
||||
youGotNewFollower: "te ha seguido"
|
||||
receiveFollowRequest: "Recibiste una solicitud de seguimiento"
|
||||
followRequestAccepted: "La solicitud de seguimiento fue aceptada"
|
||||
@ -646,6 +647,14 @@ driveUsage: "Uso del drive"
|
||||
noCrawle: "Rechazar indexación del crawler"
|
||||
noCrawleDescription: "Pedir a los motores de búsqueda que no indexen tu perfil, notas, páginas, etc."
|
||||
clips: "Clip"
|
||||
clearCache: "Limpiar caché"
|
||||
backgroundColor: "Fondo"
|
||||
accentColor: "Acento"
|
||||
textColor: "Texto"
|
||||
value: "Valores"
|
||||
_registry:
|
||||
key: "Clave"
|
||||
keys: "Clave"
|
||||
_mfm:
|
||||
cheatSheet: "Hoja de referencia de MFM"
|
||||
intro: "MFM es un lenguaje de marcado dedicado que se puede usar en varios lugares dentro de Misskey. Aquí puede ver una lista de sintaxis disponibles en MFM."
|
||||
@ -663,6 +672,7 @@ _mfm:
|
||||
search: "Buscar"
|
||||
flip: "Echar de un capirotazo"
|
||||
flipDescription: "Voltea el contenido hacia arriba / abajo o hacia la izquierda / derecha."
|
||||
font: "Fuente"
|
||||
_reversi:
|
||||
reversi: "Reversi"
|
||||
gameSettings: "Configuración del juego"
|
||||
@ -918,6 +928,7 @@ _widgets:
|
||||
federation: "Federación"
|
||||
postForm: "Formulario"
|
||||
button: "Botón"
|
||||
jobQueue: "Cola de trabajos"
|
||||
_cw:
|
||||
hide: "Ocultar"
|
||||
show: "Ver más"
|
||||
|
@ -46,6 +46,7 @@ copyUsername: "Copier le nom d’utilisateur·rice"
|
||||
searchUser: "Chercher un·e utilisateur·rice"
|
||||
reply: "Répondre"
|
||||
loadMore: "Afficher plus …"
|
||||
showMore: "Afficher plus …"
|
||||
youGotNewFollower: "Vous suit"
|
||||
receiveFollowRequest: "Demande d’abonnement reçue"
|
||||
followRequestAccepted: "La demande d’abonnement a été acceptée"
|
||||
@ -577,6 +578,13 @@ fileIdOrUrl: "ID du fichier ou URL"
|
||||
chatOpenBehavior: "Comportement de la fenêtre de discussion lors de son ouverture"
|
||||
random: "Aléatoire"
|
||||
public: "Public"
|
||||
clearCache: "Vider le cache"
|
||||
backgroundColor: "Arrière-plan"
|
||||
textColor: "Texte"
|
||||
value: "Valeur"
|
||||
_registry:
|
||||
key: "Clé "
|
||||
keys: "Clé "
|
||||
_mfm:
|
||||
mention: "Mentionner"
|
||||
hashtag: "Hashtags"
|
||||
@ -585,6 +593,7 @@ _mfm:
|
||||
quote: "Citer"
|
||||
emoji: "Émojis personnalisés"
|
||||
search: "Rechercher"
|
||||
font: "Police de caractères"
|
||||
_reversi:
|
||||
total: "Total"
|
||||
_serverDisconnectedBehavior:
|
||||
@ -763,6 +772,7 @@ _widgets:
|
||||
federation: "Fédération"
|
||||
postForm: "Formulaire à publier"
|
||||
button: "Bouton"
|
||||
jobQueue: "File d’attente"
|
||||
_cw:
|
||||
hide: "Masquer"
|
||||
show: "Afficher plus …"
|
||||
|
219
locales/id-ID.yml
Normal file
@ -0,0 +1,219 @@
|
||||
---
|
||||
_lang_: "Bahasa Jepang"
|
||||
monthAndDay: "{day} {month}"
|
||||
search: "Pencarian"
|
||||
notifications: "Notifikasi"
|
||||
username: "Nama Pengguna"
|
||||
password: "Kata sandi"
|
||||
ok: "OK"
|
||||
gotIt: "Saya mengerti"
|
||||
cancel: "Batalkan"
|
||||
enterUsername: "Masukkan nama pengguna"
|
||||
renotedBy: "direnote oleh {user}"
|
||||
noNotes: "Tidak ada notes"
|
||||
noNotifications: "Tidak ada notifikasi"
|
||||
settings: "Pengaturan"
|
||||
basicSettings: "Pengaturan umum"
|
||||
otherSettings: "Pengaturan lainnya"
|
||||
openInWindow: "Buka di jendela"
|
||||
profile: "Profil"
|
||||
timeline: "Linimasa"
|
||||
noAccountDescription: "Pengguna ini belum menulis bio"
|
||||
login: "Masuk"
|
||||
loggingIn: "Sedang masuk"
|
||||
logout: "Keluar"
|
||||
signup: "Daftar"
|
||||
uploading: "Sedang mengunggah"
|
||||
save: "Simpan"
|
||||
users: "Pengguna"
|
||||
addUser: "Tambah pengguna"
|
||||
favorite: "Favorit"
|
||||
favorites: "Favorit"
|
||||
unfavorite: "Hapus favorit"
|
||||
favorited: "Ditambahkan ke favorit"
|
||||
alreadyFavorited: "Telah ditambahkan ke favorit"
|
||||
cantFavorite: "Tidak dapat menambahkan ke favorit"
|
||||
pin: "Sematkan ke profil"
|
||||
unpin: "Lepas sematan dari profil"
|
||||
copyContent: "Salin konten"
|
||||
copyLink: "Salin tautan"
|
||||
delete: "Hapus"
|
||||
deleteAndEdit: "Hapus dan sunting"
|
||||
addToList: "Tambahkan ke daftar"
|
||||
sendMessage: "Kirim pesan"
|
||||
copyUsername: "Salin nama pengguna"
|
||||
searchUser: "Cari pengguna"
|
||||
reply: "Balas"
|
||||
cantReRenote: "Renote tidak dapat direnote"
|
||||
quote: "Kutip"
|
||||
pinnedNote: "Note yang disematkan"
|
||||
you: "Anda"
|
||||
clickToShow: "Klik untuk melihat"
|
||||
sensitive: "Konten sensitif"
|
||||
add: "Tambahkan"
|
||||
reaction: "Reaksi"
|
||||
reactionSettingDescription: "Masukkan reaksi favorit yang ingin anda sematkan pada bilah reaksi"
|
||||
reactionSettingDescription2: "Geser untuk memindah urutkan, klik untuk menghapus, tekan \"+\" untuk menambahkan"
|
||||
rememberNoteVisibility: "Ingat pengaturan visibilitas note"
|
||||
attachCancel: "Hapus lampiran"
|
||||
markAsSensitive: "Tandai sebagai konten sensitif"
|
||||
unmarkAsSensitive: "Hapus tanda konten sensitif"
|
||||
enterFileName: "Masukkan nama berkas"
|
||||
mute: "Bisukan"
|
||||
unmute: "Hapus bisukan"
|
||||
block: "Blokir"
|
||||
unblock: "Buka blokir"
|
||||
suspend: "Bekukan"
|
||||
unsuspend: "Buka pembekuan"
|
||||
blockConfirm: "Apakah anda yakin ingin memblokir akun ini?"
|
||||
unblockConfirm: "Apakah anda yakin ingin membuka blokir akun ini?"
|
||||
suspendConfirm: "Apakah anda yakin ingin membekukan akun ini?"
|
||||
unsuspendConfirm: "Apakah anda yakin ingin membuka pembekuan akun ini?"
|
||||
selectList: "Pilih daftar"
|
||||
selectAntenna: "Pilih Antena"
|
||||
selectWidget: "Pilih gawit"
|
||||
editWidgets: "Sunting gawit"
|
||||
editWidgetsExit: "Selesai"
|
||||
customEmojis: "Emoji kustom"
|
||||
emoji: "Emoji"
|
||||
emojiName: "Nama emoji"
|
||||
emojiUrl: "URL Emoji"
|
||||
addEmoji: "Tambahkan emoji"
|
||||
settingGuide: "Pengaturan rekomendasi"
|
||||
cacheRemoteFiles: "Tembolokkan berkas remote"
|
||||
flagAsBot: "Atur akun ini sebagai Bot"
|
||||
flagAsCat: "Atur akun ini sebagai kucing"
|
||||
autoAcceptFollowed: "Setujui otomatis permintaan mengikuti dari pengguna yang anda ikuti"
|
||||
addAcount: "Tambahkan akun"
|
||||
loginFailed: "Gagal untuk masuk"
|
||||
showOnRemote: "Lihat profil asli"
|
||||
general: "Umum"
|
||||
wallpaper: "Wallpaper"
|
||||
setWallpaper: "Atur wallpaper"
|
||||
removeWallpaper: "Hapus wallpaper"
|
||||
intro: "Instalasi Misskey telah selesai! Mohon untuk membuat pengguna admin."
|
||||
done: "Selesai"
|
||||
processing: "Memproses"
|
||||
preview: "Pratinjau"
|
||||
default: "Bawaan"
|
||||
noCustomEmojis: "Tidak ada emoji kustom"
|
||||
federating: "memfederasi"
|
||||
blocked: "Diblokir"
|
||||
all: "Semua"
|
||||
subscribing: "Berlangganan"
|
||||
publishing: "Sedang menyiarkan langsung"
|
||||
notResponding: "Tidak ada respon"
|
||||
instanceFollowing: "Mengikuti instance"
|
||||
instanceFollowers: "Pengikut instance"
|
||||
instanceUsers: "Pengguna pada instance ini"
|
||||
changePassword: "Ubah kata sandi"
|
||||
security: "Keamanan"
|
||||
retypedNotMatch: "Input tidak sama"
|
||||
currentPassword: "Kata sandi saat ini"
|
||||
newPassword: "Kata sandi baru"
|
||||
newPasswordRetype: "Ulangi kata sandi baru"
|
||||
attachFile: "Lampirkan berkas"
|
||||
more: "Lagi !"
|
||||
featured: "Sorotan"
|
||||
usernameOrUserId: "Nama pengguna atau User ID"
|
||||
noSuchUser: "Pengguna tidak ditemukan"
|
||||
lookup: "Mencari"
|
||||
announcements: "Pengumuman"
|
||||
imageUrl: "URL Gambar"
|
||||
remove: "Hapus"
|
||||
removed: "Telah dihapus"
|
||||
removeAreYouSure: "Apakah anda yakin ingin menghapus \"{x}\"?"
|
||||
deleteAreYouSure: "Apakah anda yakin ingin menghapus \"{x}\"?"
|
||||
saved: "Telah disimpan"
|
||||
messaging: "Pesan"
|
||||
upload: "Unggah"
|
||||
fromDrive: "Dari Drive"
|
||||
fromUrl: "Dari URL"
|
||||
uploadFromUrl: "Unggah dari URL"
|
||||
uploadFromUrlDescription: "URL berkas yang ingin anda unggah"
|
||||
uploadFromUrlRequested: "Pengunggahan telah diminta"
|
||||
uploadFromUrlMayTakeTime: "Membutuhkan beberapa waktu hingga pengunggahan selesai"
|
||||
explore: "Jelajahi"
|
||||
games: "Permainan Misskey"
|
||||
messageRead: "Telah dibaca"
|
||||
noMoreHistory: "Tidak ada sejarah lagi"
|
||||
startMessaging: "Mulai mengirim pesan"
|
||||
nUsersRead: "Dibaca oleh {n}"
|
||||
nsfw: "Konten sensitif"
|
||||
watch: "Tonton"
|
||||
unwatch: "Batal tonton"
|
||||
accept: "Terima"
|
||||
reject: "Tolak"
|
||||
normal: "Normal"
|
||||
instanceName: "Nama instance"
|
||||
instanceDescription: "Tentang instance"
|
||||
maintainerName: "Pengelola"
|
||||
maintainerEmail: "Surel pengelola"
|
||||
tosUrl: "URL Syarat dan Ketentuan"
|
||||
thisYear: "Tahun ini"
|
||||
thisMonth: "Bulan ini"
|
||||
today: "Hari ini"
|
||||
dayX: "{day}"
|
||||
monthX: "{month}"
|
||||
yearX: "{year}"
|
||||
pages: "Halaman"
|
||||
integration: "Integrasi"
|
||||
connectSerice: "Sambungkan"
|
||||
disconnectSerice: "Putuskan"
|
||||
enableLocalTimeline: "Nyalakan linimasa lokal"
|
||||
enableGlobalTimeline: "Nyalakan linimasa global"
|
||||
registration: "Pendaftaran"
|
||||
enableRegistration: "Nyalakan pendaftaran pengguna baru"
|
||||
invite: "Undang"
|
||||
proxyRemoteFiles: "Proksi berkas remote"
|
||||
driveCapacityPerLocalAccount: "Kapasitas drive per pengguna lokal"
|
||||
driveCapacityPerRemoteAccount: "Kapasitas drive per pengguna remote"
|
||||
inMb: "dalam Megabytes"
|
||||
iconUrl: "URL Gambar ikon"
|
||||
bannerUrl: "URL Banner"
|
||||
basicInfo: "Informasi Umum"
|
||||
pinnedUsers: "Pengguna yang disematkan"
|
||||
pinnedPages: "Halaman yang disematkan"
|
||||
pinnedNotes: "Note yang disematkan"
|
||||
hcaptcha: "hCaptcha"
|
||||
enableHcaptcha: "Nyalakan hCaptcha"
|
||||
hcaptchaSiteKey: "Site Key"
|
||||
hcaptchaSecretKey: "Secret Key"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "Nyalakan reCAPTCHA"
|
||||
recaptchaSiteKey: "Site key"
|
||||
recaptchaSecretKey: "Secret Key"
|
||||
antennas: "Antena"
|
||||
manageAntennas: "Pengelola Antena"
|
||||
notFound: "Tidak dapat ditemukan"
|
||||
invites: "Undang"
|
||||
invitations: "Undang"
|
||||
smtpUser: "Nama Pengguna"
|
||||
smtpPass: "Kata sandi"
|
||||
_mfm:
|
||||
quote: "Kutip"
|
||||
emoji: "Emoji kustom"
|
||||
search: "Pencarian"
|
||||
_sfx:
|
||||
notification: "Notifikasi"
|
||||
chat: "Pesan"
|
||||
_widgets:
|
||||
notifications: "Notifikasi"
|
||||
timeline: "Linimasa"
|
||||
_profile:
|
||||
username: "Nama Pengguna"
|
||||
_exportOrImport:
|
||||
muteList: "Bisukan"
|
||||
blockingList: "Blokir"
|
||||
_rooms:
|
||||
_roomType:
|
||||
default: "Bawaan"
|
||||
_notification:
|
||||
_types:
|
||||
quote: "Kutip"
|
||||
reaction: "Reaksi"
|
||||
_deck:
|
||||
_columns:
|
||||
notifications: "Notifikasi"
|
||||
tl: "Linimasa"
|
||||
antenna: "Antena"
|
@ -677,6 +677,28 @@ clearCache: "キャッシュをクリア"
|
||||
onlineUsersCount: "{n}人がオンライン"
|
||||
nUsers: "{n}ユーザー"
|
||||
nNotes: "{n}ノート"
|
||||
sendErrorReports: "エラーリポートを送信"
|
||||
sendErrorReportsDescription: "オンにすると、問題が発生したときにエラーの詳細情報がMisskeyに共有され、ソフトウェアの品質向上に役立てることができます。"
|
||||
myTheme: "マイテーマ"
|
||||
backgroundColor: "背景"
|
||||
accentColor: "アクセント"
|
||||
textColor: "文字"
|
||||
saveAs: "名前を付けて保存"
|
||||
advanced: "高度"
|
||||
value: "値"
|
||||
updatedAt: "更新日時"
|
||||
saveConfirm: "保存しますか?"
|
||||
deleteConfirm: "削除しますか?"
|
||||
invalidValue: "有効な値ではありません。"
|
||||
registry: "レジストリ"
|
||||
closeAccount: "アカウントを閉鎖する"
|
||||
|
||||
_registry:
|
||||
scope: "スコープ"
|
||||
key: "キー"
|
||||
keys: "キー"
|
||||
domain: "ドメイン"
|
||||
createKey: "キーを作成"
|
||||
|
||||
_aboutMisskey:
|
||||
about: "Misskeyはsyuiloによって2014年から開発されている、オープンソースのソフトウェアです。"
|
||||
@ -749,6 +771,8 @@ _mfm:
|
||||
x4Description: "内容を究極に大きく表示します。"
|
||||
blur: "ぼかし"
|
||||
blurDescription: "内容をぼかすことができます。ポインターを上に乗せるとはっきり見えるようになります。"
|
||||
font: "フォント"
|
||||
fontDescription: "内容のフォントを指定することができます。"
|
||||
|
||||
_reversi:
|
||||
reversi: "リバーシ"
|
||||
@ -1028,6 +1052,8 @@ _widgets:
|
||||
slideshow: "スライドショー"
|
||||
button: "ボタン"
|
||||
onlineUsers: "オンラインユーザー"
|
||||
jobQueue: "ジョブキュー"
|
||||
serverMetric: "サーバーメトリクス"
|
||||
|
||||
_cw:
|
||||
hide: "隠す"
|
||||
@ -1546,6 +1572,7 @@ _deck:
|
||||
swapDown: "下に移動"
|
||||
stackLeft: "左に重ねる"
|
||||
popRight: "右に出す"
|
||||
profile: "プロファイル"
|
||||
|
||||
_columns:
|
||||
main: "メイン"
|
||||
|
@ -49,6 +49,7 @@ copyUsername: "ユーザー名をコピー"
|
||||
searchUser: "ユーザーを検索"
|
||||
reply: "返す"
|
||||
loadMore: "もっとあるやろ!"
|
||||
showMore: "もっとあるやろ!"
|
||||
youGotNewFollower: "フォローされたで"
|
||||
receiveFollowRequest: "フォローリクエストされたで"
|
||||
followRequestAccepted: "フォローが承認されたで"
|
||||
@ -454,6 +455,7 @@ remote: "リモート"
|
||||
smtpHost: "ホスト"
|
||||
smtpUser: "ユーザー名"
|
||||
smtpPass: "パスワード"
|
||||
clearCache: "キャッシュにさいなら"
|
||||
_mfm:
|
||||
mention: "メンション"
|
||||
quote: "引用"
|
||||
@ -497,6 +499,7 @@ _widgets:
|
||||
timeline: "タイムライン"
|
||||
activity: "アクティビティ"
|
||||
federation: "連合"
|
||||
jobQueue: "ジョブキュー"
|
||||
_cw:
|
||||
show: "もっとあるやろ!"
|
||||
_poll:
|
||||
|
@ -13,6 +13,7 @@ delete: "Kkes"
|
||||
addToList: "Rnu ɣer tebdart"
|
||||
reply: "Err"
|
||||
loadMore: "Wali ugar"
|
||||
showMore: "Wali ugar"
|
||||
youGotNewFollower: "Yeṭṭafaṛ-ik·em-id"
|
||||
mention: "Bder"
|
||||
import: "Kter"
|
||||
@ -38,6 +39,7 @@ smtpPass: "Awal uffir"
|
||||
_mfm:
|
||||
mention: "Bder"
|
||||
search: "Nadi"
|
||||
font: "Tasefsit"
|
||||
_theme:
|
||||
keys:
|
||||
mention: "Bder"
|
||||
|
@ -40,6 +40,7 @@ sendMessage: "ಸಂದೇಶ ಕಳುಹಿಸು"
|
||||
copyUsername: "ಬಳಕೆಹೆಸರು ನಕಲಿಸು"
|
||||
reply: "ಉತ್ತರಿಸು"
|
||||
loadMore: "ಇನ್ನಷ್ಟು ನೋಡು"
|
||||
showMore: "ಇನ್ನಷ್ಟು ನೋಡು"
|
||||
youGotNewFollower: "ಹಿಂಬಾಲಿಸಿದರು"
|
||||
receiveFollowRequest: "ಹಿಂಬಾಲನೆ ವಿನಂತಿ ಬಂದಿದೆ"
|
||||
followRequestAccepted: "ಹಿಂಬಾಲನೆ ವಿನಂತಿ ಸ್ವೀಕರಿಸಲಾಯಿತು"
|
||||
|
@ -46,6 +46,7 @@ copyUsername: "유저명 복사"
|
||||
searchUser: "사용자 검색"
|
||||
reply: "답글"
|
||||
loadMore: "더 보기"
|
||||
showMore: "더 보기"
|
||||
youGotNewFollower: "새로운 팔로워가 있습니다"
|
||||
receiveFollowRequest: "새로운 팔로우 요청이 있습니다"
|
||||
followRequestAccepted: "팔로우가 수락되었습니다"
|
||||
@ -612,6 +613,11 @@ lockedAccountInfo: "팔로우를 승인으로 승인받더라도 노트의 공
|
||||
alwaysMarkSensitive: "미디어를 항상 열람 주의로 설정"
|
||||
emailVerified: "메일 주소가 확인되었습니다."
|
||||
clips: "클립"
|
||||
clearCache: "캐시 비우기"
|
||||
value: "값"
|
||||
_registry:
|
||||
key: "키"
|
||||
keys: "키"
|
||||
_nsfw:
|
||||
ignore: "열람 주의 미디어 항상 표시"
|
||||
_mfm:
|
||||
@ -642,6 +648,7 @@ _mfm:
|
||||
flip: "플립"
|
||||
flipDescription: "내용을 상하 또는 좌우로 반전시킵니다."
|
||||
jump: "애니메이션(점프)"
|
||||
font: "폰트"
|
||||
_reversi:
|
||||
reversi: "리버시"
|
||||
gameSettings: "대국 설정"
|
||||
@ -835,6 +842,7 @@ _widgets:
|
||||
federation: "연합"
|
||||
postForm: "글 입력란"
|
||||
button: "버튼"
|
||||
jobQueue: "작업 대기열"
|
||||
_cw:
|
||||
hide: "숨기기"
|
||||
show: "더 보기"
|
||||
|
1159
locales/pl-PL.yml
@ -1,2 +1,31 @@
|
||||
---
|
||||
_lang_: "Português"
|
||||
monthAndDay: "{day}/{month}"
|
||||
search: "Pesquisar"
|
||||
notifications: "Notificações"
|
||||
username: "Nome de usuário"
|
||||
password: "Senha"
|
||||
ok: "OK"
|
||||
cancel: "Cancelar"
|
||||
enterUsername: "Digite o nome de usuário"
|
||||
renotedBy: "Repostado por {user}"
|
||||
settings: "Configurações"
|
||||
basicSettings: "Configurações básicas"
|
||||
otherSettings: "Outras configurações"
|
||||
profile: "Perfil"
|
||||
timeline: "Timeline"
|
||||
smtpUser: "Nome de usuário"
|
||||
smtpPass: "Senha"
|
||||
_mfm:
|
||||
search: "Pesquisar"
|
||||
_sfx:
|
||||
notification: "Notificações"
|
||||
_widgets:
|
||||
notifications: "Notificações"
|
||||
timeline: "Timeline"
|
||||
_profile:
|
||||
username: "Nome de usuário"
|
||||
_deck:
|
||||
_columns:
|
||||
notifications: "Notificações"
|
||||
tl: "Timeline"
|
||||
|
@ -49,6 +49,7 @@ copyUsername: "Скопировать имя пользователя"
|
||||
searchUser: "Поиск людей"
|
||||
reply: "Ответить"
|
||||
loadMore: "Показать еще"
|
||||
showMore: "Показать еще"
|
||||
youGotNewFollower: "Новый подписчик"
|
||||
receiveFollowRequest: "Получен запрос на подписку"
|
||||
followRequestAccepted: "Запрос на подписку принят"
|
||||
@ -667,6 +668,19 @@ showGapBetweenNotesInTimeline: "Показывать разделитель ме
|
||||
duplicate: "Дубликат"
|
||||
left: "Влево"
|
||||
center: "По центру"
|
||||
reloadToApplySetting: "Это настройка вступает в силу при загрузке страницы. Перезагрузить сейчас?"
|
||||
showTitlebar: "Показать заголовок"
|
||||
clearCache: "Очистить кэш"
|
||||
onlineUsersCount: "Пользователей сейчас в сети: {n}"
|
||||
nUsers: "Пользователей: {n}"
|
||||
nNotes: "Заметок: {n}"
|
||||
backgroundColor: "Фон"
|
||||
accentColor: "Акцент"
|
||||
textColor: "Текст"
|
||||
value: "Значения"
|
||||
_registry:
|
||||
key: "Ключ"
|
||||
keys: "Ключ"
|
||||
_aboutMisskey:
|
||||
about: "Misskey — программа с открытым исходным кодом, которую разрабатывает syuilo с 2014 года."
|
||||
contributors: "Основные соавторы"
|
||||
@ -728,6 +742,7 @@ _mfm:
|
||||
twitchDescription: "Заставляет трястись как одержимого"
|
||||
spin: "Вращение"
|
||||
spinDescription: "Так можно крутить содержимое в разных направлениях."
|
||||
font: "Шрифт"
|
||||
_reversi:
|
||||
reversi: "Реверси"
|
||||
gameSettings: "Настройки игры"
|
||||
@ -988,6 +1003,8 @@ _widgets:
|
||||
postForm: "Форма отправки"
|
||||
slideshow: "Показ слайдов"
|
||||
button: "Кнопка"
|
||||
onlineUsers: "Пользователи сейчас с сети"
|
||||
jobQueue: "Очередь заданий"
|
||||
_cw:
|
||||
hide: "Спрятать"
|
||||
show: "Показать еще"
|
||||
|
1
locales/th-TH.yml
Normal file
@ -0,0 +1 @@
|
||||
---
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
_lang_: "Українська"
|
||||
introMisskey: "Ласкаво просимо! Misskey - децентралізована служба мікроблогів з відкритим кодом.\nСтворюйте \"нотатки\", щоб поділитися тим, що відбувається, і розповісти всім про себе 📡\nЗа допомогою \"реакцій\" ви також можете швидко висловити свої почуття щодо нотаток інших 👍\nДавайте досліджувати новий світ 🚀"
|
||||
headlineMisskey: "Мережа об'єднана записами"
|
||||
introMisskey: "Ласкаво просимо! Misskey - децентралізована служба мікроблогів з відкритим кодом.\nСтворюйте \"записи\", щоб поділитися тим, що відбувається, і розповісти всім про себе 📡\nЗа допомогою \"реакцій\" ви також можете швидко висловити свої почуття щодо записів інших 👍\nДавайте досліджувати новий світ 🚀"
|
||||
monthAndDay: "{month}/{day}"
|
||||
search: "Пошук"
|
||||
notifications: "Сповіщення"
|
||||
@ -33,6 +34,9 @@ addUser: "Додати користувача"
|
||||
favorite: "Обране"
|
||||
favorites: "Обране"
|
||||
unfavorite: "Видалити з обраного"
|
||||
favorited: "Додано до вподобаних."
|
||||
alreadyFavorited: "Вже додано до вподобаних."
|
||||
cantFavorite: "Неможливо вподобати."
|
||||
pin: "Закріпити"
|
||||
unpin: "Відкріпити"
|
||||
copyContent: "Скопіювати контент"
|
||||
@ -46,6 +50,7 @@ copyUsername: "Скопіювати ім’я користувача"
|
||||
searchUser: "Пошук користувачів"
|
||||
reply: "Відповісти"
|
||||
loadMore: "Показати більше"
|
||||
showMore: "Показати більше"
|
||||
youGotNewFollower: "Новий підписник"
|
||||
receiveFollowRequest: "Отримано запит на підписку"
|
||||
followRequestAccepted: "Підписка прийнята"
|
||||
@ -59,12 +64,12 @@ files: "Файли"
|
||||
download: "Завантажити"
|
||||
driveFileDeleteConfirm: "Ви впевнені, що хочете видалити файл {name}? Нотатки із цим файлом також буде видалено."
|
||||
unfollowConfirm: "Ви впевнені, що хочете відписатися від {name}?"
|
||||
exportRequested: "Ви запросили експорт. Це може зайняти деякий час. Після завершення експорту отриманий файл буде додано на диск."
|
||||
importRequested: "Ви запросили імпорт. Це може зайняти деякий час."
|
||||
exportRequested: "Експортування розпочато. Це може зайняти деякий час. Після завершення експорту отриманий файл буде додано на диск."
|
||||
importRequested: "Імпортування розпочато. Це може зайняти деякий час."
|
||||
lists: "Списки"
|
||||
noLists: "Немає списків"
|
||||
note: "Нотатки"
|
||||
notes: "Нотатки"
|
||||
note: "Запис"
|
||||
notes: "Записи"
|
||||
following: "Підписки"
|
||||
followers: "Підписники"
|
||||
followsYou: "Підписаний(-а) на вас"
|
||||
@ -76,10 +81,10 @@ retry: "Спробувати знову"
|
||||
pageLoadError: "Помилка при завантаженні сторінки"
|
||||
pageLoadErrorDescription: "Зазвичай це пов’язано з помилками мережі або кешем браузера. Очистіть кеш або почекайте трохи й спробуйте ще раз."
|
||||
enterListName: "Введіть назву списку"
|
||||
privacy: "Приватність"
|
||||
privacy: "Конфіденційність"
|
||||
makeFollowManuallyApprove: "Підтверджувати підписників уручну"
|
||||
defaultNoteVisibility: "Видимість за замовчуванням"
|
||||
follow: "Підписка"
|
||||
follow: "Підписатись"
|
||||
followRequest: "Запит на підписку"
|
||||
followRequests: "Запити на підписку"
|
||||
unfollow: "Відписатись"
|
||||
@ -87,14 +92,18 @@ followRequestPending: "Очікуючі запити на підписку"
|
||||
enterEmoji: "Введіть емодзі"
|
||||
renote: "Поширити"
|
||||
unrenote: "Відміна поширення"
|
||||
renoted: "Поширити запис."
|
||||
cantRenote: "Неможливо поширити."
|
||||
cantReRenote: "Поширення не можливо поширити."
|
||||
quote: "Цитата"
|
||||
pinnedNote: "Закріплена нотатка"
|
||||
pinnedNote: "Закріплений запис"
|
||||
you: "Ви"
|
||||
clickToShow: "Натисніть для перегляду"
|
||||
sensitive: "NSFW"
|
||||
add: "Додати"
|
||||
reaction: "Реакції"
|
||||
reactionSettingDescription: "Виберіть свої улюблені реакції, які хочете закріпити в селекторі реакцій."
|
||||
reactionSettingDescription2: "Перемістити щоб змінити порядок, Клацнути мишою щоб видалити, Натиснути \"+\" щоб додати."
|
||||
rememberNoteVisibility: "Пам’ятати параметри видимісті"
|
||||
attachCancel: "Видалити вкладення"
|
||||
markAsSensitive: "Позначити як NSFW"
|
||||
@ -124,7 +133,9 @@ settingGuide: "Рекомендована конфігурація"
|
||||
cacheRemoteFiles: "Кешувати дані з інших інстансів"
|
||||
cacheRemoteFilesDescription: "Якщо кешування вимкнено, віддалені файли завантажуються безпосередньо з віддаленого інстансу. Це зменшує використання сховища, але збільшує трафік, оскільки не генеруются ескізи."
|
||||
flagAsBot: "Акаунт бота"
|
||||
flagAsBotDescription: "Ввімкніть якщо цей обліковий запис використовується ботом. Ця опція позначить обліковий запис як бота. Це потрібно щоб виключити безкінечну інтеракцію між ботами а також відповідного підлаштування Misskey."
|
||||
flagAsCat: "Акаунт кота"
|
||||
flagAsCatDescription: "Ввімкніть, щоб позначити, що обліковий запис є котиком."
|
||||
autoAcceptFollowed: "Автоматично приймати запити на підписку від користувачів, на яких ви підписані"
|
||||
addAcount: "Додати акаунт"
|
||||
loginFailed: "Не вдалося увійти"
|
||||
@ -139,15 +150,15 @@ followConfirm: "Підписатися на {name}?"
|
||||
proxyAccount: "Проксі-акаунт"
|
||||
host: "Хост"
|
||||
selectUser: "Виберіть користувача"
|
||||
recipient: "Кому"
|
||||
annotation: "Коментар"
|
||||
recipient: "Отримувач"
|
||||
annotation: "Коментарі"
|
||||
federation: "Федіверс"
|
||||
instances: "Інстанс"
|
||||
registeredAt: "Приєднався(лась)"
|
||||
latestRequestSentAt: "Останній запит надіслано"
|
||||
latestRequestReceivedAt: "Останній запит прийнято"
|
||||
latestStatus: "Останній статус"
|
||||
storageUsage: "Використання простіру"
|
||||
storageUsage: "Використання простору"
|
||||
charts: "Графіки"
|
||||
perHour: "Щогодинно"
|
||||
perDay: "Щоденно"
|
||||
@ -172,19 +183,19 @@ clearCachedFiles: "Очистити кеш"
|
||||
clearCachedFilesConfirm: "Ви впевнені, що хочете видалити всі кешовані файли?"
|
||||
blockedInstances: "Заблоковані інстанси"
|
||||
blockedInstancesDescription: "Вкажіть інстанси, які потрібно заблокувати. Перелічені інстанси більше не зможуть спілкуватися з цим інстансом."
|
||||
muteAndBlock: "Ігнор і блокування"
|
||||
mutedUsers: "Ігноровані користувачі"
|
||||
muteAndBlock: "Заглушення і блокування"
|
||||
mutedUsers: "Заглушені користувачі"
|
||||
blockedUsers: "Заблоковані користувачі"
|
||||
noUsers: "Немає користувачів"
|
||||
editProfile: "Редагувати профіль"
|
||||
noteDeleteConfirm: "Ви дійсно хочете видалити цю нотатку?"
|
||||
pinLimitExceeded: "Більше нотаток не можна закріпити"
|
||||
intro: "Встановлення Misskey завершено! Будь ласка, створіть акаунт адміністратора."
|
||||
editProfile: "Редагувати обліковий запис"
|
||||
noteDeleteConfirm: "Ви дійсно хочете видалити цей запис?"
|
||||
pinLimitExceeded: "Більше записів не можна закріпити"
|
||||
intro: "Встановлення Misskey завершено! Будь ласка, створіть обліковий запис адміністратора."
|
||||
done: "Готово"
|
||||
processing: "Обробка"
|
||||
preview: "Передогляд"
|
||||
preview: "Попередній перегляд"
|
||||
default: "За умовчанням"
|
||||
noCustomEmojis: "Немає кастомних емоджі"
|
||||
noCustomEmojis: "Немає нетипових емоджі"
|
||||
noJobs: "Немає завдань"
|
||||
federating: "Федерується"
|
||||
blocked: "Заблоковано"
|
||||
@ -202,27 +213,27 @@ retypedNotMatch: "Введені дані не збігаються."
|
||||
currentPassword: "Поточний пароль"
|
||||
newPassword: "Новий пароль"
|
||||
newPasswordRetype: "Новий пароль (повторно)"
|
||||
attachFile: "Вкласти файл"
|
||||
attachFile: "Прикріпити файл"
|
||||
more: "Бiльше!"
|
||||
featured: "Популярні"
|
||||
usernameOrUserId: "Ім'я або ID користувача"
|
||||
noSuchUser: "Користувача не знайдено"
|
||||
lookup: "Пошук"
|
||||
announcements: "Оголошення"
|
||||
imageUrl: "URL зображення"
|
||||
imageUrl: "Посилання на зображення"
|
||||
remove: "Видалити"
|
||||
removed: "Видалено"
|
||||
removeAreYouSure: "Ви впевнені, що хочете видалити \"{x}\"?"
|
||||
deleteAreYouSure: "Ви впевнені, що хочете видалити \"{x}\"?"
|
||||
resetAreYouSure: "Дійсно ресет?"
|
||||
resetAreYouSure: "Справді скинути?"
|
||||
saved: "Збережено"
|
||||
messaging: "Чати"
|
||||
upload: "Завантажити"
|
||||
fromDrive: "З диска"
|
||||
fromUrl: "З URL"
|
||||
uploadFromUrl: "Завантажити з URL"
|
||||
uploadFromUrlDescription: "URL-адреса файлу для завантаження"
|
||||
uploadFromUrlRequested: "Обрано завантаження"
|
||||
fromUrl: "З посилання"
|
||||
uploadFromUrl: "Завантажити з посилання"
|
||||
uploadFromUrlDescription: "Посилання на файл для завантаження"
|
||||
uploadFromUrlRequested: "Завантаження розпочалось"
|
||||
uploadFromUrlMayTakeTime: "Завантаження може зайняти деякий час."
|
||||
explore: "Огляд"
|
||||
games: "Ігри Misskey"
|
||||
@ -282,6 +293,7 @@ watch: "Стежити"
|
||||
unwatch: "Не стежити"
|
||||
accept: "Прийняти"
|
||||
reject: "Відхилити"
|
||||
normal: "Нормальний"
|
||||
instanceName: "Назва інстансу"
|
||||
instanceDescription: "Описання інстансу"
|
||||
maintainerName: "Ім'я адміністратора"
|
||||
@ -304,6 +316,7 @@ registration: "Реєстрація"
|
||||
enableRegistration: "Дозволити реєстрацію"
|
||||
invite: "Запросити"
|
||||
proxyRemoteFiles: "Проксувати файли з інших інстансів"
|
||||
proxyRemoteFilesDescription: "При увімкненні віддалені файли, які не зберігаються локально або були видалені через недостатнії обсяг пам'яті, будуть локально проксовані включаючи обкладинки. Це налаштування не впливає на пам'ять серверу. "
|
||||
driveCapacityPerLocalAccount: "Об'єм диска на одного локального користувача"
|
||||
driveCapacityPerRemoteAccount: "Об'єм диска на одного віддаленого користувача"
|
||||
inMb: "В мегабайтах"
|
||||
@ -311,8 +324,10 @@ iconUrl: "URL аватара"
|
||||
bannerUrl: "URL банера"
|
||||
basicInfo: "Основна інформація"
|
||||
pinnedUsers: "Закріплені користувачі"
|
||||
pinnedUsersDescription: "Впишіть в список користувачів, яких хочете закріпити на сторінці \"Знайти\", ім'я в стовпчик."
|
||||
pinnedPages: "Закріплені сторінки"
|
||||
pinnedPagesDescription: "Введіть шляхи сторінок, які ви бажаєте закріпити на головній сторінці цього інстанса, розділені новими рядками."
|
||||
pinnedClipId: "Ідентифікатор закріпленої замітки."
|
||||
pinnedNotes: "Закріплена нотатка"
|
||||
hcaptcha: "hCaptcha"
|
||||
enableHcaptcha: "Увімкнути hCaptcha"
|
||||
@ -328,10 +343,12 @@ name: "Ім'я"
|
||||
antennaSource: "Джерело антени"
|
||||
antennaKeywords: "Ключові слова антени"
|
||||
antennaExcludeKeywords: "Винятки"
|
||||
antennaKeywordsDescription: "Розділення ключових слів пробілами для \"І\" або з нової лінійки для \"АБО\""
|
||||
notifyAntenna: "Сповіщати про нові нотатки"
|
||||
withFileAntenna: "Тільки нотатки з вкладеними файлами"
|
||||
serviceworker: "ServiceWorker"
|
||||
enableServiceworker: "Ввімкнути ServiceWorker"
|
||||
antennaUsersDescription: "Список імя користувачів в стопчик"
|
||||
caseSensitive: "З урахуванням регістру"
|
||||
withReplies: "Включаючи відповіді"
|
||||
connectedTo: "Наступні акаунти під'єднані"
|
||||
@ -455,13 +472,21 @@ objectStorage: "Object Storage"
|
||||
useObjectStorage: "Використовувати object storage"
|
||||
objectStorageBaseUrl: "Base URL"
|
||||
objectStorageBucket: "Bucket"
|
||||
objectStorageBucketDesc: "Будь ласка вкажіть назву відра в налаштованому сервісі."
|
||||
objectStoragePrefix: "Prefix"
|
||||
objectStoragePrefixDesc: "Файли будуть зберігатись у розташуванні з цим префіксом."
|
||||
objectStorageEndpoint: "Endpoint"
|
||||
objectStorageEndpointDesc: "Залиште пустим при використанні AWS S3. Інакше введіть кінцевий пункт як '<host>' або '<host>:<port>' слідуючи інструкціям сервісу, який використовується."
|
||||
objectStorageRegion: "Region"
|
||||
objectStorageRegionDesc: "Введіть регіон у формі 'xx-east-1'. Залиште пустим, якщо ваш сервіс не різниться відповідно до регіонів, або введіть 'us-east-1'."
|
||||
objectStorageUseSSL: "Використовувати SSL"
|
||||
objectStorageUseSSLDesc: "Вимкніть коли не використовується HTTPS для з'єднання API"
|
||||
objectStorageUseProxy: "Використовувати Proxy"
|
||||
objectStorageUseProxyDesc: "Вимкніть коли проксі не використовується для з'єднання ObjectStorage"
|
||||
objectStorageSetPublicRead: "Встановіть 'публічне читання' при завантаженні"
|
||||
serverLogs: "Журнал сервера"
|
||||
deleteAll: "Видалити все"
|
||||
showFixedPostForm: "Показати форму запису над стрічкою новин."
|
||||
newNoteRecived: "Є нові нотатки"
|
||||
sounds: "Звуки"
|
||||
listen: "Слухати"
|
||||
@ -494,6 +519,8 @@ deleteAllFiles: "Видалити всі файли"
|
||||
deleteAllFilesConfirm: "Ви дійсно хочете видалити всі файли?"
|
||||
removeAllFollowing: "Скасувати всі підписки"
|
||||
removeAllFollowingDescription: "Скасувати підписку на всі акаунти з {host}. Будь ласка, робіть це, якщо інстанс більше не існує."
|
||||
userSuspended: "Обліковий запис заблокований."
|
||||
userSilenced: "Обліковий запис приглушений."
|
||||
sidebar: "Бокова панель"
|
||||
divider: "Розділювач"
|
||||
addItem: "Додати елемент"
|
||||
@ -504,6 +531,7 @@ inboxUrl: "Inbox URL"
|
||||
addedRelays: "Додані ретранслятори"
|
||||
serviceworkerInfo: "Повинен бути ввімкнений для push-сповіщень."
|
||||
deletedNote: "Видалена нотатка"
|
||||
invisibleNote: "Приховані записи"
|
||||
enableInfiniteScroll: "Увімкнути нескінченну прокрутку"
|
||||
visibility: "Видимість"
|
||||
poll: "Опитування"
|
||||
@ -514,8 +542,10 @@ expandTweet: "Розгорнути твіт"
|
||||
themeEditor: "Редактор тем"
|
||||
description: "Опис"
|
||||
author: "Автор"
|
||||
leaveConfirm: "Зміни не збережені. Ви дійсно хочете скасувати зміни?"
|
||||
manage: "Управління"
|
||||
plugins: "Плагіни"
|
||||
pluginInstallWarn: "Будь ласка не встановлюйте плагінів яким ви не довіряєте."
|
||||
deck: "Дек"
|
||||
undeck: "Залишити Дек"
|
||||
useBlurEffectForModal: "Ефект розмиття під модальними діалогами"
|
||||
@ -530,18 +560,26 @@ permission: "Права"
|
||||
enableAll: "Увімкнути все"
|
||||
disableAll: "Вимкнути все"
|
||||
tokenRequested: "Надати доступ до акаунту"
|
||||
pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані."
|
||||
notificationType: "Тип сповіщення"
|
||||
edit: "Редагувати"
|
||||
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
|
||||
emailConfig: "Налаштування email сервера"
|
||||
enableEmail: "Увімкнути функцію доставки пошти"
|
||||
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
|
||||
email: "E-mail"
|
||||
emailAddress: "E-mail адреса"
|
||||
smtpConfig: "Налаштування сервера SMTP"
|
||||
smtpHost: "Хост"
|
||||
smtpPort: "Порт"
|
||||
smtpUser: "Ім'я користувача"
|
||||
smtpPass: "Пароль"
|
||||
emptyToDisableSmtpAuth: "Залиште назву користувача і пароль пустими для вимкнення підтвердження SMTP"
|
||||
smtpSecure: "Використовувати безумовне шифрування SSL/TLS для з'єднань SMTP"
|
||||
smtpSecureInfo: "Вимкніть при використанні STARTTLS "
|
||||
testEmail: "Тестовий email"
|
||||
wordMute: "Блокування слів"
|
||||
userSaysSomething: "{name} щось сказав(ла)"
|
||||
makeActive: "Активувати"
|
||||
display: "Відображення"
|
||||
copy: "Скопіювати"
|
||||
@ -555,16 +593,24 @@ create: "Створити"
|
||||
notificationSetting: "Параметри сповіщень"
|
||||
notificationSettingDesc: "Виберіть типи сповіщень для відображення"
|
||||
useGlobalSetting: "Застосувати глобальнi параметри"
|
||||
useGlobalSettingDesc: "Якщо увімкнено, то будуть використовуватись налаштування повідомлень облікового запису, інакше можливо налаштувати індивідуально."
|
||||
other: "Інше"
|
||||
regenerateLoginToken: "Оновити Login Token"
|
||||
regenerateLoginTokenDescription: "Регенерувати внутрішній ключ використовуваний під час входу. Зазвичай цього не потрібно робити. При регенерації всі пристрої вийдуть з системи."
|
||||
setMultipleBySeparatingWithSpace: "Можна вказати кілька значень, відділивши їх пробілом."
|
||||
fileIdOrUrl: "Ідентифікатор файлу або посилання"
|
||||
chatOpenBehavior: "Поводження вікна переписки під час відкриття"
|
||||
behavior: "Поведінка"
|
||||
sample: "Приклад"
|
||||
abuseReports: "Скарги"
|
||||
reportAbuse: "Поскаржитись"
|
||||
reportAbuseOf: "Поскаржитись на {name}"
|
||||
fillAbuseReportDescription: "Будь ласка вкажіть подробиці скарги. Якщо скарга стосується запису, вкажіть посилання на нього."
|
||||
abuseReported: "Дякуємо, вашу скаргу було відправлено. "
|
||||
send: "Відправити"
|
||||
abuseMarkAsResolved: "Позначити скаргу як вирішену"
|
||||
openInNewTab: "Відкрити в новій вкладці"
|
||||
openInSideView: "Відкрити збоку"
|
||||
defaultNavigationBehaviour: "Поведінка навігації за замовчуванням"
|
||||
editTheseSettingsMayBreakAccount: "Зміна цих параметрів може призвести до пошкодження вашого акаунта."
|
||||
instanceTicker: "Мітка з назвою інстанса в нотатках"
|
||||
@ -576,6 +622,7 @@ desktop: "Десктоп"
|
||||
clip: "Добірка"
|
||||
createNew: "Створити новий"
|
||||
optional: "Необов'язково"
|
||||
createNewClip: "Створити нотатку"
|
||||
public: "Публічний"
|
||||
i18nInfo: "Misskey перекладається на різні мови волонтерами. Ви можете допомогти: {link}"
|
||||
manageAccessTokens: "Керування токенами доступу"
|
||||
@ -597,9 +644,11 @@ driveFilesCount: "Кількість файлів на диску"
|
||||
driveUsage: "Використання місця на диску"
|
||||
noCrawle: "Заборонити індексацію"
|
||||
noCrawleDescription: "Просити пошукові системи не індексувати ваш профіль, нотатки, сторінки тощо."
|
||||
lockedAccountInfo: "Якщо видимість вашого запису не встановлена як \"Тільки підписники\", то кожен зможе побачити ваш запис, навіть якщо ви вимагаєте підтвердження підписок вручну."
|
||||
alwaysMarkSensitive: "Позначати NSFW за замовчуванням"
|
||||
loadRawImages: "Відображати вкладені зображення повністю замість ескізів"
|
||||
disableShowingAnimatedImages: "Не програвати анімовані зображення"
|
||||
verificationEmailSent: "Електронний лист з підтвердженням відісланий. Будь ласка перейдіть по посиланню в листі для підтвердження."
|
||||
notSet: "Не налаштовано"
|
||||
emailVerified: "Електронну пошту підтверджено."
|
||||
noteFavoritesCount: "Кількість улюблених нотаток"
|
||||
@ -611,6 +660,40 @@ useSystemFont: "Використовувати стандартний шрифт
|
||||
clips: "Добірка"
|
||||
experimentalFeatures: "Експериментальні функції"
|
||||
developer: "Розробник"
|
||||
makeExplorable: "Зробіть обліковий запис видимим у розділі \"Огляд\""
|
||||
makeExplorableDescription: "Вимкніть, щоб обліковий запис не показувався у розділі \"Огляд\"."
|
||||
showGapBetweenNotesInTimeline: "Показувати розрив між записами у стрічці новин"
|
||||
duplicate: "Дублікат"
|
||||
left: "Лівий"
|
||||
center: "Центр"
|
||||
wide: "Широкий"
|
||||
narrow: "Вузький"
|
||||
reloadToApplySetting: "Налаштування ввійде в дію при перезавантаженні. Перезавантажити?"
|
||||
showTitlebar: "Показати титульний рядок"
|
||||
clearCache: "Очистити кеш"
|
||||
onlineUsersCount: "{n} користувачів онлайн"
|
||||
nUsers: "{n} Користувачів"
|
||||
nNotes: "{n} Записів"
|
||||
sendErrorReports: "Надіслати звіт про помилки"
|
||||
sendErrorReportsDescription: "При увімкненні детальна інформація про помилки буде надана Misskey у разі виникнення проблем, що дасть можливість покращити Misskey."
|
||||
myTheme: "Моя тема"
|
||||
backgroundColor: "Фон"
|
||||
accentColor: "Акцент"
|
||||
textColor: "Текст"
|
||||
saveAs: "Зберегти як…"
|
||||
advanced: "Розширені"
|
||||
value: "Значення"
|
||||
updatedAt: "Останнє оновлення"
|
||||
saveConfirm: "Зберегти зміни?"
|
||||
deleteConfirm: "Ви дійсно бажаєте це видалити?"
|
||||
invalidValue: "Некоректне значення."
|
||||
registry: "Реєстр"
|
||||
closeAccount: "Закрити обліковий запис"
|
||||
_registry:
|
||||
key: "Ключ"
|
||||
keys: "Ключі"
|
||||
domain: "Домен"
|
||||
createKey: "Створити ключ"
|
||||
_aboutMisskey:
|
||||
about: "Misskey - це програмне забезпечення з відкритим кодом, яке розробляє syuilo з 2014 року."
|
||||
contributors: "Головні помічники"
|
||||
@ -649,10 +732,14 @@ _mfm:
|
||||
inlineMath: "Формула (у рядку)"
|
||||
inlineMathDescription: "Відображення математичних формул (KaTeX) у рядку"
|
||||
blockMath: "Формули (блок)"
|
||||
blockMathDescription: "Відображати багаторядкові формули (KaTeX) блоками"
|
||||
quote: "Цитата"
|
||||
quoteDescription: "Відображає зміст як цитату."
|
||||
emoji: "Кастомні емоджі"
|
||||
emojiDescription: "Щоб показати нетиповий емоджі, потрібно ввести його назву в двокрапках."
|
||||
search: "Пошук"
|
||||
searchDescription: "Відображає вікно пошуку з попередньо введеним текстом"
|
||||
flip: "Перевернути"
|
||||
flipDescription: "Віддзеркалює вміст по горизонталі або вертикалі"
|
||||
jelly: "Анімація (желе)"
|
||||
jellyDescription: "Створює желеподібну анімацію"
|
||||
@ -664,6 +751,16 @@ _mfm:
|
||||
shake: "Анімація (Shake)"
|
||||
twitch: "Анімація (Twitch)"
|
||||
spin: "Анімація (Spin)"
|
||||
x2: "Великий"
|
||||
x2Description: "Показує контент збільшеним."
|
||||
x3: "Дуже великий"
|
||||
x3Description: "Показує контент ще більшим."
|
||||
x4: "Надзвичайно великий"
|
||||
x4Description: "Показує контент надзвичайно великим."
|
||||
blur: "Розмиття"
|
||||
blurDescription: "Цей ефект зробить контент розмитим. Контент можна зробити чітким, якщо навести на нього вказівник миші."
|
||||
font: "Шрифт"
|
||||
fontDescription: "Встановлює шрифт для контенту."
|
||||
_reversi:
|
||||
reversi: "Реверсі"
|
||||
gameSettings: "Налаштування гри"
|
||||
@ -692,6 +789,9 @@ _reversi:
|
||||
myGames: "Мої ігри"
|
||||
allGames: "Усі ігри"
|
||||
ended: "Завершено"
|
||||
playing: "В даний момент у процесі гри"
|
||||
isLlotheo: "Гравець з найменшою кількістю фігур виграє (Llotheo)"
|
||||
canPutEverywhere: "Фігури можна ставити в будь якії позиції"
|
||||
_instanceTicker:
|
||||
none: "Не відображати"
|
||||
remote: "Відображати для віддалених користувачів"
|
||||
@ -706,6 +806,7 @@ _channel:
|
||||
setBanner: "Встановити банер"
|
||||
removeBanner: "Видалити банер"
|
||||
featured: "Тренди"
|
||||
following: "Підписки"
|
||||
usersCount: "{n} учасників"
|
||||
notesCount: "{n} дописів"
|
||||
_sidebar:
|
||||
@ -713,12 +814,32 @@ _sidebar:
|
||||
icon: "Аватар"
|
||||
hide: "Сховати"
|
||||
_wordMute:
|
||||
muteWords: "Заглушені слова"
|
||||
muteWordsDescription: "Розділення ключових слів пробілами для \"І\" або з нової лінійки для \"АБО\""
|
||||
muteWordsDescription2: "Для використання RegEx, ключові слова потрібно вписати поміж слешів \"/\"."
|
||||
softDescription: "Приховати записи які відповідають критеріям зі стрічки подій."
|
||||
hardDescription: "Приховати записи які відповідають критеріям зі стрічки подій. Також приховані записи не будуть додані до стрічки подій навіть якщо критерії буде змінено."
|
||||
soft: "М'яко"
|
||||
hard: "Жорстко"
|
||||
mutedNotes: "Заблоковані нотатки"
|
||||
_theme:
|
||||
explore: "Оглянути теми"
|
||||
install: "Встановити тему"
|
||||
manage: "Керування темами"
|
||||
code: "Код теми"
|
||||
installed: "Тему {name} встановлено"
|
||||
installedThemes: "Встановлені теми"
|
||||
builtinThemes: "Вбудоваі теми"
|
||||
alreadyInstalled: "Тему вже встановлено"
|
||||
invalid: "Неправильний формат теми"
|
||||
make: "Створити тему"
|
||||
base: "Основа"
|
||||
defaultValue: "Значення за замовчуванням"
|
||||
func: "Функції"
|
||||
lighten: "Яскравість"
|
||||
inputConstantName: "Введіть назву константи"
|
||||
importInfo: "Вставляючи сюди код теми, ви можете добавити її до редактору тем"
|
||||
deleteConstantConfirm: "Ви дійсно бажаєте видалити константу \"{const}\"?"
|
||||
keys:
|
||||
accent: "Акцент"
|
||||
bg: "Фон"
|
||||
@ -738,13 +859,41 @@ _theme:
|
||||
mention: "Згадка"
|
||||
mentionMe: "Згадки (мене)"
|
||||
renote: "Поширити"
|
||||
modalBg: "Модальний фон"
|
||||
divider: "Розділювач"
|
||||
scrollbarHandle: "Ручка смуги прокрутки"
|
||||
scrollbarHandleHover: "Ручка смуги прокрутки (при наведенні)"
|
||||
dateLabelFg: "Текст позначок дати"
|
||||
infoBg: "Фон інформації"
|
||||
infoFg: "Текст інформації"
|
||||
infoWarnBg: "Фон попередження"
|
||||
infoWarnFg: "Текст попередження"
|
||||
cwBg: "Фон чутливого змісту"
|
||||
cwFg: "Текст чутливого змісту"
|
||||
cwHoverBg: "Фон чутливого змісту (при наведенні)"
|
||||
toastBg: "Фон повідомлення"
|
||||
toastFg: "Текст повідомлення"
|
||||
buttonBg: "Фон кнопки"
|
||||
buttonHoverBg: "Фон кнопки (при наведенні)"
|
||||
inputBorder: "Край поля вводу"
|
||||
listItemHoverBg: "Фон елементу в списку (при наведенні)"
|
||||
driveFolderBg: "Фон папки на диску"
|
||||
wallpaperOverlay: "Накладання шпалер"
|
||||
badge: "Значок"
|
||||
messageBg: "Фон переписки"
|
||||
accentDarken: "Акцент (Затемлений)"
|
||||
accentLighten: "Акцент (Освітлений)"
|
||||
fgHighlighted: "Виділений текст"
|
||||
_sfx:
|
||||
note: "Нотатки"
|
||||
noteMy: "Мої нотатки"
|
||||
notification: "Сповіщення"
|
||||
chat: "Чати"
|
||||
chatBg: "Чати (фон)"
|
||||
antenna: "Прийом антени"
|
||||
channel: "Повідомлення каналу"
|
||||
reversiPutBlack: "Реверсі: хід Чорного"
|
||||
reversiPutWhite: "Реверсі: хід Білого"
|
||||
_ago:
|
||||
unknown: "Невідомо"
|
||||
future: "Майбутнє"
|
||||
@ -764,8 +913,26 @@ _time:
|
||||
_tutorial:
|
||||
title: "Як користуватись Misskey"
|
||||
step1_1: "Ласкаво просимо!"
|
||||
step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів на яких ви підписані."
|
||||
step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки і не підписані на інших."
|
||||
step2_1: "Перш ніж зробити запис або підписатись на когось, спочатку заповніть свій обліковий запис."
|
||||
step2_2: "Надання деякої інформації про себе дозволить іншим користувачам підписатись на вас."
|
||||
step3_1: "Ви успішно налаштували свій обліковий запис?"
|
||||
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
|
||||
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
|
||||
step3_4: "Не знаєте що написати? Спробуйте \"налаштовую свій msky\"!"
|
||||
step4_1: "Ви розмістили свій перший запис?"
|
||||
step4_2: "Ура! Ваш перший запис відображається на вашій стрічці подій."
|
||||
step5_1: "Настав час оживити вашу стрічку подій підписавшись на інших користувачів."
|
||||
step5_2: "{featured} показує популярні записи , а {explore} популярних користувачів з цього інстансу. Спробуйте підписатись на користувача, який вам сподобався!"
|
||||
step5_3: "Щоб підписатись на інших користувачів, нажміть на їхнє зображення, а потім на кнопку \"підписатись\"."
|
||||
step5_4: "Якщо користувач має замок при імені, то йому потрібно буде вручну підтвердити вашу заявку на підписку."
|
||||
step6_1: "Тепер ви повинні бачити записи інших користувачів на вашій стрічці подій."
|
||||
step6_2: "Також ви можете швидко відповісти, або \"відреагувати\" на записи інших користувачів."
|
||||
step6_3: "Щоб \"відреагувати\", нажміть на знак плюс \"+\" на записі і виберіть емоджі яким ви хочете \"відреагувати\"."
|
||||
step7_1: "Вітаю! Ви пройшли ознайомлення з Misskey."
|
||||
step7_2: "Якщо ви хочете більше дізнатись про Misskey, зайдіть в розділ {help}."
|
||||
step7_3: "Насолоджуйтесь Misskey! 🚀"
|
||||
_2fa:
|
||||
registerKey: "Зареєструвати новий ключ безпеки"
|
||||
_permissions:
|
||||
@ -825,6 +992,7 @@ _widgets:
|
||||
postForm: "Створення нотатки"
|
||||
slideshow: "Слайд-шоу"
|
||||
button: "Кнопка"
|
||||
jobQueue: "Черга завдань"
|
||||
_cw:
|
||||
hide: "Сховати"
|
||||
show: "Показати більше"
|
||||
@ -899,8 +1067,12 @@ _charts:
|
||||
_instanceCharts:
|
||||
requests: "Запити"
|
||||
usersTotal: "Сумарна кількість користувачів"
|
||||
notes: "Різниця кількості зроблених записів"
|
||||
notesTotal: "Сумарна кількість нотаток"
|
||||
ff: "Різниця кількості підписників"
|
||||
ffTotal: "Кількість підписників"
|
||||
cacheSizeTotal: "Сумарний розмір кешу"
|
||||
files: "Різниця в кількості файлів"
|
||||
filesTotal: "Сумарна кількість файлів"
|
||||
_timelines:
|
||||
home: "Домівка"
|
||||
@ -915,6 +1087,7 @@ _rooms:
|
||||
exit: "Назад"
|
||||
remove: "Видалити"
|
||||
clear: "Видалити все"
|
||||
clearConfirm: "Ви дійсно хочете позбутись усіх речей у вашій кімнаті?"
|
||||
leaveConfirm: "Є незбережені зміни. Ви дійсно хочете вийти?"
|
||||
chooseImage: "Виберіть зображення"
|
||||
roomType: "Тип кімнати"
|
||||
@ -963,6 +1136,7 @@ _rooms:
|
||||
sofa: "Диван"
|
||||
spiral: "Гвинтові сходи"
|
||||
bin: "Смітник"
|
||||
cup-noodle: "Локшина в чашці"
|
||||
holo-display: "Голографічний дисплей"
|
||||
energy-drink: "Енергетичний напій"
|
||||
doll-ai: "Лялька Аі-тян"
|
||||
@ -988,6 +1162,7 @@ _pages:
|
||||
featured: "Популярні"
|
||||
inspector: "Інспектор"
|
||||
contents: "Вміст"
|
||||
content: "Блок сторінки"
|
||||
variables: "Змінні"
|
||||
title: "Заголовок"
|
||||
url: "URL сторінки"
|
||||
@ -1002,6 +1177,7 @@ _pages:
|
||||
chooseBlock: "Додати блок"
|
||||
selectType: "Виберіть тип"
|
||||
enterVariableName: "Введіть назву для змінної"
|
||||
variableNameIsAlreadyUsed: "Ця назва вже використовується іншою змінною"
|
||||
contentBlocks: "Контент"
|
||||
inputBlocks: "Ввід"
|
||||
specialBlocks: "Особливе"
|
||||
@ -1017,6 +1193,7 @@ _pages:
|
||||
post: "Створення нотатки"
|
||||
_post:
|
||||
text: "Вміст"
|
||||
canvasId: "Ідентифікатор полотна"
|
||||
textInput: "Введення тексту"
|
||||
_textInput:
|
||||
name: "Ім'я змінної"
|
||||
@ -1032,7 +1209,9 @@ _pages:
|
||||
name: "Ім'я змінної"
|
||||
text: "Назва"
|
||||
default: "Значення за замовчуванням"
|
||||
canvas: "Полотно"
|
||||
_canvas:
|
||||
id: "Ідентифікатор полотна"
|
||||
width: "Ширина"
|
||||
height: "Висота"
|
||||
note: "Вбудована нотатка"
|
||||
@ -1052,7 +1231,10 @@ _pages:
|
||||
inc: "Збільшити на"
|
||||
_button:
|
||||
text: "Напис"
|
||||
colored: "Кольоровий"
|
||||
action: "Дія кнопки"
|
||||
_action:
|
||||
dialog: "Показати повідомлення"
|
||||
_dialog:
|
||||
content: "Вміст"
|
||||
resetRandom: "Скидання генератора випадковості"
|
||||
@ -1087,11 +1269,15 @@ _pages:
|
||||
text: "Текст"
|
||||
multiLineText: "Текст (багаторядковий)"
|
||||
textList: "Текстовий список"
|
||||
_textList:
|
||||
info: "Використовувати новий рядок як роздільник для вводу"
|
||||
strLen: "Довжина тексту"
|
||||
_strLen:
|
||||
arg1: "Текст"
|
||||
strPick: "Вибрати символ"
|
||||
_strPick:
|
||||
arg1: "Текст"
|
||||
arg2: "Розташування символу"
|
||||
strReplace: "Заміна тексту"
|
||||
_strReplace:
|
||||
arg1: "Текст"
|
||||
@ -1224,17 +1410,25 @@ _pages:
|
||||
aiScriptVar: "Змінна AiScript"
|
||||
fn: "Функції"
|
||||
_fn:
|
||||
slots: "Паз"
|
||||
slots-info: "Використовувати нову лінію як роздільник пазів"
|
||||
arg1: "Вивід"
|
||||
for: "Повторення"
|
||||
_for:
|
||||
arg1: "Кількість повторень"
|
||||
arg2: "Дія"
|
||||
typeError: "Паз {slot} приймає \"{expect}\" тип, але надана змінна має тип \"{actual}\"!"
|
||||
thereIsEmptySlot: "Паз {slot} пустий!"
|
||||
types:
|
||||
string: "Текст"
|
||||
number: "Число"
|
||||
boolean: "Прапорець"
|
||||
array: "Списки"
|
||||
stringArray: "Текстовий список"
|
||||
emptySlot: "Пустий паз"
|
||||
enviromentVariables: "Змінні середовища"
|
||||
pageVariables: "Елемент сторінки"
|
||||
argVariables: "Стрічка вводу"
|
||||
_relayStatus:
|
||||
requesting: "Очікує затвердження"
|
||||
accepted: "Затверджено"
|
||||
@ -1268,6 +1462,8 @@ _notification:
|
||||
_deck:
|
||||
alwaysShowMainColumn: "Завжди показувати головну колонку"
|
||||
columnAlign: "Вирівняти стовпці"
|
||||
columnMargin: "Відступ між стовпцями"
|
||||
columnHeaderHeight: "Висота заголовку колони"
|
||||
addColumn: "Додати стовпець"
|
||||
swapLeft: "Пересунути ліворуч"
|
||||
swapRight: "Пересунути праворуч"
|
||||
@ -1275,7 +1471,9 @@ _deck:
|
||||
swapDown: "Пересунути вниз"
|
||||
stackLeft: "У стовпчик вліво"
|
||||
popRight: "Витягнути вправо"
|
||||
profile: "Обліковий запис"
|
||||
_columns:
|
||||
main: "Головна"
|
||||
widgets: "Віджети"
|
||||
notifications: "Сповіщення"
|
||||
tl: "Стрічка"
|
||||
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
_lang_: "中文(简体)"
|
||||
headlineMisskey: "通过帖子连接在一起的网络"
|
||||
introMisskey: "欢迎!Misskey是一个开源的、去中心化的“微博客”服务。\n通过编写「帖文」来和大家分享你的以及你周围的事情吧!📡\n通过「回应」功能,可以让你快速地对大家的帖文表达反馈👍\n来探索新的世界吧!🚀"
|
||||
monthAndDay: "{month}月 {day}日"
|
||||
search: "搜索"
|
||||
@ -49,6 +50,7 @@ copyUsername: "复制用户名"
|
||||
searchUser: "搜索用户"
|
||||
reply: "回复"
|
||||
loadMore: "查看更多"
|
||||
showMore: "查看更多"
|
||||
youGotNewFollower: "你有新的关注者"
|
||||
receiveFollowRequest: "您收到了关注请求"
|
||||
followRequestAccepted: "您的关注请求被通过了"
|
||||
@ -109,12 +111,12 @@ unmarkAsSensitive: "取消标记为敏感内容"
|
||||
enterFileName: "请输入文件名"
|
||||
mute: "屏蔽"
|
||||
unmute: "解除屏蔽"
|
||||
block: "屏蔽"
|
||||
unblock: "取消屏蔽"
|
||||
block: "拉黑"
|
||||
unblock: "取消拉黑"
|
||||
suspend: "冻结"
|
||||
unsuspend: "解除冻结"
|
||||
blockConfirm: "确定要屏蔽吗?"
|
||||
unblockConfirm: "确定要解除屏蔽吗?"
|
||||
blockConfirm: "确定要拉黑吗?"
|
||||
unblockConfirm: "确定要解除拉黑吗?"
|
||||
suspendConfirm: "要冻结吗?"
|
||||
unsuspendConfirm: "要解除冻结吗?"
|
||||
selectList: "选择列表"
|
||||
@ -183,8 +185,8 @@ clearCachedFilesConfirm: "确定要清除缓存文件?"
|
||||
blockedInstances: "被阻拦的实例"
|
||||
blockedInstancesDescription: "设定要阻拦的实例,以换行来进行分割。被阻拦的实例将无法与本实例进行交换通讯。"
|
||||
muteAndBlock: "屏蔽/拉黑"
|
||||
mutedUsers: "禁言用户"
|
||||
blockedUsers: "已屏蔽用户"
|
||||
mutedUsers: "已屏蔽用户"
|
||||
blockedUsers: "被拉黑的用户"
|
||||
noUsers: "无用户"
|
||||
editProfile: "编辑资料"
|
||||
noteDeleteConfirm: "要删除该帖子吗?"
|
||||
@ -197,7 +199,7 @@ default: "默认"
|
||||
noCustomEmojis: "没有自定义表情符号"
|
||||
noJobs: "没有任务"
|
||||
federating: "联合中"
|
||||
blocked: "已拦截"
|
||||
blocked: "已拉黑"
|
||||
suspended: "停止推流"
|
||||
all: "全部"
|
||||
subscribing: "已订阅"
|
||||
@ -639,8 +641,8 @@ sentReactionsCount: "发送反应数"
|
||||
receivedReactionsCount: "收到反应数"
|
||||
pollVotesCount: "发起投票数"
|
||||
pollVotedCount: "已投票数"
|
||||
yes: "确定"
|
||||
no: "取消"
|
||||
yes: "是"
|
||||
no: "否"
|
||||
driveFilesCount: "磁盘文件数"
|
||||
driveUsage: "磁盘空间用量"
|
||||
noCrawle: "拒绝搜索器的索引"
|
||||
@ -671,6 +673,31 @@ wide: "宽"
|
||||
narrow: "窄"
|
||||
reloadToApplySetting: "页面刷新后设置才会生效。是否现在刷新页面?"
|
||||
showTitlebar: "显示标题栏"
|
||||
clearCache: "清除缓存"
|
||||
onlineUsersCount: "{n}人在线"
|
||||
nUsers: "{n}用户"
|
||||
nNotes: "{n}帖子"
|
||||
sendErrorReports: "发送错误报告"
|
||||
sendErrorReportsDescription: "启用后,如果出现问题,可以与Misskey共享详细的错误信息,从而帮助提高软件的质量。"
|
||||
myTheme: "我的主题"
|
||||
backgroundColor: "背景"
|
||||
accentColor: "强调色"
|
||||
textColor: "文本"
|
||||
saveAs: "另存为"
|
||||
advanced: "高级"
|
||||
value: "值"
|
||||
updatedAt: "更新时间"
|
||||
saveConfirm: "确定保存?"
|
||||
deleteConfirm: "确定删除?"
|
||||
invalidValue: "无效值。"
|
||||
registry: "注册表"
|
||||
closeAccount: "关闭账户"
|
||||
_registry:
|
||||
scope: "范围"
|
||||
key: "主要"
|
||||
keys: "主要"
|
||||
domain: "域"
|
||||
createKey: "创建键"
|
||||
_aboutMisskey:
|
||||
about: "Misskey是由syuilo于2014年开发的开源软件。"
|
||||
contributors: "主要贡献者"
|
||||
@ -732,6 +759,16 @@ _mfm:
|
||||
twitchDescription: "显示强烈颤抖的动画效果。"
|
||||
spin: "动画(回转)"
|
||||
spinDescription: "显示回转的动画效果。"
|
||||
x2: "大"
|
||||
x2Description: "以大尺寸显示内容。"
|
||||
x3: "非常大"
|
||||
x3Description: "以更大尺寸显示内容。"
|
||||
x4: "最大"
|
||||
x4Description: "以最大尺寸显示内容。"
|
||||
blur: "模糊"
|
||||
blurDescription: "产生模糊效果。将鼠标指针放在上面即可将内容显示出来。"
|
||||
font: "字体"
|
||||
fontDescription: "可以设置内容所使用的字体。"
|
||||
_reversi:
|
||||
reversi: "黑白棋"
|
||||
gameSettings: "对局设置"
|
||||
@ -992,6 +1029,9 @@ _widgets:
|
||||
postForm: "投稿窗口"
|
||||
slideshow: "幻灯片展示"
|
||||
button: "按钮"
|
||||
onlineUsers: "在线用户"
|
||||
jobQueue: "作业队列"
|
||||
serverMetric: "服务器指标"
|
||||
_cw:
|
||||
hide: "隐藏"
|
||||
show: "查看更多"
|
||||
@ -1057,7 +1097,7 @@ _exportOrImport:
|
||||
allNotes: "所有帖子"
|
||||
followingList: "关注中"
|
||||
muteList: "屏蔽"
|
||||
blockingList: "屏蔽"
|
||||
blockingList: "拉黑"
|
||||
userLists: "列表"
|
||||
_charts:
|
||||
federationInstancesIncDec: "联合:增加/减少"
|
||||
@ -1483,6 +1523,7 @@ _deck:
|
||||
swapDown: "向下移动"
|
||||
stackLeft: "向左折叠"
|
||||
popRight: "向右弹出"
|
||||
profile: "个人资料"
|
||||
_columns:
|
||||
main: "主列"
|
||||
widgets: "小工具"
|
||||
|
@ -1,12 +1,12 @@
|
||||
---
|
||||
_lang_: "繁體中文"
|
||||
introMisskey: "歡迎! Misskey是一個開源的去中心化的社群網站。\n通過「貼文」來分享現在發生的事情吧! 📡\n「反應」功能,可以讓你快速的對大家的「帖子」來表達感情👍\n一起來探索新的世界吧! 🚀"
|
||||
introMisskey: "歡迎! Misskey是一個開源且去中心化的社群網絡。\n通過「箋文」分享周邊新鮮事,並告訴其他人您的想法!📡\n透過「情感」功能,對大家的箋文表達情感!👍\n一起來探索這個新的世界吧!🚀"
|
||||
monthAndDay: "{month}月 {day}日"
|
||||
search: "搜尋"
|
||||
notifications: "通知"
|
||||
username: "使用者名稱"
|
||||
password: "密碼"
|
||||
fetchingAsApObject: "從 Fediverse 查詢中..."
|
||||
fetchingAsApObject: "從聯邦宇宙取得中..."
|
||||
ok: "OK"
|
||||
gotIt: "知道了"
|
||||
cancel: "取消"
|
||||
@ -31,7 +31,7 @@ save: "儲存"
|
||||
users: "使用者"
|
||||
addUser: "新增使用者"
|
||||
favorite: "收藏"
|
||||
favorites: "已加星號"
|
||||
favorites: "已收藏"
|
||||
unfavorite: "取消收藏"
|
||||
favorited: "已添加至收藏夾"
|
||||
alreadyFavorited: "已經有添加入收藏夾過了"
|
||||
@ -42,30 +42,31 @@ copyContent: "複製內容"
|
||||
copyLink: "複製連結"
|
||||
delete: "刪除"
|
||||
deleteAndEdit: "刪除並編輯"
|
||||
deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應,轉發和回覆也將會消失。"
|
||||
addToList: "添加至清單"
|
||||
deleteAndEditConfirm: "要刪除並再次編輯嗎?此箋文的所有情感、轉發和回覆也將會消失。"
|
||||
addToList: "新增至清單"
|
||||
sendMessage: "發送訊息"
|
||||
copyUsername: "複製用戶名"
|
||||
searchUser: "搜尋用戶"
|
||||
reply: "回覆"
|
||||
loadMore: "瀏覽更多"
|
||||
loadMore: "載入更多"
|
||||
showMore: "載入更多"
|
||||
youGotNewFollower: "您有新的追隨者"
|
||||
receiveFollowRequest: "收到追隨請求"
|
||||
receiveFollowRequest: "您有新的追隨請求"
|
||||
followRequestAccepted: "追隨請求已接受"
|
||||
mention: "提及"
|
||||
mentions: "提及"
|
||||
directNotes: "私信"
|
||||
importAndExport: "匯入 / 匯出"
|
||||
directNotes: "指定使用者發佈"
|
||||
importAndExport: "匯入與匯出"
|
||||
import: "匯入"
|
||||
export: "匯出"
|
||||
files: "檔案"
|
||||
download: "下載"
|
||||
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。"
|
||||
unfollowConfirm: "確定要取消對{name}的追隨嗎?"
|
||||
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的箋文也會跟著消失。\n"
|
||||
unfollowConfirm: "確定要取消追隨{name}嗎?"
|
||||
exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。"
|
||||
importRequested: "已請求匯入。這可能會花一點時間"
|
||||
lists: "清單"
|
||||
noLists: "沒有清單"
|
||||
noLists: "你沒有任何清單"
|
||||
note: "箋文"
|
||||
notes: "箋文"
|
||||
following: "追隨中"
|
||||
@ -81,28 +82,28 @@ pageLoadErrorDescription: "這通常是因為網路錯誤或是瀏覽器快取
|
||||
enterListName: "輸入清單名稱"
|
||||
privacy: "隱私"
|
||||
makeFollowManuallyApprove: "手動審核追隨請求"
|
||||
defaultNoteVisibility: "預設的筆記隱私權"
|
||||
defaultNoteVisibility: "預設可見性"
|
||||
follow: "追隨"
|
||||
followRequest: "追隨請求"
|
||||
followRequests: "追隨請求"
|
||||
unfollow: "取消追隨"
|
||||
followRequestPending: "追隨許可批准中"
|
||||
enterEmoji: "輸入表情符號"
|
||||
renote: "轉發貼文"
|
||||
unrenote: "取消轉發貼文"
|
||||
renote: "轉箋"
|
||||
unrenote: "取消轉箋"
|
||||
renoted: "轉發成功"
|
||||
cantRenote: "這篇貼文無法轉發。"
|
||||
cantRenote: "無法轉發此箋文。"
|
||||
cantReRenote: "無法轉發之前已經轉發過的內容"
|
||||
quote: "引用"
|
||||
pinnedNote: "已置頂的貼文"
|
||||
pinnedNote: "已置頂的箋文"
|
||||
you: "您"
|
||||
clickToShow: "按一下以顯示"
|
||||
sensitive: "敏感內容"
|
||||
add: "新增"
|
||||
reaction: "反應"
|
||||
reaction: "情感"
|
||||
reactionSettingDescription: "置頂「反應」表情符號\n"
|
||||
reactionSettingDescription2: "拖動以重新列序,點擊以刪除,按下 + 添加。"
|
||||
rememberNoteVisibility: "記住筆記隱私設定"
|
||||
rememberNoteVisibility: "記住箋文可見性"
|
||||
attachCancel: "移除附件"
|
||||
markAsSensitive: "標記為敏感內容"
|
||||
unmarkAsSensitive: "取消標記為敏感內容"
|
||||
@ -112,7 +113,7 @@ unmute: "解除靜音"
|
||||
block: "封鎖"
|
||||
unblock: "解除封鎖"
|
||||
suspend: "凍結"
|
||||
unsuspend: "解凍"
|
||||
unsuspend: "解除凍結"
|
||||
blockConfirm: "確定要封鎖此用戶?"
|
||||
unblockConfirm: "確定解除封鎖此用戶?"
|
||||
suspendConfirm: "確定凍結此帳號?"
|
||||
@ -121,7 +122,7 @@ selectList: "選擇清單"
|
||||
selectAntenna: "選擇天線"
|
||||
selectWidget: "選擇小工具"
|
||||
editWidgets: "編輯小工具"
|
||||
editWidgetsExit: "停止編輯"
|
||||
editWidgetsExit: "完成"
|
||||
customEmojis: "自訂表情符號"
|
||||
emoji: "表情符號"
|
||||
emojiName: "表情符號名稱"
|
||||
@ -129,13 +130,13 @@ emojiUrl: "表情符號URL"
|
||||
addEmoji: "新增表情符號"
|
||||
settingGuide: "推薦設定"
|
||||
cacheRemoteFiles: "緩存非遠程檔案"
|
||||
cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間。但資料會因直接連線從而產生額外連接數據。"
|
||||
cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外連接數據。"
|
||||
flagAsBot: "此使用者是機器人"
|
||||
flagAsBotDescription: "如果本帳戶是由程式控制,請啟用此選項。啟用後,會作為標示幫助其他開發者防止機器人之間產生無限互動的行為,並會調整Misskey內部系統將本帳戶識別為機器人"
|
||||
flagAsCat: "此使用者是貓"
|
||||
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示"
|
||||
autoAcceptFollowed: "自動許可追隨"
|
||||
addAcount: "新增帳號"
|
||||
autoAcceptFollowed: "自動追隨中使用者的追隨請求"
|
||||
addAcount: "新增帳戶"
|
||||
loginFailed: "登入失敗"
|
||||
showOnRemote: "轉到所在實例顯示"
|
||||
general: "一般"
|
||||
@ -143,15 +144,15 @@ wallpaper: "桌布"
|
||||
setWallpaper: "設定桌布"
|
||||
removeWallpaper: "移除桌布"
|
||||
searchWith: "搜尋: {q}"
|
||||
youHaveNoLists: "沒有任何清單"
|
||||
followConfirm: "你真的要關注{name}嗎?"
|
||||
youHaveNoLists: "你沒有任何清單"
|
||||
followConfirm: "你真的要追隨{name}嗎?"
|
||||
proxyAccount: "代理帳號"
|
||||
proxyAccountDescription: "代理帳號是在某些情況下充當其他服務器用戶的帳號。例如,當用戶將一個來自其他服務器的帳號放在列表中時,由於沒有其他用戶關注該帳號,該指令不會傳送到該服務器上,因此會由代理帳戶關注。"
|
||||
host: "主機"
|
||||
selectUser: "選取使用者"
|
||||
recipient: "發送至"
|
||||
recipient: "收件人"
|
||||
annotation: "註解"
|
||||
federation: "聯邦宇宙"
|
||||
federation: "站台聯邦"
|
||||
instances: "實例"
|
||||
registeredAt: "初次觀測"
|
||||
latestRequestSentAt: "上次發送的請求"
|
||||
@ -165,7 +166,7 @@ blockThisInstance: "封鎖此實例"
|
||||
operations: "操作"
|
||||
software: "軟體"
|
||||
version: "版本"
|
||||
metadata: "元資料(Metadata)"
|
||||
metadata: "元資料"
|
||||
withNFiles: "{n}個檔案"
|
||||
monitor: "監視器"
|
||||
jobQueue: "佇列"
|
||||
@ -181,13 +182,13 @@ clearCachedFiles: "清除快取資料"
|
||||
clearCachedFilesConfirm: "確定要清除緩存資料嗎?"
|
||||
blockedInstances: "已封鎖的實例"
|
||||
blockedInstancesDescription: "請逐行輸入需要封鎖的實例。已封鎖的實例將無法與本實例進行通訊。"
|
||||
muteAndBlock: "靜音/封鎖"
|
||||
muteAndBlock: "靜音和封鎖"
|
||||
mutedUsers: "已靜音用戶"
|
||||
blockedUsers: "已封鎖用戶"
|
||||
noUsers: "無用戶"
|
||||
noUsers: "沒有任何使用者"
|
||||
editProfile: "編輯個人檔案"
|
||||
noteDeleteConfirm: "確定刪除此貼文嗎?"
|
||||
pinLimitExceeded: "不能再置頂更多的貼文了"
|
||||
noteDeleteConfirm: "確定刪除此箋文嗎?"
|
||||
pinLimitExceeded: "不能置頂更多箋文了"
|
||||
intro: "Misskey 部署完成!請開設管理員帳號!"
|
||||
done: "完成"
|
||||
processing: "處理中"
|
||||
@ -200,31 +201,31 @@ blocked: "已封鎖"
|
||||
suspended: "已凍結"
|
||||
all: "全部"
|
||||
subscribing: "訂閱中"
|
||||
publishing: "現正直播"
|
||||
publishing: "直播中"
|
||||
notResponding: "沒有回應"
|
||||
instanceFollowing: "追蹤實例"
|
||||
instanceFollowers: "追蹤實例"
|
||||
instanceUsers: "用戶"
|
||||
changePassword: "修改密碼"
|
||||
security: "安全性"
|
||||
retypedNotMatch: "不相符的輸入內容"
|
||||
currentPassword: "現在的密碼"
|
||||
newPassword: "新的密碼"
|
||||
newPasswordRetype: "新的密碼(再輸入一次)"
|
||||
retypedNotMatch: "兩次輸入不一致。"
|
||||
currentPassword: "目前密碼"
|
||||
newPassword: "新密碼"
|
||||
newPasswordRetype: "確認密碼"
|
||||
attachFile: "添加附件"
|
||||
more: "更多!"
|
||||
featured: "精選"
|
||||
usernameOrUserId: "使用者名稱或使用者 ID"
|
||||
usernameOrUserId: "使用者名稱或用戶ID"
|
||||
noSuchUser: "使用者不存在"
|
||||
lookup: "查詢"
|
||||
announcements: "公告"
|
||||
imageUrl: "圖片URL"
|
||||
remove: "刪除"
|
||||
removed: "成功移除"
|
||||
removed: "已刪除"
|
||||
removeAreYouSure: "確定要刪掉「{x}」嗎?"
|
||||
deleteAreYouSure: "確定要刪掉「{x}」嗎?"
|
||||
resetAreYouSure: "確定要重設嗎?"
|
||||
saved: "已保存"
|
||||
saved: "已儲存"
|
||||
messaging: "傳送訊息"
|
||||
upload: "上傳"
|
||||
fromDrive: "從雲端"
|
||||
@ -269,9 +270,9 @@ folderName: "資料夾名稱"
|
||||
createFolder: "新增資料夾"
|
||||
renameFolder: "重新命名資料夾"
|
||||
deleteFolder: "刪除資料夾"
|
||||
addFile: "添加檔案"
|
||||
addFile: "添加附件"
|
||||
emptyDrive: "雲端硬碟為空"
|
||||
emptyFolder: "空的資料夾"
|
||||
emptyFolder: "資料夾為空"
|
||||
unableToDelete: "無法刪除"
|
||||
inputNewFileName: "輸入檔案名稱"
|
||||
inputNewFolderName: "輸入新資料夾的名稱"
|
||||
@ -279,7 +280,7 @@ circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件
|
||||
hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。"
|
||||
copyUrl: "複製URL"
|
||||
rename: "重新命名"
|
||||
avatar: "頭像"
|
||||
avatar: "大頭貼"
|
||||
banner: "橫幅"
|
||||
nsfw: "敏感內容"
|
||||
whenServerDisconnected: "與服務器的連接中斷時"
|
||||
@ -293,11 +294,11 @@ accept: "接受"
|
||||
reject: "拒絕"
|
||||
normal: "正常"
|
||||
instanceName: "實例名稱"
|
||||
instanceDescription: "實例描述"
|
||||
instanceDescription: "實例說明"
|
||||
maintainerName: "管理員名稱"
|
||||
maintainerEmail: "管理員信箱"
|
||||
maintainerEmail: "管理員郵箱"
|
||||
tosUrl: "服務條款URL"
|
||||
thisYear: "今年"
|
||||
thisYear: "本年"
|
||||
thisMonth: "本月"
|
||||
today: "本日"
|
||||
dayX: "{day}日"
|
||||
@ -325,7 +326,7 @@ pinnedUsers: "置頂用戶"
|
||||
pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的用戶。"
|
||||
pinnedPages: "釘選頁面"
|
||||
pinnedPagesDescription: "輸入要固定至實例首頁的頁面路徑,以換行符分隔。"
|
||||
pinnedNotes: "已置頂的貼文"
|
||||
pinnedNotes: "已置頂的箋文"
|
||||
hcaptcha: "hCaptcha"
|
||||
enableHcaptcha: "啟用 hCaptcha"
|
||||
hcaptchaSiteKey: "網站金鑰"
|
||||
@ -342,7 +343,7 @@ antennaSource: "接收來源"
|
||||
antennaKeywords: "包含關鍵字"
|
||||
antennaExcludeKeywords: "排除關鍵字"
|
||||
antennaKeywordsDescription: "用空格分隔指定AND、用換行符分隔指定OR"
|
||||
notifyAntenna: "通知我有新的貼文"
|
||||
notifyAntenna: "通知有新箋文"
|
||||
withFileAntenna: "僅帶有附件的箋文"
|
||||
serviceworker: "ServiceWorker"
|
||||
enableServiceworker: "開啟 ServiceWorker"
|
||||
@ -350,7 +351,7 @@ antennaUsersDescription: "指定用換行符分隔的用戶名"
|
||||
caseSensitive: "區分大小寫"
|
||||
withReplies: "包含回覆"
|
||||
connectedTo: "您的帳號已連接到以下社交帳號"
|
||||
notesAndReplies: "貼文與回覆"
|
||||
notesAndReplies: "箋文與回覆"
|
||||
withFiles: "附件"
|
||||
silence: "禁言"
|
||||
silenceConfirm: "確定要禁言此用戶嗎?"
|
||||
@ -367,7 +368,7 @@ userList: "清單"
|
||||
about: "資訊"
|
||||
aboutMisskey: "關於 Misskey"
|
||||
administrator: "管理員"
|
||||
token: "令牌"
|
||||
token: "權杖"
|
||||
twoStepAuthentication: "雙重身份驗證"
|
||||
moderator: "板主"
|
||||
nUsersMentioned: "提到了{n}"
|
||||
@ -386,7 +387,7 @@ notFoundDescription: "找不到與指定URL回應的頁面"
|
||||
uploadFolder: "預設上傳資料夾"
|
||||
cacheClear: "清除快取"
|
||||
markAsReadAllNotifications: "標記所有通知為已讀"
|
||||
markAsReadAllUnreadNotes: "標記所有貼文為已讀"
|
||||
markAsReadAllUnreadNotes: "標記所有箋文為已讀"
|
||||
markAsReadAllTalkMessages: "標記所有訊息為已讀"
|
||||
help: "幫助"
|
||||
inputMessageHere: "在此輸入訊息"
|
||||
@ -409,7 +410,7 @@ next: "下一步"
|
||||
retype: "重新輸入"
|
||||
noteOf: "{user}的箋文"
|
||||
inviteToGroup: "邀請至群組"
|
||||
maxNoteTextLength: "貼文的字數限制"
|
||||
maxNoteTextLength: "箋文的字數限制"
|
||||
quoteAttached: "引用"
|
||||
quoteQuestion: "是否要引用?"
|
||||
noMessagesYet: "沒有訊息"
|
||||
@ -461,10 +462,10 @@ dayOverDayChanges: "與前一日相比"
|
||||
appearance: "外觀"
|
||||
clientSettings: "用戶端設定"
|
||||
accountSettings: "帳號設定"
|
||||
promotion: "推廣貼文"
|
||||
promotion: "推廣"
|
||||
promote: "推廣"
|
||||
numberOfDays: "有效天數"
|
||||
hideThisNote: "隱藏此貼文"
|
||||
hideThisNote: "隱藏此箋文"
|
||||
showFeaturedNotesInTimeline: "在時間軸上顯示熱門推薦"
|
||||
objectStorageBaseUrl: "Base URL"
|
||||
objectStorageBucket: "儲存空間(Bucket)"
|
||||
@ -477,8 +478,8 @@ objectStorageUseProxy: "使用網路代理"
|
||||
objectStorageSetPublicRead: "上載時設定為\"public-read\""
|
||||
serverLogs: "伺服器日誌"
|
||||
deleteAll: "刪除所有記錄"
|
||||
showFixedPostForm: "在時間線頂部顯示貼文表格"
|
||||
newNoteRecived: "有新的箋文"
|
||||
showFixedPostForm: "於時間軸頁頂顯示「發送箋文」方框"
|
||||
newNoteRecived: "發現新的箋文"
|
||||
sounds: "音效"
|
||||
listen: "聆聽"
|
||||
none: "無"
|
||||
@ -520,10 +521,10 @@ addRelay: "添加中繼"
|
||||
inboxUrl: "私信URL"
|
||||
addedRelays: "已添加的中繼"
|
||||
serviceworkerInfo: "您需要啟用推送通知"
|
||||
deletedNote: "已删除的貼文"
|
||||
deletedNote: "已删除的箋文"
|
||||
invisibleNote: "隱藏的帖子"
|
||||
enableInfiniteScroll: "啟用自動滾動頁面模式"
|
||||
visibility: "公開範圍"
|
||||
visibility: "可見性"
|
||||
poll: "投票"
|
||||
useCw: "隱藏內容"
|
||||
enablePlayer: "打開播放器"
|
||||
@ -539,6 +540,7 @@ pluginInstallWarn: "請不要安裝來源不明的插件。"
|
||||
deck: "多欄模式"
|
||||
undeck: "取消多欄模式"
|
||||
useBlurEffectForModal: "在模態框使用模糊效果"
|
||||
useFullReactionPicker: "使用大尺寸的情感選擇欄"
|
||||
width: "寬度"
|
||||
height: "高度"
|
||||
large: "大"
|
||||
@ -593,30 +595,49 @@ send: "發送"
|
||||
abuseMarkAsResolved: "處理完畢"
|
||||
openInNewTab: "在新分頁中開啟"
|
||||
openInSideView: "在側欄中開啟"
|
||||
instanceTicker: "箋文的實例資訊"
|
||||
editTheseSettingsMayBreakAccount: "修改這些設定可能會毀壞您的帳戶"
|
||||
instanceTicker: "箋文的實例來源"
|
||||
waitingFor: "等待{x}"
|
||||
random: "隨機"
|
||||
system: "系統"
|
||||
switchUi: "切換界面"
|
||||
desktop: "桌面"
|
||||
clip: "片段"
|
||||
createNew: "新建"
|
||||
optional: "可選"
|
||||
public: "公開"
|
||||
i18nInfo: "Misskey已經被志願者們翻譯成各種語言版本,如果想要幫忙的話,可以進入{link}幫助翻譯。"
|
||||
manageAccessTokens: "管理存取權杖"
|
||||
accountInfo: "帳戶資訊"
|
||||
notesCount: "箋文數量"
|
||||
repliesCount: "回覆數量\n"
|
||||
renotesCount: "轉發數量"
|
||||
repliedCount: "回覆數量"
|
||||
renotedCount: "轉發次數"
|
||||
followingCount: "正在跟隨的用戶數量"
|
||||
followersCount: "跟隨者數量"
|
||||
sentReactionsCount: "情感發送次數"
|
||||
receivedReactionsCount: "情感收到次數"
|
||||
yes: "確定"
|
||||
no: "取消"
|
||||
driveFilesCount: "雲端硬碟檔案數量"
|
||||
driveUsage: "雲端硬碟使用量"
|
||||
noCrawleDescription: "請求網路搜尋引擎不要索引你的個人資料頁、箋文及頁面等。"
|
||||
lockedAccountInfo: "即使你通過了追隨者請求,除非你將筆記的公開範圍設定為 「追隨者」,否則任何人都能看見你的箋文。"
|
||||
lockedAccountInfo: "即使你通過了追隨者請求,除非你將箋文的可見性設定為 「追隨者」,否則任何人都能看見你的箋文。"
|
||||
loadRawImages: "以原始圖像質量顯示附件圖像的縮略圖"
|
||||
disableShowingAnimatedImages: "不播放動態圖像"
|
||||
notSet: "未設定"
|
||||
emailVerified: "已成功驗證您的電郵"
|
||||
noteFavoritesCount: "收藏箋文的數目"
|
||||
pageLikesCount: "頁面被喜歡次數"
|
||||
pageLikedCount: "頁面被喜歡次數"
|
||||
contact: "聯絡人"
|
||||
useSystemFont: "使用系統默認的字型"
|
||||
clips: "標籤"
|
||||
experimentalFeatures: "測試中的功能"
|
||||
developer: "開發者"
|
||||
makeExplorable: "讓自己的帳戶能夠在“探索”版面顯示"
|
||||
makeExplorableDescription: "如果關閉,帳戶將不會被顯示在\"探索\"版面中。"
|
||||
showGapBetweenNotesInTimeline: "分開顯示時間線上的箋文。"
|
||||
duplicate: "複製"
|
||||
left: "左"
|
||||
@ -625,6 +646,14 @@ wide: "寬"
|
||||
narrow: "窄"
|
||||
reloadToApplySetting: "設定將會在頁面重新載入之後生效。要現在就重載頁面嗎?"
|
||||
showTitlebar: "顯示標題列"
|
||||
clearCache: "清除快取資料"
|
||||
onlineUsersCount: "{n}人正在線上"
|
||||
nUsers: "{n}用戶"
|
||||
nNotes: "{n}箋文"
|
||||
backgroundColor: "背景"
|
||||
textColor: "文本"
|
||||
advanced: "進階"
|
||||
value: "數值 "
|
||||
_aboutMisskey:
|
||||
about: "Misskey是由syuilo於2014年開發的開源軟件。"
|
||||
contributors: "主要貢獻者"
|
||||
@ -635,53 +664,92 @@ _aboutMisskey:
|
||||
morePatrons: "感謝你們的支持、 幫助。 🥰"
|
||||
patrons: "贊助者"
|
||||
_nsfw:
|
||||
respect: "隱藏NSFW內容"
|
||||
ignore: "不隱藏NSFW內容"
|
||||
respect: "隱藏敏感內容"
|
||||
ignore: "不隱藏敏感內容"
|
||||
force: "隱藏所有內容"
|
||||
_mfm:
|
||||
cheatSheet: "MFM代碼小抄"
|
||||
intro: "MFM是Misskey專用的標記語言,可以在Misskey中的各個位置使用。 您可以這裏看到MFM可用語法列表。"
|
||||
mention: "提及"
|
||||
hashtag: "#tag"
|
||||
url: "URL"
|
||||
link: "鏈接"
|
||||
bold: "粗體"
|
||||
small: "縮小"
|
||||
center: "置中"
|
||||
inlineMath: "數學公式(內嵌)"
|
||||
inlineMathDescription: "顯示內嵌的KaTex數學公式。"
|
||||
blockMath: "數學公式(方塊)"
|
||||
quote: "引用"
|
||||
emoji: "自訂表情符號"
|
||||
search: "搜尋"
|
||||
flip: "翻轉"
|
||||
flipDescription: "將內容上下或左右翻轉。"
|
||||
jelly: "動畫(果凍)"
|
||||
jellyDescription: "顯示果凍一樣的動畫效果。"
|
||||
bounce: "動畫(反彈)"
|
||||
shake: "動畫(搖晃)"
|
||||
twitch: "動畫(顫抖)"
|
||||
spin: "動畫(旋轉)"
|
||||
spinDescription: "顯示旋轉的動畫效果。"
|
||||
x2: "大"
|
||||
x3: "較大"
|
||||
x3Description: "放大顯示內容。"
|
||||
x4: "最大"
|
||||
x4Description: "將顯示內容放至最大。"
|
||||
blur: "模糊"
|
||||
font: "字型"
|
||||
fontDescription: "可設置顯示內容所使用的字型"
|
||||
_reversi:
|
||||
reversi: "黑白棋"
|
||||
gameSettings: "對弈設定"
|
||||
chooseBoard: "選擇棋盤"
|
||||
blackOrWhite: "黑棋/白棋"
|
||||
blackIs: "{name}在玩黑棋"
|
||||
rules: "規則"
|
||||
botSettings: "機器人設定"
|
||||
thisGameIsStartedSoon: "遊戲即將開始"
|
||||
waitingForOther: "等待對手準備"
|
||||
waitingForMe: "等待您的準備"
|
||||
waitingBoth: "請準備"
|
||||
ready: "已就緒"
|
||||
cancelReady: "重新準備"
|
||||
opponentTurn: "對手回合"
|
||||
myTurn: "你的回合"
|
||||
turnOf: "{name}的回合"
|
||||
pastTurnOf: "{name}的回合"
|
||||
surrender: "認輸"
|
||||
surrendered: "對手認輸"
|
||||
drawn: "平局"
|
||||
won: "{name}獲勝"
|
||||
black: "黑"
|
||||
white: "白"
|
||||
total: "合計"
|
||||
turnCount: "{count}回合"
|
||||
myGames: "我的對弈"
|
||||
allGames: "所有對弈"
|
||||
ended: "已結束"
|
||||
playing: "正在對弈"
|
||||
_instanceTicker:
|
||||
none: "隱藏"
|
||||
remote: "向遠端使用者顯示"
|
||||
always: "總是顯示"
|
||||
_serverDisconnectedBehavior:
|
||||
reload: "自動重載"
|
||||
dialog: "以對話框警告"
|
||||
quiet: "適當地警告"
|
||||
dialog: "彈出式警告"
|
||||
quiet: "非侵入式警告"
|
||||
_channel:
|
||||
create: "建立頻道"
|
||||
edit: "編輯頻道"
|
||||
setBanner: "設定橫幅"
|
||||
removeBanner: "移除封面圖"
|
||||
featured: "流行"
|
||||
featured: "發燒內容"
|
||||
owned: "管理中"
|
||||
following: "關注中"
|
||||
usersCount: "有{n}人參與"
|
||||
notesCount: "有{n}個帖子"
|
||||
_sidebar:
|
||||
full: "全部"
|
||||
icon: "頭像"
|
||||
hide: "隱藏"
|
||||
_wordMute:
|
||||
@ -689,10 +757,22 @@ _wordMute:
|
||||
softDescription: "隱藏時間軸中指定條件的箋文。"
|
||||
mutedNotes: "已靜音的箋文"
|
||||
_theme:
|
||||
explore: "取得佈景主題"
|
||||
install: "安裝佈景主題"
|
||||
manage: "佈景主題管理員"
|
||||
code: "主題代碼"
|
||||
installed: "{name}已安裝"
|
||||
installedThemes: "已經安裝的主題"
|
||||
builtinThemes: "標準主題"
|
||||
alreadyInstalled: "此主題已經安裝"
|
||||
invalid: "主題格式錯誤"
|
||||
make: "製作主題"
|
||||
base: "基於"
|
||||
constant: "常數"
|
||||
defaultValue: "預設值"
|
||||
color: "顏色"
|
||||
func: "函数"
|
||||
funcKind: "功能類型"
|
||||
argument: "引數"
|
||||
alpha: "透明度"
|
||||
darken: "暗度"
|
||||
@ -701,21 +781,38 @@ _theme:
|
||||
bg: "背景"
|
||||
fg: "文本"
|
||||
shadow: "陰影"
|
||||
navIndicator: "側邊欄指示符"
|
||||
link: "鏈接"
|
||||
hashtag: "#tag"
|
||||
mention: "提及"
|
||||
mentionMe: "提及我"
|
||||
renote: "轉發貼文"
|
||||
renote: "轉發箋文"
|
||||
divider: "分割線"
|
||||
scrollbarHandle: "滾動條"
|
||||
scrollbarHandleHover: "滾動條 (漂浮)"
|
||||
dateLabelFg: "日期標籤文字"
|
||||
infoBg: "資訊背景"
|
||||
infoFg: "資訊內容"
|
||||
infoWarnBg: "警告背景"
|
||||
infoWarnFg: "警告字元"
|
||||
cwBg: "CW 按鈕背景"
|
||||
cwFg: "CW 按鈕文本"
|
||||
cwHoverBg: "CW 按鈕背景 (漂浮)"
|
||||
buttonBg: "按鈕背景"
|
||||
buttonHoverBg: "按鈕背景 (漂浮)"
|
||||
inputBorder: "輸入框邊框"
|
||||
listItemHoverBg: "列表物品背景 (漂浮)"
|
||||
driveFolderBg: "雲端硬碟文件夾背景"
|
||||
messageBg: "私信背景"
|
||||
accentDarken: "強調色(偏暗)"
|
||||
accentLighten: "強調色(明亮)"
|
||||
fgHighlighted: "高亮顯示文本"
|
||||
_sfx:
|
||||
note: "箋文"
|
||||
noteMy: "我的箋文"
|
||||
notification: "通知"
|
||||
chat: "傳送訊息"
|
||||
antenna: "天線接收"
|
||||
channel: "頻道通知"
|
||||
_ago:
|
||||
unknown: "未知"
|
||||
@ -736,28 +833,33 @@ _time:
|
||||
_tutorial:
|
||||
title: "Misskey使用方法"
|
||||
step1_1: "歡迎!"
|
||||
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人的「貼文」"
|
||||
step1_3: "由於你沒有發布任何筆記,也沒有追隨任何人,所以你的時間軸目前是空的。"
|
||||
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「箋文」"
|
||||
step1_3: "由於你沒有發佈任何箋文,也沒有追隨任何人,所以你的時間軸目前是空的。"
|
||||
step2_1: "在發文或追隨其他人之前先讓我們設定一下個人資料吧。"
|
||||
step2_2: "提供一些關於自己的資訊來讓其他人更有追隨你的意願。"
|
||||
step3_1: "個人資料都打理好了嗎?"
|
||||
step3_2: "下一步讓我們來試試看發個文,按一下畫面上的鉛筆圖示來開始"
|
||||
step3_3: "輸入完內容後,按視窗右上角的按鈕來發文"
|
||||
step3_4: "不知道該寫什麼內容嗎?試試看「開始使用Misskey了」如何。"
|
||||
step4_1: "筆記發出去了嗎?"
|
||||
step4_1: "箋文發出去了嗎?"
|
||||
step4_2: "如果你的箋文出現在時間軸上,就代表發文成功。"
|
||||
step5_1: "現在試試看追隨其他人來讓你的時間軸變得更生動吧。"
|
||||
step5_2: "你可以在{featured}上看到受歡迎的貼文,你也可以選擇從列表中追隨你喜歡的人,或者在{explore}上找到熱門使用者。"
|
||||
step5_2: "你會在{featured}上看到受歡迎的箋文,你也可以從列表中追隨你喜歡的人,或者在{explore}上找到熱門使用者。"
|
||||
step5_3: "想要追隨其他人,只要點擊他們的頭像並按「追隨」即可。"
|
||||
step5_4: "如果使用者的名字旁有鎖頭的圖示,代表他們需要手動核准你的追隨請求。"
|
||||
step6_1: "現在你可以在時間軸上看到其他用戶的貼文"
|
||||
step6_2: "你也可以在其他人的貼文上進行「反應」來表達簡單的回覆。"
|
||||
step6_3: "在他人的貼文按下「+」的圖示即可選擇想要的表情符號來進行「反應」。"
|
||||
step6_1: "現在你可以在時間軸上看到其他用戶的箋文。"
|
||||
step6_2: "你也可以對別人的箋文作出「情感」,作出簡單的回覆。"
|
||||
step7_1: "以上為Misskey的基本操作說明,教學在此告一段落。辛苦了。"
|
||||
step7_2: "歡迎到{help}來瞭解更多Misskey相關介紹。"
|
||||
step7_3: "那麼,祝您在Misskey玩的開心~ 🚀"
|
||||
_2fa:
|
||||
alreadyRegistered: "此設備已經被註冊過了"
|
||||
registerDevice: "註冊裝置"
|
||||
step1: "首先,在您的設備上安裝二步驗證程式,例如{a}或{b}。"
|
||||
step2: "然後,掃描熒幕上的QR code。"
|
||||
_permissions:
|
||||
"read:account": "查看帳戶信息"
|
||||
"write:account": "更改帳戶信息"
|
||||
"read:blocks": "已封鎖用戶名單"
|
||||
"write:blocks": "編輯已封鎖用戶名單"
|
||||
"read:drive": "存取雲端硬碟\n"
|
||||
@ -772,8 +874,8 @@ _permissions:
|
||||
"write:notes": "撰寫或刪除箋文"
|
||||
"read:notifications": "查看通知"
|
||||
"write:notifications": "編輯通知"
|
||||
"read:reactions": "查看反應"
|
||||
"write:reactions": "編輯反應"
|
||||
"read:reactions": "查看情感"
|
||||
"write:reactions": "編輯情感"
|
||||
"write:votes": "投票"
|
||||
"read:pages": "顯示頁面"
|
||||
"write:pages": "編輯頁面"
|
||||
@ -785,6 +887,11 @@ _permissions:
|
||||
"write:channels": "編輯頻道"
|
||||
_auth:
|
||||
shareAccess: "要授權「“{name}”」存取您的帳戶嗎?"
|
||||
shareAccessAsk: "您確定要授權這個應用程式使用您的帳戶嗎?"
|
||||
permissionAsk: "此應用程式需要以下權限"
|
||||
pleaseGoBack: "請返回至應用程式"
|
||||
callback: "回到應用程式"
|
||||
denied: "拒絕訪問"
|
||||
_antennaSources:
|
||||
all: "全部箋文"
|
||||
homeTimeline: "來自已追隨使用者的箋文"
|
||||
@ -804,14 +911,18 @@ _widgets:
|
||||
notifications: "通知"
|
||||
timeline: "時間軸"
|
||||
calendar: "行事曆"
|
||||
trends: "發燒貼文"
|
||||
trends: "發燒箋文"
|
||||
clock: "時鐘"
|
||||
rss: "RSS閱讀器"
|
||||
activity: "動態"
|
||||
photos: "照片"
|
||||
digitalClock: "電子時鐘"
|
||||
federation: "聯邦宇宙"
|
||||
postForm: "發佈窗口"
|
||||
slideshow: "幻燈片"
|
||||
button: "按鈕"
|
||||
onlineUsers: "在線上的用戶"
|
||||
jobQueue: "佇列"
|
||||
_cw:
|
||||
hide: "隱藏"
|
||||
show: "瀏覽更多"
|
||||
@ -819,6 +930,8 @@ _cw:
|
||||
files: "{count} 個檔案"
|
||||
_poll:
|
||||
noOnlyOneChoice: "至少需要兩個選項。"
|
||||
choiceN: "選擇{n}"
|
||||
noMore: "沒辦法再添加選項了"
|
||||
expiration: "期限"
|
||||
infinite: "無期限"
|
||||
at: "結束時間"
|
||||
@ -832,6 +945,7 @@ _poll:
|
||||
voted: "已投票"
|
||||
closed: "已結束"
|
||||
remainingDays: "{d}天{h}小時後結束"
|
||||
remainingHours: "{h}小時{m}分後結束"
|
||||
_visibility:
|
||||
public: "公開"
|
||||
home: "首頁"
|
||||
@ -842,7 +956,7 @@ _visibility:
|
||||
localOnlyDescription: "對遠端使用者隱藏"
|
||||
_postForm:
|
||||
replyPlaceholder: "回覆此箋文..."
|
||||
quotePlaceholder: "引用此貼文..."
|
||||
quotePlaceholder: "引用此箋文..."
|
||||
channelPlaceholder: "發佈到頻道"
|
||||
_placeholders:
|
||||
a: "今天過得如何?"
|
||||
@ -856,10 +970,12 @@ _profile:
|
||||
username: "使用者名稱"
|
||||
description: "關於我"
|
||||
youCanIncludeHashtags: "你也可以在「關於我」中加上 #tag"
|
||||
metadata: "進階資訊"
|
||||
metadataEdit: "編輯進階資訊"
|
||||
metadataLabel: "標籤"
|
||||
metadataContent: "内容"
|
||||
_exportOrImport:
|
||||
allNotes: "全部箋文"
|
||||
allNotes: "所有箋文"
|
||||
followingList: "追隨中"
|
||||
muteList: "靜音"
|
||||
blockingList: "封鎖"
|
||||
@ -929,12 +1045,23 @@ _rooms:
|
||||
book: "讀物"
|
||||
book2: "讀物2"
|
||||
piano: "鋼琴"
|
||||
server: "伺服器"
|
||||
moon: "月亮"
|
||||
corkboard: "木栓板"
|
||||
mousepad: "滑鼠墊"
|
||||
monitor: "監視器"
|
||||
keyboard: "鍵盤"
|
||||
carpet-stripe: "條紋地毯"
|
||||
mat: "地毯"
|
||||
wall-clock: "壁鐘"
|
||||
photoframe: "相框"
|
||||
cube: "立方體"
|
||||
tv: "電視"
|
||||
pinguin: "企鵝蠟像"
|
||||
poster-h: "海報(橫向)"
|
||||
poster-v: "海報(直向)"
|
||||
sofa: " 沙發"
|
||||
spiral: "螺旋式樓梯"
|
||||
bin: "垃圾箱"
|
||||
cup-noodle: "杯面"
|
||||
holo-display: "投影機"
|
||||
@ -990,6 +1117,7 @@ _pages:
|
||||
if: "如果"
|
||||
_if:
|
||||
variable: "變數"
|
||||
post: "發佈窗口"
|
||||
_post:
|
||||
text: "内容"
|
||||
canvasId: "畫布ID"
|
||||
@ -1049,6 +1177,7 @@ _pages:
|
||||
default: "預設值"
|
||||
script:
|
||||
categories:
|
||||
flow: "控制"
|
||||
logical: "邏輯運算"
|
||||
operation: "計算"
|
||||
comparison: "對比"
|
||||
@ -1064,6 +1193,7 @@ _pages:
|
||||
textList: "文本列表"
|
||||
_strLen:
|
||||
arg1: "文本"
|
||||
strPick: "提取字元"
|
||||
_strPick:
|
||||
arg1: "文本"
|
||||
arg2: "字元位置"
|
||||
@ -1073,6 +1203,7 @@ _pages:
|
||||
arg1: "本文"
|
||||
_join:
|
||||
arg1: "清單"
|
||||
arg2: "分隔字元"
|
||||
add: "加"
|
||||
_add:
|
||||
arg1: "A"
|
||||
@ -1089,6 +1220,7 @@ _pages:
|
||||
_divide:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
mod: "餘數"
|
||||
_mod:
|
||||
arg1: "A"
|
||||
arg2: "B"
|
||||
@ -1183,7 +1315,6 @@ _pages:
|
||||
aiScriptVar: "AiScript的變數"
|
||||
fn: "函数"
|
||||
_fn:
|
||||
slots: "欄位"
|
||||
arg1: "輸出"
|
||||
_for:
|
||||
arg1: "重複次數"
|
||||
@ -1200,9 +1331,14 @@ _relayStatus:
|
||||
accepted: "已通過核准"
|
||||
rejected: "已拒絕"
|
||||
_notification:
|
||||
youRenoted: "{name} 轉發了你的貼文"
|
||||
youGotMention: "{name}提及到您"
|
||||
youGotReply: "{name}回覆了您"
|
||||
youGotQuote: "{name}引用了您"
|
||||
youRenoted: "{name} 轉發了你的箋文"
|
||||
youGotPoll: "{name}已投票"
|
||||
youGotMessagingMessageFromUser: "{name}發送給您的訊息"
|
||||
youWereFollowed: "您有新的追隨者"
|
||||
youReceivedFollowRequest: "您有新的追隨請求"
|
||||
yourFollowRequestAccepted: "您的追隨請求已通過"
|
||||
youWereInvitedToGroup: "您有新的群組邀請"
|
||||
_types:
|
||||
@ -1210,15 +1346,19 @@ _notification:
|
||||
follow: "追隨中"
|
||||
mention: "提及"
|
||||
reply: "回覆"
|
||||
renote: "轉發貼文"
|
||||
renote: "轉發箋文"
|
||||
quote: "引用"
|
||||
reaction: "反應"
|
||||
reaction: "情感"
|
||||
pollVote: "統計已投票數"
|
||||
receiveFollowRequest: "已收到追隨請求"
|
||||
followRequestAccepted: "追隨請求已接受"
|
||||
groupInvited: "加入社群邀請"
|
||||
app: "應用程式通知"
|
||||
_deck:
|
||||
alwaysShowMainColumn: "總是顯示主欄"
|
||||
columnAlign: "對齊欄位"
|
||||
columnMargin: "列之間的邊距"
|
||||
columnHeaderHeight: "欄位標題高度"
|
||||
addColumn: "新增欄位"
|
||||
swapLeft: "向左移動"
|
||||
swapRight: "向右移動"
|
||||
@ -1226,7 +1366,9 @@ _deck:
|
||||
swapDown: "往下移動"
|
||||
stackLeft: "向左折疊"
|
||||
popRight: "向右彈出"
|
||||
profile: "個人檔案"
|
||||
_columns:
|
||||
main: "主列"
|
||||
widgets: "小工具"
|
||||
notifications: "通知"
|
||||
tl: "時間軸"
|
||||
|
22
migration/1610277136869-registry.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class registry1610277136869 implements MigrationInterface {
|
||||
name = 'registry1610277136869'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "registry_item" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "key" character varying(1024) NOT NULL, "scope" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[], "domain" character varying(512), CONSTRAINT "PK_64b3f7e6008b4d89b826cd3af95" PRIMARY KEY ("id")); COMMENT ON COLUMN "registry_item"."createdAt" IS 'The created date of the RegistryItem.'; COMMENT ON COLUMN "registry_item"."updatedAt" IS 'The updated date of the RegistryItem.'; COMMENT ON COLUMN "registry_item"."userId" IS 'The owner ID.'; COMMENT ON COLUMN "registry_item"."key" IS 'The key of the RegistryItem.'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_fb9d21ba0abb83223263df6bcb" ON "registry_item" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_22baca135bb8a3ea1a83d13df3" ON "registry_item" ("scope") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad" ON "registry_item" ("domain") `);
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ADD CONSTRAINT "FK_fb9d21ba0abb83223263df6bcb3" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" DROP CONSTRAINT "FK_fb9d21ba0abb83223263df6bcb3"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_22baca135bb8a3ea1a83d13df3"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_fb9d21ba0abb83223263df6bcb"`);
|
||||
await queryRunner.query(`DROP TABLE "registry_item"`);
|
||||
}
|
||||
|
||||
}
|
16
migration/1610277585759-registry2.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class registry21610277585759 implements MigrationInterface {
|
||||
name = 'registry21610277585759'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ADD "value" jsonb NOT NULL DEFAULT '{}'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "registry_item"."value" IS 'The value of the RegistryItem.'`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`COMMENT ON COLUMN "registry_item"."value" IS 'The value of the RegistryItem.'`);
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" DROP COLUMN "value"`);
|
||||
}
|
||||
|
||||
}
|
14
migration/1610283021566-registry3.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class registry31610283021566 implements MigrationInterface {
|
||||
name = 'registry31610283021566'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "value" DROP NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "value" SET NOT NULL`);
|
||||
}
|
||||
|
||||
}
|
42
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <syuilotan@yahoo.co.jp>",
|
||||
"version": "12.65.6",
|
||||
"version": "12.67.1",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -45,6 +45,8 @@
|
||||
"@koa/cors": "3.1.0",
|
||||
"@koa/multer": "3.0.0",
|
||||
"@koa/router": "9.0.1",
|
||||
"@sentry/browser": "5.29.2",
|
||||
"@sentry/tracing": "5.29.2",
|
||||
"@sinonjs/fake-timers": "6.0.1",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
@ -96,6 +98,7 @@
|
||||
"@types/sharp": "0.26.1",
|
||||
"@types/sinonjs__fake-timers": "6.0.1",
|
||||
"@types/speakeasy": "2.0.5",
|
||||
"@types/throttle-debounce": "2.1.0",
|
||||
"@types/tinycolor2": "1.4.2",
|
||||
"@types/tmp": "0.2.0",
|
||||
"@types/uuid": "8.3.0",
|
||||
@ -107,7 +110,7 @@
|
||||
"@typescript-eslint/parser": "4.10.0",
|
||||
"@vue/compiler-sfc": "3.0.5",
|
||||
"abort-controller": "3.0.0",
|
||||
"apexcharts": "3.22.3",
|
||||
"apexcharts": "3.23.1",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "4.0.2",
|
||||
"autwh": "0.1.0",
|
||||
@ -131,12 +134,12 @@
|
||||
"diskusage": "1.1.3",
|
||||
"double-ended-queue": "2.1.0-0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eslint": "7.16.0",
|
||||
"eslint-plugin-vue": "7.3.0",
|
||||
"eslint": "7.17.0",
|
||||
"eslint-plugin-vue": "7.4.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"feed": "4.2.1",
|
||||
"fibers": "5.0.0",
|
||||
"file-type": "16.0.1",
|
||||
"file-type": "16.1.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"glob": "7.1.6",
|
||||
"got": "11.8.1",
|
||||
@ -145,7 +148,7 @@
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-replace": "1.0.0",
|
||||
"gulp-sourcemaps": "2.6.5",
|
||||
"gulp-terser": "2.0.0",
|
||||
"gulp-terser": "2.0.1",
|
||||
"gulp-tslint": "8.1.4",
|
||||
"gulp-typescript": "6.0.0-alpha.1",
|
||||
"hard-source-webpack-plugin": "0.13.1",
|
||||
@ -164,7 +167,7 @@
|
||||
"jsonld": "3.2.0",
|
||||
"jsrsasign": "8.0.20",
|
||||
"katex": "0.12.0",
|
||||
"koa": "2.13.0",
|
||||
"koa": "2.13.1",
|
||||
"koa-bodyparser": "4.3.0",
|
||||
"koa-favicon": "2.1.0",
|
||||
"koa-json-body": "5.3.0",
|
||||
@ -192,9 +195,9 @@
|
||||
"parsimmon": "1.16.0",
|
||||
"pg": "8.5.1",
|
||||
"portscanner": "2.2.0",
|
||||
"postcss": "8.2.1",
|
||||
"postcss": "8.2.4",
|
||||
"postcss-loader": "4.1.0",
|
||||
"prismjs": "1.22.0",
|
||||
"prismjs": "1.23.0",
|
||||
"probe-image-size": "6.0.0",
|
||||
"promise-limit": "2.7.0",
|
||||
"promise-sequential": "1.1.1",
|
||||
@ -217,26 +220,27 @@
|
||||
"rimraf": "3.0.2",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.29.0",
|
||||
"sass-loader": "10.1.0",
|
||||
"sass": "1.32.4",
|
||||
"sass-loader": "10.1.1",
|
||||
"seedrandom": "3.0.5",
|
||||
"sharp": "0.26.2",
|
||||
"sharp": "0.27.0",
|
||||
"speakeasy": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"style-loader": "2.0.0",
|
||||
"summaly": "2.4.0",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "4.31.1",
|
||||
"systeminformation": "4.34.6",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.117.1",
|
||||
"throttle-debounce": "3.0.1",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tmp": "0.2.1",
|
||||
"ts-loader": "8.0.11",
|
||||
"ts-node": "9.1.0",
|
||||
"ts-loader": "8.0.14",
|
||||
"ts-node": "9.1.1",
|
||||
"tslint": "6.1.3",
|
||||
"tslint-sonarts": "1.9.0",
|
||||
"typeorm": "0.2.29",
|
||||
"typeorm": "0.2.30",
|
||||
"typescript": "4.1.3",
|
||||
"ulid": "2.3.0",
|
||||
"url-loader": "4.1.1",
|
||||
@ -252,10 +256,10 @@
|
||||
"vue-style-loader": "4.1.2",
|
||||
"vuedraggable": "4.0.1",
|
||||
"web-push": "3.4.4",
|
||||
"webpack": "5.10.1",
|
||||
"webpack-cli": "4.3.0",
|
||||
"webpack": "5.13.0",
|
||||
"webpack-cli": "4.3.1",
|
||||
"websocket": "1.0.33",
|
||||
"ws": "7.4.1",
|
||||
"ws": "7.4.2",
|
||||
"xev": "2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -7,7 +7,6 @@ import { waiting } from '@/os';
|
||||
type Account = {
|
||||
id: string;
|
||||
token: string;
|
||||
clientData: Record<string, any>;
|
||||
};
|
||||
|
||||
const data = localStorage.getItem('account');
|
||||
|
BIN
src/client/assets/about-icon.png
Normal file
After Width: | Height: | Size: 20 KiB |
@ -36,7 +36,7 @@ import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faDownload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
|
||||
import bytes from '../filters/bytes';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -17,7 +17,7 @@
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="onDragleave"
|
||||
@drop.prevent.stop="onDrop"
|
||||
@contextmenu="onContextmenu"
|
||||
@contextmenu.stop="onContextmenu"
|
||||
>
|
||||
<div class="contents" ref="contents">
|
||||
<div class="folders" ref="foldersContainer" v-show="folders.length > 0">
|
||||
|
@ -48,6 +48,11 @@ export default defineComponent({
|
||||
|
||||
const ast = (this.plain ? parsePlain : parse)(this.text);
|
||||
|
||||
const validTime = (t: string | null | undefined) => {
|
||||
if (t == null) return null;
|
||||
return t.match(/^[0-9.]+s$/) ? t : null;
|
||||
};
|
||||
|
||||
const genEl = (ast: MfmForest) => concat(ast.map((token): VNode[] => {
|
||||
switch (token.node.type) {
|
||||
case 'text': {
|
||||
@ -86,17 +91,17 @@ export default defineComponent({
|
||||
break;
|
||||
}
|
||||
case 'jelly': {
|
||||
const speed = token.node.props.args.speed || '1s';
|
||||
const speed = validTime(token.node.props.args.speed) || '1s';
|
||||
style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
|
||||
break;
|
||||
}
|
||||
case 'twitch': {
|
||||
const speed = token.node.props.args.speed || '0.5s';
|
||||
const speed = validTime(token.node.props.args.speed) || '0.5s';
|
||||
style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : '';
|
||||
break;
|
||||
}
|
||||
case 'shake': {
|
||||
const speed = token.node.props.args.speed || '0.5s';
|
||||
const speed = validTime(token.node.props.args.speed) || '0.5s';
|
||||
style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : '';
|
||||
break;
|
||||
}
|
||||
@ -109,7 +114,7 @@ export default defineComponent({
|
||||
token.node.props.args.x ? 'mfm-spinX' :
|
||||
token.node.props.args.y ? 'mfm-spinY' :
|
||||
'mfm-spin';
|
||||
const speed = token.node.props.args.speed || '1.5s';
|
||||
const speed = validTime(token.node.props.args.speed) || '1.5s';
|
||||
style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
|
||||
break;
|
||||
}
|
||||
@ -141,6 +146,18 @@ export default defineComponent({
|
||||
style = `font-size: 600%;`;
|
||||
break;
|
||||
}
|
||||
case 'font': {
|
||||
const family =
|
||||
token.node.props.args.serif ? 'serif' :
|
||||
token.node.props.args.monospace ? 'monospace' :
|
||||
token.node.props.args.cursive ? 'cursive' :
|
||||
token.node.props.args.fantasy ? 'fantasy' :
|
||||
token.node.props.args.emoji ? 'emoji' :
|
||||
token.node.props.args.math ? 'math' :
|
||||
null;
|
||||
if (family) style = `font-family: ${family};`;
|
||||
break;
|
||||
}
|
||||
case 'blur': {
|
||||
return h('span', {
|
||||
class: '_mfm_blur_',
|
||||
|
1190
src/client/components/note-detailed.vue
Normal file
@ -8,7 +8,6 @@
|
||||
v-hotkey="keymap"
|
||||
v-size="{ max: [500, 450, 350, 300] }"
|
||||
>
|
||||
<XSub v-for="note in conversation" class="reply-to-more" :key="note.id" :note="note"/>
|
||||
<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/>
|
||||
<div class="info" v-if="pinned"><Fa :icon="faThumbtack"/> {{ $ts.pinnedNote }}</div>
|
||||
<div class="info" v-if="appearNote._prId_"><Fa :icon="faBullhorn"/> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <Fa :icon="faTimes"/></button></div>
|
||||
@ -57,7 +56,7 @@
|
||||
<XMediaList :media-list="appearNote.files"/>
|
||||
</div>
|
||||
<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="detail" class="url-preview"/>
|
||||
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="false" class="url-preview"/>
|
||||
<div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div>
|
||||
<button v-if="collapsed" class="fade _button" @click="collapsed = false">
|
||||
<span>{{ $ts.showMore }}</span>
|
||||
@ -90,7 +89,6 @@
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
<XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
|
||||
</div>
|
||||
<div v-else class="_panel muted" @click="muted = false">
|
||||
<I18n :src="$ts.userSaysSomething" tag="small">
|
||||
@ -157,11 +155,6 @@ export default defineComponent({
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
detail: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
pinned: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@ -174,7 +167,6 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
connection: null,
|
||||
conversation: [],
|
||||
replies: [],
|
||||
showContent: false,
|
||||
collapsed: false,
|
||||
@ -277,7 +269,7 @@ export default defineComponent({
|
||||
this.connection = os.stream;
|
||||
}
|
||||
|
||||
this.collapsed = !this.detail && this.appearNote.cw == null && this.appearNote.text && (
|
||||
this.collapsed = this.appearNote.cw == null && this.appearNote.text && (
|
||||
(this.appearNote.text.split('\n').length > 9) ||
|
||||
(this.appearNote.text.length > 500)
|
||||
);
|
||||
@ -291,23 +283,6 @@ export default defineComponent({
|
||||
}
|
||||
this.$emit('update:note', Object.freeze(result));
|
||||
}
|
||||
|
||||
if (this.detail) {
|
||||
os.api('notes/children', {
|
||||
noteId: this.appearNote.id,
|
||||
limit: 30
|
||||
}).then(replies => {
|
||||
this.replies = replies;
|
||||
});
|
||||
|
||||
if (this.appearNote.replyId) {
|
||||
os.api('notes/conversation', {
|
||||
noteId: this.appearNote.replyId
|
||||
}).then(conversation => {
|
||||
this.conversation = conversation.reverse();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
@ -951,10 +926,6 @@ export default defineComponent({
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
> .reply-to-more {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> .renote {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
|
||||
<XNote :note="note" @update:note="updated(note, $event)" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
|
||||
<XNote :note="note" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/>
|
||||
</XList>
|
||||
|
||||
<div v-show="more && !reversed" style="margin-top: var(--margin);">
|
||||
@ -55,12 +55,6 @@ export default defineComponent({
|
||||
required: true
|
||||
},
|
||||
|
||||
detail: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
|
||||
prop: {
|
||||
type: String,
|
||||
required: false
|
||||
|
@ -1,17 +1,20 @@
|
||||
<template>
|
||||
<div class="voxdxuby">
|
||||
<XNote v-if="note" v-model:note="note" :key="note.id" :detail="value.detailed"/>
|
||||
<XNote v-if="note && !value.detailed" v-model:note="note" :key="note.id + ':normal'"/>
|
||||
<XNoteDetailed v-if="note && value.detailed" v-model:note="note" :key="note.id + ':detail'"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import XNote from '@/components/note.vue';
|
||||
import XNoteDetailed from '@/components/note-detailed.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNote
|
||||
XNote,
|
||||
XNoteDetailed,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
|
@ -9,7 +9,7 @@
|
||||
<header>
|
||||
<button v-if="!fixed" class="cancel _button" @click="cancel"><Fa :icon="faTimes"/></button>
|
||||
<div>
|
||||
<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span>
|
||||
<span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span>
|
||||
<span class="local-only" v-if="localOnly"><Fa :icon="faBiohazard"/></span>
|
||||
<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$ts.visibility" :disabled="channel != null">
|
||||
<span v-if="visibility === 'public'"><Fa :icon="faGlobe"/></span>
|
||||
@ -35,7 +35,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
|
||||
<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste"></textarea>
|
||||
<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd" />
|
||||
<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
|
||||
<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
|
||||
<footer>
|
||||
@ -142,6 +142,7 @@ export default defineComponent({
|
||||
draghover: false,
|
||||
quoteId: null,
|
||||
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
|
||||
imeText: '',
|
||||
postFormActions,
|
||||
faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug
|
||||
};
|
||||
@ -190,10 +191,14 @@ export default defineComponent({
|
||||
: this.$ts.note;
|
||||
},
|
||||
|
||||
textLength(): number {
|
||||
return length((this.text + this.imeText).trim());
|
||||
},
|
||||
|
||||
canPost(): boolean {
|
||||
return !this.posting &&
|
||||
(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
|
||||
(length(this.text.trim()) <= this.max) &&
|
||||
(1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) &&
|
||||
(this.textLength <= this.max) &&
|
||||
(!this.poll || this.poll.choices.length >= 2);
|
||||
},
|
||||
|
||||
@ -262,7 +267,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
// keep cw when reply
|
||||
if (this.$store.keepCw && this.reply && this.reply.cw) {
|
||||
if (this.$store.state.keepCw && this.reply && this.reply.cw) {
|
||||
this.useCw = true;
|
||||
this.cw = this.reply.cw;
|
||||
}
|
||||
@ -339,10 +344,6 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
trimmedLength(text: string) {
|
||||
return length(text.trim());
|
||||
},
|
||||
|
||||
addTag(tag: string) {
|
||||
insertTextAtCursor(this.$refs.text, ` #${tag} `);
|
||||
},
|
||||
@ -429,11 +430,19 @@ export default defineComponent({
|
||||
this.quoteId = null;
|
||||
},
|
||||
|
||||
onKeydown(e) {
|
||||
onKeydown(e: KeyboardEvent) {
|
||||
if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
|
||||
if (e.which === 27) this.$emit('esc');
|
||||
},
|
||||
|
||||
onCompositionUpdate(e: CompositionEvent) {
|
||||
this.imeText = e.data;
|
||||
},
|
||||
|
||||
onCompositionEnd(e: CompositionEvent) {
|
||||
this.imeText = '';
|
||||
},
|
||||
|
||||
async onPaste(e: ClipboardEvent) {
|
||||
for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
|
||||
if (item.kind == 'file') {
|
||||
|
@ -15,7 +15,7 @@
|
||||
<MkButton inline>This is</MkButton>
|
||||
<MkButton inline primary>the button</MkButton>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<div class="_content" style="pointer-events: none;">
|
||||
<Mfm :text="mfm"/>
|
||||
</div>
|
||||
<div class="_content">
|
||||
@ -49,7 +49,7 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
text: '',
|
||||
flag: false,
|
||||
flag: true,
|
||||
radio: 'misskey',
|
||||
mfm: `Hello world! This is an @example mention. BTW you are @${this.$i.username}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
|
||||
}
|
||||
|
@ -59,7 +59,6 @@ export default defineComponent({
|
||||
return {
|
||||
host: host,
|
||||
showing: false,
|
||||
searching: false,
|
||||
accounts: [],
|
||||
connection: null,
|
||||
menuDef: sidebarDef,
|
||||
@ -125,19 +124,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
search() {
|
||||
if (this.searching) return;
|
||||
|
||||
os.dialog({
|
||||
title: this.$ts.search,
|
||||
input: true
|
||||
}).then(async ({ canceled, result: query }) => {
|
||||
if (canceled || query == null || query === '') return;
|
||||
|
||||
this.searching = true;
|
||||
search(this, query).finally(() => {
|
||||
this.searching = false;
|
||||
});
|
||||
});
|
||||
search();
|
||||
},
|
||||
|
||||
async openAccountMenu(ev) {
|
||||
|
@ -134,6 +134,8 @@ export default defineComponent({
|
||||
|
||||
> header {
|
||||
position: relative;
|
||||
color: var(--panelHeaderFg);
|
||||
background: var(--panelHeaderBg);
|
||||
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
|
||||
z-index: 2;
|
||||
line-height: 1.4em;
|
||||
@ -179,6 +181,7 @@ export default defineComponent({
|
||||
> header {
|
||||
> .title {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ export default defineComponent({
|
||||
font-size: 90%;
|
||||
background: var(--infoBg);
|
||||
color: var(--infoFg);
|
||||
border-radius: 5px;
|
||||
border-radius: var(--radius);
|
||||
|
||||
&.warn {
|
||||
background: var(--infoWarnBg);
|
||||
|
@ -243,7 +243,7 @@ export default defineComponent({
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 0 0 1px var(--divider);
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
|
@ -36,13 +36,15 @@ if (localStorage.getItem('vuex') != null) {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
import { createApp, watch } from 'vue';
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
|
||||
import widgets from '@/widgets';
|
||||
import directives from '@/directives';
|
||||
import components from '@/components';
|
||||
import { version, ui, lang } from '@/config';
|
||||
import { version, ui, lang, host } from '@/config';
|
||||
import { router } from '@/router';
|
||||
import { applyTheme } from '@/scripts/theme';
|
||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||
@ -54,6 +56,7 @@ import { defaultStore, ColdDeviceStorage } from '@/store';
|
||||
import { fetchInstance, instance } from '@/instance';
|
||||
import { makeHotkey } from './scripts/hotkey';
|
||||
import { search } from './scripts/search';
|
||||
import { getThemes } from './theme-store';
|
||||
|
||||
console.info(`Misskey v${version}`);
|
||||
|
||||
@ -88,6 +91,18 @@ if (_DEV_) {
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultStore.state.reportError && !_DEV_) {
|
||||
Sentry.init({
|
||||
dsn: 'https://fd273254a07a4b61857607a9ea05d629@o501808.ingest.sentry.io/5583438',
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
||||
Sentry.setTag('misskey_version', version);
|
||||
Sentry.setTag('ui', ui);
|
||||
Sentry.setTag('lang', lang);
|
||||
Sentry.setTag('host', host);
|
||||
}
|
||||
|
||||
// タッチデバイスでCSSの:hoverを機能させる
|
||||
document.addEventListener('touchend', () => {}, { passive: true });
|
||||
|
||||
@ -197,7 +212,7 @@ app.mount('body');
|
||||
|
||||
watch(defaultStore.reactiveState.darkMode, (darkMode) => {
|
||||
import('@/scripts/theme').then(({ builtinThemes }) => {
|
||||
const themes = builtinThemes.concat(ColdDeviceStorage.get('themes'));
|
||||
const themes = builtinThemes.concat(getThemes());
|
||||
applyTheme(themes.find(x => x.id === (darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'))));
|
||||
});
|
||||
}, { immediate: localStorage.theme == null });
|
||||
@ -333,14 +348,6 @@ if ($i) {
|
||||
updateAccount({ hasUnreadAnnouncement: false });
|
||||
});
|
||||
|
||||
main.on('clientSettingUpdated', x => {
|
||||
updateAccount({
|
||||
clientData: {
|
||||
[x.key]: x.value
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on('myTokenRegenerated', () => {
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import Stream from '@/scripts/stream';
|
||||
import { apiUrl, debug } from '@/config';
|
||||
import MkPostFormDialog from '@/components/post-form-dialog.vue';
|
||||
import MkWaitingDialog from '@/components/waiting-dialog.vue';
|
||||
import { resolve } from '@/router';
|
||||
import { $i } from './account';
|
||||
import { $i } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
export const isMobile = /mobile|iphone|ipad|android/.test(ua);
|
||||
@ -54,19 +56,32 @@ export function api(endpoint: string, data: Record<string, any> = {}, token?: st
|
||||
if (res.status === 200) {
|
||||
resolve(body);
|
||||
if (debug) {
|
||||
log.res = markRaw(body);
|
||||
log.state = 'success';
|
||||
log!.res = markRaw(body);
|
||||
log!.state = 'success';
|
||||
}
|
||||
} else if (res.status === 204) {
|
||||
resolve();
|
||||
if (debug) {
|
||||
log.state = 'success';
|
||||
log!.state = 'success';
|
||||
}
|
||||
} else {
|
||||
reject(body.error);
|
||||
if (debug) {
|
||||
log.res = markRaw(body.error);
|
||||
log.state = 'failed';
|
||||
log!.res = markRaw(body.error);
|
||||
log!.state = 'failed';
|
||||
}
|
||||
|
||||
if (defaultStore.state.reportError && !_DEV_) {
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setTag('api_endpoint', endpoint);
|
||||
scope.setContext('api params', data);
|
||||
scope.setContext('api error info', body.info);
|
||||
scope.setTag('api_error_id', body.id);
|
||||
scope.setTag('api_error_code', body.code);
|
||||
scope.setTag('api_error_kind', body.kind);
|
||||
scope.setLevel(Sentry.Severity.Error);
|
||||
Sentry.captureMessage('API error');
|
||||
});
|
||||
}
|
||||
}
|
||||
}).catch(reject);
|
||||
@ -97,8 +112,8 @@ export function apiWithDialog(
|
||||
|
||||
export function promiseDialog<T extends Promise<any>>(
|
||||
promise: T,
|
||||
onSuccess?: (res: any) => void,
|
||||
onFailure?: (e: Error) => void,
|
||||
onSuccess?: ((res: any) => void) | null,
|
||||
onFailure?: ((e: Error) => void) | null,
|
||||
text?: string,
|
||||
): T {
|
||||
const showing = ref(true);
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div id="debug"></div>
|
||||
<section class="_formItem about">
|
||||
<div class="_formPanel panel" :class="{ playing: easterEggEngine != null }" ref="about">
|
||||
<img src="/assets/icons/512.png" alt="" class="icon" ref="icon" @load="iconLoaded" draggable="false"/>
|
||||
<img src="/assets/about-icon.png" alt="" class="icon" ref="icon" @load="iconLoaded" draggable="false"/>
|
||||
<div class="misskey">Misskey</div>
|
||||
<div class="version">v{{ version }}</div>
|
||||
<span class="emoji" v-for="emoji in easterEggEmojis" :key="emoji.id" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
|
||||
|
353
src/client/pages/advanced-theme-editor.vue
Normal file
@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="t9makv94">
|
||||
<section class="_section">
|
||||
<div class="_content">
|
||||
<details>
|
||||
<summary>{{ $ts.import }}</summary>
|
||||
<MkTextarea v-model:value="themeToImport">
|
||||
{{ $ts._theme.importInfo }}
|
||||
</MkTextarea>
|
||||
<MkButton :disabled="!themeToImport.trim()" @click="importTheme">{{ $ts.import }}</MkButton>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_section">
|
||||
<div class="_content _card _vMargin">
|
||||
<div class="_content">
|
||||
<MkInput v-model:value="name" required><span>{{ $ts.name }}</span></MkInput>
|
||||
<MkInput v-model:value="author" required><span>{{ $ts.author }}</span></MkInput>
|
||||
<MkTextarea v-model:value="description"><span>{{ $ts.description }}</span></MkTextarea>
|
||||
<div class="_inputs">
|
||||
<div v-text="$ts._theme.base" />
|
||||
<MkRadio v-model="baseTheme" value="light">{{ $ts.light }}</MkRadio>
|
||||
<MkRadio v-model="baseTheme" value="dark">{{ $ts.dark }}</MkRadio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_content _card _vMargin">
|
||||
<div class="list-view _content">
|
||||
<div class="item" v-for="([ k, v ], i) in theme" :key="k">
|
||||
<div class="_inputs">
|
||||
<div>
|
||||
{{ k.startsWith('$') ? `${k} (${$ts._theme.constant})` : $t('_theme.keys.' + k) }}
|
||||
<button v-if="k.startsWith('$')" class="_button _link" @click="del(i)" v-text="$ts.delete" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="type" @click="chooseType($event, i)">
|
||||
{{ getTypeOf(v) }} <Fa :icon="faChevronDown"/>
|
||||
</div>
|
||||
<!-- default -->
|
||||
<div v-if="v === null" v-text="baseProps[k]" class="default-value" />
|
||||
<!-- color -->
|
||||
<div v-else-if="typeof v === 'string'" class="color">
|
||||
<input type="color" :value="v" @input="colorChanged($event.target.value, i)"/>
|
||||
<MkInput class="select" :value="v" @update:value="colorChanged($event, i)"/>
|
||||
</div>
|
||||
<!-- ref const -->
|
||||
<MkInput v-else-if="v.type === 'refConst'" v-model:value="v.key">
|
||||
<template #prefix>$</template>
|
||||
<span>{{ $ts.name }}</span>
|
||||
</MkInput>
|
||||
<!-- ref props -->
|
||||
<MkSelect class="select" v-else-if="v.type === 'refProp'" v-model:value="v.key">
|
||||
<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
|
||||
</MkSelect>
|
||||
<!-- func -->
|
||||
<template v-else-if="v.type === 'func'">
|
||||
<MkSelect class="select" v-model:value="v.name">
|
||||
<template #label>{{ $ts._theme.funcKind }}</template>
|
||||
<option v-for="n in ['alpha', 'darken', 'lighten']" :value="n" :key="n">{{ $t('_theme.' + n) }}</option>
|
||||
</MkSelect>
|
||||
<MkInput type="number" v-model:value="v.arg"><span>{{ $ts._theme.argument }}</span></MkInput>
|
||||
<MkSelect class="select" v-model:value="v.value">
|
||||
<template #label>{{ $ts._theme.basedProp }}</template>
|
||||
<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
|
||||
</MkSelect>
|
||||
</template>
|
||||
<!-- CSS -->
|
||||
<MkInput v-else-if="v.type === 'css'" v-model:value="v.value">
|
||||
<span>CSS</span>
|
||||
</MkInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkButton primary @click="addConst">{{ $ts._theme.addConstant }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_section">
|
||||
<details class="_content">
|
||||
<summary>{{ $ts.sample }}</summary>
|
||||
<MkSample/>
|
||||
</details>
|
||||
</section>
|
||||
<section class="_section">
|
||||
<div class="_content">
|
||||
<MkButton inline @click="preview">{{ $ts.preview }}</MkButton>
|
||||
<MkButton inline primary :disabled="!name || !author" @click="save">{{ $ts.save }}</MkButton>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faPalette, faChevronDown, faKeyboard } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as JSON5 from 'json5';
|
||||
import { toUnicode } from 'punycode';
|
||||
|
||||
import MkRadio from '@/components/ui/radio.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/ui/input.vue';
|
||||
import MkTextarea from '@/components/ui/textarea.vue';
|
||||
import MkSelect from '@/components/ui/select.vue';
|
||||
import MkSample from '@/components/sample.vue';
|
||||
|
||||
import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@/scripts/theme-editor';
|
||||
import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@/scripts/theme';
|
||||
import { host } from '@/config';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkRadio,
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkSelect,
|
||||
MkSample,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
INFO: {
|
||||
title: this.$ts.themeEditor,
|
||||
icon: faPalette,
|
||||
},
|
||||
theme: [] as ThemeViewModel,
|
||||
name: '',
|
||||
description: '',
|
||||
baseTheme: 'light' as 'dark' | 'light',
|
||||
author: `@${this.$i.username}@${toUnicode(host)}`,
|
||||
themeToImport: '',
|
||||
changed: false,
|
||||
lightTheme, darkTheme, themeProps,
|
||||
faPalette, faChevronDown, faKeyboard,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
baseProps() {
|
||||
return this.baseTheme === 'light' ? this.lightTheme.props : this.darkTheme.props;
|
||||
},
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('beforeunload', this.beforeunload);
|
||||
},
|
||||
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
if (this.changed && !(await this.confirm())) {
|
||||
next(false);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
window.addEventListener('beforeunload', this.beforeunload);
|
||||
const changed = () => this.changed = true;
|
||||
this.$watch('name', changed);
|
||||
this.$watch('description', changed);
|
||||
this.$watch('baseTheme', changed);
|
||||
this.$watch('author', changed);
|
||||
this.$watch('theme', changed);
|
||||
},
|
||||
|
||||
methods: {
|
||||
beforeunload(e: BeforeUnloadEvent) {
|
||||
if (this.changed) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
},
|
||||
|
||||
async confirm(): Promise<boolean> {
|
||||
const { canceled } = await os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$ts.leaveConfirm,
|
||||
showCancelButton: true
|
||||
});
|
||||
return !canceled;
|
||||
},
|
||||
|
||||
init() {
|
||||
const t: ThemeViewModel = [];
|
||||
for (const key of themeProps) {
|
||||
t.push([ key, null ]);
|
||||
}
|
||||
this.theme = t;
|
||||
},
|
||||
|
||||
async del(i: number) {
|
||||
const { canceled } = await os.dialog({
|
||||
type: 'warning',
|
||||
showCancelButton: true,
|
||||
text: this.$t('_theme.deleteConstantConfirm', { const: this.theme[i][0] }),
|
||||
});
|
||||
if (canceled) return;
|
||||
Vue.delete(this.theme, i);
|
||||
},
|
||||
|
||||
async addConst() {
|
||||
const { canceled, result } = await os.dialog({
|
||||
title: this.$ts._theme.inputConstantName,
|
||||
input: true
|
||||
});
|
||||
if (canceled) return;
|
||||
this.theme.push([ '$' + result, '#000000']);
|
||||
},
|
||||
|
||||
save() {
|
||||
const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
|
||||
const themes = ColdDeviceStorage.get('themes').concat(theme);
|
||||
ColdDeviceStorage.set('themes', themes);
|
||||
os.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('_theme.installed', { name: theme.name })
|
||||
});
|
||||
this.changed = false;
|
||||
},
|
||||
|
||||
preview() {
|
||||
const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
|
||||
try {
|
||||
applyTheme(theme, false);
|
||||
} catch (e) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async importTheme() {
|
||||
if (this.changed && (!await this.confirm())) return;
|
||||
|
||||
try {
|
||||
const theme = JSON5.parse(this.themeToImport) as Theme;
|
||||
if (!validateTheme(theme)) throw new Error(this.$ts._theme.invalid);
|
||||
|
||||
this.name = theme.name;
|
||||
this.description = theme.desc || '';
|
||||
this.author = theme.author;
|
||||
this.baseTheme = theme.base || 'light';
|
||||
this.theme = convertToViewModel(theme);
|
||||
this.themeToImport = '';
|
||||
} catch (e) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
colorChanged(color: string, i: number) {
|
||||
this.theme[i] = [this.theme[i][0], color];
|
||||
},
|
||||
|
||||
getTypeOf(v: ThemeValue) {
|
||||
return v === null
|
||||
? this.$ts._theme.defaultValue
|
||||
: typeof v === 'string'
|
||||
? this.$ts._theme.color
|
||||
: this.$t('_theme.' + v.type);
|
||||
},
|
||||
|
||||
async chooseType(e: MouseEvent, i: number) {
|
||||
const newValue = await this.showTypeMenu(e);
|
||||
this.theme[i] = [ this.theme[i][0], newValue ];
|
||||
},
|
||||
|
||||
showTypeMenu(e: MouseEvent) {
|
||||
return new Promise<ThemeValue>((resolve) => {
|
||||
os.modalMenu([{
|
||||
text: this.$ts._theme.defaultValue,
|
||||
action: () => resolve(null),
|
||||
}, {
|
||||
text: this.$ts._theme.color,
|
||||
action: () => resolve('#000000'),
|
||||
}, {
|
||||
text: this.$ts._theme.func,
|
||||
action: () => resolve({
|
||||
type: 'func', name: 'alpha', arg: 1, value: 'accent'
|
||||
}),
|
||||
}, {
|
||||
text: this.$ts._theme.refProp,
|
||||
action: () => resolve({
|
||||
type: 'refProp', key: 'accent',
|
||||
}),
|
||||
}, {
|
||||
text: this.$ts._theme.refConst,
|
||||
action: () => resolve({
|
||||
type: 'refConst', key: '',
|
||||
}),
|
||||
}, {
|
||||
text: 'CSS',
|
||||
action: () => resolve({
|
||||
type: 'css', value: '',
|
||||
}),
|
||||
}], e.currentTarget || e.target);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.t9makv94 {
|
||||
> ._section {
|
||||
> ._content {
|
||||
> .list-view {
|
||||
> .item {
|
||||
min-height: 48px;
|
||||
word-break: break-all;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.select {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.type {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.default-value {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.color {
|
||||
> input {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-left: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -22,7 +22,7 @@
|
||||
|
||||
<XPostForm :channel="channel" class="post-form _content _panel _vMargin" fixed v-if="$i"/>
|
||||
|
||||
<XTimeline class="_content _vMargin _noGap_" src="channel" :channel="channelId" @before="before" @after="after"/>
|
||||
<XTimeline class="_content _vMargin _noGap_" src="channel" :key="channelId" :channel="channelId" @before="before" @after="after"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -145,6 +145,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_section">
|
||||
<div class="_title">{{ $ts._mfm.font }}</div>
|
||||
<div class="_content">
|
||||
<p>{{ $ts._mfm.fontDescription }}</p>
|
||||
<div class="preview _panel">
|
||||
<Mfm :text="preview_font"/>
|
||||
<MkTextarea v-model:value="preview_font"><span>MFM</span></MkTextarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_section">
|
||||
<div class="_title">{{ $ts._mfm.x2 }}</div>
|
||||
<div class="_content">
|
||||
@ -295,6 +305,7 @@ export default defineComponent({
|
||||
preview_twitch: `[twitch 🍮]`,
|
||||
preview_spin: `[spin 🍮] [spin.left 🍮] [spin.alternate 🍮]\n[spin.x 🍮] [spin.x,left 🍮] [spin.x,alternate 🍮]\n[spin.y 🍮] [spin.y,left 🍮] [spin.y,alternate 🍮]`,
|
||||
preview_flip: `[flip ${this.$ts._mfm.dummy}]\n[flip.v ${this.$ts._mfm.dummy}]\n[flip.h,v ${this.$ts._mfm.dummy}]`,
|
||||
preview_font: `[font.serif ${this.$ts._mfm.dummy}]\n[font.monospace ${this.$ts._mfm.dummy}]\n[font.cursive ${this.$ts._mfm.dummy}]\n[font.fantasy ${this.$ts._mfm.dummy}]`,
|
||||
preview_x2: `[x2 🍮]`,
|
||||
preview_x3: `[x3 🍮]`,
|
||||
preview_x4: `[x4 🍮]`,
|
||||
|
@ -2,14 +2,14 @@
|
||||
<div class="fcuexfpr">
|
||||
<div v-if="note" class="note">
|
||||
<div class="_section" v-if="showNext">
|
||||
<XNotes class="_content" :pagination="next"/>
|
||||
<XNotes class="_content _noGap_" :pagination="next"/>
|
||||
</div>
|
||||
|
||||
<div class="_section main">
|
||||
<MkButton v-if="!showNext && hasNext" class="load next _content" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton>
|
||||
<div class="_content _vMargin">
|
||||
<MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_vMargin"/>
|
||||
<XNote v-model:note="note" :key="note.id" :detail="true" class="_vMargin"/>
|
||||
<XNoteDetailed v-model:note="note" :key="note.id" class="_vMargin"/>
|
||||
</div>
|
||||
<div class="_content clips _vMargin" v-if="clips && clips.length > 0">
|
||||
<div class="title">{{ $ts.clip }}</div>
|
||||
@ -25,7 +25,7 @@
|
||||
</div>
|
||||
|
||||
<div class="_section" v-if="showPrev">
|
||||
<XNotes class="_content" :pagination="prev"/>
|
||||
<XNotes class="_content _noGap_" :pagination="prev"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { faChevronUp, faChevronDown } from '@fortawesome/free-solid-svg-icons';
|
||||
import XNote from '@/components/note.vue';
|
||||
import XNoteDetailed from '@/components/note-detailed.vue';
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import MkRemoteCaution from '@/components/remote-caution.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
@ -47,6 +48,7 @@ import * as os from '@/os';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNote,
|
||||
XNoteDetailed,
|
||||
XNotes,
|
||||
MkRemoteCaution,
|
||||
MkButton,
|
||||
|
@ -9,7 +9,8 @@
|
||||
</MkInput>
|
||||
<MkSwitch v-model:value="value.detailed"><span>{{ $ts._pages.blocks._note.detailed }}</span></MkSwitch>
|
||||
|
||||
<XNote v-if="note" v-model:note="note" :key="note.id + ':' + (value.detailed ? 'detailed' : 'normal')" :detail="value.detailed" style="margin-bottom: 16px;"/>
|
||||
<XNote v-if="note && !value.detailed" v-model:note="note" :key="note.id + ':normal'" style="margin-bottom: 16px;"/>
|
||||
<XNoteDetailed v-if="note && value.detailed" v-model:note="note" :key="note.id + ':detail'" style="margin-bottom: 16px;"/>
|
||||
</section>
|
||||
</XContainer>
|
||||
</template>
|
||||
@ -21,11 +22,12 @@ import XContainer from '../page-editor.container.vue';
|
||||
import MkInput from '@/components/ui/input.vue';
|
||||
import MkSwitch from '@/components/ui/switch.vue';
|
||||
import XNote from '@/components/note.vue';
|
||||
import XNoteDetailed from '@/components/note-detailed.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XContainer, MkInput, MkSwitch, XNote
|
||||
XContainer, MkInput, MkSwitch, XNote, XNoteDetailed,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -7,7 +7,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
import Progress from '@/scripts/loading';
|
||||
import XNotes from '@/components/notes.vue';
|
||||
@ -20,7 +20,7 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
INFO: {
|
||||
title: this.$t('searchWith', { q: this.$route.query.q }),
|
||||
title: computed(() => this.$t('searchWith', { q: this.$route.query.q })),
|
||||
icon: faSearch
|
||||
},
|
||||
pagination: {
|
||||
|
@ -24,6 +24,8 @@
|
||||
<span>{{ $ts._deck.columnMargin }}</span>
|
||||
<template #suffix>px</template>
|
||||
</FormInput>
|
||||
|
||||
<FormLink @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
@ -31,7 +33,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { faImage, faCog, faColumns } from '@fortawesome/free-solid-svg-icons';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
@ -42,7 +44,7 @@ import * as os from '@/os';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormSelect,
|
||||
FormLink,
|
||||
FormInput,
|
||||
FormRadios,
|
||||
FormBase,
|
||||
@ -67,6 +69,7 @@ export default defineComponent({
|
||||
columnAlign: deckStore.makeGetterSetter('columnAlign'),
|
||||
columnMargin: deckStore.makeGetterSetter('columnMargin'),
|
||||
columnHeaderHeight: deckStore.makeGetterSetter('columnHeaderHeight'),
|
||||
profile: deckStore.makeGetterSetter('profile'),
|
||||
},
|
||||
|
||||
watch: {
|
||||
@ -85,5 +88,19 @@ export default defineComponent({
|
||||
mounted() {
|
||||
this.$emit('info', this.INFO);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async setProfile() {
|
||||
const { canceled, result: name } = await os.dialog({
|
||||
title: this.$ts._deck.profile,
|
||||
input: {
|
||||
allowEmpty: false
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
this.profile = name;
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
|
||||
<FormButton @click="error()">error test</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
@ -42,5 +42,11 @@ export default defineComponent({
|
||||
mounted() {
|
||||
this.$emit('info', this.INFO);
|
||||
},
|
||||
|
||||
methods: {
|
||||
error() {
|
||||
throw new Error('Test error');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -181,6 +181,10 @@ export default defineComponent({
|
||||
titlebar() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
|
||||
instanceTicker() {
|
||||
this.reloadAsk();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
@ -1,49 +1,71 @@
|
||||
<template>
|
||||
<section class="_section">
|
||||
<div class="_title"><Fa :icon="faBoxes"/> {{ $ts.importAndExport }}</div>
|
||||
<div class="_content">
|
||||
<MkSelect v-model:value="exportTarget">
|
||||
<option value="notes">{{ $ts._exportOrImport.allNotes }}</option>
|
||||
<option value="following">{{ $ts._exportOrImport.followingList }}</option>
|
||||
<option value="user-lists">{{ $ts._exportOrImport.userLists }}</option>
|
||||
<option value="mute">{{ $ts._exportOrImport.muteList }}</option>
|
||||
<option value="blocking">{{ $ts._exportOrImport.blockingList }}</option>
|
||||
</MkSelect>
|
||||
<MkButton inline primary @click="doExport"><Fa :icon="faDownload"/> {{ $ts.export }}</MkButton>
|
||||
<MkButton inline primary @click="doImport" :disabled="!['following', 'user-lists'].includes(exportTarget)"><Fa :icon="faUpload"/> {{ $ts.import }}</MkButton>
|
||||
</div>
|
||||
</section>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts._exportOrImport.allNotes }}</template>
|
||||
<FormButton @click="doExport('notes')"><Fa :icon="faDownload"/> {{ $ts.export }}</FormButton>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts._exportOrImport.followingList }}</template>
|
||||
<FormButton @click="doExport('following')"><Fa :icon="faDownload"/> {{ $ts.export }}</FormButton>
|
||||
<FormButton @click="doImport('following', $event)"><Fa :icon="faUpload"/> {{ $ts.import }}</FormButton>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts._exportOrImport.userLists }}</template>
|
||||
<FormButton @click="doExport('user-lists')"><Fa :icon="faDownload"/> {{ $ts.export }}</FormButton>
|
||||
<FormButton @click="doImport('user-lists', $event)"><Fa :icon="faUpload"/> {{ $ts.import }}</FormButton>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts._exportOrImport.muteList }}</template>
|
||||
<FormButton @click="doExport('mute')"><Fa :icon="faDownload"/> {{ $ts.export }}</FormButton>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts._exportOrImport.blockingList }}</template>
|
||||
<FormButton @click="doExport('blocking')"><Fa :icon="faDownload"/> {{ $ts.export }}</FormButton>
|
||||
</FormGroup>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faDownload, faUpload, faBoxes } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkSelect from '@/components/ui/select.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormButton from '@/components/form/button.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import * as os from '@/os';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkSelect,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
FormButton,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
exportTarget: 'notes',
|
||||
INFO: {
|
||||
title: this.$ts.importAndExport,
|
||||
icon: faBoxes
|
||||
},
|
||||
faDownload, faUpload, faBoxes
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this.INFO);
|
||||
},
|
||||
|
||||
methods: {
|
||||
doExport() {
|
||||
doExport(target) {
|
||||
os.api(
|
||||
this.exportTarget == 'notes' ? 'i/export-notes' :
|
||||
this.exportTarget == 'following' ? 'i/export-following' :
|
||||
this.exportTarget == 'blocking' ? 'i/export-blocking' :
|
||||
this.exportTarget == 'user-lists' ? 'i/export-user-lists' :
|
||||
this.exportTarget == 'mute' ? 'i/export-mute' :
|
||||
target == 'notes' ? 'i/export-notes' :
|
||||
target == 'following' ? 'i/export-following' :
|
||||
target == 'blocking' ? 'i/export-blocking' :
|
||||
target == 'user-lists' ? 'i/export-user-lists' :
|
||||
target == 'mute' ? 'i/export-mute' :
|
||||
null, {})
|
||||
.then(() => {
|
||||
os.dialog({
|
||||
@ -58,12 +80,12 @@ export default defineComponent({
|
||||
});
|
||||
},
|
||||
|
||||
async doImport(e) {
|
||||
async doImport(target, e) {
|
||||
const file = await selectFile(e.currentTarget || e.target);
|
||||
|
||||
os.api(
|
||||
this.exportTarget == 'following' ? 'i/import-following' :
|
||||
this.exportTarget == 'user-lists' ? 'i/import-user-lists' :
|
||||
target == 'following' ? 'i/import-following' :
|
||||
target == 'user-lists' ? 'i/import-user-lists' :
|
||||
null, {
|
||||
fileId: file.id
|
||||
}).then(() => {
|
||||
|
@ -35,13 +35,13 @@
|
||||
</FormGroup>
|
||||
</FormBase>
|
||||
<div class="main">
|
||||
<component :is="component" @info="onInfo"/>
|
||||
<component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faLaugh, faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||
import { i18n } from '@/i18n';
|
||||
@ -78,7 +78,9 @@ export default defineComponent({
|
||||
const onInfo = (viewInfo) => {
|
||||
INFO.value = viewInfo;
|
||||
};
|
||||
const pageProps = ref({});
|
||||
const component = computed(() => {
|
||||
if (props.page == null) return null;
|
||||
switch (props.page) {
|
||||
case 'profile': return defineAsyncComponent(() => import('./profile.vue'));
|
||||
case 'privacy': return defineAsyncComponent(() => import('./privacy.vue'));
|
||||
@ -104,16 +106,35 @@ export default defineComponent({
|
||||
case 'plugins': return defineAsyncComponent(() => import('./plugins.vue'));
|
||||
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
|
||||
case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
|
||||
case 'registry': return defineAsyncComponent(() => import('./registry.vue'));
|
||||
case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue'));
|
||||
default: return null;
|
||||
}
|
||||
if (props.page.startsWith('registry/keys/system/')) {
|
||||
return defineAsyncComponent(() => import('./registry.keys.vue'));
|
||||
}
|
||||
if (props.page.startsWith('registry/value/system/')) {
|
||||
return defineAsyncComponent(() => import('./registry.value.vue'));
|
||||
}
|
||||
});
|
||||
|
||||
watch(component, () => {
|
||||
pageProps.value = {};
|
||||
|
||||
if (props.page) {
|
||||
if (props.page.startsWith('registry/keys/system/')) {
|
||||
pageProps.value.scope = props.page.replace('registry/keys/system/', '').split('/');
|
||||
}
|
||||
if (props.page.startsWith('registry/value/system/')) {
|
||||
const path = props.page.replace('registry/value/system/', '').split('/');
|
||||
pageProps.value.xKey = path.pop();
|
||||
pageProps.value.scope = path;
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
scroll(el.value, 0);
|
||||
});
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
onMounted(() => {
|
||||
narrow.value = el.value.offsetWidth < 1025;
|
||||
@ -125,6 +146,7 @@ export default defineComponent({
|
||||
view,
|
||||
el,
|
||||
onInfo,
|
||||
pageProps,
|
||||
component,
|
||||
logout: () => {
|
||||
signout();
|
||||
|
@ -1,34 +1,34 @@
|
||||
<template>
|
||||
<section class="rrfwjxfl _section">
|
||||
<FormBase>
|
||||
<MkTab v-model:value="tab" style="margin-bottom: var(--margin);">
|
||||
<option value="mute">{{ $ts.mutedUsers }}</option>
|
||||
<option value="block">{{ $ts.blockedUsers }}</option>
|
||||
</MkTab>
|
||||
<div class="_content" v-if="tab === 'mute'">
|
||||
<div v-if="tab === 'mute'">
|
||||
<MkPagination :pagination="mutingPagination" class="muting">
|
||||
<template #empty><MkInfo>{{ $ts.noUsers }}</MkInfo></template>
|
||||
<template #default="{items}">
|
||||
<div class="user" v-for="mute in items" :key="mute.id">
|
||||
<MkA class="name" :to="userPage(mute.mutee)">
|
||||
<FormGroup>
|
||||
<FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)">
|
||||
<MkAcct :user="mute.mutee"/>
|
||||
</MkA>
|
||||
</div>
|
||||
</FormLink>
|
||||
</FormGroup>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div class="_content" v-if="tab === 'block'">
|
||||
<div v-if="tab === 'block'">
|
||||
<MkPagination :pagination="blockingPagination" class="blocking">
|
||||
<template #empty><MkInfo>{{ $ts.noUsers }}</MkInfo></template>
|
||||
<template #default="{items}">
|
||||
<div class="user" v-for="block in items" :key="block.id">
|
||||
<MkA class="name" :to="userPage(block.blockee)">
|
||||
<FormGroup>
|
||||
<FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)">
|
||||
<MkAcct :user="block.blockee"/>
|
||||
</MkA>
|
||||
</div>
|
||||
</FormLink>
|
||||
</FormGroup>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</section>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -37,6 +37,9 @@ import { faBan } from '@fortawesome/free-solid-svg-icons';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import { userPage } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
|
||||
@ -45,6 +48,9 @@ export default defineComponent({
|
||||
MkPagination,
|
||||
MkTab,
|
||||
MkInfo,
|
||||
FormBase,
|
||||
FormGroup,
|
||||
FormLink,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
@ -76,19 +82,3 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rrfwjxfl {
|
||||
> ._content {
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
|
||||
> .muting,
|
||||
> .blocking {
|
||||
> .empty {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -4,6 +4,8 @@
|
||||
{{ $ts.showFeaturedNotesInTimeline }}
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model:value="reportError">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
|
||||
|
||||
<FormLink to="/settings/account-info">{{ $ts.accountInfo }}</FormLink>
|
||||
<FormLink to="/settings/experimental-features">{{ $ts.experimentalFeatures }}</FormLink>
|
||||
|
||||
@ -13,16 +15,19 @@
|
||||
DEBUG MODE
|
||||
</FormSwitch>
|
||||
<template v-if="debug">
|
||||
<FormLink to="/settings/regedit">RegEdit</FormLink>
|
||||
<FormButton @click="taskmanager">Task Manager</FormButton>
|
||||
</template>
|
||||
</FormGroup>
|
||||
|
||||
<FormLink to="/settings/registry"><template #icon><Fa :icon="faCogs"/></template>{{ $ts.registry }}</FormLink>
|
||||
|
||||
<FormButton @click="closeAccount" danger>{{ $ts.closeAccount }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faEllipsisH, faCogs } from '@fortawesome/free-solid-svg-icons';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
@ -31,6 +36,8 @@ import FormGroup from '@/components/form/group.vue';
|
||||
import FormButton from '@/components/form/button.vue';
|
||||
import * as os from '@/os';
|
||||
import { debug } from '@/config';
|
||||
import { defaultStore } from '@/store';
|
||||
import { signout } from '@/account';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -50,10 +57,15 @@ export default defineComponent({
|
||||
title: this.$ts.other,
|
||||
icon: faEllipsisH
|
||||
},
|
||||
debug
|
||||
debug,
|
||||
faCogs
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
reportError: defaultStore.makeGetterSetter('reportError'),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this.INFO);
|
||||
},
|
||||
@ -74,6 +86,22 @@ export default defineComponent({
|
||||
taskmanager() {
|
||||
os.popup(import('@/components/taskmanager.vue'), {
|
||||
}, {}, 'closed');
|
||||
},
|
||||
|
||||
closeAccount() {
|
||||
os.dialog({
|
||||
title: this.$ts.password,
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
os.api('i/delete-account', {
|
||||
password: password
|
||||
}).then(() => {
|
||||
signout();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
115
src/client/pages/settings/registry.keys.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.domain }}</template>
|
||||
<template #value>{{ $ts.system }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.scope }}</template>
|
||||
<template #value>{{ scope.join('/') }}</template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup v-if="keys">
|
||||
<template #label>{{ $ts._registry.keys }}</template>
|
||||
<FormLink v-for="key in keys" :to="`/settings/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
|
||||
</FormGroup>
|
||||
|
||||
<FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { faCogs } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as JSON5 from 'json5';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import FormButton from '@/components/form/button.vue';
|
||||
import FormKeyValueView from '@/components/form/key-value-view.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkInfo,
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormLink,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
props: {
|
||||
scope: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
INFO: {
|
||||
title: this.$ts.registry,
|
||||
icon: faCogs
|
||||
},
|
||||
keys: null,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
scope() {
|
||||
this.fetch();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this.INFO);
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
os.api('i/registry/keys-with-type', {
|
||||
scope: this.scope
|
||||
}).then(keys => {
|
||||
this.keys = Object.entries(keys).sort((a, b) => a[0].localeCompare(b[0]));
|
||||
});
|
||||
},
|
||||
|
||||
async createKey() {
|
||||
const { canceled, result } = await os.form(this.$ts._registry.createKey, {
|
||||
key: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.key,
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
label: this.$ts.value,
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.scope,
|
||||
default: this.scope.join('/')
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: result.scope.split('/'),
|
||||
key: result.key,
|
||||
value: JSON5.parse(result.value),
|
||||
}).then(() => {
|
||||
this.fetch();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
149
src/client/pages/settings/registry.value.vue
Normal file
@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<MkInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</MkInfo>
|
||||
|
||||
<template v-if="value">
|
||||
<FormGroup>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.domain }}</template>
|
||||
<template #value>{{ $ts.system }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.scope }}</template>
|
||||
<template #value>{{ scope.join('/') }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.key }}</template>
|
||||
<template #value>{{ xKey }}</template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormTextarea tall v-model:value="valueForEditor" class="_monospace" style="tab-size: 2;">
|
||||
<span>{{ $ts.value }} (JSON)</span>
|
||||
</FormTextarea>
|
||||
<FormButton @click="save" primary><Fa :icon="faSave"/> {{ $ts.save }}</FormButton>
|
||||
</FormGroup>
|
||||
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.updatedAt }}</template>
|
||||
<template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
|
||||
</FormKeyValueView>
|
||||
|
||||
<FormButton danger @click="del"><Fa :icon="faTrash"/> {{ $ts.delete }}</FormButton>
|
||||
</template>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { faCogs, faSave, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as JSON5 from 'json5';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import FormButton from '@/components/form/button.vue';
|
||||
import FormKeyValueView from '@/components/form/key-value-view.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkInfo,
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormTextarea,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
props: {
|
||||
scope: {
|
||||
required: true
|
||||
},
|
||||
xKey: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
INFO: {
|
||||
title: this.$ts.registry,
|
||||
icon: faCogs
|
||||
},
|
||||
value: null,
|
||||
valueForEditor: null,
|
||||
faSave, faTrash,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
key() {
|
||||
this.fetch();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this.INFO);
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
os.api('i/registry/get-detail', {
|
||||
scope: this.scope,
|
||||
key: this.xKey
|
||||
}).then(value => {
|
||||
this.value = value;
|
||||
this.valueForEditor = JSON5.stringify(this.value.value, null, '\t');
|
||||
});
|
||||
},
|
||||
|
||||
save() {
|
||||
try {
|
||||
JSON5.parse(this.valueForEditor);
|
||||
} catch (e) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: this.$ts.invalidValue
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$ts.saveConfirm,
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: this.scope,
|
||||
key: this.xKey,
|
||||
value: JSON5.parse(this.valueForEditor)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
del() {
|
||||
os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$ts.deleteConfirm,
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/remove', {
|
||||
scope: this.scope,
|
||||
key: this.xKey
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
91
src/client/pages/settings/registry.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormGroup v-if="scopes">
|
||||
<template #label>{{ $ts.system }}</template>
|
||||
<FormLink v-for="scope in scopes" :to="`/settings/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink>
|
||||
</FormGroup>
|
||||
<FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { faCogs } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as JSON5 from 'json5';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import FormButton from '@/components/form/button.vue';
|
||||
import FormKeyValueView from '@/components/form/key-value-view.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkInfo,
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormLink,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
INFO: {
|
||||
title: this.$ts.registry,
|
||||
icon: faCogs
|
||||
},
|
||||
scopes: null,
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this.INFO);
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
os.api('i/registry/scopes').then(scopes => {
|
||||
this.scopes = scopes.slice().sort((a, b) => a.join('/').localeCompare(b.join('/')));
|
||||
});
|
||||
},
|
||||
|
||||
async createKey() {
|
||||
const { canceled, result } = await os.form(this.$ts._registry.createKey, {
|
||||
key: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.key,
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
label: this.$ts.value,
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.scope,
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: result.scope.split('/'),
|
||||
key: result.key,
|
||||
value: JSON5.parse(result.value),
|
||||
}).then(() => {
|
||||
this.fetch();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -25,6 +25,7 @@ import FormButton from '@/components/form/button.vue';
|
||||
import { applyTheme, validateTheme } from '@/scripts/theme';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { addTheme, getThemes } from '@/theme-store';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -74,7 +75,7 @@ export default defineComponent({
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (ColdDeviceStorage.get('themes').some(t => t.id === theme.id)) {
|
||||
if (getThemes().some(t => t.id === theme.id)) {
|
||||
os.dialog({
|
||||
type: 'info',
|
||||
text: this.$ts._theme.alreadyInstalled
|
||||
@ -90,11 +91,10 @@ export default defineComponent({
|
||||
if (theme) applyTheme(theme, false);
|
||||
},
|
||||
|
||||
install(code) {
|
||||
async install(code) {
|
||||
const theme = this.parseThemeCode(code);
|
||||
if (!theme) return;
|
||||
const themes = ColdDeviceStorage.get('themes').concat(theme);
|
||||
ColdDeviceStorage.set('themes', themes);
|
||||
await addTheme(theme);
|
||||
os.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('_theme.installed', { name: theme.name })
|
||||
|
@ -37,6 +37,7 @@ import { Theme, builtinThemes } from '@/scripts/theme';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { getThemes, removeTheme } from '@/theme-store';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -57,7 +58,7 @@ export default defineComponent({
|
||||
title: this.$ts._theme.manage,
|
||||
icon: faFolderOpen
|
||||
},
|
||||
installedThemes: ColdDeviceStorage.ref('themes'),
|
||||
installedThemes: getThemes(),
|
||||
builtinThemes,
|
||||
selectedThemeId: null,
|
||||
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
|
||||
@ -91,10 +92,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
uninstall() {
|
||||
const theme = this.selectedTheme;
|
||||
const themes = ColdDeviceStorage.get('themes').filter(t => t.id != theme.id);
|
||||
ColdDeviceStorage.set('themes', themes);
|
||||
os.success();
|
||||
removeTheme(this.selectedTheme);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
@ -49,11 +49,14 @@
|
||||
<FormButton primary v-else @click="wallpaper = null">{{ $ts.removeWallpaper }}</FormButton>
|
||||
|
||||
<FormGroup>
|
||||
<FormLink to="https://assets.msky.cafe/theme/list" external>{{ $ts._theme.explore }}</FormLink>
|
||||
<FormLink to="/theme-editor">{{ $ts._theme.make }}</FormLink>
|
||||
<FormLink to="https://assets.msky.cafe/theme/list" external><template #icon><Fa :icon="faGlobe"/></template>{{ $ts._theme.explore }}</FormLink>
|
||||
<FormLink to="/settings/theme/install"><template #icon><Fa :icon="faDownload"/></template>{{ $ts._theme.install }}</FormLink>
|
||||
</FormGroup>
|
||||
|
||||
<FormLink to="/settings/theme/install"><template #icon><Fa :icon="faDownload"/></template>{{ $ts._theme.install }}</FormLink>
|
||||
<FormGroup>
|
||||
<FormLink to="/theme-editor"><template #icon><Fa :icon="faPaintRoller"/></template>{{ $ts._theme.make }}</FormLink>
|
||||
<FormLink to="/advanced-theme-editor"><template #icon><Fa :icon="faPaintRoller"/></template>{{ $ts._theme.make }} ({{ $ts.advanced }})</FormLink>
|
||||
</FormGroup>
|
||||
|
||||
<FormLink to="/settings/theme/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $ts._theme.manage }}</FormLink>
|
||||
</FormBase>
|
||||
@ -61,7 +64,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, ref, watch } from 'vue';
|
||||
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye, faGlobe, faPaintRoller } from '@fortawesome/free-solid-svg-icons';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
@ -74,6 +77,7 @@ import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import { fetchThemes, getThemes } from '@/theme-store';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -93,7 +97,7 @@ export default defineComponent({
|
||||
icon: faPalette
|
||||
};
|
||||
|
||||
const installedThemes = ColdDeviceStorage.ref('themes');
|
||||
const installedThemes = ref(getThemes());
|
||||
const themes = computed(() => builtinThemes.concat(installedThemes.value));
|
||||
const darkThemes = computed(() => themes.value.filter(t => t.base == 'dark' || t.kind == 'dark'));
|
||||
const lightThemes = computed(() => themes.value.filter(t => t.base == 'light' || t.kind == 'light'));
|
||||
@ -134,6 +138,10 @@ export default defineComponent({
|
||||
emit('info', INFO);
|
||||
});
|
||||
|
||||
fetchThemes().then(() => {
|
||||
installedThemes.value = getThemes();
|
||||
});
|
||||
|
||||
return {
|
||||
INFO,
|
||||
darkThemes,
|
||||
@ -148,7 +156,7 @@ export default defineComponent({
|
||||
wallpaper.value = file.url;
|
||||
});
|
||||
},
|
||||
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
|
||||
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye, faGlobe, faPaintRoller,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -1,121 +1,70 @@
|
||||
<template>
|
||||
<div class="t9makv94">
|
||||
<section class="_section">
|
||||
<div class="_content">
|
||||
<details>
|
||||
<summary>{{ $ts.import }}</summary>
|
||||
<MkTextarea v-model:value="themeToImport">
|
||||
{{ $ts._theme.importInfo }}
|
||||
</MkTextarea>
|
||||
<MkButton :disabled="!themeToImport.trim()" @click="importTheme">{{ $ts.import }}</MkButton>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_section">
|
||||
<div class="_content _card _vMargin">
|
||||
<div class="_content">
|
||||
<MkInput v-model:value="name" required><span>{{ $ts.name }}</span></MkInput>
|
||||
<MkInput v-model:value="author" required><span>{{ $ts.author }}</span></MkInput>
|
||||
<MkTextarea v-model:value="description"><span>{{ $ts.description }}</span></MkTextarea>
|
||||
<div class="_inputs">
|
||||
<div v-text="$ts._theme.base" />
|
||||
<MkRadio v-model="baseTheme" value="light">{{ $ts.light }}</MkRadio>
|
||||
<MkRadio v-model="baseTheme" value="dark">{{ $ts.dark }}</MkRadio>
|
||||
</div>
|
||||
<FormBase class="cwepdizn">
|
||||
<div class="_formItem colorPicker">
|
||||
<div class="_formLabel">{{ $ts.backgroundColor }}</div>
|
||||
<div class="_formPanel colors">
|
||||
<div class="row">
|
||||
<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" @click="bgColor = color" class="color _button" :class="{ active: bgColor?.color === color.color }">
|
||||
<div class="preview" :style="{ background: color.forPreview }"></div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" @click="bgColor = color" class="color _button" :class="{ active: bgColor?.color === color.color }">
|
||||
<div class="preview" :style="{ background: color.forPreview }"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_content _card _vMargin">
|
||||
<div class="list-view _content">
|
||||
<div class="item" v-for="([ k, v ], i) in theme" :key="k">
|
||||
<div class="_inputs">
|
||||
<div>
|
||||
{{ k.startsWith('$') ? `${k} (${$ts._theme.constant})` : $t('_theme.keys.' + k) }}
|
||||
<button v-if="k.startsWith('$')" class="_button _link" @click="del(i)" v-text="$ts.delete" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="type" @click="chooseType($event, i)">
|
||||
{{ getTypeOf(v) }} <Fa :icon="faChevronDown"/>
|
||||
</div>
|
||||
<!-- default -->
|
||||
<div v-if="v === null" v-text="baseProps[k]" class="default-value" />
|
||||
<!-- color -->
|
||||
<div v-else-if="typeof v === 'string'" class="color">
|
||||
<input type="color" :value="v" @input="colorChanged($event.target.value, i)"/>
|
||||
<MkInput class="select" :value="v" @update:value="colorChanged($event, i)"/>
|
||||
</div>
|
||||
<!-- ref const -->
|
||||
<MkInput v-else-if="v.type === 'refConst'" v-model:value="v.key">
|
||||
<template #prefix>$</template>
|
||||
<span>{{ $ts.name }}</span>
|
||||
</MkInput>
|
||||
<!-- ref props -->
|
||||
<MkSelect class="select" v-else-if="v.type === 'refProp'" v-model:value="v.key">
|
||||
<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
|
||||
</MkSelect>
|
||||
<!-- func -->
|
||||
<template v-else-if="v.type === 'func'">
|
||||
<MkSelect class="select" v-model:value="v.name">
|
||||
<template #label>{{ $ts._theme.funcKind }}</template>
|
||||
<option v-for="n in ['alpha', 'darken', 'lighten']" :value="n" :key="n">{{ $t('_theme.' + n) }}</option>
|
||||
</MkSelect>
|
||||
<MkInput type="number" v-model:value="v.arg"><span>{{ $ts._theme.argument }}</span></MkInput>
|
||||
<MkSelect class="select" v-model:value="v.value">
|
||||
<template #label>{{ $ts._theme.basedProp }}</template>
|
||||
<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
|
||||
</MkSelect>
|
||||
</template>
|
||||
<!-- CSS -->
|
||||
<MkInput v-else-if="v.type === 'css'" v-model:value="v.value">
|
||||
<span>CSS</span>
|
||||
</MkInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkButton primary @click="addConst">{{ $ts._theme.addConstant }}</MkButton>
|
||||
</div>
|
||||
<div class="_formItem colorPicker">
|
||||
<div class="_formLabel">{{ $ts.accentColor }}</div>
|
||||
<div class="_formPanel colors">
|
||||
<div class="row">
|
||||
<button v-for="color in accentColors" :key="color" @click="accentColor = color" class="color rounded _button" :class="{ active: accentColor === color }">
|
||||
<div class="preview" :style="{ background: color }"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_section">
|
||||
<details class="_content">
|
||||
<summary>{{ $ts.sample }}</summary>
|
||||
<MkSample/>
|
||||
</details>
|
||||
</section>
|
||||
<section class="_section">
|
||||
<div class="_content">
|
||||
<MkButton inline @click="preview">{{ $ts.preview }}</MkButton>
|
||||
<MkButton inline primary :disabled="!name || !author" @click="save">{{ $ts.save }}</MkButton>
|
||||
</div>
|
||||
<div class="_formItem colorPicker">
|
||||
<div class="_formLabel">{{ $ts.textColor }}</div>
|
||||
<div class="_formPanel colors">
|
||||
<div class="row">
|
||||
<button v-for="color in fgColors" :key="color" @click="fgColor = color" class="color char _button" :class="{ active: fgColor === color }">
|
||||
<div class="preview" :style="{ color: color.forPreview ? color.forPreview : bgColor?.kind === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_formItem preview">
|
||||
<div class="_formLabel">{{ $ts.preview }}</div>
|
||||
<div class="_formPanel preview">
|
||||
<MkSample class="preview"/>
|
||||
</div>
|
||||
</div>
|
||||
<FormButton @click="saveAs">{{ $ts.saveAs }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faPalette, faChevronDown, faKeyboard } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as JSON5 from 'json5';
|
||||
import { toUnicode } from 'punycode';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import { v4 as uuid} from 'uuid';
|
||||
|
||||
import MkRadio from '@/components/ui/radio.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/ui/input.vue';
|
||||
import MkTextarea from '@/components/ui/textarea.vue';
|
||||
import MkSelect from '@/components/ui/select.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
import FormButton from '@/components/form/button.vue';
|
||||
import MkSample from '@/components/sample.vue';
|
||||
|
||||
import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@/scripts/theme-editor';
|
||||
import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@/scripts/theme';
|
||||
import { Theme, applyTheme, validateTheme } from '@/scripts/theme';
|
||||
import { host } from '@/config';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkRadio,
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkSelect,
|
||||
FormBase,
|
||||
FormButton,
|
||||
MkSample,
|
||||
},
|
||||
|
||||
@ -125,229 +74,172 @@ export default defineComponent({
|
||||
title: this.$ts.themeEditor,
|
||||
icon: faPalette,
|
||||
},
|
||||
theme: [] as ThemeViewModel,
|
||||
name: '',
|
||||
description: '',
|
||||
baseTheme: 'light' as 'dark' | 'light',
|
||||
author: `@${this.$i.username}@${toUnicode(host)}`,
|
||||
themeToImport: '',
|
||||
changed: false,
|
||||
lightTheme, darkTheme, themeProps,
|
||||
faPalette, faChevronDown, faKeyboard,
|
||||
bgColors: [
|
||||
{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
|
||||
{ color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
|
||||
{ color: '#e9eff0', kind: 'light', forPreview: '#bfe3e8' },
|
||||
{ color: '#f0e9ee', kind: 'light', forPreview: '#f1d1e8' },
|
||||
{ color: '#dce2e0', kind: 'light', forPreview: '#a4dccc' },
|
||||
{ color: '#e2e0dc', kind: 'light', forPreview: '#d8c7a5' },
|
||||
{ color: '#d5dbe0', kind: 'light', forPreview: '#b0cae0' },
|
||||
{ color: '#dad5d5', kind: 'light', forPreview: '#d6afaf' },
|
||||
{ color: '#2b2b2b', kind: 'dark', forPreview: '#444444' },
|
||||
{ color: '#362e29', kind: 'dark', forPreview: '#735c4d' },
|
||||
{ color: '#303629', kind: 'dark', forPreview: '#506d2f' },
|
||||
{ color: '#293436', kind: 'dark', forPreview: '#258192' },
|
||||
{ color: '#2e2936', kind: 'dark', forPreview: '#504069' },
|
||||
{ color: '#252722', kind: 'dark', forPreview: '#3c462f' },
|
||||
{ color: '#212525', kind: 'dark', forPreview: '#303e3e' },
|
||||
{ color: '#191919', kind: 'dark', forPreview: '#272727' },
|
||||
],
|
||||
bgColor: null,
|
||||
accentColors: ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'],
|
||||
accentColor: null,
|
||||
fgColors: [
|
||||
{ color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null },
|
||||
{ color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' },
|
||||
{ color: 'yellow', forLight: '#736955', forDark: '#e0d5c0', forPreview: '#d49923' },
|
||||
{ color: 'green', forLight: '#586d5b', forDark: '#d1e4d4', forPreview: '#4cbd5c' },
|
||||
{ color: 'cyan', forLight: '#5d7475', forDark: '#d1e3e4', forPreview: '#2abdc3' },
|
||||
{ color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' },
|
||||
{ color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
|
||||
],
|
||||
fgColor: null,
|
||||
faPalette,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
baseProps() {
|
||||
return this.baseTheme === 'light' ? this.lightTheme.props : this.darkTheme.props;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const currentBgColor = getComputedStyle(document.documentElement).getPropertyValue('--bg');
|
||||
const matchedBgColor = this.bgColors.find(x => tinycolor(x.color).toRgbString() === tinycolor(currentBgColor).toRgbString());
|
||||
if (matchedBgColor) this.bgColor = matchedBgColor;
|
||||
const currentAccentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent');
|
||||
const matchedAccentColor = this.accentColors.find(x => tinycolor(x).toRgbString() === tinycolor(currentAccentColor).toRgbString());
|
||||
if (matchedAccentColor) this.accentColor = matchedAccentColor;
|
||||
const currentFgColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(currentFgColor).toRgbString()));
|
||||
if (matchedFgColor) this.fgColor = matchedFgColor;
|
||||
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('beforeunload', this.beforeunload);
|
||||
},
|
||||
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
if (this.changed && !(await this.confirm())) {
|
||||
next(false);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
window.addEventListener('beforeunload', this.beforeunload);
|
||||
const changed = () => this.changed = true;
|
||||
this.$watch('name', changed);
|
||||
this.$watch('description', changed);
|
||||
this.$watch('baseTheme', changed);
|
||||
this.$watch('author', changed);
|
||||
this.$watch('theme', changed);
|
||||
this.$watch('bgColor', this.apply);
|
||||
this.$watch('accentColor', this.apply);
|
||||
this.$watch('fgColor', this.apply);
|
||||
},
|
||||
|
||||
methods: {
|
||||
beforeunload(e: BeforeUnloadEvent) {
|
||||
if (this.changed) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
convert() {
|
||||
return {
|
||||
id: '#MY_THEME#',
|
||||
name: this.$ts.myTheme,
|
||||
base: this.bgColor.kind,
|
||||
props: {
|
||||
bg: this.bgColor.color,
|
||||
fg: this.bgColor.kind === 'light' ? this.fgColor.forLight : this.fgColor.forDark,
|
||||
accent: this.accentColor,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
async confirm(): Promise<boolean> {
|
||||
const { canceled } = await os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$ts.leaveConfirm,
|
||||
showCancelButton: true
|
||||
});
|
||||
return !canceled;
|
||||
apply() {
|
||||
if (this.bgColor == null) this.bgColor = this.bgColors[0];
|
||||
if (this.accentColor == null) this.accentColor = this.accentColors[0];
|
||||
if (this.fgColor == null) this.fgColor = this.fgColors[0];
|
||||
|
||||
const theme = this.convert();
|
||||
applyTheme(theme, true);
|
||||
|
||||
const themes = ColdDeviceStorage.get('themes').filter(t => t.id != '#MY_THEME#').concat(theme);
|
||||
ColdDeviceStorage.set('themes', themes);
|
||||
ColdDeviceStorage.set('lightTheme', theme.id);
|
||||
ColdDeviceStorage.set('darkTheme', theme.id);
|
||||
},
|
||||
|
||||
init() {
|
||||
const t: ThemeViewModel = [];
|
||||
for (const key of themeProps) {
|
||||
t.push([ key, null ]);
|
||||
}
|
||||
this.theme = t;
|
||||
},
|
||||
|
||||
async del(i: number) {
|
||||
const { canceled } = await os.dialog({
|
||||
type: 'warning',
|
||||
showCancelButton: true,
|
||||
text: this.$t('_theme.deleteConstantConfirm', { const: this.theme[i][0] }),
|
||||
async saveAs() {
|
||||
const { canceled, result: name } = await os.dialog({
|
||||
title: this.$ts.name,
|
||||
input: {
|
||||
allowEmpty: false
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
Vue.delete(this.theme, i);
|
||||
},
|
||||
|
||||
async addConst() {
|
||||
const { canceled, result } = await os.dialog({
|
||||
title: this.$ts._theme.inputConstantName,
|
||||
input: true
|
||||
});
|
||||
if (canceled) return;
|
||||
this.theme.push([ '$' + result, '#000000']);
|
||||
},
|
||||
|
||||
save() {
|
||||
const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
|
||||
|
||||
const theme = this.convert();
|
||||
theme.id = uuid();
|
||||
theme.name = name;
|
||||
theme.author = `@${this.$i.username}@${toUnicode(host)}`;
|
||||
const themes = ColdDeviceStorage.get('themes').concat(theme);
|
||||
ColdDeviceStorage.set('themes', themes);
|
||||
ColdDeviceStorage.set('lightTheme', theme.id);
|
||||
ColdDeviceStorage.set('darkTheme', theme.id);
|
||||
os.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('_theme.installed', { name: theme.name })
|
||||
});
|
||||
this.changed = false;
|
||||
},
|
||||
|
||||
preview() {
|
||||
const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
|
||||
try {
|
||||
applyTheme(theme, false);
|
||||
} catch (e) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async importTheme() {
|
||||
if (this.changed && (!await this.confirm())) return;
|
||||
|
||||
try {
|
||||
const theme = JSON5.parse(this.themeToImport) as Theme;
|
||||
if (!validateTheme(theme)) throw new Error(this.$ts._theme.invalid);
|
||||
|
||||
this.name = theme.name;
|
||||
this.description = theme.desc || '';
|
||||
this.author = theme.author;
|
||||
this.baseTheme = theme.base || 'light';
|
||||
this.theme = convertToViewModel(theme);
|
||||
this.themeToImport = '';
|
||||
} catch (e) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
colorChanged(color: string, i: number) {
|
||||
this.theme[i] = [this.theme[i][0], color];
|
||||
},
|
||||
|
||||
getTypeOf(v: ThemeValue) {
|
||||
return v === null
|
||||
? this.$ts._theme.defaultValue
|
||||
: typeof v === 'string'
|
||||
? this.$ts._theme.color
|
||||
: this.$t('_theme.' + v.type);
|
||||
},
|
||||
|
||||
async chooseType(e: MouseEvent, i: number) {
|
||||
const newValue = await this.showTypeMenu(e);
|
||||
this.theme[i] = [ this.theme[i][0], newValue ];
|
||||
},
|
||||
|
||||
showTypeMenu(e: MouseEvent) {
|
||||
return new Promise<ThemeValue>((resolve) => {
|
||||
os.modalMenu([{
|
||||
text: this.$ts._theme.defaultValue,
|
||||
action: () => resolve(null),
|
||||
}, {
|
||||
text: this.$ts._theme.color,
|
||||
action: () => resolve('#000000'),
|
||||
}, {
|
||||
text: this.$ts._theme.func,
|
||||
action: () => resolve({
|
||||
type: 'func', name: 'alpha', arg: 1, value: 'accent'
|
||||
}),
|
||||
}, {
|
||||
text: this.$ts._theme.refProp,
|
||||
action: () => resolve({
|
||||
type: 'refProp', key: 'accent',
|
||||
}),
|
||||
}, {
|
||||
text: this.$ts._theme.refConst,
|
||||
action: () => resolve({
|
||||
type: 'refConst', key: '',
|
||||
}),
|
||||
}, {
|
||||
text: 'CSS',
|
||||
action: () => resolve({
|
||||
type: 'css', value: '',
|
||||
}),
|
||||
}], e.currentTarget || e.target);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.t9makv94 {
|
||||
> ._section {
|
||||
> ._content {
|
||||
> .list-view {
|
||||
> .item {
|
||||
min-height: 48px;
|
||||
word-break: break-all;
|
||||
.cwepdizn {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
> .colorPicker {
|
||||
> .colors {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
|
||||
> .row {
|
||||
> .color {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
|
||||
> .preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgb(0 0 0 / 30%);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.select {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.type {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.default-value {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.color {
|
||||
> input {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
&:hover {
|
||||
> .preview {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-left: 8px;
|
||||
display: inline-block;
|
||||
&.active {
|
||||
box-shadow: 0 0 0 2px var(--divider) inset;
|
||||
}
|
||||
|
||||
&.rounded {
|
||||
border-radius: 999px;
|
||||
|
||||
> .preview {
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
&.char {
|
||||
line-height: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .preview > .preview > .preview {
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -253,6 +253,7 @@ export default defineComponent({
|
||||
> .logo {
|
||||
vertical-align: bottom;
|
||||
max-height: 120px;
|
||||
max-width: min(100%, 300px);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
|
||||
|
||||
export class Storage<T extends StateDef> {
|
||||
public readonly key: string;
|
||||
public readonly keyForLocalStorage: string;
|
||||
|
||||
public readonly def: T;
|
||||
|
||||
@ -19,20 +20,22 @@ export class Storage<T extends StateDef> {
|
||||
public readonly reactiveState: { [K in keyof T]: Ref<T[K]['default']> };
|
||||
|
||||
constructor(key: string, def: T) {
|
||||
this.key = 'pizzax::' + key;
|
||||
this.key = key;
|
||||
this.keyForLocalStorage = 'pizzax::' + key;
|
||||
this.def = def;
|
||||
|
||||
// TODO: indexedDBにする
|
||||
const deviceState = JSON.parse(localStorage.getItem(this.key) || '{}');
|
||||
const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.key + '::' + $i.id) || '{}') : {};
|
||||
const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
|
||||
const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}') : {};
|
||||
const registryCache = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}') : {};
|
||||
|
||||
const state = {};
|
||||
const reactiveState = {};
|
||||
for (const [k, v] of Object.entries(def)) {
|
||||
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
|
||||
state[k] = deviceState[k];
|
||||
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call($i.clientData, k)) {
|
||||
state[k] = $i.clientData[k];
|
||||
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
|
||||
state[k] = registryCache[k];
|
||||
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
|
||||
state[k] = deviceAccountState[k];
|
||||
} else {
|
||||
@ -47,16 +50,27 @@ export class Storage<T extends StateDef> {
|
||||
this.reactiveState = reactiveState as any;
|
||||
|
||||
if ($i) {
|
||||
watch($i, () => {
|
||||
if (_DEV_) console.log('$i updated');
|
||||
|
||||
for (const [k, v] of Object.entries(def)) {
|
||||
if (v.where === 'account' && Object.prototype.hasOwnProperty.call($i!.clientData, k)) {
|
||||
state[k] = $i!.clientData[k];
|
||||
reactiveState[k].value = $i!.clientData[k];
|
||||
// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
|
||||
setTimeout(() => {
|
||||
api('i/registry/get-all', { scope: ['client', this.key] }).then(kvs => {
|
||||
const cache = {};
|
||||
for (const [k, v] of Object.entries(def)) {
|
||||
if (v.where === 'account') {
|
||||
if (Object.prototype.hasOwnProperty.call(kvs, k)) {
|
||||
state[k] = kvs[k];
|
||||
reactiveState[k].value = kvs[k];
|
||||
cache[k] = kvs[k];
|
||||
} else {
|
||||
state[k] = v.default;
|
||||
reactiveState[k].value = v.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
|
||||
});
|
||||
}, 1);
|
||||
|
||||
// TODO: streamingのuser storage updateイベントを監視して更新
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,21 +82,26 @@ export class Storage<T extends StateDef> {
|
||||
|
||||
switch (this.def[key].where) {
|
||||
case 'device': {
|
||||
const deviceState = JSON.parse(localStorage.getItem(this.key) || '{}');
|
||||
const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
|
||||
deviceState[key] = value;
|
||||
localStorage.setItem(this.key, JSON.stringify(deviceState));
|
||||
localStorage.setItem(this.keyForLocalStorage, JSON.stringify(deviceState));
|
||||
break;
|
||||
}
|
||||
case 'deviceAccount': {
|
||||
if ($i == null) break;
|
||||
const deviceAccountState = JSON.parse(localStorage.getItem(this.key + '::' + $i.id) || '{}');
|
||||
const deviceAccountState = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}');
|
||||
deviceAccountState[key] = value;
|
||||
localStorage.setItem(this.key + '::' + $i.id, JSON.stringify(deviceAccountState));
|
||||
localStorage.setItem(this.keyForLocalStorage + '::' + $i.id, JSON.stringify(deviceAccountState));
|
||||
break;
|
||||
}
|
||||
case 'account': {
|
||||
api('i/update-client-setting', {
|
||||
name: key,
|
||||
if ($i == null) break;
|
||||
const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}');
|
||||
cache[key] = value;
|
||||
localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
|
||||
api('i/registry/set', {
|
||||
scope: ['client', this.key],
|
||||
key: key,
|
||||
value: value
|
||||
});
|
||||
break;
|
||||
|
@ -29,6 +29,7 @@ export const router = createRouter({
|
||||
{ path: '/featured', component: page('featured') },
|
||||
{ path: '/docs', component: page('docs') },
|
||||
{ path: '/theme-editor', component: page('theme-editor') },
|
||||
{ path: '/advanced-theme-editor', component: page('advanced-theme-editor') },
|
||||
{ path: '/docs/:doc', component: page('doc'), props: route => ({ doc: route.params.doc }) },
|
||||
{ path: '/explore', component: page('explore') },
|
||||
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
|
||||
@ -80,7 +81,6 @@ export const router = createRouter({
|
||||
{ path: '/miauth/:session', component: page('miauth') },
|
||||
{ path: '/authorize-follow', component: page('follow') },
|
||||
{ path: '/share', component: page('share') },
|
||||
{ path: '/test', component: page('test') },
|
||||
{ path: '/:catchAll(.*)', component: page('not-found') }
|
||||
],
|
||||
// なんかHacky
|
||||
|
@ -1,19 +1,13 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import * as seedrandom from 'seedrandom';
|
||||
import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.';
|
||||
import { Variable, PageVar, envVarsDef, Block, isFnBlock, Fn, HpmlScope, HpmlError } from '.';
|
||||
import { version } from '@/config';
|
||||
import { AiScript, utils, values } from '@syuilo/aiscript';
|
||||
import { createAiScriptEnv } from '../aiscript/api';
|
||||
import { collectPageVars } from '../collect-page-vars';
|
||||
import { initLib } from './lib';
|
||||
import { initHpmlLib, initAiLib } from './lib';
|
||||
import * as os from '@/os';
|
||||
import { markRaw, ref, Ref } from 'vue';
|
||||
|
||||
type Fn = {
|
||||
slots: string[];
|
||||
exec: (args: Record<string, any>) => ReturnType<Hpml['evaluate']>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hpml evaluator
|
||||
*/
|
||||
@ -41,7 +35,7 @@ export class Hpml {
|
||||
if (this.opts.enableAiScript) {
|
||||
this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({
|
||||
storageKey: 'pages:' + this.page.id
|
||||
}), ...initLib(this)}, {
|
||||
}), ...initAiLib(this)}, {
|
||||
in: (q) => {
|
||||
return new Promise(ok => {
|
||||
os.dialog({
|
||||
@ -137,7 +131,7 @@ export class Hpml {
|
||||
}
|
||||
|
||||
@autobind
|
||||
private _interpolate(str: string, scope: Scope) {
|
||||
private _interpolateScope(str: string, scope: HpmlScope) {
|
||||
return str.replace(/{(.+?)}/g, match => {
|
||||
const v = scope.getState(match.slice(1, -1).trim());
|
||||
return v == null ? 'NULL' : v.toString();
|
||||
@ -157,14 +151,14 @@ export class Hpml {
|
||||
}
|
||||
|
||||
for (const v of this.variables) {
|
||||
values[v.name] = this.evaluate(v, new Scope([values]));
|
||||
values[v.name] = this.evaluate(v, new HpmlScope([values]));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
@autobind
|
||||
private evaluate(block: Block, scope: Scope): any {
|
||||
private evaluate(block: Block, scope: HpmlScope): any {
|
||||
if (block.type === null) {
|
||||
return null;
|
||||
}
|
||||
@ -174,11 +168,11 @@ export class Hpml {
|
||||
}
|
||||
|
||||
if (block.type === 'text' || block.type === 'multiLineText') {
|
||||
return this._interpolate(block.value || '', scope);
|
||||
return this._interpolateScope(block.value || '', scope);
|
||||
}
|
||||
|
||||
if (block.type === 'textList') {
|
||||
return this._interpolate(block.value || '', scope).trim().split('\n');
|
||||
return this._interpolateScope(block.value || '', scope).trim().split('\n');
|
||||
}
|
||||
|
||||
if (block.type === 'ref') {
|
||||
@ -197,7 +191,8 @@ export class Hpml {
|
||||
}
|
||||
}
|
||||
|
||||
if (isFnBlock(block)) { // ユーザー関数定義
|
||||
// Define user function
|
||||
if (isFnBlock(block)) {
|
||||
return {
|
||||
slots: block.value.slots.map(x => x.name),
|
||||
exec: (slotArg: Record<string, any>) => {
|
||||
@ -206,7 +201,8 @@ export class Hpml {
|
||||
} as Fn;
|
||||
}
|
||||
|
||||
if (block.type.startsWith('fn:')) { // ユーザー関数呼び出し
|
||||
// Call user function
|
||||
if (block.type.startsWith('fn:')) {
|
||||
const fnName = block.type.split(':')[1];
|
||||
const fn = scope.getState(fnName);
|
||||
const args = {} as Record<string, any>;
|
||||
@ -219,77 +215,9 @@ export class Hpml {
|
||||
|
||||
if (block.args === undefined) return null;
|
||||
|
||||
const date = new Date();
|
||||
const day = `${this.opts.visitor ? this.opts.visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
|
||||
|
||||
const funcs: { [p in keyof typeof funcDefs]: Function } = {
|
||||
not: (a: boolean) => !a,
|
||||
or: (a: boolean, b: boolean) => a || b,
|
||||
and: (a: boolean, b: boolean) => a && b,
|
||||
eq: (a: any, b: any) => a === b,
|
||||
notEq: (a: any, b: any) => a !== b,
|
||||
gt: (a: number, b: number) => a > b,
|
||||
lt: (a: number, b: number) => a < b,
|
||||
gtEq: (a: number, b: number) => a >= b,
|
||||
ltEq: (a: number, b: number) => a <= b,
|
||||
if: (bool: boolean, a: any, b: any) => bool ? a : b,
|
||||
for: (times: number, fn: Fn) => {
|
||||
const result = [];
|
||||
for (let i = 0; i < times; i++) {
|
||||
result.push(fn.exec({
|
||||
[fn.slots[0]]: i + 1
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
add: (a: number, b: number) => a + b,
|
||||
subtract: (a: number, b: number) => a - b,
|
||||
multiply: (a: number, b: number) => a * b,
|
||||
divide: (a: number, b: number) => a / b,
|
||||
mod: (a: number, b: number) => a % b,
|
||||
round: (a: number) => Math.round(a),
|
||||
strLen: (a: string) => a.length,
|
||||
strPick: (a: string, b: number) => a[b - 1],
|
||||
strReplace: (a: string, b: string, c: string) => a.split(b).join(c),
|
||||
strReverse: (a: string) => a.split('').reverse().join(''),
|
||||
join: (texts: string[], separator: string) => texts.join(separator || ''),
|
||||
stringToNumber: (a: string) => parseInt(a),
|
||||
numberToString: (a: number) => a.toString(),
|
||||
splitStrByLine: (a: string) => a.split('\n'),
|
||||
pick: (list: any[], i: number) => list[i - 1],
|
||||
listLen: (list: any[]) => list.length,
|
||||
random: (probability: number) => Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * 100) < probability,
|
||||
rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * (max - min + 1)),
|
||||
randomPick: (list: any[]) => list[Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * list.length)],
|
||||
dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${block.id}`)() * 100) < probability,
|
||||
dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${block.id}`)() * (max - min + 1)),
|
||||
dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${block.id}`)() * list.length)],
|
||||
seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability,
|
||||
seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)),
|
||||
seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)],
|
||||
DRPWPM: (list: string[]) => {
|
||||
const xs = [];
|
||||
let totalFactor = 0;
|
||||
for (const x of list) {
|
||||
const parts = x.split(' ');
|
||||
const factor = parseInt(parts.pop()!, 10);
|
||||
const text = parts.join(' ');
|
||||
totalFactor += factor;
|
||||
xs.push({ factor, text });
|
||||
}
|
||||
const r = seedrandom(`${day}:${block.id}`)() * totalFactor;
|
||||
let stackedFactor = 0;
|
||||
for (const x of xs) {
|
||||
if (r >= stackedFactor && r <= stackedFactor + x.factor) {
|
||||
return x.text;
|
||||
} else {
|
||||
stackedFactor += x.factor;
|
||||
}
|
||||
}
|
||||
return xs[0].text;
|
||||
},
|
||||
};
|
||||
const funcs = initHpmlLib(block, scope, this.opts.randomSeed, this.opts.visitor);
|
||||
|
||||
// Call function
|
||||
const fnName = block.type;
|
||||
const fn = (funcs as any)[fnName];
|
||||
if (fn == null) {
|
||||
@ -299,53 +227,3 @@ export class Hpml {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HpmlError extends Error {
|
||||
public info?: any;
|
||||
|
||||
constructor(message: string, info?: any) {
|
||||
super(message);
|
||||
|
||||
this.info = info;
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, HpmlError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Scope {
|
||||
private layerdStates: Record<string, any>[];
|
||||
public name: string;
|
||||
|
||||
constructor(layerdStates: Scope['layerdStates'], name?: Scope['name']) {
|
||||
this.layerdStates = layerdStates;
|
||||
this.name = name || 'anonymous';
|
||||
}
|
||||
|
||||
@autobind
|
||||
public createChildScope(states: Record<string, any>, name?: Scope['name']): Scope {
|
||||
const layer = [states, ...this.layerdStates];
|
||||
return new Scope(layer, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定した名前の変数の値を取得します
|
||||
* @param name 変数名
|
||||
*/
|
||||
@autobind
|
||||
public getState(name: string): any {
|
||||
for (const later of this.layerdStates) {
|
||||
const state = later[name];
|
||||
if (state !== undefined) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
throw new HpmlError(
|
||||
`No such variable '${name}' in scope '${this.name}'`, {
|
||||
scope: this.layerdStates
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
* Hpml
|
||||
*/
|
||||
|
||||
import autobind from 'autobind-decorator';
|
||||
|
||||
import {
|
||||
faMagic,
|
||||
faSquareRootAlt,
|
||||
@ -27,6 +29,7 @@ import {
|
||||
faCalculator,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faFlag } from '@fortawesome/free-regular-svg-icons';
|
||||
import { Hpml } from './evaluator';
|
||||
|
||||
export type Block<V = any> = {
|
||||
id: string;
|
||||
@ -47,6 +50,11 @@ export type Variable = Block & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Fn = {
|
||||
slots: string[];
|
||||
exec: (args: Record<string, any>) => ReturnType<Hpml['evaluate']>;
|
||||
};
|
||||
|
||||
export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null;
|
||||
|
||||
export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = {
|
||||
@ -137,3 +145,53 @@ export function isLiteralBlock(v: Block) {
|
||||
if (literalDefs[v.type]) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export class HpmlScope {
|
||||
private layerdStates: Record<string, any>[];
|
||||
public name: string;
|
||||
|
||||
constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) {
|
||||
this.layerdStates = layerdStates;
|
||||
this.name = name || 'anonymous';
|
||||
}
|
||||
|
||||
@autobind
|
||||
public createChildScope(states: Record<string, any>, name?: HpmlScope['name']): HpmlScope {
|
||||
const layer = [states, ...this.layerdStates];
|
||||
return new HpmlScope(layer, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定した名前の変数の値を取得します
|
||||
* @param name 変数名
|
||||
*/
|
||||
@autobind
|
||||
public getState(name: string): any {
|
||||
for (const later of this.layerdStates) {
|
||||
const state = later[name];
|
||||
if (state !== undefined) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
throw new HpmlError(
|
||||
`No such variable '${name}' in scope '${this.name}'`, {
|
||||
scope: this.layerdStates
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class HpmlError extends Error {
|
||||
public info?: any;
|
||||
|
||||
constructor(message: string, info?: any) {
|
||||
super(message);
|
||||
|
||||
this.info = info;
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, HpmlError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import * as tinycolor from 'tinycolor2';
|
||||
import Chart from 'chart.js';
|
||||
import { Hpml } from './evaluator';
|
||||
import { values, utils } from '@syuilo/aiscript';
|
||||
import { Block, Fn, HpmlScope } from '.';
|
||||
import * as seedrandom from 'seedrandom';
|
||||
|
||||
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
|
||||
Chart.pluginService.register({
|
||||
@ -16,7 +18,7 @@ Chart.pluginService.register({
|
||||
}
|
||||
});
|
||||
|
||||
export function initLib(hpml: Hpml) {
|
||||
export function initAiLib(hpml: Hpml) {
|
||||
return {
|
||||
'MkPages:updated': values.FN_NATIVE(([callback]) => {
|
||||
hpml.pageVarUpdatedCallback = (callback as values.VFn);
|
||||
@ -122,3 +124,79 @@ export function initLib(hpml: Hpml) {
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
export function initHpmlLib(block: Block, scope: HpmlScope, randomSeed: string, visitor?: any) {
|
||||
|
||||
const date = new Date();
|
||||
const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
|
||||
|
||||
const funcs: Record<string, Function> = {
|
||||
not: (a: boolean) => !a,
|
||||
or: (a: boolean, b: boolean) => a || b,
|
||||
and: (a: boolean, b: boolean) => a && b,
|
||||
eq: (a: any, b: any) => a === b,
|
||||
notEq: (a: any, b: any) => a !== b,
|
||||
gt: (a: number, b: number) => a > b,
|
||||
lt: (a: number, b: number) => a < b,
|
||||
gtEq: (a: number, b: number) => a >= b,
|
||||
ltEq: (a: number, b: number) => a <= b,
|
||||
if: (bool: boolean, a: any, b: any) => bool ? a : b,
|
||||
for: (times: number, fn: Fn) => {
|
||||
const result: any[] = [];
|
||||
for (let i = 0; i < times; i++) {
|
||||
result.push(fn.exec({
|
||||
[fn.slots[0]]: i + 1
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
add: (a: number, b: number) => a + b,
|
||||
subtract: (a: number, b: number) => a - b,
|
||||
multiply: (a: number, b: number) => a * b,
|
||||
divide: (a: number, b: number) => a / b,
|
||||
mod: (a: number, b: number) => a % b,
|
||||
round: (a: number) => Math.round(a),
|
||||
strLen: (a: string) => a.length,
|
||||
strPick: (a: string, b: number) => a[b - 1],
|
||||
strReplace: (a: string, b: string, c: string) => a.split(b).join(c),
|
||||
strReverse: (a: string) => a.split('').reverse().join(''),
|
||||
join: (texts: string[], separator: string) => texts.join(separator || ''),
|
||||
stringToNumber: (a: string) => parseInt(a),
|
||||
numberToString: (a: number) => a.toString(),
|
||||
splitStrByLine: (a: string) => a.split('\n'),
|
||||
pick: (list: any[], i: number) => list[i - 1],
|
||||
listLen: (list: any[]) => list.length,
|
||||
random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${block.id}`)() * 100) < probability,
|
||||
rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${block.id}`)() * (max - min + 1)),
|
||||
randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${block.id}`)() * list.length)],
|
||||
dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${block.id}`)() * 100) < probability,
|
||||
dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${block.id}`)() * (max - min + 1)),
|
||||
dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${block.id}`)() * list.length)],
|
||||
seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability,
|
||||
seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)),
|
||||
seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)],
|
||||
DRPWPM: (list: string[]) => {
|
||||
const xs: any[] = [];
|
||||
let totalFactor = 0;
|
||||
for (const x of list) {
|
||||
const parts = x.split(' ');
|
||||
const factor = parseInt(parts.pop()!, 10);
|
||||
const text = parts.join(' ');
|
||||
totalFactor += factor;
|
||||
xs.push({ factor, text });
|
||||
}
|
||||
const r = seedrandom(`${day}:${block.id}`)() * totalFactor;
|
||||
let stackedFactor = 0;
|
||||
for (const x of xs) {
|
||||
if (r >= stackedFactor && r <= stackedFactor + x.factor) {
|
||||
return x.text;
|
||||
} else {
|
||||
stackedFactor += x.factor;
|
||||
}
|
||||
}
|
||||
return xs[0].text;
|
||||
},
|
||||
};
|
||||
|
||||
return funcs;
|
||||
}
|
||||
|
@ -3,19 +3,14 @@ import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { router } from '@/router';
|
||||
|
||||
export async function search(q?: string | null | undefined) {
|
||||
if (q == null) {
|
||||
const { canceled, result: query } = await os.dialog({
|
||||
title: i18n.locale.search,
|
||||
input: true
|
||||
});
|
||||
export async function search() {
|
||||
const { canceled, result: query } = await os.dialog({
|
||||
title: i18n.locale.search,
|
||||
input: true
|
||||
});
|
||||
if (canceled || query == null || query === '') return;
|
||||
|
||||
if (canceled || query == null || query === '') return;
|
||||
|
||||
q = query;
|
||||
}
|
||||
|
||||
q = q.trim();
|
||||
const q = query.trim();
|
||||
|
||||
if (q.startsWith('@') && !q.includes(' ')) {
|
||||
router.push(`/${q}`);
|
||||
@ -39,7 +34,8 @@ export async function search(q?: string | null | undefined) {
|
||||
date.setHours(23, 59, 59, 999);
|
||||
}
|
||||
|
||||
v.$root.$emit('warp', date);
|
||||
// TODO
|
||||
//v.$root.$emit('warp', date);
|
||||
os.dialog({
|
||||
icon: faHistory,
|
||||
iconOnly: true, autoClose: true
|
||||
|
@ -184,6 +184,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'device',
|
||||
default: true
|
||||
},
|
||||
reportError: {
|
||||
where: 'device',
|
||||
default: false
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
@ -204,7 +208,7 @@ type Plugin = {
|
||||
*/
|
||||
export class ColdDeviceStorage {
|
||||
public static default = {
|
||||
themes: [] as Theme[],
|
||||
themes: [] as Theme[], // TODO: そのうち消す
|
||||
darkTheme: '8050783a-7f63-445a-b270-36d0f6ba1677',
|
||||
lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37',
|
||||
syncDeviceDarkMode: true,
|
||||
|
@ -29,6 +29,7 @@ html {
|
||||
font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
|
||||
line-height: 1.35;
|
||||
text-size-adjust: 100%;
|
||||
tab-size: 2;
|
||||
|
||||
&, * {
|
||||
scrollbar-color: var(--scrollbarHandle) inherit;
|
||||
|
62
src/client/theme-store.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { api } from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { ColdDeviceStorage } from './store';
|
||||
import { Theme } from './scripts/theme';
|
||||
|
||||
const lsCacheKey = $i ? `themes:${$i.id}` : '';
|
||||
|
||||
export function getThemes(): Theme[] {
|
||||
return JSON.parse(localStorage.getItem(lsCacheKey) || '[]');
|
||||
}
|
||||
|
||||
export async function fetchThemes(): Promise<void> {
|
||||
if ($i == null) return;
|
||||
|
||||
try {
|
||||
const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' });
|
||||
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
||||
} catch (e) {
|
||||
if (e.code === 'NO_SUCH_KEY') return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addTheme(theme: Theme): Promise<void> {
|
||||
await fetchThemes();
|
||||
const themes = getThemes().concat(theme);
|
||||
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
|
||||
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
||||
}
|
||||
|
||||
export async function removeTheme(theme: Theme): Promise<void> {
|
||||
const themes = getThemes().filter(t => t.id != theme.id);
|
||||
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
|
||||
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
||||
}
|
||||
|
||||
// TODO: そのうち消す
|
||||
if (ColdDeviceStorage.get('themes').length > 0) {
|
||||
const lsThemes = ColdDeviceStorage.get('themes');
|
||||
let registryThemes;
|
||||
try {
|
||||
registryThemes = await api('i/registry/get', { scope: ['client'], key: 'themes' });
|
||||
} catch (e) {
|
||||
if (e.code === 'NO_SUCH_KEY') {
|
||||
registryThemes = [];
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
const themes = [] as Theme[];
|
||||
for (const theme of lsThemes) {
|
||||
if (themes.some(x => x.id === theme.id)) continue;
|
||||
themes.push(theme);
|
||||
}
|
||||
for (const theme of registryThemes) {
|
||||
if (themes.some(x => x.id === theme.id)) continue;
|
||||
themes.push(theme);
|
||||
}
|
||||
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
|
||||
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
|
||||
ColdDeviceStorage.set('themes', []);
|
||||
}
|
@ -15,11 +15,11 @@
|
||||
focus: ':alpha<0.3<@accent',
|
||||
bg: '#000',
|
||||
acrylicBg: ':alpha<0.5<@bg',
|
||||
fg: '#c7d1d8',
|
||||
fg: '#dadada',
|
||||
fgHighlighted: ':lighten<3<@fg',
|
||||
divider: 'rgba(255, 255, 255, 0.1)',
|
||||
indicator: '@accent',
|
||||
panel: '#000',
|
||||
panel: ':lighten<3<@bg',
|
||||
panelHighlight: ':lighten<3<@panel',
|
||||
panelHeaderBg: ':lighten<3<@panel',
|
||||
panelHeaderFg: '@fg',
|
||||
|
@ -15,11 +15,11 @@
|
||||
focus: ':alpha<0.3<@accent',
|
||||
bg: '#fff',
|
||||
acrylicBg: ':alpha<0.5<@bg',
|
||||
fg: '#5c6a73',
|
||||
fg: '#5f5f5f',
|
||||
fgHighlighted: ':darken<3<@fg',
|
||||
divider: 'rgba(0, 0, 0, 0.1)',
|
||||
indicator: '@accent',
|
||||
panel: '#fff',
|
||||
panel: ':lighten<3<@bg',
|
||||
panelHighlight: ':darken<3<@panel',
|
||||
panelHeaderBg: ':lighten<3<@panel',
|
||||
panelHeaderFg: '@fg',
|
||||
|
@ -14,8 +14,6 @@
|
||||
divider: 'rgba(255, 255, 255, 0.14)',
|
||||
panel: 'rgb(41, 43, 41)',
|
||||
panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)',
|
||||
panelHeaderBg: '@panel',
|
||||
panelHeaderDivider: '@divider',
|
||||
infoFg: '@fg',
|
||||
infoBg: '#333c3b',
|
||||
header: ':alpha<0.7<@bg',
|
||||
|
@ -41,7 +41,7 @@ import { getScrollContainer } from '@/scripts/scroll';
|
||||
import * as os from '@/os';
|
||||
import { sidebarDef } from '@/sidebar';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import { deckStore, addColumn } from './deck/deck-store';
|
||||
import { deckStore, addColumn, loadDeck } from './deck/deck-store';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -88,6 +88,7 @@ export default defineComponent({
|
||||
document.documentElement.style.overflowY = 'hidden';
|
||||
document.documentElement.style.scrollBehavior = 'auto';
|
||||
window.addEventListener('wheel', this.onWheel);
|
||||
loadDeck();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
@ -70,7 +70,6 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
deckStore,
|
||||
active: true,
|
||||
dragging: false,
|
||||
draghover: false,
|
||||
dropready: false,
|
||||
@ -83,6 +82,10 @@ export default defineComponent({
|
||||
return this.column.type === 'main';
|
||||
},
|
||||
|
||||
active(): boolean {
|
||||
return this.column.active !== false;
|
||||
},
|
||||
|
||||
keymap(): any {
|
||||
return {
|
||||
'shift+up': () => this.$parent.$emit('parent-focus', 'up'),
|
||||
@ -124,7 +127,9 @@ export default defineComponent({
|
||||
|
||||
toggleActive() {
|
||||
if (!this.isStacked) return;
|
||||
this.active = !this.active;
|
||||
updateColumn(this.column.id, {
|
||||
active: !this.column.active
|
||||
});
|
||||
},
|
||||
|
||||
getMenu() {
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { throttle } from 'throttle-debounce';
|
||||
import { i18n } from '@/i18n';
|
||||
import { markRaw } from 'vue';
|
||||
import { api } from '@/os';
|
||||
import { markRaw, watch } from 'vue';
|
||||
import { Storage } from '../../pizzax';
|
||||
|
||||
type ColumnWidget = {
|
||||
@ -14,6 +16,7 @@ type Column = {
|
||||
name: string | null;
|
||||
width: number;
|
||||
widgets?: ColumnWidget[];
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
function copy<T>(x: T): T {
|
||||
@ -21,23 +24,17 @@ function copy<T>(x: T): T {
|
||||
}
|
||||
|
||||
export const deckStore = markRaw(new Storage('deck', {
|
||||
profile: {
|
||||
where: 'deviceAccount',
|
||||
default: 'default'
|
||||
},
|
||||
columns: {
|
||||
where: 'deviceAccount',
|
||||
default: [{
|
||||
id: 'a',
|
||||
type: 'main',
|
||||
name: i18n.locale._deck._columns.main,
|
||||
width: 350,
|
||||
}, {
|
||||
id: 'b',
|
||||
type: 'notifications',
|
||||
name: i18n.locale._deck._columns.notifications,
|
||||
width: 330,
|
||||
}] as Column[]
|
||||
default: [] as Column[]
|
||||
},
|
||||
layout: {
|
||||
where: 'deviceAccount',
|
||||
default: [['a'], ['b']] as Column['id'][][]
|
||||
default: [] as Column['id'][][]
|
||||
},
|
||||
columnAlign: {
|
||||
where: 'deviceAccount',
|
||||
@ -61,10 +58,60 @@ export const deckStore = markRaw(new Storage('deck', {
|
||||
},
|
||||
}));
|
||||
|
||||
export const loadDeck = async () => {
|
||||
let deck;
|
||||
|
||||
try {
|
||||
deck = await api('i/registry/get', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
key: deckStore.state.profile,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.code === 'NO_SUCH_KEY') {
|
||||
// 後方互換性のため
|
||||
if (deckStore.state.profile === 'default') {
|
||||
saveDeck();
|
||||
return;
|
||||
}
|
||||
|
||||
deckStore.set('columns', [{
|
||||
id: 'a',
|
||||
type: 'main',
|
||||
name: i18n.locale._deck._columns.main,
|
||||
width: 350,
|
||||
}, {
|
||||
id: 'b',
|
||||
type: 'notifications',
|
||||
name: i18n.locale._deck._columns.notifications,
|
||||
width: 330,
|
||||
}]);
|
||||
deckStore.set('layout', [['a'], ['b']]);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
deckStore.set('columns', deck.columns);
|
||||
deckStore.set('layout', deck.layout);
|
||||
};
|
||||
|
||||
// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
|
||||
export const saveDeck = throttle(1000, () => {
|
||||
api('i/registry/set', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
key: deckStore.state.profile,
|
||||
value: {
|
||||
columns: deckStore.reactiveState.columns.value,
|
||||
layout: deckStore.reactiveState.layout.value,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export function addColumn(column: Column) {
|
||||
if (column.name == undefined) column.name = null;
|
||||
deckStore.push('columns', column);
|
||||
deckStore.push('layout', [column.id]);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function removeColumn(id: Column['id']) {
|
||||
@ -72,6 +119,7 @@ export function removeColumn(id: Column['id']) {
|
||||
deckStore.set('layout', deckStore.state.layout
|
||||
.map(ids => ids.filter(_id => _id !== id))
|
||||
.filter(ids => ids.length > 0));
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapColumn(a: Column['id'], b: Column['id']) {
|
||||
@ -83,6 +131,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
|
||||
layout[aX][aY] = b;
|
||||
layout[bX][bY] = a;
|
||||
deckStore.set('layout', layout);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapLeftColumn(id: Column['id']) {
|
||||
@ -98,6 +147,7 @@ export function swapLeftColumn(id: Column['id']) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapRightColumn(id: Column['id']) {
|
||||
@ -113,6 +163,7 @@ export function swapRightColumn(id: Column['id']) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapUpColumn(id: Column['id']) {
|
||||
@ -132,6 +183,7 @@ export function swapUpColumn(id: Column['id']) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapDownColumn(id: Column['id']) {
|
||||
@ -151,6 +203,7 @@ export function swapDownColumn(id: Column['id']) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function stackLeftColumn(id: Column['id']) {
|
||||
@ -160,6 +213,7 @@ export function stackLeftColumn(id: Column['id']) {
|
||||
layout[i - 1].push(id);
|
||||
layout = layout.filter(ids => ids.length > 0);
|
||||
deckStore.set('layout', layout);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function popRightColumn(id: Column['id']) {
|
||||
@ -169,6 +223,7 @@ export function popRightColumn(id: Column['id']) {
|
||||
layout.splice(i + 1, 0, [id]);
|
||||
layout = layout.filter(ids => ids.length > 0);
|
||||
deckStore.set('layout', layout);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
@ -180,6 +235,7 @@ export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
column.widgets.unshift(widget);
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
@ -190,6 +246,7 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
column.widgets = column.widgets.filter(w => w.id != widget.id);
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
||||
@ -200,6 +257,7 @@ export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
||||
column.widgets = widgets;
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function updateColumnWidget(id: Column['id'], widgetId: string, data: any) {
|
||||
@ -213,6 +271,7 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, data: any
|
||||
} : w);
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
||||
@ -225,4 +284,5 @@ export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
||||
}
|
||||
columns[columnIndex] = currentColumn;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
@ -58,6 +58,7 @@ export default defineComponent({
|
||||
|
||||
onContextmenu(e) {
|
||||
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
const path = this.$route.path;
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
|
@ -188,6 +188,7 @@ export default defineComponent({
|
||||
|
||||
onContextmenu(e) {
|
||||
if (['INPUT', 'TEXTAREA'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
|
||||
if (window.getSelection().toString() !== '') return;
|
||||
const path = this.$route.path;
|
||||
os.contextMenu([{
|
||||
type: 'label',
|
||||
|
@ -14,7 +14,9 @@ export default function(app: App) {
|
||||
app.component('MkwFederation', defineAsyncComponent(() => import('./federation.vue')));
|
||||
app.component('MkwPostForm', defineAsyncComponent(() => import('./post-form.vue')));
|
||||
app.component('MkwSlideshow', defineAsyncComponent(() => import('./slideshow.vue')));
|
||||
app.component('MkwServerMetric', defineAsyncComponent(() => import('./server-metric/index.vue')));
|
||||
app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue')));
|
||||
app.component('MkwJobQueue', defineAsyncComponent(() => import('./job-queue.vue')));
|
||||
app.component('MkwButton', defineAsyncComponent(() => import('./button.vue')));
|
||||
}
|
||||
|
||||
@ -32,6 +34,8 @@ export const widgets = [
|
||||
'federation',
|
||||
'postForm',
|
||||
'slideshow',
|
||||
'serverMetric',
|
||||
'onlineUsers',
|
||||
'jobQueue',
|
||||
'button',
|
||||
];
|
||||
|
159
src/client/widgets/job-queue.vue
Normal file
@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="mkw-jobQueue _monospace" :class="{ _panel: !props.transparent }">
|
||||
<div class="inbox">
|
||||
<div class="label">Inbox queue<Fa :icon="faExclamationTriangle" v-if="inbox.waiting > 0" class="icon"/></div>
|
||||
<div class="values">
|
||||
<div>
|
||||
<div>Process</div>
|
||||
<div>{{ number(inbox.activeSincePrevTick) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>Active</div>
|
||||
<div>{{ number(inbox.active) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>Delayed</div>
|
||||
<div>{{ number(inbox.delayed) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>Waiting</div>
|
||||
<div>{{ number(inbox.waiting) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="deliver">
|
||||
<div class="label">Deliver queue<Fa :icon="faExclamationTriangle" v-if="inbox.waiting > 0" class="icon"/></div>
|
||||
<div class="values">
|
||||
<div>
|
||||
<div>Process</div>
|
||||
<div>{{ number(deliver.activeSincePrevTick) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>Active</div>
|
||||
<div>{{ number(deliver.active) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>Delayed</div>
|
||||
<div>{{ number(deliver.delayed) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>Waiting</div>
|
||||
<div>{{ number(deliver.waiting) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import define from './define';
|
||||
import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
|
||||
const widget = define({
|
||||
name: 'jobQueue',
|
||||
props: () => ({
|
||||
transparent: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export default defineComponent({
|
||||
extends: widget,
|
||||
data() {
|
||||
return {
|
||||
connection: os.stream.useSharedConnection('queueStats'),
|
||||
inbox: {
|
||||
activeSincePrevTick: 0,
|
||||
active: 0,
|
||||
waiting: 0,
|
||||
delayed: 0,
|
||||
},
|
||||
deliver: {
|
||||
activeSincePrevTick: 0,
|
||||
active: 0,
|
||||
waiting: 0,
|
||||
delayed: 0,
|
||||
},
|
||||
faExclamationTriangle,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
|
||||
this.connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 1
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.connection.off('stats', this.onStats);
|
||||
this.connection.off('statsLog', this.onStatsLog);
|
||||
this.connection.dispose();
|
||||
},
|
||||
methods: {
|
||||
onStats(stats) {
|
||||
for (const domain of ['inbox', 'deliver']) {
|
||||
this[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
|
||||
this[domain].active = stats[domain].active;
|
||||
this[domain].waiting = stats[domain].waiting;
|
||||
this[domain].delayed = stats[domain].delayed;
|
||||
}
|
||||
},
|
||||
|
||||
onStatsLog(statsLog) {
|
||||
for (const stats of [...statsLog].reverse()) {
|
||||
this.onStats(stats);
|
||||
}
|
||||
},
|
||||
|
||||
number
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@keyframes warnBlink {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
.mkw-jobQueue {
|
||||
font-size: 0.9em;
|
||||
|
||||
> div {
|
||||
padding: 16px;
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: solid 1px var(--divider);
|
||||
}
|
||||
|
||||
> .label {
|
||||
display: flex;
|
||||
|
||||
> .icon {
|
||||
color: var(--warn);
|
||||
margin-left: auto;
|
||||
animation: warnBlink 1s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
> .values {
|
||||
display: flex;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
> div:first-child {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
174
src/client/widgets/server-metric/cpu-mem.vue
Normal file
@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="lcfyofjk">
|
||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
||||
<defs>
|
||||
<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
|
||||
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
|
||||
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
|
||||
</linearGradient>
|
||||
<mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
|
||||
<polygon
|
||||
:points="cpuPolygonPoints"
|
||||
fill="#fff"
|
||||
fill-opacity="0.5"
|
||||
/>
|
||||
<polyline
|
||||
:points="cpuPolylinePoints"
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<circle
|
||||
:cx="cpuHeadX"
|
||||
:cy="cpuHeadY"
|
||||
r="1.5"
|
||||
fill="#fff"
|
||||
/>
|
||||
</mask>
|
||||
</defs>
|
||||
<rect
|
||||
x="-2" y="-2"
|
||||
:width="viewBoxX + 4" :height="viewBoxY + 4"
|
||||
:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"
|
||||
/>
|
||||
<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
|
||||
</svg>
|
||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
||||
<defs>
|
||||
<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
|
||||
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
|
||||
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
|
||||
</linearGradient>
|
||||
<mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
|
||||
<polygon
|
||||
:points="memPolygonPoints"
|
||||
fill="#fff"
|
||||
fill-opacity="0.5"
|
||||
/>
|
||||
<polyline
|
||||
:points="memPolylinePoints"
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<circle
|
||||
:cx="memHeadX"
|
||||
:cy="memHeadY"
|
||||
r="1.5"
|
||||
fill="#fff"
|
||||
/>
|
||||
</mask>
|
||||
</defs>
|
||||
<rect
|
||||
x="-2" y="-2"
|
||||
:width="viewBoxX + 4" :height="viewBoxY + 4"
|
||||
:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"
|
||||
/>
|
||||
<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
connection: {
|
||||
required: true,
|
||||
},
|
||||
meta: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
viewBoxX: 50,
|
||||
viewBoxY: 30,
|
||||
stats: [],
|
||||
cpuGradientId: uuid(),
|
||||
cpuMaskId: uuid(),
|
||||
memGradientId: uuid(),
|
||||
memMaskId: uuid(),
|
||||
cpuPolylinePoints: '',
|
||||
memPolylinePoints: '',
|
||||
cpuPolygonPoints: '',
|
||||
memPolygonPoints: '',
|
||||
cpuHeadX: null,
|
||||
cpuHeadY: null,
|
||||
memHeadX: null,
|
||||
memHeadY: null,
|
||||
cpuP: '',
|
||||
memP: ''
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
this.connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8)
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.connection.off('stats', this.onStats);
|
||||
this.connection.off('statsLog', this.onStatsLog);
|
||||
},
|
||||
methods: {
|
||||
onStats(stats) {
|
||||
this.stats.push(stats);
|
||||
if (this.stats.length > 50) this.stats.shift();
|
||||
|
||||
const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu) * this.viewBoxY]);
|
||||
const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / this.meta.mem.total)) * this.viewBoxY]);
|
||||
this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
||||
this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
||||
|
||||
this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
|
||||
this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
|
||||
|
||||
this.cpuHeadX = cpuPolylinePoints[cpuPolylinePoints.length - 1][0];
|
||||
this.cpuHeadY = cpuPolylinePoints[cpuPolylinePoints.length - 1][1];
|
||||
this.memHeadX = memPolylinePoints[memPolylinePoints.length - 1][0];
|
||||
this.memHeadY = memPolylinePoints[memPolylinePoints.length - 1][1];
|
||||
|
||||
this.cpuP = (stats.cpu * 100).toFixed(0);
|
||||
this.memP = (stats.mem.used / this.meta.mem.total * 100).toFixed(0);
|
||||
},
|
||||
onStatsLog(statsLog) {
|
||||
for (const stats of [...statsLog].reverse()) {
|
||||
this.onStats(stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lcfyofjk {
|
||||
display: flex;
|
||||
|
||||
> svg {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
width: 50%;
|
||||
|
||||
&:first-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
> text {
|
||||
font-size: 5px;
|
||||
fill: currentColor;
|
||||
|
||||
> tspan {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
78
src/client/widgets/server-metric/cpu.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="vrvdvrys">
|
||||
<XPie class="pie" :value="usage"/>
|
||||
<div>
|
||||
<p><fa :icon="faMicrochip"/>CPU</p>
|
||||
<p>{{ meta.cpu.cores }} Logical cores</p>
|
||||
<p>{{ meta.cpu.model }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faMicrochip } from '@fortawesome/free-solid-svg-icons';
|
||||
import XPie from './pie.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XPie
|
||||
},
|
||||
props: {
|
||||
connection: {
|
||||
required: true,
|
||||
},
|
||||
meta: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
usage: 0,
|
||||
faMicrochip,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.connection.on('stats', this.onStats);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.connection.off('stats', this.onStats);
|
||||
},
|
||||
methods: {
|
||||
onStats(stats) {
|
||||
this.usage = stats.cpu;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vrvdvrys {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
> .pie {
|
||||
height: 82px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
|
||||
> [data-icon] {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
72
src/client/widgets/server-metric/disk.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="zbwaqsat">
|
||||
<XPie class="pie" :value="usage"/>
|
||||
<div>
|
||||
<p><fa :icon="faHdd"/>Disk</p>
|
||||
<p>Total: {{ bytes(total, 1) }}</p>
|
||||
<p>Free: {{ bytes(available, 1) }}</p>
|
||||
<p>Used: {{ bytes(used, 1) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faHdd } from '@fortawesome/free-solid-svg-icons';
|
||||
import XPie from './pie.vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XPie
|
||||
},
|
||||
props: {
|
||||
meta: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
usage: this.meta.fs.used / this.meta.fs.total,
|
||||
total: this.meta.fs.total,
|
||||
used: this.meta.fs.used,
|
||||
available: this.meta.fs.total - this.meta.fs.used,
|
||||
faHdd,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
bytes
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zbwaqsat {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
> .pie {
|
||||
height: 82px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
|
||||
> [data-icon] {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
84
src/client/widgets/server-metric/index.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<MkContainer :show-header="props.showHeader" :naked="props.transparent">
|
||||
<template #header><Fa :icon="faServer"/>{{ $ts._widgets.serverMetric }}</template>
|
||||
<template #func><button @click="toggleView()" class="_button"><Fa :icon="faSort"/></button></template>
|
||||
|
||||
<div class="mkw-serverMetric" v-if="meta">
|
||||
<XCpuMemory v-if="props.view === 0" :connection="connection" :meta="meta"/>
|
||||
<XNet v-if="props.view === 1" :connection="connection" :meta="meta"/>
|
||||
<XCpu v-if="props.view === 2" :connection="connection" :meta="meta"/>
|
||||
<XMemory v-if="props.view === 3" :connection="connection" :meta="meta"/>
|
||||
<XDisk v-if="props.view === 4" :connection="connection" :meta="meta"/>
|
||||
</div>
|
||||
</MkContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faServer, faSort } from '@fortawesome/free-solid-svg-icons';
|
||||
import define from '../define';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import XCpuMemory from './cpu-mem.vue';
|
||||
import XNet from './net.vue';
|
||||
import XCpu from './cpu.vue';
|
||||
import XMemory from './mem.vue';
|
||||
import XDisk from './disk.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
const widget = define({
|
||||
name: 'serverMetric',
|
||||
props: () => ({
|
||||
showHeader: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
transparent: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
view: {
|
||||
type: 'number',
|
||||
default: 0,
|
||||
hidden: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export default defineComponent({
|
||||
extends: widget,
|
||||
components: {
|
||||
MkContainer,
|
||||
XCpuMemory,
|
||||
XNet,
|
||||
XCpu,
|
||||
XMemory,
|
||||
XDisk,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
meta: null,
|
||||
connection: null,
|
||||
faServer, faSort,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
os.api('server-info', {}).then(res => {
|
||||
this.meta = res;
|
||||
});
|
||||
this.connection = os.stream.useSharedConnection('serverStats');
|
||||
},
|
||||
unmounted() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
methods: {
|
||||
toggleView() {
|
||||
if (this.props.view == 4) {
|
||||
this.props.view = 0;
|
||||
} else {
|
||||
this.props.view++;
|
||||
}
|
||||
this.save();
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
87
src/client/widgets/server-metric/mem.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="zlxnikvl">
|
||||
<XPie class="pie" :value="usage"/>
|
||||
<div>
|
||||
<p><fa :icon="faMemory"/>RAM</p>
|
||||
<p>Total: {{ bytes(total, 1) }}</p>
|
||||
<p>Used: {{ bytes(used, 1) }}</p>
|
||||
<p>Free: {{ bytes(free, 1) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { faMemory } from '@fortawesome/free-solid-svg-icons';
|
||||
import XPie from './pie.vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XPie
|
||||
},
|
||||
props: {
|
||||
connection: {
|
||||
required: true,
|
||||
},
|
||||
meta: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
usage: 0,
|
||||
total: 0,
|
||||
used: 0,
|
||||
free: 0,
|
||||
faMemory,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.connection.on('stats', this.onStats);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.connection.off('stats', this.onStats);
|
||||
},
|
||||
methods: {
|
||||
onStats(stats) {
|
||||
this.usage = stats.mem.used / this.meta.mem.total;
|
||||
this.total = this.meta.mem.total;
|
||||
this.used = stats.mem.used;
|
||||
this.free = this.meta.mem.total - stats.mem.used;
|
||||
},
|
||||
bytes
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zlxnikvl {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
> .pie {
|
||||
height: 82px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
|
||||
&:first-child {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
|
||||
> [data-icon] {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
148
src/client/widgets/server-metric/net.vue
Normal file
@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="oxxrhrto">
|
||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
||||
<polygon
|
||||
:points="inPolygonPoints"
|
||||
fill="#94a029"
|
||||
fill-opacity="0.5"
|
||||
/>
|
||||
<polyline
|
||||
:points="inPolylinePoints"
|
||||
fill="none"
|
||||
stroke="#94a029"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<circle
|
||||
:cx="inHeadX"
|
||||
:cy="inHeadY"
|
||||
r="1.5"
|
||||
fill="#94a029"
|
||||
/>
|
||||
<text x="1" y="5">NET rx <tspan>{{ bytes(inRecent) }}</tspan></text>
|
||||
</svg>
|
||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
||||
<polygon
|
||||
:points="outPolygonPoints"
|
||||
fill="#ff9156"
|
||||
fill-opacity="0.5"
|
||||
/>
|
||||
<polyline
|
||||
:points="outPolylinePoints"
|
||||
fill="none"
|
||||
stroke="#ff9156"
|
||||
stroke-width="1"
|
||||
/>
|
||||
<circle
|
||||
:cx="outHeadX"
|
||||
:cy="outHeadY"
|
||||
r="1.5"
|
||||
fill="#ff9156"
|
||||
/>
|
||||
<text x="1" y="5">NET tx <tspan>{{ bytes(outRecent) }}</tspan></text>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
connection: {
|
||||
required: true,
|
||||
},
|
||||
meta: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
viewBoxX: 50,
|
||||
viewBoxY: 30,
|
||||
stats: [],
|
||||
inPolylinePoints: '',
|
||||
outPolylinePoints: '',
|
||||
inPolygonPoints: '',
|
||||
outPolygonPoints: '',
|
||||
inHeadX: null,
|
||||
inHeadY: null,
|
||||
outHeadX: null,
|
||||
outHeadY: null,
|
||||
inRecent: 0,
|
||||
outRecent: 0
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
this.connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8)
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.connection.off('stats', this.onStats);
|
||||
this.connection.off('statsLog', this.onStatsLog);
|
||||
},
|
||||
methods: {
|
||||
onStats(stats) {
|
||||
this.stats.push(stats);
|
||||
if (this.stats.length > 50) this.stats.shift();
|
||||
|
||||
const inPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.rx)));
|
||||
const outPeak = Math.max(1024 * 64, Math.max(...this.stats.map(s => s.net.tx)));
|
||||
|
||||
const inPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.rx / inPeak)) * this.viewBoxY]);
|
||||
const outPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.net.tx / outPeak)) * this.viewBoxY]);
|
||||
this.inPolylinePoints = inPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
||||
this.outPolylinePoints = outPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
||||
|
||||
this.inPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.inPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
|
||||
this.outPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.outPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
|
||||
|
||||
this.inHeadX = inPolylinePoints[inPolylinePoints.length - 1][0];
|
||||
this.inHeadY = inPolylinePoints[inPolylinePoints.length - 1][1];
|
||||
this.outHeadX = outPolylinePoints[outPolylinePoints.length - 1][0];
|
||||
this.outHeadY = outPolylinePoints[outPolylinePoints.length - 1][1];
|
||||
|
||||
this.inRecent = stats.net.rx;
|
||||
this.outRecent = stats.net.tx;
|
||||
},
|
||||
onStatsLog(statsLog) {
|
||||
for (const stats of [...statsLog].reverse()) {
|
||||
this.onStats(stats);
|
||||
}
|
||||
},
|
||||
bytes
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.oxxrhrto {
|
||||
display: flex;
|
||||
|
||||
> svg {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
width: 50%;
|
||||
|
||||
&:first-child {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
> text {
|
||||
font-size: 5px;
|
||||
fill: currentColor;
|
||||
|
||||
> tspan {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
65
src/client/widgets/server-metric/pie.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<svg class="hsalcinq" viewBox="0 0 1 1" preserveAspectRatio="none">
|
||||
<circle
|
||||
:r="r"
|
||||
cx="50%" cy="50%"
|
||||
fill="none"
|
||||
stroke-width="0.1"
|
||||
stroke="rgba(0, 0, 0, 0.05)"
|
||||
/>
|
||||
<circle
|
||||
:r="r"
|
||||
cx="50%" cy="50%"
|
||||
:stroke-dasharray="Math.PI * (r * 2)"
|
||||
:stroke-dashoffset="strokeDashoffset"
|
||||
fill="none"
|
||||
stroke-width="0.1"
|
||||
:stroke="color"
|
||||
/>
|
||||
<text x="50%" y="50%" dy="0.05" text-anchor="middle">{{ (value * 100).toFixed(0) }}%</text>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
r: 0.45
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
color(): string {
|
||||
return `hsl(${180 - (this.value * 180)}, 80%, 70%)`;
|
||||
},
|
||||
strokeDashoffset(): number {
|
||||
return (1 - this.value) * (Math.PI * (this.r * 2));
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hsalcinq {
|
||||
display: block;
|
||||
height: 100%;
|
||||
|
||||
> circle {
|
||||
transform-origin: center;
|
||||
transform: rotate(-90deg);
|
||||
transition: stroke-dashoffset 0.5s ease;
|
||||
}
|
||||
|
||||
> text {
|
||||
font-size: 0.15px;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -6,6 +6,9 @@ const ev = new Xev();
|
||||
|
||||
const interval = 2000;
|
||||
|
||||
const roundCpu = (num: number) => Math.round(num * 1000) / 1000;
|
||||
const round = (num: number) => Math.round(num * 10) / 10;
|
||||
|
||||
/**
|
||||
* Report server stats regularly
|
||||
*/
|
||||
@ -23,18 +26,18 @@ export default function() {
|
||||
const fsStats = await fs();
|
||||
|
||||
const stats = {
|
||||
cpu: cpu,
|
||||
cpu: roundCpu(cpu),
|
||||
mem: {
|
||||
used: memStats.used,
|
||||
active: memStats.active,
|
||||
used: round(memStats.used),
|
||||
active: round(memStats.active),
|
||||
},
|
||||
net: {
|
||||
rx: Math.max(0, netStats.rx_sec),
|
||||
tx: Math.max(0, netStats.tx_sec),
|
||||
rx: round(Math.max(0, netStats.rx_sec)),
|
||||
tx: round(Math.max(0, netStats.tx_sec)),
|
||||
},
|
||||
fs: {
|
||||
r: Math.max(0, fsStats.rIO_sec),
|
||||
w: Math.max(0, fsStats.wIO_sec),
|
||||
r: round(Math.max(0, fsStats.rIO_sec)),
|
||||
w: round(Math.max(0, fsStats.wIO_sec)),
|
||||
}
|
||||
};
|
||||
ev.emit('serverStats', stats);
|
||||
|
@ -63,6 +63,7 @@ import { MutedNote } from '../models/entities/muted-note';
|
||||
import { Channel } from '../models/entities/channel';
|
||||
import { ChannelFollowing } from '../models/entities/channel-following';
|
||||
import { ChannelNotePining } from '../models/entities/channel-note-pining';
|
||||
import { RegistryItem } from '../models/entities/registry-item';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||
|
||||
@ -159,6 +160,7 @@ export const entities = [
|
||||
Channel,
|
||||
ChannelFollowing,
|
||||
ChannelNotePining,
|
||||
RegistryItem,
|
||||
...charts as any
|
||||
];
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# AiScript
|
||||
|
||||
## 関数
|
||||
## وظائف
|
||||
デフォルトで値渡しです。
|
||||
|
@ -1,15 +1,15 @@
|
||||
# タイムラインの比較
|
||||
# مقارنة الجدول الزمني
|
||||
|
||||
https://docs.google.com/spreadsheets/d/1lxQ2ugKrhz58Bg96HTDK_2F98BUritkMyIiBkOByjHA/edit?usp=sharing
|
||||
|
||||
## الرئيسي
|
||||
自分のフォローしているユーザーの投稿
|
||||
## الواجهة الرئيسية
|
||||
مشاركات المستخدمين الذين تتابعهم
|
||||
|
||||
## المحلي
|
||||
全てのローカルユーザーの「ホーム」指定されていない投稿
|
||||
جميع مشاركات المستخدمين المحليين التي لم يتم وضع علامة عليها على أنها "صفحة رئيسية فقط "
|
||||
|
||||
## الاجتماعي
|
||||
自分のフォローしているユーザーの投稿と、全てのローカルユーザーの「ホーム」指定されていない投稿
|
||||
## إجتماعي
|
||||
مشاركات المستخدمين الذين تتابعهم بالإضافة إلى جميع مشاركات المستخدمين المحليين التي لم يتم وضع علامة عليها كـ "الصفحة الرئيسية فقط"
|
||||
|
||||
## الشامل
|
||||
## عالمي
|
||||
全てのローカルユーザーの「ホーム」指定されていない投稿と、サーバーに届いた全てのリモートユーザーの「ホーム」指定されていない投稿
|
||||
|
@ -1,4 +1,4 @@
|
||||
# AiScript
|
||||
|
||||
## Funktionen
|
||||
デフォルトで値渡しです。
|
||||
Verwenden für Parameter standardmäßig Pass-by-Value (Wertparameter).
|
||||
|