Merge branch 'develop'
This commit is contained in:
		
						commit
						9c2f5ee041
					
				
					 138 changed files with 2046 additions and 1511 deletions
				
			
		
							
								
								
									
										18
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -7,6 +7,24 @@ | |||
| 
 | ||||
| --> | ||||
| 
 | ||||
| ## 12.103.0 (2022/02/02) | ||||
| 
 | ||||
| ### Improvements | ||||
| - クライアント: 連合インスタンスページからインスタンス情報再取得を行えるように | ||||
| 
 | ||||
| ### Bugfixes | ||||
| - クライアント: 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正 | ||||
| - クライアント: 「クリップ」ページが開かない問題を修正 | ||||
| - クライアント: トレンドウィジェットが動作しないのを修正 | ||||
| - クライアント: フェデレーションウィジェットが動作しないのを修正 | ||||
| - クライアント: リアクション設定で絵文字ピッカーが開かないのを修正 | ||||
| - クライアント: DMページでメンションが含まれる問題を修正 | ||||
| - クライアント: 投稿フォームのハッシュタグ保持フィールドが動作しない問題を修正 | ||||
| - クライアント: サイドビューが動かないのを修正 | ||||
| - クライアント: ensure that specified users does not get duplicates | ||||
| - Add `img-src` and `media-src` directives to `Content-Security-Policy` for | ||||
|   files and media proxy | ||||
| 
 | ||||
| ## 12.102.1 (2022/01/27) | ||||
| ### Bugfixes | ||||
| - チャットが表示できない問題を修正 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ We're glad you're interested in contributing Misskey! In this document you will | |||
| 
 | ||||
| **ℹ️ Important:** This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.** | ||||
| Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\ | ||||
| The accuracy of translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. | ||||
| The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. | ||||
| It will also allow the reader to use the translation tool of their preference if necessary. | ||||
| 
 | ||||
