Merge branch 'notification-read-api' into swn

This commit is contained in:
tamaina 2021-12-05 12:47:24 +09:00
commit 4a8dfbdf2d
95 changed files with 1935 additions and 1162 deletions

View File

@ -10,19 +10,26 @@
-->
## 12.x.x (unreleased)
## 12.98.0 (2021/12/03)
### Improvements
- API: /antennas/notes API で日付による絞り込みができるように
- クライアント: アンケートに投票する際に確認ダイアログを出すように
- クライアント: Renoteなート詳細ページから元のートページに遷移できるように
- クライアント: 画像ポップアップでクリックで閉じられるように
- クライアント: デザインの調整
- フォロワーを解除できる機能
### Bugfixes
- クライアント: LTLやGTLが無効になっている場合でもUI上にタブが表示される問題を修正
- クライアント: ログインにおいてパスワードが誤っている際のエラーメッセージが正しく表示されない問題を修正
- クライアント: リアクションツールチップ、Renoteツールチップのユーザーの並び順を修正
- クライアント: サウンドのマスターボリュームが正しく保存されない問題を修正
- クライアント: 一部環境において通知が表示されると操作不能になる問題を修正
- クライアント: モバイルでタップしたときにツールチップが表示される問題を修正
- クライアント: リモートインスタンスのノートに返信するとき、対象のノートにそのリモートインスタンス内のユーザーへのメンションが含まれていると、返信テキスト内にローカルユーザーへのメンションとして引き継がれてしまう場合がある問題を修正
- クライアント: 画像ビューワーで全体表示した時に上側の一部しか表示されない画像がある問題を修正
- API: ユーザーを取得時に条件によっては内部エラーになる問題を修正
### Changes
- クライアント: ノートにモデレーターバッジを表示するのを廃止

View File

@ -734,7 +734,10 @@ translate: "ترجم"
translatedFrom: "تُرجم من {x}"
accountDeletionInProgress: "حذف الحساب جارٍ"
usernameInfo: "الاسم الذي يميزك عن بافي مستخدمي هذا الخادم، يمكنك استخدام الحروف اللاتينية (a~z, A~Z) والأرقام (0~9) والشرطة السفلية (_). لا يمكنك تغييره بعد تسجيله."
keepCw: "أبقِ على تحذيرات المحتوى"
lastCommunication: "آخر تواصل"
resolved: "عولج"
unresolved: "لم يعالج"
itsOn: "مفعّل"
itsOff: "معطّل"
emailRequiredForSignup: "عنوان البريد الإلكتروني إلزامي للتسجيل"
@ -747,6 +750,16 @@ makeReactionsPublicDescription: "هذا سيجعل قائمة تفاعلاتك
classic: "تقليدي"
muteThread: "اكتم النقاش"
unmuteThread: "ارفع الكتم عن النقاش"
deleteAccountConfirm: "سيحذف حسابك نهائيًا، أتريد المتابعة؟"
incorrectPassword: "كلمة السر خاطئة."
_emailUnavailable:
used: "هذا البريد الإلكتروني مستخدم"
format: "صيغة البريد الإلكتروني غير صالحة"
mx: "خادم البريد الإلكتروني غير صالح"
smtp: "خادم البريد الإلكتروتي لا يستجيب"
_ffVisibility:
public: "علني"
private: "خاص"
_signup:
almostThere: "كدت تنتهي"
emailAddressInfo: "رجاءً أدخل بريدك الإلكتروني."
@ -829,6 +842,7 @@ _mfm:
font: "الخط"
rainbow: "قوس قزح"
rainbowDescription: "اجعل المحتوى يظهر بألوان الطيف"
rotate: "تدوير"
_reversi:
gameSettings: "إعدادات اللعبة"
chooseBoard: "اختر اللوح"
@ -980,9 +994,13 @@ _tutorial:
step7_2: "إذا أردت معرفة المزيد عن ميسكي زر {help}."
step7_3: "حظًا سعيدًا واستمتع بوقتك مع ميسكي! 🚀"
_2fa:
alreadyRegistered: "سجلت سلفًا جهازًا للاستيثاق بعاملين."
registerDevice: "سجّل جهازًا جديدًا"
registerKey: "تسجيل مفتاح أمان جديد"
step1: "أولًا ثبّت تطبيق استيثاق على جهازك (مثل {a} و{b})."
step2: "امسح رمز الاستجابة السريعة الموجد على الشاشة."
step3: "أدخل الرمز الموجود في تطبيقك لإكمال التثبيت."
step4: "من هذه اللحظة أثناء ولوجك سيُطلب منك الرمز."
_permissions:
"read:account": "اعرض معلومات حسابك"
"write:account": "تعديل معلومات حسابك"
@ -993,6 +1011,7 @@ _permissions:
"read:favorites": "اعرض المفضلة"
"write:favorites": "عدّل المفضلة"
"read:following": "اعرض معلومات متابَعيك"
"write:following": "تابع أو ألغ متابعة حسابات"
"read:messaging": "اعرض المحادثات"
"write:messaging": "اكتب أو احذف رسائل محادثة"
"read:mutes": "اعرض قائمة المستخدمين المكتومين"
@ -1005,11 +1024,14 @@ _permissions:
"write:votes": "صوّت"
"read:pages": "اعرض صفحاتك"
"write:pages": "عدّل أو احذف صفحاتك"
"read:page-likes": "يعرض ما أعجبك من ملاحظات في صفحات"
"read:user-groups": "اعرض فِرق المستخدمين"
"write:user-groups": "عدّل أو احذف فِرق المستخدمين"
"read:channels": "طالع قنواتك"
"write:channels": "عدّل القنوات"
"read:gallery": "اعرض المعرض"
"write:gallery": "عدّل المعرض"
"read:gallery-likes": "يعرض ما أعجبك من مشاركات المعرض"
_auth:
shareAccess: "أتريد التفويض لـ \"{name}\" بالوصول لحسابك؟"
shareAccessAsk: "هل تخول لهذا التطبيق الوصول لحسابك؟"
@ -1173,6 +1195,7 @@ _rooms:
tv: "تلفاز"
pinguin: "بطريق"
sofa: "أريكة"
bin: "سلة مهملات"
banknote: "أوراق نقدية"
_pages:
newPage: "أنشئ صفحة جديدة"
@ -1212,6 +1235,7 @@ _pages:
name: "اسم المتغير"
text: "العنوان"
default: "القيمة الافتراضية"
textareaInput: "مدخل نصي متعدد الأسطر"
_textareaInput:
name: "اسم المتغير"
text: "العنوان"
@ -1227,6 +1251,7 @@ _pages:
note: "ملاحظة مضمّنة"
_note:
id: "معرّف الملاحظة"
idDescription: "كبديل يمكنك إدخال رابك الملاحظة هنا"
detailed: "عرض مفصّل"
switch: "بدّل"
_switch:

View File

@ -792,6 +792,7 @@ pubSub: "Pub/Sub Benutzerkonten"
lastCommunication: "Letzte Kommunikation"
resolved: "Gelöst"
unresolved: "Ungelöst"
breakFollow: "Follower entfernen"
itsOn: "Eingeschaltet"
itsOff: "Ausgeschaltet"
emailRequiredForSignup: "Angaben einer Email-Adresse als benötigt markieren"
@ -808,6 +809,8 @@ ffVisibility: "Sichtbarkeit von Gefolgten/Followern"
ffVisibilityDescription: "Konfiguriere wer sehen kann, wem du folgst sowie wer dir folgt."
continueThread: "Weiteren Threadverlauf anzeigen"
deleteAccountConfirm: "Dein Benutzerkonto wird unwiderruflich gelöscht. Trotzdem fortfahren?"
incorrectPassword: "Falsches Passwort."
voteConfirm: "Wirklich für \"{choice}\" abstimmen?"
_emailUnavailable:
used: "Diese Email-Adresse wird bereits verwendet"
format: "Das Format dieser Email-Adresse ist ungültig"
@ -931,6 +934,8 @@ _mfm:
rainbowDescription: "Lässt den Inhalt in Regenbogenfarben erscheinen."
sparkle: "Glitzer"
sparkleDescription: "Verleiht Inhalt einen glitzernden Partikeleffekt."
rotate: "Drehen"
rotateDescription: "Dreht den Inhalt um einen angegebenen Winkel"
_reversi:
reversi: "Reversi"
gameSettings: "Spieleinstellungen"

View File

@ -808,6 +808,8 @@ ffVisibility: "Follows/Followers Visibility"
ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you."
continueThread: "View thread continuation"
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
incorrectPassword: "Incorrect password."
voteConfirm: "Confirm your vote for \"{choice}\"?"
_emailUnavailable:
used: "This email address is already being used"
format: "The format of this email address is invalid"
@ -932,7 +934,7 @@ _mfm:
sparkle: "Sparkle"
sparkleDescription: "Gives content a sparkling particle effect."
rotate: "Rotate"
rotateDescription: "Rotates the content by 90 degrees"
rotateDescription: "Turns content by a specified angle."
_reversi:
reversi: "Reversi"
gameSettings: "Game settings"

View File

@ -2,18 +2,18 @@
_lang_: "Esperanto"
headlineMisskey: "Jen la reto konektata de notoj"
introMisskey: "Bonvenon! Misskey estas malfermitkoda malcentraliza etbloga servo.\nKreu \"noto\"n por paroli vian penson al iuj ĉirkaŭ vi. 📡\nLa funkcion \"reago\" ebligas esprimi rapide vian senton pri ies noto en Fediverso. 👍\nBonvole esploru novan mondon. 🚀"
monthAndDay: "La {day}a de la {month}a"
monthAndDay: "la {day}a de la {month}a"
search: "Serĉi"
notifications: "Sciigoj"
username: "Uzantnomo"
password: "Pasvorto"
forgotPassword: "Ĉu vi forgesis pasvorton?"
fetchingAsApObject: "Informpetado de kunfederaĵo…"
ok: "Akcepteble"
gotIt: "Mi komprenas"
fetchingAsApObject: "Informpetado de la Fediverso…"
ok: "OK"
gotIt: "Kompreni"
cancel: "Nuligi"
enterUsername: "Entajpu uzantnomon"
renotedBy: "Noto plusendita de {user}"
renotedBy: "Plusendita de {user}"
noNotes: "Neniu noto!"
noNotifications: "Vi ne havas sciigojn."
instance: "Nodo"
@ -35,22 +35,22 @@ addUser: "Aldoni uzanton"
favorite: "Preferi"
favorites: "Preferaĵoj"
unfavorite: "Malpreferi"
favorited: "Aldonita al via listo de preferaĵoj."
alreadyFavorited: "Jam aldonita al via listo de preferaĵoj."
cantFavorite: "Ĝi ne povis esti aldonita al via listo de preferaĵoj."
favorited: "Aldonita al viaj preferaĵoj."
alreadyFavorited: "Jam aldonita al viaj preferaĵoj."
cantFavorite: "Oni ne povis aldoni al viaj preferaĵoj."
pin: "Alpingli"
unpin: "Depingli"
copyContent: "Kopii enhavon"
copyLink: "Kopii ligilon"
delete: "Forviŝi"
deleteAndEdit: "Forviŝi kaj redakti"
deleteAndEditConfirm: "Ĉu vi certas ke vi volas redakti forviŝinte la noton? Tio forviŝos ankaŭ ĉiujn reagojn, plusendojn, kaj respondojn apartenantajn al ĝi."
deleteAndEditConfirm: "Ĉu vi certas ke vi volas redakti foriginte la noton? Tio forviŝos reagojn, plusendojn, kaj respondojn ĉiujn apartenantajn al ĝi."
addToList: "Aldoni al listo"
sendMessage: "Sendi mesaĝon"
copyUsername: "Kopii uzantnomon"
searchUser: "Serĉi uzanton"
reply: "Respondi"
loadMore: "Vidu pli"
loadMore: "Vidi pli"
showMore: "Vidi pli"
youGotNewFollower: "eksekvis vin"
receiveFollowRequest: "Peto de sekvado estas ricevita"
@ -77,10 +77,11 @@ manageLists: "Administri liston"
error: "Eraro"
somethingHappened: "Problemo okazis"
retry: "Provi denove"
serverIsDead: "La servilo ne respondas. Vole atendu iom kaj penu denove."
enterListName: "Entajpu nomon de la listo"
privacy: "Privateco"
makeFollowManuallyApprove: "Eksekvi vin devas peti al vi"
defaultNoteVisibility: "Implicitaĵo de videbleco"
defaultNoteVisibility: "Implicita videbleco de la noto"
follow: "Sekvi"
followRequest: "Peti de sekvado"
followRequests: "Petoj de sekvado"
@ -88,10 +89,10 @@ unfollow: "Ne plu sekvi"
followRequestPending: "Atendado akcepti vian peton de eksekvado"
enterEmoji: "Entajpu emoĵion"
renote: "Plusendi la noton"
unrenote: "Malfari plusendadon"
unrenote: "Malfari plusendon"
renoted: "Sukcese plusendita"
cantRenote: "Oni ne povas plusendi la noton."
cantReRenote: "Plusendo de noto ne estas plusendebla."
cantReRenote: "Plusendo ne estas plusendebla."
quote: "Citi"
pinnedNote: "Alpinglita noto"
pinned: "Alpingli"
@ -101,7 +102,7 @@ sensitive: "Enhavo ne estas deca por laborejo (NSFW)"
add: "Aldoni"
reaction: "Reagoj"
reactionSettingDescription: "Agordi la reagojn kiujn vi volas prefere montrigi ĉe la elektilo de reagoj"
rememberNoteVisibility: "Rememori la agordon de videbleco de la noto laste sendita "
rememberNoteVisibility: "Rememori la agordon de videbleco de la laste sendita"
attachCancel: "Deigi aldonaĵon"
markAsSensitive: "Troviĝi NSFW"
unmarkAsSensitive: "Ne troviĝi NSFW"
@ -121,16 +122,16 @@ selectAntenna: "Elekti antenon"
selectWidget: "Elekti enestraĵon"
editWidgets: "Redakti fenestraĵon"
editWidgetsExit: "Fini la redaktadon"
customEmojis: "Personecigitaj emoĵioj"
emoji: "Emoĵio"
emojis: "Emoĵio"
emojiName: "Nomo de emoĵio"
emojiName: "Nomo de la emoĵio"
emojiUrl: "URL de la emoĵio"
addEmoji: "Aldoni emoĵion"
settingGuide: "Agordaj rekomendoj"
cacheRemoteFiles: "Stapli transajn dosierojn"
flagAsBot: "Agordo por robota uzanto"
flagAsCat: "Agi kat-iĝon"
cacheRemoteFiles: "Stapli forajn dosierojn"
flagAsBot: "Fari la flagon por robota uzanto"
flagAsCat: "Fari la flagon por kat-iĝi"
autoAcceptFollowed: "Aŭtomate akcepti la peton de sekvado far uzantoj kiujn vi sekvas"
addAccount: "Aldoni konton"
showOnRemote: "Vidi ĉe la surloka nodo"
general: "Ĝenerala"
@ -140,7 +141,7 @@ removeWallpaper: "Forviŝi ekranfonon. "
searchWith: "Serĉi: {q}"
youHaveNoLists: "Vi ne havas listojn."
followConfirm: "Ĉu vi certas ke vi volas sekvi {name}'(o)n?"
host: "Gastigo"
host: "Nodo"
selectUser: "Elekti uzanton"
recipient: "Ricevonto"
annotation: "Komentarioj"
@ -164,8 +165,9 @@ disk: "Disko"
instanceInfo: "Informoj pri la nodo"
statistics: "Statistikoj"
clearCachedFiles: "Malplenigi la staplon"
clearCachedFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn transajn dosierojn en la staplo?"
clearCachedFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn forajn dosierojn en la staplo?"
blockedInstances: "Blokitaj nodoj"
muteAndBlock: "Silentigi / Bloki"
mutedUsers: "Silentigitaj uzantoj"
blockedUsers: "Blokitaj uzantoj"
noUsers: "Neniu uzanto"
@ -175,7 +177,7 @@ pinLimitExceeded: "Vi ne povas alpingli pli"
done: "Fini"
processing: "Prilaborado…"
preview: "Antaŭmontro"
default: "Defaŭlta"
default: "Implicitaĵo"
noCustomEmojis: "Neniu emoĵio"
noJobs: "Neniu laboro"
federating: "Federantaj"
@ -195,7 +197,7 @@ currentPassword: "Aktuala pasvorto"
newPassword: "Nova pasvorto"
newPasswordRetype: "Reentajpu la novan pasvorton"
attachFile: "Aldoni dosieron"
more: "Plu!"
more: "Pli!"
featured: "Maksimumi"
usernameOrUserId: "Uzantnomo aŭ identigilo de uzanto"
noSuchUser: "Neniuj uzantoj trovitaj"
@ -204,8 +206,8 @@ announcements: "Novaĵoj"
imageUrl: "URL de la bildo"
remove: "Forigi"
removed: "Forigita"
removeAreYouSure: "Ĉu vi certas ke vi volas forigi \"{x}\"'(o)n?"
deleteAreYouSure: "Ĉu vi certas ke vi volas forviŝi \"{x}\"'(o)n?"
removeAreYouSure: "Ĉu vi certas ke vi volas forigi \"{x}\"n?"
deleteAreYouSure: "Ĉu vi certas ke vi volas forviŝi \"{x}\"'?"
resetAreYouSure: "Ĉu vi certas restarigi?"
saved: "Konservita"
messaging: "Retbabili"
@ -225,13 +227,13 @@ agreeTo: "Mi akceptas {0}'(o)n"
tos: "Kondiĉoj de uzado"
start: "Komenciĝi"
home: "Hejma"
remoteUserCaution: "Ĉi tiuj infomoj estas ne tute ekzaktaj pro transa uzanto."
remoteUserCaution: "Ĉi tiuj infomoj de la uzanto el fora nodo, ne estas tute ekzaktaj."
activity: "Aktiveco"
images: "Bildoj"
birthday: "Naskiĝdato"
yearsOld: "{age} jaroj aĝa"
registeredDate: "Dato de registriĝo"
location: "Loko"
location: "Kie"
theme: "Koloraro"
themeForLightMode: "Luma kolararo en la luma modo"
themeForDarkMode: "Malluma kolararo en la malluma modo"
@ -253,7 +255,7 @@ deleteFolder: "Forviŝi dosierujon"
addFile: "Aldoni dosieron"
emptyDrive: "La disko malplenas"
emptyFolder: "La dosierujo malplenas"
unableToDelete: "Ne forigebla"
unableToDelete: "Ne forviŝebla"
inputNewFileName: "Entajpu novan nomon de la dosiero"
inputNewDescription: "Entajpu novan priskribon"
inputNewFolderName: "Entajpu novan nomon de la dosierujo"
@ -266,9 +268,11 @@ nsfw: "Enhavo ne estas deca por laborejo (NSFW)"
disconnectedFromServer: "Malkonektita de servilo"
reload: "Reŝargi"
doNothing: "Ignori"
reloadConfirm: "Ĉu vi volas reŝargi?"
watch: "Observi"
unwatch: "Malobservi"
accept: "Permesi"
reject: "Malakcepti"
normal: "Normala"
instanceName: "Nomo de la nodo"
instanceDescription: "Priskribo de la nodo "
@ -291,20 +295,22 @@ registration: "Registri"
enableRegistration: "Ebligi novan uzanton registriĝon"
invite: "Inviti"
driveCapacityPerLocalAccount: "Volumo de disko po unu loka uzanto"
driveCapacityPerRemoteAccount: "Volumo de disko po unu transa uzanto"
driveCapacityPerRemoteAccount: "Volumo de disko po unu fora uzanto"
iconUrl: "URL de la ikono (retpaĝsimbolo, ktp)"
bannerUrl: "URL de standardo"
backgroundImageUrl: "URL de fona bildo"
basicInfo: "Baza informo"
pinnedUsers: "Alpinglita uzanto"
pinnedUsersDescription: "Listigu uzantnomojn apartige en ĉiu linio por alpingli al la paĝoj ekz \"Esplori\"."
pinnedPages: "Alpinglitaj paĝoj"
pinnedPagesDescription: "Listigu dosierindiko apartige en ĉiu linio por alpingli al la ĉefpaĝo de la nodo."
pinnedNotes: "Alpinglita noto"
hcaptcha: "hCaptcha"
enableHcaptcha: "Ebligi hCaptcha"
hcaptchaSiteKey: "Reteja ŝlosilo"
hcaptchaSecretKey: "Sekreta ŝlosilo"
recaptcha: "reCAPTCHA"
enableRecaptcha: "Ebligi reCAPTCHA'on"
enableRecaptcha: "Ebligi reCAPTCHA"
recaptchaSiteKey: "Reteja ŝlosilo"
recaptchaSecretKey: "Sekreta ŝlosilo"
antennas: "Antenoj"
@ -338,15 +344,17 @@ moderator: "Kontrolisto"
nUsersMentioned: "{n} uzanto(j) menciis"
securityKey: "Sekureca ŝlosilo"
securityKeyName: "Nomo de la ŝlosilo"
registerSecurityKey: "Registri ŝlosilon de sekureco"
lastUsed: "Plej malnove uzita"
unregister: "Malregistriĝi"
passwordLessLogin: "Ensaluti sen pasvorto"
resetPassword: "Restarigi pasvorton"
newPasswordIs: "La nova pasvorto estas {password}."
share: "Diskonigi"
reduceUiAnimation: "Redukti la animacioj de la fasado"
share: "Kundividi"
notFound: "Ne trovita"
cacheClear: "Malplenigi staplon"
markAsReadAllNotifications: "Marki ĉiujn sciigojn kiel legito"
markAsReadAllNotifications: "Marki ĉiujn sciigojn kiel legita"
help: "Manlibro de uzado"
inputMessageHere: "Entajpu masaĝo tie ĉi"
close: "Fermi"
@ -354,10 +362,11 @@ group: "Grupo"
groups: "Grupoj"
createGroup: "Krei grupon"
ownedGroups: "Administrataj grupoj"
joinedGroups: "La grupoj kiujn la uzanto aliĝis"
joinedGroups: "Al grupoj kiuj vi aliĝis"
invites: "Inviti"
groupName: "Grupa nomo"
members: "Membroj"
transfer: "Movi"
messagingWithUser: "Babili private"
messagingWithGroup: "Babili grupe"
title: "Titolo"
@ -366,6 +375,7 @@ enable: "Ebligi"
next: "Sekve"
retype: "Retajpu"
noteOf: "Noto de {user}"
inviteToGroup: "Inviti al grupo"
quoteAttached: "Kun citaĵo"
quoteQuestion: "Ĉu vi aldonas citaĵon?"
noMessagesYet: "Ankoraŭ neniu mesaĝo"
@ -374,27 +384,38 @@ onlyOneFileCanBeAttached: "Oni povas aldoni nur unu dosieron po mesaĝo."
signinRequired: "Bonvolu ensaluti"
invitations: "Inviti"
invitationCode: "Invita kodo"
available: "Disposabla"
unavailable: "Ne disponebla"
usernameInvalidFormat: "La uzantnomo povas enhavi minusklajn kaj majusklajn literojn, numerojn, nur kaj '_'."
tooShort: "Tro mallonga"
tooLong: "Tro longa"
weakPassword: "Malforta pasvorto"
normalPassword: "Normala pasvorto"
strongPassword: "Forta pasvorto"
passwordMatched: "Konforma"
passwordNotMatched: "Nekonforma"
signinWith: "Ensaluti kun {x}"
or: "Aŭ"
language: "Lingvo"
uiLanguage: "Lingvo de fasado"
aboutX: "Pri {x}"
useOsNativeEmojis: "Oni uzas la emoĵioj de la denaska sistemo"
useOsNativeEmojis: "Uzi la emoĵiojn implicitan de la operaciumo"
youHaveNoGroups: "Neniuj grupoj"
noHistory: "Neniom historio"
signinHistory: "Historio de aliroj al la konto"
doing: "Traktado..."
category: "Kategorio"
tags: "Etikedoj"
docSource: "Fonto de la dokumento"
createAccount: "Krei konton"
existingAccount: "Ekzista konto"
regenerate: "Regeneri"
fontSize: "Tipara grando"
noFollowRequests: "Vi ne havas peto de sekvado"
openImageInNewTab: "Fermi la bildon en nova tablo"
openImageInNewTab: "Malfermi la bildojn en nova tablo"
dashboard: "Stirpanelo"
local: "Loka"
remote: "Transa"
remote: "Fora"
total: "Entute"
appearance: "Eksteraĵo"
clientSettings: "Agordoj de kliento"
@ -402,6 +423,7 @@ accountSettings: "Agordoj de konto"
numberOfDays: "Nombro de tagoj"
hideThisNote: "Kaŝi la noton"
objectStorageBaseUrl: "Baza URL"
objectStoragePrefix: "Prefix"
objectStorageRegion: "Regiono"
objectStorageUseSSL: "Oni uzas SSL"
serverLogs: "Servila protokolo"
@ -416,7 +438,7 @@ volume: "Laŭteco"
masterVolume: "Baza laŭteco"
details: "Detaloj"
chooseEmoji: "Elekti emoĵion"
recentUsed: "Lastatempaj uzitaj"
recentUsed: "Lastatempe uzitaj"
install: "Instali"
uninstall: "Malinstali"
installedApps: "Instalita programo"
@ -425,10 +447,12 @@ installedDate: "Dato de instalado"
lastUsedDate: "Lastfoje uzita je"
state: "Stato"
sort: "Ordigado"
ascendingOrder: "Kreski"
descendingOrder: "Malkreski"
scratchpad: "Malneta redaktilo"
output: "Elmeto"
script: "Skripto"
disablePagesScript: "Malebligi AiScripto en la paĝoj"
disablePagesScript: "Malebligi AiScript en la paĝoj"
deleteAllFiles: "Forviŝi ĉiujn dosierojn"
deleteAllFilesConfirm: "Ĉu vi certas, ke vi volas forviŝi ĉiujn dosierojn?"
removeAllFollowing: "Ĉesi sekvi ĉiujn sekvatojn"
@ -438,7 +462,8 @@ menu: "Menuo"
addItem: "Aldoni novaĵon"
rooms: "Ĉambro"
deletedNote: "Forviŝita noto"
invisibleNote: "Malpublika noto"
invisibleNote: "Malpublikigita noto"
enableInfiniteScroll: "Ebligi infinitan rulumon"
visibility: "Videbleco"
poll: "Balotujo"
useCw: "Kaŝi enhavo"
@ -453,16 +478,23 @@ author: "Aŭtoro"
manage: "Administro"
plugins: "Kromaĵoj"
deck: "Kartaro"
useFullReactionPicker: "Uzi la tuton de la elektilon de reagoj"
width: "Larĝeco"
height: "Alteco"
large: "Granda"
medium: "Meza"
small: "Malgranda"
generateAccessToken: "Generi ĵetonon de aliro"
permission: "Permesoj"
enableAll: "Ebligi ĉiujn"
disableAll: "Malebligi ĉiujn"
notificationType: "Tipo de sciigoj"
edit: "Redakti"
emailServer: "Retpoŝta servilo"
email: "Retpoŝto"
emailAddress: "Retpoŝta adreso"
smtpConfig: "Agordoj de SMTP servilo"
smtpHost: "Gastigo"
smtpHost: "Transa servilo"
smtpPort: "Pordo"
smtpUser: "Uzantnomo"
smtpPass: "Pasvorto"
@ -471,13 +503,20 @@ userSaysSomething: "{name} parolis ion"
makeActive: "Aktivigi"
display: "Vidi"
copy: "Kopii"
metrics: "mezurciferoj"
overview: "Resumo"
logs: "Protokoloj"
delayed: "Prokrasto "
database: "Datumbazo"
channel: "Kanalo"
create: "Krei"
notificationSetting: "Agordoj de sciigoj"
useGlobalSetting: "Oni uzas malloka agordo"
other: "Aliaj"
regenerateLoginToken: "Regeneri la ĵetonon de aliro"
fileIdOrUrl: "Dosiera identigilo aŭ URL"
chatOpenBehavior: "Konduto por malfermi la fenestron de babilejo"
behavior: "Konduto"
sample: "Ekzemplo"
abuseReports: "Signaloj"
reportAbuse: "Signalo"
@ -485,20 +524,21 @@ reportAbuseOf: "Signali kontraŭ {name}'(o)"
send: "Sendi"
openInNewTab: "Malfermi en nova langeto"
editTheseSettingsMayBreakAccount: "Redakti ĉi tiujn agordojn povas damaĝi vian konton."
instanceTicker: "Informoj pri la nodo kiu dissendas la noton"
instanceTicker: "Nomo de la nodo sendinta notojn"
waitingFor: "Atendado pro {x}"
random: "Hazarde"
system: "Sistemo"
desktop: "Labortablo"
createNew: "Krei novan"
optional: "Opciaj"
public: "Publika"
i18nInfo: "Misskey estas tradukata en diversaj lingvoj far volontuloj. Oni povas kontribui por la tradukado ĉe {link}."
i18nInfo: "Misskey estas tradukata en diversaj lingvoj de volontuloj. Oni povas kontribui ĉe {link}."
accountInfo: "Kontaj Informoj"
notesCount: "La nombro de notoj"
repliesCount: "La nombro de respondoj senditaj"
renotesCount: "La nombro de notoj kiujn la uzanto plusendis"
renotesCount: "La nombro de notoj plusenditaj de la uzanto"
repliedCount: "La nombro de respondoj ricevitaj"
renotedCount: "La nombro de uzantulaj notoj plusenditaj"
renotedCount: "La nombro de plusendoj de la notoj skribitaj de la uzanto"
followingCount: "La nombro de sekvatoj"
followersCount: "La nombro de sekvantoj"
sentReactionsCount: "La nombro de la reagoj senditaj"
@ -512,10 +552,15 @@ noteFavoritesCount: "La nombro de notoj preferataj"
pageLikesCount: "La nombro de paĝoj kiun la uzanto preferas"
pageLikedCount: "La nombro de uzantoj, kiuj preferas paĝon de ĉi tiu uzanto"
contact: "Kontakto"
useSystemFont: "Uzi la tiparon implicitan de la sistemo"
developer: "Evoluiganto"
makeExplorable: "Videbligi konton sur la paĝo \"Esplori\""
makeExplorableDescription: "Se vi elŝaltas tiun, via konto ne montros en la paĝo \"Esplori\"."
duplicate: "Duobligi"
left: "Maldekstra"
center: "Centra"
wide: "Vasta"
narrow: "Malvasta"
showTitlebar: "Videbligi titolan stangon"
clearCache: "Malplenigi staplon"
onlineUsersCount: "{n} uzanto(j) estas surlinea"
@ -525,9 +570,11 @@ myTheme: "Miaj koloraroj"
backgroundColor: "Fona koloro"
textColor: "Teksto"
saveAs: "Konservi kiel…"
advanced: "Altnivela"
value: "Valoro"
createdAt: "Kreita je"
updatedAt: "Laste ĝisdatigita"
saveConfirm: "Ĉu vi konservas la ŝanĝon?"
deleteConfirm: "Ĉu certas forviŝi?"
closeAccount: "Forigi konton"
currentVersion: "Nuna versio"
@ -538,9 +585,10 @@ inUse: "Uzata"
editCode: "Redakti kodon"
emailNotification: "Sciigoj per retpoŝto"
inChannelSearch: "Serĉi en kanalo"
useReactionPickerForContextMenu: "Malfermi reago-elektilon per dekstro-klaki"
useReactionPickerForContextMenu: "Dekstre-klaki por malfermi la elektilon de reagoj"
typingUsers: "{users} nun skribas…"
clear: "Vakigi"
markAllAsRead: "Marki ĉiujn kiel legito"
goBack: "Reiri antaŭ"
addDescription: "Priskribi"
info: "Informoj"
@ -559,7 +607,7 @@ memo: "Memorigilo"
high: "Alta"
middle: "Meza"
low: "Malalta"
customCss: "Uzantula CSS"
customCss: "Personecigita CSS"
global: "Malloka"
sent: "Sendi"
received: "Ricevita"
@ -569,10 +617,27 @@ troubleshooting: "Problemsolvi"
learnMore: "Lernu pli"
translate: "Traduki"
translatedFrom: "Tradukita el {x}"
itsOn: "Ŝaltita"
unread: "Nelegita"
controlPanel: "Ŝaltpodio"
classic: "Klasika"
ffVisibility: "Videbleco pri viaj sekvataro/sekvantaro\n"
ffVisibilityDescription: "Agordi la videblecon kiu povas vidi tiujn kiujn vi sekvas kaj tiujn kiuj sekvas vin."
continueThread: "Vidi pli mesaĝarojn"
incorrectPassword: "Nevalida pasvorto"
_emailUnavailable:
used: "La retpoŝto jam estas uzita."
format: "Nevalida formato."
disposable: "Dumtempa retpoŝto ne estas uzebla."
smtp: "Tiu retpoŝta servilo ne respondas"
_ffVisibility:
public: "Publika"
followers: "Afiŝi nur al sekvantoj"
private: "Malpublikigita"
_signup:
emailAddressInfo: "Entajpu vian retpoŝton"
_accountDelete:
accountDelete: "Forigi konton"
_ad:
back: "Nuligi"
_forgotPassword:
@ -598,7 +663,7 @@ _aboutMisskey:
contributors: "Precipaj kontribuantoj"
allContributors: "Ĉiuj kontribuantoj"
source: "Fontkodo"
translation: "Traduki Misskey'on"
translation: "Traduki Misskey"
patrons: "Mecenatoj"
_mfm:
dummy: "Misskey evoluigas la mondon de Fediverso"
@ -614,19 +679,21 @@ _mfm:
inlineMath: "Formulo (en linio)"
blockMath: "Formulo (bloko)"
quote: "Citi"
emoji: "Personecigitaj emoĵioj"
search: "Serĉi"
flip: "Inversa"
x2: "Granda"
x3: "Grandega"
x4: "Pli grandega"
font: "Presliteraro"
rotate: "Orientiĝo"
_reversi:
total: "Entute"
_instanceTicker:
none: "Ne montri"
remote: "Montri al transaj uzantoj"
remote: "Montri al foraj uzantoj"
always: "Ĉiam montri"
_serverDisconnectedBehavior:
reload: "Aŭtomate reŝargi"
_channel:
create: "Krei kanalon"
edit: "Redakti kanalon"
@ -640,13 +707,14 @@ _menuDisplay:
hide: "Kaŝi"
_wordMute:
muteWords: "Silentigitaj vortoj"
soft: "En kliento"
hard: "En servilo"
soft: "Per la kliento"
hard: "Per la servilo"
mutedNotes: "Silentigitaj notoj"
_theme:
manage: "Administri kolorarojn"
code: "Kolorara kodo"
description: "Priskribo"
defaultValue: "Implicitaĵa valoro"
color: "Koloro"
darken: "Malbrileco"
lighten: "Brileco"
@ -657,7 +725,7 @@ _theme:
hashtag: "Kradvorto"
mention: "Mencioj"
mentionMe: "Mencio al vi"
renote: "Noto plusendita"
renote: "Plusendita"
buttonBg: "Fono de butono"
driveFolderBg: "Fono de dosierujo de la disko"
messageBg: "Fono de retbabilejo"
@ -688,7 +756,7 @@ _tutorial:
title: "Uzado de Misskey"
step1_1: "Bonvenon."
step7_2: "Se vi volas scii pli pri Misskey, rigardu la fakon {help}."
step7_3: "Do, bonvolu amuziĝi Misskey'on🚀"
step7_3: "Do, bonvolu amuziĝi sur Misskey🚀"
_2fa:
registerKey: "Nove registri ŝlosilon"
_permissions:
@ -732,10 +800,10 @@ _widgets:
federation: "Federaĵo"
slideshow: "Bildoprezento"
button: "Butono"
onlineUsers: "Surkonektita uzanto"
onlineUsers: "Surkonektitaj uzantoj"
aichan: "Ai"
_cw:
show: "Vidu pli"
show: "Vidi pli"
files: "{count} dosiero(j)"
_poll:
choiceN: "Balotilo {n}"
@ -747,15 +815,15 @@ _poll:
closed: "Oni jam balotis ĝin"
_visibility:
public: "Publika"
publicDescription: "Via noto estos videbla de ĉiuj uzantoj"
publicDescription: "Afiŝi al ĉiuj en la Fediverso"
home: "Hejma"
homeDescription: "Dissendi nur sur hejma templinio"
followers: "Nur al sekvantoj"
followersDescription: "Publiki nur al viaj sekvantoj"
followersDescription: "Afiŝi nur al sekvantoj"
specified: "Rekte"
specifiedDescription: "Montri nur al specifaj uzantoj"
specifiedDescription: "Afiŝi nur al specifaj uzantoj"
localOnly: "Nur loka"
localOnlyDescription: "Ne montri al transaj uzantoj"
localOnlyDescription: "Ne afiŝi al foraj uzantoj"
_postForm:
replyPlaceholder: "Respondi la noton…"
quotePlaceholder: "Citi la noton…"
@ -789,7 +857,7 @@ _rooms:
translate: "Movi"
chooseImage: "Elekti bildon"
_roomType:
default: "Defaŭlta"
default: "Implicitaĵo"
_furnitures:
bed: "Lito"
low-table: "Malaltotablo"
@ -835,18 +903,22 @@ _pages:
textInput: "Enmeto el teksto"
_textInput:
text: "Titolo"
default: "Implicitaĵa valoro"
textareaInput: "Enmeto el teksto en multaj linioj"
_textareaInput:
text: "Titolo"
default: "Implicitaĵa valoro"
numberInput: "Nombra enmeto"
_numberInput:
text: "Titolo"
default: "Implicitaĵa valoro"
_canvas:
id: "Kanvasa identigilo"
_note:
id: "Identigilo de noto"
_switch:
text: "Titolo"
default: "Implicitaĵa valoro"
_counter:
text: "Titolo"
_button:
@ -856,6 +928,7 @@ _pages:
event: "Nomo de la evento"
_radioButton:
title: "Titolo"
default: "Implicitaĵa valoro"
script:
categories:
text: "Manipulo de teksto"
@ -874,6 +947,7 @@ _pages:
arg1: "Teksto"
_join:
arg1: "Listoj"
arg2: "apartigilo"
_randomPick:
arg1: "Listoj"
_dailyRandomPick:
@ -904,6 +978,7 @@ _pages:
_relayStatus:
requesting: "Atendado de aprobon"
accepted: "Konfirmita"
rejected: "Malakceptita"
_notification:
fileUploaded: "La dosiero sukcese alŝutiĝis."
youGotMention: "{name} mencis"
@ -918,13 +993,13 @@ _notification:
yourFollowRequestAccepted: "Via peto de sekvado estis akceptita."
_types:
all: "Ĉio"
follow: "Nova sekvatoj"
follow: "Novaj sekvatoj"
mention: "Mencioj"
reply: "Respondoj"
renote: "Notoj plusenditaj"
renote: "Plusendoj"
quote: "Citi"
reaction: "Reagoj"
receiveFollowRequest: "Ricevita peton de sekvado"
receiveFollowRequest: "Ricevi peton de sekvado"
followRequestAccepted: "Akceptita peto por sekvado"
_deck:
profile: "Agordaro"

View File

@ -737,6 +737,7 @@ pubSub: "Cuentas Pub/Sub"
lastCommunication: "Última comunicación"
resolved: "Resuelto"
unresolved: "Sin resolver"
controlPanel: "Panel de control"
_accountDelete:
accountDelete: "Eliminar Cuenta"
_ad:
@ -767,6 +768,7 @@ _mfm:
flip: "Echar de un capirotazo"
flipDescription: "Voltea el contenido hacia arriba / abajo o hacia la izquierda / derecha."
font: "Fuente"
rotate: "Rotar"
_reversi:
reversi: "Reversi"
gameSettings: "Configuración del juego"

View File

@ -919,6 +919,7 @@ _mfm:
rainbowDescription: "Permet d'afficher le contenu en couleurs arc-en-ciel."
sparkle: "Paillettes"
sparkleDescription: "Ajoute un effet scintillant au contenu."
rotate: "Pivoter"
_reversi:
reversi: "Reversi"
gameSettings: "Réglages de la partie"

View File

@ -806,6 +806,10 @@ muteThread: "Bisukan thread"
unmuteThread: "Suarakan thread"
ffVisibility: "Visibilitas Mengikuti/Pengikut"
ffVisibilityDescription: "Mengatur siapa yang dapat melihat pengikutmu dan yang kamu ikuti."
continueThread: "Lihat lanjutan thread"
deleteAccountConfirm: "Akun akan dihapus. Apakah kamu yakin?"
incorrectPassword: "Kata sandi salah."
voteConfirm: "Konfirmasi suara kamu untuk ({choice})"
_emailUnavailable:
used: "Alamat surel ini telah digunakan"
format: "Format tidak valid."
@ -929,6 +933,8 @@ _mfm:
rainbowDescription: "Membuat konten muncul dalam warna pelangi."
sparkle: "Kelap-kelip"
sparkleDescription: "Memberikan konten efek partikel kelap-kelip."
rotate: "Putar"
rotateDescription: "Putar konten sesuai sudut yang ditentukan."
_reversi:
reversi: "Reversi"
gameSettings: "Pengaturan permainan"

View File

@ -806,6 +806,7 @@ _mfm:
font: "Tipo di carattere"
fontDescription: "Puoi scegliere il tipo di carattere per il contenuto."
rainbow: "Arcobaleno"
rotate: "Ruota"
_reversi:
reversi: "Reversi"
gameSettings: "Impostazioni di gioco"

View File

@ -792,6 +792,7 @@ pubSub: "Pub/Subのアカウント"
lastCommunication: "直近の通信"
resolved: "解決済み"
unresolved: "未解決"
breakFollow: "フォロワーを解除"
itsOn: "オンになっています"
itsOff: "オフになっています"
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"

View File

@ -700,6 +700,7 @@ _mfm:
spin: "アニメーション(回転)"
blur: "ぼかし"
font: "フォント"
rotate: "回転"
_reversi:
reversi: "リバーシ"
gameSettings: "対局の設定"

View File

@ -899,6 +899,7 @@ _mfm:
rainbowDescription: "내용을 무지개로 표시합니다."
sparkle: "반짝반짝"
sparkleDescription: "반짝이는 파티클 효과를 추가합니다."
rotate: "회전"
_reversi:
reversi: "리버시"
gameSettings: "대국 설정"

View File

@ -1,5 +1,193 @@
---
_lang_: "Nederlands"
headlineMisskey: "Netwerk verbonden door notities"
introMisskey: "Welkom! Misskey is een open source, gedecentraliseerde microblogdienst.\nMaak \"notities\" om je gedachten te delen met iedereen om je heen. 📡\nMet \"reacties\" kun je ook snel je mening geven over berichten van anderen. 👍\nLaten we een nieuwe wereld verkennen! 🚀"
monthAndDay: "{day} {month}"
search: "Zoeken"
notifications: "Meldingen"
username: "Gebruikersnaam"
password: "Wachtwoord"
forgotPassword: "Wachtwoord vergeten"
fetchingAsApObject: "Ophalen vanuit de Fediverse"
ok: "Ok"
gotIt: "Begrepen"
cancel: "Annuleren"
enterUsername: "Voer een gebruikersnaam in"
renotedBy: "Hergedeeld door {user}"
noNotes: "Geen notities"
noNotifications: "Geen meldingen"
instance: "Server"
settings: "Instellingen"
basicSettings: "Basisinstellingen"
otherSettings: "Overige instellingen"
openInWindow: "In een venster openen"
profile: "Profiel"
timeline: "Tijdlijn"
noAccountDescription: "Deze gebruiker heeft nog geen bio geschreven"
login: "Inloggen"
loggingIn: "Aan het inloggen"
logout: "Afmelden"
signup: "Registreren"
uploading: "Bezig met uploaden"
save: "Opslaan"
users: "Gebruikers"
addUser: "Toevoegen gebruiker"
favorite: "Favorieten"
favorites: "Toevoegen aan favorieten"
unfavorite: "Verwijderen uit favorieten"
favorited: "Toegevoegd aan favorieten."
alreadyFavorited: "Al toegevoegd aan favorieten"
cantFavorite: "Kon niet toevoegen aan favorieten"
pin: "Vastmaken aan profielpagina"
unpin: "Losmaken van profielpagina"
copyContent: "Kopiëren inhoud"
copyLink: "Kopiëren link"
delete: "Verwijderen"
deleteAndEdit: "Verwijderen en bewerken"
deleteAndEditConfirm: "Weet je zeker dat je deze notitie wilt verwijderen en dan bewerken? Je verliest alle reacties, herdelingen en antwoorden erop."
addToList: "Aan lijst toevoegen"
sendMessage: "Verstuur bericht"
copyUsername: "Kopiëren gebruikersnaam "
searchUser: "Zoeken een gebruiker"
reply: "Antwoord"
loadMore: "Laad meer"
showMore: "Toon meer"
youGotNewFollower: "volgde jou"
receiveFollowRequest: "Volgverzoek ontvangen"
followRequestAccepted: "Volgverzoek geaccepteerd"
mention: "Vermelding"
mentions: "Vermeldingen"
directNotes: "Directe notities"
importAndExport: "Import / export"
import: "Import"
export: "Export"
files: "Bestanden"
download: "Downloaden"
driveFileDeleteConfirm: "Weet je zeker dat je het bestand \"{name}\" wilt verwijderen? Notities met dit bestand als bijlage worden ook verwijderd."
unfollowConfirm: "Weet je zeker dat je {name} wilt ontvolgen?"
exportRequested: "Je hebt een export aangevraagd. Dit kan een tijdje duren. Het wordt toegevoegd aan je Drive zodra het is voltooid."
importRequested: "Je hebt een import aangevraagd. Dit kan even duren."
lists: "Lijsten"
noLists: "Je hebt geen lijsten"
note: "Notitie"
notes: "Notities"
following: "Volgend"
followers: "Volgers"
followsYou: "Volgt jou"
createList: "Creëer lijst"
manageLists: "Beheren lijsten"
error: "Fout"
somethingHappened: "Er is iets misgegaan."
retry: "Probeer opnieuw"
pageLoadError: "Pagina laden mislukt"
pageLoadErrorDescription: "Dit wordt normaal gesproken veroorzaakt door netwerkfouten of door de cache van de browser. Probeer de cache te wissen en probeer het na een tijdje wachten opnieuw."
serverIsDead: "De server reageert niet. Wacht even en probeer het opnieuw."
youShouldUpgradeClient: "Werk je client bij om deze pagina te zien."
enterListName: "Voer de naam van de lijst in"
privacy: "Privacy"
makeFollowManuallyApprove: "Volgverzoeken vergen een goedkeuring"
defaultNoteVisibility: "Standaard zichtbaarheid"
follow: "Volgen"
followRequest: "Verzoek om te mogen volgen"
followRequests: "Volgverzoeken"
unfollow: "Ontvolgen"
followRequestPending: "Wachten op goedkeuring volgverzoek"
enterEmoji: "Voer een emoji in"
renote: "Herdelen"
unrenote: "Stop herdelen"
renoted: "Herdeeld"
cantRenote: "Dit bericht kan niet worden herdeeld"
cantReRenote: "Een herdeling kan niet worden herdeeld"
quote: "Quote"
pinnedNote: "Vastgemaakte notitie"
pinned: "Vastmaken aan profielpagina"
you: "Jij"
clickToShow: "Klik om te bekijken"
sensitive: "NSFW"
add: "Toevoegen"
reaction: "Reacties"
reactionSettingDescription: "Configureer welke reacties je wilt weergeven in de reactiekiezer."
reactionSettingDescription2: "Sleep om opnieuw te ordenen, Klik om te verwijderen, Druk op \"+\" om toe te voegen"
rememberNoteVisibility: "Vergeet niet de notitie zichtbaarheidsinstellingen"
attachCancel: "Verwijder bijlage"
markAsSensitive: "Markeren als NSFW"
unmarkAsSensitive: "Geen NSFW"
enterFileName: "Invoeren bestandsnaam"
mute: "Dempen"
unmute: "Stop dempen"
block: "Blokkeren"
unblock: "Deblokkeren"
suspend: "Opschorten"
unsuspend: "Heractiveren"
blockConfirm: "Weet je zeker dat je dit account wil blokkeren?"
instances: "Server"
remove: "Verwijderen"
nsfw: "NSFW"
pinnedNotes: "Vastgemaakte notitie"
userList: "Lijsten"
smtpUser: "Gebruikersnaam"
smtpPass: "Wachtwoord"
user: "Gebruikers"
muteThread: "Discussies dempen "
unmuteThread: "Dempen van discussie ongedaan maken"
_email:
_follow:
title: "volgde jou"
_mfm:
mention: "Vermelding"
quote: "Quote"
search: "Zoeken"
_theme:
keys:
mention: "Vermelding"
renote: "Herdelen"
_sfx:
note: "Notities"
notification: "Meldingen"
_widgets:
notifications: "Meldingen"
timeline: "Tijdlijn"
_cw:
show: "Laad meer"
_visibility:
followers: "Volgers"
_profile:
username: "Gebruikersnaam"
_exportOrImport:
followingList: "Volgend"
muteList: "Dempen"
blockingList: "Blokkeren"
userLists: "Lijsten"
_pages:
script:
categories:
list: "Lijsten"
blocks:
_join:
arg1: "Lijsten"
_randomPick:
arg1: "Lijsten"
_dailyRandomPick:
arg1: "Lijsten"
_seedRandomPick:
arg2: "Lijsten"
_pick:
arg1: "Lijsten"
_listLen:
arg1: "Lijsten"
types:
array: "Lijsten"
_notification:
youWereFollowed: "volgde jou"
_types:
follow: "Volgend"
mention: "Vermelding"
renote: "Herdelen"
quote: "Quote"
reaction: "Reacties"
_deck:
_columns:
notifications: "Meldingen"
tl: "Tijdlijn"
list: "Lijsten"
mentions: "Vermeldingen"

View File

@ -815,6 +815,7 @@ _mfm:
blur: "Rozmycie"
font: "Czcionka"
fontDescription: "Wybiera czcionkę do wyświetlania treści."
rotate: "Obróć"
_reversi:
reversi: "Reversi"
gameSettings: "Ustawienia gry"

View File

@ -1,22 +1,33 @@
---
_lang_: "Português"
headlineMisskey: "Rede conectada por notas"
monthAndDay: "{day}/{month}"
search: "Pesquisar"
notifications: "Notificações"
username: "Nome de usuário"
password: "Senha"
forgotPassword: "Esqueci a senha"
fetchingAsApObject: "Buscando no Fediverso"
ok: "OK"
gotIt: "Entendi"
cancel: "Cancelar"
enterUsername: "Digite o nome de usuário"
renotedBy: "Repostado por {user}"
noNotes: "Sem posts"
noNotifications: "Sem notificações"
instance: "Instância"
settings: "Configurações"
basicSettings: "Configurações básicas"
otherSettings: "Outras configurações"
openInWindow: "Abrir numa janela"
profile: "Perfil"
timeline: "Timeline"
login: "Iniciar sessão"
loggingIn: "Iniciando sessão…"
logout: "Sair"
signup: "Registrar-se"
uploading: "Enviando…"
save: "Guardar"
users: "Usuários"
favorite: "Favoritar"
favorites: "Favoritar"

View File

@ -922,6 +922,7 @@ _mfm:
rainbowDescription: "Заставлять содержимое отображаться в цветах радуги."
sparkle: "Блеск"
sparkleDescription: "Добавьте эффект искрящихся частиц."
rotate: "Повернуть"
_reversi:
reversi: "Реверси"
gameSettings: "Настройки игры"

View File

@ -771,6 +771,7 @@ _mfm:
blurDescription: "Цей ефект зробить контент розмитим. Контент можна зробити чітким, якщо навести на нього вказівник миші."
font: "Шрифт"
fontDescription: "Встановлює шрифт для контенту."
rotate: "Обертати"
_reversi:
reversi: "Реверсі"
gameSettings: "Налаштування гри"

View File

@ -792,6 +792,7 @@ pubSub: "Pub/Sub账户"
lastCommunication: "最近通信"
resolved: "已解决"
unresolved: "未解决"
breakFollow: "移除关注者"
itsOn: "已开启"
itsOff: "已关闭"
emailRequiredForSignup: "注册账户需要电子邮件地址"
@ -808,6 +809,8 @@ ffVisibility: "连接的可见范围"
ffVisibilityDescription: "您可以设置您的关注/关注者信息的公开范围"
continueThread: "查看更多帖子"
deleteAccountConfirm: "将要删除账户。是否确认?"
incorrectPassword: "密码错误"
voteConfirm: "确定投给“{choice}” "
_emailUnavailable:
used: "已经被使用过"
format: "无效的格式"
@ -931,6 +934,8 @@ _mfm:
rainbowDescription: "用彩虹色来显示内容。"
sparkle: "闪光"
sparkleDescription: "添加发光粒子效果。"
rotate: "旋转"
rotateDescription: "旋转指定的角度。"
_reversi:
reversi: "黑白棋"
gameSettings: "对局设置"

View File

@ -840,6 +840,7 @@ _mfm:
blur: "模糊"
font: "字型"
fontDescription: "您可以設定顯示內容的字型"
rotate: "旋轉"
_reversi:
reversi: "黑白棋"
gameSettings: "對弈設定"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "12.97.1",
"version": "12.98.0",
"codename": "indigo",
"repository": {
"type": "git",
@ -46,7 +46,7 @@
"@types/fluent-ffmpeg": "2.1.17",
"@typescript-eslint/parser": "5.4.0",
"cross-env": "7.0.3",
"cypress": "9.0.0",
"cypress": "9.1.0",
"start-server-and-test": "1.14.0",
"typescript": "4.5.2"
}

View File

@ -19,6 +19,7 @@ export type FileInfo = {
};
width?: number;
height?: number;
orientation?: number;
blurhash?: string;
warnings: string[];
};
@ -47,6 +48,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
// image dimensions
let width: number | undefined;
let height: number | undefined;
let orientation: number | undefined;
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) {
const imageSize = await detectImageSize(path).catch(e => {
@ -61,6 +63,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
} else if (imageSize.wUnits === 'px') {
width = imageSize.width;
height = imageSize.height;
orientation = imageSize.orientation;
// 制限を超えている画像は octet-stream にする
if (imageSize.width > 16383 || imageSize.height > 16383) {
@ -87,6 +90,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
type,
width,
height,
orientation,
blurhash,
warnings,
};
@ -163,6 +167,7 @@ async function detectImageSize(path: string): Promise<{
height: number;
wUnits: string;
hUnits: string;
orientation?: number;
}> {
const readable = fs.createReadStream(path);
const imageSize = await probeImageSize(readable);

View File

@ -77,7 +77,7 @@ export class DriveFile {
default: {},
comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
})
public properties: { width?: number; height?: number; avgColor?: string };
public properties: { width?: number; height?: number; orientation?: number; avgColor?: string };
@Index()
@Column('boolean')

View File

@ -28,6 +28,19 @@ export class DriveFileRepository extends Repository<DriveFile> {
);
}
public getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) {
const properties = JSON.parse(JSON.stringify(file.properties));
if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width];
}
properties.orientation = undefined;
return properties;
}
return file.properties;
}
public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null {
// リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
@ -122,7 +135,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
size: file.size,
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: file.properties,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
thumbnailUrl: this.getPublicUrl(file, true, meta),
comment: file.comment,
@ -202,6 +215,11 @@ export const packedDriveFileSchema = {
optional: true as const, nullable: false as const,
example: 720
},
orientation: {
type: 'number' as const,
optional: true as const, nullable: false as const,
example: 8
},
avgColor: {
type: 'string' as const,
optional: true as const, nullable: false as const,

View File

@ -189,12 +189,12 @@ export class UserRepository extends Repository<User> {
const followingCount = profile == null ? null :
(profile.ffVisibility === 'public') || (meId === user.id) ? user.followingCount :
(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followingCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
null;
const followersCount = profile == null ? null :
(profile.ffVisibility === 'public') || (meId === user.id) ? user.followersCount :
(profile.ffVisibility === 'followers') && (relation!.isFollowing) ? user.followersCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;
const falsy = opts.detail ? false : undefined;

View File

@ -1,8 +1,9 @@
import { IRemoteUser } from '@/models/entities/user';
import reject from '@/services/following/requests/reject';
import { remoteReject } from '@/services/following/reject';
import { IFollow } from '../../type';
import DbResolver from '../../db-resolver';
import { relayRejected } from '@/services/relay';
import { Users } from '@/models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
@ -14,7 +15,7 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
return `skip: follower not found`;
}
if (follower.host != null) {
if (!Users.isLocalUser(follower)) {
return `skip: follower is not a local user`;
}
@ -24,6 +25,6 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
return await relayRejected(match[1]);
}
await reject(actor, follower);
await remoteReject(actor, follower);
return `ok`;
};

View File

@ -0,0 +1,27 @@
import unfollow from '@/services/following/delete';
import cancelRequest from '@/services/following/requests/cancel';
import {IAccept} from '../../type';
import { IRemoteUser } from '@/models/entities/user';
import { Followings } from '@/models/index';
import DbResolver from '../../db-resolver';
export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
const dbResolver = new DbResolver();
const follower = await dbResolver.getUserFromApId(activity.object);
if (follower == null) {
return `skip: follower not found`;
}
const following = await Followings.findOne({
followerId: follower.id,
followeeId: actor.id
});
if (following) {
await unfollow(follower, actor);
return `ok: unfollowed`;
}
return `skip: フォローされていない`;
};

View File

@ -1,8 +1,9 @@
import { IRemoteUser } from '@/models/entities/user';
import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType } from '../../type';
import {IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept} from '../../type';
import unfollow from './follow';
import unblock from './block';
import undoLike from './like';
import undoAccept from './accept';
import { undoAnnounce } from './announce';
import Resolver from '../../resolver';
import { apLogger } from '../../logger';
@ -29,6 +30,7 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => {
if (isBlock(object)) return await unblock(actor, object);
if (isLike(object)) return await undoLike(actor, object);
if (isAnnounce(object)) return await undoAnnounce(actor, object);
if (isAccept(object)) return await undoAccept(actor, object);
return `skip: unknown object type ${getApType(object)}`;
};

View File

@ -0,0 +1,82 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import * as ms from 'ms';
import deleteFollowing from '@/services/following/delete';
import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
import { Followings, Users } from '@/models/index';
export const meta = {
tags: ['following', 'users'],
limit: {
duration: ms('1hour'),
max: 100
},
requireCredential: true as const,
kind: 'write:following',
params: {
userId: {
validator: $.type(ID),
}
},
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8'
},
followerIsYourself: {
message: 'Follower is yourself.',
code: 'FOLLOWER_IS_YOURSELF',
id: '07dc03b9-03da-422d-885b-438313707662'
},
notFollowing: {
message: 'The other use is not following you.',
code: 'NOT_FOLLOWING',
id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09'
},
},
res: {
type: 'object' as const,
optional: false as const, nullable: false as const,
ref: 'User'
}
};
export default define(meta, async (ps, user) => {
const followee = user;
// Check if the follower is yourself
if (user.id === ps.userId) {
throw new ApiError(meta.errors.followerIsYourself);
}
// Get follower
const follower = await getUser(ps.userId).catch(e => {
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw e;
});
// Check not following
const exist = await Followings.findOne({
followerId: follower.id,
followeeId: followee.id
});
if (exist == null) {
throw new ApiError(meta.errors.notFollowing);
}
await deleteFollowing(follower, followee);
return await Users.pack(followee.id, user);
});

View File

@ -1,6 +1,6 @@
import $ from 'cafy';
import { ID } from '@/misc/cafy-id';
import rejectFollowRequest from '@/services/following/requests/reject';
import { rejectFollowRequest } from '@/services/following/reject';
import define from '../../../define';
import { ApiError } from '../../../error';
import { getUser } from '../../../common/getters';

View File

@ -372,12 +372,16 @@ export default async function(
const properties: {
width?: number;
height?: number;
orientation?: number;
} = {};
if (info.width) {
properties['width'] = info.width;
properties['height'] = info.height;
}
if (info.orientation != null) {
properties['orientation'] = info.orientation;
}
const profile = user ? await UserProfiles.findOne(user.id) : null;

View File

@ -2,6 +2,7 @@ import { publishMainStream, publishUserEvent } from '@/services/stream';
import { renderActivity } from '@/remote/activitypub/renderer/index';
import renderFollow from '@/remote/activitypub/renderer/follow';
import renderUndo from '@/remote/activitypub/renderer/undo';
import renderReject from '@/remote/activitypub/renderer/reject';
import { deliver } from '@/queue/index';
import Logger from '../logger';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
@ -40,6 +41,12 @@ export default async function(follower: { id: User['id']; host: User['host']; ur
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox);
}
if (Users.isLocalUser(followee) && Users.isRemoteUser(follower)) {
// local user has null host
const content = renderActivity(renderReject(renderFollow(follower, followee), followee));
deliver(followee, content, follower.inbox);
}
}
export async function decrementFollowing(follower: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }) {

View File

@ -0,0 +1,105 @@
import { renderActivity } from '@/remote/activitypub/renderer/index';
import renderFollow from '@/remote/activitypub/renderer/follow';
import renderReject from '@/remote/activitypub/renderer/reject';
import { deliver } from '@/queue/index';
import { publishMainStream, publishUserEvent } from '@/services/stream';
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
import { Users, FollowRequests, Followings } from '@/models/index';
import { decrementFollowing } from './delete';
type Local = ILocalUser | { id: User['id']; host: User['host']; uri: User['host'] };
type Remote = IRemoteUser;
type Both = Local | Remote;
/**
* API following/request/reject
*/
export async function rejectFollowRequest(user: Local, follower: Both) {
if (Users.isRemoteUser(follower)) {
deliverReject(user, follower);
}
await removeFollowRequest(user, follower);
if (Users.isLocalUser(follower)) {
publishUnfollow(user, follower);
}
}
/**
* API following/reject
*/
export async function rejectFollow(user: Local, follower: Both) {
if (Users.isRemoteUser(follower)) {
deliverReject(user, follower);
}
await removeFollow(user, follower);
if (Users.isLocalUser(follower)) {
publishUnfollow(user, follower);
}
}
/**
* AP Reject/Follow
*/
export async function remoteReject(actor: Remote, follower: Local) {
await removeFollowRequest(actor, follower);
await removeFollow(actor, follower);
publishUnfollow(actor, follower);
}
/**
* Remove follow request record
*/
async function removeFollowRequest(followee: Both, follower: Both) {
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (!request) return;
await FollowRequests.delete(request.id);
}
/**
* Remove follow record
*/
async function removeFollow(followee: Both, follower: Both) {
const following = await Followings.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (!following) return;
await Followings.delete(following.id);
decrementFollowing(follower, followee);
}
/**
* Deliver Reject to remote
*/
async function deliverReject(followee: Local, follower: Remote) {
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
const content = renderActivity(renderReject(renderFollow(follower, followee, request?.requestId || undefined), followee));
deliver(followee, content, follower.inbox);
}
/**
* Publish unfollow to local
*/
async function publishUnfollow(followee: Both, follower: Local) {
const packedFollowee = await Users.pack(followee.id, follower, {
detail: true
});
publishUserEvent(follower.id, 'unfollow', packedFollowee);
publishMainStream(follower.id, 'unfollow', packedFollowee);
}

View File

@ -1,46 +0,0 @@
import { renderActivity } from '@/remote/activitypub/renderer/index';
import renderFollow from '@/remote/activitypub/renderer/follow';
import renderReject from '@/remote/activitypub/renderer/reject';
import { deliver } from '@/queue/index';
import { publishMainStream, publishUserEvent } from '@/services/stream';
import { User, ILocalUser } from '@/models/entities/user';
import { Users, FollowRequests, Followings } from '@/models/index';
import { decrementFollowing } from '../delete';
export default async function(followee: { id: User['id']; host: User['host']; uri: User['host'] }, follower: User) {
if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) {
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
const content = renderActivity(renderReject(renderFollow(follower, followee, request!.requestId!), followee));
deliver(followee, content, follower.inbox);
}
const request = await FollowRequests.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (request) {
await FollowRequests.delete(request.id);
} else {
const following = await Followings.findOne({
followeeId: followee.id,
followerId: follower.id
});
if (following) {
await Followings.delete(following.id);
decrementFollowing(follower, followee);
}
}
Users.pack(followee.id, follower, {
detail: true
}).then(packed => {
publishUserEvent(follower.id, 'unfollow', packed);
publishMainStream(follower.id, 'unfollow', packed);
});
}

View File

