diff --git a/CHANGELOG.md b/CHANGELOG.md index 712e57f0c7..75f98d6785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - クライアント: ノートにモデレーターバッジを表示するのを廃止 diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index cd69d921b8..c831fafff1 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -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: diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 39748e2b23..69d90c7624 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -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" diff --git a/locales/en-US.yml b/locales/en-US.yml index c5c8afb50f..5388a7a636 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -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" diff --git a/locales/eo-UY.yml b/locales/eo-UY.yml index 7b641303b1..a57206e090 100644 --- a/locales/eo-UY.yml +++ b/locales/eo-UY.yml @@ -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" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 3421c64389..f81c3772aa 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -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" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 5d67b5269a..cf5e2238b1 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -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" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index f4997e3a64..d9e6368c3a 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -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" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index fc032e068c..d650f44357 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -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" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ba3ea3b7f3..3a3c15601c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -792,6 +792,7 @@ pubSub: "Pub/Subのアカウント" lastCommunication: "直近の通信" resolved: "解決済み" unresolved: "未解決" +breakFollow: "フォロワーを解除" itsOn: "オンになっています" itsOff: "オフになっています" emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index b73be17035..49ef286a59 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -700,6 +700,7 @@ _mfm: spin: "アニメーション(回転)" blur: "ぼかし" font: "フォント" + rotate: "回転" _reversi: reversi: "リバーシ" gameSettings: "対局の設定" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 10a9b6e3e1..dde60c4c81 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -899,6 +899,7 @@ _mfm: rainbowDescription: "내용을 무지개로 표시합니다." sparkle: "반짝반짝" sparkleDescription: "반짝이는 파티클 효과를 추가합니다." + rotate: "회전" _reversi: reversi: "리버시" gameSettings: "대국 설정" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 7a3c568f6f..0393d94303 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -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" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 5e1fba8382..1a52f35235 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -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" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index c46f02f102..d095887bdc 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -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" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 1eb6dff0ef..1d889866db 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -922,6 +922,7 @@ _mfm: rainbowDescription: "Заставлять содержимое отображаться в цветах радуги." sparkle: "Блеск" sparkleDescription: "Добавьте эффект искрящихся частиц." + rotate: "Повернуть" _reversi: reversi: "Реверси" gameSettings: "Настройки игры" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 73f43669af..9104b0839d 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -771,6 +771,7 @@ _mfm: blurDescription: "Цей ефект зробить контент розмитим. Контент можна зробити чітким, якщо навести на нього вказівник миші." font: "Шрифт" fontDescription: "Встановлює шрифт для контенту." + rotate: "Обертати" _reversi: reversi: "Реверсі" gameSettings: "Налаштування гри" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 604f1e74d8..829c47e7dc 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -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: "对局设置" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 888e12490a..56eaaa0f0d 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -840,6 +840,7 @@ _mfm: blur: "模糊" font: "字型" fontDescription: "您可以設定顯示內容的字型" + rotate: "旋轉" _reversi: reversi: "黑白棋" gameSettings: "對弈設定" diff --git a/package.json b/package.json index 343e3f36cc..ef08f2d5e8 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/packages/backend/src/misc/get-file-info.ts b/packages/backend/src/misc/get-file-info.ts index 39ba541395..8d7f6b1bf9 100644 --- a/packages/backend/src/misc/get-file-info.ts +++ b/packages/backend/src/misc/get-file-info.ts @@ -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); diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 698dfac222..4ec7b94ed2 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -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') diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index ddf9a46afd..f2f0308dc0 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -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, diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index fc0860970c..81468d6de2 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -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; diff --git a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts index 356547440f..049437b18f 100644 --- a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts +++ b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts @@ -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`; }; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts new file mode 100644 index 0000000000..5112d1d4ea --- /dev/null +++ b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts @@ -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: フォローされていない`; +}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/index.ts b/packages/backend/src/remote/activitypub/kernel/undo/index.ts index 14b1add152..8de78420e3 100644 --- a/packages/backend/src/remote/activitypub/kernel/undo/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/undo/index.ts @@ -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)}`; }; diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts new file mode 100644 index 0000000000..c0e9df3652 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -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); +}); diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts index 620324361f..30d0e094c3 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/reject.ts @@ -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'; diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 6c5fefd4ad..a57f9cf068 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -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; diff --git a/packages/backend/src/services/following/delete.ts b/packages/backend/src/services/following/delete.ts index 29e3372b6a..ea612147df 100644 --- a/packages/backend/src/services/following/delete.ts +++ b/packages/backend/src/services/following/delete.ts @@ -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']; }) { diff --git a/packages/backend/src/services/following/reject.ts b/packages/backend/src/services/following/reject.ts new file mode 100644 index 0000000000..0ec4d7d00c --- /dev/null +++ b/packages/backend/src/services/following/reject.ts @@ -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); +} diff --git a/packages/backend/src/services/following/requests/reject.ts b/packages/backend/src/services/following/requests/reject.ts deleted file mode 100644 index 41cebd9e41..0000000000 --- a/packages/backend/src/services/following/requests/reject.ts +++ /dev/null @@ -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); - }); -} diff --git a/packages/backend/test/get-file-info.ts b/packages/backend/test/get-file-info.ts index cc9eefbfc6..a0146bd815 100644 --- a/packages/backend/test/get-file-info.ts +++ b/packages/backend/test/get-file-info.ts @@ -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, }); })); }); diff --git a/packages/backend/test/resources/rotate.jpg b/packages/backend/test/resources/rotate.jpg new file mode 100644 index 0000000000..477c2baf5b Binary files /dev/null and b/packages/backend/test/resources/rotate.jpg differ diff --git a/packages/client/src/components/form-dialog.vue b/packages/client/src/components/form-dialog.vue index 27810d315a..efd0da443d 100644 --- a/packages/client/src/components/form-dialog.vue +++ b/packages/client/src/components/form-dialog.vue @@ -12,66 +12,67 @@ <template #header> {{ title }} </template> - <FormBase class="xkpnjxcv"> - <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> - <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> - <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> - <FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]"> - <span v-text="form[item].label || item"></span> - <template v-if="form[item].description" #desc>{{ form[item].description }}</template> - </FormSwitch> - <FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]"> - <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> - <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"> - <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> - </FormRange> - <FormButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)"> - <span v-text="form[item].content || item"></span> - </FormButton> - </template> - </FormBase> + + <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" 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" 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]" 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]" class="_formBlock"> + <span v-text="form[item].label || item"></span> + <template v-if="form[item].description" #caption>{{ form[item].description }}</template> + </FormSwitch> + <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]" 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" :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" #caption>{{ form[item].description }}</template> + </FormRange> + <MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)" class="_formBlock"> + <span v-text="form[item].content || item"></span> + </MkButton> + </template> + </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, }, diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue index 79a83d6a93..3e02cacb9b 100644 --- a/packages/client/src/components/form/range.vue +++ b/packages/client/src/components/form/range.vue @@ -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(); diff --git a/packages/client/src/components/global/spacer.vue b/packages/client/src/components/global/spacer.vue index 34297a3c8b..e2f1d1aec7 100644 --- a/packages/client/src/components/global/spacer.vue +++ b/packages/client/src/components/global/spacer.vue @@ -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; diff --git a/packages/client/src/components/media-list.vue b/packages/client/src/components/media-list.vue index 4eef95af54..79fe36b540 100644 --- a/packages/client/src/components/media-list.vue +++ b/packages/client/src/components/media-list.vue @@ -44,16 +44,36 @@ export default defineComponent({ onMounted(() => { const lightbox = new PhotoSwipeLightbox({ - dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => ({ - src: media.url, - w: media.properties.width, - h: media.properties.height, - alt: media.name, - })), + 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; }); diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index 03f6a767f2..55a02f1e73 100644 --- a/packages/client/src/components/note-detailed.vue +++ b/packages/client/src/components/note-detailed.vue @@ -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, { diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index 25d4b48147..c4040388a9 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -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, { diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue index c2f760acbd..fad0cf1593 100644 --- a/packages/client/src/components/poll-editor.vue +++ b/packages/client/src/components/poll-editor.vue @@ -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; diff --git a/packages/client/src/components/poll.vue b/packages/client/src/components/poll.vue index 20a9900258..171b4a4770 100644 --- a/packages/client/src/components/poll.vue +++ b/packages/client/src/components/poll.vue @@ -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, - }; - }, - 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) - }); - }, - 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; + 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; + } }; - update(); + tick(); + const intevalId = window.setInterval(tick, 3000); + onUnmounted(() => { + window.clearInterval(intevalId); + }); } - }, - 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; - } } } } diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue index 6f75e12a77..9bad9a84f8 100644 --- a/packages/client/src/components/post-form.vue +++ b/packages/client/src/components/post-form.vue @@ -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; diff --git a/packages/client/src/components/reaction-tooltip.vue b/packages/client/src/components/reaction-tooltip.vue index a52c295277..dda8e7c6d7 100644 --- a/packages/client/src/components/reaction-tooltip.vue +++ b/packages/client/src/components/reaction-tooltip.vue @@ -41,6 +41,7 @@ export default defineComponent({ > .icon { display: block; width: 60px; + font-size: 60px; // unicodeな絵文字についてはwidthが効かないため margin: 0 auto; } diff --git a/packages/client/src/components/reactions-viewer.details.vue b/packages/client/src/components/reactions-viewer.details.vue index 63c22b98c4..d6374517a2 100644 --- a/packages/client/src/components/reactions-viewer.details.vue +++ b/packages/client/src/components/reactions-viewer.details.vue @@ -62,6 +62,7 @@ export default defineComponent({ > .icon { display: block; width: 60px; + font-size: 60px; // unicodeな絵文字についてはwidthが効かないため margin: 0 auto; } diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue index 6ca5e32555..687ce5e548 100644 --- a/packages/client/src/components/ui/menu.vue +++ b/packages/client/src/components/ui/menu.vue @@ -153,6 +153,7 @@ export default defineComponent({ box-sizing: border-box; min-width: 200px; overflow: auto; + overscroll-behavior: contain; &.center { > .item { diff --git a/packages/client/src/components/ui/super-menu.vue b/packages/client/src/components/ui/super-menu.vue index cb2154c48d..63a1d7063d 100644 --- a/packages/client/src/components/ui/super-menu.vue +++ b/packages/client/src/components/ui/super-menu.vue @@ -52,7 +52,7 @@ export default defineComponent({ > .title { opacity: 0.7; - margin: 0 0 8px 12px; + margin: 0 0 8px 0; } > .items { diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts index b96671be35..0e36322cd9 100644 --- a/packages/client/src/directives/tooltip.ts +++ b/packages/client/src/directives/tooltip.ts @@ -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'; diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts index ae74740bb8..bd155ba16d 100644 --- a/packages/client/src/menu.ts +++ b/packages/client/src/menu.ts @@ -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); }, }, -}; +}); diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 30f6b35964..37b57557c3 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -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インスタンスが使いまわされ、 diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue index a3a3d3cfb7..04f68b7201 100644 --- a/packages/client/src/pages/about.vue +++ b/packages/client/src/pages/about.vue @@ -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> diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue index ff1c4c57fc..8df20097b3 100644 --- a/packages/client/src/pages/admin/abuses.vue +++ b/packages/client/src/pages/admin/abuses.vue @@ -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"/> diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue index 6f9a955da2..9c9b3b2d4f 100644 --- a/packages/client/src/pages/admin/emojis.vue +++ b/packages/client/src/pages/admin/emojis.vue @@ -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"/> diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue index a6b0f8e59e..032e394a66 100644 --- a/packages/client/src/pages/admin/files.vue +++ b/packages/client/src/pages/admin/files.vue @@ -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"> diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue index 016a013e51..e7a3437167 100644 --- a/packages/client/src/pages/admin/users.vue +++ b/packages/client/src/pages/admin/users.vue @@ -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"> diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue index 34879a18bd..ca94640dda 100644 --- a/packages/client/src/pages/announcements.vue +++ b/packages/client/src/pages/announcements.vue @@ -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"> diff --git a/packages/client/src/pages/api-console.vue b/packages/client/src/pages/api-console.vue index 1c41315d21..16018be712 100644 --- a/packages/client/src/pages/api-console.vue +++ b/packages/client/src/pages/api-console.vue @@ -1,26 +1,28 @@ <template> -<div class="_root"> - <div class="_block" style="padding: 24px;"> - <MkInput v-model="endpoint" :datalist="endpoints" class="" @update:modelValue="onEndpointChange()"> - <template #label>Endpoint</template> - </MkInput> - <MkTextarea v-model="body" code> - <template #label>Params (JSON or JSON5)</template> - </MkTextarea> - <MkSwitch v-model="withCredential"> - With credential - </MkSwitch> - <MkButton primary full :disabled="sending" @click="send"> - <template v-if="sending"><MkEllipsis/></template> - <template v-else><i class="fas fa-paper-plane"></i> Send</template> - </MkButton> +<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" class="_formBlock" code> + <template #label>Params (JSON or JSON5)</template> + </MkTextarea> + <MkSwitch v-model="withCredential" class="_formBlock"> + With credential + </MkSwitch> + <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="_formBlock"> + <MkTextarea v-model="res" code readonly tall> + <template #label>Response</template> + </MkTextarea> + </div> </div> - <div v-if="res" class="_block" style="padding: 24px;"> - <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 => { diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue index a7bd8a018c..a08c273279 100644 --- a/packages/client/src/pages/channels.vue +++ b/packages/client/src/pages/channels.vue @@ -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> diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue index a868c34478..4e5f428ff9 100644 --- a/packages/client/src/pages/federation.vue +++ b/packages/client/src/pages/federation.vue @@ -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> diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue index a4de393995..54d695091d 100644 --- a/packages/client/src/pages/follow-requests.vue +++ b/packages/client/src/pages/follow-requests.vue @@ -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"> diff --git a/packages/client/src/pages/gallery/index.vue b/packages/client/src/pages/gallery/index.vue index a036f4286b..cd0d2a40e4 100644 --- a/packages/client/src/pages/gallery/index.vue +++ b/packages/client/src/pages/gallery/index.vue @@ -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> diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue index f145caf28e..096947e6f8 100644 --- a/packages/client/src/pages/gallery/post.vue +++ b/packages/client/src/pages/gallery/post.vue @@ -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> diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue index 8fc17c3606..d185e796c3 100644 --- a/packages/client/src/pages/my-antennas/index.vue +++ b/packages/client/src/pages/my-antennas/index.vue @@ -1,15 +1,17 @@ <template> -<div class="ieepwinx _section"> - <MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> +<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"> - <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 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> -</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; diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue index fc2f6d7380..a5bbc3fd2d 100644 --- a/packages/client/src/pages/my-clips/index.vue +++ b/packages/client/src/pages/my-clips/index.vue @@ -1,16 +1,16 @@ <template> -<div class="_section qtcaoidl"> - <MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> +<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,17 +87,15 @@ export default defineComponent({ margin: 0 auto 16px auto; } - > ._content { - > .list { - > .item { - display: block; - padding: 16px; + > .list { + > .item { + display: block; + padding: 16px; - > .description { - margin-top: 8px; - padding-top: 8px; - border-top: solid 0.5px var(--divider); - } + > .description { + margin-top: 8px; + padding-top: 8px; + border-top: solid 0.5px var(--divider); } } } diff --git a/packages/client/src/pages/my-groups/index.vue b/packages/client/src/pages/my-groups/index.vue index e203b497df..c5019a5e5b 100644 --- a/packages/client/src/pages/my-groups/index.vue +++ b/packages/client/src/pages/my-groups/index.vue @@ -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> diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue index 645035d4ed..94a869b9ff 100644 --- a/packages/client/src/pages/my-lists/index.vue +++ b/packages/client/src/pages/my-lists/index.vue @@ -1,14 +1,16 @@ <template> -<div class="qkcjvfiv"> - <MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton> +<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"> - <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> + <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; } diff --git a/packages/client/src/pages/my-lists/list.vue b/packages/client/src/pages/my-lists/list.vue index bf73cdafde..0bfa20514b 100644 --- a/packages/client/src/pages/my-lists/list.vue +++ b/packages/client/src/pages/my-lists/list.vue @@ -1,35 +1,37 @@ <template> -<div class="mk-list-page"> - <transition name="zoom" mode="out-in"> - <div v-if="list" class="_section"> - <div class="_content"> - <MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton> - <MkButton inline @click="renameList()">{{ $ts.rename }}</MkButton> - <MkButton inline @click="deleteList()">{{ $ts.delete }}</MkButton> +<MkSpacer :content-max="700"> + <div class="mk-list-page"> + <transition name="zoom" mode="out-in"> + <div v-if="list" class="_section"> + <div class="_content"> + <MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton> + <MkButton inline @click="renameList()">{{ $ts.rename }}</MkButton> + <MkButton inline @click="deleteList()">{{ $ts.delete }}</MkButton> + </div> </div> - </div> - </transition> + </transition> - <transition name="zoom" mode="out-in"> - <div v-if="list" class="_section members _gap"> - <div class="_title">{{ $ts.members }}</div> - <div class="_content"> - <div class="users"> - <div v-for="user in users" :key="user.id" class="user _panel"> - <MkAvatar :user="user" class="avatar" :show-indicator="true"/> - <div class="body"> - <MkUserName :user="user" class="name"/> - <MkAcct :user="user" class="acct"/> - </div> - <div class="action"> - <button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button> + <transition name="zoom" mode="out-in"> + <div v-if="list" class="_section members _gap"> + <div class="_title">{{ $ts.members }}</div> + <div class="_content"> + <div class="users"> + <div v-for="user in users" :key="user.id" class="user _panel"> + <MkAvatar :user="user" class="avatar" :show-indicator="true"/> + <div class="body"> + <MkUserName :user="user" class="name"/> + <MkAcct :user="user" class="acct"/> + </div> + <div class="action"> + <button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button> + </div> </div> </div> </div> </div> - </div> - </transition> -</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: [], diff --git a/packages/client/src/pages/page-editor/page-editor.script-block.vue b/packages/client/src/pages/page-editor/page-editor.script-block.vue index 1d3dc25d4b..ded9368b89 100644 --- a/packages/client/src/pages/page-editor/page-editor.script-block.vue +++ b/packages/client/src/pages/page-editor/page-editor.script-block.vue @@ -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"/> diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue index 4de1121fd3..52f6858b0b 100644 --- a/packages/client/src/pages/page-editor/page-editor.vue +++ b/packages/client/src/pages/page-editor/page-editor.vue @@ -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"> diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue index efbdc175d8..3a4803c3a3 100644 --- a/packages/client/src/pages/page.vue +++ b/packages/client/src/pages/page.vue @@ -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; } diff --git a/packages/client/src/pages/pages.vue b/packages/client/src/pages/pages.vue index a8ded5cda9..f1dd64f119 100644 --- a/packages/client/src/pages/pages.vue +++ b/packages/client/src/pages/pages.vue @@ -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> +<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 class="_section"> - <div v-if="tab === 'featured'" class="rknalgpo _content"> - <MkPagination #default="{items}" :pagination="featuredPagesPagination"> - <MkPagePreview v-for="page in items" :key="page.id" class="ckltabjg" :page="page"/> - </MkPagination> - </div> + <div v-else-if="tab === 'my'" class="rknalgpo my"> + <MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> + <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 === 'my'" class="rknalgpo _content my"> - <MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton> - <MkPagination #default="{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"> - <MkPagePreview v-for="like in items" :key="like.page.id" class="ckltabjg" :page="like.page"/> - </MkPagination> - </div> + <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> </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', diff --git a/packages/client/src/pages/settings/apps.vue b/packages/client/src/pages/settings/apps.vue index 10b5fc603e..b5fe4e0aed 100644 --- a/packages/client/src/pages/settings/apps.vue +++ b/packages/client/src/pages/settings/apps.vue @@ -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"> diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue index 7a22a8dcd9..bfac1be77d 100644 --- a/packages/client/src/pages/settings/index.vue +++ b/packages/client/src/pages/settings/index.vue @@ -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> diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue index 4a9633a20d..4f42d5e429 100644 --- a/packages/client/src/pages/settings/mute-block.vue +++ b/packages/client/src/pages/settings/mute-block.vue @@ -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"/> diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue index 33dc299f5d..069f9d964d 100644 --- a/packages/client/src/pages/settings/security.vue +++ b/packages/client/src/pages/settings/security.vue @@ -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> diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue index 1492a989a2..0977dd8322 100644 --- a/packages/client/src/pages/settings/sounds.vue +++ b/packages/client/src/pages/settings/sounds.vue @@ -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 }, diff --git a/packages/client/src/pages/settings/theme.manage.vue b/packages/client/src/pages/settings/theme.manage.vue index ac4edbe66e..c605b1eb64 100644 --- a/packages/client/src/pages/settings/theme.manage.vue +++ b/packages/client/src/pages/settings/theme.manage.vue @@ -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'; diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index 81de0277f5..494932c602 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -1,20 +1,22 @@ <template> -<div v-size="{ min: [800] }" 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/> +<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/> - <div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> - <div class="tl _block"> - <XTimeline ref="tl" :key="src" - class="tl" - :src="src" - :sound="true" - @before="before()" - @after="after()" - @queue="queueUpdated" - /> + <div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> + <div class="tl _block"> + <XTimeline ref="tl" :key="src" + class="tl" + :src="src" + :sound="true" + @before="before()" + @after="after()" + @queue="queueUpdated" + /> + </div> </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> diff --git a/packages/client/src/pages/user/clips.vue b/packages/client/src/pages/user/clips.vue index 9e16bfc45b..aad5317ce0 100644 --- a/packages/client/src/pages/user/clips.vue +++ b/packages/client/src/pages/user/clips.vue @@ -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> diff --git a/packages/client/src/pages/user/follow-list.vue b/packages/client/src/pages/user/follow-list.vue index 2cfb8ee1ad..9fb8943fb8 100644 --- a/packages/client/src/pages/user/follow-list.vue +++ b/packages/client/src/pages/user/follow-list.vue @@ -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> diff --git a/packages/client/src/pages/user/gallery.vue b/packages/client/src/pages/user/gallery.vue index 9def25c2ae..860aa9f44f 100644 --- a/packages/client/src/pages/user/gallery.vue +++ b/packages/client/src/pages/user/gallery.vue @@ -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> diff --git a/packages/client/src/pages/user/pages.vue b/packages/client/src/pages/user/pages.vue index eb8f10d8aa..40d1fe3842 100644 --- a/packages/client/src/pages/user/pages.vue +++ b/packages/client/src/pages/user/pages.vue @@ -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> diff --git a/packages/client/src/pages/user/reactions.vue b/packages/client/src/pages/user/reactions.vue index eff456372c..69c27de55b 100644 --- a/packages/client/src/pages/user/reactions.vue +++ b/packages/client/src/pages/user/reactions.vue @@ -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"/> diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts index 0c04547101..ebe101bc0f 100644 --- a/packages/client/src/scripts/get-user-menu.ts +++ b/packages/client/src/scripts/get-user-menu.ts @@ -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, diff --git a/packages/client/src/scripts/use-tooltip.ts b/packages/client/src/scripts/use-tooltip.ts index 89e6b1be9d..a9bf6d93db 100644 --- a/packages/client/src/scripts/use-tooltip.ts +++ b/packages/client/src/scripts/use-tooltip.ts @@ -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); diff --git a/packages/client/src/ui/_common_/sidebar-for-mobile.vue b/packages/client/src/ui/_common_/sidebar-for-mobile.vue new file mode 100644 index 0000000000..5babdb98a8 --- /dev/null +++ b/packages/client/src/ui/_common_/sidebar-for-mobile.vue @@ -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> diff --git a/packages/client/src/ui/_common_/sidebar.vue b/packages/client/src/ui/_common_/sidebar.vue index 6abb21d963..e363c3abd9 100644 --- a/packages/client/src/ui/_common_/sidebar.vue +++ b/packages/client/src/ui/_common_/sidebar.vue @@ -1,386 +1,305 @@ <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> - <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="otherNavItemIndicated" 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> - </nav> - </transition> +<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"/> + </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 { 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; - }, + }); + + const calcViewState = () => { + iconOnly.value = (window.innerWidth <= 1279) || (defaultStore.state.menuDisplay === 'sideIcon'); + }; + + calcViewState(); + + window.addEventListener('resize', calcViewState); + + watch(defaultStore.reactiveState.menuDisplay, () => { + calcViewState(); + }); + + 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'); + }, + }; }, - - 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'); - }); - } - }, - - 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) { - 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; + $avatar-size: 32px; + $avatar-margin: 8px; - > .nav-back { + flex: 0 0 $nav-width; + width: $nav-width; + box-sizing: border-box; + + > div { + position: fixed; + top: 0; + left: 0; z-index: 1001; - } - - > .nav { - $avatar-size: 32px; - $avatar-margin: 8px; - - flex: 0 0 $nav-width; width: $nav-width; + // ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ + height: calc(var(--vh, 1vh) * 100); box-sizing: border-box; + overflow: auto; + overflow-x: clip; + background: var(--navBg); - &.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; - } - } - } - } + > .divider { + margin: 16px 16px; + border-top: solid 0.5px var(--divider); } - &.hidden { - position: fixed; - top: 0; - left: 0; - z-index: 1001; - } - - &:not(.hidden) { - display: block !important; - } - - > div { - position: fixed; - top: 0; - left: 0; - z-index: 1001; - width: $nav-width; - // ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ - height: calc(var(--vh, 1vh) * 100); + > .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; - overflow: auto; - overflow-x: clip; - background: var(--navBg); + color: var(--navFg); - > .divider { - margin: 16px 16px; - border-top: solid 0.5px var(--divider); + > i { + position: relative; + width: 32px; } - > .item { + > 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; - 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); + font-size: 0.9em; + } - > i { - position: relative; - width: 32px; - } + &:hover { + text-decoration: none; + color: var(--navHoverFg); + } - > i, - > .avatar { - margin-right: $avatar-margin; - } + &.active { + color: var(--navActive); + } - > .avatar { - width: $avatar-size; - height: $avatar-size; - vertical-align: middle; - } - - > .indicator { + &:hover, &.active { + &:before { + content: ""; + display: block; + width: calc(100% - 24px); + height: 100%; + margin: auto; position: absolute; top: 0; - left: 20px; - color: var(--navIndicator); - font-size: 8px; - animation: blink 1s infinite; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: var(--accentedBg); } + } - > .text { - position: relative; - font-size: 0.9em; - } + &: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)); + } - &:hover { - text-decoration: none; - color: var(--navHoverFg); - } - - &.active { - color: var(--navActive); - } + &:first-child { + top: 0; &: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); + content: none; } } + } - &: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)); + &: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); + } + } + } + } + } + + &.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 { - top: 0; - - &:hover, &.active { - &:before { - content: none; - } - } + margin-bottom: 8px; } &:last-child { - bottom: 0; - color: var(--fgOnAccent); + margin-top: 8px; + } - &: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); - } + &: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%; + } } } } diff --git a/packages/client/src/ui/chat/note.vue b/packages/client/src/ui/chat/note.vue index 512c87a59e..6927dd0eaf 100644 --- a/packages/client/src/ui/chat/note.vue +++ b/packages/client/src/ui/chat/note.vue @@ -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, { diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue index fe533662d0..41da973152 100644 --- a/packages/client/src/ui/classic.vue +++ b/packages/client/src/ui/classic.vue @@ -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> + <div v-if="!showMenuOnTop" class="sidebar"> + <XSidebar/> + </div> + <div v-else ref="widgetsLeft" class="widgets left"> + <XWidgets :place="'left'" @mounted="attachSticky('widgetsLeft')"/> + </div> <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; } diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue index 329716664e..e1b2887bb2 100644 --- a/packages/client/src/ui/deck.vue +++ b/packages/client/src/ui/deck.vue @@ -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,43 +222,89 @@ export default defineComponent({ } } - > .post, - > .nav { + > .buttons { position: fixed; z-index: 1000; - bottom: 32px; - width: 64px; - 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; + bottom: 0; + left: 0; + padding: 16px; + display: flex; + width: 100%; + box-sizing: border-box; - @media (min-width: ($nav-hide-threshold + 1px)) { - display: none; + > .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; + } + } } } - > .post { - right: 32px; + > .menu-back { + z-index: 1001; } - > .nav { - left: 32px; - background: var(--panel); - color: var(--fg); - - &:hover { - background: var(--X2); - } - - > .indicator { - position: absolute; - top: 0; - left: 0; - color: var(--indicator); - font-size: 16px; - animation: blink 1s infinite; - } + > .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> diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue index 09d089c528..d3c7cf8213 100644 --- a/packages/client/src/ui/deck/column.vue +++ b/packages/client/src/ui/deck/column.vue @@ -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; } diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue index 55afc5217f..9fc2177ee0 100644 --- a/packages/client/src/ui/universal.vue +++ b/packages/client/src/ui/universal.vue @@ -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 importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる + XSideView, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる }, - 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> diff --git a/packages/client/src/ui/visitor/kanban.vue b/packages/client/src/ui/visitor/kanban.vue index 2da4dd40b6..ee0f11b838 100644 --- a/packages/client/src/ui/visitor/kanban.vue +++ b/packages/client/src/ui/visitor/kanban.vue @@ -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"> diff --git a/yarn.lock b/yarn.lock index fb93b79758..7f74114102 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"