| ## Issues | ||||
|  |  | |||
|  | @ -176,3 +176,7 @@ describe('After user singed in', () => { | |||
| 		cy.contains('Hello, Misskey!'); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| // TODO: 投稿フォームの公開範囲指定のテスト
 | ||||
| // TODO: 投稿フォームのファイル添付のテスト
 | ||||
| // TODO: 投稿フォームのハッシュタグ保持フィールドのテスト
 | ||||
|  |  | |||
|  | @ -1 +1,510 @@ | |||
| --- | ||||
| _lang_: "বাংলা" | ||||
| headlineMisskey: "নোট ব্যাবহার করে সংযুক্ত নেটওয়ার্ক" | ||||
| introMisskey: "স্বাগতম! মিসকি একটি ওপেন সোর্স, ডিসেন্ট্রালাইজড মাইক্রোব্লগিং পরিষেবা। \n\"নোট\" তৈরির মাধ্যমে যা ঘটছে তা সবার সাথে শেয়ার করুন 📡\n\"রিঅ্যাকশন\" গুলির মাধ্যমে যেকোনো নোট সম্পর্কে আপনার অনুভূতি ব্যাক্ত করতে পারেন 👍\nএকটি নতুন দুনিয়া ঘুরে দেখুন 🚀\n" | ||||
| monthAndDay: "{day}/{month}" | ||||
| search: "খুঁজুন" | ||||
| notifications: "বিজ্ঞপ্তি" | ||||
| username: "ব্যবহারকারীর নাম" | ||||
| password: "পাসওয়ার্ড" | ||||
| forgotPassword: "পাসওয়ার্ড ভুলে গেছেন" | ||||
| fetchingAsApObject: "ফেডিভার্স থেকে খবর আনা হচ্ছে..." | ||||
| ok: "ঠিক" | ||||
| gotIt: "বুঝেছি" | ||||
| cancel: "বাতিল" | ||||
| enterUsername: "ইউজারনেম লিখুন" | ||||
| renotedBy: "{user} রিনোট করেছেন" | ||||
| noNotes: "কোন নোট নেই" | ||||
| noNotifications: "কোনো বিজ্ঞপ্তি নেই" | ||||
| instance: "ইন্সট্যান্স" | ||||
| settings: "সেটিংস" | ||||
| basicSettings: "সাধারণ সেটিংস" | ||||
| otherSettings: "অন্যান্য সেটিংস" | ||||
| openInWindow: "নতুন উইন্ডোতে খুলা" | ||||
| profile: "প্রোফাইল" | ||||
| timeline: "টাইমলাইন" | ||||
| noAccountDescription: "এই ব্যাবহারকারীর কোন বায়ো নেই" | ||||
| login: "প্রবেশ করুন" | ||||
| loggingIn: "প্রবেশ করা হচ্ছে..." | ||||
| logout: "লগআউট" | ||||
| signup: "নিবন্ধন করুন" | ||||
| uploading: "আপলোড হচ্ছ …" | ||||
| save: "সংরক্ষণ" | ||||
| users: "ব্যবহারকারীগণ" | ||||
| addUser: "ব্যবহারকারী যোগ করুন" | ||||
| favorite: "পছন্দ" | ||||
| favorites: "পছন্দগুলি" | ||||
| unfavorite: "পছন্দ না" | ||||
| favorited: "পছন্দ করা হয়েছে" | ||||
| alreadyFavorited: "ইতিমধ্যে পছন্দ করা হয়েছে" | ||||
| cantFavorite: "পছন্দ করা যায়নি" | ||||
| pin: "পিন করা" | ||||
| unpin: "পিন সরান" | ||||
| copyContent: "বিষয়বস্তু কপি করুন" | ||||
| copyLink: "লিঙ্ক কপি করুন" | ||||
| delete: "মুছুন" | ||||
| deleteAndEdit: "মুছুন এবং সম্পাদনা করুন" | ||||
| deleteAndEditConfirm: "আপনি কি এই নোটটি মুছে এটি সম্পাদনা করার বিষয়ে নিশ্চিত? আপনি এটির সমস্ত রিঅ্যাকশন, রিনোট এবং জবাব হারাবেন।" | ||||
| addToList: "লিস্ট এ যোগ করুন" | ||||
| sendMessage: "একটি বার্তা পাঠান" | ||||
| copyUsername: "ব্যবহারকারীর নাম কপি করুন" | ||||
| searchUser: "ব্যবহারকারী খুঁজুন..." | ||||
| reply: "জবাব" | ||||
| loadMore: "আরও দেখুন" | ||||
| showMore: "আরও দেখুন" | ||||
| youGotNewFollower: "আপনাকে অনুসরণ করছে" | ||||
| receiveFollowRequest: "অনুসরণ করার জন্য অনুরোধ পাওয়া গেছে" | ||||
| followRequestAccepted: "অনুসরণ করার অনুরোধ গৃহীত হয়েছে" | ||||
| mention: "উল্লেখ" | ||||
| mentions: "উল্লেখসমূহ" | ||||
| directNotes: "ডাইরেক্ট নোটগুলি" | ||||
| importAndExport: "আমদানি এবং রপ্তানি" | ||||
| import: "আমদানি করুণ" | ||||
| export: "রপ্তানি" | ||||
| files: "ফাইলগুলি" | ||||
| download: "ডাউনলোড" | ||||
| driveFileDeleteConfirm: "আপনি কি নিশ্চিত যে আপনি \"{name}\" ডিলিট করতে চান? যে সকল নোটের সাথে এই ফাইলটি সংযুক্ত সেগুলোও ডিলিট করা হবে।" | ||||
| unfollowConfirm: "{name} কে আনফলোও করার ব্যাপারে নিশ্চিত?" | ||||
| exportRequested: "আপনার তথ্যসমূহ রপ্তানির জন্য অনুরোধ করেছেন। এতে কিছু সময় লাগতে পারে। রপ্তানি সম্পন্ন হলে তা আপনার ড্রাইভে সংরক্ষিত হবে।" | ||||
| importRequested: "আপনার তথ্যসমূহ আমদানির জন্য অনুরোধ করেছেন। এতে কিছু সময় লাগতে পারে। " | ||||
| lists: "লিস্ট" | ||||
| noLists: "কোন লিস্ট নেই" | ||||
| note: "নোট" | ||||
| notes: "নোটগুলি" | ||||
| following: "অনুসরণ করা হচ্ছে" | ||||
| followers: "অনুসরণকারী" | ||||
| followsYou: "আপনাকে অনুসরণ করে" | ||||
| createList: "লিস্ট তৈরি করুন" | ||||
| manageLists: "লিস্ট ব্যাবস্থাপনা" | ||||
| error: "সমস্যা" | ||||
| somethingHappened: "একটি ত্রুটি হয়েছে" | ||||
| retry: "আবার চেষ্টা করুন" | ||||
| pageLoadError: "পেজ লোড করা যায়নি" | ||||
| pageLoadErrorDescription: "এটি সাধারনত নেটওয়ার্কের সমস্যার বা ব্রাউজার ক্যাশের কারণে ঘটে থাকে। ব্রাউজার এর ক্যাশ পরিষ্কার করুন এবং একটু পর আবার চেষ্টা করুন। " | ||||
| serverIsDead: "এই সার্ভার বর্তমানে সাড়া দিচ্ছে না। একটু পরে আবার চেষ্টা করুন।" | ||||
| youShouldUpgradeClient: "এই পেজ দেখার জন্য আপনার ব্রাউজার রিফ্রেশ করে ক্লায়েন্ট আপডেট করুন। " | ||||
| enterListName: "লিস্টের নাম লিখুন" | ||||
| privacy: "গোপনীয়তা" | ||||
| makeFollowManuallyApprove: "অনুসরণ করার অনুরোধগুলি গৃহীত হওয়ার জন্য আপনার অনুমতি লাগবে" | ||||
| defaultNoteVisibility: "ডিফল্ট দৃশ্যমান্যতা" | ||||
| follow: "অনুসরণ" | ||||
| followRequest: "অনুসরণ করার অনুরোধ" | ||||
| followRequests: "অনুসরণ করার অনুরোধসমূহ" | ||||
| unfollow: "অনুসরণ বাতিল" | ||||
| followRequestPending: "অনুসরণ করার অনুরোধ বিচারাধীন" | ||||
| enterEmoji: "ইমোজি প্রবেশ করান" | ||||
| renote: "রিনোট" | ||||
| unrenote: "রিনোট সরান " | ||||
| renoted: "রিনোট করা হয়েছে" | ||||
| cantRenote: "এই নোটটি রিনোট করা যাবে না।" | ||||
| cantReRenote: "রিনোটকে রিনোট করা যাবে না।" | ||||
| quote: "উদ্ধৃতি" | ||||
| pinnedNote: "পিন করা নোট" | ||||
| pinned: "পিন করা" | ||||
| you: "আপনি" | ||||
| clickToShow: "দেখার জন্য ক্লিক করুন" | ||||
| sensitive: "সংবেদনশীল বিষয়বস্তু" | ||||
| add: "যুক্ত করুন" | ||||
| reaction: "প্রতিক্রিয়া" | ||||
| reactionSetting: "রিঅ্যাকশন পিকারে যেসকল প্রতিক্রিয়া দেখানো হবে" | ||||
| reactionSettingDescription2: "পুনরায় সাজাতে টেনে আনুন, মুছতে ক্লিক করুন, যোগ করতে + টিপুন।" | ||||
| rememberNoteVisibility: "নোটের দৃশ্যমান্যতার সেটিংস মনে রাখুন" | ||||
| attachCancel: "অ্যাটাচমেন্ট সরান " | ||||
| markAsSensitive: "সংবেদনশীল হিসাবে চিহ্নিত করুন" | ||||
| unmarkAsSensitive: "সংবেদনশীল চিহ্ন সরান" | ||||
| enterFileName: "ফাইলের নাম লিখুন" | ||||
| mute: "মিউট" | ||||
| unmute: "আনমিউট" | ||||
| block: "ব্লক" | ||||
| unblock: "ব্লক সরান" | ||||
| suspend: "স্থগিত করা" | ||||
| unsuspend: "অস্থগিত করা" | ||||
| blockConfirm: "ব্লক করতে চান?" | ||||
| unblockConfirm: "ব্লক সরাতে চান?" | ||||
| suspendConfirm: "স্থগিত করতে চান?" | ||||
| unsuspendConfirm: "অস্থগিত করতে চান?" | ||||
| selectList: "লিস্ট নির্বাচন করুন" | ||||
| selectAntenna: "অ্যান্টেনা নির্বাচন করুন" | ||||
| selectWidget: "উইজেট নির্বাচন করুন" | ||||
| editWidgets: "উইজেট সম্পাদনা করুন" | ||||
| editWidgetsExit: "সম্পাদনা শেষ করুন" | ||||
| customEmojis: "স্বনির্ধারিত ইমোজিগুলি" | ||||
| emoji: "ইমোজি" | ||||
| emojis: "ইমোজিগুলি" | ||||
| emojiName: "ইমোজির নাম" | ||||
| emojiUrl: "ইমোজির URL" | ||||
| addEmoji: "ইমোজি যুক্ত করুন" | ||||
| settingGuide: "সুপারিশকৃত সেটিংস" | ||||
| cacheRemoteFiles: "রিমোট ফাইলসমুহ ক্যাশ করুন" | ||||
| cacheRemoteFilesDescription: "যখন এই অপশনটি বন্ধ থাকে তখন রিমোট ফাইল সমূহ সরাসরি রিমোট ইন্সট্যান্স থেকে লোড করা হয়। এই অপশনটি বন্ধ করলে স্টোরেজ এর ব্যাবহার কমবে তবে থাম্বনেইল তৈরি না করার কারণে নেটওয়ার্ক ব্যান্ডউইথ বেশী লাগবে।  " | ||||
| flagAsBot: "বট হিসাবে চিহ্নিত করুন" | ||||
| flagAsBotDescription: "এই অ্যাকাউন্টটি যদি একটি প্রোগ্রাম দ্বারা পরিচালিত হয়, তাহলে এই অপশনটি চালু করুন। ইন্টারঅ্যাকশান চেইনিং রোধ করতে, মিস্কির সিস্টেম পরিচালনাকে বট-বান্ধব করতে এবং অন্যান্য ডেভেলপারদের সাহায্য করতে আপনার বট এ এই অপশনটি চালু করুন৷" | ||||
| flagAsCat: "বিড়াল হিসাবে চিহ্নিত করুন" | ||||
| flagAsCatDescription: "অ্যাকাউন্টটিকে বিড়াল হিসাবে চিহ্নিত করার জন্য অপশনটি চালু করুন।" | ||||
| autoAcceptFollowed: "আপনি যেসব অ্যাকাউন্ট অনুসরণ করেন, স্বয়ংক্রিয়ভাবে তাদের অনুসরণের অনুরধ স্বীকার করুন" | ||||
| addAccount: "অ্যাকাউন্ট যোগ করুন" | ||||
| loginFailed: "প্রবেশ করা যায়নি" | ||||
| showOnRemote: "রিমোট সার্ভারে দেখুন" | ||||
| general: "সাধারণ" | ||||
| wallpaper: "ওয়ালপেপার" | ||||
| setWallpaper: "ওয়ালপেপার সেট করুন" | ||||
| removeWallpaper: "ওয়ালপেপার সরান" | ||||
| searchWith: "খুঁজুন: {q}" | ||||
| youHaveNoLists: "আপনার কোন লিস্ট নেই" | ||||
| followConfirm: "{name} কে ফলোও করার ব্যাপারে নিশ্চিত?" | ||||
| proxyAccount: "প্রক্সি অ্যাকাউন্ট" | ||||
| proxyAccountDescription: "একটি প্রক্সি অ্যাকাউন্ট এমন একটি অ্যাকাউন্ট যা নির্দিষ্ট শর্তে ব্যবহারকারীদের জন্য রিমোট অনুসরণকারী হিসাবে কাজ করে। উদাহরণস্বরূপ, যখন একজন ব্যবহারকারী একটি রিমোট ব্যবহারকারীকে তালিকাভুক্ত করে, তখন ক্রিয়াকলাপের দৃষ্টান্তে বিতরণ করা হবে না যদি না কেউ তালিকাভুক্ত ব্যবহারকারীকে অনুসরণ করে, তাই প্রক্সি অ্যাকাউন্ট দ্বারা তাকে অনুসরণ করা হবে।" | ||||
| host: "হোস্ট" | ||||
| selectUser: "ব্যবহারকারী নির্বাচন করুন" | ||||
| recipient: "প্রতি" | ||||
| annotation: "মন্তব্য" | ||||
| federation: "ফেডিভার্স" | ||||
| instances: "ইন্সট্যান্স" | ||||
| registeredAt: "যোগ দিয়েছেন" | ||||
| latestRequestSentAt: "শেষ রিকুয়েস্ট পাঠানো হয়েছে" | ||||
| latestRequestReceivedAt: "শেষ রিকুয়েস্ট গৃহীত হয়েছে" | ||||
| latestStatus: "সর্বশেষ অবস্থা" | ||||
| storageUsage: "স্টোরেজের ব্যাবহার" | ||||
| charts: "চার্ট" | ||||
| perHour: "ঘন্টা প্রতি" | ||||
| perDay: "দৈনিক" | ||||
| stopActivityDelivery: "অ্যাক্টিভিটি পাঠানো বন্ধ করুন" | ||||
| blockThisInstance: "ইন্সট্যান্স ব্লক করুন" | ||||
| operations: "ক্রিয়াকলাপ" | ||||
| software: "সফটওয়্যার" | ||||
| version: "সংস্করণ" | ||||
| metadata: "মেটাডাটা" | ||||
| withNFiles: "{n} টি ফাইল" | ||||
| monitor: "মনিটর" | ||||
| jobQueue: "জব কিউ" | ||||
| cpuAndMemory: "সিপিউ এবং মেমরি" | ||||
| network: "নেটওয়ার্ক" | ||||
| disk: "ডিস্ক" | ||||
| instanceInfo: "ইন্সট্যান্সের তথ্য" | ||||
| statistics: "পরিসংখ্যান" | ||||
| clearQueue: "কিউ পরিষ্কার করুন" | ||||
| clearQueueConfirmTitle: "আপনি কি কিউ পরিষ্কার করার ব্যাপারে নিশ্চিত?" | ||||
| clearQueueConfirmText: "বিতরণ না করা নোট আর বিতরণ করা হবে না। সাধারণত আপনার এটি করার দরকার নেই।" | ||||
| clearCachedFiles: "ক্যাশ পরিষ্কার করুন" | ||||
| clearCachedFilesConfirm: "আপনি কি ক্যাশ পরিষ্কার করার ব্যাপারে নিশ্চিত?" | ||||
| blockedInstances: "ব্লককৃত ইন্সট্যান্সসমুহ" | ||||
| blockedInstancesDescription: "আপনি যে ইন্সট্যান্সগুলি ব্লক করতে চান তার হোস্টনেমগুলি প্রত্যেকটি আলাদা লাইনে লিখুন।  ব্লককৃত ইন্সট্যান্সগুলি এই ইন্সট্যান্সের সাথে যোগাযোগ করতে পারবেনা৷" | ||||
| muteAndBlock: "মিউট এবং ব্লকগুলি" | ||||
| mutedUsers: "নিঃশব্দকৃত ব্যবহারকারী" | ||||
| blockedUsers: "যাদের ব্লক করা হয়েছে" | ||||
| noUsers: "কোন ব্যাবহারকারী নেই" | ||||
| editProfile: "প্রোফাইল সম্পাদনা করুন" | ||||
| noteDeleteConfirm: "আপনি কি নোট ডিলিট করার ব্যাপারে নিশ্চিত?" | ||||
| pinLimitExceeded: "আপনি আর কোন নোট পিন করতে পারবেন না" | ||||
| intro: "Misskey এর ইন্সটলেশন সম্পন্ন হয়েছে!দয়া করে অ্যাডমিন ইউজার তৈরি করুন।" | ||||
| done: "সম্পন্ন" | ||||
| processing: "প্রক্রিয়াধীন..." | ||||
| preview: "পূর্বরূপ দেখুন" | ||||
| default: "পূর্বনির্ধারিত" | ||||
| noCustomEmojis: "কোন ইমোজি নাই" | ||||
| noJobs: "কোন জব নাই" | ||||
| federating: "ফেডারেট করা হচ্ছে" | ||||
| blocked: "ব্লক করা হয়েছে" | ||||
| suspended: "স্থগিত করা হয়েছে" | ||||
| all: "সবগুলো" | ||||
| subscribing: "সদস্যতা নেয়া হচ্ছে" | ||||
| publishing: "প্রকাশ করা হচ্ছে" | ||||
| notResponding: "সাড়া নেই" | ||||
| instanceFollowing: "ইন্সট্যান্স অনুসরণ করা হচ্ছে" | ||||
| instanceFollowers: "ইন্সট্যান্স অনুসরণকারী" | ||||
| instanceUsers: "ইন্সট্যান্স ব্যাবহারকারী" | ||||
| changePassword: "পাসওয়ার্ড পরিবর্তন করুন" | ||||
| security: "নিরাপত্তা" | ||||
| retypedNotMatch: "ইনপুট মেলে না।" | ||||
| currentPassword: "বর্তমান পাসওয়ার্ড" | ||||
| newPassword: "নতুন পাসওয়ার্ড" | ||||
| newPasswordRetype: "নতুন পাসওয়ার্ড (পুনরায় লিখুন)" | ||||
| attachFile: "ফাইল সংযুক্ত করুন" | ||||
| more: "আরও!" | ||||
| featured: "হাইলাইট" | ||||
| usernameOrUserId: "ব্যাবহারকারীর নাম বা ব্যাবহারকারী ID" | ||||
| noSuchUser: "কোন ব্যবহারকারী খুঁজে পাওয়া যায়নি" | ||||
| lookup: "খুঁজে দেখো" | ||||
| announcements: "ঘোষণা" | ||||
| imageUrl: "চিত্রের URL" | ||||
| remove: "মুছুন" | ||||
| removed: "সরানো হয়েছে" | ||||
| removeAreYouSure: "আপনি কি \"{x}\" সরানোর ব্যাপারে নিশ্চিত?" | ||||
| deleteAreYouSure: "আপনি কি \"{x}\" সরানোর ব্যাপারে নিশ্চিত?" | ||||
| resetAreYouSure: "রিসেট করার ব্যাপারে নিশ্চিত?" | ||||
| saved: "সংরক্ষিত হয়েছে" | ||||
| messaging: "চ্যাট" | ||||
| upload: "আপলোড" | ||||
| keepOriginalUploading: "আসল ছবি রাখুন" | ||||
| keepOriginalUploadingDescription: "ছবিটি আপলোড করার সময় আসল সংস্করণটি রাখুন। অপশনটি বন্ধ থাকলে, আপলোডের সময় ওয়েব প্রকাশনার জন্য ছবি ব্রাউজারে তৈরি করা হবে।" | ||||
| fromDrive: "ড্রাইভ হতে" | ||||
| fromUrl: "URL হতে" | ||||
| uploadFromUrl: "URL হতে আপলোড" | ||||
| uploadFromUrlDescription: "যে ফাইলটি আপলোড করতে চান, সেটির URL" | ||||
| uploadFromUrlRequested: "আপলোড অনুরোধ করা হয়েছে" | ||||
| uploadFromUrlMayTakeTime: "URL হতে আপলোড হতে কিছু সময় লাগতে পারে।" | ||||
| explore: "ঘুরে দেখুন" | ||||
| messageRead: "পড়া" | ||||
| noMoreHistory: "আর কোন ইতিহাস নেই" | ||||
| startMessaging: "চ্যাট শুরু করুন" | ||||
| nUsersRead: "{n} জন পড়েছেন" | ||||
| agreeTo: "{0} এর প্রতি আমি সম্মত" | ||||
| tos: "পরিষেবার শর্তাদি" | ||||
| start: "শুরু করুন" | ||||
| home: "মূল পাতা" | ||||
| remoteUserCaution: "এই ব্যাবহারকারী রিমোট ইন্সট্যান্সের, নিম্নক্ত তথ্য অসম্পূর্ণ হতে পারে।" | ||||
| activity: "কার্যকলাপ" | ||||
| images: "ছবি" | ||||
| birthday: "জন্মদিন" | ||||
| yearsOld: "{age} বছর" | ||||
| registeredDate: "যোগদানের তারিখ" | ||||
| location: "অবস্থান" | ||||
| theme: "থিম" | ||||
| themeForLightMode: "লাইট মোডের থিম" | ||||
| themeForDarkMode: "ডার্ক মোডের থিম" | ||||
| light: "আলোকিত" | ||||
| dark: "অন্ধকার" | ||||
| lightThemes: "আলোকিত থিম" | ||||
| darkThemes: "অন্ধকার থিম" | ||||
| syncDeviceDarkMode: "ডিভাইসের সেটিং অনুযায়ী ডার্ক মোড সেট করুন" | ||||
| drive: "ড্রাইভ" | ||||
| fileName: "ফাইলের নাম" | ||||
| selectFile: "ফাইল নির্বাচন করুন" | ||||
| selectFiles: "ফাইল নির্বাচন করুন" | ||||
| selectFolder: "ফোল্ডার নির্বাচন করুন" | ||||
| selectFolders: "ফোল্ডার নির্বাচন করুন" | ||||
| renameFile: "ফাইল পুনঃনামকরন" | ||||
| folderName: "ফোল্ডারের নাম" | ||||
| createFolder: "ফোল্ডার তৈরি করুন" | ||||
| renameFolder: "ফোল্ডার পুনঃনামকরন" | ||||
| deleteFolder: "ফোল্ডার মুছুন" | ||||
| addFile: "ফাইল যোগ করুন" | ||||
| emptyDrive: "আপনার ড্রাইভ খালি" | ||||
| emptyFolder: "এই ফোল্ডার খালি" | ||||
| unableToDelete: "মুছে ফেলা যায়নি" | ||||
| inputNewFileName: "ফাইলের নতুন নাম লিখুন" | ||||
| inputNewDescription: "নতুন ক্যাপশন লিখুন" | ||||
| inputNewFolderName: "ফোল্ডারের নতুন নাম লিখুন" | ||||
| circularReferenceFolder: "গন্তব্য ফোল্ডারটি আপনি যে ফোল্ডারটি সরাতে চান তার একটি সাবফোল্ডার।" | ||||
| hasChildFilesOrFolders: "এই ফোল্ডারটি খালি না হওয়ায় ডিলিট করা যায়নি।" | ||||
| copyUrl: "URL কপি করুন" | ||||
| rename: "পুনঃনামকরণ" | ||||
| avatar: "প্রোফাইল ছবি" | ||||
| banner: "ব্যানার" | ||||
| nsfw: "সংবেদনশীল বিষয়বস্তু" | ||||
| whenServerDisconnected: "সার্ভারের সাথে সংযোগ বিচ্ছিন্ন হয়ে গেলে" | ||||
| disconnectedFromServer: "সার্ভার থেকে সংযোগ বিচ্ছিন্ন হয়েছে" | ||||
| reload: "আবার লোড করুন" | ||||
| doNothing: "কিছু করবেন না" | ||||
| reloadConfirm: "আপনি কি রিলোড করতে চান?" | ||||
| watch: "দেখুন" | ||||
| unwatch: "দেখা বন্ধ করুন " | ||||
| accept: "অনুমোদন" | ||||
| reject: "প্রত্যাখ্যান" | ||||
| normal: "স্বাভাবিক" | ||||
| instanceName: "ইন্সট্যান্সের নাম" | ||||
| instanceDescription: "ইন্সট্যান্সের বর্ণনা" | ||||
| maintainerName: "মেইনটেইনার" | ||||
| maintainerEmail: "মেইনটেইনারের ইমেইল" | ||||
| tosUrl: "ব্যবহারের শর্তাবলীর URL" | ||||
| thisYear: "বছর" | ||||
| thisMonth: "মাস" | ||||
| today: "আজ" | ||||
| dayX: "{day}" | ||||
| monthX: "{month}" | ||||
| yearX: "{year}" | ||||
| pages: "পৃষ্ঠা" | ||||
| integration: "ইন্টিগ্রেশন" | ||||
| connectService: "সংযুক্ত করুন" | ||||
| disconnectService: "সংযোগ বিচ্ছিন্ন করুন" | ||||
| enableLocalTimeline: "স্থানীয় টাইমলাইন চালু করুন" | ||||
| enableGlobalTimeline: "গ্লোবাল টাইমলাইন চালু করুন" | ||||
| disablingTimelinesInfo: "আপনি এই টাইমলাইনগুলি বন্ধ করলেও প্রশাসক এবং মডারেটররা এই টাইমলাইনগুলি ব্যাবহার করতে পারবে" | ||||
| registration: "নিবন্ধন" | ||||
| enableRegistration: "নতুন ব্যাবহারকারী নিবন্ধন চালু করুন" | ||||
| invite: "আমন্ত্রণ" | ||||
| proxyRemoteFiles: "রিমোট ফাইলসমুহ প্রক্সি করুন" | ||||
| proxyRemoteFilesDescription: "যখন এই সেটিংটি চালু থাকে, তখন অসংরক্ষিত বা অতিরিক্ত ক্ষমতার কারণে দূরবর্তী ফাইলগুলিকে স্থানীয়ভাবে প্রক্সি করা হবে এবং থাম্বনেলগুলিও তৈরি করা হবে৷ সার্ভার স্টোরেজ ব্যাবহার করে না," | ||||
| driveCapacityPerLocalAccount: "প্রত্যেক স্থানীয় ব্যাবহারকারীর জন্য ড্রাইভের জায়গা" | ||||
| driveCapacityPerRemoteAccount: "প্রত্যেক রিমোট ব্যাবহারকারীর জন্য ড্রাইভের জায়গা" | ||||
| inMb: "মেগাবাইটে লিখুন" | ||||
| iconUrl: "আইকনের URL (ফ্যাভিকন, ইত্যাদি)" | ||||
| bannerUrl: "ব্যানার ছবির URL" | ||||
| backgroundImageUrl: "পটভূমির চিত্রের URL" | ||||
| basicInfo: "আপনার ব্যক্তিগত তথ্য" | ||||
| pinnedUsers: "পিন করা ব্যাবহারকারীগণ" | ||||
| pinnedUsersDescription: "আপনি যেসব ব্যবহারকারীদের \"ঘুরে দেখুন\" পৃষ্ঠায় পিন করতে চান তাদের বর্ণনা করুন, প্রত্যেকের বর্ণনা আলাদা লাইনে লিখুন" | ||||
| pinnedPages: "পিন করা পৃষ্ঠাসুমহ" | ||||
| pinnedPagesDescription: "আপনি যেসকল পৃষ্ঠাসমূহকে \"ঘুরে দেখুন\" পৃষ্ঠায় পিন করতে চান তাদের বর্ণনা করুন, প্রত্যেকের বর্ণনা আলাদা লাইনে লিখুন" | ||||
| pinnedClipId: "পিনকৃত ক্লিপের ID" | ||||
| pinnedNotes: "পিন করা নোট" | ||||
| hcaptcha: "hCaptcha" | ||||
| enableHcaptcha: "hCaptcha চালু করুন" | ||||
| hcaptchaSiteKey: "সাইট কী" | ||||
| hcaptchaSecretKey: "সিক্রেট কী" | ||||
| recaptcha: "reCAPTCHA" | ||||
| enableRecaptcha: "reCAPTCHA চালু করুন" | ||||
| recaptchaSiteKey: "সাইট কী" | ||||
| antennas: "অ্যান্টেনা" | ||||
| manageAntennas: "অ্যান্টেনা ব্যবস্থাপনা" | ||||
| name: "নাম" | ||||
| antennaSource: "অ্যান্টেনার উৎস" | ||||
| antennaKeywords: "যেসব কীওয়ার্ড দেখা হবে" | ||||
| antennaExcludeKeywords: "যেসব কীওয়ার্ড দেখা হবে না" | ||||
| antennaKeywordsDescription: "স্পেস দিয়ে আলাদা করলে AND শর্ত তৈরি হবে এবং আলাদা লাইনে লিখলে OR শর্ত তৈরি হবে।" | ||||
| notifyAntenna: "নতুন নোট সম্পর্কে অবহিত করুন" | ||||
| withFileAntenna: "শুধুমাত্র ফাইলযুক্ত নোট" | ||||
| enableServiceworker: "ServiceWorker চালু করুন" | ||||
| antennaUsersDescription: "প্রত্যেক লাইনে একজন ব্যবহারকারীর নাম লিখুন" | ||||
| caseSensitive: "ছোট হাতের এবং বড় হাতের অক্ষর নির্দিষ্ট করুন" | ||||
| withReplies: "জবাবসমুহ যুক্ত করুন" | ||||
| connectedTo: "আপনি নিম্নলিখিত অ্যাকাউন্টের সাথে সংযুক্ত" | ||||
| notesAndReplies: "নোটসমূহ এবং জবাবগুলি" | ||||
| withFiles: "ফাইলগুলি যুক্ত করুন" | ||||
| silence: "নীরব" | ||||
| silenceConfirm: "আপনি কি এই ব্যাবহারকারীকের নীরব করতে চান?" | ||||
| unsilence: "সরব" | ||||
| unsilenceConfirm: "আপনি কি এই ব্যাবহারকারীকের সরব করতে চান?" | ||||
| popularUsers: "জনপ্রিয় ব্যবহারকারীগন" | ||||
| recentlyUpdatedUsers: "সম্প্রতি পোস্ট করা ব্যবহারকারীগন" | ||||
| recentlyRegisteredUsers: "নতুন যোগ দেওয়া ব্যবহারকারীগন" | ||||
| recentlyDiscoveredUsers: "নতুন খুঁজে পাওয়া ব্যবহারকারীগন" | ||||
| exploreUsersCount: "{count} জন ব্যাবহারকারী" | ||||
| exploreFediverse: "Fediverse ঘুরে দেখুন" | ||||
| popularTags: "জনপ্রিয় ট্যাগগুলি" | ||||
| userList: "লিস্ট" | ||||
| about: "আপনার সম্পর্কে" | ||||
| aboutMisskey: "Misskey সম্পর্কে" | ||||
| administrator: "প্রশাসক" | ||||
| token: "টোকেন" | ||||
| twoStepAuthentication: "২-ধাপ প্রমাণীকরণ" | ||||
| moderator: "মডারেটর" | ||||
| nUsersMentioned: "{n} জনকে উল্লেখ করা হয়েছে" | ||||
| securityKey: "সিকিউরিটি কী" | ||||
| securityKeyName: "কী'র নাম" | ||||
| registerSecurityKey: "সিকিউরিটি কী নিবন্ধন করুন" | ||||
| lastUsed: "শেষ ব্যাবহার করা হয়েছে" | ||||
| unregister: "নিবন্ধনমুক্ত হন" | ||||
| passwordLessLogin: "পাসওয়ার্ড-বিহীন লগইন সেট আপ করুন" | ||||
| resetPassword: "পাসওয়ার্ড রিসেট করুন" | ||||
| newPasswordIs: "নতুন পাসওয়ার্ড হচ্ছে \"{password}\"" | ||||
| reduceUiAnimation: "UI অ্যানিমেশন কমান" | ||||
| share: "শেয়ার" | ||||
| notFound: "পাওয়া যায়নি" | ||||
| notFoundDescription: "এই URL-এর সাথে সম্পর্কিত কোনো পৃষ্ঠা নেই।" | ||||
| uploadFolder: "আপলোডের জন্য ডিফল্ট ফোল্ডার" | ||||
| cacheClear: "ক্যাশ পরিষ্কার করুন" | ||||
| markAsReadAllNotifications: "সমস্ত বিজ্ঞপ্তিগুলি পঠিত হিসাবে চিহ্নিত করুন" | ||||
| markAsReadAllUnreadNotes: "সমস্ত নোটগুলি পঠিত হিসাবে চিহ্নিত করুন" | ||||
| invites: "আমন্ত্রণ" | ||||
| invitations: "আমন্ত্রণ" | ||||
| useOsNativeEmojis: "অপারেটিং সিস্টেমের নেটিভ ইমোজি ব্যবহার করুন" | ||||
| disableDrawer: "ড্রয়ার মেনু প্রদর্শন করবেন না" | ||||
| youHaveNoGroups: "আপনার কোন গ্রুপ নেই " | ||||
| joinOrCreateGroup: "একটি বিদ্যমান গ্রুপের আমন্ত্রণ পান বা একটি নতুন গ্রুপ তৈরি করুন৷" | ||||
| noHistory: "কোনো ইতিহাস নেই" | ||||
| signinHistory: "প্রবেশ করার ইতিহাস" | ||||
| disableAnimatedMfm: "অ্যানিমেটেড MFM অক্ষম করুন" | ||||
| doing: "প্রক্রিয়া করছে..." | ||||
| category: "বিভাগ" | ||||
| tags: "ট্যাগসমূহ" | ||||
| docSource: "ডকুমেন্টের উৎস" | ||||
| createAccount: "অ্যাকাউন্ট তৈরি করুন" | ||||
| existingAccount: "বিদ্যমান অ্যাকাউন্ট" | ||||
| regenerate: "আবারও তৈরি করুন" | ||||
| fontSize: "ফন্টের আকার" | ||||
| noFollowRequests: "আপনার কোন ফলোও রিকুয়েস্ট নেই" | ||||
| openImageInNewTab: "ছবি নতুন ট্যাবে খুলুন" | ||||
| dashboard: "ড্যাশবোর্ড" | ||||
| local: "স্থানীয়" | ||||
| remote: "রিমোট" | ||||
| total: "মোট" | ||||
| weekOverWeekChanges: "গত সপ্তাহে" | ||||
| dayOverDayChanges: "গতকাল" | ||||
| appearance: "অবয়ব" | ||||
| clientSettings: "ক্লায়েন্ট সেটিংস" | ||||
| accountSettings: "অ্যাকাউন্ট সেটিংস" | ||||
| promotion: "প্রমোশন" | ||||
| promote: "প্রচার করুন" | ||||
| numberOfDays: "দিনের সংখ্যা" | ||||
| hideThisNote: "নোটটি লুকান" | ||||
| smtpHost: "হোস্ট" | ||||
| smtpUser: "ব্যবহারকারীর নাম" | ||||
| smtpPass: "পাসওয়ার্ড" | ||||
| clearCache: "ক্যাশ পরিষ্কার করুন" | ||||
| info: "আপনার সম্পর্কে" | ||||
| user: "ব্যবহারকারীগণ" | ||||
| controlPanel: "নিয়ন্ত্রন কেন্দ্র" | ||||
| _email: | ||||
|   _follow: | ||||
|     title: "আপনাকে অনুসরণ করছে" | ||||
| _mfm: | ||||
|   mention: "উল্লেখ" | ||||
|   quote: "উদ্ধৃতি" | ||||
|   emoji: "স্বনির্ধারিত ইমোজিগুলি" | ||||
|   search: "খুঁজুন" | ||||
| _theme: | ||||
|   keys: | ||||
|     mention: "উল্লেখ" | ||||
|     renote: "রিনোট" | ||||
| _sfx: | ||||
|   note: "নোটগুলি" | ||||
|   notification: "বিজ্ঞপ্তি" | ||||
|   chat: "চ্যাট" | ||||
| _widgets: | ||||
|   notifications: "বিজ্ঞপ্তি" | ||||
|   timeline: "টাইমলাইন" | ||||
|   activity: "কার্যকলাপ" | ||||
|   federation: "ফেডিভার্স" | ||||
|   jobQueue: "জব কিউ" | ||||
| _cw: | ||||
|   show: "আরও দেখুন" | ||||
| _visibility: | ||||
|   home: "মূল পাতা" | ||||
|   followers: "অনুসরণকারী" | ||||
| _profile: | ||||
|   name: "নাম" | ||||
|   username: "ব্যবহারকারীর নাম" | ||||
| _exportOrImport: | ||||
|   followingList: "অনুসরণ করা হচ্ছে" | ||||
|   muteList: "মিউট" | ||||
|   blockingList: "ব্লক" | ||||
|   userLists: "লিস্ট" | ||||
| _timelines: | ||||
|   home: "মূল পাতা" | ||||
| _pages: | ||||
|   blocks: | ||||
|     image: "ছবি" | ||||
|   script: | ||||
|     categories: | ||||
|       list: "লিস্ট" | ||||
|     blocks: | ||||
|       _join: | ||||
|         arg1: "লিস্ট" | ||||
|       _randomPick: | ||||
|         arg1: "লিস্ট" | ||||
|       _dailyRandomPick: | ||||
|         arg1: "লিস্ট" | ||||
|       _seedRandomPick: | ||||
|         arg2: "লিস্ট" | ||||
|       _pick: | ||||
|         arg1: "লিস্ট" | ||||
|       _listLen: | ||||
|         arg1: "লিস্ট" | ||||
|     types: | ||||
|       array: "লিস্ট" | ||||
| _notification: | ||||
|   youWereFollowed: "আপনাকে অনুসরণ করছে" | ||||
|   _types: | ||||
|     follow: "অনুসরণ করা হচ্ছে" | ||||
|     mention: "উল্লেখ" | ||||
|     renote: "রিনোট" | ||||
|     quote: "উদ্ধৃতি" | ||||
|     reaction: "প্রতিক্রিয়া" | ||||
| _deck: | ||||
|   _columns: | ||||
|     notifications: "বিজ্ঞপ্তি" | ||||
|     tl: "টাইমলাইন" | ||||
|     antenna: "অ্যান্টেনা" | ||||
|     list: "লিস্ট" | ||||
|     mentions: "উল্লেখসমূহ" | ||||
|  |  | |||
|  | @ -235,6 +235,8 @@ resetAreYouSure: "Wirklich zurücksetzen?" | |||
| saved: "Gespeichert" | ||||
| messaging: "Chat" | ||||
| upload: "Hochladen" | ||||
| keepOriginalUploading: "Originalbild speichern" | ||||
| keepOriginalUploadingDescription: "Speichert das Originalbild so, wie es ist. Ist dies deaktiviert, wird eine Version zum Anzeigen im Internet generiert." | ||||
| fromDrive: "Aus Drive" | ||||
| fromUrl: "Von einer URL" | ||||
| uploadFromUrl: "Von einer URL hochladen" | ||||
|  |  | |||
|  | @ -235,6 +235,8 @@ resetAreYouSure: "Really reset?" | |||
| saved: "Saved" | ||||
| messaging: "Chat" | ||||
| upload: "Upload" | ||||
| keepOriginalUploading: "Keep original image" | ||||
| keepOriginalUploadingDescription: "Saves the originally uploaded image as-is. If turned off, a version to display on the web will be generated on upload." | ||||
| fromDrive: "From Drive" | ||||
| fromUrl: "From URL" | ||||
| uploadFromUrl: "Upload from a URL" | ||||
|  |  | |||
|  | @ -235,6 +235,7 @@ resetAreYouSure: "Voulez-vous réinitialiser ?" | |||
| saved: "Enregistré" | ||||
| messaging: "Discuter" | ||||
| upload: "Téléverser" | ||||
| keepOriginalUploading: "Garder l’image d’origine" | ||||
| fromDrive: "Depuis le Drive" | ||||
| fromUrl: "Depuis une URL" | ||||
| uploadFromUrl: "Téléverser via une URL" | ||||
|  | @ -743,6 +744,7 @@ notRecommended: "Déconseillé" | |||
| botProtection: "Protection contre les bots" | ||||
| instanceBlocking: "Instances bloquées" | ||||
| selectAccount: "Sélectionner un compte" | ||||
| switchAccount: "Changer de compte" | ||||
| enabled: "Activé" | ||||
| disabled: "Désactivé" | ||||
| quickAction: "Actions rapides" | ||||
|  | @ -803,6 +805,7 @@ makeReactionsPublic: "Rendre les réactions publiques" | |||
| makeReactionsPublicDescription: "Ceci rendra la liste de toutes vos réactions données publique." | ||||
| classic: "Classique" | ||||
| muteThread: "Mettre ce thread en sourdine" | ||||
| unmuteThread: "Ne plus masquer le fil" | ||||
| ffVisibility: "Visibilité des abonnés/abonnements" | ||||
| ffVisibilityDescription: "Permet de configurer qui peut voir les personnes que tu suis et les personnes qui te suivent." | ||||
| continueThread: "Afficher la suite du fil" | ||||
|  | @ -1241,6 +1244,7 @@ _exportOrImport: | |||
|   muteList: "Comptes masqués" | ||||
|   blockingList: "Comptes bloqués" | ||||
|   userLists: "Listes" | ||||
|   excludeMutingUsers: "Exclure les utilisateur·rice·s mis en sourdine" | ||||
|   excludeInactiveUsers: "Exclure les utilisateur·rice·s inactifs" | ||||
| _charts: | ||||
|   federationInstancesIncDec: "Variation du nombre d'instances fédérées" | ||||
|  |  | |||
|  | @ -235,6 +235,8 @@ resetAreYouSure: "リセットしますか?" | |||
| saved: "保存しました" | ||||
| messaging: "チャット" | ||||
| upload: "アップロード" | ||||
| keepOriginalUploading: "オリジナル画像を保持" | ||||
| keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。" | ||||
| fromDrive: "ドライブから" | ||||
| fromUrl: "URLから" | ||||
| uploadFromUrl: "URLアップロード" | ||||
|  |  | |||
|  | @ -106,6 +106,7 @@ clickToShow: "클릭하여 보기" | |||
| sensitive: "열람주의" | ||||
| add: "추가" | ||||
| reaction: "리액션" | ||||
| reactionSetting: "선택기에 표시할 리액션" | ||||
| reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다." | ||||
| rememberNoteVisibility: "공개 범위를 기억하기" | ||||
| attachCancel: "첨부 취소" | ||||
|  | @ -234,6 +235,8 @@ resetAreYouSure: "초기화 하시겠습니까?" | |||
| saved: "저장하였습니다" | ||||
| messaging: "대화" | ||||
| upload: "업로드" | ||||
| keepOriginalUploading: "원본 이미지를 유지" | ||||
| keepOriginalUploadingDescription: "이미지를 업로드할 때에 원본을 그대로 유지합니다. 비활성화하면 업로드할 때 브라우저에서 웹 공개용 이미지를 생성합니다." | ||||
| fromDrive: "드라이브에서" | ||||
| fromUrl: "URL로부터" | ||||
| uploadFromUrl: "URL 업로드" | ||||
|  | @ -446,6 +449,7 @@ uiLanguage: "UI 표시 언어" | |||
| groupInvited: "그룹에 초대되었습니다" | ||||
| aboutX: "{x}에 대하여" | ||||
| useOsNativeEmojis: "OS 기본 이모지를 사용" | ||||
| disableDrawer: "드로어 메뉴를 사용하지 않기" | ||||
| youHaveNoGroups: "그룹이 없습니다" | ||||
| joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들어 보세요." | ||||
| noHistory: "기록이 없습니다" | ||||
|  | @ -617,8 +621,11 @@ reportAbuse: "신고" | |||
| reportAbuseOf: "{name}을 신고하기" | ||||
| fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요. 특정 게시물을 신고할 때에는 게시물의 URL도 포함해 주세요." | ||||
| abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다." | ||||
| reporter: "신고자" | ||||
| reporteeOrigin: "피신고자" | ||||
| reporterOrigin: "신고자" | ||||
| forwardReport: "리모트 인스턴스에도 신고 내용 보내기" | ||||
| forwardReportIsAnonymous: "리모트 인스턴스에서는 나의 정보를 볼 수 없으며, 익명의 시스템 계정으로 표시됩니다." | ||||
| send: "전송" | ||||
| abuseMarkAsResolved: "해결됨으로 표시" | ||||
| openInNewTab: "새 탭에서 열기" | ||||
|  | @ -680,6 +687,7 @@ center: "가운데" | |||
| wide: "넓게" | ||||
| narrow: "좁게" | ||||
| reloadToApplySetting: "이 설정을 적용하려면 페이지를 새로고침해야 합니다. 바로 새로고침하시겠습니까?" | ||||
| needReloadToApply: "변경 사항은 새로고침하면 적용됩니다." | ||||
| showTitlebar: "타이틀 바를 표시하기" | ||||
| clearCache: "캐시 비우기" | ||||
| onlineUsersCount: "{n}명이 접속 중" | ||||
|  | @ -740,6 +748,7 @@ notRecommended: "추천하지 않음" | |||
| botProtection: "Bot 방어" | ||||
| instanceBlocking: "인스턴스 차단" | ||||
| selectAccount: "계정 선택" | ||||
| switchAccount: "계정 바꾸기" | ||||
| enabled: "활성화" | ||||
| disabled: "비활성화" | ||||
| quickAction: "빠른 동작" | ||||
|  | @ -808,6 +817,11 @@ deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다.  | |||
| incorrectPassword: "비밀번호가 올바르지 않습니다." | ||||
| voteConfirm: "\"{choice}\"에 투표하시겠습니까?" | ||||
| hide: "숨기기" | ||||
| leaveGroup: "그룹 나가기" | ||||
| leaveGroupConfirm: "\"{name}\"에서 나갈까요?" | ||||
| useDrawerReactionPickerForMobile: "모바일에서 드로어 메뉴로 표시" | ||||
| welcomeBackWithName: "환영합니다, {name}님" | ||||
| clickToFinishEmailVerification: "[{ok}]를 눌러 이메일 인증을 완료하세요." | ||||
| _emailUnavailable: | ||||
|   used: "이 메일 주소는 사용중입니다" | ||||
|   format: "형식이 올바르지 않습니다" | ||||
|  |  | |||
|  | @ -235,6 +235,8 @@ resetAreYouSure: "恢复默认设置?" | |||
| saved: "已保存" | ||||
| messaging: "聊天" | ||||
| upload: "本地上传" | ||||
| keepOriginalUploading: "保留原图" | ||||
| keepOriginalUploadingDescription: "上传图片时保留原始图片。关闭时,浏览器会在上传时生成一张用于web发布的图片。" | ||||
| fromDrive: "从网盘中" | ||||
| fromUrl: "从 URL" | ||||
| uploadFromUrl: "从网址上传" | ||||
|  | @ -619,8 +621,11 @@ reportAbuse: "举报" | |||
| reportAbuseOf: "举报{name}" | ||||
| fillAbuseReportDescription: "请填写举报的详细原因。如果有对方发的帖子,请同时填写URL地址。" | ||||
| abuseReported: "内容已发送。感谢您的报告。" | ||||
| reporter: "报告者" | ||||
| reporteeOrigin: "举报来源" | ||||
| reporterOrigin: "举报者来源" | ||||
| forwardReport: "将报告转发给远程实例" | ||||
| forwardReportIsAnonymous: "在远程实例上显示的报告者是匿名的系统账号,而不是您的账号。" | ||||
| send: "发送" | ||||
| abuseMarkAsResolved: "处理完毕" | ||||
| openInNewTab: "在新标签页中打开" | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| { | ||||
| 	"name": "misskey", | ||||
| 	"version": "12.102.1", | ||||
| 	"version": "12.103.0", | ||||
| 	"codename": "indigo", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
|  |  | |||
|  | @ -122,7 +122,7 @@ | |||
| 		"langmap": "0.0.16", | ||||
| 		"mfm-js": "0.21.0", | ||||
| 		"mime-types": "2.1.34", | ||||
| 		"misskey-js": "0.0.13", | ||||
| 		"misskey-js": "0.0.14", | ||||
| 		"mocha": "8.4.0", | ||||
| 		"ms": "3.0.0-canary.1", | ||||
| 		"multer": "1.4.4", | ||||
|  |  | |||
|  | @ -32,7 +32,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => { | |||
| 	// Authentication
 | ||||
| 	authenticate(body['i']).then(([user, app]) => { | ||||
| 		// API invoking
 | ||||
| 		call(endpoint.name, user, app, body, (ctx as any).file).then((res: any) => { | ||||
| 		call(endpoint.name, user, app, body, ctx).then((res: any) => { | ||||
| 			reply(res); | ||||
| 		}).catch((e: ApiError) => { | ||||
| 			reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import * as Koa from 'koa'; | ||||
| import { performance } from 'perf_hooks'; | ||||
| import { limiter } from './limiter'; | ||||
| import { User } from '@/models/entities/user'; | ||||
|  | @ -12,7 +13,7 @@ const accessDenied = { | |||
| 	id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e', | ||||
| }; | ||||
| 
 | ||||
| export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => { | ||||
| export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { | ||||
| 	const isSecure = user != null && token == null; | ||||
| 
 | ||||
| 	const ep = endpoints.find(e => e.name === endpoint); | ||||
|  | @ -76,9 +77,20 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac | |||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	// Cast non JSON input
 | ||||
| 	if (ep.meta.requireFile && ep.meta.params) { | ||||
| 		const body = (ctx!.request as any).body; | ||||
| 		for (const k of Object.keys(ep.meta.params)) { | ||||
| 			const param = ep.meta.params[k]; | ||||
| 			if (['Boolean', 'Number'].includes(param.validator.name) && typeof body[k] === 'string') { | ||||
| 				body[k] = JSON.parse(body[k]); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// API invoking
 | ||||
| 	const before = performance.now(); | ||||
| 	return await ep.exec(data, user, token, file).catch((e: Error) => { | ||||
| 	return await ep.exec(data, user, token, ctx!.file).catch((e: Error) => { | ||||
| 		if (e instanceof ApiError) { | ||||
| 			throw e; | ||||
| 		} else { | ||||
|  |  | |||
|  | @ -39,15 +39,13 @@ export const meta = { | |||
| 		}, | ||||
| 
 | ||||
| 		isSensitive: { | ||||
| 			validator: $.optional.either($.bool, $.str), | ||||
| 			validator: $.optional.bool, | ||||
| 			default: false, | ||||
| 			transform: (v: any): boolean => v === true || v === 'true', | ||||
| 		}, | ||||
| 
 | ||||
| 		force: { | ||||
| 			validator: $.optional.either($.bool, $.str), | ||||
| 			validator: $.optional.bool, | ||||
| 			default: false, | ||||
| 			transform: (v: any): boolean => v === true || v === 'true', | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ const _dirname = dirname(_filename); | |||
| const app = new Koa(); | ||||
| app.use(cors()); | ||||
| app.use(async (ctx, next) => { | ||||
| 	ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`); | ||||
| 	ctx.set('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`); | ||||
| 	await next(); | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ import { proxyMedia } from './proxy-media'; | |||
| const app = new Koa(); | ||||
| app.use(cors()); | ||||
| app.use(async (ctx, next) => { | ||||
| 	ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`); | ||||
| 	ctx.set('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`); | ||||
| 	await next(); | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -4967,10 +4967,10 @@ minizlib@^2.0.0, minizlib@^2.1.1: | |||
|     minipass "^3.0.0" | ||||
|     yallist "^4.0.0" | ||||
| 
 | ||||
| misskey-js@0.0.13: | ||||
|   version "0.0.13" | ||||
|   resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970" | ||||
|   integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ== | ||||
| misskey-js@0.0.14: | ||||
|   version "0.0.14" | ||||
|   resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d" | ||||
|   integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww== | ||||
|   dependencies: | ||||
|     autobind-decorator "^2.4.0" | ||||
|     eventemitter3 "^4.0.7" | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ module.exports = { | |||
| 		// data の禁止理由: 抽象的すぎるため
 | ||||
| 		// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
 | ||||
| 		"id-denylist": ["error", "window", "data", "e"], | ||||
| 		'eqeqeq': ['error', 'always', { 'null': 'ignore' }], | ||||
| 		"vue/attributes-order": ["error", { | ||||
| 			"alphabetical": false | ||||
| 		}], | ||||
|  |  | |||
|  | @ -69,7 +69,7 @@ | |||
| 		"langmap": "0.0.16", | ||||
| 		"matter-js": "0.18.0", | ||||
| 		"mfm-js": "0.21.0", | ||||
| 		"misskey-js": "0.0.13", | ||||
| 		"misskey-js": "0.0.14", | ||||
| 		"mocha": "8.4.0", | ||||
| 		"ms": "2.1.3", | ||||
| 		"nested-property": "4.0.0", | ||||
|  |  | |||
|  | @ -192,31 +192,31 @@ export async function openAccountMenu(opts: { | |||
| 	if (opts.withExtraOperation) { | ||||
| 		popupMenu([...[{ | ||||
| 			type: 'link', | ||||
| 			text: i18n.locale.profile, | ||||
| 			text: i18n.ts.profile, | ||||
| 			to: `/@${ $i.username }`, | ||||
| 			avatar: $i, | ||||
| 		}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { | ||||
| 			icon: 'fas fa-plus', | ||||
| 			text: i18n.locale.addAccount, | ||||
| 			text: i18n.ts.addAccount, | ||||
| 			action: () => { | ||||
| 				popupMenu([{ | ||||
| 					text: i18n.locale.existingAccount, | ||||
| 					text: i18n.ts.existingAccount, | ||||
| 					action: () => { showSigninDialog(); }, | ||||
| 				}, { | ||||
| 					text: i18n.locale.createAccount, | ||||
| 					text: i18n.ts.createAccount, | ||||
| 					action: () => { createAccount(); }, | ||||
| 				}], ev.currentTarget || ev.target); | ||||
| 				}], ev.currentTarget ?? ev.target); | ||||
| 			}, | ||||
| 		}, { | ||||
| 			type: 'link', | ||||
| 			icon: 'fas fa-users', | ||||
| 			text: i18n.locale.manageAccounts, | ||||
| 			text: i18n.ts.manageAccounts, | ||||
| 			to: `/settings/accounts`, | ||||
| 		}]], ev.currentTarget || ev.target, { | ||||
| 		}]], ev.currentTarget ?? ev.target, { | ||||
| 			align: 'left' | ||||
| 		}); | ||||
| 	} else { | ||||
| 		popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget || ev.target, { | ||||
| 		popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, { | ||||
| 			align: 'left' | ||||
| 		}); | ||||
| 	} | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| <XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')"> | ||||
| 	<template #header> | ||||
| 		<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i> | ||||
| 		<I18n :src="i18n.locale.reportAbuseOf" tag="span"> | ||||
| 		<I18n :src="i18n.ts.reportAbuseOf" tag="span"> | ||||
| 			<template #name> | ||||
| 				<b><MkAcct :user="user"/></b> | ||||
| 			</template> | ||||
|  | @ -11,12 +11,12 @@ | |||
| 	<div class="dpvffvvy _monolithic_"> | ||||
| 		<div class="_section"> | ||||
| 			<MkTextarea v-model="comment"> | ||||
| 				<template #label>{{ i18n.locale.details }}</template> | ||||
| 				<template #caption>{{ i18n.locale.fillAbuseReportDescription }}</template> | ||||
| 				<template #label>{{ i18n.ts.details }}</template> | ||||
| 				<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template> | ||||
| 			</MkTextarea> | ||||
| 		</div> | ||||
| 		<div class="_section"> | ||||
| 			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.locale.send }}</MkButton> | ||||
| 			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </XWindow> | ||||
|  | @ -50,7 +50,7 @@ function send() { | |||
| 	}, undefined).then(res => { | ||||
| 		os.alert({ | ||||
| 			type: 'success', | ||||
| 			text: i18n.locale.abuseReported | ||||
| 			text: i18n.ts.abuseReported | ||||
| 		}); | ||||
| 		window.value?.close(); | ||||
| 		emit('closed'); | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
| 			</span> | ||||
| 			<span class="username">@{{ acct(user) }}</span> | ||||
| 		</li> | ||||
| 		<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.locale.selectUser }}</li> | ||||
| 		<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li> | ||||
| 	</ol> | ||||
| 	<ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags"> | ||||
| 		<li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown"> | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<span v-if="!available">{{ i18n.locale.waiting }}<MkEllipsis/></span> | ||||
| 	<span v-if="!available">{{ i18n.ts.waiting }}<MkEllipsis/></span> | ||||
| 	<div ref="captchaEl"></div> | ||||
| </div> | ||||
| </template> | ||||
|  |  | |||
|  | @ -6,14 +6,14 @@ | |||
| > | ||||
| 	<template v-if="!wait"> | ||||
| 		<template v-if="isFollowing"> | ||||
| 			<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i> | ||||
| 			<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i> | ||||
| 		</template> | ||||
| 		<template v-else> | ||||
| 			<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i> | ||||
| 			<span v-if="full">{{ i18n.ts.follow }}</span><i class="fas fa-plus"></i> | ||||
| 		</template> | ||||
| 	</template> | ||||
| 	<template v-else> | ||||
| 		<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> | ||||
| 		<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> | ||||
| 	</template> | ||||
| </button> | ||||
| </template> | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
| 		<div class="status"> | ||||
| 			<div> | ||||
| 				<i class="fas fa-users fa-fw"></i> | ||||
| 				<I18n :src="i18n.locale._channel.usersCount" tag="span" style="margin-left: 4px;"> | ||||
| 				<I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"> | ||||
| 					<template #n> | ||||
| 						<b>{{ channel.usersCount }}</b> | ||||
| 					</template> | ||||
|  | @ -14,7 +14,7 @@ | |||
| 			</div> | ||||
| 			<div> | ||||
| 				<i class="fas fa-pencil-alt fa-fw"></i> | ||||
| 				<I18n :src="i18n.locale._channel.notesCount" tag="span" style="margin-left: 4px;"> | ||||
| 				<I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"> | ||||
| 					<template #n> | ||||
| 						<b>{{ channel.notesCount }}</b> | ||||
| 					</template> | ||||
|  | @ -27,7 +27,7 @@ | |||
| 	</article> | ||||
| 	<footer> | ||||
| 		<span v-if="channel.lastNotedAt"> | ||||
| 			{{ i18n.locale.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> | ||||
| 			{{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> | ||||
| 		</span> | ||||
| 	</footer> | ||||
| </MkA> | ||||
|  |  | |||
							
								
								
									
										51
									
								
								packages/client/src/components/chart-tooltip.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								packages/client/src/components/chart-tooltip.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | |||
| <template> | ||||
| <MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" @closed="emit('closed')"> | ||||
| 	<div v-if="title" class="qpcyisrl"> | ||||
| 		<div class="title">{{ title }}</div> | ||||
| 		<div v-for="x in series" class="series"> | ||||
| 			<span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span> | ||||
| 			<span>{{ x.text }}</span> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkTooltip> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import {  } from 'vue'; | ||||
| import MkTooltip from './ui/tooltip.vue'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	showing: boolean; | ||||
| 	x: number; | ||||
| 	y: number; | ||||
| 	title: string; | ||||
| 	series: { | ||||
| 		backgroundColor: string; | ||||
| 		borderColor: string; | ||||
| 		text: string; | ||||
| 	}[]; | ||||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .qpcyisrl { | ||||
| 	> .title { | ||||
| 		margin-bottom: 4px; | ||||
| 	} | ||||
| 
 | ||||
| 	> .series { | ||||
| 		> .color { | ||||
| 			display: inline-block; | ||||
| 			width: 8px; | ||||
| 			height: 8px; | ||||
| 			border-width: 1px; | ||||
| 			border-style: solid; | ||||
| 			margin-right: 8px; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -8,7 +8,7 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, onMounted, ref, watch, PropType } from 'vue'; | ||||
| import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
|  | @ -31,6 +31,7 @@ import { enUS } from 'date-fns/locale'; | |||
| import zoomPlugin from 'chartjs-plugin-zoom'; | ||||
| import * as os from '@/os'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import MkChartTooltip from '@/components/chart-tooltip.vue'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
|  | @ -137,12 +138,50 @@ export default defineComponent({ | |||
| 			})); | ||||
| 		}; | ||||
| 
 | ||||
| 		const tooltipShowing = ref(false); | ||||
| 		const tooltipX = ref(0); | ||||
| 		const tooltipY = ref(0); | ||||
| 		const tooltipTitle = ref(null); | ||||
| 		const tooltipSeries = ref(null); | ||||
| 		let disposeTooltipComponent; | ||||
| 
 | ||||
| 		os.popup(MkChartTooltip, { | ||||
| 			showing: tooltipShowing, | ||||
| 			x: tooltipX, | ||||
| 			y: tooltipY, | ||||
| 			title: tooltipTitle, | ||||
| 			series: tooltipSeries, | ||||
| 		}, {}).then(({ dispose }) => { | ||||
| 			disposeTooltipComponent = dispose; | ||||
| 		}); | ||||
| 
 | ||||
| 		function externalTooltipHandler(context) { | ||||
| 			if (context.tooltip.opacity === 0) { | ||||
| 				tooltipShowing.value = false; | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			tooltipTitle.value = context.tooltip.title[0]; | ||||
| 			tooltipSeries.value = context.tooltip.body.map((b, i) => ({ | ||||
| 				backgroundColor: context.tooltip.labelColors[i].backgroundColor, | ||||
| 				borderColor: context.tooltip.labelColors[i].borderColor, | ||||
| 				text: b.lines[0], | ||||
| 			})); | ||||
| 
 | ||||
| 			const rect = context.chart.canvas.getBoundingClientRect(); | ||||
| 
 | ||||
| 			tooltipShowing.value = true; | ||||
| 			tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; | ||||
| 			tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; | ||||
| 		} | ||||
| 
 | ||||
| 		const render = () => { | ||||
| 			if (chartInstance) { | ||||
| 				chartInstance.destroy(); | ||||
| 			} | ||||
| 
 | ||||
| 			const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; | ||||
| 			const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; | ||||
| 
 | ||||
| 			// フォントカラー | ||||
| 			Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||
|  | @ -221,10 +260,12 @@ export default defineComponent({ | |||
| 							}, | ||||
| 						}, | ||||
| 						tooltip: { | ||||
| 							enabled: false, | ||||
| 							mode: 'index', | ||||
| 							animation: { | ||||
| 								duration: 0, | ||||
| 							}, | ||||
| 							external: externalTooltipHandler, | ||||
| 						}, | ||||
| 						zoom: { | ||||
| 							pan: { | ||||
|  | @ -255,6 +296,27 @@ export default defineComponent({ | |||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				plugins: [{ | ||||
| 					id: 'vLine', | ||||
| 					beforeDraw(chart, args, options) { | ||||
| 						if (chart.tooltip._active && chart.tooltip._active.length) { | ||||
| 							const activePoint = chart.tooltip._active[0]; | ||||
| 							const ctx = chart.ctx; | ||||
| 							const x = activePoint.element.x; | ||||
| 							const topY = chart.scales.y.top; | ||||
| 							const bottomY = chart.scales.y.bottom; | ||||
| 
 | ||||
| 							ctx.save(); | ||||
| 							ctx.beginPath(); | ||||
| 							ctx.moveTo(x, bottomY); | ||||
| 							ctx.lineTo(x, topY); | ||||
| 							ctx.lineWidth = 1; | ||||
| 							ctx.strokeStyle = vLineColor; | ||||
| 							ctx.stroke(); | ||||
| 							ctx.restore(); | ||||
| 						} | ||||
| 					} | ||||
| 				}] | ||||
| 			}); | ||||
| 		}; | ||||
| 
 | ||||
|  | @ -662,6 +724,10 @@ export default defineComponent({ | |||
| 			fetchAndRender(); | ||||
| 		}); | ||||
| 
 | ||||
| 		onUnmounted(() => { | ||||
| 			if (disposeTooltipComponent) disposeTooltipComponent(); | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			chartEl, | ||||
| 			fetching, | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <button class="nrvgflfu _button" @click="toggle"> | ||||
| 	<b>{{ modelValue ? i18n.locale._cw.hide : i18n.locale._cw.show }}</b> | ||||
| 	<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b> | ||||
| 	<span v-if="!modelValue">{{ label }}</span> | ||||
| </button> | ||||
| </template> | ||||
|  | @ -25,7 +25,7 @@ const label = computed(() => { | |||
| 	return concat([ | ||||
| 		props.note.text ? [i18n.t('_cw.chars', { count: length(props.note.text) })] : [], | ||||
| 		props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length }) ] : [], | ||||
| 		props.note.poll != null ? [i18n.locale.poll] : [] | ||||
| 		props.note.poll != null ? [i18n.ts.poll] : [] | ||||
| 	] as string[][]).join(' / '); | ||||
| }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -28,8 +28,8 @@ | |||
| 			</template> | ||||
| 		</MkSelect> | ||||
| 		<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons"> | ||||
| 			<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.locale.ok : i18n.locale.gotIt }}</MkButton> | ||||
| 			<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</MkButton> | ||||
| 			<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt }}</MkButton> | ||||
| 			<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton> | ||||
| 		</div> | ||||
| 		<div v-if="actions" class="buttons"> | ||||
| 			<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton> | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ | |||
| 	@closed="emit('closed')" | ||||
| > | ||||
| 	<template #header> | ||||
| 		{{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.selectFolder) }} | ||||
| 		{{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }} | ||||
| 		<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span> | ||||
| 	</template> | ||||
| 	<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/> | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
| 	@closed="emit('closed')" | ||||
| > | ||||
| 	<template #header> | ||||
| 		{{ i18n.locale.drive }} | ||||
| 		{{ i18n.ts.drive }} | ||||
| 	</template> | ||||
| 	<XDrive :initial-folder="initialFolder"/> | ||||
| </XWindow> | ||||
|  |  | |||
|  | @ -10,15 +10,15 @@ | |||
| > | ||||
| 	<div v-if="$i?.avatarId == file.id" class="label"> | ||||
| 		<img src="/client-assets/label.svg"/> | ||||
| 		<p>{{ i18n.locale.avatar }}</p> | ||||
| 		<p>{{ i18n.ts.avatar }}</p> | ||||
| 	</div> | ||||
| 	<div v-if="$i?.bannerId == file.id" class="label"> | ||||
| 		<img src="/client-assets/label.svg"/> | ||||
| 		<p>{{ i18n.locale.banner }}</p> | ||||
| 		<p>{{ i18n.ts.banner }}</p> | ||||
| 	</div> | ||||
| 	<div v-if="file.isSensitive" class="label red"> | ||||
| 		<img src="/client-assets/label-red.svg"/> | ||||
| 		<p>{{ i18n.locale.nsfw }}</p> | ||||
| 		<p>{{ i18n.ts.nsfw }}</p> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||
|  | @ -61,30 +61,30 @@ const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(pro | |||
| 
 | ||||
| function getMenu() { | ||||
| 	return [{ | ||||
| 		text: i18n.locale.rename, | ||||
| 		text: i18n.ts.rename, | ||||
| 		icon: 'fas fa-i-cursor', | ||||
| 		action: rename | ||||
| 	}, { | ||||
| 		text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive, | ||||
| 		text: props.file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, | ||||
| 		icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash', | ||||
| 		action: toggleSensitive | ||||
| 	}, { | ||||
| 		text: i18n.locale.describeFile, | ||||
| 		text: i18n.ts.describeFile, | ||||
| 		icon: 'fas fa-i-cursor', | ||||
| 		action: describe | ||||
| 	}, null, { | ||||
| 		text: i18n.locale.copyUrl, | ||||
| 		text: i18n.ts.copyUrl, | ||||
| 		icon: 'fas fa-link', | ||||
| 		action: copyUrl | ||||
| 	}, { | ||||
| 		type: 'a', | ||||
| 		href: props.file.url, | ||||
| 		target: '_blank', | ||||
| 		text: i18n.locale.download, | ||||
| 		text: i18n.ts.download, | ||||
| 		icon: 'fas fa-download', | ||||
| 		download: props.file.name | ||||
| 	}, null, { | ||||
| 		text: i18n.locale.delete, | ||||
| 		text: i18n.ts.delete, | ||||
| 		icon: 'fas fa-trash-alt', | ||||
| 		danger: true, | ||||
| 		action: deleteFile | ||||
|  | @ -95,7 +95,7 @@ function onClick(ev: MouseEvent) { | |||
| 	if (props.selectMode) { | ||||
| 		emit('chosen', props.file); | ||||
| 	} else { | ||||
| 		os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined); | ||||
| 		os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -120,8 +120,8 @@ function onDragend() { | |||
| 
 | ||||
| function rename() { | ||||
| 	os.inputText({ | ||||
| 		title: i18n.locale.renameFile, | ||||
| 		placeholder: i18n.locale.inputNewFileName, | ||||
| 		title: i18n.ts.renameFile, | ||||
| 		placeholder: i18n.ts.inputNewFileName, | ||||
| 		default: props.file.name, | ||||
| 	}).then(({ canceled, result: name }) => { | ||||
| 		if (canceled) return; | ||||
|  | @ -134,9 +134,9 @@ function rename() { | |||
| 
 | ||||
| function describe() { | ||||
| 	os.popup(import('@/components/media-caption.vue'), { | ||||
| 		title: i18n.locale.describeFile, | ||||
| 		title: i18n.ts.describeFile, | ||||
| 		input: { | ||||
| 			placeholder: i18n.locale.inputNewDescription, | ||||
| 			placeholder: i18n.ts.inputNewDescription, | ||||
| 			default: props.file.comment !== null ? props.file.comment : '', | ||||
| 		}, | ||||
| 		image: props.file | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ | |||
| 		{{ folder.name }} | ||||
| 	</p> | ||||
| 	<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload"> | ||||
| 		{{ i18n.locale.uploadFolder }} | ||||
| 		{{ i18n.ts.uploadFolder }} | ||||
| 	</p> | ||||
| 	<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button> | ||||
| </div> | ||||
|  | @ -146,14 +146,14 @@ function onDrop(ev: DragEvent) { | |||
| 			switch (err) { | ||||
| 				case 'detected-circular-definition': | ||||
| 					os.alert({ | ||||
| 						title: i18n.locale.unableToProcess, | ||||
| 						text: i18n.locale.circularReferenceFolder | ||||
| 						title: i18n.ts.unableToProcess, | ||||
| 						text: i18n.ts.circularReferenceFolder | ||||
| 					}); | ||||
| 					break; | ||||
| 				default: | ||||
| 					os.alert({ | ||||
| 						type: 'error', | ||||
| 						text: i18n.locale.somethingHappened | ||||
| 						text: i18n.ts.somethingHappened | ||||
| 					}); | ||||
| 			} | ||||
| 		}); | ||||
|  | @ -184,8 +184,8 @@ function go() { | |||
| 
 | ||||
| function rename() { | ||||
| 	os.inputText({ | ||||
| 		title: i18n.locale.renameFolder, | ||||
| 		placeholder: i18n.locale.inputNewFolderName, | ||||
| 		title: i18n.ts.renameFolder, | ||||
| 		placeholder: i18n.ts.inputNewFolderName, | ||||
| 		default: props.folder.name | ||||
| 	}).then(({ canceled, result: name }) => { | ||||
| 		if (canceled) return; | ||||
|  | @ -208,14 +208,14 @@ function deleteFolder() { | |||
| 			case 'b0fc8a17-963c-405d-bfbc-859a487295e1': | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					title: i18n.locale.unableToDelete, | ||||
| 					text: i18n.locale.hasChildFilesOrFolders | ||||
| 					title: i18n.ts.unableToDelete, | ||||
| 					text: i18n.ts.hasChildFilesOrFolders | ||||
| 				}); | ||||
| 				break; | ||||
| 			default: | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					text: i18n.locale.unableToDelete | ||||
| 					text: i18n.ts.unableToDelete | ||||
| 				}); | ||||
| 		} | ||||
| 	}); | ||||
|  | @ -227,7 +227,7 @@ function setAsUploadFolder() { | |||
| 
 | ||||
| function onContextmenu(ev: MouseEvent) { | ||||
| 	os.contextMenu([{ | ||||
| 		text: i18n.locale.openInWindow, | ||||
| 		text: i18n.ts.openInWindow, | ||||
| 		icon: 'fas fa-window-restore', | ||||
| 		action: () => { | ||||
| 			os.popup(import('./drive-window.vue'), { | ||||
|  | @ -236,11 +236,11 @@ function onContextmenu(ev: MouseEvent) { | |||
| 			}, 'closed'); | ||||
| 		} | ||||
| 	}, null, { | ||||
| 		text: i18n.locale.rename, | ||||
| 		text: i18n.ts.rename, | ||||
| 		icon: 'fas fa-i-cursor', | ||||
| 		action: rename, | ||||
| 	}, null, { | ||||
| 		text: i18n.locale.delete, | ||||
| 		text: i18n.ts.delete, | ||||
| 		icon: 'fas fa-trash-alt', | ||||
| 		danger: true, | ||||
| 		action: deleteFolder, | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
| 	@drop.stop="onDrop" | ||||
| > | ||||
| 	<i v-if="folder == null" class="fas fa-cloud"></i> | ||||
| 	<span>{{ folder == null ? i18n.locale.drive : folder.name }}</span> | ||||
| 	<span>{{ folder == null ? i18n.ts.drive : folder.name }}</span> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ | |||
| 				/> | ||||
| 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> | ||||
| 				<div v-for="(n, i) in 16" :key="i" class="padding"></div> | ||||
| 				<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton> | ||||
| 				<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.ts.loadMore }}</MkButton> | ||||
| 			</div> | ||||
| 			<div v-show="files.length > 0" ref="filesContainer" class="files"> | ||||
| 				<XFile | ||||
|  | @ -71,12 +71,12 @@ | |||
| 				/> | ||||
| 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> | ||||
| 				<div v-for="(n, i) in 16" :key="i" class="padding"></div> | ||||
| 				<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.locale.loadMore }}</MkButton> | ||||
| 				<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton> | ||||
| 			</div> | ||||
| 			<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty"> | ||||
| 				<p v-if="draghover">{{ i18n.t('empty-draghover') }}</p> | ||||
| 				<p v-if="!draghover && folder == null"><strong>{{ i18n.locale.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p> | ||||
| 				<p v-if="!draghover && folder != null">{{ i18n.locale.emptyFolder }}</p> | ||||
| 				<p v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p> | ||||
| 				<p v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</p> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<MkLoading v-if="fetching"/> | ||||
|  | @ -253,14 +253,14 @@ function onDrop(e: DragEvent): any { | |||
| 			switch (err) { | ||||
| 				case 'detected-circular-definition': | ||||
| 					os.alert({ | ||||
| 						title: i18n.locale.unableToProcess, | ||||
| 						text: i18n.locale.circularReferenceFolder | ||||
| 						title: i18n.ts.unableToProcess, | ||||
| 						text: i18n.ts.circularReferenceFolder | ||||
| 					}); | ||||
| 					break; | ||||
| 				default: | ||||
| 					os.alert({ | ||||
| 						type: 'error', | ||||
| 						text: i18n.locale.somethingHappened | ||||
| 						text: i18n.ts.somethingHappened | ||||
| 					}); | ||||
| 			} | ||||
| 		}); | ||||
|  | @ -274,9 +274,9 @@ function selectLocalFile() { | |||
| 
 | ||||
| function urlUpload() { | ||||
| 	os.inputText({ | ||||
| 		title: i18n.locale.uploadFromUrl, | ||||
| 		title: i18n.ts.uploadFromUrl, | ||||
| 		type: 'url', | ||||
| 		placeholder: i18n.locale.uploadFromUrlDescription | ||||
| 		placeholder: i18n.ts.uploadFromUrlDescription | ||||
| 	}).then(({ canceled, result: url }) => { | ||||
| 		if (canceled || !url) return; | ||||
| 		os.api('drive/files/upload-from-url', { | ||||
|  | @ -285,16 +285,16 @@ function urlUpload() { | |||
| 		}); | ||||
| 
 | ||||
| 		os.alert({ | ||||
| 			title: i18n.locale.uploadFromUrlRequested, | ||||
| 			text: i18n.locale.uploadFromUrlMayTakeTime | ||||
| 			title: i18n.ts.uploadFromUrlRequested, | ||||
| 			text: i18n.ts.uploadFromUrlMayTakeTime | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function createFolder() { | ||||
| 	os.inputText({ | ||||
| 		title: i18n.locale.createFolder, | ||||
| 		placeholder: i18n.locale.folderName | ||||
| 		title: i18n.ts.createFolder, | ||||
| 		placeholder: i18n.ts.folderName | ||||
| 	}).then(({ canceled, result: name }) => { | ||||
| 		if (canceled) return; | ||||
| 		os.api('drive/folders/create', { | ||||
|  | @ -308,8 +308,8 @@ function createFolder() { | |||
| 
 | ||||
| function renameFolder(folderToRename: Misskey.entities.DriveFolder) { | ||||
| 	os.inputText({ | ||||
| 		title: i18n.locale.renameFolder, | ||||
| 		placeholder: i18n.locale.inputNewFolderName, | ||||
| 		title: i18n.ts.renameFolder, | ||||
| 		placeholder: i18n.ts.inputNewFolderName, | ||||
| 		default: folderToRename.name | ||||
| 	}).then(({ canceled, result: name }) => { | ||||
| 		if (canceled) return; | ||||
|  | @ -334,14 +334,14 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) { | |||
| 			case 'b0fc8a17-963c-405d-bfbc-859a487295e1': | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					title: i18n.locale.unableToDelete, | ||||
| 					text: i18n.locale.hasChildFilesOrFolders | ||||
| 					title: i18n.ts.unableToDelete, | ||||
| 					text: i18n.ts.hasChildFilesOrFolders | ||||
| 				}); | ||||
| 				break; | ||||
| 			default: | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					text: i18n.locale.unableToDelete | ||||
| 					text: i18n.ts.unableToDelete | ||||
| 				}); | ||||
| 			} | ||||
| 	}); | ||||
|  | @ -562,36 +562,36 @@ function fetchMoreFiles() { | |||
| 
 | ||||
| function getMenu() { | ||||
| 	return [{ | ||||
| 		text: i18n.locale.addFile, | ||||
| 		text: i18n.ts.addFile, | ||||
| 		type: 'label' | ||||
| 	}, { | ||||
| 		text: i18n.locale.upload, | ||||
| 		text: i18n.ts.upload, | ||||
| 		icon: 'fas fa-upload', | ||||
| 		action: () => { selectLocalFile(); } | ||||
| 	}, { | ||||
| 		text: i18n.locale.fromUrl, | ||||
| 		text: i18n.ts.fromUrl, | ||||
| 		icon: 'fas fa-link', | ||||
| 		action: () => { urlUpload(); } | ||||
| 	}, null, { | ||||
| 		text: folder.value ? folder.value.name : i18n.locale.drive, | ||||
| 		text: folder.value ? folder.value.name : i18n.ts.drive, | ||||
| 		type: 'label' | ||||
| 	}, folder.value ? { | ||||
| 		text: i18n.locale.renameFolder, | ||||
| 		text: i18n.ts.renameFolder, | ||||
| 		icon: 'fas fa-i-cursor', | ||||
| 		action: () => { renameFolder(folder.value); } | ||||
| 	} : undefined, folder.value ? { | ||||
| 		text: i18n.locale.deleteFolder, | ||||
| 		text: i18n.ts.deleteFolder, | ||||
| 		icon: 'fas fa-trash-alt', | ||||
| 		action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); } | ||||
| 	} : undefined, { | ||||
| 		text: i18n.locale.createFolder, | ||||
| 		text: i18n.ts.createFolder, | ||||
| 		icon: 'fas fa-folder-plus', | ||||
| 		action: () => { createFolder(); } | ||||
| 	}]; | ||||
| } | ||||
| 
 | ||||
| function showMenu(ev: MouseEvent) { | ||||
| 	os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined); | ||||
| 	os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); | ||||
| } | ||||
| 
 | ||||
| function onContextmenu(ev: MouseEvent) { | ||||
|  |  | |||
|  | @ -32,20 +32,20 @@ import MkEmojiPicker from '@/components/emoji-picker.vue'; | |||
| import { defaultStore } from '@/store'; | ||||
| 
 | ||||
| withDefaults(defineProps<{ | ||||
| 	manualShowing?: boolean; | ||||
| 	manualShowing?: boolean | null; | ||||
| 	src?: HTMLElement; | ||||
| 	showPinned?: boolean; | ||||
| 	asReactionPicker?: boolean; | ||||
| }>(), { | ||||
| 	manualShowing: false, | ||||
| 	manualShowing: null, | ||||
| 	showPinned: true, | ||||
| 	asReactionPicker: false, | ||||
| }); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'done', v: any): void; | ||||
| 	(e: 'close'): void; | ||||
| 	(e: 'closed'): void; | ||||
| 	(ev: 'done', v: any): void; | ||||
| 	(ev: 'close'): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| 
 | ||||
| const modal = ref<InstanceType<typeof MkModal>>(); | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> | ||||
| 	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.locale.search" @paste.stop="paste" @keyup.enter="done()"> | ||||
| 	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" @paste.stop="paste" @keyup.enter="done()"> | ||||
| 	<div ref="emojis" class="emojis"> | ||||
| 		<section class="result"> | ||||
| 			<div v-if="searchResultCustom.length > 0"> | ||||
|  | @ -43,7 +43,7 @@ | |||
| 			</section> | ||||
| 
 | ||||
| 			<section> | ||||
| 				<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header> | ||||
| 				<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.ts.recentUsed }}</header> | ||||
| 				<div> | ||||
| 					<button v-for="emoji in recentlyUsedEmojis" | ||||
| 						:key="emoji" | ||||
|  | @ -56,11 +56,11 @@ | |||
| 			</section> | ||||
| 		</div> | ||||
| 		<div> | ||||
| 			<header class="_acrylic">{{ i18n.locale.customEmojis }}</header> | ||||
| 			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection> | ||||
| 			<header class="_acrylic">{{ i18n.ts.customEmojis }}</header> | ||||
| 			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection> | ||||
| 		</div> | ||||
| 		<div> | ||||
| 			<header class="_acrylic">{{ i18n.locale.emoji }}</header> | ||||
| 			<header class="_acrylic">{{ i18n.ts.emoji }}</header> | ||||
| 			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection> | ||||
| 		</div> | ||||
| 	</div> | ||||
|  | @ -280,7 +280,7 @@ function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): | |||
| } | ||||
| 
 | ||||
| function chosen(emoji: any, ev?: MouseEvent) { | ||||
| 	const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined; | ||||
| 	const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; | ||||
| 	if (el) { | ||||
| 		const rect = el.getBoundingClientRect(); | ||||
| 		const x = rect.left + (el.offsetWidth / 2); | ||||
|  |  | |||
|  | @ -6,23 +6,23 @@ | |||
| > | ||||
| 	<template v-if="!wait"> | ||||
| 		<template v-if="hasPendingFollowRequestFromYou && user.isLocked"> | ||||
| 			<span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i> | ||||
| 			<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i> | ||||
| 		</template> | ||||
| 		<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 --> | ||||
| 			<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i> | ||||
| 			<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i> | ||||
| 		</template> | ||||
| 		<template v-else-if="isFollowing"> | ||||
| 			<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i> | ||||
| 			<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i> | ||||
| 		</template> | ||||
| 		<template v-else-if="!isFollowing && user.isLocked"> | ||||
| 			<span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i> | ||||
| 			<span v-if="full">{{ i18n.ts.followRequest }}</span><i class="fas fa-plus"></i> | ||||
| 		</template> | ||||
| 		<template v-else-if="!isFollowing && !user.isLocked"> | ||||
| 			<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i> | ||||
| 			<span v-if="full">{{ i18n.ts.follow }}</span><i class="fas fa-plus"></i> | ||||
| 		</template> | ||||
| 	</template> | ||||
| 	<template v-else> | ||||
| 		<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> | ||||
| 		<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> | ||||
| 	</template> | ||||
| </button> | ||||
| </template> | ||||
|  |  | |||
|  | @ -5,28 +5,28 @@ | |||
| 	@close="dialog.close()" | ||||
| 	@closed="emit('closed')" | ||||
| > | ||||
| 	<template #header>{{ i18n.locale.forgotPassword }}</template> | ||||
| 	<template #header>{{ i18n.ts.forgotPassword }}</template> | ||||
| 
 | ||||
| 	<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit"> | ||||
| 		<div class="main _formRoot"> | ||||
| 			<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required> | ||||
| 				<template #label>{{ i18n.locale.username }}</template> | ||||
| 				<template #label>{{ i18n.ts.username }}</template> | ||||
| 				<template #prefix>@</template> | ||||
| 			</MkInput> | ||||
| 
 | ||||
| 			<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required> | ||||
| 				<template #label>{{ i18n.locale.emailAddress }}</template> | ||||
| 				<template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template> | ||||
| 				<template #label>{{ i18n.ts.emailAddress }}</template> | ||||
| 				<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template> | ||||
| 			</MkInput> | ||||
| 
 | ||||
| 			<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton> | ||||
| 			<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton> | ||||
| 		</div> | ||||
| 		<div class="sub"> | ||||
| 			<MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA> | ||||
| 			<MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA> | ||||
| 		</div> | ||||
| 	</form> | ||||
| 	<div v-else class="bafecedb"> | ||||
| 		{{ i18n.locale._forgotPassword.contactAdmin }} | ||||
| 		{{ i18n.ts._forgotPassword.contactAdmin }} | ||||
| 	</div> | ||||
| </XModalWindow> | ||||
| </template> | ||||
|  |  | |||
|  | @ -117,7 +117,7 @@ export default defineComponent({ | |||
| 				text: computed(() => { | ||||
| 					return props.textConverter(finalValue.value); | ||||
| 				}), | ||||
| 				source: thumbEl, | ||||
| 				targetElement: thumbEl, | ||||
| 			}, {}, 'closed'); | ||||
| 
 | ||||
| 			const style = document.createElement('style'); | ||||
|  |  | |||
|  | @ -20,45 +20,33 @@ | |||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, ref, toRefs } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { toRefs, Ref } from 'vue'; | ||||
| import * as os from '@/os'; | ||||
| import Ripple from '@/components/ripple.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		modelValue: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		disabled: { | ||||
| 			type: Boolean, | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
| const props = defineProps<{ | ||||
| 	modelValue: boolean | Ref<boolean>; | ||||
| 	disabled?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| 	setup(props, context) { | ||||
| 		const button = ref<HTMLElement>(); | ||||
| 		const checked = toRefs(props).modelValue; | ||||
| 		const toggle = () => { | ||||
| 			if (props.disabled) return; | ||||
| 			context.emit('update:modelValue', !checked.value); | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'update:modelValue', v: boolean): void; | ||||
| }>(); | ||||
| 
 | ||||
| 			if (!checked.value) { | ||||
| 				const rect = button.value.getBoundingClientRect(); | ||||
| 				const x = rect.left + (button.value.offsetWidth / 2); | ||||
| 				const y = rect.top + (button.value.offsetHeight / 2); | ||||
| 				os.popup(Ripple, { x, y, particle: false }, {}, 'end'); | ||||
| 			} | ||||
| 		}; | ||||
| let button = $ref<HTMLElement>(); | ||||
| const checked = toRefs(props).modelValue; | ||||
| const toggle = () => { | ||||
| 	if (props.disabled) return; | ||||
| 	emit('update:modelValue', !checked.value); | ||||
| 
 | ||||
| 		return { | ||||
| 			button, | ||||
| 			checked, | ||||
| 			toggle, | ||||
| 		}; | ||||
| 	}, | ||||
| }); | ||||
| 	if (!checked.value) { | ||||
| 		const rect = button.getBoundingClientRect(); | ||||
| 		const x = rect.left + (button.offsetWidth / 2); | ||||
| 		const y = rect.top + (button.offsetHeight / 2); | ||||
| 		os.popup(Ripple, { x, y, particle: false }, {}, 'end'); | ||||
| 	} | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -23,8 +23,9 @@ const props = withDefaults(defineProps<{ | |||
| 	behavior: null, | ||||
| }); | ||||
| 
 | ||||
| const navHook = inject('navHook', null); | ||||
| const sideViewHook = inject('sideViewHook', null); | ||||
| type Navigate = (path: string, record?: boolean) => void; | ||||
| const navHook = inject<null | Navigate>('navHook', null); | ||||
| const sideViewHook = inject<null | Navigate>('sideViewHook', null); | ||||
| 
 | ||||
| const active = $computed(() => { | ||||
| 	if (props.activeClass == null) return false; | ||||
|  | @ -43,31 +44,31 @@ function onContextmenu(ev) { | |||
| 		text: props.to, | ||||
| 	}, { | ||||
| 		icon: 'fas fa-window-maximize', | ||||
| 		text: i18n.locale.openInWindow, | ||||
| 		text: i18n.ts.openInWindow, | ||||
| 		action: () => { | ||||
| 			os.pageWindow(props.to); | ||||
| 		} | ||||
| 	}, sideViewHook ? { | ||||
| 		icon: 'fas fa-columns', | ||||
| 		text: i18n.locale.openInSideView, | ||||
| 		text: i18n.ts.openInSideView, | ||||
| 		action: () => { | ||||
| 			sideViewHook(props.to); | ||||
| 		} | ||||
| 	} : undefined, { | ||||
| 		icon: 'fas fa-expand-alt', | ||||
| 		text: i18n.locale.showInPage, | ||||
| 		text: i18n.ts.showInPage, | ||||
| 		action: () => { | ||||
| 			router.push(props.to); | ||||
| 		} | ||||
| 	}, null, { | ||||
| 		icon: 'fas fa-external-link-alt', | ||||
| 		text: i18n.locale.openInNewTab, | ||||
| 		text: i18n.ts.openInNewTab, | ||||
| 		action: () => { | ||||
| 			window.open(props.to, '_blank'); | ||||
| 		} | ||||
| 	}, { | ||||
| 		icon: 'fas fa-link', | ||||
| 		text: i18n.locale.copyLink, | ||||
| 		text: i18n.ts.copyLink, | ||||
| 		action: () => { | ||||
| 			copyToClipboard(`${url}${props.to}`); | ||||
| 		} | ||||
|  |  | |||
|  | @ -104,7 +104,7 @@ export default defineComponent({ | |||
| 			if (props.info.share) { | ||||
| 				if (menu.length > 0) menu.push(null); | ||||
| 				menu.push({ | ||||
| 					text: i18n.locale.share, | ||||
| 					text: i18n.ts.share, | ||||
| 					icon: 'fas fa-share-alt', | ||||
| 					action: share | ||||
| 				}); | ||||
|  | @ -113,7 +113,7 @@ export default defineComponent({ | |||
| 				if (menu.length > 0) menu.push(null); | ||||
| 				menu = menu.concat(props.menu); | ||||
| 			} | ||||
| 			popupMenu(menu, ev.currentTarget || ev.target); | ||||
| 			popupMenu(menu, ev.currentTarget ?? ev.target); | ||||
| 		}; | ||||
| 
 | ||||
| 		const showTabsPopup = (ev: MouseEvent) => { | ||||
|  | @ -126,7 +126,7 @@ export default defineComponent({ | |||
| 				icon: tab.icon, | ||||
| 				action: tab.onClick, | ||||
| 			})); | ||||
| 			popupMenu(menu, ev.currentTarget || ev.target); | ||||
| 			popupMenu(menu, ev.currentTarget ?? ev.target); | ||||
| 		}; | ||||
| 
 | ||||
| 		const preventDrag = (ev: TouchEvent) => { | ||||
|  |  | |||
|  | @ -24,16 +24,16 @@ let now = $ref(new Date()); | |||
| const relative = $computed(() => { | ||||
| 	const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/; | ||||
| 	return ( | ||||
| 		ago >= 31536000 ? i18n.t('_ago.yearsAgo',   { n: (~~(ago / 31536000)).toString() }) : | ||||
| 		ago >= 2592000  ? i18n.t('_ago.monthsAgo',  { n: (~~(ago / 2592000)).toString() }) : | ||||
| 		ago >= 604800   ? i18n.t('_ago.weeksAgo',   { n: (~~(ago / 604800)).toString() }) : | ||||
| 		ago >= 86400    ? i18n.t('_ago.daysAgo',    { n: (~~(ago / 86400)).toString() }) : | ||||
| 		ago >= 3600     ? i18n.t('_ago.hoursAgo',   { n: (~~(ago / 3600)).toString() }) : | ||||
| 		ago >= 31536000 ? i18n.t('_ago.yearsAgo',   { n: Math.round(ago / 31536000).toString() }) : | ||||
| 		ago >= 2592000  ? i18n.t('_ago.monthsAgo',  { n: Math.round(ago / 2592000).toString() }) : | ||||
| 		ago >= 604800   ? i18n.t('_ago.weeksAgo',   { n: Math.round(ago / 604800).toString() }) : | ||||
| 		ago >= 86400    ? i18n.t('_ago.daysAgo',    { n: Math.round(ago / 86400).toString() }) : | ||||
| 		ago >= 3600     ? i18n.t('_ago.hoursAgo',   { n: Math.round(ago / 3600).toString() }) : | ||||
| 		ago >= 60       ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : | ||||
| 		ago >= 10       ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : | ||||
| 		ago >= -1       ? i18n.locale._ago.justNow : | ||||
| 		ago <  -1       ? i18n.locale._ago.future : | ||||
| 		i18n.locale._ago.unknown); | ||||
| 		ago >= -1       ? i18n.ts._ago.justNow : | ||||
| 		ago <  -1       ? i18n.ts._ago.future : | ||||
| 		i18n.ts._ago.unknown); | ||||
| }); | ||||
| 
 | ||||
| function tick() { | ||||
|  |  | |||
|  | @ -20,52 +20,32 @@ | |||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { watch } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | ||||
| import ImgWithBlurhash from '@/components/img-with-blurhash.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { defaultStore } from '@/store'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		ImgWithBlurhash | ||||
| 	}, | ||||
| 	props: { | ||||
| 		image: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		raw: { | ||||
| 			default: false | ||||
| 		} | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			hide: true, | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		url(): any { | ||||
| 			let url = this.$store.state.disableShowingAnimatedImages | ||||
| 				? getStaticImageUrl(this.image.thumbnailUrl) | ||||
| 				: this.image.thumbnailUrl; | ||||
| const props = defineProps<{ | ||||
| 	image: misskey.entities.DriveFile; | ||||
| 	raw?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| 			if (this.raw || this.$store.state.loadRawImages) { | ||||
| 				url = this.image.url; | ||||
| 			} | ||||
| let hide = $ref(true); | ||||
| 
 | ||||
| 			return url; | ||||
| 		} | ||||
| 	}, | ||||
| 	created() { | ||||
| 		// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする | ||||
| 		this.$watch('image', () => { | ||||
| 			this.hide = (this.$store.state.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.nsfw !== 'ignore'); | ||||
| 		}, { | ||||
| 			deep: true, | ||||
| 			immediate: true, | ||||
| 		}); | ||||
| 	}, | ||||
| const url = (props.raw || defaultStore.state.loadRawImages) | ||||
| 	? props.image.url | ||||
| 	: defaultStore.state.disableShowingAnimatedImages | ||||
| 			? getStaticImageUrl(props.image.thumbnailUrl) | ||||
| 			: props.image.thumbnailUrl; | ||||
| 
 | ||||
| // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする | ||||
| watch(() => props.image, () => { | ||||
| 	hide = (defaultStore.state.nsfw === 'force') ? true : props.image.isSensitive && (defaultStore.state.nsfw !== 'ignore'); | ||||
| }, { | ||||
| 	deep: true, | ||||
| 	immediate: true, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,8 +12,8 @@ | |||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, onMounted, PropType, ref } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, ref } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import PhotoSwipeLightbox from 'photoswipe/dist/photoswipe-lightbox.esm.js'; | ||||
| import PhotoSwipe from 'photoswipe/dist/photoswipe.esm.js'; | ||||
|  | @ -25,98 +25,80 @@ import * as os from '@/os'; | |||
| import { FILE_TYPE_BROWSERSAFE } from '@/const'; | ||||
| import { defaultStore } from '@/store'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XBanner, | ||||
| 		XImage, | ||||
| 		XVideo, | ||||
| 	}, | ||||
| 	props: { | ||||
| 		mediaList: { | ||||
| 			type: Array as PropType<misskey.entities.DriveFile[]>, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		raw: { | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 	setup(props) { | ||||
| 		const gallery = ref(null); | ||||
| const props = defineProps<{ | ||||
| 	mediaList: misskey.entities.DriveFile[]; | ||||
| 	raw?: boolean; | ||||
| }>(); | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			const lightbox = new PhotoSwipeLightbox({ | ||||
| 				dataSource: props.mediaList | ||||
| 					.filter(media => { | ||||
| 						if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue | ||||
| 						return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type); | ||||
| 					}) | ||||
| 					.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', | ||||
| 				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, | ||||
| 			}); | ||||
| const gallery = ref(null); | ||||
| const pswpZIndex = os.claimZIndex('middle'); | ||||
| 
 | ||||
| 			lightbox.on('itemData', (e) => { | ||||
| 				const { itemData } = e; | ||||
| 
 | ||||
| 				// element is children | ||||
| 				const { element } = itemData; | ||||
| 
 | ||||
| 				const id = element.dataset.id; | ||||
| 				const file = props.mediaList.find(media => media.id === id); | ||||
| 
 | ||||
| 				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]; | ||||
| onMounted(() => { | ||||
| 	const lightbox = new PhotoSwipeLightbox({ | ||||
| 		dataSource: props.mediaList | ||||
| 			.filter(media => { | ||||
| 				if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue | ||||
| 				return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type); | ||||
| 			}) | ||||
| 			.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]; | ||||
| 				} | ||||
| 				itemData.msrc = file.thumbnailUrl; | ||||
| 				itemData.thumbCropped = true; | ||||
| 			}); | ||||
| 				return item; | ||||
| 			}), | ||||
| 		gallery: gallery.value, | ||||
| 		children: '.image', | ||||
| 		thumbSelector: '.image', | ||||
| 		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.init(); | ||||
| 		}); | ||||
| 	lightbox.on('itemData', (ev) => { | ||||
| 		const { itemData } = ev; | ||||
| 
 | ||||
| 		const previewable = (file: misskey.entities.DriveFile): boolean => { | ||||
| 			if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue | ||||
| 			// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切 | ||||
| 			return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type); | ||||
| 		}; | ||||
| 		// element is children | ||||
| 		const { element } = itemData; | ||||
| 
 | ||||
| 		return { | ||||
| 			previewable, | ||||
| 			gallery, | ||||
| 			pswpZIndex: os.claimZIndex('middle'), | ||||
| 		}; | ||||
| 	}, | ||||
| 		const id = element.dataset.id; | ||||
| 		const file = props.mediaList.find(media => media.id === id); | ||||
| 
 | ||||
| 		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; | ||||
| 	}); | ||||
| 
 | ||||
| 	lightbox.init(); | ||||
| }); | ||||
| 
 | ||||
| const previewable = (file: misskey.entities.DriveFile): boolean => { | ||||
| 	if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue | ||||
| 	// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切 | ||||
| 	return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type); | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -250,7 +250,7 @@ function menu(viaKeyboard = false): void { | |||
| function showRenoteMenu(viaKeyboard = false): void { | ||||
| 	if (!isMyRenote) return; | ||||
| 	os.popupMenu([{ | ||||
| 		text: i18n.locale.unrenote, | ||||
| 		text: i18n.ts.unrenote, | ||||
| 		icon: 'fas fa-trash-alt', | ||||
| 		danger: true, | ||||
| 		action: () => { | ||||
|  |  | |||
|  | @ -10,13 +10,13 @@ | |||
| 	:class="{ renote: isRenote }" | ||||
| > | ||||
| 	<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/> | ||||
| 	<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.locale.pinnedNote }}</div> | ||||
| 	<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.locale.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.locale.hideThisNote }} <i class="fas fa-times"></i></button></div> | ||||
| 	<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.locale.featured }}</div> | ||||
| 	<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.ts.pinnedNote }}</div> | ||||
| 	<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="fas fa-times"></i></button></div> | ||||
| 	<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.ts.featured }}</div> | ||||
| 	<div v-if="isRenote" class="renote"> | ||||
| 		<MkAvatar class="avatar" :user="note.user"/> | ||||
| 		<i class="fas fa-retweet"></i> | ||||
| 		<I18n :src="i18n.locale.renotedBy" tag="span"> | ||||
| 		<I18n :src="i18n.ts.renotedBy" tag="span"> | ||||
| 			<template #user> | ||||
| 				<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)"> | ||||
| 					<MkUserName :user="note.user"/> | ||||
|  | @ -48,7 +48,7 @@ | |||
| 				</p> | ||||
| 				<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }"> | ||||
| 					<div class="text"> | ||||
| 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.locale.private }})</span> | ||||
| 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> | ||||
| 						<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA> | ||||
| 						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> | ||||
| 						<a v-if="appearNote.renote != null" class="rp">RN:</a> | ||||
|  | @ -67,7 +67,7 @@ | |||
| 					<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/> | ||||
| 					<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div> | ||||
| 					<button v-if="collapsed" class="fade _button" @click="collapsed = false"> | ||||
| 						<span>{{ i18n.locale.showMore }}</span> | ||||
| 						<span>{{ i18n.ts.showMore }}</span> | ||||
| 					</button> | ||||
| 				</div> | ||||
| 				<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA> | ||||
|  | @ -94,7 +94,7 @@ | |||
| 	</article> | ||||
| </div> | ||||
| <div v-else class="muted" @click="muted = false"> | ||||
| 	<I18n :src="i18n.locale.userSaysSomething" tag="small"> | ||||
| 	<I18n :src="i18n.ts.userSaysSomething" tag="small"> | ||||
| 		<template #name> | ||||
| 			<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)"> | ||||
| 				<MkUserName :user="appearNote.user"/> | ||||
|  | @ -238,7 +238,7 @@ function menu(viaKeyboard = false): void { | |||
| function showRenoteMenu(viaKeyboard = false): void { | ||||
| 	if (!isMyRenote) return; | ||||
| 	os.popupMenu([{ | ||||
| 		text: i18n.locale.unrenote, | ||||
| 		text: i18n.ts.unrenote, | ||||
| 		icon: 'fas fa-trash-alt', | ||||
| 		danger: true, | ||||
| 		action: () => { | ||||
|  |  | |||
|  | @ -153,7 +153,7 @@ export default defineComponent({ | |||
| 				showing, | ||||
| 				reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction, | ||||
| 				emojis: props.notification.note.emojis, | ||||
| 				source: reactionRef.value.$el, | ||||
| 				targetElement: reactionRef.value.$el, | ||||
| 			}, {}, 'closed'); | ||||
| 		}); | ||||
| 
 | ||||
|  |  | |||
|  | @ -160,7 +160,7 @@ export default defineComponent({ | |||
| 				action: () => { | ||||
| 					copyToClipboard(this.url); | ||||
| 				} | ||||
| 			}], ev.currentTarget || ev.target); | ||||
| 			}], ev.currentTarget ?? ev.target); | ||||
| 		}, | ||||
| 
 | ||||
| 		back() { | ||||
|  |  | |||
|  | @ -127,7 +127,7 @@ export default defineComponent({ | |||
| 				text: this.$ts.attachCancel, | ||||
| 				icon: 'fas fa-times-circle', | ||||
| 				action: () => { this.detachMedia(file.id) } | ||||
| 			}], ev.currentTarget || ev.target).then(() => this.menu = null); | ||||
| 			}], ev.currentTarget ?? ev.target).then(() => this.menu = null); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -8,28 +8,28 @@ | |||
| > | ||||
| 	<header> | ||||
| 		<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button> | ||||
| 		<button v-click-anime v-tooltip="i18n.locale.switchAccount" class="account _button" @click="openAccountMenu"> | ||||
| 		<button v-click-anime v-tooltip="i18n.ts.switchAccount" class="account _button" @click="openAccountMenu"> | ||||
| 			<MkAvatar :user="postAccount ?? $i" class="avatar"/> | ||||
| 		</button> | ||||
| 		<div> | ||||
| 			<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span> | ||||
| 			<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span> | ||||
| 			<button ref="visibilityButton" v-tooltip="i18n.locale.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility"> | ||||
| 			<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility"> | ||||
| 				<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span> | ||||
| 				<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span> | ||||
| 				<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span> | ||||
| 				<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span> | ||||
| 			</button> | ||||
| 			<button v-tooltip="i18n.locale.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button> | ||||
| 			<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button> | ||||
| 			<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button> | ||||
| 		</div> | ||||
| 	</header> | ||||
| 	<div class="form" :class="{ fixed }"> | ||||
| 		<XNoteSimple v-if="reply" class="preview" :note="reply"/> | ||||
| 		<XNoteSimple v-if="renote" class="preview" :note="renote"/> | ||||
| 		<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.locale.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div> | ||||
| 		<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div> | ||||
| 		<div v-if="visibility === 'specified'" class="to-specified"> | ||||
| 			<span style="margin-right: 8px;">{{ i18n.locale.recipient }}</span> | ||||
| 			<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span> | ||||
| 			<div class="visibleUsers"> | ||||
| 				<span v-for="u in visibleUsers" :key="u.id"> | ||||
| 					<MkAcct :user="u"/> | ||||
|  | @ -38,21 +38,21 @@ | |||
| 				<button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.locale.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.locale.add }}</button></MkInfo> | ||||
| 		<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.locale.annotation" @keydown="onKeydown"> | ||||
| 		<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> | ||||
| 		<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> | ||||
| 		<textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> | ||||
| 		<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.locale.hashtags" list="hashtags"> | ||||
| 		<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> | ||||
| 		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> | ||||
| 		<XPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> | ||||
| 		<XNotePreview v-if="showPreview" class="preview" :text="text"/> | ||||
| 		<footer> | ||||
| 			<button v-tooltip="i18n.locale.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button> | ||||
| 			<button v-tooltip="i18n.locale.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button> | ||||
| 			<button v-tooltip="i18n.locale.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button> | ||||
| 			<button v-tooltip="i18n.locale.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button> | ||||
| 			<button v-tooltip="i18n.locale.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button> | ||||
| 			<button v-tooltip="i18n.locale.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button> | ||||
| 			<button v-if="postFormActions.length > 0" v-tooltip="i18n.locale.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button> | ||||
| 			<button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button> | ||||
| 			<button v-tooltip="i18n.ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button> | ||||
| 			<button v-tooltip="i18n.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button> | ||||
| 			<button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button> | ||||
| 			<button v-tooltip="i18n.ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button> | ||||
| 			<button v-tooltip="i18n.ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button> | ||||
| 			<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button> | ||||
| 		</footer> | ||||
| 		<datalist id="hashtags"> | ||||
| 			<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/> | ||||
|  | @ -135,7 +135,10 @@ let showPreview = $ref(false); | |||
| let cw = $ref<string | null>(null); | ||||
| let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); | ||||
| let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof misskey.noteVisibilities[number]); | ||||
| let visibleUsers = $ref(props.initialVisibleUsers ?? []); | ||||
| let visibleUsers = $ref([]); | ||||
| if (props.initialVisibleUsers) { | ||||
| 	props.initialVisibleUsers.forEach(pushVisibleUser); | ||||
| } | ||||
| let autocomplete = $ref(null); | ||||
| let draghover = $ref(false); | ||||
| let quoteId = $ref(null); | ||||
|  | @ -165,19 +168,19 @@ const draftKey = $computed((): string => { | |||
| 
 | ||||
| const placeholder = $computed((): string => { | ||||
| 	if (props.renote) { | ||||
| 		return i18n.locale._postForm.quotePlaceholder; | ||||
| 		return i18n.ts._postForm.quotePlaceholder; | ||||
| 	} else if (props.reply) { | ||||
| 		return i18n.locale._postForm.replyPlaceholder; | ||||
| 		return i18n.ts._postForm.replyPlaceholder; | ||||
| 	} else if (props.channel) { | ||||
| 		return i18n.locale._postForm.channelPlaceholder; | ||||
| 		return i18n.ts._postForm.channelPlaceholder; | ||||
| 	} else { | ||||
| 		const xs = [ | ||||
| 			i18n.locale._postForm._placeholders.a, | ||||
| 			i18n.locale._postForm._placeholders.b, | ||||
| 			i18n.locale._postForm._placeholders.c, | ||||
| 			i18n.locale._postForm._placeholders.d, | ||||
| 			i18n.locale._postForm._placeholders.e, | ||||
| 			i18n.locale._postForm._placeholders.f | ||||
| 			i18n.ts._postForm._placeholders.a, | ||||
| 			i18n.ts._postForm._placeholders.b, | ||||
| 			i18n.ts._postForm._placeholders.c, | ||||
| 			i18n.ts._postForm._placeholders.d, | ||||
| 			i18n.ts._postForm._placeholders.e, | ||||
| 			i18n.ts._postForm._placeholders.f | ||||
| 		]; | ||||
| 		return xs[Math.floor(Math.random() * xs.length)]; | ||||
| 	} | ||||
|  | @ -185,10 +188,10 @@ const placeholder = $computed((): string => { | |||
| 
 | ||||
| const submitText = $computed((): string => { | ||||
| 	return props.renote | ||||
| 		? i18n.locale.quote | ||||
| 		? i18n.ts.quote | ||||
| 		: props.reply | ||||
| 			? i18n.locale.reply | ||||
| 			: i18n.locale.note; | ||||
| 			? i18n.ts.reply | ||||
| 			: i18n.ts.note; | ||||
| }); | ||||
| 
 | ||||
| const textLength = $computed((): number => { | ||||
|  | @ -262,12 +265,12 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib | |||
| 		os.api('users/show', { | ||||
| 			userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId) | ||||
| 		}).then(users => { | ||||
| 			visibleUsers.push(...users); | ||||
| 			users.forEach(pushVisibleUser); | ||||
| 		}); | ||||
| 
 | ||||
| 		if (props.reply.userId !== $i.id) { | ||||
| 			os.api('users/show', { userId: props.reply.userId }).then(user => { | ||||
| 				visibleUsers.push(user); | ||||
| 				pushVisibleUser(user); | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  | @ -275,7 +278,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib | |||
| 
 | ||||
| if (props.specified) { | ||||
| 	visibility = 'specified'; | ||||
| 	visibleUsers.push(props.specified); | ||||
| 	pushVisibleUser(props.specified); | ||||
| } | ||||
| 
 | ||||
| // keep cw when reply | ||||
|  | @ -342,7 +345,7 @@ function focus() { | |||
| } | ||||
| 
 | ||||
| function chooseFileFrom(ev) { | ||||
| 	selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files_ => { | ||||
| 	selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => { | ||||
| 		for (const file of files_) { | ||||
| 			files.push(file); | ||||
| 		} | ||||
|  | @ -397,9 +400,15 @@ function setVisibility() { | |||
| 	}, 'closed'); | ||||
| } | ||||
| 
 | ||||
| function pushVisibleUser(user) { | ||||
| 	if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) { | ||||
| 		visibleUsers.push(user); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| function addVisibleUser() { | ||||
| 	os.selectUser().then(user => { | ||||
| 		visibleUsers.push(user); | ||||
| 		pushVisibleUser(user); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | @ -447,7 +456,7 @@ async function onPaste(e: ClipboardEvent) { | |||
| 
 | ||||
| 		os.confirm({ | ||||
| 			type: 'info', | ||||
| 			text: i18n.locale.quoteQuestion, | ||||
| 			text: i18n.ts.quoteQuestion, | ||||
| 		}).then(({ canceled }) => { | ||||
| 			if (canceled) { | ||||
| 				insertTextAtCursor(textareaEl, paste); | ||||
|  | @ -540,8 +549,8 @@ async function post() { | |||
| 	}; | ||||
| 
 | ||||
| 	if (withHashtags && hashtags && hashtags.trim() !== '') { | ||||
| 		const hashtags = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); | ||||
| 		data.text = data.text ? `${data.text} ${hashtags}` : hashtags; | ||||
| 		const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' '); | ||||
| 		data.text = data.text ? `${data.text} ${hashtags_}` : hashtags_; | ||||
| 	} | ||||
| 
 | ||||
| 	// plugin | ||||
|  | @ -565,9 +574,9 @@ async function post() { | |||
| 			deleteDraft(); | ||||
| 			emit('posted'); | ||||
| 			if (data.text && data.text != '') { | ||||
| 				const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); | ||||
| 				const hashtags_ = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); | ||||
| 				const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; | ||||
| 				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); | ||||
| 				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history)))); | ||||
| 			} | ||||
| 			posting = false; | ||||
| 			postAccount = null; | ||||
|  | @ -592,7 +601,7 @@ function insertMention() { | |||
| } | ||||
| 
 | ||||
| async function insertEmoji(ev: MouseEvent) { | ||||
| 	os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl); | ||||
| 	os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl); | ||||
| } | ||||
| 
 | ||||
| function showActions(ev) { | ||||
|  | @ -605,7 +614,7 @@ function showActions(ev) { | |||
| 				if (key === 'text') { text = value; } | ||||
| 			}); | ||||
| 		} | ||||
| 	})), ev.currentTarget || ev.target); | ||||
| 	})), ev.currentTarget ?? ev.target); | ||||
| } | ||||
| 
 | ||||
| let postAccount = $ref<misskey.entities.UserDetailed | null>(null); | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> | ||||
| <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> | ||||
| 	<div class="beeadbfb"> | ||||
| 		<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> | ||||
| 		<div class="name">{{ reaction.replace('@.', '') }}</div> | ||||
|  | @ -15,11 +15,11 @@ import XReactionIcon from './reaction-icon.vue'; | |||
| const props = defineProps<{ | ||||
| 	reaction: string; | ||||
| 	emojis: any[]; // TODO | ||||
| 	source: any; // TODO | ||||
| 	targetElement: HTMLElement; | ||||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'closed'): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> | ||||
| <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> | ||||
| 	<div class="bqxuuuey"> | ||||
| 		<div class="reaction"> | ||||
| 			<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> | ||||
|  | @ -26,11 +26,11 @@ const props = defineProps<{ | |||
| 	users: any[]; // TODO | ||||
| 	count: number; | ||||
| 	emojis: any[]; // TODO | ||||
| 	source: any; // TODO | ||||
| 	targetElement: HTMLElement; | ||||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'closed'): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -59,7 +59,7 @@ export default defineComponent({ | |||
| 		const renote = (viaKeyboard = false) => { | ||||
| 			pleaseLogin(); | ||||
| 			os.popupMenu([{ | ||||
| 				text: i18n.locale.renote, | ||||
| 				text: i18n.ts.renote, | ||||
| 				icon: 'fas fa-retweet', | ||||
| 				action: () => { | ||||
| 					os.api('notes/create', { | ||||
|  | @ -67,7 +67,7 @@ export default defineComponent({ | |||
| 					}); | ||||
| 				} | ||||
| 			}, { | ||||
| 				text: i18n.locale.quote, | ||||
| 				text: i18n.ts.quote, | ||||
| 				icon: 'fas fa-quote-right', | ||||
| 				action: () => { | ||||
| 					os.post({ | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="emit('closed')"> | ||||
| <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="250" @closed="emit('closed')"> | ||||
| 	<div class="beaffaef"> | ||||
| 		<div v-for="u in users" :key="u.id" class="user"> | ||||
| 			<MkAvatar class="avatar" :user="u"/> | ||||
|  | @ -17,11 +17,11 @@ import MkTooltip from './ui/tooltip.vue'; | |||
| const props = defineProps<{ | ||||
| 	users: any[]; // TODO | ||||
| 	count: number; | ||||
| 	source: any; // TODO | ||||
| 	targetElement: HTMLElement; | ||||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'closed'): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -109,7 +109,7 @@ export default defineComponent({ | |||
| 				text: 'Delete some bananas', | ||||
| 				danger: true, | ||||
| 				action: () => {}, | ||||
| 			}], ev.currentTarget || ev.target); | ||||
| 			}], ev.currentTarget ?? ev.target); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -1,88 +1,71 @@ | |||
| <template> | ||||
| <transition :name="$store.state.animation ? 'fade' : ''" appear> | ||||
| 	<div class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> | ||||
| 	<div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> | ||||
| 		<MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/> | ||||
| 	</div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, onBeforeUnmount } from 'vue'; | ||||
| import contains from '@/scripts/contains'; | ||||
| import MkMenu from './menu.vue'; | ||||
| import { MenuItem } from './types/menu.vue'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkMenu, | ||||
| 	}, | ||||
| 	props: { | ||||
| 		items: { | ||||
| 			type: Array, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		ev: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 		viaKeyboard: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 	}, | ||||
| 	emits: ['closed'], | ||||
| 	data() { | ||||
| 		return { | ||||
| 			zIndex: os.claimZIndex('high'), | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		keymap(): any { | ||||
| 			return { | ||||
| 				'esc': () => this.$emit('closed'), | ||||
| 			}; | ||||
| 		}, | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		let left = this.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 | ||||
| 		let top = this.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 | ||||
| const props = defineProps<{ | ||||
| 	items: MenuItem[]; | ||||
| 	ev: MouseEvent; | ||||
| }>(); | ||||
| 
 | ||||
| 		const width = this.$el.offsetWidth; | ||||
| 		const height = this.$el.offsetHeight; | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'closed'): void; | ||||
| }>(); | ||||
| 
 | ||||
| 		if (left + width - window.pageXOffset > window.innerWidth) { | ||||
| 			left = window.innerWidth - width + window.pageXOffset; | ||||
| 		} | ||||
| let rootEl = $ref<HTMLDivElement>(); | ||||
| 
 | ||||
| 		if (top + height - window.pageYOffset > window.innerHeight) { | ||||
| 			top = window.innerHeight - height + window.pageYOffset; | ||||
| 		} | ||||
| let zIndex = $ref<number>(os.claimZIndex('high')); | ||||
| 
 | ||||
| 		if (top < 0) { | ||||
| 			top = 0; | ||||
| 		} | ||||
| onMounted(() => { | ||||
| 	let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 | ||||
| 	let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1 | ||||
| 
 | ||||
| 		if (left < 0) { | ||||
| 			left = 0; | ||||
| 		} | ||||
| 	const width = rootEl.offsetWidth; | ||||
| 	const height = rootEl.offsetHeight; | ||||
| 
 | ||||
| 		this.$el.style.top = top + 'px'; | ||||
| 		this.$el.style.left = left + 'px'; | ||||
| 	if (left + width - window.pageXOffset > window.innerWidth) { | ||||
| 		left = window.innerWidth - width + window.pageXOffset; | ||||
| 	} | ||||
| 
 | ||||
| 		for (const el of Array.from(document.querySelectorAll('body *'))) { | ||||
| 			el.addEventListener('mousedown', this.onMousedown); | ||||
| 		} | ||||
| 	}, | ||||
| 	beforeUnmount() { | ||||
| 		for (const el of Array.from(document.querySelectorAll('body *'))) { | ||||
| 			el.removeEventListener('mousedown', this.onMousedown); | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		onMousedown(e) { | ||||
| 			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.$emit('closed'); | ||||
| 		}, | ||||
| 	if (top + height - window.pageYOffset > window.innerHeight) { | ||||
| 		top = window.innerHeight - height + window.pageYOffset; | ||||
| 	} | ||||
| 
 | ||||
| 	if (top < 0) { | ||||
| 		top = 0; | ||||
| 	} | ||||
| 
 | ||||
| 	if (left < 0) { | ||||
| 		left = 0; | ||||
| 	} | ||||
| 
 | ||||
| 	rootEl.style.top = `${top}px`; | ||||
| 	rootEl.style.left = `${left}px`; | ||||
| 
 | ||||
| 	for (const el of Array.from(document.querySelectorAll('body *'))) { | ||||
| 		el.addEventListener('mousedown', onMousedown); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| onBeforeUnmount(() => { | ||||
| 	for (const el of Array.from(document.querySelectorAll('body *'))) { | ||||
| 		el.removeEventListener('mousedown', onMousedown); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| function onMousedown(e: Event) { | ||||
| 	if (!contains(rootEl, e.target) && (rootEl != e.target)) emit('closed'); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| <template> | ||||
| <div ref="items" v-hotkey="keymap" | ||||
| <div ref="itemsEl" v-hotkey="keymap" | ||||
| 	class="rrevdjwt" | ||||
| 	:class="{ center: align === 'center', asDrawer }" | ||||
| 	:style="{ width: (width && !asDrawer) ? width + 'px' : null, maxHeight: maxHeight ? maxHeight + 'px' : null }" | ||||
| 	:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" | ||||
| 	@contextmenu.self="e => e.preventDefault()" | ||||
| > | ||||
| 	<template v-for="(item, i) in items2"> | ||||
|  | @ -28,6 +28,9 @@ | |||
| 			<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/> | ||||
| 			<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span> | ||||
| 		</button> | ||||
| 		<span v-else-if="item.type === 'switch'" :tabindex="i" class="item"> | ||||
| 			<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch> | ||||
| 		</span> | ||||
| 		<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)"> | ||||
| 			<i v-if="item.icon" class="fa-fw" :class="item.icon"></i> | ||||
| 			<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> | ||||
|  | @ -41,114 +44,78 @@ | |||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, ref, unref } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { nextTick, onMounted, watch } from 'vue'; | ||||
| import { focusPrev, focusNext } from '@/scripts/focus'; | ||||
| import contains from '@/scripts/contains'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		items: { | ||||
| 			type: Array, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		viaKeyboard: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		asDrawer: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		align: { | ||||
| 			type: String, | ||||
| 			requried: false | ||||
| 		}, | ||||
| 		width: { | ||||
| 			type: Number, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		maxHeight: { | ||||
| 			type: Number, | ||||
| 			required: false | ||||
| 		}, | ||||
| 	}, | ||||
| 	emits: ['close'], | ||||
| 	data() { | ||||
| 		return { | ||||
| 			items2: [], | ||||
| 		}; | ||||
| 	}, | ||||
| 	computed: { | ||||
| 		keymap(): any { | ||||
| 			return { | ||||
| 				'up|k|shift+tab': this.focusUp, | ||||
| 				'down|j|tab': this.focusDown, | ||||
| 				'esc': this.close, | ||||
| 			}; | ||||
| 		}, | ||||
| 	}, | ||||
| 	watch: { | ||||
| 		items: { | ||||
| 			handler() { | ||||
| 				const items = ref(unref(this.items).filter(item => item !== undefined)); | ||||
| const props = defineProps<{ | ||||
| 	items: MenuItem[]; | ||||
| 	viaKeyboard?: boolean; | ||||
| 	asDrawer?: boolean; | ||||
| 	align?: 'center' | string; | ||||
| 	width?: number; | ||||
| 	maxHeight?: number; | ||||
| }>(); | ||||
| 
 | ||||
| 				for (let i = 0; i < items.value.length; i++) { | ||||
| 					const item = items.value[i]; | ||||
| 					 | ||||
| 					if (item && item.then) { // if item is Promise | ||||
| 						items.value[i] = { type: 'pending' }; | ||||
| 						item.then(actualItem => { | ||||
| 							items.value[i] = actualItem; | ||||
| 						}); | ||||
| 					} | ||||
| 				} | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'close'): void; | ||||
| }>(); | ||||
| 
 | ||||
| 				this.items2 = items; | ||||
| 			}, | ||||
| 			immediate: true | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		if (this.viaKeyboard) { | ||||
| 			this.$nextTick(() => { | ||||
| 				focusNext(this.$refs.items.children[0], true, false); | ||||
| let itemsEl = $ref<HTMLDivElement>(); | ||||
| 
 | ||||
| let items2: InnerMenuItem[] = $ref([]); | ||||
| 
 | ||||
| let keymap = $computed(() => ({ | ||||
| 	'up|k|shift+tab': focusUp, | ||||
| 	'down|j|tab': focusDown, | ||||
| 	'esc': close, | ||||
| })); | ||||
| 
 | ||||
| watch(() => props.items, () => { | ||||
| 	const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined); | ||||
| 
 | ||||
| 	for (let i = 0; i < items.length; i++) { | ||||
| 		const item = items[i]; | ||||
| 
 | ||||
| 		if (item && 'then' in item) { // if item is Promise | ||||
| 			items[i] = { type: 'pending' }; | ||||
| 			item.then(actualItem => { | ||||
| 				items2[i] = actualItem; | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 		if (this.contextmenuEvent) { | ||||
| 			this.$el.style.top = this.contextmenuEvent.pageY + 'px'; | ||||
| 			this.$el.style.left = this.contextmenuEvent.pageX + 'px'; | ||||
| 	items2 = items as InnerMenuItem[]; | ||||
| }, { | ||||
| 	immediate: true, | ||||
| }); | ||||
| 
 | ||||
| 			for (const el of Array.from(document.querySelectorAll('body *'))) { | ||||
| 				el.addEventListener('mousedown', this.onMousedown); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
| 	beforeUnmount() { | ||||
| 		for (const el of Array.from(document.querySelectorAll('body *'))) { | ||||
| 			el.removeEventListener('mousedown', this.onMousedown); | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		clicked(fn, ev) { | ||||
| 			fn(ev); | ||||
| 			this.close(); | ||||
| 		}, | ||||
| 		close() { | ||||
| 			this.$emit('close'); | ||||
| 		}, | ||||
| 		focusUp() { | ||||
| 			focusPrev(document.activeElement); | ||||
| 		}, | ||||
| 		focusDown() { | ||||
| 			focusNext(document.activeElement); | ||||
| 		}, | ||||
| 		onMousedown(e) { | ||||
| 			if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close(); | ||||
| 		}, | ||||
| onMounted(() => { | ||||
| 	if (props.viaKeyboard) { | ||||
| 		nextTick(() => { | ||||
| 			focusNext(itemsEl.children[0], true, false); | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| function clicked(fn: MenuAction, ev: MouseEvent) { | ||||
| 	fn(ev); | ||||
| 	close(); | ||||
| } | ||||
| 
 | ||||
| function close() { | ||||
| 	emit('close'); | ||||
| } | ||||
| 
 | ||||
| function focusUp() { | ||||
| 	focusPrev(document.activeElement); | ||||
| } | ||||
| 
 | ||||
| function focusDown() { | ||||
| 	focusNext(document.activeElement); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="$emit('closed')" @enter="$emit('opening')" @after-enter="childRendered"> | ||||
| <transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="childRendered"> | ||||
| 	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> | ||||
| 		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> | ||||
| 		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick"> | ||||
|  | @ -9,8 +9,8 @@ | |||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, nextTick, onMounted, computed, PropType, ref, watch } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { nextTick, onMounted, computed, ref, watch, provide } from 'vue'; | ||||
| import * as os from '@/os'; | ||||
| import { isTouchUsing } from '@/scripts/touch'; | ||||
| import { defaultStore } from '@/store'; | ||||
|  | @ -25,234 +25,206 @@ function getFixedContainer(el: Element | null): Element | null { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	provide: { | ||||
| 		modal: true | ||||
| 	}, | ||||
| type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer'; | ||||
| 
 | ||||
| 	props: { | ||||
| 		manualShowing: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: null, | ||||
| 		}, | ||||
| 		srcCenter: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		src: { | ||||
| 			type: Object as PropType<HTMLElement>, | ||||
| 			required: false, | ||||
| 			default: null, | ||||
| 		}, | ||||
| 		preferType: { | ||||
| 			required: false, | ||||
| 			type: String, | ||||
| 			default: 'auto', | ||||
| 		}, | ||||
| 		zPriority: { | ||||
| 			type: String as PropType<'low' | 'middle' | 'high'>, | ||||
| 			required: false, | ||||
| 			default: 'low', | ||||
| 		}, | ||||
| 		noOverlap: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: true, | ||||
| 		}, | ||||
| 		transparentBg: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}, | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	manualShowing?: boolean | null; | ||||
| 	srcCenter?: boolean; | ||||
| 	src?: HTMLElement; | ||||
| 	preferType?: ModalTypes | 'auto'; | ||||
| 	zPriority?: 'low' | 'middle' | 'high'; | ||||
| 	noOverlap?: boolean; | ||||
| 	transparentBg?: boolean; | ||||
| }>(), { | ||||
| 	manualShowing: null, | ||||
| 	src: null, | ||||
| 	preferType: 'auto', | ||||
| 	zPriority: 'low', | ||||
| 	noOverlap: true, | ||||
| 	transparentBg: false, | ||||
| }); | ||||
| 
 | ||||
| 	emits: ['opening', 'click', 'esc', 'close', 'closed'], | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'opening'): void; | ||||
| 	(ev: 'click'): void; | ||||
| 	(ev: 'esc'): void; | ||||
| 	(ev: 'close'): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| 
 | ||||
| 	setup(props, context) { | ||||
| 		const maxHeight = ref<number>(); | ||||
| 		const fixed = ref(false); | ||||
| 		const transformOrigin = ref('center'); | ||||
| 		const showing = ref(true); | ||||
| 		const content = ref<HTMLElement>(); | ||||
| 		const zIndex = os.claimZIndex(props.zPriority); | ||||
| 		const type = computed(() => { | ||||
| 			if (props.preferType === 'auto') { | ||||
| 				if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) { | ||||
| 					return 'drawer'; | ||||
| 				} else { | ||||
| 					return props.src != null ? 'popup' : 'dialog'; | ||||
| 				} | ||||
| 			} else { | ||||
| 				return props.preferType; | ||||
| 			} | ||||
| 		}); | ||||
| 		 | ||||
| 		let contentClicking = false; | ||||
| provide('modal', true); | ||||
| 
 | ||||
| 		const close = () => { | ||||
| 			// eslint-disable-next-line vue/no-mutating-props | ||||
| 			if (props.src) props.src.style.pointerEvents = 'auto'; | ||||
| 			showing.value = false; | ||||
| 			context.emit('close'); | ||||
| 		}; | ||||
| const maxHeight = ref<number>(); | ||||
| const fixed = ref(false); | ||||
| const transformOrigin = ref('center'); | ||||
| const showing = ref(true); | ||||
| const content = ref<HTMLElement>(); | ||||
| const zIndex = os.claimZIndex(props.zPriority); | ||||
| const type = computed(() => { | ||||
| 	if (props.preferType === 'auto') { | ||||
| 		if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) { | ||||
| 			return 'drawer'; | ||||
| 		} else { | ||||
| 			return props.src != null ? 'popup' : 'dialog'; | ||||
| 		} | ||||
| 	} else { | ||||
| 		return props.preferType!; | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| 		const onBgClick = () => { | ||||
| 			if (contentClicking) return; | ||||
| 			context.emit('click'); | ||||
| 		}; | ||||
| let contentClicking = false; | ||||
| 
 | ||||
| 		if (type.value === 'drawer') { | ||||
| 			maxHeight.value = window.innerHeight / 2; | ||||
| const close = () => { | ||||
| 	// eslint-disable-next-line vue/no-mutating-props | ||||
| 	if (props.src) props.src.style.pointerEvents = 'auto'; | ||||
| 	showing.value = false; | ||||
| 	emit('close'); | ||||
| }; | ||||
| 
 | ||||
| const onBgClick = () => { | ||||
| 	if (contentClicking) return; | ||||
| 	emit('click'); | ||||
| }; | ||||
| 
 | ||||
| if (type.value === 'drawer') { | ||||
| 	maxHeight.value = window.innerHeight / 2; | ||||
| } | ||||
| 
 | ||||
| const keymap = { | ||||
| 	'esc': () => emit('esc'), | ||||
| }; | ||||
| 
 | ||||
| const MARGIN = 16; | ||||
| 
 | ||||
| const align = () => { | ||||
| 	if (props.src == null) return; | ||||
| 	if (type.value === 'drawer') return; | ||||
| 
 | ||||
| 	const popover = content.value!; | ||||
| 
 | ||||
| 	if (popover == null) return; | ||||
| 
 | ||||
| 	const rect = props.src.getBoundingClientRect(); | ||||
| 	 | ||||
| 	const width = popover.offsetWidth; | ||||
| 	const height = popover.offsetHeight; | ||||
| 
 | ||||
| 	let left; | ||||
| 	let top; | ||||
| 
 | ||||
| 	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 + (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 (fixed.value) { | ||||
| 		// 画面から横にはみ出る場合 | ||||
| 		if (left + width > window.innerWidth) { | ||||
| 			left = window.innerWidth - width; | ||||
| 		} | ||||
| 
 | ||||
| 		const keymap = { | ||||
| 			'esc': () => context.emit('esc'), | ||||
| 		}; | ||||
| 
 | ||||
| 		const MARGIN = 16; | ||||
| 
 | ||||
| 		const align = () => { | ||||
| 			if (props.src == null) return; | ||||
| 			if (type.value === 'drawer') return; | ||||
| 
 | ||||
| 			const popover = content.value!; | ||||
| 
 | ||||
| 			if (popover == null) return; | ||||
| 
 | ||||
| 			const rect = props.src.getBoundingClientRect(); | ||||
| 			 | ||||
| 			const width = popover.offsetWidth; | ||||
| 			const height = popover.offsetHeight; | ||||
| 
 | ||||
| 			let left; | ||||
| 			let top; | ||||
| 
 | ||||
| 			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 + (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 (fixed.value) { | ||||
| 				// 画面から横にはみ出る場合 | ||||
| 				if (left + width > window.innerWidth) { | ||||
| 					left = window.innerWidth - width; | ||||
| 				} | ||||
| 
 | ||||
| 				// 画面から縦にはみ出る場合 | ||||
| 				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; | ||||
| 					} | ||||
| 		// 画面から縦にはみ出る場合 | ||||
| 		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 { | ||||
| 				// 画面から横にはみ出る場合 | ||||
| 				if (left + width - window.pageXOffset > window.innerWidth) { | ||||
| 					left = window.innerWidth - width + window.pageXOffset - 1; | ||||
| 				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 - 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); | ||||
| 				} | ||||
| 
 | ||||
| 				// 画面から縦にはみ出る場合 | ||||
| 				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 = MARGIN; | ||||
| 			} | ||||
| 
 | ||||
| 			if (left < 0) { | ||||
| 				left = 0; | ||||
| 			} | ||||
| 
 | ||||
| 			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 { | ||||
| 				transformOrigin.value = 'center'; | ||||
| 				top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 			popover.style.left = left + 'px'; | ||||
| 			popover.style.top = top + 'px'; | ||||
| 		}; | ||||
| 	if (top < 0) { | ||||
| 		top = MARGIN; | ||||
| 	} | ||||
| 
 | ||||
| 		const childRendered = () => { | ||||
| 			// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する | ||||
| 			const el = content.value!.children[0]; | ||||
| 			el.addEventListener('mousedown', e => { | ||||
| 				contentClicking = true; | ||||
| 				window.addEventListener('mouseup', e => { | ||||
| 					// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ | ||||
| 					window.setTimeout(() => { | ||||
| 						contentClicking = false; | ||||
| 					}, 100); | ||||
| 				}, { passive: true, once: true }); | ||||
| 			}, { passive: true }); | ||||
| 		}; | ||||
| 	if (left < 0) { | ||||
| 		left = 0; | ||||
| 	} | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			watch(() => props.src, async () => { | ||||
| 				if (props.src) { | ||||
| 					// eslint-disable-next-line vue/no-mutating-props | ||||
| 					props.src.style.pointerEvents = 'none'; | ||||
| 				} | ||||
| 				fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null); | ||||
| 	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 { | ||||
| 		transformOrigin.value = 'center'; | ||||
| 	} | ||||
| 
 | ||||
| 				await nextTick() | ||||
| 				 | ||||
| 				align(); | ||||
| 			}, { immediate: true, }); | ||||
| 	popover.style.left = left + 'px'; | ||||
| 	popover.style.top = top + 'px'; | ||||
| }; | ||||
| 
 | ||||
| 			nextTick(() => { | ||||
| 				const popover = content.value; | ||||
| 				new ResizeObserver((entries, observer) => { | ||||
| 					align(); | ||||
| 				}).observe(popover!); | ||||
| 			}); | ||||
| 		}); | ||||
| const childRendered = () => { | ||||
| 	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する | ||||
| 	const el = content.value!.children[0]; | ||||
| 	el.addEventListener('mousedown', ev => { | ||||
| 		contentClicking = true; | ||||
| 		window.addEventListener('mouseup', ev => { | ||||
| 			// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ | ||||
| 			window.setTimeout(() => { | ||||
| 				contentClicking = false; | ||||
| 			}, 100); | ||||
| 		}, { passive: true, once: true }); | ||||
| 	}, { passive: true }); | ||||
| }; | ||||
| 
 | ||||
| 		return { | ||||
| 			showing, | ||||
| 			type, | ||||
| 			fixed, | ||||
| 			content, | ||||
| 			transformOrigin, | ||||
| 			maxHeight, | ||||
| 			close, | ||||
| 			zIndex, | ||||
| 			keymap, | ||||
| 			onBgClick, | ||||
| 			childRendered, | ||||
| 		}; | ||||
| 	}, | ||||
| onMounted(() => { | ||||
| 	watch(() => props.src, async () => { | ||||
| 		if (props.src) { | ||||
| 			// eslint-disable-next-line vue/no-mutating-props | ||||
| 			props.src.style.pointerEvents = 'none'; | ||||
| 		} | ||||
| 		fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null); | ||||
| 
 | ||||
| 		await nextTick() | ||||
| 		 | ||||
| 		align(); | ||||
| 	}, { immediate: true, }); | ||||
| 
 | ||||
| 	nextTick(() => { | ||||
| 		const popover = content.value; | ||||
| 		new ResizeObserver((entries, observer) => { | ||||
| 			align(); | ||||
| 		}).observe(popover!); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	close, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,44 +1,28 @@ | |||
| <template> | ||||
| <MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="$refs.modal.close()" @closed="$emit('closed')"> | ||||
| 	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="$refs.modal.close()"/> | ||||
| <MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')"> | ||||
| 	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq _popup _shadow" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/> | ||||
| </MkModal> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import MkModal from './modal.vue'; | ||||
| import MkMenu from './menu.vue'; | ||||
| import { MenuItem } from '@/types/menu'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkModal, | ||||
| 		MkMenu, | ||||
| 	}, | ||||
| defineProps<{ | ||||
| 	items: MenuItem[]; | ||||
| 	align?: 'center' | string; | ||||
| 	width?: number; | ||||
| 	viaKeyboard?: boolean; | ||||
| 	src?: any; | ||||
| }>(); | ||||
| 
 | ||||
| 	props: { | ||||
| 		items: { | ||||
| 			type: Array, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		align: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		width: { | ||||
| 			type: Number, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		viaKeyboard: { | ||||
| 			type: Boolean, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		src: { | ||||
| 			required: false | ||||
| 		}, | ||||
| 	}, | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'closed'): void; | ||||
| }>(); | ||||
| 
 | ||||
| 	emits: ['close', 'closed'], | ||||
| }); | ||||
| let modal = $ref<InstanceType<typeof MkModal>>(); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  |  | |||
|  | @ -1,99 +1,96 @@ | |||
| <template> | ||||
| <transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="$emit('closed')"> | ||||
| <transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="emit('closed')"> | ||||
| 	<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }"> | ||||
| 		<slot>{{ text }}</slot> | ||||
| 	</div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue'; | ||||
| <script lang="ts" setup> | ||||
| import { nextTick, onMounted, onUnmounted, ref } from 'vue'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
| 		showing: { | ||||
| 			type: Boolean, | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		source: { | ||||
| 			required: true, | ||||
| 		}, | ||||
| 		text: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 		maxWidth: { | ||||
| 			type: Number, | ||||
| 			required: false, | ||||
| 			default: 250, | ||||
| 		}, | ||||
| 	}, | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	showing: boolean; | ||||
| 	targetElement?: HTMLElement; | ||||
| 	x?: number; | ||||
| 	y?: number; | ||||
| 	text?: string; | ||||
| 	maxWidth?: number; | ||||
| }>(), { | ||||
| 	maxWidth: 250, | ||||
| }); | ||||
| 
 | ||||
| 	emits: ['closed'], | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| 
 | ||||
| 	setup(props, context) { | ||||
| 		const el = ref<HTMLElement>(); | ||||
| 		const zIndex = os.claimZIndex('high'); | ||||
| const el = ref<HTMLElement>(); | ||||
| const zIndex = os.claimZIndex('high'); | ||||
| 
 | ||||
| 		const setPosition = () => { | ||||
| 			if (el.value == null) return; | ||||
| const setPosition = () => { | ||||
| 	if (el.value == null) return; | ||||
| 
 | ||||
| 			const rect = props.source.getBoundingClientRect(); | ||||
| 	const contentWidth = el.value.offsetWidth; | ||||
| 	const contentHeight = el.value.offsetHeight; | ||||
| 
 | ||||
| 			const contentWidth = el.value.offsetWidth; | ||||
| 			const contentHeight = el.value.offsetHeight; | ||||
| 	let left: number; | ||||
| 	let top: number; | ||||
| 
 | ||||
| 			let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2); | ||||
| 			let top = rect.top + window.pageYOffset - contentHeight; | ||||
| 	let rect: DOMRect; | ||||
| 
 | ||||
| 			left -= (el.value.offsetWidth / 2); | ||||
| 	if (props.targetElement) { | ||||
| 		rect = props.targetElement.getBoundingClientRect(); | ||||
| 
 | ||||
| 			if (left + contentWidth - window.pageXOffset > window.innerWidth) { | ||||
| 				left = window.innerWidth - contentWidth + window.pageXOffset - 1; | ||||
| 			} | ||||
| 		left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); | ||||
| 		top = rect.top + window.pageYOffset - contentHeight; | ||||
| 
 | ||||
| 			if (top - window.pageYOffset < 0) { | ||||
| 				top = rect.top + window.pageYOffset + props.source.offsetHeight; | ||||
| 				el.value.style.transformOrigin = 'center top'; | ||||
| 			} | ||||
| 		el.value.style.transformOrigin = 'center bottom'; | ||||
| 	} else { | ||||
| 		left = props.x; | ||||
| 		top = props.y - contentHeight; | ||||
| 	} | ||||
| 
 | ||||
| 			el.value.style.left = left + 'px'; | ||||
| 			el.value.style.top = top + 'px'; | ||||
| 		}; | ||||
| 	left -= (el.value.offsetWidth / 2); | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			nextTick(() => { | ||||
| 				if (props.source == null) { | ||||
| 					context.emit('closed'); | ||||
| 					return; | ||||
| 				} | ||||
| 	if (left + contentWidth - window.pageXOffset > window.innerWidth) { | ||||
| 		left = window.innerWidth - contentWidth + window.pageXOffset - 1; | ||||
| 	} | ||||
| 
 | ||||
| 	// ツールチップを上に向かって表示するスペースがなければ下に向かって出す | ||||
| 	if (top - window.pageYOffset < 0) { | ||||
| 		if (props.targetElement) { | ||||
| 			top = rect.top + window.pageYOffset + props.targetElement.offsetHeight; | ||||
| 			el.value.style.transformOrigin = 'center top'; | ||||
| 		} else { | ||||
| 			top = props.y; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	el.value.style.left = left + 'px'; | ||||
| 	el.value.style.top = top + 'px'; | ||||
| }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	nextTick(() => { | ||||
| 		setPosition(); | ||||
| 
 | ||||
| 		let loopHandler; | ||||
| 
 | ||||
| 		const loop = () => { | ||||
| 			loopHandler = window.requestAnimationFrame(() => { | ||||
| 				setPosition(); | ||||
| 
 | ||||
| 				let loopHandler; | ||||
| 
 | ||||
| 				const loop = () => { | ||||
| 					loopHandler = window.requestAnimationFrame(() => { | ||||
| 						setPosition(); | ||||
| 						loop(); | ||||
| 					}); | ||||
| 				}; | ||||
| 
 | ||||
| 				loop(); | ||||
| 
 | ||||
| 				onUnmounted(() => { | ||||
| 					window.cancelAnimationFrame(loopHandler); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			el, | ||||
| 			zIndex, | ||||
| 		}; | ||||
| 	}, | ||||
| }) | ||||
| 
 | ||||
| 		loop(); | ||||
| 
 | ||||
| 		onUnmounted(() => { | ||||
| 			window.cancelAnimationFrame(loopHandler); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|  | @ -118,6 +115,6 @@ export default defineComponent({ | |||
| 	border-radius: 4px; | ||||
| 	border: solid 0.5px var(--divider); | ||||
| 	pointer-events: none; | ||||
| 	transform-origin: center bottom; | ||||
| 	transform-origin: center center; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -13,10 +13,10 @@ const props = defineProps<{ | |||
| 
 | ||||
| const text = $computed(() => { | ||||
| 	switch (props.user.onlineStatus) { | ||||
| 		case 'online': return i18n.locale.online; | ||||
| 		case 'active': return i18n.locale.active; | ||||
| 		case 'offline': return i18n.locale.offline; | ||||
| 		case 'unknown': return i18n.locale.unknown; | ||||
| 		case 'online': return i18n.ts.online; | ||||
| 		case 'active': return i18n.ts.active; | ||||
| 		case 'offline': return i18n.ts.offline; | ||||
| 		case 'unknown': return i18n.ts.unknown; | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ export default { | |||
| 			popup(import('@/components/ui/tooltip.vue'), { | ||||
| 				showing, | ||||
| 				text: self.text, | ||||
| 				source: el | ||||
| 				targetElement: el, | ||||
| 			}, {}, 'closed'); | ||||
| 
 | ||||
| 			self._close = () => { | ||||
|  | @ -56,8 +56,8 @@ export default { | |||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		el.addEventListener('selectstart', e => { | ||||
| 			e.preventDefault(); | ||||
| 		el.addEventListener('selectstart', ev => { | ||||
| 			ev.preventDefault(); | ||||
| 		}); | ||||
| 
 | ||||
| 		el.addEventListener(start, () => { | ||||
|  |  | |||
|  | @ -185,7 +185,7 @@ app.config.globalProperties = { | |||
| 	$store: defaultStore, | ||||
| 	$instance: instance, | ||||
| 	$t: i18n.t, | ||||
| 	$ts: i18n.locale, | ||||
| 	$ts: i18n.ts, | ||||
| }; | ||||
| 
 | ||||
| app.use(router); | ||||
|  | @ -299,8 +299,8 @@ stream.on('_disconnected_', async () => { | |||
| 		reloadDialogShowing = true; | ||||
| 		const { canceled } = await confirm({ | ||||
| 			type: 'warning', | ||||
| 			title: i18n.locale.disconnectedFromServer, | ||||
| 			text: i18n.locale.reloadConfirm, | ||||
| 			title: i18n.ts.disconnectedFromServer, | ||||
| 			text: i18n.ts.reloadConfirm, | ||||
| 		}); | ||||
| 		reloadDialogShowing = false; | ||||
| 		if (!canceled) { | ||||
|  | @ -324,7 +324,7 @@ if ($i) { | |||
| 	if ($i.isDeleted) { | ||||
| 		alert({ | ||||
| 			type: 'warning', | ||||
| 			text: i18n.locale.accountDeletionInProgress, | ||||
| 			text: i18n.ts.accountDeletionInProgress, | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -73,12 +73,12 @@ export const menuDef = reactive({ | |||
| 				})), null, { | ||||
| 					type: 'link', | ||||
| 					to: '/my/lists', | ||||
| 					text: i18n.locale.manageLists, | ||||
| 					text: i18n.ts.manageLists, | ||||
| 					icon: 'fas fa-cog', | ||||
| 				}]; | ||||
| 				items.value = _items; | ||||
| 			}); | ||||
| 			os.popupMenu(items, ev.currentTarget || ev.target); | ||||
| 			os.popupMenu(items, ev.currentTarget ?? ev.target); | ||||
| 		}, | ||||
| 	}, | ||||
| 	groups: { | ||||
|  | @ -104,12 +104,12 @@ export const menuDef = reactive({ | |||
| 				})), null, { | ||||
| 					type: 'link', | ||||
| 					to: '/my/antennas', | ||||
| 					text: i18n.locale.manageAntennas, | ||||
| 					text: i18n.ts.manageAntennas, | ||||
| 					icon: 'fas fa-cog', | ||||
| 				}]; | ||||
| 				items.value = _items; | ||||
| 			}); | ||||
| 			os.popupMenu(items, ev.currentTarget || ev.target); | ||||
| 			os.popupMenu(items, ev.currentTarget ?? ev.target); | ||||
| 		}, | ||||
| 	}, | ||||
| 	mentions: { | ||||
|  | @ -173,34 +173,34 @@ export const menuDef = reactive({ | |||
| 		icon: 'fas fa-columns', | ||||
| 		action: (ev) => { | ||||
| 			os.popupMenu([{ | ||||
| 				text: i18n.locale.default, | ||||
| 				text: i18n.ts.default, | ||||
| 				active: ui === 'default' || ui === null, | ||||
| 				action: () => { | ||||
| 					localStorage.setItem('ui', 'default'); | ||||
| 					unisonReload(); | ||||
| 				} | ||||
| 			}, { | ||||
| 				text: i18n.locale.deck, | ||||
| 				text: i18n.ts.deck, | ||||
| 				active: ui === 'deck', | ||||
| 				action: () => { | ||||
| 					localStorage.setItem('ui', 'deck'); | ||||
| 					unisonReload(); | ||||
| 				} | ||||
| 			}, { | ||||
| 				text: i18n.locale.classic, | ||||
| 				text: i18n.ts.classic, | ||||
| 				active: ui === 'classic', | ||||
| 				action: () => { | ||||
| 					localStorage.setItem('ui', 'classic'); | ||||
| 					unisonReload(); | ||||
| 				} | ||||
| 			}, /*{ | ||||
| 				text: i18n.locale.desktop + ' (β)', | ||||
| 				text: i18n.ts.desktop + ' (β)', | ||||
| 				active: ui === 'desktop', | ||||
| 				action: () => { | ||||
| 					localStorage.setItem('ui', 'desktop'); | ||||
| 					unisonReload(); | ||||
| 				} | ||||
| 			}*/], ev.currentTarget || ev.target); | ||||
| 			}*/], ev.currentTarget ?? ev.target); | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
|  |  | |||
|  | @ -7,8 +7,10 @@ import * as Misskey from 'misskey-js'; | |||
| import { apiUrl, url } from '@/config'; | ||||
| import MkPostFormDialog from '@/components/post-form-dialog.vue'; | ||||
| import MkWaitingDialog from '@/components/waiting-dialog.vue'; | ||||
| import { MenuItem } from '@/types/menu'; | ||||
| import { resolve } from '@/router'; | ||||
| import { $i } from '@/account'; | ||||
| import { defaultStore } from '@/store'; | ||||
| 
 | ||||
| export const pendingApiRequestsCount = ref(0); | ||||
| 
 | ||||
|  | @ -403,7 +405,7 @@ export async function selectDriveFolder(multiple: boolean) { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| export async function pickEmoji(src?: HTMLElement, opts) { | ||||
| export async function pickEmoji(src: HTMLElement | null, opts) { | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		popup(import('@/components/emoji-picker-dialog.vue'), { | ||||
| 			src, | ||||
|  | @ -470,7 +472,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { | ||||
| export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement, options?: { | ||||
| 	align?: string; | ||||
| 	width?: number; | ||||
| 	viaKeyboard?: boolean; | ||||
|  | @ -494,7 +496,7 @@ export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options? | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| export function contextMenu(items: any[], ev: MouseEvent) { | ||||
| export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) { | ||||
| 	ev.preventDefault(); | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		let dispose; | ||||
|  | @ -541,7 +543,7 @@ export const uploads = ref<{ | |||
| 	img: string; | ||||
| }[]>([]); | ||||
| 
 | ||||
| export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> { | ||||
| export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise<Misskey.entities.DriveFile> { | ||||
| 	if (folder && typeof folder == 'object') folder = folder.id; | ||||
| 
 | ||||
| 	return new Promise((resolve, reject) => { | ||||
|  | @ -559,6 +561,8 @@ export function upload(file: File, folder?: any, name?: string): Promise<Misskey | |||
| 
 | ||||
| 			uploads.value.push(ctx); | ||||
| 
 | ||||
| 			console.log(keepOriginal); | ||||
| 
 | ||||
| 			const data = new FormData(); | ||||
| 			data.append('i', $i.token); | ||||
| 			data.append('force', 'true'); | ||||
|  |  | |||
|  | @ -3,15 +3,15 @@ | |||
| <transition :name="$store.state.animation ? 'zoom' : ''" appear> | ||||
| 	<div v-show="loaded" class="mjndxjch"> | ||||
| 		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> | ||||
| 		<p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.locale.pageLoadError }}</b></p> | ||||
| 		<p v-if="meta && (version === meta.version)">{{ i18n.locale.pageLoadErrorDescription }}</p> | ||||
| 		<p v-else-if="serverIsDead">{{ i18n.locale.serverIsDead }}</p> | ||||
| 		<p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p> | ||||
| 		<p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p> | ||||
| 		<p v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</p> | ||||
| 		<template v-else> | ||||
| 			<p>{{ i18n.locale.newVersionOfClientAvailable }}</p> | ||||
| 			<p>{{ i18n.locale.youShouldUpgradeClient }}</p> | ||||
| 			<MkButton class="button primary" @click="reload">{{ i18n.locale.reload }}</MkButton> | ||||
| 			<p>{{ i18n.ts.newVersionOfClientAvailable }}</p> | ||||
| 			<p>{{ i18n.ts.youShouldUpgradeClient }}</p> | ||||
| 			<MkButton class="button primary" @click="reload">{{ i18n.ts.reload }}</MkButton> | ||||
| 		</template> | ||||
| 		<p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.locale.troubleshooting }}</MkA></p> | ||||
| 		<p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.ts.troubleshooting }}</MkA></p> | ||||
| 		<p v-if="error" class="error">ERROR: {{ error }}</p> | ||||
| 	</div> | ||||
| </transition> | ||||
|  | @ -54,7 +54,7 @@ function reload() { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.error, | ||||
| 		title: i18n.ts.error, | ||||
| 		icon: 'fas fa-exclamation-triangle', | ||||
| 	}, | ||||
| }); | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ | |||
| 				<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span> | ||||
| 			</div> | ||||
| 			<div class="_formBlock" style="text-align: center;"> | ||||
| 				{{ i18n.locale._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.locale.learnMore }}</a> | ||||
| 				{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a> | ||||
| 			</div> | ||||
| 			<div class="_formBlock" style="text-align: center;"> | ||||
| 				<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton> | ||||
|  | @ -19,23 +19,23 @@ | |||
| 				<div class="_formLinks"> | ||||
| 					<FormLink to="https://github.com/misskey-dev/misskey" external> | ||||
| 						<template #icon><i class="fas fa-code"></i></template> | ||||
| 						{{ i18n.locale._aboutMisskey.source }} | ||||
| 						{{ i18n.ts._aboutMisskey.source }} | ||||
| 						<template #suffix>GitHub</template> | ||||
| 					</FormLink> | ||||
| 					<FormLink to="https://crowdin.com/project/misskey" external> | ||||
| 						<template #icon><i class="fas fa-language"></i></template> | ||||
| 						{{ i18n.locale._aboutMisskey.translation }} | ||||
| 						{{ i18n.ts._aboutMisskey.translation }} | ||||
| 						<template #suffix>Crowdin</template> | ||||
| 					</FormLink> | ||||
| 					<FormLink to="https://www.patreon.com/syuilo" external> | ||||
| 						<template #icon><i class="fas fa-hand-holding-medical"></i></template> | ||||
| 						{{ i18n.locale._aboutMisskey.donate }} | ||||
| 						{{ i18n.ts._aboutMisskey.donate }} | ||||
| 						<template #suffix>Patreon</template> | ||||
| 					</FormLink> | ||||
| 				</div> | ||||
| 			</FormSection> | ||||
| 			<FormSection> | ||||
| 				<template #label>{{ i18n.locale._aboutMisskey.contributors }}</template> | ||||
| 				<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template> | ||||
| 				<div class="_formLinks"> | ||||
| 					<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink> | ||||
| 					<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink> | ||||
|  | @ -47,12 +47,12 @@ | |||
| 					<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink> | ||||
| 					<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink> | ||||
| 				</div> | ||||
| 				<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.locale._aboutMisskey.allContributors }}</MkLink></template> | ||||
| 				<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template> | ||||
| 			</FormSection> | ||||
| 			<FormSection> | ||||
| 				<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.locale._aboutMisskey.patrons }}</template> | ||||
| 				<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template> | ||||
| 				<div v-for="patron in patrons" :key="patron">{{ patron }}</div> | ||||
| 				<template #caption>{{ i18n.locale._aboutMisskey.morePatrons }}</template> | ||||
| 				<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template> | ||||
| 			</FormSection> | ||||
| 		</div> | ||||
| 	</MkSpacer> | ||||
|  | @ -194,7 +194,7 @@ onBeforeUnmount(() => { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.aboutMisskey, | ||||
| 		title: i18n.ts.aboutMisskey, | ||||
| 		icon: null, | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
|  |  | |||
|  | @ -90,7 +90,7 @@ const initStats = () => os.api('stats', { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.instanceInfo, | ||||
| 		title: i18n.ts.instanceInfo, | ||||
| 		icon: 'fas fa-info-circle', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
|  |  | |||
|  | @ -118,7 +118,7 @@ const toggleSelect = (emoji) => { | |||
| }; | ||||
| 
 | ||||
| const add = async (ev: MouseEvent) => { | ||||
| 	const files = await selectFiles(ev.currentTarget || ev.target, null); | ||||
| 	const files = await selectFiles(ev.currentTarget ?? ev.target, null); | ||||
| 
 | ||||
| 	const promise = Promise.all(files.map(file => os.api('admin/emoji/add', { | ||||
| 		fileId: file.id, | ||||
|  | @ -157,23 +157,23 @@ const remoteMenu = (emoji, ev: MouseEvent) => { | |||
| 		type: 'label', | ||||
| 		text: ':' + emoji.name + ':', | ||||
| 	}, { | ||||
| 		text: i18n.locale.import, | ||||
| 		text: i18n.ts.import, | ||||
| 		icon: 'fas fa-plus', | ||||
| 		action: () => { im(emoji) } | ||||
| 	}], ev.currentTarget || ev.target); | ||||
| 	}], ev.currentTarget ?? ev.target); | ||||
| }; | ||||
| 
 | ||||
| const menu = (ev: MouseEvent) => { | ||||
| 	os.popupMenu([{ | ||||
| 		icon: 'fas fa-download', | ||||
| 		text: i18n.locale.export, | ||||
| 		text: i18n.ts.export, | ||||
| 		action: async () => { | ||||
| 			os.api('export-custom-emojis', { | ||||
| 			}) | ||||
| 			.then(() => { | ||||
| 				os.alert({ | ||||
| 					type: 'info', | ||||
| 					text: i18n.locale.exportRequested, | ||||
| 					text: i18n.ts.exportRequested, | ||||
| 				}); | ||||
| 			}).catch((e) => { | ||||
| 				os.alert({ | ||||
|  | @ -184,16 +184,16 @@ const menu = (ev: MouseEvent) => { | |||
| 		} | ||||
| 	}, { | ||||
| 		icon: 'fas fa-upload', | ||||
| 		text: i18n.locale.import, | ||||
| 		text: i18n.ts.import, | ||||
| 		action: async () => { | ||||
| 			const file = await selectFile(ev.currentTarget || ev.target); | ||||
| 			const file = await selectFile(ev.currentTarget ?? ev.target); | ||||
| 			os.api('admin/emoji/import-zip', { | ||||
| 				fileId: file.id, | ||||
| 			}) | ||||
| 			.then(() => { | ||||
| 				os.alert({ | ||||
| 					type: 'info', | ||||
| 					text: i18n.locale.importRequested, | ||||
| 					text: i18n.ts.importRequested, | ||||
| 				}); | ||||
| 			}).catch((e) => { | ||||
| 				os.alert({ | ||||
|  | @ -202,7 +202,7 @@ const menu = (ev: MouseEvent) => { | |||
| 				}); | ||||
| 			}); | ||||
| 		} | ||||
| 	}], ev.currentTarget || ev.target); | ||||
| 	}], ev.currentTarget ?? ev.target); | ||||
| }; | ||||
| 
 | ||||
| const setCategoryBulk = async () => { | ||||
|  | @ -256,7 +256,7 @@ const setTagBulk = async () => { | |||
| const delBulk = async () => { | ||||
| 	const { canceled } = await os.confirm({ | ||||
| 		type: 'warning', | ||||
| 		text: i18n.locale.deleteConfirm, | ||||
| 		text: i18n.ts.deleteConfirm, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 	await os.apiWithDialog('admin/emoji/delete-bulk', { | ||||
|  | @ -267,13 +267,13 @@ const delBulk = async () => { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.locale.customEmojis, | ||||
| 		title: i18n.ts.customEmojis, | ||||
| 		icon: 'fas fa-laugh', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			asFullButton: true, | ||||
| 			icon: 'fas fa-plus', | ||||
| 			text: i18n.locale.addEmoji, | ||||
| 			text: i18n.ts.addEmoji, | ||||
| 			handler: add, | ||||
| 		}, { | ||||
| 			icon: 'fas fa-ellipsis-h', | ||||
|  | @ -281,11 +281,11 @@ defineExpose({ | |||
| 		}], | ||||
| 		tabs: [{ | ||||
| 			active: tab.value === 'local', | ||||
| 			title: i18n.locale.local, | ||||
| 			title: i18n.ts.local, | ||||
| 			onClick: () => { tab.value = 'local'; }, | ||||
| 		}, { | ||||
| 			active: tab.value === 'remote', | ||||
| 			title: i18n.locale.remote, | ||||
| 			title: i18n.ts.remote, | ||||
| 			onClick: () => { tab.value = 'remote'; }, | ||||
| 		},] | ||||
| 	})), | ||||
|  |  | |||
|  | @ -55,7 +55,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 	setup(props, context) { | ||||
| 		const indexInfo = { | ||||
| 			title: i18n.locale.controlPanel, | ||||
| 			title: i18n.ts.controlPanel, | ||||
| 			icon: 'fas fa-cog', | ||||
| 			bg: 'var(--bg)', | ||||
| 			hideHeader: true, | ||||
|  | @ -91,119 +91,119 @@ export default defineComponent({ | |||
| 		}); | ||||
| 
 | ||||
| 		const menuDef = computed(() => [{ | ||||
| 			title: i18n.locale.quickAction, | ||||
| 			title: i18n.ts.quickAction, | ||||
| 			items: [{ | ||||
| 				type: 'button', | ||||
| 				icon: 'fas fa-search', | ||||
| 				text: i18n.locale.lookup, | ||||
| 				text: i18n.ts.lookup, | ||||
| 				action: lookup, | ||||
| 			}, ...(instance.disableRegistration ? [{ | ||||
| 				type: 'button', | ||||
| 				icon: 'fas fa-user', | ||||
| 				text: i18n.locale.invite, | ||||
| 				text: i18n.ts.invite, | ||||
| 				action: invite, | ||||
| 			}] : [])], | ||||
| 		}, { | ||||
| 			title: i18n.locale.administration, | ||||
| 			title: i18n.ts.administration, | ||||
| 			items: [{ | ||||
| 				icon: 'fas fa-tachometer-alt', | ||||
| 				text: i18n.locale.dashboard, | ||||
| 				text: i18n.ts.dashboard, | ||||
| 				to: '/admin/overview', | ||||
| 				active: page.value === 'overview', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-users', | ||||
| 				text: i18n.locale.users, | ||||
| 				text: i18n.ts.users, | ||||
| 				to: '/admin/users', | ||||
| 				active: page.value === 'users', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-laugh', | ||||
| 				text: i18n.locale.customEmojis, | ||||
| 				text: i18n.ts.customEmojis, | ||||
| 				to: '/admin/emojis', | ||||
| 				active: page.value === 'emojis', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-globe', | ||||
| 				text: i18n.locale.federation, | ||||
| 				text: i18n.ts.federation, | ||||
| 				to: '/admin/federation', | ||||
| 				active: page.value === 'federation', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-clipboard-list', | ||||
| 				text: i18n.locale.jobQueue, | ||||
| 				text: i18n.ts.jobQueue, | ||||
| 				to: '/admin/queue', | ||||
| 				active: page.value === 'queue', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-cloud', | ||||
| 				text: i18n.locale.files, | ||||
| 				text: i18n.ts.files, | ||||
| 				to: '/admin/files', | ||||
| 				active: page.value === 'files', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-broadcast-tower', | ||||
| 				text: i18n.locale.announcements, | ||||
| 				text: i18n.ts.announcements, | ||||
| 				to: '/admin/announcements', | ||||
| 				active: page.value === 'announcements', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-audio-description', | ||||
| 				text: i18n.locale.ads, | ||||
| 				text: i18n.ts.ads, | ||||
| 				to: '/admin/ads', | ||||
| 				active: page.value === 'ads', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-exclamation-circle', | ||||
| 				text: i18n.locale.abuseReports, | ||||
| 				text: i18n.ts.abuseReports, | ||||
| 				to: '/admin/abuses', | ||||
| 				active: page.value === 'abuses', | ||||
| 			}], | ||||
| 		}, { | ||||
| 			title: i18n.locale.settings, | ||||
| 			title: i18n.ts.settings, | ||||
| 			items: [{ | ||||
| 				icon: 'fas fa-cog', | ||||
| 				text: i18n.locale.general, | ||||
| 				text: i18n.ts.general, | ||||
| 				to: '/admin/settings', | ||||
| 				active: page.value === 'settings', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-envelope', | ||||
| 				text: i18n.locale.emailServer, | ||||
| 				text: i18n.ts.emailServer, | ||||
| 				to: '/admin/email-settings', | ||||
| 				active: page.value === 'email-settings', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-cloud', | ||||
| 				text: i18n.locale.objectStorage, | ||||
| 				text: i18n.ts.objectStorage, | ||||
| 				to: '/admin/object-storage', | ||||
| 				active: page.value === 'object-storage', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-lock', | ||||
| 				text: i18n.locale.security, | ||||
| 				text: i18n.ts.security, | ||||
| 				to: '/admin/security', | ||||
| 				active: page.value === 'security', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-globe', | ||||
| 				text: i18n.locale.relays, | ||||
| 				text: i18n.ts.relays, | ||||
| 				to: '/admin/relays', | ||||
| 				active: page.value === 'relays', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-share-alt', | ||||
| 				text: i18n.locale.integration, | ||||
| 				text: i18n.ts.integration, | ||||
| 				to: '/admin/integrations', | ||||
| 				active: page.value === 'integrations', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-ban', | ||||
| 				text: i18n.locale.instanceBlocking, | ||||
| 				text: i18n.ts.instanceBlocking, | ||||
| 				to: '/admin/instance-block', | ||||
| 				active: page.value === 'instance-block', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-ghost', | ||||
| 				text: i18n.locale.proxyAccount, | ||||
| 				text: i18n.ts.proxyAccount, | ||||
| 				to: '/admin/proxy-account', | ||||
| 				active: page.value === 'proxy-account', | ||||
| 			}, { | ||||
| 				icon: 'fas fa-cogs', | ||||
| 				text: i18n.locale.other, | ||||
| 				text: i18n.ts.other, | ||||
| 				to: '/admin/other-settings', | ||||
| 				active: page.value === 'other-settings', | ||||
| 			}], | ||||
| 		}, { | ||||
| 			title: i18n.locale.info, | ||||
| 			title: i18n.ts.info, | ||||
| 			items: [{ | ||||
| 				icon: 'fas fa-database', | ||||
| 				text: i18n.locale.database, | ||||
| 				text: i18n.ts.database, | ||||
| 				to: '/admin/database', | ||||
| 				active: page.value === 'database', | ||||
| 			}], | ||||
|  | @ -275,37 +275,37 @@ export default defineComponent({ | |||
| 
 | ||||
| 		const lookup = (ev) => { | ||||
| 			os.popupMenu([{ | ||||
| 				text: i18n.locale.user, | ||||
| 				text: i18n.ts.user, | ||||
| 				icon: 'fas fa-user', | ||||
| 				action: () => { | ||||
| 					lookupUser(); | ||||
| 				} | ||||
| 			}, { | ||||
| 				text: i18n.locale.note, | ||||
| 				text: i18n.ts.note, | ||||
| 				icon: 'fas fa-pencil-alt', | ||||
| 				action: () => { | ||||
| 					alert('TODO'); | ||||
| 				} | ||||
| 			}, { | ||||
| 				text: i18n.locale.file, | ||||
| 				text: i18n.ts.file, | ||||
| 				icon: 'fas fa-cloud', | ||||
| 				action: () => { | ||||
| 					alert('TODO'); | ||||
| 				} | ||||
| 			}, { | ||||
| 				text: i18n.locale.instance, | ||||
| 				text: i18n.ts.instance, | ||||
| 				icon: 'fas fa-globe', | ||||
| 				action: () => { | ||||
| 					alert('TODO'); | ||||
| 				} | ||||
| 			}], ev.currentTarget || ev.target); | ||||
| 			}], ev.currentTarget ?? ev.target); | ||||
| 		}; | ||||
| 
 | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: INFO, | ||||
| 			menuDef, | ||||
| 			header: { | ||||
| 				title: i18n.locale.controlPanel, | ||||
| 				title: i18n.ts.controlPanel, | ||||
| 			}, | ||||
| 			noMaintainerInformation, | ||||
| 			noBotProtection, | ||||
|  |  | |||
|  | @ -112,7 +112,7 @@ export default defineComponent({ | |||
| 		}, | ||||
| 
 | ||||
| 		setBannerImage(e) { | ||||
| 			selectFile(e.currentTarget || e.target, null).then(file => { | ||||
| 			selectFile(e.currentTarget ?? e.target, null).then(file => { | ||||
| 				this.bannerId = file.id; | ||||
| 			}); | ||||
| 		}, | ||||
|  |  | |||
|  | @ -127,7 +127,7 @@ export default defineComponent({ | |||
| 						clipId: this.clip.id, | ||||
| 					}); | ||||
| 				} | ||||
| 			} : undefined], ev.currentTarget || ev.target); | ||||
| 			} : undefined], ev.currentTarget ?? ev.target); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ let folder = $ref(null); | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: folder ? folder.name : i18n.locale.drive, | ||||
| 		title: folder ? folder.name : i18n.ts.drive, | ||||
| 		icon: 'fas fa-cloud', | ||||
| 		bg: 'var(--bg)', | ||||
| 		hideHeader: true, | ||||
|  |  | |||
|  | @ -23,13 +23,13 @@ function menu(ev) { | |||
| 		type: 'label', | ||||
| 		text: ':' + props.emoji.name + ':', | ||||
| 	}, { | ||||
| 		text: i18n.locale.copy, | ||||
| 		text: i18n.ts.copy, | ||||
| 		icon: 'fas fa-copy', | ||||
| 		action: () => { | ||||
| 			copyToClipboard(`:${props.emoji.name}:`); | ||||
| 			os.success(); | ||||
| 		} | ||||
| 	}], ev.currentTarget || ev.target); | ||||
| 	}], ev.currentTarget ?? ev.target); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,14 +16,14 @@ const tab = ref('category'); | |||
| function menu(ev) { | ||||
| 	os.popupMenu([{ | ||||
| 		icon: 'fas fa-download', | ||||
| 		text: i18n.locale.export, | ||||
| 		text: i18n.ts.export, | ||||
| 		action: async () => { | ||||
| 			os.api('export-custom-emojis', { | ||||
| 			}) | ||||
| 			.then(() => { | ||||
| 				os.alert({ | ||||
| 					type: 'info', | ||||
| 					text: i18n.locale.exportRequested, | ||||
| 					text: i18n.ts.exportRequested, | ||||
| 				}); | ||||
| 			}).catch((e) => { | ||||
| 				os.alert({ | ||||
|  | @ -32,12 +32,12 @@ function menu(ev) { | |||
| 				}); | ||||
| 			}); | ||||
| 		} | ||||
| 	}], ev.currentTarget || ev.target); | ||||
| 	}], ev.currentTarget ?? ev.target); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.customEmojis, | ||||
| 		title: i18n.ts.customEmojis, | ||||
| 		icon: 'fas fa-laugh', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>(); | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.favorites, | ||||
| 		title: i18n.ts.favorites, | ||||
| 		icon: 'fas fa-star', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ const pagination = { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.featured, | ||||
| 		title: i18n.ts.featured, | ||||
| 		icon: 'fas fa-fire-alt', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
|  |  | |||
|  | @ -115,7 +115,7 @@ const pagination = { | |||
| 	offsetMode: true, | ||||
| 	params: computed(() => ({ | ||||
| 		sort: sort, | ||||
| 		host: host != '' ? host : null, | ||||
| 		host: host !== '' ? host : null, | ||||
| 		...( | ||||
| 			state === 'federating' ? { federating: true } : | ||||
| 			state === 'subscribing' ? { subscribing: true } : | ||||
|  | @ -135,7 +135,7 @@ function getStatus(instance) { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.federation, | ||||
| 		title: i18n.ts.federation, | ||||
| 		icon: 'fas fa-globe', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
|  | @ -157,11 +157,10 @@ defineExpose({ | |||
| 
 | ||||
| 	> .instance { | ||||
| 		padding: 16px; | ||||
| 		border: solid 1px var(--divider); | ||||
| 		border-radius: 6px; | ||||
| 		background: var(--panel); | ||||
| 		border-radius: 8px; | ||||
| 
 | ||||
| 		&:hover { | ||||
| 			border: solid 1px var(--accent); | ||||
| 			text-decoration: none; | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -60,7 +60,7 @@ function reject(user) { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.locale.followRequests, | ||||
| 		title: i18n.ts.followRequests, | ||||
| 		icon: 'fas fa-user-clock', | ||||
| 		bg: 'var(--bg)', | ||||
| 	})), | ||||
|  |  | |||
|  | @ -92,7 +92,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 	methods: { | ||||
| 		selectFile(e) { | ||||
| 			selectFiles(e.currentTarget || e.target, null).then(files => { | ||||
| 			selectFiles(e.currentTarget ?? e.target, null).then(files => { | ||||
| 				this.files = this.files.concat(files); | ||||
| 			}); | ||||
| 		}, | ||||
|  |  | |||
|  | @ -29,6 +29,7 @@ | |||
| 			<template #label>Moderation</template> | ||||
| 			<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch> | ||||
| 			<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch> | ||||
| 			<MkButton @click="refreshMetadata">Refresh metadata</MkButton> | ||||
| 		</FormSection> | ||||
| 
 | ||||
| 		<FormSection> | ||||
|  | @ -111,6 +112,7 @@ import MkChart from '@/components/chart.vue'; | |||
| import MkObjectView from '@/components/object-view.vue'; | ||||
| import FormLink from '@/components/form/link.vue'; | ||||
| import MkLink from '@/components/link.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import MkKeyValue from '@/components/key-value.vue'; | ||||
| import MkSelect from '@/components/form/select.vue'; | ||||
|  | @ -155,6 +157,15 @@ async function toggleSuspend(v) { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function refreshMetadata() { | ||||
| 	os.api('admin/federation/refresh-remote-instance-metadata', { | ||||
| 		host: instance.host, | ||||
| 	}); | ||||
| 	os.alert({ | ||||
| 		text: 'Refresh requested', | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| fetch(); | ||||
| 
 | ||||
| defineExpose({ | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ const pagination = { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.mentions, | ||||
| 		title: i18n.ts.mentions, | ||||
| 		icon: 'fas fa-at', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
|  |  | |||
|  | @ -12,14 +12,14 @@ import { i18n } from '@/i18n'; | |||
| const pagination = { | ||||
| 	endpoint: 'notes/mentions' as const, | ||||
| 	limit: 10, | ||||
| 	params: () => ({ | ||||
| 	params: { | ||||
| 		visibility: 'specified' | ||||
| 	}), | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.directNotes, | ||||
| 		title: i18n.ts.directNotes, | ||||
| 		icon: 'fas fa-envelope', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
|  |  | |||
|  | @ -128,7 +128,7 @@ export default defineComponent({ | |||
| 				text: this.$ts.messagingWithGroup, | ||||
| 				icon: 'fas fa-users', | ||||
| 				action: () => { this.startGroup() } | ||||
| 			}], ev.currentTarget || ev.target); | ||||
| 			}], ev.currentTarget ?? ev.target); | ||||
| 		}, | ||||
| 
 | ||||
| 		async startUser() { | ||||
|  |  | |||
|  | @ -154,7 +154,7 @@ export default defineComponent({ | |||
| 		}, | ||||
| 
 | ||||
| 		chooseFile(e) { | ||||
| 			selectFile(e.currentTarget || e.target, this.$ts.selectFile).then(file => { | ||||
| 			selectFile(e.currentTarget ?? e.target, this.$ts.selectFile).then(file => { | ||||
| 				this.file = file; | ||||
| 			}); | ||||
| 		}, | ||||
|  | @ -214,7 +214,7 @@ export default defineComponent({ | |||
| 		}, | ||||
| 
 | ||||
| 		async insertEmoji(ev) { | ||||
| 			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text); | ||||
| 			os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, this.$refs.text); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -335,7 +335,7 @@ const Component = defineComponent({ | |||
| 					popout(path); | ||||
| 					this.$router.back(); | ||||
| 				}, | ||||
| 			}], ev.currentTarget || ev.target); | ||||
| 			}], ev.currentTarget ?? ev.target); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ function onAntennaCreated() { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.manageAntennas, | ||||
| 		title: i18n.ts.manageAntennas, | ||||
| 		icon: 'fas fa-satellite', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ import MkPagination from '@/components/ui/pagination.vue'; | |||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import i18n from '@/components/global/i18n'; | ||||
| import { i18n } from '@/i18n'; | ||||
| 
 | ||||
| const pagination = { | ||||
| 	endpoint: 'clips/list' as const, | ||||
|  | @ -29,20 +29,20 @@ const pagination = { | |||
| const pagingComponent = $ref<InstanceType<typeof MkPagination>>(); | ||||
| 
 | ||||
| async function create() { | ||||
| 	const { canceled, result } = await os.form(i18n.locale.createNewClip, { | ||||
| 	const { canceled, result } = await os.form(i18n.ts.createNewClip, { | ||||
| 		name: { | ||||
| 			type: 'string', | ||||
| 			label: i18n.locale.name, | ||||
| 			label: i18n.ts.name, | ||||
| 		}, | ||||
| 		description: { | ||||
| 			type: 'string', | ||||
| 			required: false, | ||||
| 			multiline: true, | ||||
| 			label: i18n.locale.description, | ||||
| 			label: i18n.ts.description, | ||||
| 		}, | ||||
| 		isPublic: { | ||||
| 			type: 'boolean', | ||||
| 			label: i18n.locale.public, | ||||
| 			label: i18n.ts.public, | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}); | ||||
|  | @ -63,7 +63,7 @@ function onClipDeleted() { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.clip, | ||||
| 		title: i18n.ts.clip, | ||||
| 		icon: 'fas fa-paperclip', | ||||
| 		bg: 'var(--bg)', | ||||
| 		action: { | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ const pagination = { | |||
| 
 | ||||
| async function create() { | ||||
| 	const { canceled, result: name } = await os.inputText({ | ||||
| 		title: i18n.locale.enterListName, | ||||
| 		title: i18n.ts.enterListName, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 	await os.apiWithDialog('users/lists/create', { name: name }); | ||||
|  | @ -40,7 +40,7 @@ async function create() { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.manageLists, | ||||
| 		title: i18n.ts.manageLists, | ||||
| 		icon: 'fas fa-list-ul', | ||||
| 		bg: 'var(--bg)', | ||||
| 		action: { | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ import { i18n } from '@/i18n'; | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.notFound, | ||||
| 		title: i18n.ts.notFound, | ||||
| 		icon: 'fas fa-exclamation-triangle', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
|  |  | |||
|  | @ -27,26 +27,26 @@ function setFilter(ev) { | |||
| 	})); | ||||
| 	const items = includeTypes != null ? [{ | ||||
| 		icon: 'fas fa-times', | ||||
| 		text: i18n.locale.clear, | ||||
| 		text: i18n.ts.clear, | ||||
| 		action: () => { | ||||
| 			includeTypes = null; | ||||
| 		} | ||||
| 	}, null, ...typeItems] : typeItems; | ||||
| 	os.popupMenu(items, ev.currentTarget || ev.target); | ||||
| 	os.popupMenu(items, ev.currentTarget ?? ev.target); | ||||
| } | ||||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.locale.notifications, | ||||
| 		title: i18n.ts.notifications, | ||||
| 		icon: 'fas fa-bell', | ||||
| 		bg: 'var(--bg)', | ||||
| 		actions: [{ | ||||
| 			text: i18n.locale.filter, | ||||
| 			text: i18n.ts.filter, | ||||
| 			icon: 'fas fa-filter', | ||||
| 			highlighted: includeTypes != null, | ||||
| 			handler: setFilter, | ||||
| 		}, { | ||||
| 			text: i18n.locale.markAllAsRead, | ||||
| 			text: i18n.ts.markAllAsRead, | ||||
| 			icon: 'fas fa-check', | ||||
| 			handler: () => { | ||||
| 				os.apiWithDialog('notifications/mark-all-as-read'); | ||||
|  | @ -54,11 +54,11 @@ defineExpose({ | |||
| 		}], | ||||
| 		tabs: [{ | ||||
| 			active: tab === 'all', | ||||
| 			title: i18n.locale.all, | ||||
| 			title: i18n.ts.all, | ||||
| 			onClick: () => { tab = 'all'; }, | ||||
| 		}, { | ||||
| 			active: tab === 'unread', | ||||
| 			title: i18n.locale.unread, | ||||
| 			title: i18n.ts.unread, | ||||
| 			onClick: () => { tab = 'unread'; }, | ||||
| 		},] | ||||
| 	})), | ||||
|  |  | |||
|  | @ -448,7 +448,7 @@ export default defineComponent({ | |||
| 		}, | ||||
| 
 | ||||
| 		setEyeCatchingImage(e) { | ||||
| 			selectFile(e.currentTarget || e.target, null).then(file => { | ||||
| 			selectFile(e.currentTarget ?? e.target, null).then(file => { | ||||
| 				this.eyeCatchingImageId = file.id; | ||||
| 			}); | ||||
| 		}, | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ import { i18n } from '@/i18n'; | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 		title: i18n.locale.preview, | ||||
| 		title: i18n.ts.preview, | ||||
| 		icon: 'fas fa-eye', | ||||
| 		bg: 'var(--bg)', | ||||
| 	})), | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| 	<div class="_formRoot"> | ||||
| 		<FormInput v-model="password" type="password" class="_formBlock"> | ||||
| 			<template #prefix><i class="fas fa-lock"></i></template> | ||||
| 			<template #label>{{ i18n.locale.newPassword }}</template> | ||||
| 			<template #label>{{ i18n.ts.newPassword }}</template> | ||||
| 		</FormInput> | ||||
| 		 | ||||
| 		<FormButton primary class="_formBlock" @click="save">{{ i18n.locale.save }}</FormButton> | ||||
| 		<FormButton primary class="_formBlock" @click="save">{{ i18n.ts.save }}</FormButton> | ||||
| 	</div> | ||||
| </MkSpacer> | ||||
| </template> | ||||
|  | @ -43,7 +43,7 @@ onMounted(() => { | |||
| 
 | ||||
| defineExpose({ | ||||
| 	[symbols.PAGE_INFO]: { | ||||
| 		title: i18n.locale.resetPassword, | ||||
| 		title: i18n.ts.resetPassword, | ||||
| 		icon: 'fas fa-lock', | ||||
| 		bg: 'var(--bg)', | ||||
| 	}, | ||||
|  |  | |||
|  | @ -64,7 +64,7 @@ export default defineComponent({ | |||
| 				icon: 'fas fa-trash-alt', | ||||
| 				danger: true, | ||||
| 				action: () => this.removeAccount(account), | ||||
| 			}], ev.currentTarget || ev.target); | ||||
| 			}], ev.currentTarget ?? ev.target); | ||||
| 		}, | ||||
| 
 | ||||
| 		addAccount(ev) { | ||||
|  | @ -74,7 +74,7 @@ export default defineComponent({ | |||
| 			}, { | ||||
| 				text: this.$ts.createAccount, | ||||
| 				action: () => { this.createAccount(); }, | ||||
| 			}], ev.currentTarget || ev.target); | ||||
| 			}], ev.currentTarget ?? ev.target); | ||||
| 		}, | ||||
| 
 | ||||
| 		addExistingAccount() { | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ | |||
| 			<template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> | ||||
| 			<template #suffixIcon><i class="fas fa-folder-open"></i></template> | ||||
| 		</FormLink> | ||||
| 		<FormSwitch v-model="keepOriginalUploading" class="_formBlock">{{ $ts.keepOriginalUploading }}<template #caption>{{ $ts.keepOriginalUploadingDescription }}</template></FormSwitch> | ||||
| 	</FormSection> | ||||
| </div> | ||||
| </template> | ||||
|  | @ -36,18 +37,21 @@ | |||
| import { defineComponent } from 'vue'; | ||||
| import * as tinycolor from 'tinycolor2'; | ||||
| import FormLink from '@/components/form/link.vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import MkKeyValue from '@/components/key-value.vue'; | ||||
| import FormSplit from '@/components/form/split.vue'; | ||||
| import * as os from '@/os'; | ||||
| import bytes from '@/filters/bytes'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { defaultStore } from '@/store'; | ||||
| 
 | ||||
| // TODO: render chart | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormLink, | ||||
| 		FormSwitch, | ||||
| 		FormSection, | ||||
| 		MkKeyValue, | ||||
| 		FormSplit, | ||||
|  | @ -79,7 +83,8 @@ export default defineComponent({ | |||
| 					l: 0.5 | ||||
| 				}) | ||||
| 			}; | ||||
| 		} | ||||
| 		}, | ||||
| 		keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'), | ||||
| 	}, | ||||
| 
 | ||||
| 	async created() { | ||||
|  |  | |||
|  | @ -62,7 +62,7 @@ export default defineComponent({ | |||
| 		const emailAddress = ref($i.email); | ||||
| 
 | ||||
| 		const INFO = { | ||||
| 			title: i18n.locale.email, | ||||
| 			title: i18n.ts.email, | ||||
| 			icon: 'fas fa-envelope', | ||||
| 			bg: 'var(--bg)', | ||||
| 		}; | ||||
|  | @ -75,7 +75,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 		const saveEmailAddress = () => { | ||||
| 			os.inputText({ | ||||
| 				title: i18n.locale.password, | ||||
| 				title: i18n.ts.password, | ||||
| 				type: 'password' | ||||
| 			}).then(({ canceled, result: password }) => { | ||||
| 				if (canceled) return; | ||||
|  |  | |||
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