Merge branch 'develop'
This commit is contained in:
		
						commit
						83a77f1064
					
				
					 165 changed files with 3765 additions and 2704 deletions
				
			
		
							
								
								
									
										24
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										24
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -7,6 +7,30 @@ | |||
| 
 | ||||
| --> | ||||
| 
 | ||||
| ## 12.98.0 (2021/12/03) | ||||
| 
 | ||||
| ### Improvements | ||||
| - API: /antennas/notes API で日付による絞り込みができるように | ||||
| - クライアント: アンケートに投票する際に確認ダイアログを出すように | ||||
| - クライアント: Renoteなノート詳細ページから元のノートページに遷移できるように | ||||
| - クライアント: 画像ポップアップでクリックで閉じられるように | ||||
| - クライアント: デザインの調整 | ||||
| - フォロワーを解除できる機能 | ||||
| 
 | ||||
| ### Bugfixes | ||||
| - クライアント: LTLやGTLが無効になっている場合でもUI上にタブが表示される問題を修正 | ||||
| - クライアント: ログインにおいてパスワードが誤っている際のエラーメッセージが正しく表示されない問題を修正 | ||||
| - クライアント: リアクションツールチップ、Renoteツールチップのユーザーの並び順を修正 | ||||
| - クライアント: サウンドのマスターボリュームが正しく保存されない問題を修正 | ||||
| - クライアント: 一部環境において通知が表示されると操作不能になる問題を修正 | ||||
| - クライアント: モバイルでタップしたときにツールチップが表示される問題を修正 | ||||
| - クライアント: リモートインスタンスのノートに返信するとき、対象のノートにそのリモートインスタンス内のユーザーへのメンションが含まれていると、返信テキスト内にローカルユーザーへのメンションとして引き継がれてしまう場合がある問題を修正 | ||||
| - クライアント: 画像ビューワーで全体表示した時に上側の一部しか表示されない画像がある問題を修正 | ||||
| - API: ユーザーを取得時に条件によっては内部エラーになる問題を修正 | ||||
| 
 | ||||
| ### Changes | ||||
| - クライアント: ノートにモデレーターバッジを表示するのを廃止 | ||||
| 
 | ||||
| ## 12.97.0 (2021/11/19) | ||||
| 
 | ||||
| ### Improvements | ||||
|  |  | |||
							
								
								
									
										129
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										129
									
								
								README.md
									
										
									
									
									
								
							|  | @ -1,116 +1,57 @@ | |||