@ -17,6 +17,7 @@ describe('Get file info', () => {
},
width: undefined,
height: undefined,
orientation: undefined,
});
}));
@ -34,6 +35,7 @@ describe('Get file info', () => {
},
width: 512,
height: 512,
orientation: undefined,
});
}));
@ -51,6 +53,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
orientation: undefined,
});
}));
@ -68,6 +71,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
orientation: undefined,
});
}));
@ -85,6 +89,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
orientation: undefined,
});
}));
@ -102,6 +107,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
orientation: undefined,
});
}));
@ -120,6 +126,7 @@ describe('Get file info', () => {
},
width: 256,
height: 256,
orientation: undefined,
});
}));
@ -137,6 +144,25 @@ describe('Get file info', () => {
},
width: 25000,
height: 25000,
orientation: undefined,
});
}));
it('Rotate JPEG', async (async () => {
const path = `${__dirname}/resources/rotate.jpg`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 12624,
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
type: {
mime: 'image/jpeg',
ext: 'jpg'
},
width: 512,
height: 256,
orientation: 8,
});
}));
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -12,66 +12,67 @@
<template #header>
{{ title }}
</template>
<FormBase class="xkpnjxcv">
<MkSpacer :margin-min="20" :margin-max="32">
<div class="xkpnjxcv _formRoot">
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1" class="_formBlock">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormInput>
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" class="_formBlock">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormInput>
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" class="_formBlock">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormTextarea>
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]" class="_formBlock">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormSwitch>
<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]" class="_formBlock">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
</FormSelect>
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
<template #desc><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock">
<template #caption><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
</FormRadios>
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step">
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormRange>
<FormButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)" class="_formBlock">
<span v-text="form[item].content || item"></span>
</FormButton>
</MkButton>
</template>
</FormBase>
</div>
</MkSpacer>
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import FormBase from './debobigego/base.vue';
import FormInput from './debobigego/input.vue';
import FormTextarea from './debobigego/textarea.vue';
import FormSwitch from './debobigego/switch.vue';
import FormSelect from './debobigego/select.vue';
import FormRange from './debobigego/range.vue';
import FormButton from './debobigego/button.vue';
import FormRadios from './debobigego/radios.vue';
import FormInput from './form/input.vue';
import FormTextarea from './form/textarea.vue';
import FormSwitch from './form/switch.vue';
import FormSelect from './form/select.vue';
import FormRange from './form/range.vue';
import MkButton from './ui/button.vue';
import FormRadios from './form/radios.vue';
export default defineComponent({
components: {
XModalWindow,
FormBase,
FormInput,
FormTextarea,
FormSwitch,
FormSelect,
FormRange,
FormButton,
MkButton,
FormRadios,
},

View File

@ -16,7 +16,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, ref, watch } from 'vue';
import { computed, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
import * as os from '@/os';
export default defineComponent({
@ -58,6 +58,9 @@ export default defineComponent({
},
setup(props, context) {
const containerEl = ref<HTMLElement>();
const thumbEl = ref<HTMLElement>();
const rawValue = ref((props.modelValue - props.min) / (props.max - props.min));
const steppedValue = computed(() => {
if (props.step) {
@ -78,10 +81,25 @@ export default defineComponent({
if (thumbEl.value == null) return 0;
return thumbEl.value!.offsetWidth;
});
const thumbPosition = computed(() => {
if (containerEl.value == null) return 0;
return (containerEl.value.offsetWidth - thumbWidth.value) * steppedValue.value;
const thumbPosition = ref(0);
const calcThumbPosition = () => {
if (containerEl.value == null) {
thumbPosition.value = 0;
} else {
thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedValue.value;
}
};
watch([steppedValue, containerEl], calcThumbPosition);
onMounted(() => {
const ro = new ResizeObserver((entries, observer) => {
calcThumbPosition();
});
ro.observe(containerEl.value);
onUnmounted(() => {
ro.disconnect();
});
});
const steps = computed(() => {
if (props.step) {
return (props.max - props.min) / props.step;
@ -89,8 +107,6 @@ export default defineComponent({
return 0;
}
});
const containerEl = ref<HTMLElement>();
const thumbEl = ref<HTMLElement>();
const onMousedown = (ev: MouseEvent | TouchEvent) => {
ev.preventDefault();

View File

@ -7,7 +7,7 @@
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
import { defineComponent, inject, onMounted, onUnmounted, ref } from 'vue';
export default defineComponent({
props: {
@ -24,7 +24,7 @@ export default defineComponent({
marginMax: {
type: Number,
required: false,
default: 32,
default: 24,
},
},
@ -33,8 +33,14 @@ export default defineComponent({
const root = ref<HTMLElement>();
const content = ref<HTMLElement>();
const margin = ref(0);
const shouldSpacerMin = inject('shouldSpacerMin', false);
const adjust = (rect: { width: number; height: number; }) => {
if (rect.width > (props.contentMax || 500)) {
if (shouldSpacerMin) {
margin.value = props.marginMin;
return;
}
if (rect.width > props.contentMax || rect.width > 500) {
margin.value = props.marginMax;
} else {
margin.value = props.marginMin;

View File

@ -44,16 +44,36 @@ export default defineComponent({
onMounted(() => {
const lightbox = new PhotoSwipeLightbox({
dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => ({
dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => {
const item = {
src: media.url,
w: media.properties.width,
h: media.properties.height,
alt: media.name,
})),
};
if (media.properties.orientation != null && media.properties.orientation >= 5) {
[item.w, item.h] = [item.h, item.w];
}
return item;
}),
gallery: gallery.value,
children: '.image',
thumbSelector: '.image',
pswpModule: PhotoSwipe
loop: false,
padding: window.innerWidth > 500 ? {
top: 32,
bottom: 32,
left: 32,
right: 32,
} : {
top: 0,
bottom: 0,
left: 0,
right: 0,
},
imageClickAction: 'close',
tapAction: 'toggle-controls',
pswpModule: PhotoSwipe,
});
lightbox.on('itemData', (e) => {
@ -68,6 +88,9 @@ export default defineComponent({
itemData.src = file.url;
itemData.w = Number(file.properties.width);
itemData.h = Number(file.properties.height);
if (file.properties.orientation != null && file.properties.orientation >= 5) {
[itemData.w, itemData.h] = [itemData.h, itemData.w];
}
itemData.msrc = file.thumbnailUrl;
itemData.thumbCropped = true;
});

View File

@ -649,7 +649,7 @@ export default defineComponent({
text: this.$ts.pin,
action: () => this.togglePin(true)
} : undefined,
...(this.$i.isModerator || this.$i.isAdmin ? [
/*...(this.$i.isModerator || this.$i.isAdmin ? [
null,
{
icon: 'fas fa-bullhorn',
@ -657,7 +657,7 @@ export default defineComponent({
action: this.promote
}]
: []
),
),*/
...(this.appearNote.userId != this.$i.id ? [
null,
{

View File

@ -623,6 +623,7 @@ export default defineComponent({
text: this.$ts.pin,
action: () => this.togglePin(true)
} : undefined,
/*
...(this.$i.isModerator || this.$i.isAdmin ? [
null,
{
@ -631,7 +632,7 @@ export default defineComponent({
action: this.promote
}]
: []
),
),*/
...(this.appearNote.userId != this.$i.id ? [
null,
{

View File

@ -206,8 +206,6 @@ export default defineComponent({
> .input {
flex: 1;
margin-top: 16px;
margin-bottom: 0;
}
> button {
@ -223,7 +221,7 @@ export default defineComponent({
}
> section {
margin: 16px 0 -16px 0;
margin: 16px 0 0 0;
> div {
margin: 0 8px;

View File

@ -1,7 +1,7 @@
<template>
<div class="tivcixzd" :class="{ done: closed || isVoted }">
<ul>
<li v-for="(choice, i) in poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)">
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)">
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span>
<template v-if="choice.isVoted"><i class="fas fa-check"></i></template>
@ -13,7 +13,7 @@
<p v-if="!readOnly">
<span>{{ $t('_poll.totalVotes', { n: total }) }}</span>
<span> · </span>
<a v-if="!closed && !isVoted" @click="toggleShowResult">{{ showResult ? $ts._poll.vote : $ts._poll.showResult }}</a>
<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? $ts._poll.vote : $ts._poll.showResult }}</a>
<span v-if="isVoted">{{ $ts._poll.voted }}</span>
<span v-else-if="closed">{{ $ts._poll.closed }}</span>
<span v-if="remaining > 0"> · {{ timer }}</span>
@ -22,9 +22,10 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, onUnmounted, ref, toRef } from 'vue';
import { sum } from '@/scripts/array';
import * as os from '@/os';
import { i18n } from '@/i18n';
export default defineComponent({
props: {
@ -38,71 +39,67 @@ export default defineComponent({
default: false,
}
},
data() {
return {
remaining: -1,
showResult: false,
setup(props) {
const remaining = ref(-1);
const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
const closed = computed(() => remaining.value === 0);
const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
const timer = computed(() => i18n.t(
remaining.value >= 86400 ? '_poll.remainingDays' :
remaining.value >= 3600 ? '_poll.remainingHours' :
remaining.value >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
s: Math.floor(remaining.value % 60),
m: Math.floor(remaining.value / 60) % 60,
h: Math.floor(remaining.value / 3600) % 24,
d: Math.floor(remaining.value / 86400)
}));
const showResult = ref(props.readOnly || isVoted.value);
//
if (props.note.poll.expiresAt) {
const tick = () => {
remaining.value = Math.floor(Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000);
if (remaining.value === 0) {
showResult.value = true;
}
};
},
computed: {
poll(): any {
return this.note.poll;
},
total(): number {
return sum(this.poll.choices.map(x => x.votes));
},
closed(): boolean {
return !this.remaining;
},
timer(): string {
return this.$t(
this.remaining >= 86400 ? '_poll.remainingDays' :
this.remaining >= 3600 ? '_poll.remainingHours' :
this.remaining >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
s: Math.floor(this.remaining % 60),
m: Math.floor(this.remaining / 60) % 60,
h: Math.floor(this.remaining / 3600) % 24,
d: Math.floor(this.remaining / 86400)
tick();
const intevalId = window.setInterval(tick, 3000);
onUnmounted(() => {
window.clearInterval(intevalId);
});
},
isVoted(): boolean {
return !this.poll.multiple && this.poll.choices.some(c => c.isVoted);
}
},
created() {
this.showResult = this.readOnly || this.isVoted;
if (this.note.poll.expiresAt) {
const update = () => {
if (this.remaining = Math.floor(Math.max(new Date(this.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000))
requestAnimationFrame(update);
else
this.showResult = true;
};
update();
}
},
methods: {
toggleShowResult() {
this.showResult = !this.showResult;
},
async vote(id) {
if (this.readOnly || this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return;
const vote = async (id) => {
if (props.readOnly || closed.value || isVoted.value) return;
const { canceled } = await os.confirm({
type: 'question',
text: this.$t('voteConfirm', { choice: this.poll.choices[id].text }),
text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }),
});
if (canceled) return;
await os.api('notes/polls/vote', {
noteId: this.note.id,
choice: id
noteId: props.note.id,
choice: id,
});
if (!this.showResult) this.showResult = !this.poll.multiple;
}
}
if (!showResult.value) showResult.value = !props.note.poll.multiple;
};
return {
remaining,
showResult,
total,
isVoted,
closed,
timer,
vote,
};
},
});
</script>
@ -118,38 +115,38 @@ export default defineComponent({
display: block;
position: relative;
margin: 4px 0;
padding: 4px 8px;
border: solid 0.5px var(--divider);
padding: 4px;
//border: solid 0.5px var(--divider);
background: var(--accentedBg);
border-radius: 4px;
overflow: hidden;
cursor: pointer;
&:hover {
background: rgba(#000, 0.05);
}
&:active {
background: rgba(#000, 0.1);
}
> .backdrop {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: var(--accent);
background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
transition: width 1s ease;
}
> span {
position: relative;
display: inline-block;
padding: 3px 5px;
background: var(--panel);
border-radius: 3px;
> i {
margin-right: 4px;
color: var(--accent);
}
> .votes {
margin-left: 4px;
opacity: 0.7;
}
}
}
@ -166,14 +163,6 @@ export default defineComponent({
&.done {
> ul > li {
cursor: default;
&:hover {
background: transparent;
}
&:active {
background: transparent;
}
}
}
}

View File

@ -289,9 +289,14 @@ export default defineComponent({
if (this.reply && this.reply.text != null) {
const ast = mfm.parse(this.reply.text);
const otherHost = this.reply.user.host;
for (const x of extractMentions(ast)) {
const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
const mention = x.host ?
`@${x.username}@${toASCII(x.host)}` :
(otherHost == null || otherHost == host) ?
`@${x.username}` :
`@${x.username}@${toASCII(otherHost)}`;
//
if (this.$i.username == x.username && x.host == null) continue;

View File

@ -41,6 +41,7 @@ export default defineComponent({
> .icon {
display: block;
width: 60px;
font-size: 60px; // unicodewidth
margin: 0 auto;
}

View File

@ -62,6 +62,7 @@ export default defineComponent({
> .icon {
display: block;
width: 60px;
font-size: 60px; // unicodewidth
margin: 0 auto;
}

View File

@ -153,6 +153,7 @@ export default defineComponent({
box-sizing: border-box;
min-width: 200px;
overflow: auto;
overscroll-behavior: contain;
&.center {
> .item {

View File

@ -52,7 +52,7 @@ export default defineComponent({
> .title {
opacity: 0.7;
margin: 0 0 8px 12px;
margin: 0 0 8px 0;
}
> .items {

View File

@ -1,3 +1,6 @@
// TODO: useTooltip関数使うようにしたい
// ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明
import { Directive, ref } from 'vue';
import { isDeviceTouch } from '@/scripts/is-device-touch';
import { popup, alert } from '@/os';

View File

@ -1,4 +1,4 @@
import { computed, ref } from 'vue';
import { computed, ref, reactive } from 'vue';
import { search } from '@/scripts/search';
import * as os from '@/os';
import { i18n } from '@/i18n';
@ -7,7 +7,7 @@ import { $i } from './account';
import { unisonReload } from '@/scripts/unison-reload';
import { router } from './router';
export const menuDef = {
export const menuDef = reactive({
notifications: {
title: 'notifications',
icon: 'fas fa-bell',
@ -221,4 +221,4 @@ export const menuDef = {
}*/], ev.currentTarget || ev.target);
},
},
};
});

View File

@ -556,7 +556,7 @@ export function contextMenu(items: any[], ev: MouseEvent) {
});
}
export function post(props: Record<string, any>) {
export function post(props: Record<string, any> = {}) {
return new Promise((resolve, reject) => {
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、

View File

@ -24,7 +24,7 @@
</FormSection>
<FormSection>
<div class="_inputSplit">
<div class="_inputSplit _formBlock">
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.administrator }}</template>
<template #value>{{ $instance.maintainerName }}</template>
@ -34,10 +34,9 @@
<template #value>{{ $instance.maintainerEmail }}</template>
</MkKeyValue>
</div>
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink>
</FormSection>
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" external>{{ $ts.tos }}</FormLink>
<FormSuspense :p="initStats">
<FormSection>
<template #label>{{ $ts.statistics }}</template>

View File

@ -33,7 +33,7 @@
</div>
-->
<MkPagination #default="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
<div v-for="report in items" :key="report.id" class="bcekxzvu _card _gap">
<div class="_content target">
<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>

View File

@ -7,7 +7,7 @@
</MkInput>
<MkPagination ref="emojis" :pagination="pagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template #default="{items}">
<template v-slot="{items}">
<div class="ldhfsamy">
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
@ -31,7 +31,7 @@
</MkInput>
<MkPagination ref="remoteEmojis" :pagination="remotePagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template #default="{items}">
<template v-slot="{items}">
<div class="ldhfsamy">
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>

View File

@ -28,7 +28,7 @@
<template #label>MIME type</template>
</MkInput>
</div>
<MkPagination #default="{items}" ref="files" :pagination="pagination" class="urempief">
<MkPagination v-slot="{items}" ref="files" :pagination="pagination" class="urempief">
<button v-for="file in items" :key="file.id" class="file _panel _button _gap" @click="show(file, $event)">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
<div class="body">

View File

@ -36,7 +36,7 @@
</MkInput>
</div>
<MkPagination #default="{items}" ref="users" :pagination="pagination" class="users">
<MkPagination v-slot="{items}" ref="users" :pagination="pagination" class="users">
<button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)">
<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
<div class="body">

View File

@ -1,6 +1,6 @@
<template>
<MkSpacer :content-max="800">
<MkPagination #default="{items}" :pagination="pagination" class="ruryvtyk _content">
<MkPagination v-slot="{items}" :pagination="pagination" class="ruryvtyk _content">
<section v-for="(announcement, i) in items" :key="announcement.id" class="_card announcement">
<div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
<div class="_content">

View File

@ -1,26 +1,28 @@
<template>
<div class="_root">
<div class="_block" style="padding: 24px;">
<MkInput v-model="endpoint" :datalist="endpoints" class="" @update:modelValue="onEndpointChange()">
<MkSpacer :content-max="700">
<div class="_formRoot">
<div class="_formBlock">
<MkInput v-model="endpoint" :datalist="endpoints" class="_formBlock" @update:modelValue="onEndpointChange()">
<template #label>Endpoint</template>
</MkInput>
<MkTextarea v-model="body" code>
<MkTextarea v-model="body" class="_formBlock" code>
<template #label>Params (JSON or JSON5)</template>
</MkTextarea>
<MkSwitch v-model="withCredential">
<MkSwitch v-model="withCredential" class="_formBlock">
With credential
</MkSwitch>
<MkButton primary full :disabled="sending" @click="send">
<MkButton class="_formBlock" primary :disabled="sending" @click="send">
<template v-if="sending"><MkEllipsis/></template>
<template v-else><i class="fas fa-paper-plane"></i> Send</template>
</MkButton>
</div>
<div v-if="res" class="_block" style="padding: 24px;">
<div v-if="res" class="_formBlock">
<MkTextarea v-model="res" code readonly tall>
<template #label>Response</template>
</MkTextarea>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
@ -64,7 +66,8 @@ export default defineComponent({
methods: {
send() {
this.sending = true;
os.api(this.endpoint, JSON5.parse(this.body)).then(res => {
const body = JSON5.parse(this.body);
os.api(this.endpoint, body, body.i || this.withCredential ? undefined : null).then(res => {
this.sending = false;
this.res = JSON5.stringify(res, null, 2);
}, err => {

View File

@ -10,20 +10,20 @@
<div class="_section">
<div v-if="tab === 'featured'" class="_content grwlizim featured">
<MkPagination #default="{items}" :pagination="featuredPagination">
<MkPagination v-slot="{items}" :pagination="featuredPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>
<div v-if="tab === 'following'" class="_content grwlizim following">
<MkPagination #default="{items}" :pagination="followingPagination">
<MkPagination v-slot="{items}" :pagination="followingPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>
<div v-if="tab === 'owned'" class="_content grwlizim owned">
<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
<MkPagination #default="{items}" :pagination="ownedPagination">
<MkPagination v-slot="{items}" :pagination="ownedPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>

View File

@ -41,7 +41,7 @@
</div>
</div>
<MkPagination #default="{items}" ref="instances" :key="host + state" :pagination="pagination">
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
<div class="dqokceoi">
<MkA v-for="instance in items" :key="instance.id" class="instance" :to="`/instance-info/${instance.host}`">
<div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div>

View File

@ -7,7 +7,7 @@
<div>{{ $ts.noFollowRequests }}</div>
</div>
</template>
<template #default="{items}">
<template v-slot="{items}">
<div v-for="req in items" :key="req.id" class="user _panel">
<MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
<div class="body">

View File

@ -9,7 +9,7 @@
<div v-if="tab === 'explore'">
<MkFolder class="_gap">
<template #header><i class="fas fa-clock"></i>{{ $ts.recentPosts }}</template>
<MkPagination #default="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
<div class="vfpdbgtk">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>
@ -17,7 +17,7 @@
</MkFolder>
<MkFolder class="_gap">
<template #header><i class="fas fa-fire-alt"></i>{{ $ts.popularPosts }}</template>
<MkPagination #default="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
<div class="vfpdbgtk">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>
@ -25,7 +25,7 @@
</MkFolder>
</div>
<div v-else-if="tab === 'liked'">
<MkPagination #default="{items}" :pagination="likedPostsPagination">
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
<div class="vfpdbgtk">
<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
</div>
@ -33,7 +33,7 @@
</div>
<div v-else-if="tab === 'my'">
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ $ts.postToGallery }}</MkA>
<MkPagination #default="{items}" :pagination="myPostsPagination">
<MkPagination v-slot="{items}" :pagination="myPostsPagination">
<div class="vfpdbgtk">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>

View File

@ -36,7 +36,7 @@
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
<MkContainer :max-height="300" :foldable="true" class="other">
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
<MkPagination #default="{items}" :pagination="otherPostsPagination">
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
<div class="sdrarzaf">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>

View File

@ -1,15 +1,17 @@
<template>
<div class="ieepwinx _section">
<MkSpacer :content-max="700">
<div class="ieepwinx">
<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
<div class="_content">
<MkPagination #default="{items}" ref="list" :pagination="pagination">
<div class="">
<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
<MkA v-for="antenna in items" :key="antenna.id" class="ljoevbzj" :to="`/my/antennas/${antenna.id}`">
<div class="name">{{ antenna.name }}</div>
</MkA>
</MkPagination>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
@ -29,6 +31,7 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: this.$ts.manageAntennas,
icon: 'fas fa-satellite',
bg: 'var(--bg)',
action: {
icon: 'fas fa-plus',
handler: this.create
@ -45,7 +48,6 @@ export default defineComponent({
<style lang="scss" scoped>
.ieepwinx {
padding: 16px;
> .add {
margin: 0 auto 16px auto;

View File

@ -1,16 +1,16 @@
<template>
<div class="_section qtcaoidl">
<MkSpacer :content-max="700">
<div class="qtcaoidl">
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
<div class="_content">
<MkPagination #default="{items}" ref="list" :pagination="pagination" class="list">
<MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="list">
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
<b>{{ item.name }}</b>
<div v-if="item.description" class="description">{{ item.description }}</div>
</MkA>
</MkPagination>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
@ -31,6 +31,7 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: this.$ts.clip,
icon: 'fas fa-paperclip',
bg: 'var(--bg)',
action: {
icon: 'fas fa-plus',
handler: this.create
@ -86,7 +87,6 @@ export default defineComponent({
margin: 0 auto 16px auto;
}
> ._content {
> .list {
> .item {
display: block;
@ -100,5 +100,4 @@ export default defineComponent({
}
}
}
}
</style>

View File

@ -12,7 +12,7 @@
<div v-if="tab === 'owned'" class="_content">
<MkButton primary style="margin: 0 auto var(--margin) auto;" @click="create"><i class="fas fa-plus"></i> {{ $ts.createGroup }}</MkButton>
<MkPagination #default="{items}" ref="owned" :pagination="ownedPagination">
<MkPagination v-slot="{items}" ref="owned" :pagination="ownedPagination">
<div v-for="group in items" :key="group.id" class="_card">
<div class="_title"><MkA :to="`/my/groups/${ group.id }`" class="_link">{{ group.name }}</MkA></div>
<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
@ -21,7 +21,7 @@
</div>
<div v-else-if="tab === 'joined'" class="_content">
<MkPagination #default="{items}" ref="joined" :pagination="joinedPagination">
<MkPagination v-slot="{items}" ref="joined" :pagination="joinedPagination">
<div v-for="group in items" :key="group.id" class="_card">
<div class="_title">{{ group.name }}</div>
<div class="_content"><MkAvatars :user-ids="group.userIds"/></div>
@ -30,7 +30,7 @@
</div>
<div v-else-if="tab === 'invites'" class="_content">
<MkPagination #default="{items}" ref="invitations" :pagination="invitationPagination">
<MkPagination v-slot="{items}" ref="invitations" :pagination="invitationPagination">
<div v-for="invitation in items" :key="invitation.id" class="_card">
<div class="_title">{{ invitation.group.name }}</div>
<div class="_content"><MkAvatars :user-ids="invitation.group.userIds"/></div>

View File

@ -1,14 +1,16 @@
<template>
<MkSpacer :content-max="700">
<div class="qkcjvfiv">
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
<MkPagination #default="{items}" ref="list" :pagination="pagination" class="lists _content">
<MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="lists _content">
<MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
<div class="name">{{ list.name }}</div>
<MkAvatars :user-ids="list.userIds"/>
</MkA>
</MkPagination>
</div>
</MkSpacer>
</template>
<script lang="ts">
@ -60,8 +62,6 @@ export default defineComponent({
<style lang="scss" scoped>
.qkcjvfiv {
padding: 16px;
> .add {
margin: 0 auto var(--margin) auto;
}

View File

@ -1,4 +1,5 @@
<template>
<MkSpacer :content-max="700">
<div class="mk-list-page">
<transition name="zoom" mode="out-in">
<div v-if="list" class="_section">
@ -30,6 +31,7 @@
</div>
</transition>
</div>
</MkSpacer>
</template>
<script lang="ts">
@ -49,6 +51,7 @@ export default defineComponent({
[symbols.PAGE_INFO]: computed(() => this.list ? {
title: this.list.name,
icon: 'fas fa-list-ul',
bg: 'var(--bg)',
} : null),
list: null,
users: [],

View File

@ -45,10 +45,10 @@
<template #label>{{ $ts._pages.script.blocks._fn.slots }}</template>
<template #caption>{{ $t('_pages.script.blocks._fn.slots-info') }}</template>
</MkTextarea>
<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="value.value.slots" :name="name"/>
<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="modelValue.value.slots" :name="name"/>
</section>
<section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;">
<XV v-for="(x, i) in modelValue.args" :key="i" v-model="value.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name"/>
<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name"/>
</section>
<section v-else class="" style="padding:16px;">
<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="$t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots"/>

View File

@ -1,6 +1,6 @@
<template>
<div>
<div class="jqqmcavi" style="margin: 16px;">
<MkSpacer :content-max="700">
<div class="jqqmcavi">
<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkButton>
<MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<MkButton v-if="pageId" inline class="button" @click="duplicate"><i class="fas fa-copy"></i> {{ $ts.duplicate }}</MkButton>
@ -8,7 +8,7 @@
</div>
<div v-if="tab === 'settings'">
<div style="padding: 16px;" class="_formRoot">
<div class="_formRoot">
<MkInput v-model="title" class="_formBlock">
<template #label>{{ $ts._pages.title }}</template>
</MkInput>
@ -43,7 +43,7 @@
</div>
<div v-else-if="tab === 'contents'">
<div style="padding: 16px;">
<div>
<XBlocks v-model="content" class="content" :hpml="hpml"/>
<MkButton v-if="!readonly" @click="add()"><i class="fas fa-plus"></i></MkButton>
@ -75,7 +75,7 @@
<MkTextarea v-model="script" class="_code"/>
</div>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">

View File

@ -1,5 +1,5 @@
<template>
<div>
<MkSpacer :content-max="700">
<transition name="fade" mode="out-in">
<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
<div class="_block main">
@ -48,7 +48,7 @@
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
<MkContainer :max-height="300" :foldable="true" class="other">
<template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
<MkPagination #default="{items}" :pagination="otherPostsPagination">
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_gap"/>
</MkPagination>
</MkContainer>
@ -56,7 +56,7 @@
<MkError v-else-if="error" @retry="fetch()"/>
<MkLoading v-else/>
</transition>
</div>
</MkSpacer>
</template>
<script lang="ts">
@ -201,14 +201,7 @@ export default defineComponent({
}
.xcukqgmh {
--padding: 32px;
&.max-width_450px {
--padding: 16px;
}
> .main {
padding: var(--padding);
> .header {
padding: 16px;
@ -302,7 +295,7 @@ export default defineComponent({
}
> .footer {
margin: var(--padding);
margin: var(--margin) 0 var(--margin) 0;
font-size: 85%;
opacity: 0.75;
}

View File

@ -1,50 +1,40 @@
<template>
<MkSpacer>
<!-- TODO: MkHeaderに統合 -->
<MkTab v-if="$i" v-model="tab">
<option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._pages.featured }}</option>
<option value="my"><i class="fas fa-edit"></i> {{ $ts._pages.my }}</option>
<option value="liked"><i class="fas fa-heart"></i> {{ $ts._pages.liked }}</option>
</MkTab>
<div class="_section">
<div v-if="tab === 'featured'" class="rknalgpo _content">
<MkPagination #default="{items}" :pagination="featuredPagesPagination">
<MkSpacer :content-max="700">
<div v-if="tab === 'featured'" class="rknalgpo">
<MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
</MkPagination>
</div>
<div v-if="tab === 'my'" class="rknalgpo _content my">
<div v-else-if="tab === 'my'" class="rknalgpo my">
<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
<MkPagination #default="{items}" :pagination="myPagesPagination">
<MkPagination v-slot="{items}" :pagination="myPagesPagination">
<MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/>
</MkPagination>
</div>
<div v-if="tab === 'liked'" class="rknalgpo _content">
<MkPagination #default="{items}" :pagination="likedPagesPagination">
<div v-else-if="tab === 'liked'" class="rknalgpo">
<MkPagination v-slot="{items}" :pagination="likedPagesPagination">
<MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/>
</MkPagination>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent } from 'vue';
import MkPagePreview from '@/components/page-preview.vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue';
import MkTab from '@/components/tab.vue';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
MkPagePreview, MkPagination, MkButton, MkTab
MkPagePreview, MkPagination, MkButton
},
data() {
return {
[symbols.PAGE_INFO]: {
[symbols.PAGE_INFO]: computed(() => ({
title: this.$ts.pages,
icon: 'fas fa-sticky-note',
bg: 'var(--bg)',
@ -53,7 +43,23 @@ export default defineComponent({
text: this.$ts.create,
handler: this.create,
}],
},
tabs: [{
active: this.tab === 'featured',
title: this.$ts._pages.featured,
icon: 'fas fa-fire-alt',
onClick: () => { this.tab = 'featured'; },
}, {
active: this.tab === 'my',
title: this.$ts._pages.my,
icon: 'fas fa-edit',
onClick: () => { this.tab = 'my'; },
}, {
active: this.tab === 'liked',
title: this.$ts._pages.liked,
icon: 'fas fa-heart',
onClick: () => { this.tab = 'liked'; },
},]
})),
tab: 'featured',
featuredPagesPagination: {
endpoint: 'pages/featured',

View File

@ -7,7 +7,7 @@
<div>{{ $ts.nothing }}</div>
</div>
</template>
<template #default="{items}">
<template v-slot="{items}">
<div v-for="token in items" :key="token.id" class="_debobigegoPanel bfomjevm">
<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/>
<div class="body">

View File

@ -1,7 +1,7 @@
<template>
<div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
<div v-if="!narrow || page == null" class="nav">
<MkSpacer :content-max="700">
<MkSpacer :content-max="700" :margin-min="20">
<div class="baaadecd">
<div class="title">{{ $ts.settings }}</div>
<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>

View File

@ -7,7 +7,7 @@
<div v-if="tab === 'mute'">
<MkPagination :pagination="mutingPagination" class="muting">
<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
<template #default="{items}">
<template v-slot="{items}">
<FormGroup>
<FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)">
<MkAcct :user="mute.mutee"/>
@ -19,7 +19,7 @@
<div v-if="tab === 'block'">
<MkPagination :pagination="blockingPagination" class="blocking">
<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
<template #default="{items}">
<template v-slot="{items}">
<FormGroup>
<FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)">
<MkAcct :user="block.blockee"/>

View File

@ -13,7 +13,7 @@
<FormSection>
<template #label>{{ $ts.signinHistory }}</template>
<FormPagination :pagination="pagination">
<template #default="{items}">
<template v-slot="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
<header>

View File

@ -119,6 +119,7 @@ export default defineComponent({
mim: 0,
max: 1,
step: 0.05,
textConverter: (v) => `${Math.floor(v * 100)}%`,
label: this.$ts.volume,
default: this.sounds[type].volume
},

View File

@ -1,6 +1,6 @@
<template>
<FormBase>
<FormSelect v-model="selectedThemeId">
<div class="_formRoot">
<FormSelect v-model="selectedThemeId" class="_formBlock">
<template #label>{{ $ts.theme }}</template>
<optgroup :label="$ts._theme.installedThemes">
<option v-for="x in installedThemes" :key="x.id" :value="x.id">{{ x.name }}</option>
@ -10,31 +10,31 @@
</optgroup>
</FormSelect>
<template v-if="selectedTheme">
<FormInput readonly :modelValue="selectedTheme.author">
<span>{{ $ts.author }}</span>
<FormInput readonly :modelValue="selectedTheme.author" class="_formBlock">
<template #label>{{ $ts.author }}</template>
</FormInput>
<FormTextarea v-if="selectedTheme.desc" readonly :modelValue="selectedTheme.desc">
<span>{{ $ts._theme.description }}</span>
<FormTextarea v-if="selectedTheme.desc" readonly :modelValue="selectedTheme.desc" class="_formBlock">
<template #label>{{ $ts._theme.description }}</template>
</FormTextarea>
<FormTextarea readonly tall :modelValue="selectedThemeCode">
<span>{{ $ts._theme.code }}</span>
<template #desc><button class="_textButton" @click="copyThemeCode()">{{ $ts.copy }}</button></template>
<FormTextarea readonly tall :modelValue="selectedThemeCode" class="_formBlock">
<template #label>{{ $ts._theme.code }}</template>
<template #caption><button class="_textButton" @click="copyThemeCode()">{{ $ts.copy }}</button></template>
</FormTextarea>
<FormButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" danger @click="uninstall()"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</FormButton>
<FormButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" class="_formBlock" danger @click="uninstall()"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</FormButton>
</template>
</FormBase>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as JSON5 from 'json5';
import FormTextarea from '@/components/debobigego/textarea.vue';
import FormSelect from '@/components/debobigego/select.vue';
import FormRadios from '@/components/debobigego/radios.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/debobigego/base.vue';
import FormGroup from '@/components/debobigego/group.vue';
import FormInput from '@/components/debobigego/input.vue';
import FormButton from '@/components/debobigego/button.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
import { Theme, builtinThemes } from '@/scripts/theme';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import * as os from '@/os';

View File

@ -1,5 +1,6 @@
<template>
<div v-size="{ min: [800] }" v-hotkey.global="keymap" class="cmuxhskf">
<MkSpacer :content-max="800">
<div v-hotkey.global="keymap" class="cmuxhskf">
<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
@ -15,6 +16,7 @@
/>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
@ -66,7 +68,7 @@ export default defineComponent({
icon: 'fas fa-home',
iconOnly: true,
onClick: () => { this.src = 'home'; this.saveSrc(); },
}, {
}, ...(this.isLocalTimelineAvailable ? [{
active: this.src === 'local',
title: this.$ts._timelines.local,
icon: 'fas fa-comments',
@ -78,13 +80,13 @@ export default defineComponent({
icon: 'fas fa-share-alt',
iconOnly: true,
onClick: () => { this.src = 'social'; this.saveSrc(); },
}, {
}] : []), ...(this.isGlobalTimelineAvailable ? [{
active: this.src === 'global',
title: this.$ts._timelines.global,
icon: 'fas fa-globe',
iconOnly: true,
onClick: () => { this.src = 'global'; this.saveSrc(); },
}],
}] : [])],
})),
};
},
@ -188,8 +190,6 @@ export default defineComponent({
<style lang="scss" scoped>
.cmuxhskf {
padding: var(--margin);
> .new {
position: sticky;
top: calc(var(--stickyTop, 0px) + 16px);
@ -213,10 +213,5 @@ export default defineComponent({
border-radius: var(--radius);
overflow: clip;
}
&.min-width_800px {
max-width: 800px;
margin: 0 auto;
}
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div>
<MkPagination #default="{items}" ref="list" :pagination="pagination">
<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
<b>{{ item.name }}</b>
<div v-if="item.description" class="description">{{ item.description }}</div>

View File

@ -1,6 +1,6 @@
<template>
<div>
<MkPagination #default="{items}" ref="list" :pagination="pagination" class="mk-following-or-followers">
<MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="mk-following-or-followers">
<div class="users _isolated">
<MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" class="user" :user="user"/>
</div>

View File

@ -1,6 +1,6 @@
<template>
<div>
<MkPagination #default="{items}" :pagination="pagination">
<MkPagination v-slot="{items}" :pagination="pagination">
<div class="jrnovfpt">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>

View File

@ -1,6 +1,6 @@
<template>
<div>
<MkPagination #default="{items}" ref="list" :pagination="pagination">
<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_gap"/>
</MkPagination>
</div>

View File

@ -1,6 +1,6 @@
<template>
<div>
<MkPagination #default="{items}" ref="list" :pagination="pagination">
<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
<div v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap afdcfbfb">
<div class="header">
<MkAvatar class="avatar" :user="user"/>

View File

@ -109,6 +109,14 @@ export function getUserMenu(user) {
return !confirm.canceled;
}
async function invalidateFollow() {
os.apiWithDialog('following/invalidate', {
userId: user.id
}).then(() => {
user.isFollowed = !user.isFollowed;
})
}
let menu = [{
icon: 'fas fa-at',
text: i18n.locale.copyUsername,
@ -153,6 +161,14 @@ export function getUserMenu(user) {
action: toggleBlock
}]);
if (user.isFollowed) {
menu = menu.concat([{
icon: 'fas fa-unlink',
text: i18n.locale.breakFollow,
action: invalidateFollow
}]);
}
menu = menu.concat([null, {
icon: 'fas fa-exclamation-circle',
text: i18n.locale.reportAbuse,

View File

@ -1,5 +1,6 @@
import { isScreenTouching } from '@/os';
import { Ref, ref } from 'vue';
import { isDeviceTouch } from './is-device-touch';
export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
let isHovering = false;
@ -13,7 +14,7 @@ export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
// iOS(Androidも)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、その対策
// これが無いと、画面に触れてないのにツールチップが出たりしてしまう
if (!isScreenTouching) return;
if (isDeviceTouch && !isScreenTouching) return;
const showing = ref(true);
onShow(showing);

View File

@ -0,0 +1,205 @@
<template>
<div class="kmwsukvl">
<div>
<button v-click-anime class="item _button account" @click="openAccountMenu">
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
</button>
<MkA v-click-anime class="item index" active-class="active" to="/" exact>
<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
</MkA>
<template v-for="item in menu">
<div v-if="item === '-'" class="divider"></div>
<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
</component>
</template>
<div class="divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
</MkA>
<button v-click-anime class="item _button" @click="more">
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
</button>
<MkA v-click-anime class="item" active-class="active" to="/settings">
<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
</MkA>
<button class="item _button post" data-cy-open-post-form @click="post">
<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
</button>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, toRef, watch } from 'vue';
import { host } from '@/config';
import { search } from '@/scripts/search';
import * as os from '@/os';
import { menuDef } from '@/menu';
import { openAccountMenu } from '@/account';
import { defaultStore } from '@/store';
export default defineComponent({
setup(props, context) {
const menu = toRef(defaultStore.state, 'menu');
const otherMenuItemIndicated = computed(() => {
for (const def in menuDef) {
if (menu.value.includes(def)) continue;
if (menuDef[def].indicated) return true;
}
return false;
});
return {
host: host,
accounts: [],
connection: null,
menu,
menuDef: menuDef,
otherMenuItemIndicated,
post: os.post,
search,
openAccountMenu,
more: () => {
os.popup(import('@/components/launch-pad.vue'), {}, {
}, 'closed');
},
};
},
});
</script>
<style lang="scss" scoped>
.kmwsukvl {
$ui-font-size: 1em; // TODO:
$avatar-size: 32px;
$avatar-margin: 8px;
> div {
> .divider {
margin: 16px 16px;
border-top: solid 0.5px var(--divider);
}
> .item {
position: relative;
display: block;
padding-left: 24px;
font-size: $ui-font-size;
line-height: 2.85rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
text-align: left;
box-sizing: border-box;
color: var(--navFg);
> i {
position: relative;
width: 32px;
}
> i,
> .avatar {
margin-right: $avatar-margin;
}
> .avatar {
width: $avatar-size;
height: $avatar-size;
vertical-align: middle;
}
> .indicator {
position: absolute;
top: 0;
left: 20px;
color: var(--navIndicator);
font-size: 8px;
animation: blink 1s infinite;
}
> .text {
position: relative;
font-size: 0.9em;
}
&:hover {
text-decoration: none;
color: var(--navHoverFg);
}
&.active {
color: var(--navActive);
}
&:hover, &.active {
&:before {
content: "";
display: block;
width: calc(100% - 24px);
height: 100%;
margin: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 999px;
background: var(--accentedBg);
}
}
&:first-child, &:last-child {
position: sticky;
z-index: 1;
padding-top: 8px;
padding-bottom: 8px;
background: var(--X14);
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(8px));
}
&:first-child {
top: 0;
&:hover, &.active {
&:before {
content: none;
}
}
}
&:last-child {
bottom: 0;
color: var(--fgOnAccent);
&:before {
content: "";
display: block;
width: calc(100% - 20px);
height: calc(100% - 20px);
margin: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
}
&:hover, &.active {
&:before {
background: var(--accentLighten);
}
}
}
}
}
}
</style>

View File

@ -1,15 +1,5 @@
<template>
<div class="mvcprjjd">
<transition name="nav-back">
<div v-if="showing"
class="nav-back _modalBg"
@click="showing = false"
@touchstart.passive="showing = false"
></div>
</transition>
<transition name="nav">
<nav v-show="showing" class="nav" :class="{ iconOnly, hidden }">
<div class="mvcprjjd" :class="{ iconOnly }">
<div>
<button v-click-anime class="item _button account" @click="openAccountMenu">
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
@ -30,7 +20,7 @@
</MkA>
<button v-click-anime class="item _button" @click="more">
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
<span v-if="otherNavItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
</button>
<MkA v-click-anime class="item" active-class="active" to="/settings">
<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
@ -39,144 +29,68 @@
<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
</button>
</div>
</nav>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, ref, watch } from 'vue';
import { host } from '@/config';
import { search } from '@/scripts/search';
import * as os from '@/os';
import { menuDef } from '@/menu';
import { openAccountMenu } from '@/account';
import { defaultStore } from '@/store';
export default defineComponent({
props: {
defaultHidden: {
type: Boolean,
required: false,
default: false,
}
},
setup(props, context) {
const iconOnly = ref(false);
data() {
return {
host: host,
showing: false,
accounts: [],
connection: null,
menuDef: menuDef,
iconOnly: false,
hidden: this.defaultHidden,
};
},
computed: {
menu(): string[] {
return this.$store.state.menu;
},
otherNavItemIndicated(): boolean {
for (const def in this.menuDef) {
if (this.menu.includes(def)) continue;
if (this.menuDef[def].indicated) return true;
const menu = computed(() => defaultStore.state.menu);
const otherMenuItemIndicated = computed(() => {
for (const def in menuDef) {
if (menu.value.includes(def)) continue;
if (menuDef[def].indicated) return true;
}
return false;
},
},
watch: {
$route(to, from) {
this.showing = false;
},
'$store.reactiveState.menuDisplay.value'() {
this.calcViewState();
},
iconOnly() {
this.$nextTick(() => {
this.$emit('change-view-mode');
});
},
hidden() {
this.$nextTick(() => {
this.$emit('change-view-mode');
const calcViewState = () => {
iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon');
};
calcViewState();
window.addEventListener('resize', calcViewState);
watch(defaultStore.reactiveState.menuDisplay, () => {
calcViewState();
});
}
},
created() {
window.addEventListener('resize', this.calcViewState);
this.calcViewState();
},
methods: {
calcViewState() {
this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.menuDisplay === 'sideIcon');
if (!this.defaultHidden) {
this.hidden = (window.innerWidth <= 650);
}
},
show() {
this.showing = true;
},
post() {
os.post();
},
search() {
search();
},
more(ev) {
return {
host: host,
accounts: [],
connection: null,
menu,
menuDef: menuDef,
otherMenuItemIndicated,
iconOnly,
post: os.post,
search,
openAccountMenu,
more: () => {
os.popup(import('@/components/launch-pad.vue'), {}, {
}, 'closed');
},
openAccountMenu,
}
};
},
});
</script>
<style lang="scss" scoped>
.nav-enter-active,
.nav-leave-active {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.nav-enter-from,
.nav-leave-active {
opacity: 0;
transform: translateX(-240px);
}
.nav-back-enter-active,
.nav-back-leave-active {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.nav-back-enter-from,
.nav-back-leave-active {
opacity: 0;
}
.mvcprjjd {
$ui-font-size: 1em; // TODO:
$nav-width: 250px;
$nav-icon-only-width: 86px;
> .nav-back {
z-index: 1001;
}
> .nav {
$avatar-size: 32px;
$avatar-margin: 8px;
@ -184,70 +98,6 @@ export default defineComponent({
width: $nav-width;
box-sizing: border-box;
&.iconOnly {
flex: 0 0 $nav-icon-only-width;
width: $nav-icon-only-width;
&:not(.hidden) {
> div {
width: $nav-icon-only-width;
> .divider {
margin: 8px auto;
width: calc(100% - 32px);
}
> .item {
padding-left: 0;
padding: 18px 0;
width: 100%;
text-align: center;
font-size: $ui-font-size * 1.1;
line-height: initial;
> i,
> .avatar {
display: block;
margin: 0 auto;
}
> i {
opacity: 0.7;
}
> .text {
display: none;
}
&:hover, &.active {
> i, > .text {
opacity: 1;
}
}
&:first-child {
margin-bottom: 8px;
}
&:last-child {
margin-top: 8px;
}
}
}
}
}
&.hidden {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
}
&:not(.hidden) {
display: block !important;
}
> div {
position: fixed;
top: 0;
@ -383,6 +233,75 @@ export default defineComponent({
}
}
}
&.iconOnly {
flex: 0 0 $nav-icon-only-width;
width: $nav-icon-only-width;
> div {
width: $nav-icon-only-width;
> .divider {
margin: 8px auto;
width: calc(100% - 32px);
}
> .item {
padding-left: 0;
padding: 18px 0;
width: 100%;
text-align: center;
font-size: $ui-font-size * 1.1;
line-height: initial;
> i,
> .avatar {
display: block;
margin: 0 auto;
}
> i {
opacity: 0.7;
}
> .text {
display: none;
}
&:hover, &.active {
> i, > .text {
opacity: 1;
}
}
&:first-child {
margin-bottom: 8px;
}
&:last-child {
margin-top: 8px;
}
&:before {
width: 100%;
border-radius: 0;
}
&.post {
height: $nav-icon-only-width;
> i {
opacity: 1;
}
}
&.post:before {
width: calc(100% - 32px);
height: calc(100% - 32px);
border-radius: 100%;
}
}
}
}
}
</style>

View File

@ -632,6 +632,7 @@ export default defineComponent({
text: this.$ts.pin,
action: () => this.togglePin(true)
} : undefined,
/*
...(this.$i.isModerator || this.$i.isAdmin ? [
null,
{
@ -640,7 +641,7 @@ export default defineComponent({
action: this.promote
}]
: []
),
),*/
...(this.appearNote.userId != this.$i.id ? [
null,
{

View File

@ -1,16 +1,14 @@
<template>
<div class="mk-app" :class="{ wallpaper, isMobile }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
<div class="gbhvwtnk" :class="{ wallpaper }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
<XHeaderMenu v-if="showMenuOnTop" v-get-size="(w, h) => globalHeaderHeight = h"/>
<div class="columns" :class="{ fullView, withGlobalHeader: showMenuOnTop }">
<template v-if="!isMobile">
<div v-if="!showMenuOnTop" class="sidebar">
<XSidebar/>
</div>
<div v-else ref="widgetsLeft" class="widgets left">
<XWidgets :place="'left'" @mounted="attachSticky('widgetsLeft')"/>
</div>
</template>
<main class="main" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
<div class="content">
@ -32,16 +30,6 @@
</div>
</div>
<div v-if="isMobile" class="buttons">
<button ref="navButton" class="button nav _button" @click="showDrawerNav"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
</div>
<XDrawerSidebar v-if="isMobile" ref="drawerNav" class="sidebar"/>
<transition name="tray-back">
<div v-if="widgetsShowing"
class="tray-back _modalBg"
@ -65,20 +53,17 @@ import { defineComponent, defineAsyncComponent, markRaw } from 'vue';
import { instanceName } from '@/config';
import { StickySidebar } from '@/scripts/sticky-sidebar';
import XSidebar from './classic.sidebar.vue';
import XDrawerSidebar from '@/ui/_common_/sidebar.vue';
import XCommon from './_common_/common.vue';
import * as os from '@/os';
import { menuDef } from '@/menu';
import * as symbols from '@/symbols';
const DESKTOP_THRESHOLD = 1100;
const MOBILE_THRESHOLD = 600;
export default defineComponent({
components: {
XCommon,
XSidebar,
XDrawerSidebar,
XHeaderMenu: defineAsyncComponent(() => import('./classic.header.vue')),
XWidgets: defineAsyncComponent(() => import('./classic.widgets.vue')),
},
@ -86,6 +71,7 @@ export default defineComponent({
provide() {
return {
shouldHeaderThin: this.showMenuOnTop,
shouldSpacerMin: true,
};
},
@ -94,7 +80,6 @@ export default defineComponent({
pageInfo: null,
menuDef: menuDef,
globalHeaderHeight: 0,
isMobile: window.innerWidth <= MOBILE_THRESHOLD,
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
widgetsShowing: false,
fullView: false,
@ -103,20 +88,17 @@ export default defineComponent({
},
computed: {
navIndicated(): boolean {
for (const def in this.menuDef) {
if (def === 'notifications') continue; //
if (this.menuDef[def].indicated) return true;
}
return false;
},
showMenuOnTop(): boolean {
return !this.isMobile && this.$store.state.menuDisplay === 'top';
return this.$store.state.menuDisplay === 'top';
}
},
created() {
if (window.innerWidth < 1024) {
localStorage.setItem('ui', 'default');
location.reload();
}
document.documentElement.style.overflowY = 'scroll';
if (this.$store.state.widgets.length === 0) {
@ -135,7 +117,6 @@ export default defineComponent({
mounted() {
window.addEventListener('resize', () => {
this.isMobile = (window.innerWidth <= MOBILE_THRESHOLD);
this.isDesktop = (window.innerWidth >= DESKTOP_THRESHOLD);
}, { passive: true });
@ -178,22 +159,10 @@ export default defineComponent({
}, { passive: true });
},
post() {
os.post();
},
top() {
window.scroll({ top: 0, behavior: 'smooth' });
},
back() {
history.back();
},
showDrawerNav() {
this.$refs.drawerNav.show();
},
onTransition() {
if (window._scroll) window._scroll();
},
@ -257,10 +226,9 @@ export default defineComponent({
opacity: 0;
}
.mk-app {
.gbhvwtnk {
$ui-font-size: 1em;
$widgets-hide-threshold: 1200px;
$nav-icon-only-width: 78px; // TODO:
// 100vh ... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
min-height: calc(var(--vh, 1vh) * 100);
@ -271,21 +239,6 @@ export default defineComponent({
//backdrop-filter: var(--blur, blur(4px));
}
&.isMobile {
> .columns {
display: block;
margin: 0;
> .main {
margin: 0;
padding-bottom: 92px;
border: none;
width: 100%;
border-radius: 0;
}
}
}
> .columns {
display: flex;
justify-content: center;
@ -371,76 +324,6 @@ export default defineComponent({
}
}
> .buttons {
position: fixed;
z-index: 1000;
bottom: 0;
padding: 16px;
display: flex;
width: 100%;
box-sizing: border-box;
-webkit-backdrop-filter: var(--blur, blur(32px));
backdrop-filter: var(--blur, blur(32px));
background-color: var(--header);
border-top: solid 0.5px var(--divider);
> .button {
position: relative;
flex: 1;
padding: 0;
margin: auto;
height: 64px;
border-radius: 8px;
background: var(--panel);
color: var(--fg);
&:not(:last-child) {
margin-right: 12px;
}
@media (max-width: 400px) {
height: 60px;
&:not(:last-child) {
margin-right: 8px;
}
}
&:hover {
background: var(--X2);
}
> .indicator {
position: absolute;
top: 0;
left: 0;
color: var(--indicator);
font-size: 16px;
animation: blink 1s infinite;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
> * {
font-size: 22px;
}
&:disabled {
cursor: default;
> * {
opacity: 0.5;
}
}
}
}
> .tray-back {
z-index: 1001;
}

View File

@ -1,8 +1,8 @@
<template>
<div class="mk-deck" :class="`${deckStore.reactiveState.columnAlign.value}`" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
<div class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
@contextmenu.self.prevent="onContextmenu"
>
<XSidebar ref="nav"/>
<XSidebar v-if="!isMobile"/>
<template v-for="ids in layout">
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
@ -22,91 +22,76 @@
/>
</template>
<button v-if="$i" class="nav _button" @click="showNav()"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
<button v-if="$i" class="post _buttonPrimary" @click="post()"><i class="fas fa-pencil-alt"></i></button>
<div v-if="isMobile" class="buttons">
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="button home _button" @click="$router.push('/')"><i class="fas fa-home"></i></button>
<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
</div>
<transition name="menu-back">
<div v-if="drawerMenuShowing"
class="menu-back _modalBg"
@click="drawerMenuShowing = false"
@touchstart.passive="drawerMenuShowing = false"
></div>
</transition>
<transition name="menu">
<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
</transition>
<XCommon/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, provide, ref, watch } from 'vue';
import { v4 as uuid } from 'uuid';
import { host } from '@/config';
import DeckColumnCore from '@/ui/deck/column-core.vue';
import XSidebar from '@/ui/_common_/sidebar.vue';
import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
import { getScrollContainer } from '@/scripts/scroll';
import * as os from '@/os';
import { menuDef } from '@/menu';
import XCommon from './_common_/common.vue';
import { deckStore, addColumn, loadDeck } from './deck/deck-store';
import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store';
import { useRoute } from 'vue-router';
import { $i } from '@/account';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
XCommon,
XSidebar,
XDrawerMenu,
DeckColumnCore,
},
provide() {
return deckStore.state.navWindow ? {
navHook: (url) => {
os.pageWindow(url);
}
} : {};
},
setup() {
const isMobile = ref(window.innerWidth <= 500);
window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= 500;
});
data() {
return {
deckStore,
host: host,
menuDef: menuDef,
wallpaper: localStorage.getItem('wallpaper') != null,
};
},
const drawerMenuShowing = ref(false);
computed: {
columns() {
return deckStore.reactiveState.columns.value;
},
layout() {
return deckStore.reactiveState.layout.value;
},
navIndicated(): boolean {
if (!this.$i) return false;
for (const def in this.menuDef) {
if (this.menuDef[def].indicated) return true;
const route = useRoute();
watch(route, () => {
drawerMenuShowing.value = false;
});
const columns = deckStore.reactiveState.columns;
const layout = deckStore.reactiveState.layout.value;
const menuIndicated = computed(() => {
if ($i == null) return false;
for (const def in menuDef) {
if (menuDef[def].indicated) return true;
}
return false;
},
},
});
created() {
document.documentElement.style.overflowY = 'hidden';
document.documentElement.style.scrollBehavior = 'auto';
window.addEventListener('wheel', this.onWheel);
loadDeck();
},
mounted() {
},
methods: {
onWheel(e) {
if (getScrollContainer(e.target) == null) {
document.documentElement.scrollLeft += e.deltaY > 0 ? 96 : -96;
}
},
showNav() {
this.$refs.nav.show();
},
post() {
os.post();
},
async addColumn(ev) {
const addColumn = async (ev) => {
const columns = [
'main',
'widgets',
@ -119,33 +104,83 @@ export default defineComponent({
];
const { canceled, result: column } = await os.select({
title: this.$ts._deck.addColumn,
title: i18n.locale._deck.addColumn,
items: columns.map(column => ({
value: column, text: this.$t('_deck._columns.' + column)
value: column, text: i18n.t('_deck._columns.' + column)
}))
});
if (canceled) return;
addColumn({
addColumnToStore({
type: column,
id: uuid(),
name: this.$t('_deck._columns.' + column),
name: i18n.t('_deck._columns.' + column),
width: 330,
});
},
};
onContextmenu(e) {
const onContextmenu = (ev) => {
os.contextMenu([{
text: this.$ts._deck.addColumn,
text: i18n.locale._deck.addColumn,
icon: null,
action: this.addColumn
}], e);
},
action: addColumn
}], ev);
};
provide('shouldSpacerMin', true);
if (deckStore.state.navWindow) {
provide('navHook', (url) => {
os.pageWindow(url);
});
}
document.documentElement.style.overflowY = 'hidden';
document.documentElement.style.scrollBehavior = 'auto';
window.addEventListener('wheel', (ev) => {
if (getScrollContainer(ev.target) == null) {
document.documentElement.scrollLeft += ev.deltaY > 0 ? 96 : -96;
}
});
loadDeck();
return {
isMobile,
deckStore,
drawerMenuShowing,
columns,
layout,
menuIndicated,
onContextmenu,
wallpaper: localStorage.getItem('wallpaper') != null,
post: os.post,
};
},
});
</script>
<style lang="scss" scoped>
.menu-enter-active,
.menu-leave-active {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.menu-enter-from,
.menu-leave-active {
opacity: 0;
transform: translateX(-240px);
}
.menu-back-enter-active,
.menu-back-leave-active {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.menu-back-enter-from,
.menu-back-leave-active {
opacity: 0;
}
.mk-deck {
$nav-hide-threshold: 650px; // TODO:
@ -169,6 +204,10 @@ export default defineComponent({
}
}
&.isMobile {
padding-bottom: 100px;
}
> .column {
flex-shrink: 0;
margin-right: var(--deckMargin);
@ -183,31 +222,38 @@ export default defineComponent({
}
}
> .post,
> .nav {
> .buttons {
position: fixed;
z-index: 1000;
bottom: 32px;
width: 64px;
bottom: 0;
left: 0;
padding: 16px;
display: flex;
width: 100%;
box-sizing: border-box;
> .button {
position: relative;
flex: 1;
padding: 0;
margin: auto;
height: 64px;
border-radius: 100%;
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
font-size: 22px;
@media (min-width: ($nav-hide-threshold + 1px)) {
display: none;
}
}
> .post {
right: 32px;
}
> .nav {
left: 32px;
border-radius: 8px;
background: var(--panel);
color: var(--fg);
&:not(:last-child) {
margin-right: 12px;
}
@media (max-width: 400px) {
height: 60px;
&:not(:last-child) {
margin-right: 8px;
}
}
&:hover {
background: var(--X2);
}
@ -220,6 +266,45 @@ export default defineComponent({
font-size: 16px;
animation: blink 1s infinite;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
> * {
font-size: 22px;
}
&:disabled {
cursor: default;
> * {
opacity: 0.5;
}
}
}
}
> .menu-back {
z-index: 1001;
}
> .menu {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
// 100vh ... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
height: calc(var(--vh, 1vh) * 100);
width: 240px;
box-sizing: border-box;
overflow: auto;
overscroll-behavior: contain;
background: var(--bg);
}
}
</style>

View File

@ -401,6 +401,7 @@ export default defineComponent({
height: calc(100% - var(--deckColumnHeaderHeight));
overflow: auto;
overflow-x: hidden;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
}

View File

@ -1,9 +1,9 @@
<template>
<div class="mk-app" :class="{ wallpaper }">
<XSidebar ref="nav" class="sidebar"/>
<div class="dkgtipfy" :class="{ wallpaper }">
<XSidebar v-if="!isMobile" class="sidebar"/>
<div ref="contents" class="contents" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
<main ref="main">
<div class="contents" :style="{ background: pageInfo?.bg }" @contextmenu.stop="onContextmenu">
<main>
<div class="content">
<MkStickyContainer>
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
@ -20,32 +20,44 @@
</main>
</div>
<XSide v-if="isDesktop" ref="side" class="side"/>
<XSideView v-if="isDesktop" ref="side" class="side"/>
<div v-if="isDesktop" ref="widgets" class="widgets">
<div v-if="isDesktop" ref="widgetsEl" class="widgets">
<XWidgets @mounted="attachSticky"/>
</div>
<div class="buttons" :class="{ navHidden }">
<button ref="navButton" class="button nav _button" @click="showNav"><i class="fas fa-bars"></i><span v-if="navIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="widgetButton _button" :class="{ show: true }" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
<div v-if="isMobile" class="buttons">
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="button home _button" @click="$route.name === 'index' ? top() : $router.push('/')"><i class="fas fa-home"></i></button>
<button class="button notifications _button" @click="$router.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="button widget _button" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
<button class="button post _button" @click="post"><i class="fas fa-pencil-alt"></i></button>
<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
</div>
<button class="widgetButton _button" :class="{ navHidden }" @click="widgetsShowing = true"><i class="fas fa-layer-group"></i></button>
<transition name="menuDrawer-back">
<div v-if="drawerMenuShowing"
class="menuDrawer-back _modalBg"
@click="drawerMenuShowing = false"
@touchstart.passive="drawerMenuShowing = false"
></div>
</transition>
<transition name="tray-back">
<transition name="menuDrawer">
<XDrawerMenu v-if="drawerMenuShowing" class="menuDrawer"/>
</transition>
<transition name="widgetsDrawer-back">
<div v-if="widgetsShowing"
class="tray-back _modalBg"
class="widgetsDrawer-back _modalBg"
@click="widgetsShowing = false"
@touchstart.passive="widgetsShowing = false"
></div>
</transition>
<transition name="tray">
<XWidgets v-if="widgetsShowing" class="tray"/>
<transition name="widgetsDrawer">
<XWidgets v-if="widgetsShowing" class="widgetsDrawer"/>
</transition>
<XCommon/>
@ -53,60 +65,69 @@
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { defineComponent, defineAsyncComponent, provide, onMounted, computed, ref, watch } from 'vue';
import { instanceName } from '@/config';
import { StickySidebar } from '@/scripts/sticky-sidebar';
import XSidebar from '@/ui/_common_/sidebar.vue';
import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
import XCommon from './_common_/common.vue';
import XSide from './classic.side.vue';
import XSideView from './classic.side.vue';
import * as os from '@/os';
import { menuDef } from '@/menu';
import * as symbols from '@/symbols';
import { defaultStore } from '@/store';
import * as EventEmitter from 'eventemitter3';
import { menuDef } from '@/menu';
import { useRoute } from 'vue-router';
import { i18n } from '@/i18n';
const DESKTOP_THRESHOLD = 1100;
const MOBILE_THRESHOLD = 500;
export default defineComponent({
components: {
XCommon,
XSidebar,
XDrawerMenu,
XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')),
XSide, // NOTE: dynamic importAsyncComponentWrapperref
XSideView, // NOTE: dynamic importAsyncComponentWrapperref
},
provide() {
return {
sideViewHook: this.isDesktop ? (url) => {
this.$refs.side.navigate(url);
} : null
};
},
setup() {
const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
const isMobile = ref(window.innerWidth <= MOBILE_THRESHOLD);
window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= MOBILE_THRESHOLD;
});
data() {
return {
pageInfo: null,
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
menuDef: menuDef,
navHidden: false,
widgetsShowing: false,
wallpaper: localStorage.getItem('wallpaper') != null,
};
},
const pageInfo = ref();
const widgetsEl = ref<HTMLElement>();
const widgetsShowing = ref(false);
computed: {
navIndicated(): boolean {
for (const def in this.menuDef) {
const sideViewController = new EventEmitter();
provide('sideViewHook', isDesktop.value ? (url) => {
sideViewController.emit('navigate', url);
} : null);
const menuIndicated = computed(() => {
for (const def in menuDef) {
if (def === 'notifications') continue; //
if (this.menuDef[def].indicated) return true;
if (menuDef[def].indicated) return true;
}
return false;
}
},
});
const drawerMenuShowing = ref(false);
const route = useRoute();
watch(route, () => {
drawerMenuShowing.value = false;
});
created() {
document.documentElement.style.overflowY = 'scroll';
if (this.$store.state.widgets.length === 0) {
this.$store.set('widgets', [{
if (defaultStore.state.widgets.length === 0) {
defaultStore.set('widgets', [{
name: 'calendar',
id: 'a', place: 'right', data: {}
}, {
@ -117,123 +138,129 @@ export default defineComponent({
id: 'c', place: 'right', data: {}
}]);
}
},
mounted() {
this.adjustUI();
const ro = new ResizeObserver((entries, observer) => {
this.adjustUI();
onMounted(() => {
if (!isDesktop.value) {
window.addEventListener('resize', () => {
if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
}, { passive: true });
}
});
ro.observe(this.$refs.contents);
window.addEventListener('resize', this.adjustUI, { passive: true });
if (!this.isDesktop) {
window.addEventListener('resize', () => {
if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
}, { passive: true });
}
},
methods: {
changePage(page) {
const changePage = (page) => {
if (page == null) return;
if (page[symbols.PAGE_INFO]) {
this.pageInfo = page[symbols.PAGE_INFO];
document.title = `${this.pageInfo.title} | ${instanceName}`;
pageInfo.value = page[symbols.PAGE_INFO];
document.title = `${pageInfo.value.title} | ${instanceName}`;
}
},
};
adjustUI() {
const navWidth = this.$refs.nav.$el.offsetWidth;
this.navHidden = navWidth === 0;
},
showNav() {
this.$refs.nav.show();
},
attachSticky(el) {
const sticky = new StickySidebar(this.$refs.widgets);
window.addEventListener('scroll', () => {
sticky.calc(window.scrollY);
}, { passive: true });
},
post() {
os.post();
},
top() {
window.scroll({ top: 0, behavior: 'smooth' });
},
back() {
history.back();
},
onTransition() {
if (window._scroll) window._scroll();
},
onContextmenu(e) {
const onContextmenu = (ev) => {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(e.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (isLink(ev.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;
const path = route.path;
os.contextMenu([{
type: 'label',
text: path,
}, {
icon: 'fas fa-columns',
text: this.$ts.openInSideView,
text: i18n.locale.openInSideView,
action: () => {
this.$refs.side.navigate(path);
}
}, {
icon: 'fas fa-window-maximize',
text: this.$ts.openInWindow,
text: i18n.locale.openInWindow,
action: () => {
os.pageWindow(path);
}
}], e);
}], ev);
};
const attachSticky = (el) => {
const sticky = new StickySidebar(widgetsEl.value);
window.addEventListener('scroll', () => {
sticky.calc(window.scrollY);
}, { passive: true });
};
return {
pageInfo,
isDesktop,
isMobile,
widgetsEl,
widgetsShowing,
drawerMenuShowing,
menuIndicated,
wallpaper: localStorage.getItem('wallpaper') != null,
changePage,
top: () => {
window.scroll({ top: 0, behavior: 'smooth' });
},
onTransition: () => {
if (window._scroll) window._scroll();
},
post: os.post,
onContextmenu,
attachSticky,
};
},
}
});
</script>
<style lang="scss" scoped>
.tray-enter-active,
.tray-leave-active {
.widgetsDrawer-enter-active,
.widgetsDrawer-leave-active {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.tray-enter-from,
.tray-leave-active {
.widgetsDrawer-enter-from,
.widgetsDrawer-leave-active {
opacity: 0;
transform: translateX(240px);
}
.tray-back-enter-active,
.tray-back-leave-active {
.widgetsDrawer-back-enter-active,
.widgetsDrawer-back-leave-active {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.tray-back-enter-from,
.tray-back-leave-active {
.widgetsDrawer-back-enter-from,
.widgetsDrawer-back-leave-active {
opacity: 0;
}
.mk-app {
.menuDrawer-enter-active,
.menuDrawer-leave-active {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.menuDrawer-enter-from,
.menuDrawer-leave-active {
opacity: 0;
transform: translateX(-240px);
}
.menuDrawer-back-enter-active,
.menuDrawer-back-leave-active {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.menuDrawer-back-enter-from,
.menuDrawer-back-leave-active {
opacity: 0;
}
.dkgtipfy {
$ui-font-size: 1em; // TODO:
$widgets-hide-threshold: 1090px;
@ -248,6 +275,7 @@ export default defineComponent({
}
> .sidebar {
border-right: solid 0.5px var(--divider);
}
> .contents {
@ -284,6 +312,7 @@ export default defineComponent({
}
}
/*
> .widgetButton {
display: block;
position: fixed;
@ -304,12 +333,35 @@ export default defineComponent({
@media (min-width: ($widgets-hide-threshold + 1px)) {
display: none;
}
}*/
> .widgetButton {
display: none;
}
> .widgetsDrawer-back {
z-index: 1001;
}
> .widgetsDrawer {
position: fixed;
top: 0;
right: 0;
z-index: 1001;
// 100vh ... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
height: calc(var(--vh, 1vh) * 100);
padding: var(--margin);
box-sizing: border-box;
overflow: auto;
overscroll-behavior: contain;
background: var(--bg);
}
> .buttons {
position: fixed;
z-index: 1000;
bottom: 0;
left: 0;
padding: 16px;
display: flex;
width: 100%;
@ -318,10 +370,6 @@ export default defineComponent({
backdrop-filter: var(--blur, blur(32px));
background-color: var(--header);
&:not(.navHidden) {
display: none;
}
> .button {
position: relative;
flex: 1;
@ -379,22 +427,24 @@ export default defineComponent({
}
}
> .tray-back {
> .menuDrawer-back {
z-index: 1001;
}
> .tray {
> .menuDrawer {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 1001;
// 100vh ... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
height: calc(var(--vh, 1vh) * 100);
padding: var(--margin);
width: 240px;
box-sizing: border-box;
overflow: auto;
overscroll-behavior: contain;
background: var(--bg);
}
}
</style>

View File

@ -16,7 +16,7 @@
</div>
<div class="announcements panel">
<header>{{ $ts.announcements }}</header>
<MkPagination #default="{items}" :pagination="announcements" class="list">
<MkPagination v-slot="{items}" :pagination="announcements" class="list">
<section v-for="announcement in items" :key="announcement.id" class="item">
<div class="title">{{ announcement.title }}</div>
<div class="content">

View File

@ -631,7 +631,7 @@ blob-util@^2.0.2:
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
bluebird@3.7.2, bluebird@^3.7.2:
bluebird@3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
@ -1115,10 +1115,10 @@ csso@~2.3.1:
clap "^1.0.9"
source-map "^0.5.3"
cypress@9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.0.0.tgz#8c496f7f350e611604cc2f77b663fb81d0c235d2"
integrity sha512-/93SWBZTw7BjFZ+I9S8SqkFYZx7VhedDjTtRBmXO0VzTeDbmxgK/snMJm/VFjrqk/caWbI+XY4Qr80myDMQvYg==
cypress@9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.1.0.tgz#5d23c1b363b7d4853009c74a422a083a8ad2601c"
integrity sha512-fyXcCN51vixkPrz/vO/Qy6WL3hKYJzCQFeWofOpGOFewVVXrGfmfSOGFntXpzWBXsIwPn3wzW0HOFw51jZajNQ==
dependencies:
"@cypress/request" "^2.88.7"
"@cypress/xvfb" "^1.2.4"
@ -1127,7 +1127,7 @@ cypress@9.0.0:
"@types/sizzle" "^2.3.2"
arch "^2.2.0"
blob-util "^2.0.2"
bluebird "^3.7.2"
bluebird "3.7.2"
cachedir "^2.3.0"
chalk "^4.1.0"
check-more-types "^2.24.0"