| [](https://join.misskey.page/) | ||||
| 
 | ||||
| <h1 align="center">Misskey</h1> | ||||
| [](https://join.misskey.page/) | ||||
| 
 | ||||
| <div align="center"> | ||||
| 
 | ||||
| [](https://david-dm.org/misskey-dev/misskey) | ||||
| [](http://makeapullrequest.com) | ||||
| [](https://github.com/humanetech-community/awesome-humane-tech) | ||||
| **🌎 A forever evolving, interplanetary microblogging platform. 🚀** | ||||
| 
 | ||||
| **A forever evolving, interplanetary microblogging platform.** | ||||
| **Misskey** is a distributed microblogging platform with advanced features such as Reactions and a highly customizable UI. | ||||
| 
 | ||||
| <a href="https://join.misskey.page/">Misskey</a> is a decentralized microblogging platform born on Earth. | ||||
| Since it exists within the Fediverse (a universe where various social media platforms are organized), | ||||
| it is mutually linked with other social media platforms. | ||||
| Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? <a href="https://join.misskey.page/">Find an instance!</a> | ||||
| [Learn more](https://misskey-hub.net/) | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| [✨ Find an instance](https://misskey-hub.net/instances.html) | ||||
| • | ||||
| [📦 Create your own instance](https://misskey-hub.net/docs/install.html) | ||||
| • | ||||
| [🛠️ Contribute](./CONTRIBUTING.md) | ||||
| • | ||||
| [🚀 Join the community](https://discord.gg/Wp8gVStHW3) | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| <a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a> | ||||
| 
 | ||||
| </div> | ||||
| 
 | ||||
| --- | ||||
| <div> | ||||
| 
 | ||||
| Do you have a question? Or are you experiencing trouble? | ||||
| Visit [our forum](https://forum.misskey.io/)! | ||||
| <a href="https://xn--931a.moe/"><img src="https://github.com/misskey-dev/misskey/blob/develop/assets/ai.png?raw=true" align="right" height="320px"/></a> | ||||
| 
 | ||||
| --- | ||||
| ## ✨ Features | ||||
| - **ActivityPub support**\ | ||||
| 	It is possible to interact with other software. | ||||
| - **Reactions**\ | ||||
| 	You can add "reactions" to each post, making it easy for you to express your feelings. | ||||
| - **Drive**\ | ||||
| 	An interface to manage uploaded files such as images, videos, sounds, etc. | ||||
| 	You can also organize your favorite content into folders, making it easy to share again. | ||||
| - **Rich Web UI**\ | ||||
| 	Misskey has a rich WebUI by default. | ||||
| 	It is highly customizable by flexibly changing the layout and installing various widgets and themes. | ||||
| 	Furthermore, plug-ins can be created using AiScript, a original programming language. | ||||
| - and more... | ||||
| 
 | ||||
|  | ||||
| </div> | ||||
| 
 | ||||
| :sparkles: Features | ||||
| ---------------------------------------------------------------- | ||||
| <a href="https://xn--931a.moe/"><img src="https://github.com/misskey-dev/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a> | ||||
| <div style="clear: both;"></div> | ||||
| 
 | ||||
| <h3>Posting</h3> | ||||
| <p> | ||||
| Post your ideas, discussion topics, fun moments, or anything else you want to share! Misskey supports text, emoji, pictures, videos, and polls! | ||||
| </p> | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| <h3 >Reactions</h3> | ||||
| <p> | ||||
| Reactions are the simplest way to respond to others' posts. Simply pick a reaction emote from the list! Reactions on Misskey are much more expressive than other social media services which only allow “liking”. | ||||
| </p> | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| <h3>Interface</h3> | ||||
| <p> | ||||
| Customize the UI to your own tastes! No UI will work for everyone, so Misskey is completely customizable. Make Misskey *yours* by editing the style, adjusting timeline layouts, and placing widgets. | ||||
| </p> | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| <h3>Misskey Drive</h3> | ||||
| <p> | ||||
| Organize and store your files! Want to post a picture you have already uploaded? Wish you could organize your files into folders? Misskey Drive is a solution! | ||||
| </p> | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| ...and more! | ||||
| 
 | ||||
| :package: Create your own instance | ||||
| ---------------------------------------------------------------- | ||||
| Please see the [Setup and Installation Guide](https://misskey-hub.net/docs/install/install.html). | ||||
| 
 | ||||
| :wrench: Contribution | ||||
| ---------------------------------------------------------------- | ||||
| Please see the [Contribution Guide](./CONTRIBUTING.md). | ||||
| 
 | ||||
| ### Collaborators | ||||
| <table> | ||||
| 	<tr> | ||||
| 		<td><img src="https://avatars3.githubusercontent.com/u/4439005?s=460&v=4" alt="syuilo" width="100"></td> | ||||
| 		<td><img src="https://avatars0.githubusercontent.com/u/10798641?s=460&v=4" alt="AyaMorisawa" width="100"></td> | ||||
| 		<td><img src="https://avatars1.githubusercontent.com/u/30769358?s=460&v=4" alt="mei23" width="100"></td> | ||||
| 		<td><img src="https://avatars2.githubusercontent.com/u/20679825?s=460&v=4" alt="acid-chicken" width="100"></td> | ||||
| 		<td><img src="https://avatars2.githubusercontent.com/u/6533808?s=460&v=4" alt="rinsuki" width="100"></td> | ||||
| 		<td><img src="https://avatars0.githubusercontent.com/u/7973572?s=460&v=4" alt="tamaina" width="100"></td> | ||||
| 		<td><img src="https://avatars1.githubusercontent.com/u/7106976?s=460&v=4" alt="Xeltica" width="100"></td> | ||||
| 		<td><img src="https://avatars1.githubusercontent.com/u/17376330?s=460&v=4" alt="u1-liquid" width="100"></td> | ||||
| 	</tr> | ||||
| 	<tr> | ||||
| 		<td align="center"><a href="https://github.com/syuilo">@syuilo</a></td> | ||||
| 		<td align="center"><a href="https://github.com/AyaMorisawa">@AyaMorisawa</a></td> | ||||
| 		<td align="center"><a href="https://github.com/mei23">@mei23</a></td> | ||||
| 		<td align="center"><a href="https://github.com/acid-chicken">@acid-chicken</a></td> | ||||
| 		<td align="center"><a href="https://github.com/rinsuki">@rinsuki</a></td> | ||||
| 		<td align="center"><a href="https://github.com/tamaina">@tamaina</a></td> | ||||
| 		<td align="center"><a href="https://github.com/Xeltica">@Xeltica</a></td> | ||||
| 		<td align="center"><a href="https://github.com/u1-liquid">@u1-liquid</a></td> | ||||
| 	</tr> | ||||
| </table> | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| To receive updates of this repo, follow [@repo@misskey.io](https://misskey.io/@repo) on fediverse. | ||||
| 
 | ||||
| Related projects | ||||
| ---------------------------------------------------------------- | ||||
| - [misskey.js](https://github.com/misskey-dev/misskey.js) - Misskey SDK for JavaScript | ||||
| - [mfm.js](https://github.com/misskey-dev/mfm.js) - MFM parser | ||||
| 
 | ||||
| Sponsors | ||||
| ---------------------------------------------------------------- | ||||
| ## Sponsors | ||||
| <div align="center"> | ||||
| 	<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank" style="display: inline-block;"><img src="https://rss3.io/assets/images/Logo.svg" alt="RSS3" style="display: inline-block; height: 60px;"></a> | ||||
| </div> | ||||
| 
 | ||||
| :heart: Backers | ||||
| ---------------------------------------------------------------- | ||||
| ## Backers | ||||
| <!-- PATREON_START --> | ||||
| <table><tr> | ||||
| <td><img src="https://c8.patreon.com/2/200/20832595" alt="Roujo " width="100"></td> | ||||
|  |  | |||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| Before Width: | Height: | Size: 38 KiB | 
|  | @ -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: | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  | @ -931,6 +933,8 @@ _mfm: | |||
|   rainbowDescription: "Makes the content appear in rainbow colors." | ||||
|   sparkle: "Sparkle" | ||||
|   sparkleDescription: "Gives content a sparkling particle effect." | ||||
|   rotate: "Rotate" | ||||
|   rotateDescription: "Turns content by a specified angle." | ||||
| _reversi: | ||||
|   reversi: "Reversi" | ||||
|   gameSettings: "Game settings" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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: "既に使用されています" | ||||
|  | @ -823,7 +826,7 @@ _ffVisibility: | |||
| 
 | ||||
| _signup: | ||||
|   almostThere: "ほとんど完了です" | ||||
|   emailAddressInfo: "あなたが使っているメールアドレスを入力してください。" | ||||
|   emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。" | ||||
|   emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。" | ||||
| 
 | ||||
| _accountDelete: | ||||
|  | @ -944,6 +947,8 @@ _mfm: | |||
|   rainbowDescription: "内容をレインボーにします。" | ||||
|   sparkle: "キラキラ" | ||||
|   sparkleDescription: "キラキラしたパーティクルのエフェクトを追加します。" | ||||
|   rotate: "回転" | ||||
|   rotateDescription: "指定した角度で回転させます。" | ||||
| 
 | ||||
| _reversi: | ||||
|   reversi: "リバーシ" | ||||
|  |  | |||
|  | @ -700,6 +700,7 @@ _mfm: | |||
|   spin: "アニメーション(回転)" | ||||
|   blur: "ぼかし" | ||||
|   font: "フォント" | ||||
|   rotate: "回転" | ||||
| _reversi: | ||||
|   reversi: "リバーシ" | ||||
|   gameSettings: "対局の設定" | ||||
|  |  | |||
|  | @ -899,6 +899,7 @@ _mfm: | |||
|   rainbowDescription: "내용을 무지개로 표시합니다." | ||||
|   sparkle: "반짝반짝" | ||||
|   sparkleDescription: "반짝이는 파티클 효과를 추가합니다." | ||||
|   rotate: "회전" | ||||
| _reversi: | ||||
|   reversi: "리버시" | ||||
|   gameSettings: "대국 설정" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -922,6 +922,7 @@ _mfm: | |||
|   rainbowDescription: "Заставлять содержимое отображаться в цветах радуги." | ||||
|   sparkle: "Блеск" | ||||
|   sparkleDescription: "Добавьте эффект искрящихся частиц." | ||||
|   rotate: "Повернуть" | ||||
| _reversi: | ||||
|   reversi: "Реверси" | ||||
|   gameSettings: "Настройки игры" | ||||
|  |  | |||
|  | @ -771,6 +771,7 @@ _mfm: | |||
|   blurDescription: "Цей ефект зробить контент розмитим. Контент можна зробити чітким, якщо навести на нього вказівник миші." | ||||
|   font: "Шрифт" | ||||
|   fontDescription: "Встановлює шрифт для контенту." | ||||
|   rotate: "Обертати" | ||||
| _reversi: | ||||
|   reversi: "Реверсі" | ||||
|   gameSettings: "Налаштування гри" | ||||
|  |  | |||
|  | @ -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: "对局设置" | ||||
|  |  | |||
|  | @ -840,6 +840,7 @@ _mfm: | |||
|   blur: "模糊" | ||||
|   font: "字型" | ||||
|   fontDescription: "您可以設定顯示內容的字型" | ||||
|   rotate: "旋轉" | ||||
| _reversi: | ||||
|   reversi: "黑白棋" | ||||
|   gameSettings: "對弈設定" | ||||
|  |  | |||
|  | @ -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" | ||||
| 	} | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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') | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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`; | ||||
| }; | ||||
|  |  | |||
|  | @ -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: フォローされていない`; | ||||
| }; | ||||
|  | @ -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)}`; | ||||
| }; | ||||
|  |  | |||
|  | @ -33,6 +33,14 @@ export const meta = { | |||
| 		untilId: { | ||||
| 			validator: $.optional.type(ID), | ||||
| 		}, | ||||
| 
 | ||||
| 		sinceDate: { | ||||
| 			validator: $.optional.num, | ||||
| 		}, | ||||
| 
 | ||||
| 		untilDate: { | ||||
| 			validator: $.optional.num, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	errors: { | ||||
|  | @ -68,7 +76,8 @@ export default define(meta, async (ps, user) => { | |||
| 		.select('joining.noteId') | ||||
| 		.where('joining.antennaId = :antennaId', { antennaId: antenna.id }); | ||||
| 
 | ||||
| 	const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) | ||||
| 	const query = makePaginationQuery(Notes.createQueryBuilder('note'), | ||||
| 			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) | ||||
| 		.andWhere(`note.id IN (${ antennaQuery.getQuery() })`) | ||||
| 		.innerJoinAndSelect('note.user', 'user') | ||||
| 		.leftJoinAndSelect('note.reply', 'reply') | ||||
|  |  | |||
|  | @ -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); | ||||
| }); | ||||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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']; }) { | ||||
|  |  | |||
							
								
								
									
										105
									
								
								packages/backend/src/services/following/reject.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								packages/backend/src/services/following/reject.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | |||
| import { renderActivity } from '@/remote/activitypub/renderer/index'; | ||||
| import renderFollow from '@/remote/activitypub/renderer/follow'; | ||||
| import renderReject from '@/remote/activitypub/renderer/reject'; | ||||
| import { deliver } from '@/queue/index'; | ||||
| import { publishMainStream, publishUserEvent } from '@/services/stream'; | ||||
| import { User, ILocalUser, IRemoteUser } from '@/models/entities/user'; | ||||
| import { Users, FollowRequests, Followings } from '@/models/index'; | ||||
| import { decrementFollowing } from './delete'; | ||||
| 
 | ||||
| type Local = ILocalUser | { id: User['id']; host: User['host']; uri: User['host'] }; | ||||
| type Remote = IRemoteUser; | ||||
| type Both = Local | Remote; | ||||
| 
 | ||||
| /** | ||||
|  * API following/request/reject | ||||
|  */ | ||||
| export async function rejectFollowRequest(user: Local, follower: Both) { | ||||
| 	if (Users.isRemoteUser(follower)) { | ||||
| 		deliverReject(user, follower); | ||||
| 	} | ||||
| 
 | ||||
| 	await removeFollowRequest(user, follower); | ||||
| 
 | ||||
| 	if (Users.isLocalUser(follower)) { | ||||
| 		publishUnfollow(user, follower); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * API following/reject | ||||
|  */ | ||||
| export async function rejectFollow(user: Local, follower: Both) { | ||||
| 	if (Users.isRemoteUser(follower)) { | ||||
| 		deliverReject(user, follower); | ||||
| 	} | ||||
| 
 | ||||
| 	await removeFollow(user, follower); | ||||
| 
 | ||||
| 	if (Users.isLocalUser(follower)) { | ||||
| 		publishUnfollow(user, follower); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * AP Reject/Follow | ||||
|  */ | ||||
| export async function remoteReject(actor: Remote, follower: Local) { | ||||
| 	await removeFollowRequest(actor, follower); | ||||
| 	await removeFollow(actor, follower); | ||||
| 	publishUnfollow(actor, follower); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Remove follow request record | ||||
|  */ | ||||
| async function removeFollowRequest(followee: Both, follower: Both) { | ||||
| 	const request = await FollowRequests.findOne({ | ||||
| 		followeeId: followee.id, | ||||
| 		followerId: follower.id | ||||
| 	}); | ||||
| 
 | ||||
| 	if (!request) return; | ||||
| 
 | ||||
| 	await FollowRequests.delete(request.id); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Remove follow record | ||||
|  */ | ||||
| async function removeFollow(followee: Both, follower: Both) { | ||||
| 	const following = await Followings.findOne({ | ||||
| 		followeeId: followee.id, | ||||
| 		followerId: follower.id | ||||
| 	}); | ||||
| 
 | ||||
| 	if (!following) return; | ||||
| 
 | ||||
| 	await Followings.delete(following.id); | ||||
| 	decrementFollowing(follower, followee); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Deliver Reject to remote | ||||
|  */ | ||||
| async function deliverReject(followee: Local, follower: Remote) { | ||||
| 	const request = await FollowRequests.findOne({ | ||||
| 		followeeId: followee.id, | ||||
| 		followerId: follower.id | ||||
| 	}); | ||||
| 
 | ||||
| 	const content = renderActivity(renderReject(renderFollow(follower, followee, request?.requestId || undefined), followee)); | ||||
| 	deliver(followee, content, follower.inbox); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Publish unfollow to local | ||||
|  */ | ||||
| async function publishUnfollow(followee: Both, follower: Local) { | ||||
| 	const packedFollowee = await Users.pack(followee.id, follower, { | ||||
| 		detail: true | ||||
| 	}); | ||||
| 
 | ||||
| 	publishUserEvent(follower.id, 'unfollow', packedFollowee); | ||||
| 	publishMainStream(follower.id, 'unfollow', packedFollowee); | ||||
| } | ||||
|  | @ -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); | ||||
| 	}); | ||||
| } | ||||
|  | @ -1,7 +1,10 @@ | |||
| import { Emojis } from '@/models/index'; | ||||
| import { initDb } from '@/db/postgre'; | ||||
| import { genId } from '@/misc/gen-id'; | ||||
| 
 | ||||
| async function main(name: string, url: string, alias?: string): Promise<any> { | ||||
| 	await initDb(); | ||||
| 	const { Emojis } = await import('@/models/index'); | ||||
| 
 | ||||
| 	const aliases = alias != null ? [ alias ] : []; | ||||
| 
 | ||||
| 	await Emojis.save({ | ||||
|  | @ -1,13 +1,11 @@ | |||
| import { initDb } from '../db/postgre'; | ||||
| import { getRepository } from 'typeorm'; | ||||
| import { User } from '@/models/entities/user'; | ||||
| 
 | ||||
| async function main(username: string) { | ||||
| 	if (!username) throw `username required`; | ||||
| 	username = username.replace(/^@/, ''); | ||||
| 
 | ||||
| 	await initDb(); | ||||
| 	const Users = getRepository(User); | ||||
| 	const { Users } = await import('@/models/index'); | ||||
| 
 | ||||
| 	const res = await Users.update({ | ||||
| 		usernameLower: username.toLowerCase(), | ||||
|  | @ -1,13 +1,11 @@ | |||
| import { initDb } from '../db/postgre'; | ||||
| import { getRepository } from 'typeorm'; | ||||
| import { User } from '@/models/entities/user'; | ||||
| 
 | ||||
| async function main(username: string) { | ||||
| 	if (!username) throw `username required`; | ||||
| 	username = username.replace(/^@/, ''); | ||||
| 
 | ||||
| 	await initDb(); | ||||
| 	const Users = getRepository(User); | ||||
| 	const { Users } = await import('@/models/index'); | ||||
| 
 | ||||
| 	const res = await Users.update({ | ||||
| 		usernameLower: username.toLowerCase(), | ||||
|  | @ -1,6 +1,9 @@ | |||
| import { updateQuestion } from '@/remote/activitypub/models/question'; | ||||
| import { initDb } from '@/db/postgre'; | ||||
| 
 | ||||
| async function main(uri: string): Promise<any> { | ||||
| 	await initDb(); | ||||
| 	const { updateQuestion } = await import('@/remote/activitypub/models/question'); | ||||
| 
 | ||||
| 	return await updateQuestion(uri); | ||||
| } | ||||
| 
 | ||||
|  | @ -1,4 +1,4 @@ | |||
| import { Users, Signins } from '@/models/index'; | ||||
| import { initDb } from '@/db/postgre'; | ||||
| 
 | ||||
| // node built/tools/show-signin-history username
 | ||||
| //  => {Success} {Date} {IPAddrsss}
 | ||||
|  | @ -10,6 +10,9 @@ import { Users, Signins } from '@/models/index'; | |||
| //  with full request headers
 | ||||
| 
 | ||||
| async function main(username: string, headers?: string[]) { | ||||
| 	await initDb(); | ||||
| 	const { Users, Signins } = await import('@/models/index'); | ||||
| 
 | ||||
| 	const user = await Users.findOne({ | ||||
| 		host: null, | ||||
| 		usernameLower: username.toLowerCase(), | ||||
|  | @ -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, | ||||
| 		}); | ||||
| 	})); | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								packages/backend/test/resources/rotate.jpg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/backend/test/resources/rotate.jpg
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 12 KiB | 
|  | @ -1,26 +0,0 @@ | |||
| "use strict"; | ||||
| // ex) node built/tools/accept-migration Yo 1000000000001
 | ||||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||||
| const typeorm_1 = require("typeorm"); | ||||
| const index_1 = require("@/config/index"); | ||||
| (0, typeorm_1.createConnection)({ | ||||
|     type: 'postgres', | ||||
|     host: index_1.default.db.host, | ||||
|     port: index_1.default.db.port, | ||||
|     username: index_1.default.db.user, | ||||
|     password: index_1.default.db.pass, | ||||
|     database: index_1.default.db.db, | ||||
|     extra: index_1.default.db.extra, | ||||
|     synchronize: false, | ||||
|     dropSchema: false, | ||||
| }).then(c => { | ||||
|     c.query(`INSERT INTO migrations(timestamp,name) VALUES (${process.argv[3]}, '${process.argv[2]}${process.argv[3]}');`).then(() => { | ||||
|         console.log('done'); | ||||
|         process.exit(0); | ||||
|     }).catch(e => { | ||||
|         console.log('ERROR:'); | ||||
|         console.log(e); | ||||
|         process.exit(1); | ||||
|     }); | ||||
| }); | ||||
| //# sourceMappingURL=accept-migration.js.map
 | ||||
|  | @ -1 +0,0 @@ | |||
| {"version":3,"file":"accept-migration.js","sourceRoot":"","sources":["accept-migration.ts"],"names":[],"mappings":";AAAA,yDAAyD;;AAEzD,qCAA2C;AAC3C,0CAAoC;AAEpC,IAAA,0BAAgB,EAAC;IAChB,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACpB,IAAI,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACpB,QAAQ,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACxB,QAAQ,EAAE,eAAM,CAAC,EAAE,CAAC,IAAI;IACxB,QAAQ,EAAE,eAAM,CAAC,EAAE,CAAC,EAAE;IACtB,KAAK,EAAE,eAAM,CAAC,EAAE,CAAC,KAAK;IACtB,WAAW,EAAE,KAAK;IAClB,UAAU,EAAE,KAAK;CACjB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;IACX,CAAC,CAAC,KAAK,CAAC,kDAAkD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;QAChI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACZ,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"} | ||||
|  | @ -1,30 +0,0 @@ | |||
| "use strict"; | ||||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||||
| const index_1 = require("@/models/index"); | ||||
| const gen_id_1 = require("@/misc/gen-id"); | ||||
| async function main(name, url, alias) { | ||||
|     const aliases = alias != null ? [alias] : []; | ||||
|     await index_1.Emojis.save({ | ||||
|         id: (0, gen_id_1.genId)(), | ||||
|         host: null, | ||||
|         name, | ||||
|         url, | ||||
|         aliases, | ||||
|         updatedAt: new Date() | ||||
|     }); | ||||
| } | ||||
| const args = process.argv.slice(2); | ||||
| const name = args[0]; | ||||
| const url = args[1]; | ||||
| if (!name) | ||||
|     throw new Error('require name'); | ||||
| if (!url) | ||||
|     throw new Error('require url'); | ||||
| main(name, url).then(() => { | ||||
|     console.log('success'); | ||||
|     process.exit(0); | ||||
| }).catch(e => { | ||||
|     console.warn(e); | ||||
|     process.exit(1); | ||||
| }); | ||||
| //# sourceMappingURL=add-emoji.js.map
 | ||||
|  | @ -1 +0,0 @@ | |||
| {"version":3,"file":"add-emoji.js","sourceRoot":"","sources":["add-emoji.ts"],"names":[],"mappings":";;AAAA,0CAAwC;AACxC,0CAAsC;AAEtC,KAAK,UAAU,IAAI,CAAC,IAAY,EAAE,GAAW,EAAE,KAAc;IAC5D,MAAM,OAAO,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAE,KAAK,CAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAE/C,MAAM,cAAM,CAAC,IAAI,CAAC;QACjB,EAAE,EAAE,IAAA,cAAK,GAAE;QACX,IAAI,EAAE,IAAI;QACV,IAAI;QACJ,GAAG;QACH,OAAO;QACP,SAAS,EAAE,IAAI,IAAI,EAAE;KACrB,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AACrB,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAEpB,IAAI,CAAC,IAAI;IAAE,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;AAC3C,IAAI,CAAC,GAAG;IAAE,MAAM,IAAI,KAAK,CAAC,aAAa,CAAC,CAAC;AAEzC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACzB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"} | ||||
|  | @ -1,30 +0,0 @@ | |||
| "use strict"; | ||||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||||
| const postgre_1 = require("../db/postgre"); | ||||
| const typeorm_1 = require("typeorm"); | ||||
| const user_1 = require("@/models/entities/user"); | ||||
| async function main(username) { | ||||
|     if (!username) | ||||
|         throw `username required`; | ||||
|     username = username.replace(/^@/, ''); | ||||
|     await (0, postgre_1.initDb)(); | ||||
|     const Users = (0, typeorm_1.getRepository)(user_1.User); | ||||
|     const res = await Users.update({ | ||||
|         usernameLower: username.toLowerCase(), | ||||
|         host: null | ||||
|     }, { | ||||
|         isAdmin: false | ||||
|     }); | ||||
|     if (res.affected !== 1) { | ||||
|         throw 'Failed'; | ||||
|     } | ||||
| } | ||||
| const args = process.argv.slice(2); | ||||
| main(args[0]).then(() => { | ||||
|     console.log('Success'); | ||||
|     process.exit(0); | ||||
| }).catch(e => { | ||||
|     console.error(`Error: ${e.message || e}`); | ||||
|     process.exit(1); | ||||
| }); | ||||
| //# sourceMappingURL=demote-admin.js.map
 | ||||
|  | @ -1 +0,0 @@ | |||
| {"version":3,"file":"demote-admin.js","sourceRoot":"","sources":["demote-admin.ts"],"names":[],"mappings":";;AAAA,2CAAuC;AACvC,qCAAwC;AACxC,iDAA8C;AAE9C,KAAK,UAAU,IAAI,CAAC,QAAgB;IACnC,IAAI,CAAC,QAAQ;QAAE,MAAM,mBAAmB,CAAC;IACzC,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAEtC,MAAM,IAAA,gBAAM,GAAE,CAAC;IACf,MAAM,KAAK,GAAG,IAAA,uBAAa,EAAC,WAAI,CAAC,CAAC;IAElC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;QAC9B,aAAa,EAAE,QAAQ,CAAC,WAAW,EAAE;QACrC,IAAI,EAAE,IAAI;KACV,EAAE;QACF,OAAO,EAAE,KAAK;KACd,CAAC,CAAC;IAEH,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC,EAAE;QACvB,MAAM,QAAQ,CAAC;KACf;AACF,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"} | ||||
|  | @ -1,30 +0,0 @@ | |||
| "use strict"; | ||||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||||
| const postgre_1 = require("../db/postgre"); | ||||
| const typeorm_1 = require("typeorm"); | ||||
| const user_1 = require("@/models/entities/user"); | ||||
| async function main(username) { | ||||
|     if (!username) | ||||
|         throw `username required`; | ||||
|     username = username.replace(/^@/, ''); | ||||
|     await (0, postgre_1.initDb)(); | ||||
|     const Users = (0, typeorm_1.getRepository)(user_1.User); | ||||
|     const res = await Users.update({ | ||||
|         usernameLower: username.toLowerCase(), | ||||
|         host: null | ||||
|     }, { | ||||
|         isAdmin: true | ||||
|     }); | ||||
|     if (res.affected !== 1) { | ||||
|         throw 'Failed'; | ||||
|     } | ||||
| } | ||||
| const args = process.argv.slice(2); | ||||
| main(args[0]).then(() => { | ||||
|     console.log('Success'); | ||||
|     process.exit(0); | ||||
| }).catch(e => { | ||||
|     console.error(`Error: ${e.message || e}`); | ||||
|     process.exit(1); | ||||
| }); | ||||
| //# sourceMappingURL=mark-admin.js.map
 | ||||
|  | @ -1 +0,0 @@ | |||
| {"version":3,"file":"mark-admin.js","sourceRoot":"","sources":["mark-admin.ts"],"names":[],"mappings":";;AAAA,2CAAuC;AACvC,qCAAwC;AACxC,iDAA8C;AAE9C,KAAK,UAAU,IAAI,CAAC,QAAgB;IACnC,IAAI,CAAC,QAAQ;QAAE,MAAM,mBAAmB,CAAC;IACzC,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAEtC,MAAM,IAAA,gBAAM,GAAE,CAAC;IACf,MAAM,KAAK,GAAG,IAAA,uBAAa,EAAC,WAAI,CAAC,CAAC;IAElC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;QAC9B,aAAa,EAAE,QAAQ,CAAC,WAAW,EAAE;QACrC,IAAI,EAAE,IAAI;KACV,EAAE;QACF,OAAO,EAAE,IAAI;KACb,CAAC,CAAC;IAEH,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC,EAAE;QACvB,MAAM,QAAQ,CAAC;KACf;AACF,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"} | ||||
|  | @ -1,14 +0,0 @@ | |||
| "use strict"; | ||||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||||
| const question_1 = require("@/remote/activitypub/models/question"); | ||||
| async function main(uri) { | ||||
|     return await (0, question_1.updateQuestion)(uri); | ||||
| } | ||||
| const args = process.argv.slice(2); | ||||
| const uri = args[0]; | ||||
| main(uri).then(result => { | ||||
|     console.log(`Done: ${result}`); | ||||
| }).catch(e => { | ||||
|     console.warn(e); | ||||
| }); | ||||
| //# sourceMappingURL=refresh-question.js.map
 | ||||
|  | @ -1 +0,0 @@ | |||
| {"version":3,"file":"refresh-question.js","sourceRoot":"","sources":["refresh-question.ts"],"names":[],"mappings":";;AAAA,mEAAsE;AAEtE,KAAK,UAAU,IAAI,CAAC,GAAW;IAC9B,OAAO,MAAM,IAAA,yBAAc,EAAC,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAEpB,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;AAChC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"} | ||||
|  | @ -1,26 +0,0 @@ | |||
| "use strict"; | ||||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||||
| const postgre_1 = require("@/db/postgre"); | ||||
| const Acct = require("misskey-js/built/acct"); | ||||
| async function main(acct) { | ||||
|     await (0, postgre_1.initDb)(); | ||||
|     const { resolveUser } = await Promise.resolve().then(() => require('@/remote/resolve-user')); | ||||
|     const { username, host } = Acct.parse(acct); | ||||
|     await resolveUser(username, host, {}, true); | ||||
| } | ||||
| // get args
 | ||||
| const args = process.argv.slice(2); | ||||
| let acct = args[0]; | ||||
| // normalize args
 | ||||
| acct = acct.replace(/^@/, ''); | ||||
| // check args
 | ||||
| if (!acct.match(/^\w+@\w/)) { | ||||
|     throw `Invalid acct format. Valid format are user@host`; | ||||
| } | ||||
| console.log(`resync ${acct}`); | ||||
| main(acct).then(() => { | ||||
|     console.log('Done'); | ||||
| }).catch(e => { | ||||
|     console.warn(e); | ||||
| }); | ||||
| //# sourceMappingURL=resync-remote-user.js.map
 | ||||
|  | @ -1 +0,0 @@ | |||
| {"version":3,"file":"resync-remote-user.js","sourceRoot":"","sources":["resync-remote-user.ts"],"names":[],"mappings":";;AAAA,0CAAsC;AACtC,8CAA8C;AAE9C,KAAK,UAAU,IAAI,CAAC,IAAY;IAC/B,MAAM,IAAA,gBAAM,GAAE,CAAC;IACf,MAAM,EAAE,WAAW,EAAE,GAAG,2CAAa,uBAAuB,EAAC,CAAC;IAE9D,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,WAAW;AACX,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,IAAI,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAEnB,iBAAiB;AACjB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAE9B,aAAa;AACb,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE;IAC3B,MAAM,iDAAiD,CAAC;CACxD;AAED,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;AAE9B,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACrB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"} | ||||
|  | @ -1,47 +0,0 @@ | |||
| "use strict"; | ||||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||||
| const index_1 = require("@/models/index"); | ||||
| // node built/tools/show-signin-history username
 | ||||
| //  => {Success} {Date} {IPAddrsss}
 | ||||
| // node built/tools/show-signin-history username user-agent,x-forwarded-for
 | ||||
| //  with user-agent and x-forwarded-for
 | ||||
| // node built/tools/show-signin-history username all
 | ||||
| //  with full request headers
 | ||||
| async function main(username, headers) { | ||||
|     const user = await index_1.Users.findOne({ | ||||
|         host: null, | ||||
|         usernameLower: username.toLowerCase(), | ||||
|     }); | ||||
|     if (user == null) | ||||
|         throw new Error('User not found'); | ||||
|     const history = await index_1.Signins.find({ | ||||
|         userId: user.id | ||||
|     }); | ||||
|     for (const signin of history) { | ||||
|         console.log(`${signin.success ? 'OK' : 'NG'} ${signin.createdAt ? signin.createdAt.toISOString() : 'Unknown'} ${signin.ip}`); | ||||
|         // headers
 | ||||
|         if (headers != null) { | ||||
|             for (const key of Object.keys(signin.headers)) { | ||||
|                 if (headers.includes('all') || headers.includes(key)) { | ||||
|                     console.log(`   ${key}: ${signin.headers[key]}`); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| // get args
 | ||||
| const args = process.argv.slice(2); | ||||
| let username = args[0]; | ||||
| let headers; | ||||
| if (args[1] != null) { | ||||
|     headers = args[1].split(/,/).map(header => header.toLowerCase()); | ||||
| } | ||||
| // normalize args
 | ||||
| username = username.replace(/^@/, ''); | ||||
| main(username, headers).then(() => { | ||||
|     process.exit(0); | ||||
| }).catch(e => { | ||||
|     console.warn(e); | ||||
|     process.exit(1); | ||||
| }); | ||||
| //# sourceMappingURL=show-signin-history.js.map
 | ||||
|  | @ -1 +0,0 @@ | |||
| {"version":3,"file":"show-signin-history.js","sourceRoot":"","sources":["show-signin-history.ts"],"names":[],"mappings":";;AAAA,0CAAgD;AAEhD,gDAAgD;AAChD,mCAAmC;AAEnC,2EAA2E;AAC3E,uCAAuC;AAEvC,oDAAoD;AACpD,6BAA6B;AAE7B,KAAK,UAAU,IAAI,CAAC,QAAgB,EAAE,OAAkB;IACvD,MAAM,IAAI,GAAG,MAAM,aAAK,CAAC,OAAO,CAAC;QAChC,IAAI,EAAE,IAAI;QACV,aAAa,EAAE,QAAQ,CAAC,WAAW,EAAE;KACrC,CAAC,CAAC;IAEH,IAAI,IAAI,IAAI,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAEpD,MAAM,OAAO,GAAG,MAAM,eAAO,CAAC,IAAI,CAAC;QAClC,MAAM,EAAE,IAAI,CAAC,EAAE;KACf,CAAC,CAAC;IAEH,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE;QAC7B,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QAE7H,UAAU;QACV,IAAI,OAAO,IAAI,IAAI,EAAE;YACpB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE;gBAC9C,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE;oBACrD,OAAO,CAAC,GAAG,CAAC,MAAM,GAAG,KAAK,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;iBACjD;aACD;SACD;KACD;AACF,CAAC;AAED,WAAW;AACX,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AACvB,IAAI,OAA6B,CAAC;AAElC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE;IACpB,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;CACjE;AAED,iBAAiB;AACjB,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAEtC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;IACjC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"} | ||||
|  | @ -14,7 +14,9 @@ | |||
| 		</div> | ||||
| 		<header v-if="title"><Mfm :text="title"/></header> | ||||
| 		<div v-if="text" class="body"><Mfm :text="text"/></div> | ||||
| 		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></MkInput> | ||||
| 		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"> | ||||
| 			<template v-if="input.type === 'password'" #prefix><i class="fas fa-lock"></i></template> | ||||
| 		</MkInput> | ||||
| 		<MkSelect v-if="select" v-model="selectedValue" autofocus> | ||||
| 			<template v-if="select.items"> | ||||
| 				<option v-for="item in select.items" :value="item.value">{{ item.text }}</option> | ||||
|  | @ -165,6 +167,10 @@ export default defineComponent({ | |||
| 	> .icon { | ||||
| 		font-size: 32px; | ||||
| 
 | ||||
| 		&.info { | ||||
| 			color: #55c4dd; | ||||
| 		} | ||||
| 
 | ||||
| 		&.success { | ||||
| 			color: var(--success); | ||||
| 		} | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkPopup ref="popup" #default="{point}" :manual-showing="manualShowing" :src="src" :front="true" @click="$refs.popup.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')"> | ||||
| <MkPopup ref="popup" v-slot="{ point, close }" :manual-showing="manualShowing" :src="src" :front="true" @click="close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')"> | ||||
| 	<MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ pointer: point === 'top' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/> | ||||
| </MkPopup> | ||||
| </template> | ||||
|  |  | |||
|  | @ -12,66 +12,67 @@ | |||
| 	<template #header> | ||||
| 		{{ title }} | ||||
| 	</template> | ||||
| 	<FormBase class="xkpnjxcv"> | ||||
| 
 | ||||
| 	<MkSpacer :margin-min="20" :margin-max="32"> | ||||
| 		<div class="xkpnjxcv _formRoot"> | ||||
| 			<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)"> | ||||
| 			<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"> | ||||
| 				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span> | ||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||
| 				<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1" class="_formBlock"> | ||||
| 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | ||||
| 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template> | ||||
| 				</FormInput> | ||||
| 			<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text"> | ||||
| 				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span> | ||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||
| 				<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" class="_formBlock"> | ||||
| 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | ||||
| 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template> | ||||
| 				</FormInput> | ||||
| 			<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]"> | ||||
| 				<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span> | ||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||
| 				<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" class="_formBlock"> | ||||
| 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | ||||
| 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template> | ||||
| 				</FormTextarea> | ||||
| 			<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]"> | ||||
| 				<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]" class="_formBlock"> | ||||
| 					<span v-text="form[item].label || item"></span> | ||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||
| 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template> | ||||
| 				</FormSwitch> | ||||
| 			<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]"> | ||||
| 				<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]" class="_formBlock"> | ||||
| 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | ||||
| 					<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option> | ||||
| 				</FormSelect> | ||||
| 			<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]"> | ||||
| 				<template #desc><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | ||||
| 				<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock"> | ||||
| 					<template #caption><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | ||||
| 					<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option> | ||||
| 				</FormRadios> | ||||
| 			<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step"> | ||||
| 				<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step" class="_formBlock"> | ||||
| 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | ||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||
| 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template> | ||||
| 				</FormRange> | ||||
| 			<FormButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)"> | ||||
| 				<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)" class="_formBlock"> | ||||
| 					<span v-text="form[item].content || item"></span> | ||||
| 			</FormButton> | ||||
| 				</MkButton> | ||||
| 			</template> | ||||
| 	</FormBase> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
| </XModalWindow> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import XModalWindow from '@/components/ui/modal-window.vue'; | ||||
| import FormBase from './debobigego/base.vue'; | ||||
| import FormInput from './debobigego/input.vue'; | ||||
| import FormTextarea from './debobigego/textarea.vue'; | ||||
| import FormSwitch from './debobigego/switch.vue'; | ||||
| import FormSelect from './debobigego/select.vue'; | ||||
| import FormRange from './debobigego/range.vue'; | ||||
| import FormButton from './debobigego/button.vue'; | ||||
| import FormRadios from './debobigego/radios.vue'; | ||||
| import FormInput from './form/input.vue'; | ||||
| import FormTextarea from './form/textarea.vue'; | ||||
| import FormSwitch from './form/switch.vue'; | ||||
| import FormSelect from './form/select.vue'; | ||||
| import FormRange from './form/range.vue'; | ||||
| import MkButton from './ui/button.vue'; | ||||
| import FormRadios from './form/radios.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XModalWindow, | ||||
| 		FormBase, | ||||
| 		FormInput, | ||||
| 		FormTextarea, | ||||
| 		FormSwitch, | ||||
| 		FormSelect, | ||||
| 		FormRange, | ||||
| 		FormButton, | ||||
| 		MkButton, | ||||
| 		FormRadios, | ||||
| 	}, | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										35
									
								
								packages/client/src/components/form/group.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								packages/client/src/components/form/group.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| <template> | ||||
| <div v-sticky-container v-panel class="adfeebaf _formBlock"> | ||||
| 	<div class="label"><slot name="label"></slot></div> | ||||
| 	<div class="main _formRoot"> | ||||
| 		<slot></slot> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .adfeebaf { | ||||
| 	padding: 24px 24px; | ||||
| 	border-radius: var(--radius); | ||||
| 
 | ||||
| 	> .label { | ||||
| 		font-weight: bold; | ||||
| 		padding: 0 0 16px 0; | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			display: none; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .main { | ||||
| 
 | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -5,6 +5,7 @@ | |||
| 		<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> | ||||
| 		<input ref="inputEl" | ||||
| 			v-model="v" | ||||
| 			v-panel | ||||
| 			:type="type" | ||||
| 			:disabled="disabled" | ||||
| 			:required="required" | ||||
|  | @ -27,7 +28,7 @@ | |||
| 	</div> | ||||
| 	<div class="caption"><slot name="caption"></slot></div> | ||||
| 
 | ||||
| 	<MkButton v-if="manualSave && changed" primary @click="updated"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> | ||||
| 	<MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="fas fa-check"></i> {{ $ts.save }}</MkButton> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -114,9 +115,9 @@ export default defineComponent({ | |||
| 		const changed = ref(false); | ||||
| 		const invalid = ref(false); | ||||
| 		const filled = computed(() => v.value !== '' && v.value != null); | ||||
| 		const inputEl = ref(null); | ||||
| 		const prefixEl = ref(null); | ||||
| 		const suffixEl = ref(null); | ||||
| 		const inputEl = ref<HTMLElement>(); | ||||
| 		const prefixEl = ref<HTMLElement>(); | ||||
| 		const suffixEl = ref<HTMLElement>(); | ||||
| 
 | ||||
| 		const focus = () => inputEl.value.focus(); | ||||
| 		const onInput = (ev) => { | ||||
|  | @ -208,7 +209,7 @@ export default defineComponent({ | |||
| .matxzzsk { | ||||
| 	> .label { | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 0 0 8px 12px; | ||||
| 		padding: 0 0 8px 0; | ||||
| 		user-select: none; | ||||
| 
 | ||||
| 		&:empty { | ||||
|  | @ -217,8 +218,8 @@ export default defineComponent({ | |||
| 	} | ||||
| 
 | ||||
| 	> .caption { | ||||
| 		font-size: 0.8em; | ||||
| 		padding: 8px 0 0 12px; | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 8px 0 0 0; | ||||
| 		color: var(--fgTransparentWeak); | ||||
| 
 | ||||
| 		&:empty { | ||||
|  | @ -242,8 +243,7 @@ export default defineComponent({ | |||
| 			font-weight: normal; | ||||
| 			font-size: 1em; | ||||
| 			color: var(--fg); | ||||
| 			background: var(--panel); | ||||
| 			border: solid 0.5px var(--inputBorder); | ||||
| 			border: solid 0.5px var(--panel); | ||||
| 			border-radius: 6px; | ||||
| 			outline: none; | ||||
| 			box-shadow: none; | ||||
|  | @ -311,5 +311,9 @@ export default defineComponent({ | |||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .save { | ||||
| 		margin: 8px 0 0 0; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
							
								
								
									
										112
									
								
								packages/client/src/components/form/link.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								packages/client/src/components/form/link.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | |||
| <template> | ||||
| <div class="ffcbddfc" :class="{ inline }"> | ||||
| 	<a v-if="external" class="main _button" :href="to" target="_blank"> | ||||
| 		<span class="icon"><slot name="icon"></slot></span> | ||||
| 		<span class="text"><slot></slot></span> | ||||
| 		<span class="right"> | ||||
| 			<span class="text"><slot name="suffix"></slot></span> | ||||
| 			<i class="fas fa-external-link-alt icon"></i> | ||||
| 		</span> | ||||
| 	</a> | ||||
| 	<MkA v-else class="main _button" :class="{ active }" :to="to" :behavior="behavior"> | ||||
| 		<span class="icon"><slot name="icon"></slot></span> | ||||
| 		<span class="text"><slot></slot></span> | ||||
| 		<span class="right"> | ||||
| 			<span class="text"><slot name="suffix"></slot></span> | ||||
| 			<i class="fas fa-chevron-right icon"></i> | ||||
| 		</span> | ||||
| 	</MkA> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		to: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		active: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		external: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		behavior: { | ||||
| 			type: String, | ||||
| 			required: false, | ||||
| 		}, | ||||
| 		inline: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .ffcbddfc { | ||||
| 	display: block; | ||||
| 
 | ||||
| 	&.inline { | ||||
| 		display: inline-block; | ||||
| 	} | ||||
| 
 | ||||
| 	> .main { | ||||
| 		display: flex; | ||||
| 		align-items: center; | ||||
| 		width: 100%; | ||||
| 		box-sizing: border-box; | ||||
| 		padding: 12px 14px 12px 14px; | ||||
| 		background: var(--buttonBg); | ||||
| 		border-radius: 6px; | ||||
| 		font-size: 0.9em; | ||||
| 
 | ||||
| 		&:hover { | ||||
| 			text-decoration: none; | ||||
| 			background: var(--buttonHoverBg); | ||||
| 		} | ||||
| 
 | ||||
| 		&.active { | ||||
| 			color: var(--accent); | ||||
| 			background: var(--buttonHoverBg); | ||||
| 		} | ||||
| 
 | ||||
| 		> .icon { | ||||
| 			margin-right: 0.75em; | ||||
| 			flex-shrink: 0; | ||||
| 			text-align: center; | ||||
| 			opacity: 0.8; | ||||
| 
 | ||||
| 			&:empty { | ||||
| 				display: none; | ||||
| 
 | ||||
| 				& + .text { | ||||
| 					padding-left: 4px; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .text { | ||||
| 			white-space: nowrap; | ||||
| 			text-overflow: ellipsis; | ||||
| 			overflow: hidden; | ||||
| 			padding-right: 12px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .right { | ||||
| 			margin-left: auto; | ||||
| 			opacity: 0.7; | ||||
| 			white-space: nowrap; | ||||
| 
 | ||||
| 			> .text:not(:empty) { | ||||
| 				margin-right: 0.75em; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										44
									
								
								packages/client/src/components/form/pagination.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								packages/client/src/components/form/pagination.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | |||
| <template> | ||||
| <FormSlot> | ||||
| 	<template #label><slot name="label"></slot></template> | ||||
| 	<div class="abcaccfa"> | ||||
| 		<slot :items="items"></slot> | ||||
| 		<div v-if="empty" key="_empty_" class="empty"> | ||||
| 			<slot name="empty"></slot> | ||||
| 		</div> | ||||
| 		<MkButton v-show="more" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore"> | ||||
| 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||
| 			<template v-if="moreFetching"><MkLoading inline/></template> | ||||
| 		</MkButton> | ||||
| 	</div> | ||||
| </FormSlot> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import FormSlot from './slot.vue'; | ||||
| import paging from '@/scripts/paging'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, | ||||
| 		FormSlot, | ||||
| 	}, | ||||
| 
 | ||||
| 	mixins: [ | ||||
| 		paging({}), | ||||
| 	], | ||||
| 
 | ||||
| 	props: { | ||||
| 		pagination: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .abcaccfa { | ||||
| } | ||||
| </style> | ||||
|  | @ -1,5 +1,6 @@ | |||
| <template> | ||||
| <div | ||||
| 	v-panel | ||||
| 	class="novjtctn" | ||||
| 	:class="{ disabled, checked }" | ||||
| 	:aria-checked="checked" | ||||
|  | @ -50,9 +51,10 @@ export default defineComponent({ | |||
| .novjtctn { | ||||
| 	position: relative; | ||||
| 	display: inline-block; | ||||
| 	margin: 8px 20px 0 0; | ||||
| 	text-align: left; | ||||
| 	cursor: pointer; | ||||
| 	padding: 11px 14px; | ||||
| 	border-radius: 6px; | ||||
| 	transition: all 0.3s; | ||||
| 
 | ||||
| 	> * { | ||||
|  | @ -68,6 +70,14 @@ export default defineComponent({ | |||
| 	} | ||||
| 
 | ||||
| 	&.checked { | ||||
| 		background: var(--accentedBg) !important; | ||||
| 		border-color: var(--accent); | ||||
| 		color: var(--accent); | ||||
| 
 | ||||
| 		&, * { | ||||
| 			cursor: default !important; | ||||
| 		} | ||||
| 
 | ||||
| 		> .button { | ||||
| 			border-color: var(--accent); | ||||
| 
 | ||||
|  | @ -79,6 +89,11 @@ export default defineComponent({ | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&:hover { | ||||
| 		border-color: var(--inputBorderHover); | ||||
| 		color: var(--accent); | ||||
| 	} | ||||
| 
 | ||||
| 	> input { | ||||
| 		position: absolute; | ||||
| 		width: 0; | ||||
|  | @ -89,8 +104,8 @@ export default defineComponent({ | |||
| 
 | ||||
| 	> .button { | ||||
| 		position: absolute; | ||||
| 		width: 20px; | ||||
| 		height: 20px; | ||||
| 		width: 14px; | ||||
| 		height: 14px; | ||||
| 		background: none; | ||||
| 		border: solid 2px var(--inputBorder); | ||||
| 		border-radius: 100%; | ||||
|  | @ -114,7 +129,6 @@ export default defineComponent({ | |||
| 	> .label { | ||||
| 		margin-left: 28px; | ||||
| 		display: block; | ||||
| 		font-size: 16px; | ||||
| 		line-height: 20px; | ||||
| 		cursor: pointer; | ||||
| 	} | ||||
|  |  | |||
|  | @ -23,6 +23,8 @@ export default defineComponent({ | |||
| 	}, | ||||
| 	render() { | ||||
| 		let options = this.$slots.default(); | ||||
| 		const label = this.$slots.label && this.$slots.label(); | ||||
| 		const caption = this.$slots.caption && this.$slots.caption(); | ||||
| 
 | ||||
| 		// なぜかFragmentになることがあるため | ||||
| 		if (options.length === 1 && options[0].props == null) options = options[0].children; | ||||
|  | @ -30,12 +32,21 @@ export default defineComponent({ | |||
| 		return h('div', { | ||||
| 			class: 'novjtcto' | ||||
| 		}, [ | ||||
| 			...options.map(option => h(MkRadio, { | ||||
| 			...(label ? [h('div', { | ||||
| 				class: 'label' | ||||
| 			}, [label])] : []), | ||||
| 			h('div', { | ||||
| 				class: 'body' | ||||
| 			}, options.map(option => h(MkRadio, { | ||||
| 					key: option.key, | ||||
| 					value: option.props.value, | ||||
| 					modelValue: this.value, | ||||
| 					'onUpdate:modelValue': value => this.value = value, | ||||
| 			}, option.children)) | ||||
| 				}, option.children)), | ||||
| 			), | ||||
| 			...(caption ? [h('div', { | ||||
| 				class: 'caption' | ||||
| 			}, [caption])] : []), | ||||
| 		]); | ||||
| 	} | ||||
| }); | ||||
|  | @ -43,12 +54,30 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss"> | ||||
| .novjtcto { | ||||
| 	&:first-child { | ||||
| 		margin-top: 0; | ||||
| 	> .label { | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 0 0 8px 0; | ||||
| 		user-select: none; | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			display: none; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&:last-child { | ||||
| 		margin-bottom: 0; | ||||
| 	> .body { | ||||
| 		display: grid; | ||||
| 		grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); | ||||
| 		grid-gap: 12px; | ||||
| 	} | ||||
| 
 | ||||
| 	> .caption { | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 8px 0 0 0; | ||||
| 		color: var(--fgTransparentWeak); | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			display: none; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,29 +1,27 @@ | |||
| <template> | ||||
| <div class="timctyfi" :class="{ focused, disabled }"> | ||||
| 	<div class="icon"><slot name="icon"></slot></div> | ||||
| 	<span class="label"><slot name="label"></slot></span> | ||||
| 	<input | ||||
| 		ref="input" | ||||
| 		v-model="v" | ||||
| 		type="range" | ||||
| 		:disabled="disabled" | ||||
| 		:min="min" | ||||
| 		:max="max" | ||||
| 		:step="step" | ||||
| 		:autofocus="autofocus" | ||||
| 		@focus="focused = true" | ||||
| 		@blur="focused = false" | ||||
| 		@input="$emit('update:value', $event.target.value)" | ||||
| 	/> | ||||
| <div class="timctyfi" :class="{ disabled }"> | ||||
| 	<div class="label"><slot name="label"></slot></div> | ||||
| 	<div v-panel class="body"> | ||||
| 		<div ref="containerEl" class="container"> | ||||
| 			<div class="track"> | ||||
| 				<div class="highlight" :style="{ width: (steppedValue * 100) + '%' }"></div> | ||||
| 			</div> | ||||
| 			<div v-if="steps" class="ticks"> | ||||
| 				<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div> | ||||
| 			</div> | ||||
| 			<div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { computed, defineComponent, ref, watch } from 'vue'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		value: { | ||||
| 		modelValue: { | ||||
| 			type: Number, | ||||
| 			required: false, | ||||
| 			default: 0 | ||||
|  | @ -51,88 +49,198 @@ export default defineComponent({ | |||
| 		autofocus: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		} | ||||
| 		}, | ||||
| 	data() { | ||||
| 		textConverter: { | ||||
| 			type: Function, | ||||
| 			required: false, | ||||
| 			default: (v) => v.toString(), | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	setup(props, context) { | ||||
| 		const rawValue = ref((props.modelValue - props.min) / (props.max - props.min)); | ||||
| 		const steppedValue = computed(() => { | ||||
| 			if (props.step) { | ||||
| 				const step = props.step / (props.max - props.min); | ||||
| 				return (step * Math.round(rawValue.value / step)); | ||||
| 			} else { | ||||
| 				return rawValue.value; | ||||
| 			} | ||||
| 		}); | ||||
| 		const finalValue = computed(() => { | ||||
| 			return (steppedValue.value * (props.max - props.min)) + props.min; | ||||
| 		}); | ||||
| 		watch(finalValue, () => { | ||||
| 			context.emit('update:modelValue', finalValue.value); | ||||
| 		}); | ||||
| 
 | ||||
| 		const thumbWidth = computed(() => { | ||||
| 			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 steps = computed(() => { | ||||
| 			if (props.step) { | ||||
| 				return (props.max - props.min) / props.step; | ||||
| 			} else { | ||||
| 				return 0; | ||||
| 			} | ||||
| 		}); | ||||
| 		const containerEl = ref<HTMLElement>(); | ||||
| 		const thumbEl = ref<HTMLElement>(); | ||||
| 
 | ||||
| 		const onMousedown = (ev: MouseEvent | TouchEvent) => { | ||||
| 			ev.preventDefault(); | ||||
| 
 | ||||
| 			const tooltipShowing = ref(true); | ||||
| 			os.popup(import('@/components/ui/tooltip.vue'), { | ||||
| 				showing: tooltipShowing, | ||||
| 				text: computed(() => { | ||||
| 					return props.textConverter(finalValue.value); | ||||
| 				}), | ||||
| 				source: thumbEl, | ||||
| 			}, {}, 'closed'); | ||||
| 
 | ||||
| 			const style = document.createElement('style'); | ||||
| 			style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }')); | ||||
| 			document.head.appendChild(style); | ||||
| 
 | ||||
| 			const onDrag = (ev: MouseEvent | TouchEvent) => { | ||||
| 				ev.preventDefault(); | ||||
| 				const containerRect = containerEl.value!.getBoundingClientRect(); | ||||
| 				const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX; | ||||
| 				const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth.value / 2)); | ||||
| 				rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value))); | ||||
| 			}; | ||||
| 
 | ||||
| 			const onMouseup = () => { | ||||
| 				document.head.removeChild(style); | ||||
| 				tooltipShowing.value = false; | ||||
| 				window.removeEventListener('mousemove', onDrag); | ||||
| 				window.removeEventListener('touchmove', onDrag); | ||||
| 				window.removeEventListener('mouseup', onMouseup); | ||||
| 				window.removeEventListener('touchend', onMouseup); | ||||
| 			}; | ||||
| 
 | ||||
| 			window.addEventListener('mousemove', onDrag); | ||||
| 			window.addEventListener('touchmove', onDrag); | ||||
| 			window.addEventListener('mouseup', onMouseup, { once: true }); | ||||
| 			window.addEventListener('touchend', onMouseup, { once: true }); | ||||
| 		}; | ||||
| 
 | ||||
| 		return { | ||||
| 			v: this.value, | ||||
| 			focused: false | ||||
| 			rawValue, | ||||
| 			finalValue, | ||||
| 			steppedValue, | ||||
| 			onMousedown, | ||||
| 			containerEl, | ||||
| 			thumbEl, | ||||
| 			thumbPosition, | ||||
| 			steps, | ||||
| 		}; | ||||
| 	}, | ||||
| 	watch: { | ||||
| 		value(v) { | ||||
| 			this.v = parseFloat(v); | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		if (this.autofocus) { | ||||
| 			this.$nextTick(() => { | ||||
| 				this.$refs.input.focus(); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| @use "sass:math"; | ||||
| 
 | ||||
| .timctyfi { | ||||
| 	position: relative; | ||||
| 	margin: 8px; | ||||
| 
 | ||||
| 	> .icon { | ||||
| 		display: inline-block; | ||||
| 		width: 24px; | ||||
| 		text-align: center; | ||||
| 	> .label { | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 0 0 8px 0; | ||||
| 		user-select: none; | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			display: none; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .title { | ||||
| 		pointer-events: none; | ||||
| 		font-size: 16px; | ||||
| 		color: var(--inputLabel); | ||||
| 		overflow: hidden; | ||||
| 	> .caption { | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 8px 0 0 0; | ||||
| 		color: var(--fgTransparentWeak); | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			display: none; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> input { | ||||
| 		-webkit-appearance: none; | ||||
| 		-moz-appearance: none; | ||||
| 		appearance: none; | ||||
| 		background: var(--X10); | ||||
| 		height: 7px; | ||||
| 		margin: 0 8px; | ||||
| 		outline: 0; | ||||
| 		border: 0; | ||||
| 		border-radius: 7px; | ||||
| 	$thumbHeight: 20px; | ||||
| 	$thumbWidth: 20px; | ||||
| 
 | ||||
| 		&.disabled { | ||||
| 			opacity: 0.6; | ||||
| 			cursor: not-allowed; | ||||
| 		} | ||||
| 	> .body { | ||||
| 		padding: 12px; | ||||
| 		border-radius: 6px; | ||||
| 
 | ||||
| 		&::-webkit-slider-thumb { | ||||
| 			-webkit-appearance: none; | ||||
| 			appearance: none; | ||||
| 			cursor: pointer; | ||||
| 			width: 20px; | ||||
| 			height: 20px; | ||||
| 			display: block; | ||||
| 			border-radius: 50%; | ||||
| 			border: none; | ||||
| 		> .container { | ||||
| 			position: relative; | ||||
| 			height: $thumbHeight; | ||||
| 
 | ||||
| 			> .track { | ||||
| 				position: absolute; | ||||
| 				top: 0; | ||||
| 				bottom: 0; | ||||
| 				left: 0; | ||||
| 				right: 0; | ||||
| 				margin: auto; | ||||
| 				width: calc(100% - #{$thumbWidth}); | ||||
| 				height: 3px; | ||||
| 				background: rgba(0, 0, 0, 0.1); | ||||
| 				border-radius: 999px; | ||||
| 				overflow: clip; | ||||
| 
 | ||||
| 				> .highlight { | ||||
| 					position: absolute; | ||||
| 					top: 0; | ||||
| 					left: 0; | ||||
| 					height: 100%; | ||||
| 					background: var(--accent); | ||||
| 			box-shadow: 0 0 6px rgba(0, 0, 0, 0.3); | ||||
| 			box-sizing: content-box; | ||||
| 					opacity: 0.5; | ||||
| 					transition: width 0.2s cubic-bezier(0,0,0,1); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 		&::-moz-range-thumb { | ||||
| 			-moz-appearance: none; | ||||
| 			appearance: none; | ||||
| 			cursor: pointer; | ||||
| 			width: 20px; | ||||
| 			height: 20px; | ||||
| 			display: block; | ||||
| 			border-radius: 50%; | ||||
| 			border: none; | ||||
| 			> .ticks { | ||||
| 				$tickWidth: 3px; | ||||
| 
 | ||||
| 				position: absolute; | ||||
| 				top: 0; | ||||
| 				bottom: 0; | ||||
| 				left: 0; | ||||
| 				right: 0; | ||||
| 				margin: auto; | ||||
| 				width: calc(100% - #{$thumbWidth}); | ||||
| 
 | ||||
| 				> .tick { | ||||
| 					position: absolute; | ||||
| 					bottom: 0; | ||||
| 					width: $tickWidth; | ||||
| 					height: 3px; | ||||
| 					margin-left: - math.div($tickWidth, 2); | ||||
| 					background: var(--divider); | ||||
| 					border-radius: 999px; | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			> .thumb { | ||||
| 				position: absolute; | ||||
| 				width: $thumbWidth; | ||||
| 				height: $thumbHeight; | ||||
| 				cursor: grab; | ||||
| 				background: var(--accent); | ||||
| 			box-shadow: 0 0 6px rgba(0, 0, 0, 0.3); | ||||
| 				border-radius: 999px; | ||||
| 				transition: left 0.2s cubic-bezier(0,0,0,1); | ||||
| 
 | ||||
| 				&:hover { | ||||
| 					background: var(--accentLighten); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <div v-size="{ max: [500] }" v-sticky-container class="vrtktovh"> | ||||
| <div v-size="{ max: [500] }" v-sticky-container class="vrtktovh _formBlock"> | ||||
| 	<div class="label"><slot name="label"></slot></div> | ||||
| 	<div class="main"> | ||||
| 	<div class="main _formRoot"> | ||||
| 		<slot></slot> | ||||
| 	</div> | ||||
| </div> | ||||
|  | @ -17,15 +17,33 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .vrtktovh { | ||||
| 	margin: 0; | ||||
| 	border-top: solid 0.5px var(--divider); | ||||
| 	border-bottom: solid 0.5px var(--divider); | ||||
| 	padding: 24px 0; | ||||
| 
 | ||||
| 	& + .vrtktovh { | ||||
| 		border-top: none; | ||||
| 	} | ||||
| 
 | ||||
| 	&:first-child { | ||||
| 		border-top: none; | ||||
| 	} | ||||
| 
 | ||||
| 	&:last-child { | ||||
| 		border-bottom: none; | ||||
| 	} | ||||
| 
 | ||||
| 	> .label { | ||||
| 		font-weight: bold; | ||||
| 		padding: 24px 0 16px 0; | ||||
| 		padding: 0 0 16px 0; | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			display: none; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .main { | ||||
| 		margin-bottom: 32px; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| 	<div class="label" @click="focus"><slot name="label"></slot></div> | ||||
| 	<div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick"> | ||||
| 		<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> | ||||
| 		<select ref="inputEl" v-model="v" | ||||
| 		<select ref="inputEl" v-model="v" v-panel | ||||
| 			class="select" | ||||
| 			:disabled="disabled" | ||||
| 			:required="required" | ||||
|  | @ -201,7 +201,7 @@ export default defineComponent({ | |||
| .vblkjoeq { | ||||
| 	> .label { | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 0 0 8px 12px; | ||||
| 		padding: 0 0 8px 0; | ||||
| 		user-select: none; | ||||
| 
 | ||||
| 		&:empty { | ||||
|  | @ -210,8 +210,8 @@ export default defineComponent({ | |||
| 	} | ||||
| 
 | ||||
| 	> .caption { | ||||
| 		font-size: 0.8em; | ||||
| 		padding: 8px 0 0 12px; | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 8px 0 0 0; | ||||
| 		color: var(--fgTransparentWeak); | ||||
| 
 | ||||
| 		&:empty { | ||||
|  | @ -242,8 +242,7 @@ export default defineComponent({ | |||
| 			font-weight: normal; | ||||
| 			font-size: 1em; | ||||
| 			color: var(--fg); | ||||
| 			background: var(--panel); | ||||
| 			border: solid 1px var(--inputBorder); | ||||
| 			border: solid 1px var(--panel); | ||||
| 			border-radius: 6px; | ||||
| 			outline: none; | ||||
| 			box-shadow: none; | ||||
|  |  | |||
|  | @ -18,11 +18,9 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .adhpbeou { | ||||
| 	margin: 1.5em 0; | ||||
| 
 | ||||
| 	> .label { | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 0 0 8px 12px; | ||||
| 		padding: 0 0 8px 0; | ||||
| 		user-select: none; | ||||
| 
 | ||||
| 		&:empty { | ||||
|  | @ -31,20 +29,13 @@ export default defineComponent({ | |||
| 	} | ||||
| 
 | ||||
| 	> .caption { | ||||
| 		font-size: 0.8em; | ||||
| 		padding: 8px 0 0 12px; | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 8px 0 0 0; | ||||
| 		color: var(--fgTransparentWeak); | ||||
| 
 | ||||
| 		&:empty { | ||||
| 			display: none; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .content { | ||||
| 		position: relative; | ||||
| 		background: var(--panel); | ||||
| 		border: solid 0.5px var(--inputBorder); | ||||
| 		border-radius: 6px; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
							
								
								
									
										98
									
								
								packages/client/src/components/form/suspense.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								packages/client/src/components/form/suspense.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,98 @@ | |||
| <template> | ||||
| <transition name="fade" mode="out-in"> | ||||
| 	<div v-if="pending"> | ||||
| 		<MkLoading/> | ||||
| 	</div> | ||||
| 	<div v-else-if="resolved"> | ||||
| 		<slot :result="result"></slot> | ||||
| 	</div> | ||||
| 	<div v-else> | ||||
| 		<div class="wszdbhzo"> | ||||
| 			<div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div> | ||||
| 			<MkButton inline class="retry" @click="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, PropType, ref, watch } from 'vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		p: { | ||||
| 			type: Function as PropType<() => Promise<any>>, | ||||
| 			required: true, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	setup(props, context) { | ||||
| 		const pending = ref(true); | ||||
| 		const resolved = ref(false); | ||||
| 		const rejected = ref(false); | ||||
| 		const result = ref(null); | ||||
| 
 | ||||
| 		const process = () => { | ||||
| 			if (props.p == null) { | ||||
| 				return; | ||||
| 			} | ||||
| 			const promise = props.p(); | ||||
| 			pending.value = true; | ||||
| 			resolved.value = false; | ||||
| 			rejected.value = false; | ||||
| 			promise.then((_result) => { | ||||
| 				pending.value = false; | ||||
| 				resolved.value = true; | ||||
| 				result.value = _result; | ||||
| 			}); | ||||
| 			promise.catch(() => { | ||||
| 				pending.value = false; | ||||
| 				rejected.value = true; | ||||
| 			}); | ||||
| 		}; | ||||
| 
 | ||||
| 		watch(() => props.p, () => { | ||||
| 			process(); | ||||
| 		}, { | ||||
| 			immediate: true | ||||
| 		}); | ||||
| 
 | ||||
| 		const retry = () => { | ||||
| 			process(); | ||||
| 		}; | ||||
| 
 | ||||
| 		return { | ||||
| 			pending, | ||||
| 			resolved, | ||||
| 			rejected, | ||||
| 			result, | ||||
| 			retry, | ||||
| 		}; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
| 	transition: opacity 0.125s ease; | ||||
| } | ||||
| .fade-enter-from, | ||||
| .fade-leave-to { | ||||
| 	opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .wszdbhzo { | ||||
| 	padding: 16px; | ||||
| 	text-align: center; | ||||
| 
 | ||||
| 	> .retry { | ||||
| 		margin-top: 16px; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -18,7 +18,7 @@ | |||
| 	</span> | ||||
| 	<span class="label"> | ||||
| 		<span><slot></slot></span> | ||||
| 		<p><slot name="caption"></slot></p> | ||||
| 		<p class="caption"><slot name="caption"></slot></p> | ||||
| 	</span> | ||||
| </div> | ||||
| </template> | ||||
|  | @ -118,10 +118,14 @@ export default defineComponent({ | |||
| 			transition: inherit; | ||||
| 		} | ||||
| 
 | ||||
| 		> p { | ||||
| 			margin: 0; | ||||
| 		> .caption { | ||||
| 			margin: 8px 0 0 0; | ||||
| 			color: var(--fgTransparentWeak); | ||||
| 			font-size: 90%; | ||||
| 			font-size: 0.85em; | ||||
| 
 | ||||
| 			&:empty { | ||||
| 				display: none; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ | |||
| 	<div class="input" :class="{ disabled, focused, tall, pre }"> | ||||
| 		<textarea ref="inputEl" | ||||
| 			v-model="v" | ||||
| 			v-panel | ||||
| 			:class="{ code, _monospace: code }" | ||||
| 			:disabled="disabled" | ||||
| 			:required="required" | ||||
|  | @ -20,7 +21,7 @@ | |||
| 	</div> | ||||
| 	<div class="caption"><slot name="caption"></slot></div> | ||||
| 
 | ||||
| 	<MkButton v-if="manualSave && changed" primary @click="updated"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> | ||||
| 	<MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -174,7 +175,7 @@ export default defineComponent({ | |||
| .adhpbeos { | ||||
| 	> .label { | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 0 0 8px 12px; | ||||
| 		padding: 0 0 8px 0; | ||||
| 		user-select: none; | ||||
| 
 | ||||
| 		&:empty { | ||||
|  | @ -183,8 +184,8 @@ export default defineComponent({ | |||
| 	} | ||||
| 
 | ||||
| 	> .caption { | ||||
| 		font-size: 0.8em; | ||||
| 		padding: 8px 0 0 12px; | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 8px 0 0 0; | ||||
| 		color: var(--fgTransparentWeak); | ||||
| 
 | ||||
| 		&:empty { | ||||
|  | @ -209,8 +210,7 @@ export default defineComponent({ | |||
| 			font-weight: normal; | ||||
| 			font-size: 1em; | ||||
| 			color: var(--fg); | ||||
| 			background: var(--panel); | ||||
| 			border: solid 0.5px var(--inputBorder); | ||||
| 			border: solid 0.5px var(--panel); | ||||
| 			border-radius: 6px; | ||||
| 			outline: none; | ||||
| 			box-shadow: none; | ||||
|  | @ -248,5 +248,9 @@ export default defineComponent({ | |||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .save { | ||||
| 		margin: 8px 0 0 0; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -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: { | ||||
|  | @ -15,19 +15,35 @@ export default defineComponent({ | |||
| 			type: Number, | ||||
| 			required: false, | ||||
| 			default: null, | ||||
| 		} | ||||
| 		}, | ||||
| 		marginMin: { | ||||
| 			type: Number, | ||||
| 			required: false, | ||||
| 			default: 12, | ||||
| 		}, | ||||
| 		marginMax: { | ||||
| 			type: Number, | ||||
| 			required: false, | ||||
| 			default: 24, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	setup(props, context) { | ||||
| 		let ro: ResizeObserver; | ||||
| 		const root = ref<HTMLElement>(null); | ||||
| 		const content = ref<HTMLElement>(null); | ||||
| 		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)) { | ||||
| 				margin.value = 32; | ||||
| 			if (shouldSpacerMin) { | ||||
| 				margin.value = props.marginMin; | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			if (rect.width > props.contentMax || rect.width > 500) { | ||||
| 				margin.value = props.marginMax; | ||||
| 			} else { | ||||
| 				margin.value = 12; | ||||
| 				margin.value = props.marginMin; | ||||
| 			} | ||||
| 		}; | ||||
| 
 | ||||
|  | @ -40,14 +56,14 @@ export default defineComponent({ | |||
| 				}); | ||||
| 				*/ | ||||
| 				adjust({ | ||||
| 					width: root.value.offsetWidth, | ||||
| 					height: root.value.offsetHeight, | ||||
| 					width: root.value!.offsetWidth, | ||||
| 					height: root.value!.offsetHeight, | ||||
| 				}); | ||||
| 			}); | ||||
| 			ro.observe(root.value); | ||||
| 			ro.observe(root.value!); | ||||
| 
 | ||||
| 			if (props.contentMax) { | ||||
| 				content.value.style.maxWidth = `${props.contentMax}px`; | ||||
| 				content.value!.style.maxWidth = `${props.contentMax}px`; | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										48
									
								
								packages/client/src/components/key-value.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								packages/client/src/components/key-value.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | |||
| <template> | ||||
| <div class="alqyeyti"> | ||||
| 	<div class="key"> | ||||
| 		<slot name="key"></slot> | ||||
| 	</div> | ||||
| 	<div class="value"> | ||||
| 		<slot name="value"></slot> | ||||
| 		<button v-if="copy" v-tooltip="$ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="far fa-copy"></i></button> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		copy: { | ||||
| 			type: String, | ||||
| 			required: false, | ||||
| 			default: null, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	setup(props) { | ||||
| 		const copy_ = () => { | ||||
| 			copyToClipboard(props.copy); | ||||
| 			os.success(); | ||||
| 		}; | ||||
| 
 | ||||
| 		return { | ||||
| 			copy_ | ||||
| 		}; | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .alqyeyti { | ||||
| 	> .key { | ||||
| 		font-size: 0.85em; | ||||
| 		padding: 0 0 0.25em 0; | ||||
| 		opacity: 0.75; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -44,16 +44,36 @@ export default defineComponent({ | |||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			const lightbox = new PhotoSwipeLightbox({ | ||||
| 				dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => ({ | ||||
| 				dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => { | ||||
| 					const item = { | ||||
| 						src: media.url, | ||||
| 						w: media.properties.width, | ||||
| 						h: media.properties.height, | ||||
| 						alt: media.name, | ||||
| 				})), | ||||
| 					}; | ||||
| 					if (media.properties.orientation != null && media.properties.orientation >= 5) { | ||||
| 						[item.w, item.h] = [item.h, item.w]; | ||||
| 					} | ||||
| 					return item; | ||||
| 				}), | ||||
| 				gallery: gallery.value, | ||||
| 				children: '.image', | ||||
| 				thumbSelector: '.image', | ||||
| 				pswpModule: PhotoSwipe | ||||
| 				loop: false, | ||||
| 				padding: window.innerWidth > 500 ? { | ||||
| 					top: 32, | ||||
| 					bottom: 32, | ||||
| 					left: 32, | ||||
| 					right: 32, | ||||
| 				} : { | ||||
| 					top: 0, | ||||
| 					bottom: 0, | ||||
| 					left: 0, | ||||
| 					right: 0, | ||||
| 				}, | ||||
| 				imageClickAction: 'close', | ||||
| 				tapAction: 'toggle-controls', | ||||
| 				pswpModule: PhotoSwipe, | ||||
| 			}); | ||||
| 
 | ||||
| 			lightbox.on('itemData', (e) => { | ||||
|  | @ -68,6 +88,9 @@ export default defineComponent({ | |||
| 				itemData.src = file.url; | ||||
| 				itemData.w = Number(file.properties.width); | ||||
| 				itemData.h = Number(file.properties.height); | ||||
| 				if (file.properties.orientation != null && file.properties.orientation >= 5) { | ||||
| 					[itemData.w, itemData.h] = [itemData.h, itemData.w]; | ||||
| 				} | ||||
| 				itemData.msrc = file.thumbnailUrl; | ||||
| 				itemData.thumbCropped = true; | ||||
| 			}); | ||||
|  |  | |||
|  | @ -184,6 +184,11 @@ export default defineComponent({ | |||
| 								count, speed, | ||||
| 							}, genEl(token.children)); | ||||
| 						} | ||||
| 						case 'rotate': { | ||||
| 							const degrees = parseInt(token.props.args.deg) || '90'; | ||||
| 							style = `transform: rotate(${degrees}deg); transform-origin: center center;`; | ||||
| 							break; | ||||
| 						} | ||||
| 					} | ||||
| 					if (style == null) { | ||||
| 						return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']); | ||||
|  |  | |||
|  | @ -42,8 +42,6 @@ | |||
| 						<MkUserName :user="appearNote.user"/> | ||||
| 					</MkA> | ||||
| 					<span v-if="appearNote.user.isBot" class="is-bot">bot</span> | ||||
| 					<span v-if="appearNote.user.isAdmin" class="admin"><i class="fas fa-bookmark"></i></span> | ||||
| 					<span v-if="!appearNote.user.isAdmin && appearNote.user.isModerator" class="moderator"><i class="far fa-bookmark"></i></span> | ||||
| 					<span v-if="appearNote.visibility !== 'public'" class="visibility"> | ||||
| 						<i v-if="appearNote.visibility === 'home'" class="fas fa-home"></i> | ||||
| 						<i v-else-if="appearNote.visibility === 'followers'" class="fas fa-unlock"></i> | ||||
|  | @ -86,7 +84,9 @@ | |||
| 			</div> | ||||
| 			<footer class="footer"> | ||||
| 				<div class="info"> | ||||
| 					<MkTime class="created-at" :time="appearNote.createdAt" mode="detail"/> | ||||
| 					<MkA class="created-at" :to="notePage(appearNote)"> | ||||
| 						<MkTime :time="appearNote.createdAt" mode="detail"/> | ||||
| 					</MkA> | ||||
| 				</div> | ||||
| 				<XReactionsViewer ref="reactionsViewer" :note="appearNote"/> | ||||
| 				<button class="button _button" @click="reply()"> | ||||
|  | @ -138,6 +138,7 @@ import { url } from '@/config'; | |||
| import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||
| import { checkWordMute } from '@/scripts/check-word-mute'; | ||||
| import { userPage } from '@/filters/user'; | ||||
| import { notePage } from '@/filters/note'; | ||||
| import * as os from '@/os'; | ||||
| import { noteActions, noteViewInterruptors } from '@/store'; | ||||
| import { reactionPicker } from '@/scripts/reaction-picker'; | ||||
|  | @ -183,6 +184,7 @@ export default defineComponent({ | |||
| 			muted: false, | ||||
| 			translation: null, | ||||
| 			translating: false, | ||||
| 			notePage, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -647,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', | ||||
|  | @ -655,7 +657,7 @@ export default defineComponent({ | |||
| 						action: this.promote | ||||
| 					}] | ||||
| 					: [] | ||||
| 				), | ||||
| 				),*/ | ||||
| 				...(this.appearNote.userId != this.$i.id ? [ | ||||
| 					null, | ||||
| 					{ | ||||
|  | @ -1017,12 +1019,6 @@ export default defineComponent({ | |||
| 						border: solid 0.5px var(--divider); | ||||
| 						border-radius: 4px; | ||||
| 					} | ||||
| 
 | ||||
| 					> .admin, | ||||
| 					> .moderator { | ||||
| 						margin-right: 0.5em; | ||||
| 						color: var(--badge); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -5,8 +5,6 @@ | |||
| 	</MkA> | ||||
| 	<div v-if="note.user.isBot" class="is-bot">bot</div> | ||||
| 	<div class="username"><MkAcct :user="note.user"/></div> | ||||
| 	<div v-if="note.user.isAdmin" class="admin"><i class="fas fa-bookmark"></i></div> | ||||
| 	<div v-if="!note.user.isAdmin && note.user.isModerator" class="moderator"><i class="far fa-bookmark"></i></div> | ||||
| 	<div class="info"> | ||||
| 		<MkA class="created-at" :to="notePage(note)"> | ||||
| 			<MkTime :time="note.createdAt"/> | ||||
|  | @ -23,7 +21,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import notePage from '@/filters/note'; | ||||
| import { notePage } from '@/filters/note'; | ||||
| import { userPage } from '@/filters/user'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
|  | @ -79,13 +77,6 @@ export default defineComponent({ | |||
| 		border-radius: 3px; | ||||
| 	} | ||||
| 
 | ||||
| 	> .admin, | ||||
| 	> .moderator { | ||||
| 		flex-shrink: 0; | ||||
| 		margin-right: 0.5em; | ||||
| 		color: var(--badge); | ||||
| 	} | ||||
| 
 | ||||
| 	> .username { | ||||
| 		flex-shrink: 9999999; | ||||
| 		margin: 0 .5em 0 0; | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import notePage from '@/filters/note'; | ||||
| import { notePage } from '@/filters/note'; | ||||
| import XNoteHeader from './note-header.vue'; | ||||
| import XSubNoteContent from './sub-note-content.vue'; | ||||
| import XCwButton from './cw-button.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, | ||||
| 					{ | ||||
|  | @ -858,6 +859,7 @@ export default defineComponent({ | |||
| .tkcbzcuz { | ||||
| 	position: relative; | ||||
| 	transition: box-shadow 0.1s ease; | ||||
| 	font-size: 1.05em; | ||||
| 	overflow: clip; | ||||
| 	contain: content; | ||||
| 
 | ||||
|  |  | |||
|  | @ -74,7 +74,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary'; | |||
| import XReactionIcon from './reaction-icon.vue'; | ||||
| import MkFollowButton from './follow-button.vue'; | ||||
| import XReactionTooltip from './reaction-tooltip.vue'; | ||||
| import notePage from '@/filters/note'; | ||||
| import { notePage } from '@/filters/note'; | ||||
| import { userPage } from '@/filters/user'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import * as os from '@/os'; | ||||
|  | @ -107,28 +107,25 @@ export default defineComponent({ | |||
| 		const reactionRef = ref(null); | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			let readObserver: IntersectionObserver | null = null; | ||||
| 			let connection = null; | ||||
| 
 | ||||
| 			if (!props.notification.isRead) { | ||||
| 				readObserver = new IntersectionObserver((entries, observer) => { | ||||
| 				const readObserver = new IntersectionObserver((entries, observer) => { | ||||
| 					if (!entries.some(entry => entry.isIntersecting)) return; | ||||
| 					os.stream.send('readNotification', { | ||||
| 						id: props.notification.id | ||||
| 					}); | ||||
| 					entries.map(({ target }) => observer.unobserve(target)); | ||||
| 					observer.disconnect(); | ||||
| 				}); | ||||
| 
 | ||||
| 				readObserver.observe(elRef.value); | ||||
| 
 | ||||
| 				connection = os.stream.useChannel('main'); | ||||
| 				connection.on('readAllNotifications', () => readObserver.unobserve(elRef.value)); | ||||
| 			} | ||||
| 				const connection = os.stream.useChannel('main'); | ||||
| 				connection.on('readAllNotifications', () => readObserver.disconnect()); | ||||
| 
 | ||||
| 				onUnmounted(() => { | ||||
| 				if (readObserver) readObserver.unobserve(elRef.value); | ||||
| 				if (connection) connection.dispose(); | ||||
| 					readObserver.disconnect(); | ||||
| 					connection.dispose(); | ||||
| 				}); | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		const followRequestDone = ref(false); | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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,65 +39,67 @@ export default defineComponent({ | |||
| 			default: false, | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 
 | ||||
| 	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; | ||||
| 				} | ||||
| 			}; | ||||
| 
 | ||||
| 			tick(); | ||||
| 			const intevalId = window.setInterval(tick, 3000); | ||||
| 			onUnmounted(() => { | ||||
| 				window.clearInterval(intevalId); | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		const vote = async (id) => { | ||||
| 			if (props.readOnly || closed.value || isVoted.value) return; | ||||
| 
 | ||||
| 			const { canceled } = await os.confirm({ | ||||
| 				type: 'question', | ||||
| 				text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }), | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 
 | ||||
| 			await os.api('notes/polls/vote', { | ||||
| 				noteId: props.note.id, | ||||
| 				choice: id, | ||||
| 			}); | ||||
| 			if (!showResult.value) showResult.value = !props.note.poll.multiple; | ||||
| 		}; | ||||
| 
 | ||||
| 		return { | ||||
| 			remaining: -1, | ||||
| 			showResult: false, | ||||
| 			remaining, | ||||
| 			showResult, | ||||
| 			total, | ||||
| 			isVoted, | ||||
| 			closed, | ||||
| 			timer, | ||||
| 			vote, | ||||
| 		}; | ||||
| 	}, | ||||
| 	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; | ||||
| 			}; | ||||
| 
 | ||||
| 			update(); | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		toggleShowResult() { | ||||
| 			this.showResult = !this.showResult; | ||||
| 		}, | ||||
| 		vote(id) { | ||||
| 			if (this.readOnly || this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return; | ||||
| 			os.api('notes/polls/vote', { | ||||
| 				noteId: this.note.id, | ||||
| 				choice: id | ||||
| 			}).then(() => { | ||||
| 				if (!this.showResult) this.showResult = !this.poll.multiple; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  | @ -112,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; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | @ -160,14 +163,6 @@ export default defineComponent({ | |||
| 	&.done { | ||||
| 		> ul > li { | ||||
| 			cursor: default; | ||||
| 
 | ||||
| 			&:hover { | ||||
| 				background: transparent; | ||||
| 			} | ||||
| 
 | ||||
| 			&:active { | ||||
| 				background: transparent; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -41,6 +41,7 @@ export default defineComponent({ | |||
| 	> .icon { | ||||
| 		display: block; | ||||
| 		width: 60px; | ||||
| 		font-size: 60px; // unicodeな絵文字についてはwidthが効かないため | ||||
| 		margin: 0 auto; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -62,6 +62,7 @@ export default defineComponent({ | |||
| 		> .icon { | ||||
| 			display: block; | ||||
| 			width: 60px; | ||||
| 			font-size: 60px; // unicodeな絵文字についてはwidthが効かないため | ||||
| 			margin: 0 auto; | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -97,9 +97,7 @@ export default defineComponent({ | |||
| 				limit: 11 | ||||
| 			}); | ||||
| 
 | ||||
| 			const users = reactions | ||||
| 				.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) | ||||
| 				.map(x => x.user); | ||||
| 			const users = reactions.map(x => x.user); | ||||
| 
 | ||||
| 			os.popup(XDetails, { | ||||
| 				showing, | ||||
|  |  | |||
|  | @ -48,9 +48,7 @@ export default defineComponent({ | |||
| 				limit: 11 | ||||
| 			}); | ||||
| 
 | ||||
| 			const users = renotes | ||||
| 				.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) | ||||
| 				.map(x => x.user); | ||||
| 			const users = renotes.map(x => x.user); | ||||
| 
 | ||||
| 			if (users.length < 1) return; | ||||
| 
 | ||||
|  |  | |||
|  | @ -197,6 +197,14 @@ export default defineComponent({ | |||
| 					}); | ||||
| 					break; | ||||
| 				} | ||||
| 				case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': { | ||||
| 					os.alert({ | ||||
| 						type: 'error', | ||||
| 						title: this.$ts.loginFailed, | ||||
| 						text: this.$ts.incorrectPassword, | ||||
| 					}); | ||||
| 					break; | ||||
| 				} | ||||
| 				case 'e03a5f46-d309-4865-9b69-56282d94e1eb': { | ||||
| 					showSuspendedDialog(); | ||||
| 					break; | ||||
|  |  | |||
|  | @ -142,12 +142,12 @@ export default defineComponent({ | |||
| 	padding: 8px 14px; | ||||
| 	text-align: center; | ||||
| 	font-weight: normal; | ||||
| 	font-size: 0.8em; | ||||
| 	font-size: 0.9em; | ||||
| 	line-height: 22px; | ||||
| 	box-shadow: none; | ||||
| 	text-decoration: none; | ||||
| 	background: var(--buttonBg); | ||||
| 	border-radius: 4px; | ||||
| 	border-radius: 5px; | ||||
| 	overflow: clip; | ||||
| 	box-sizing: border-box; | ||||
| 	transition: background 0.1s ease; | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| <div ref="items" v-hotkey="keymap" | ||||
| 	class="rrevdjwt" | ||||
| 	:class="{ center: align === 'center' }" | ||||
| 	:style="{ width: width ? width + 'px' : null }" | ||||
| 	:style="{ width: width ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }" | ||||
| 	@contextmenu.self="e => e.preventDefault()" | ||||
| > | ||||
| 	<template v-for="(item, i) in items2"> | ||||
|  | @ -64,6 +64,10 @@ export default defineComponent({ | |||
| 			type: Number, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		maxHeight: { | ||||
| 			type: Number, | ||||
| 			required: false | ||||
| 		}, | ||||
| 	}, | ||||
| 	emits: ['close'], | ||||
| 	data() { | ||||
|  | @ -146,9 +150,10 @@ export default defineComponent({ | |||
| <style lang="scss" scoped> | ||||
| .rrevdjwt { | ||||
| 	padding: 8px 0; | ||||
| 	box-sizing: border-box; | ||||
| 	min-width: 200px; | ||||
| 	max-height: 90vh; | ||||
| 	overflow: auto; | ||||
| 	overscroll-behavior: contain; | ||||
| 
 | ||||
| 	&.center { | ||||
| 		> .item { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <MkPopup ref="popup" :src="src" @closed="$emit('closed')"> | ||||
| 	<MkMenu :items="items" :align="align" :width="width" class="_popup _shadow" @close="$refs.popup.close()"/> | ||||
| <MkPopup ref="popup" v-slot="{ maxHeight, close }" :src="src" @closed="$emit('closed')"> | ||||
| 	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" class="_popup _shadow" @close="close()"/> | ||||
| </MkPopup> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,15 +1,15 @@ | |||
| <template> | ||||
| <transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered"> | ||||
| <transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="$emit('closed')" @enter="$emit('opening')"> | ||||
| 	<div v-show="manualShowing != null ? manualShowing : showing" ref="content" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> | ||||
| 		<slot></slot> | ||||
| 		<slot :max-height="maxHeight" :close="close"></slot> | ||||
| 	</div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, PropType } from 'vue'; | ||||
| import { defineComponent, nextTick, onMounted, onUnmounted, PropType, ref, watch } from 'vue'; | ||||
| 
 | ||||
| function getFixedContainer(el: Element | null): Element | null { | ||||
| function getFixedContainer(el: Element | null | undefined): Element | null { | ||||
| 	if (el == null || el.tagName === 'BODY') return null; | ||||
| 	const position = window.getComputedStyle(el).getPropertyValue('position'); | ||||
| 	if (position === 'fixed') { | ||||
|  | @ -41,55 +41,40 @@ export default defineComponent({ | |||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false, | ||||
| 		} | ||||
| 		}, | ||||
| 		noOverlap: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: true, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['opening', 'click', 'esc', 'close', 'closed'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showing: true, | ||||
| 			fixed: false, | ||||
| 			transformOrigin: 'center', | ||||
| 			contentClicking: false, | ||||
| 		}; | ||||
| 	}, | ||||
| 	setup(props, context) { | ||||
| 		const maxHeight = ref<number>(); | ||||
| 		const fixed = ref(false); | ||||
| 		const transformOrigin = ref('center'); | ||||
| 		const showing = ref(true); | ||||
| 		const content = ref<HTMLElement>(); | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$watch('src', () => { | ||||
| 			if (this.src) { | ||||
| 		const close = () => { | ||||
| 			// eslint-disable-next-line vue/no-mutating-props | ||||
| 				this.src.style.pointerEvents = 'none'; | ||||
| 			} | ||||
| 			this.fixed = getFixedContainer(this.src) != null; | ||||
| 			this.$nextTick(() => { | ||||
| 				this.align(); | ||||
| 			}); | ||||
| 		}, { immediate: true }); | ||||
| 			if (props.src) props.src.style.pointerEvents = 'auto'; | ||||
| 			showing.value = false; | ||||
| 			context.emit('close'); | ||||
| 		}; | ||||
| 
 | ||||
| 		this.$nextTick(() => { | ||||
| 			const popover = this.$refs.content as any; | ||||
| 			new ResizeObserver((entries, observer) => { | ||||
| 				this.align(); | ||||
| 			}).observe(popover); | ||||
| 		}); | ||||
| 		const MARGIN = 16; | ||||
| 
 | ||||
| 		document.addEventListener('mousedown', this.onDocumentClick, { passive: true }); | ||||
| 	}, | ||||
| 		const align = () => { | ||||
| 			if (props.src == null) return; | ||||
| 
 | ||||
| 	beforeUnmount() { | ||||
| 		document.removeEventListener('mousedown', this.onDocumentClick); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		align() { | ||||
| 			if (this.src == null) return; | ||||
| 
 | ||||
| 			const popover = this.$refs.content as any; | ||||
| 			const popover = content.value!; | ||||
| 
 | ||||
| 			if (popover == null) return; | ||||
| 
 | ||||
| 			const rect = this.src.getBoundingClientRect(); | ||||
| 			const rect = props.src.getBoundingClientRect(); | ||||
| 			 | ||||
| 			const width = popover.offsetWidth; | ||||
| 			const height = popover.offsetHeight; | ||||
|  | @ -97,81 +82,84 @@ export default defineComponent({ | |||
| 			let left; | ||||
| 			let top; | ||||
| 
 | ||||
| 			if (this.srcCenter) { | ||||
| 				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); | ||||
| 				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.src.offsetHeight / 2); | ||||
| 			if (props.srcCenter) { | ||||
| 				const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2); | ||||
| 				const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2); | ||||
| 				left = (x - (width / 2)); | ||||
| 				top = (y - (height / 2)); | ||||
| 			} else { | ||||
| 				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.src.offsetWidth / 2); | ||||
| 				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.src.offsetHeight; | ||||
| 				const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2); | ||||
| 				const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight; | ||||
| 				left = (x - (width / 2)); | ||||
| 				top = y; | ||||
| 			} | ||||
| 
 | ||||
| 			if (this.fixed) { | ||||
| 			if (fixed.value) { | ||||
| 				// 画面から横にはみ出る場合 | ||||
| 				if (left + width > window.innerWidth) { | ||||
| 					left = window.innerWidth - width; | ||||
| 				} | ||||
| 
 | ||||
| 				if (top + height > window.innerHeight) { | ||||
| 					top = window.innerHeight - height; | ||||
| 				// 画面から縦にはみ出る場合 | ||||
| 				if (top + height > (window.innerHeight - MARGIN)) { | ||||
| 					if (props.noOverlap) { | ||||
| 						const underSpace = (window.innerHeight - MARGIN) - top; | ||||
| 						const upperSpace = (rect.top - MARGIN); | ||||
| 						if (underSpace >= (upperSpace / 3)) { | ||||
| 							maxHeight.value =  underSpace; | ||||
| 						} else { | ||||
| 							maxHeight.value =  upperSpace; | ||||
| 							top = (upperSpace + MARGIN) - height; | ||||
| 						} | ||||
| 					} else { | ||||
| 						top = (window.innerHeight - MARGIN) - height; | ||||
| 					} | ||||
| 				} | ||||
| 			} else { | ||||
| 				// 画面から横にはみ出る場合 | ||||
| 				if (left + width - window.pageXOffset > window.innerWidth) { | ||||
| 					left = window.innerWidth - width + window.pageXOffset - 1; | ||||
| 				} | ||||
| 
 | ||||
| 				if (top + height - window.pageYOffset > window.innerHeight) { | ||||
| 					top = window.innerHeight - height + window.pageYOffset - 1; | ||||
| 				// 画面から縦にはみ出る場合 | ||||
| 				if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) { | ||||
| 					if (props.noOverlap) { | ||||
| 						const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset); | ||||
| 						const upperSpace = (rect.top - MARGIN); | ||||
| 						if (underSpace >= (upperSpace / 3)) { | ||||
| 							maxHeight.value =  underSpace; | ||||
| 						} else { | ||||
| 							maxHeight.value =  upperSpace; | ||||
| 							top = window.pageYOffset + ((upperSpace + MARGIN) - height); | ||||
| 						} | ||||
| 					} else { | ||||
| 						top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if (top < 0) { | ||||
| 				top = 0; | ||||
| 				top = MARGIN; | ||||
| 			} | ||||
| 
 | ||||
| 			if (left < 0) { | ||||
| 				left = 0; | ||||
| 			} | ||||
| 
 | ||||
| 			if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) { | ||||
| 				this.transformOrigin = 'center top'; | ||||
| 			if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) { | ||||
| 				transformOrigin.value = 'center top'; | ||||
| 			} else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) { | ||||
| 				transformOrigin.value = 'center bottom'; | ||||
| 			} else { | ||||
| 				this.transformOrigin = 'center'; | ||||
| 				transformOrigin.value = 'center'; | ||||
| 			} | ||||
| 
 | ||||
| 			popover.style.left = left + 'px'; | ||||
| 			popover.style.top = top + 'px'; | ||||
| 		}, | ||||
| 		}; | ||||
| 
 | ||||
| 		childRendered() { | ||||
| 			// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する | ||||
| 			const content = this.$refs.content.children[0]; | ||||
| 			content.addEventListener('mousedown', e => { | ||||
| 				this.contentClicking = true; | ||||
| 				window.addEventListener('mouseup', e => { | ||||
| 					// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ | ||||
| 					setTimeout(() => { | ||||
| 						this.contentClicking = false; | ||||
| 					}, 100); | ||||
| 				}, { passive: true, once: true }); | ||||
| 			}, { passive: true }); | ||||
| 		}, | ||||
| 
 | ||||
| 		close() { | ||||
| 			// eslint-disable-next-line vue/no-mutating-props | ||||
| 			if (this.src) this.src.style.pointerEvents = 'auto'; | ||||
| 			this.showing = false; | ||||
| 			this.$emit('close'); | ||||
| 		}, | ||||
| 
 | ||||
| 		onClosed() { | ||||
| 			this.$emit('closed'); | ||||
| 		}, | ||||
| 
 | ||||
| 		onDocumentClick(ev) { | ||||
| 			const flyoutElement = this.$refs.content; | ||||
| 		const onDocumentClick = (ev: MouseEvent) => { | ||||
| 			const flyoutElement = content.value; | ||||
| 			let targetElement = ev.target; | ||||
| 			do { | ||||
| 				if (targetElement === flyoutElement) { | ||||
|  | @ -179,9 +167,45 @@ export default defineComponent({ | |||
| 				} | ||||
| 				targetElement = targetElement.parentNode; | ||||
| 			} while (targetElement); | ||||
| 			this.close(); | ||||
| 		} | ||||
| 			close(); | ||||
| 		}; | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			watch(() => props.src, async () => { | ||||
| 				if (props.src) { | ||||
| 					// eslint-disable-next-line vue/no-mutating-props | ||||
| 					props.src.style.pointerEvents = 'none'; | ||||
| 				} | ||||
| 				fixed.value = getFixedContainer(props.src) != null; | ||||
| 
 | ||||
| 				await nextTick() | ||||
| 				 | ||||
| 				align(); | ||||
| 			}, { immediate: true, }); | ||||
| 
 | ||||
| 			nextTick(() => { | ||||
| 				const popover = content.value; | ||||
| 				new ResizeObserver((entries, observer) => { | ||||
| 					align(); | ||||
| 				}).observe(popover!); | ||||
| 			}); | ||||
| 
 | ||||
| 			document.addEventListener('mousedown', onDocumentClick, { passive: true }); | ||||
| 
 | ||||
| 			onUnmounted(() => { | ||||
| 				document.removeEventListener('mousedown', onDocumentClick); | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			showing, | ||||
| 			fixed, | ||||
| 			content, | ||||
| 			transformOrigin, | ||||
| 			maxHeight, | ||||
| 			close, | ||||
| 		}; | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -51,9 +51,8 @@ export default defineComponent({ | |||
| 		} | ||||
| 
 | ||||
| 		> .title { | ||||
| 			font-size: 0.9em; | ||||
| 			opacity: 0.7; | ||||
| 			margin: 0 0 8px 12px; | ||||
| 			margin: 0 0 8px 0; | ||||
| 		} | ||||
| 	 | ||||
| 		> .items { | ||||
|  | @ -64,7 +63,6 @@ export default defineComponent({ | |||
| 				box-sizing: border-box; | ||||
| 				padding: 10px 16px 10px 8px; | ||||
| 				border-radius: 9px; | ||||
| 				font-size: 0.9em; | ||||
| 
 | ||||
| 				&:hover { | ||||
| 					text-decoration: none; | ||||
|  |  | |||
|  | @ -1,13 +1,13 @@ | |||
| <template> | ||||
| <transition name="tooltip" appear @after-leave="$emit('closed')"> | ||||
| 	<div v-show="showing" ref="content" class="buebdbiu _acrylic _shadow" :style="{ maxWidth: maxWidth + 'px' }"> | ||||
| 	<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ maxWidth: maxWidth + 'px' }"> | ||||
| 		<slot>{{ text }}</slot> | ||||
| 	</div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
|  | @ -31,35 +31,64 @@ export default defineComponent({ | |||
| 
 | ||||
| 	emits: ['closed'], | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$nextTick(() => { | ||||
| 			if (this.source == null) { | ||||
| 				this.$emit('closed'); | ||||
| 				return; | ||||
| 			} | ||||
| 	setup(props, context) { | ||||
| 		const el = ref<HTMLElement>(); | ||||
| 
 | ||||
| 			const rect = this.source.getBoundingClientRect(); | ||||
| 		const setPosition = () => { | ||||
| 			if (el.value == null) return; | ||||
| 
 | ||||
| 			const contentWidth = this.$refs.content.offsetWidth; | ||||
| 			const contentHeight = this.$refs.content.offsetHeight; | ||||
| 			const rect = props.source.getBoundingClientRect(); | ||||
| 
 | ||||
| 			let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); | ||||
| 			const contentWidth = el.value.offsetWidth; | ||||
| 			const contentHeight = el.value.offsetHeight; | ||||
| 
 | ||||
| 			let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2); | ||||
| 			let top = rect.top + window.pageYOffset - contentHeight; | ||||
| 
 | ||||
| 			left -= (this.$el.offsetWidth / 2); | ||||
| 			left -= (el.value.offsetWidth / 2); | ||||
| 
 | ||||
| 			if (left + contentWidth - window.pageXOffset > window.innerWidth) { | ||||
| 				left = window.innerWidth - contentWidth + window.pageXOffset - 1; | ||||
| 			} | ||||
| 
 | ||||
| 			if (top - window.pageYOffset < 0) { | ||||
| 				top = rect.top + window.pageYOffset + this.source.offsetHeight; | ||||
| 				this.$refs.content.style.transformOrigin = 'center top'; | ||||
| 				top = rect.top + window.pageYOffset + props.source.offsetHeight; | ||||
| 				el.value.style.transformOrigin = 'center top'; | ||||
| 			} | ||||
| 
 | ||||
| 			this.$el.style.left = left + 'px'; | ||||
| 			this.$el.style.top = top + 'px'; | ||||
| 			el.value.style.left = left + 'px'; | ||||
| 			el.value.style.top = top + 'px'; | ||||
| 		}; | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			nextTick(() => { | ||||
| 				if (props.source == null) { | ||||
| 					context.emit('closed'); | ||||
| 					return; | ||||
| 				} | ||||
| 
 | ||||
| 				setPosition(); | ||||
| 
 | ||||
| 				let loopHandler; | ||||
| 
 | ||||
| 				const loop = () => { | ||||
| 					loopHandler = window.requestAnimationFrame(() => { | ||||
| 						setPosition(); | ||||
| 						loop(); | ||||
| 					}); | ||||
| 				}; | ||||
| 
 | ||||
| 				loop(); | ||||
| 
 | ||||
| 				onUnmounted(() => { | ||||
| 					window.cancelAnimationFrame(loopHandler); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			el, | ||||
| 		}; | ||||
| 	}, | ||||
| }) | ||||
| </script> | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import appear from './appear'; | |||
| import anim from './anim'; | ||||
| import stickyContainer from './sticky-container'; | ||||
| import clickAnime from './click-anime'; | ||||
| import panel from './panel'; | ||||
| 
 | ||||
| export default function(app: App) { | ||||
| 	app.directive('userPreview', userPreview); | ||||
|  | @ -23,4 +24,5 @@ export default function(app: App) { | |||
| 	app.directive('anim', anim); | ||||
| 	app.directive('click-anime', clickAnime); | ||||
| 	app.directive('sticky-container', stickyContainer); | ||||
| 	app.directive('panel', panel); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										24
									
								
								packages/client/src/directives/panel.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/client/src/directives/panel.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| import { Directive } from 'vue'; | ||||
| 
 | ||||
| export default { | ||||
| 	mounted(src, binding, vn) { | ||||
| 		const getBgColor = (el: HTMLElement) => { | ||||
| 			const style = window.getComputedStyle(el); | ||||
| 			if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { | ||||
| 				return style.backgroundColor; | ||||
| 			} else { | ||||
| 				return getBgColor(el.parentElement); | ||||
| 			} | ||||
| 		} | ||||
| 	 | ||||
| 		const parentBg = getBgColor(src.parentElement); | ||||
| 
 | ||||
| 		const myBg = getComputedStyle(document.documentElement).getPropertyValue('--panel'); | ||||
| 
 | ||||
| 		if (parentBg === myBg) { | ||||
| 			src.style.backgroundColor = 'var(--bg)'; | ||||
| 		} else { | ||||
| 			src.style.backgroundColor = 'var(--panel)'; | ||||
| 		} | ||||
| 	}, | ||||
| } as Directive; | ||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue