mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-10-31 13:34:12 +00:00 
			
		
		
		
	Migrate to Vue3 (#6587)
* Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip
This commit is contained in:
		
							parent
							
								
									a40f38b2b5
								
							
						
					
					
						commit
						7199e6f4e0
					
				
					 357 changed files with 15053 additions and 12496 deletions
				
			
		
							
								
								
									
										11
									
								
								gulpfile.ts
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								gulpfile.ts
									
										
									
									
									
								
							|  | @ -7,9 +7,6 @@ import * as gulp from 'gulp'; | ||||||
| import * as ts from 'gulp-typescript'; | import * as ts from 'gulp-typescript'; | ||||||
| import * as rimraf from 'rimraf'; | import * as rimraf from 'rimraf'; | ||||||
| import * as rename from 'gulp-rename'; | import * as rename from 'gulp-rename'; | ||||||
| const cleanCSS = require('gulp-clean-css'); |  | ||||||
| const sass = require('gulp-dart-sass'); |  | ||||||
| const fiber = require('fibers'); |  | ||||||
| 
 | 
 | ||||||
| const locales: { [x: string]: any } = require('./locales'); | const locales: { [x: string]: any } = require('./locales'); | ||||||
| const meta = require('./package.json'); | const meta = require('./package.json'); | ||||||
|  | @ -61,13 +58,6 @@ gulp.task('cleanall', gulp.parallel('clean', cb => | ||||||
| 	rimraf('./node_modules', cb) | 	rimraf('./node_modules', cb) | ||||||
| )); | )); | ||||||
| 
 | 
 | ||||||
| gulp.task('build:client:styles', () => |  | ||||||
| 	gulp.src('./src/client/style.scss') |  | ||||||
| 		.pipe(sass({ fiber })) |  | ||||||
| 		.pipe(cleanCSS()) |  | ||||||
| 		.pipe(gulp.dest('./built/client/assets/')) |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| gulp.task('copy:client', () => | gulp.task('copy:client', () => | ||||||
| 		gulp.src([ | 		gulp.src([ | ||||||
| 			'./assets/**/*', | 			'./assets/**/*', | ||||||
|  | @ -87,7 +77,6 @@ gulp.task('copy:docs', () => | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| gulp.task('build:client', gulp.parallel( | gulp.task('build:client', gulp.parallel( | ||||||
| 	'build:client:styles', |  | ||||||
| 	'copy:client', | 	'copy:client', | ||||||
| 	'copy:docs' | 	'copy:docs' | ||||||
| )); | )); | ||||||
|  |  | ||||||
|  | @ -16,6 +16,9 @@ noNotes: "ノートはありません" | ||||||
| noNotifications: "通知はありません" | noNotifications: "通知はありません" | ||||||
| instance: "インスタンス" | instance: "インスタンス" | ||||||
| settings: "設定" | settings: "設定" | ||||||
|  | basicSettings: "基本設定" | ||||||
|  | otherSettings: "その他の設定" | ||||||
|  | openInWindow: "ウィンドウで開く" | ||||||
| profile: "プロフィール" | profile: "プロフィール" | ||||||
| timeline: "タイムライン" | timeline: "タイムライン" | ||||||
| noAccountDescription: "自己紹介はありません" | noAccountDescription: "自己紹介はありません" | ||||||
|  | @ -40,6 +43,7 @@ deleteAndEditConfirm: "このノートを削除してもう一度編集します | ||||||
| addToList: "リストに追加" | addToList: "リストに追加" | ||||||
| sendMessage: "メッセージを送信" | sendMessage: "メッセージを送信" | ||||||
| copyUsername: "ユーザー名をコピー" | copyUsername: "ユーザー名をコピー" | ||||||
|  | searchUser: "ユーザーを検索" | ||||||
| reply: "返信" | reply: "返信" | ||||||
| loadMore: "もっと見る" | loadMore: "もっと見る" | ||||||
| youGotNewFollower: "フォローされました" | youGotNewFollower: "フォローされました" | ||||||
|  | @ -66,8 +70,11 @@ followers: "フォロワー" | ||||||
| followsYou: "フォローされています" | followsYou: "フォローされています" | ||||||
| createList: "リスト作成" | createList: "リスト作成" | ||||||
| manageLists: "リストの管理" | manageLists: "リストの管理" | ||||||
| error: "問題が発生しました" | error: "エラー" | ||||||
|  | somethingHappened: "問題が発生しました" | ||||||
| retry: "再試行" | retry: "再試行" | ||||||
|  | pageLoadError: "ページの読み込みに失敗しました。" | ||||||
|  | pageLoadErrorDescription: "これは通常、ネットワークまたはブラウザキャッシュが原因です。キャッシュをクリアするか、しばらく待ってから再度試してください。" | ||||||
| enterListName: "リスト名を入力" | enterListName: "リスト名を入力" | ||||||
| privacy: "プライバシー" | privacy: "プライバシー" | ||||||
| makeFollowManuallyApprove: "フォローを承認制にする" | makeFollowManuallyApprove: "フォローを承認制にする" | ||||||
|  | @ -106,6 +113,8 @@ unsuspendConfirm: "解凍しますか?" | ||||||
| selectList: "リストを選択" | selectList: "リストを選択" | ||||||
| selectAntenna: "アンテナを選択" | selectAntenna: "アンテナを選択" | ||||||
| selectWidget: "ウィジェットを選択" | selectWidget: "ウィジェットを選択" | ||||||
|  | editWidgets: "ウィジェットを編集" | ||||||
|  | editWidgetsExit: "編集を終了" | ||||||
| customEmojis: "カスタム絵文字" | customEmojis: "カスタム絵文字" | ||||||
| emoji: "絵文字" | emoji: "絵文字" | ||||||
| emojiName: "絵文字名" | emojiName: "絵文字名" | ||||||
|  | @ -177,7 +186,6 @@ processing: "処理中" | ||||||
| preview: "プレビュー" | preview: "プレビュー" | ||||||
| default: "デフォルト" | default: "デフォルト" | ||||||
| noCustomEmojis: "絵文字はありません" | noCustomEmojis: "絵文字はありません" | ||||||
| customEmojisOfRemote: "リモートの絵文字" |  | ||||||
| noJobs: "ジョブはありません" | noJobs: "ジョブはありません" | ||||||
| federating: "連合中" | federating: "連合中" | ||||||
| blocked: "ブロック中" | blocked: "ブロック中" | ||||||
|  | @ -445,7 +453,7 @@ total: "合計" | ||||||
| weekOverWeekChanges: "前週比" | weekOverWeekChanges: "前週比" | ||||||
| dayOverDayChanges: "前日比" | dayOverDayChanges: "前日比" | ||||||
| appearance: "アピアランス" | appearance: "アピアランス" | ||||||
| clinetSettings: "クライアント設定" | clientSettings: "クライアント設定" | ||||||
| accountSettings: "アカウント設定" | accountSettings: "アカウント設定" | ||||||
| promotion: "プロモーション" | promotion: "プロモーション" | ||||||
| promote: "プロモート" | promote: "プロモート" | ||||||
|  | @ -476,6 +484,8 @@ newNoteRecived: "新しいノートがあります" | ||||||
| sounds: "サウンド" | sounds: "サウンド" | ||||||
| listen: "聴く" | listen: "聴く" | ||||||
| none: "なし" | none: "なし" | ||||||
|  | showInPage: "ページで表示" | ||||||
|  | popout: "ポップアウト" | ||||||
| volume: "音量" | volume: "音量" | ||||||
| details: "詳細" | details: "詳細" | ||||||
| chooseEmoji: "絵文字を選択" | chooseEmoji: "絵文字を選択" | ||||||
|  | @ -518,7 +528,6 @@ enableInfiniteScroll: "自動でもっと見る" | ||||||
| visibility: "公開範囲" | visibility: "公開範囲" | ||||||
| poll: "アンケート" | poll: "アンケート" | ||||||
| useCw: "内容を隠す" | useCw: "内容を隠す" | ||||||
| fixedWidgetsPosition: "ウィジェットの位置を固定する" |  | ||||||
| enablePlayer: "プレイヤーを開く" | enablePlayer: "プレイヤーを開く" | ||||||
| disablePlayer: "プレイヤーを閉じる" | disablePlayer: "プレイヤーを閉じる" | ||||||
| expandTweet: "ツイートを展開する" | expandTweet: "ツイートを展開する" | ||||||
|  | @ -570,6 +579,12 @@ notificationSetting: "通知設定" | ||||||
| notificationSettingDesc: "表示する通知の種別を選択してください。" | notificationSettingDesc: "表示する通知の種別を選択してください。" | ||||||
| useGlobalSetting: "グローバル設定を使う" | useGlobalSetting: "グローバル設定を使う" | ||||||
| useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。" | useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。" | ||||||
|  | other: "その他" | ||||||
|  | regenerateLoginToken: "ログイントークンを再生成" | ||||||
|  | regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。" | ||||||
|  | setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" | ||||||
|  | fileIdOrUrl: "ファイルIDまたはURL" | ||||||
|  | chatOpenBehavior: "チャットを開くときの動作" | ||||||
| 
 | 
 | ||||||
| _serverDisconnectedBehavior: | _serverDisconnectedBehavior: | ||||||
|   reload: "自動でリロード" |   reload: "自動でリロード" | ||||||
|  | @ -802,6 +817,7 @@ _widgets: | ||||||
|   photos: "フォト" |   photos: "フォト" | ||||||
|   digitalClock: "デジタル時計" |   digitalClock: "デジタル時計" | ||||||
|   federation: "連合" |   federation: "連合" | ||||||
|  |   postForm: "投稿フォーム" | ||||||
| 
 | 
 | ||||||
| _cw: | _cw: | ||||||
|   hide: "隠す" |   hide: "隠す" | ||||||
|  |  | ||||||
							
								
								
									
										106
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										106
									
								
								package.json
									
										
									
									
									
								
							|  | @ -37,11 +37,11 @@ | ||||||
| 	"dependencies": { | 	"dependencies": { | ||||||
| 		"@babel/plugin-transform-runtime": "7.11.0", | 		"@babel/plugin-transform-runtime": "7.11.0", | ||||||
| 		"@elastic/elasticsearch": "7.8.0", | 		"@elastic/elasticsearch": "7.8.0", | ||||||
| 		"@fortawesome/fontawesome-svg-core": "1.2.30", | 		"@fortawesome/fontawesome-svg-core": "1.2.32", | ||||||
| 		"@fortawesome/free-brands-svg-icons": "5.14.0", | 		"@fortawesome/free-brands-svg-icons": "5.15.1", | ||||||
| 		"@fortawesome/free-regular-svg-icons": "5.14.0", | 		"@fortawesome/free-regular-svg-icons": "5.15.1", | ||||||
| 		"@fortawesome/free-solid-svg-icons": "5.14.0", | 		"@fortawesome/free-solid-svg-icons": "5.15.1", | ||||||
| 		"@fortawesome/vue-fontawesome": "0.1.10", | 		"@fortawesome/vue-fontawesome": "3.0.0-2", | ||||||
| 		"@koa/cors": "3.1.0", | 		"@koa/cors": "3.1.0", | ||||||
| 		"@koa/multer": "3.0.0", | 		"@koa/multer": "3.0.0", | ||||||
| 		"@koa/router": "9.0.1", | 		"@koa/router": "9.0.1", | ||||||
|  | @ -97,19 +97,20 @@ | ||||||
| 		"@types/speakeasy": "2.0.5", | 		"@types/speakeasy": "2.0.5", | ||||||
| 		"@types/tinycolor2": "1.4.2", | 		"@types/tinycolor2": "1.4.2", | ||||||
| 		"@types/tmp": "0.2.0", | 		"@types/tmp": "0.2.0", | ||||||
| 		"@types/uuid": "8.0.0", | 		"@types/uuid": "8.3.0", | ||||||
| 		"@types/web-push": "3.3.0", | 		"@types/web-push": "3.3.0", | ||||||
| 		"@types/webpack": "4.41.18", | 		"@types/webpack": "4.41.22", | ||||||
| 		"@types/webpack-stream": "3.2.11", | 		"@types/webpack-stream": "3.2.11", | ||||||
| 		"@types/websocket": "1.0.1", | 		"@types/websocket": "1.0.1", | ||||||
| 		"@types/ws": "7.2.6", | 		"@types/ws": "7.2.7", | ||||||
| 		"@typescript-eslint/parser": "3.6.0", | 		"@typescript-eslint/parser": "4.4.0", | ||||||
|  | 		"@vue/compiler-sfc": "3.0.0", | ||||||
| 		"abort-controller": "3.0.0", | 		"abort-controller": "3.0.0", | ||||||
| 		"apexcharts": "3.20.0", | 		"apexcharts": "3.22.0", | ||||||
| 		"autobind-decorator": "2.4.0", | 		"autobind-decorator": "2.4.0", | ||||||
| 		"autosize": "4.0.2", | 		"autosize": "4.0.2", | ||||||
| 		"autwh": "0.1.0", | 		"autwh": "0.1.0", | ||||||
| 		"aws-sdk": "2.724.0", | 		"aws-sdk": "2.770.0", | ||||||
| 		"bcryptjs": "2.4.3", | 		"bcryptjs": "2.4.3", | ||||||
| 		"blurhash": "1.1.3", | 		"blurhash": "1.1.3", | ||||||
| 		"bull": "3.18.0", | 		"bull": "3.18.0", | ||||||
|  | @ -122,35 +123,33 @@ | ||||||
| 		"content-disposition": "0.5.3", | 		"content-disposition": "0.5.3", | ||||||
| 		"core-js": "3.6.5", | 		"core-js": "3.6.5", | ||||||
| 		"crc-32": "1.2.0", | 		"crc-32": "1.2.0", | ||||||
| 		"css-loader": "4.2.1", | 		"css-loader": "4.3.0", | ||||||
| 		"cssnano": "4.1.10", | 		"cssnano": "4.1.10", | ||||||
| 		"dateformat": "3.0.3", | 		"dateformat": "3.0.3", | ||||||
| 		"deep-entries": "3.1.0", | 		"deep-entries": "3.1.0", | ||||||
| 		"diskusage": "1.1.3", | 		"diskusage": "1.1.3", | ||||||
| 		"double-ended-queue": "2.1.0-0", | 		"double-ended-queue": "2.1.0-0", | ||||||
| 		"escape-regexp": "0.0.1", | 		"escape-regexp": "0.0.1", | ||||||
| 		"eslint": "7.4.0", | 		"eslint": "7.10.0", | ||||||
| 		"eslint-plugin-vue": "6.2.2", | 		"eslint-plugin-vue": "7.0.1", | ||||||
| 		"eventemitter3": "4.0.4", | 		"eventemitter3": "4.0.7", | ||||||
| 		"feed": "4.2.1", | 		"feed": "4.2.1", | ||||||
| 		"fibers": "5.0.0", | 		"fibers": "5.0.0", | ||||||
| 		"file-type": "14.7.1", | 		"file-type": "15.0.1", | ||||||
| 		"fluent-ffmpeg": "2.1.2", | 		"fluent-ffmpeg": "2.1.2", | ||||||
| 		"glob": "7.1.6", | 		"glob": "7.1.6", | ||||||
| 		"gulp": "4.0.2", | 		"gulp": "4.0.2", | ||||||
| 		"gulp-clean-css": "4.3.0", |  | ||||||
| 		"gulp-dart-sass": "1.0.2", |  | ||||||
| 		"gulp-rename": "2.0.0", | 		"gulp-rename": "2.0.0", | ||||||
| 		"gulp-replace": "1.0.0", | 		"gulp-replace": "1.0.0", | ||||||
| 		"gulp-sourcemaps": "2.6.5", | 		"gulp-sourcemaps": "2.6.5", | ||||||
| 		"gulp-terser": "1.3.2", | 		"gulp-terser": "1.4.0", | ||||||
| 		"gulp-tslint": "8.1.4", | 		"gulp-tslint": "8.1.4", | ||||||
| 		"gulp-typescript": "6.0.0-alpha.1", | 		"gulp-typescript": "6.0.0-alpha.1", | ||||||
| 		"hard-source-webpack-plugin": "0.13.1", | 		"hard-source-webpack-plugin": "0.13.1", | ||||||
| 		"hcaptcha": "0.0.2", | 		"hcaptcha": "0.0.2", | ||||||
| 		"html-minifier": "4.0.0", | 		"html-minifier": "4.0.0", | ||||||
| 		"http-proxy-agent": "4.0.1", | 		"http-proxy-agent": "4.0.1", | ||||||
| 		"http-signature": "1.3.4", | 		"http-signature": "1.3.5", | ||||||
| 		"https-proxy-agent": "5.0.0", | 		"https-proxy-agent": "5.0.0", | ||||||
| 		"idb-keyval": "3.2.0", | 		"idb-keyval": "3.2.0", | ||||||
| 		"insert-text-at-cursor": "0.3.0", | 		"insert-text-at-cursor": "0.3.0", | ||||||
|  | @ -171,27 +170,27 @@ | ||||||
| 		"koa-mount": "4.0.0", | 		"koa-mount": "4.0.0", | ||||||
| 		"koa-send": "5.0.1", | 		"koa-send": "5.0.1", | ||||||
| 		"koa-slow": "2.1.0", | 		"koa-slow": "2.1.0", | ||||||
| 		"koa-views": "6.3.0", | 		"koa-views": "6.3.1", | ||||||
| 		"langmap": "0.0.16", | 		"langmap": "0.0.16", | ||||||
| 		"lookup-dns-cache": "2.1.0", | 		"lookup-dns-cache": "2.1.0", | ||||||
| 		"markdown-it": "11.0.0", | 		"markdown-it": "11.0.1", | ||||||
| 		"markdown-it-anchor": "5.3.0", | 		"markdown-it-anchor": "6.0.0", | ||||||
| 		"mocha": "8.1.1", | 		"mocha": "8.1.3", | ||||||
| 		"moji": "0.5.1", | 		"moji": "0.5.1", | ||||||
| 		"ms": "2.1.2", | 		"ms": "2.1.2", | ||||||
| 		"multer": "1.4.2", | 		"multer": "1.4.2", | ||||||
| 		"nested-property": "4.0.0", | 		"nested-property": "4.0.0", | ||||||
| 		"node-fetch": "2.6.0", | 		"node-fetch": "2.6.1", | ||||||
| 		"nodemailer": "6.4.11", | 		"nodemailer": "6.4.13", | ||||||
| 		"nprogress": "0.2.0", |  | ||||||
| 		"object-assign-deep": "0.4.0", | 		"object-assign-deep": "0.4.0", | ||||||
| 		"os-utils": "0.0.14", | 		"os-utils": "0.0.14", | ||||||
|  | 		"p-cancelable": "2.0.0", | ||||||
| 		"parse5": "6.0.1", | 		"parse5": "6.0.1", | ||||||
| 		"parsimmon": "1.15.0", | 		"parsimmon": "1.16.0", | ||||||
| 		"pg": "8.3.2", | 		"pg": "8.4.1", | ||||||
| 		"portal-vue": "2.1.7", |  | ||||||
| 		"portscanner": "2.2.0", | 		"portscanner": "2.2.0", | ||||||
| 		"postcss-loader": "3.0.0", | 		"postcss": "8.1.1", | ||||||
|  | 		"postcss-loader": "4.0.3", | ||||||
| 		"prismjs": "1.21.0", | 		"prismjs": "1.21.0", | ||||||
| 		"probe-image-size": "5.0.0", | 		"probe-image-size": "5.0.0", | ||||||
| 		"promise-limit": "2.7.0", | 		"promise-limit": "2.7.0", | ||||||
|  | @ -202,7 +201,7 @@ | ||||||
| 		"qrcode": "1.4.4", | 		"qrcode": "1.4.4", | ||||||
| 		"random-seed": "0.3.0", | 		"random-seed": "0.3.0", | ||||||
| 		"ratelimiter": "3.4.1", | 		"ratelimiter": "3.4.1", | ||||||
| 		"re2": "1.15.4", | 		"re2": "1.15.5", | ||||||
| 		"recaptcha-promise": "0.1.3", | 		"recaptcha-promise": "0.1.3", | ||||||
| 		"reconnecting-websocket": "4.4.0", | 		"reconnecting-websocket": "4.4.0", | ||||||
| 		"redis": "3.0.2", | 		"redis": "3.0.2", | ||||||
|  | @ -215,54 +214,49 @@ | ||||||
| 		"rimraf": "3.0.2", | 		"rimraf": "3.0.2", | ||||||
| 		"rndstr": "1.0.0", | 		"rndstr": "1.0.0", | ||||||
| 		"s-age": "1.1.2", | 		"s-age": "1.1.2", | ||||||
| 		"sass": "1.26.10", | 		"sass": "1.27.0", | ||||||
| 		"sass-loader": "9.0.3", | 		"sass-loader": "10.0.2", | ||||||
| 		"seedrandom": "3.0.5", | 		"seedrandom": "3.0.5", | ||||||
| 		"sharp": "0.25.4", | 		"sharp": "0.26.1", | ||||||
| 		"speakeasy": "2.0.0", | 		"speakeasy": "2.0.0", | ||||||
| 		"stringz": "2.1.0", | 		"stringz": "2.1.0", | ||||||
| 		"style-loader": "1.2.1", | 		"style-loader": "1.3.0", | ||||||
| 		"summaly": "2.4.0", | 		"summaly": "2.4.0", | ||||||
| 		"syslog-pro": "1.0.0", | 		"syslog-pro": "1.0.0", | ||||||
| 		"systeminformation": "4.26.12", | 		"systeminformation": "4.27.8", | ||||||
| 		"syuilo-password-strength": "0.0.1", | 		"syuilo-password-strength": "0.0.1", | ||||||
| 		"textarea-caret": "3.1.0", | 		"textarea-caret": "3.1.0", | ||||||
| 		"three": "0.117.1", | 		"three": "0.117.1", | ||||||
| 		"tinycolor2": "1.4.1", | 		"tinycolor2": "1.4.2", | ||||||
| 		"tmp": "0.2.1", | 		"tmp": "0.2.1", | ||||||
| 		"ts-loader": "8.0.2", | 		"ts-loader": "8.0.4", | ||||||
| 		"ts-node": "9.0.0", | 		"ts-node": "9.0.0", | ||||||
| 		"tslint": "6.1.3", | 		"tslint": "6.1.3", | ||||||
| 		"tslint-sonarts": "1.9.0", | 		"tslint-sonarts": "1.9.0", | ||||||
| 		"typeorm": "0.2.25", | 		"typeorm": "0.2.28", | ||||||
| 		"typescript": "4.0.2", | 		"typescript": "4.0.3", | ||||||
| 		"ulid": "2.3.0", | 		"ulid": "2.3.0", | ||||||
| 		"url-loader": "4.1.0", | 		"url-loader": "4.1.0", | ||||||
| 		"uuid": "8.3.0", | 		"uuid": "8.3.1", | ||||||
| 		"v-animate-css": "0.0.3", |  | ||||||
| 		"v-debounce": "0.1.2", | 		"v-debounce": "0.1.2", | ||||||
| 		"vue": "2.6.12", | 		"vue": "3.0.1", | ||||||
| 		"vue-color": "2.7.1", | 		"vue-color": "2.7.1", | ||||||
| 		"vue-content-loading": "1.6.0", | 		"vue-draggable-next": "1.0.8", | ||||||
| 		"vue-cropperjs": "4.1.0", | 		"vue-i18n": "9.0.0-beta.4", | ||||||
| 		"vue-i18n": "8.21.0", | 		"vue-json-pretty": "1.7.0", | ||||||
| 		"vue-json-pretty": "1.6.7", | 		"vue-loader": "16.0.0-beta.7", | ||||||
| 		"vue-loader": "15.9.3", |  | ||||||
| 		"vue-marquee-text-component": "1.1.1", |  | ||||||
| 		"vue-meta": "2.4.0", |  | ||||||
| 		"vue-prism-component": "1.2.0", | 		"vue-prism-component": "1.2.0", | ||||||
| 		"vue-prism-editor": "1.2.2", | 		"vue-prism-editor": "1.2.2", | ||||||
| 		"vue-router": "3.4.3", | 		"vue-router": "4.0.0-beta.13", | ||||||
| 		"vue-style-loader": "4.1.2", | 		"vue-style-loader": "4.1.2", | ||||||
| 		"vue-svg-inline-loader-corejs3": "1.5.0", | 		"vue-svg-inline-loader-corejs3": "1.5.0", | ||||||
| 		"vue-template-compiler": "2.6.12", | 		"vue-template-compiler": "2.6.12", | ||||||
| 		"vuedraggable": "2.24.1", | 		"vuex": "4.0.0-beta.4", | ||||||
| 		"vuex": "3.5.1", |  | ||||||
| 		"vuex-persistedstate": "3.1.0", | 		"vuex-persistedstate": "3.1.0", | ||||||
| 		"web-push": "3.4.4", | 		"web-push": "3.4.4", | ||||||
| 		"webpack": "5.0.0-beta.28", | 		"webpack": "5.1.3", | ||||||
| 		"webpack-cli": "3.3.12", | 		"webpack-cli": "3.3.12", | ||||||
| 		"websocket": "1.0.31", | 		"websocket": "1.0.32", | ||||||
| 		"ws": "7.3.1", | 		"ws": "7.3.1", | ||||||
| 		"xev": "2.0.1" | 		"xev": "2.0.1" | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								src/client/.eslintrc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/client/.eslintrc
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | { | ||||||
|  | 	"globals": { | ||||||
|  | 		"_DEV_": false, | ||||||
|  | 		"_LANGS_": false, | ||||||
|  | 		"_VERSION_": false, | ||||||
|  | 		"_ENV_": false, | ||||||
|  | 		"_PERF_PREFIX_": false, | ||||||
|  | 		"_DATA_TRANSFER_DRIVE_FILE_": false, | ||||||
|  | 		"_DATA_TRANSFER_DRIVE_FOLDER_": false, | ||||||
|  | 		"_DATA_TRANSFER_DECK_COLUMN_": false | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								src/client/@types/global.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/client/@types/global.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | declare const _LANGS_: string[]; | ||||||
|  | declare const _VERSION_: string; | ||||||
|  | declare const _ENV_: string; | ||||||
|  | declare const _DEV_: boolean; | ||||||
|  | declare const _PERF_PREFIX_: string; | ||||||
|  | declare const _DATA_TRANSFER_DRIVE_FILE_: string; | ||||||
|  | declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; | ||||||
|  | declare const _DATA_TRANSFER_DECK_COLUMN_: string; | ||||||
							
								
								
									
										11
									
								
								src/client/@types/vuex-shim.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/client/@types/vuex-shim.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | import { ComponentCustomProperties } from 'vue'; | ||||||
|  | import { Store } from 'vuex'; | ||||||
|  | 
 | ||||||
|  | declare module '@vue/runtime-core' { | ||||||
|  | 	interface State { | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	interface ComponentCustomProperties { | ||||||
|  | 		$store: Store<State> | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -1,788 +0,0 @@ | ||||||
| <template> |  | ||||||
| <div class="mk-app" v-hotkey.global="keymap"> |  | ||||||
| 	<header class="header" ref="header"> |  | ||||||
| 		<div class="title" ref="title"> |  | ||||||
| 			<transition :name="$store.state.device.animation ? 'header' : ''" mode="out-in" appear> |  | ||||||
| 				<button class="_button back" v-if="canBack" @click="back()"><fa :icon="faChevronLeft"/></button> |  | ||||||
| 			</transition> |  | ||||||
| 			<transition :name="$store.state.device.animation ? 'header' : ''" mode="out-in" appear> |  | ||||||
| 				<div class="body" :key="pageKey"> |  | ||||||
| 					<div class="default"> |  | ||||||
| 						<portal-target name="avatar" slim/> |  | ||||||
| 						<h1 class="title"><portal-target name="icon" slim/><portal-target name="title" slim/></h1> |  | ||||||
| 					</div> |  | ||||||
| 					<div class="custom"> |  | ||||||
| 						<portal-target name="header" slim/> |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 			</transition> |  | ||||||
| 		</div> |  | ||||||
| 		<div class="sub"> |  | ||||||
| 			<template v-if="$store.getters.isSignedIn"> |  | ||||||
| 				<button v-if="widgetsEditMode" class="_button edit active" @click="widgetsEditMode = false"><fa :icon="faGripVertical"/></button> |  | ||||||
| 				<button v-else class="_button edit" @click="widgetsEditMode = true"><fa :icon="faGripVertical"/></button> |  | ||||||
| 			</template> |  | ||||||
| 			<div class="search"> |  | ||||||
| 				<fa :icon="faSearch"/> |  | ||||||
| 				<input type="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/> |  | ||||||
| 			</div> |  | ||||||
| 			<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button> |  | ||||||
| 			<x-clock v-if="isDesktop" class="clock"/> |  | ||||||
| 		</div> |  | ||||||
| 	</header> |  | ||||||
| 
 |  | ||||||
| 	<x-sidebar ref="nav" @change-view-mode="calcHeaderWidth"/> |  | ||||||
| 
 |  | ||||||
| 	<div class="contents" ref="contents" :class="{ wallpaper, full: $store.state.fullView }"> |  | ||||||
| 		<main ref="main"> |  | ||||||
| 			<div class="content"> |  | ||||||
| 				<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition"> |  | ||||||
| 					<keep-alive :include="['index']"> |  | ||||||
| 						<router-view></router-view> |  | ||||||
| 					</keep-alive> |  | ||||||
| 				</transition> |  | ||||||
| 			</div> |  | ||||||
| 			<div class="powerd-by" :class="{ visible: !$store.getters.isSignedIn }"> |  | ||||||
| 				<b><router-link to="/">{{ host }}</router-link></b> |  | ||||||
| 				<small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small> |  | ||||||
| 			</div> |  | ||||||
| 		</main> |  | ||||||
| 
 |  | ||||||
| 		<template v-if="isDesktop"> |  | ||||||
| 			<div v-for="place in ['left', 'right']" ref="widgets" class="widgets" :class="{ edit: widgetsEditMode, fixed: $store.state.device.fixedWidgetsPosition, empty: widgets[place].length === 0 && !widgetsEditMode }" :key="place"> |  | ||||||
| 				<div class="spacer"></div> |  | ||||||
| 				<div class="container" v-if="widgetsEditMode"> |  | ||||||
| 					<mk-button primary @click="addWidget(place)" class="add"><fa :icon="faPlus"/></mk-button> |  | ||||||
| 					<x-draggable |  | ||||||
| 						:list="widgets[place]" |  | ||||||
| 						handle=".handle" |  | ||||||
| 						animation="150" |  | ||||||
| 						class="sortable" |  | ||||||
| 						@sort="onWidgetSort" |  | ||||||
| 					> |  | ||||||
| 						<div v-for="widget in widgets[place]" class="customize-container _panel" :key="widget.id"> |  | ||||||
| 							<header> |  | ||||||
| 								<span class="handle"><fa :icon="faBars"/></span>{{ $t('_widgets.' + widget.name) }}<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button> |  | ||||||
| 							</header> |  | ||||||
| 							<div @click="widgetFunc(widget.id)"> |  | ||||||
| 								<component class="_close_ _forceContainerFull_" :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/> |  | ||||||
| 							</div> |  | ||||||
| 						</div> |  | ||||||
| 					</x-draggable> |  | ||||||
| 				</div> |  | ||||||
| 				<div class="container" v-else> |  | ||||||
| 					<component v-for="widget in widgets[place]" class="_close_ _forceContainerFull_" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget"/> |  | ||||||
| 				</div> |  | ||||||
| 			</div> |  | ||||||
| 		</template> |  | ||||||
| 	</div> |  | ||||||
| 
 |  | ||||||
| 	<div class="buttons" :class="{ navHidden }"> |  | ||||||
| 		<button class="button nav _button" @click="showNav" ref="navButton"><fa :icon="faBars"/><i v-if="navIndicated"><fa :icon="faCircle"/></i></button> |  | ||||||
| 		<button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button> |  | ||||||
| 		<button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button> |  | ||||||
| 		<button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="$router.push('/my/notifications')"><fa :icon="faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button> |  | ||||||
| 		<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button> |  | ||||||
| 	</div> |  | ||||||
| 
 |  | ||||||
| 	<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" :class="{ navHidden }" @click="post()"><fa :icon="faPencilAlt"/></button> |  | ||||||
| 
 |  | ||||||
| 	<stream-indicator v-if="$store.getters.isSignedIn"/> |  | ||||||
| </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; |  | ||||||
| import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; |  | ||||||
| import { v4 as uuid } from 'uuid'; |  | ||||||
| import { host } from './config'; |  | ||||||
| import { search } from './scripts/search'; |  | ||||||
| import { StickySidebar } from './scripts/sticky-sidebar'; |  | ||||||
| import { widgets } from './widgets'; |  | ||||||
| import XSidebar from './components/sidebar.vue'; |  | ||||||
| 
 |  | ||||||
| const DESKTOP_THRESHOLD = 1100; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	components: { |  | ||||||
| 		XSidebar, |  | ||||||
| 		XClock: () => import('./components/header-clock.vue').then(m => m.default), |  | ||||||
| 		MkButton: () => import('./components/ui/button.vue').then(m => m.default), |  | ||||||
| 		XDraggable: () => import('vuedraggable'), |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			host: host, |  | ||||||
| 			pageKey: 0, |  | ||||||
| 			searching: false, |  | ||||||
| 			connection: null, |  | ||||||
| 			searchQuery: '', |  | ||||||
| 			searchWait: false, |  | ||||||
| 			widgetsEditMode: false, |  | ||||||
| 			isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, |  | ||||||
| 			canBack: false, |  | ||||||
| 			menuDef: this.$store.getters.nav({}), |  | ||||||
| 			navHidden: false, |  | ||||||
| 			wallpaper: localStorage.getItem('wallpaper') != null, |  | ||||||
| 			faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	computed: { |  | ||||||
| 		keymap(): any { |  | ||||||
| 			return { |  | ||||||
| 				'd': () => { |  | ||||||
| 					if (this.$store.state.device.syncDeviceDarkMode) return; |  | ||||||
| 					this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode }); |  | ||||||
| 				}, |  | ||||||
| 				'p': this.post, |  | ||||||
| 				'n': this.post, |  | ||||||
| 				's': this.search, |  | ||||||
| 				'h|/': this.help |  | ||||||
| 			}; |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		widgets(): any { |  | ||||||
| 			if (this.$store.getters.isSignedIn) { |  | ||||||
| 				const widgets = this.$store.state.deviceUser.widgets; |  | ||||||
| 				return { |  | ||||||
| 					left: widgets.filter(x => x.place === 'left'), |  | ||||||
| 					right: widgets.filter(x => x.place == null || x.place === 'right'), |  | ||||||
| 					mobile: widgets.filter(x => x.place === 'mobile'), |  | ||||||
| 				}; |  | ||||||
| 			} else { |  | ||||||
| 				const right = [{ |  | ||||||
| 					name: 'calendar', |  | ||||||
| 					id: 'b', place: 'right', data: {} |  | ||||||
| 				}, { |  | ||||||
| 					name: 'trends', |  | ||||||
| 					id: 'c', place: 'right', data: {} |  | ||||||
| 				}]; |  | ||||||
| 
 |  | ||||||
| 				if (this.$route.name !== 'index') { |  | ||||||
| 					right.unshift({ |  | ||||||
| 						name: 'welcome', |  | ||||||
| 						id: 'a', place: 'right', data: {} |  | ||||||
| 					}); |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				return { |  | ||||||
| 					left: [], |  | ||||||
| 					right, |  | ||||||
| 					mobile: [], |  | ||||||
| 				}; |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		menu(): string[] { |  | ||||||
| 			return this.$store.state.deviceUser.menu; |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		navIndicated(): boolean { |  | ||||||
| 			if (!this.$store.getters.isSignedIn) return false; |  | ||||||
| 			for (const def in this.menuDef) { |  | ||||||
| 				if (def === 'notifications') continue; // 通知は下にボタンとして表示されてるから |  | ||||||
| 				if (this.menuDef[def].indicated) return true; |  | ||||||
| 			} |  | ||||||
| 			return false; |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	watch: { |  | ||||||
| 		$route(to, from) { |  | ||||||
| 			this.pageKey++; |  | ||||||
| 			this.canBack = (window.history.length > 0 && !['index'].includes(to.name)); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		isDesktop() { |  | ||||||
| 			this.$nextTick(() => { |  | ||||||
| 				this.attachSticky(); |  | ||||||
| 			}); |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	created() { |  | ||||||
| 		document.documentElement.style.overflowY = 'scroll'; |  | ||||||
| 
 |  | ||||||
| 		if (this.$store.getters.isSignedIn) { |  | ||||||
| 			this.connection = this.$root.stream.useSharedConnection('main'); |  | ||||||
| 			this.connection.on('notification', this.onNotification); |  | ||||||
| 
 |  | ||||||
| 			if (this.$store.state.deviceUser.widgets.length === 0) { |  | ||||||
| 				this.$store.commit('deviceUser/setWidgets', [{ |  | ||||||
| 					name: 'calendar', |  | ||||||
| 					id: 'a', place: 'right', data: {} |  | ||||||
| 				}, { |  | ||||||
| 					name: 'notifications', |  | ||||||
| 					id: 'b', place: 'right', data: {} |  | ||||||
| 				}, { |  | ||||||
| 					name: 'trends', |  | ||||||
| 					id: 'c', place: 'right', data: {} |  | ||||||
| 				}]); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	mounted() { |  | ||||||
| 		this.adjustTitlePosition(); |  | ||||||
| 
 |  | ||||||
| 		const ro = new ResizeObserver((entries, observer) => { |  | ||||||
| 			this.adjustTitlePosition(); |  | ||||||
| 		}); |  | ||||||
| 
 |  | ||||||
| 		ro.observe(this.$refs.contents); |  | ||||||
| 
 |  | ||||||
| 		window.addEventListener('resize', this.adjustTitlePosition, { passive: true }); |  | ||||||
| 
 |  | ||||||
| 		if (!this.isDesktop) { |  | ||||||
| 			window.addEventListener('resize', () => { |  | ||||||
| 				if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true; |  | ||||||
| 			}, { passive: true }); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// widget follow |  | ||||||
| 		this.attachSticky(); |  | ||||||
| 
 |  | ||||||
| 		this.$nextTick(() => { |  | ||||||
| 			this.calcHeaderWidth(); |  | ||||||
| 		}); |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		adjustTitlePosition() { |  | ||||||
| 			const left = this.$refs.main.getBoundingClientRect().left - this.$refs.nav.$el.offsetWidth; |  | ||||||
| 			if (left >= 0) { |  | ||||||
| 				this.$refs.title.style.left = left + 'px'; |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		calcHeaderWidth() { |  | ||||||
| 			const navWidth = this.$refs.nav.$el.offsetWidth; |  | ||||||
| 			this.navHidden = navWidth === 0; |  | ||||||
| 			this.$refs.header.style.width = `calc(100% - ${navWidth}px)`; |  | ||||||
| 			this.adjustTitlePosition(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		showNav() { |  | ||||||
| 			this.$refs.nav.show(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		attachSticky() { |  | ||||||
| 			if (!this.isDesktop) return; |  | ||||||
| 			if (this.$store.state.device.fixedWidgetsPosition) return; |  | ||||||
| 
 |  | ||||||
| 			const stickyWidgetColumns = this.$refs.widgets.map(w => new StickySidebar(w.children[1], w.children[0], w.offsetTop)); |  | ||||||
| 			window.addEventListener('scroll', () => { |  | ||||||
| 				for (const stickyWidgetColumn of stickyWidgetColumns) { |  | ||||||
| 					stickyWidgetColumn.calc(window.scrollY); |  | ||||||
| 				} |  | ||||||
| 			}, { passive: true }); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		top() { |  | ||||||
| 			window.scroll({ top: 0, behavior: 'smooth' }); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		help() { |  | ||||||
| 			this.$router.push('/docs/keyboard-shortcut'); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		back() { |  | ||||||
| 			if (this.canBack) window.history.back(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onTransition() { |  | ||||||
| 			if (window._scroll) window._scroll(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		post() { |  | ||||||
| 			this.$root.post(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		search() { |  | ||||||
| 			if (this.searching) return; |  | ||||||
| 
 |  | ||||||
| 			this.$root.dialog({ |  | ||||||
| 				title: this.$t('search'), |  | ||||||
| 				input: true |  | ||||||
| 			}).then(async ({ canceled, result: query }) => { |  | ||||||
| 				if (canceled || query == null || query === '') return; |  | ||||||
| 
 |  | ||||||
| 				this.searching = true; |  | ||||||
| 				search(this, query).finally(() => { |  | ||||||
| 					this.searching = false; |  | ||||||
| 				}); |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		searchKeypress(e) { |  | ||||||
| 			if (e.keyCode === 13) { |  | ||||||
| 				this.searchWait = true; |  | ||||||
| 				search(this, this.searchQuery).finally(() => { |  | ||||||
| 					this.searchWait = false; |  | ||||||
| 					this.searchQuery = ''; |  | ||||||
| 				}); |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		async onNotification(notification) { |  | ||||||
| 			if (this.$store.state.i.mutingNotificationTypes.includes(notification.type)) { |  | ||||||
| 				return; |  | ||||||
| 			} |  | ||||||
| 			if (document.visibilityState === 'visible') { |  | ||||||
| 				this.$root.stream.send('readNotification', { |  | ||||||
| 					id: notification.id |  | ||||||
| 				}); |  | ||||||
| 
 |  | ||||||
| 				this.$root.new(await import('./components/toast.vue').then(m => m.default), { |  | ||||||
| 					notification |  | ||||||
| 				}); |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			this.$root.sound('notification'); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		widgetFunc(id) { |  | ||||||
| 			this.$refs[id][0].setting(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onWidgetSort() { |  | ||||||
| 			this.saveHome(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		async addWidget(place) { |  | ||||||
| 			const { canceled, result: widget } = await this.$root.dialog({ |  | ||||||
| 				type: null, |  | ||||||
| 				title: this.$t('chooseWidget'), |  | ||||||
| 				select: { |  | ||||||
| 					items: widgets.map(widget => ({ |  | ||||||
| 						value: widget, |  | ||||||
| 						text: this.$t('_widgets.' + widget), |  | ||||||
| 					})) |  | ||||||
| 				}, |  | ||||||
| 				showCancelButton: true |  | ||||||
| 			}); |  | ||||||
| 			if (canceled) return; |  | ||||||
| 
 |  | ||||||
| 			this.$store.commit('deviceUser/addWidget', { |  | ||||||
| 				name: widget, |  | ||||||
| 				id: uuid(), |  | ||||||
| 				place: place, |  | ||||||
| 				data: {} |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		removeWidget(widget) { |  | ||||||
| 			this.$store.commit('deviceUser/removeWidget', widget); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		saveHome() { |  | ||||||
| 			this.$store.commit('deviceUser/setWidgets', [...this.widgets.left, ...this.widgets.right, ...this.widgets.mobile]); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .mk-app { |  | ||||||
| 	$header-height: 60px; |  | ||||||
| 	$main-width: 670px; |  | ||||||
| 	$ui-font-size: 1em; // TODO: どこかに集約したい |  | ||||||
| 	$header-sub-hide-threshold: 1090px; |  | ||||||
| 	$left-widgets-hide-threshold: 1600px; |  | ||||||
| 	$right-widgets-hide-threshold: 1090px; |  | ||||||
| 
 |  | ||||||
| 	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ |  | ||||||
| 	min-height: calc(var(--vh, 1vh) * 100); |  | ||||||
| 	box-sizing: border-box; |  | ||||||
| 	padding-top: $header-height; |  | ||||||
| 
 |  | ||||||
| 	&, > .header > .body { |  | ||||||
| 		display: flex; |  | ||||||
| 		margin: 0 auto; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .header { |  | ||||||
| 		position: fixed; |  | ||||||
| 		z-index: 1000; |  | ||||||
| 		top: 0; |  | ||||||
| 		right: 0; |  | ||||||
| 		height: $header-height; |  | ||||||
| 		width: 100%; |  | ||||||
| 		//background-color: var(--panel); |  | ||||||
| 		-webkit-backdrop-filter: blur(32px); |  | ||||||
| 		backdrop-filter: blur(32px); |  | ||||||
| 		background-color: var(--header); |  | ||||||
| 		border-bottom: solid 1px var(--divider); |  | ||||||
| 
 |  | ||||||
| 		> .title { |  | ||||||
| 			position: relative; |  | ||||||
| 			line-height: $header-height; |  | ||||||
| 			height: $header-height; |  | ||||||
| 			max-width: $main-width; |  | ||||||
| 			text-align: center; |  | ||||||
| 
 |  | ||||||
| 			> .back { |  | ||||||
| 				position: absolute; |  | ||||||
| 				z-index: 1; |  | ||||||
| 				top: 0; |  | ||||||
| 				left: 0; |  | ||||||
| 				height: $header-height; |  | ||||||
| 				width: $header-height; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .body { |  | ||||||
| 				white-space: nowrap; |  | ||||||
| 				overflow: hidden; |  | ||||||
| 				text-overflow: ellipsis; |  | ||||||
| 				height: $header-height; |  | ||||||
| 
 |  | ||||||
| 				> .default { |  | ||||||
| 					padding: 0 $header-height; |  | ||||||
| 
 |  | ||||||
| 					> .avatar { |  | ||||||
| 						$size: 32px; |  | ||||||
| 						display: inline-block; |  | ||||||
| 						width: $size; |  | ||||||
| 						height: $size; |  | ||||||
| 						vertical-align: bottom; |  | ||||||
| 						margin: (($header-height - $size) / 2) 8px (($header-height - $size) / 2) 0; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					> .title { |  | ||||||
| 						display: inline-block; |  | ||||||
| 						font-size: $ui-font-size; |  | ||||||
| 						margin: 0; |  | ||||||
| 						line-height: $header-height; |  | ||||||
| 
 |  | ||||||
| 						> [data-icon] { |  | ||||||
| 							margin-right: 8px; |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> .custom { |  | ||||||
| 					position: absolute; |  | ||||||
| 					top: 0; |  | ||||||
| 					left: 0; |  | ||||||
| 					height: 100%; |  | ||||||
| 					width: 100%; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .sub { |  | ||||||
| 			$post-button-size: 42px; |  | ||||||
| 			$post-button-margin: (($header-height - $post-button-size) / 2); |  | ||||||
| 			display: flex; |  | ||||||
| 			align-items: center; |  | ||||||
| 			position: absolute; |  | ||||||
| 			top: 0; |  | ||||||
| 			right: 16px; |  | ||||||
| 			height: $header-height; |  | ||||||
| 
 |  | ||||||
| 			@media (max-width: $header-sub-hide-threshold) { |  | ||||||
| 				display: none; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .edit { |  | ||||||
| 				padding: 16px; |  | ||||||
| 
 |  | ||||||
| 				&.active { |  | ||||||
| 					color: var(--accent); |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .search { |  | ||||||
| 				position: relative; |  | ||||||
| 
 |  | ||||||
| 				> input { |  | ||||||
| 					width: 220px; |  | ||||||
| 					box-sizing: border-box; |  | ||||||
| 					margin-right: 8px; |  | ||||||
| 					padding: 0 12px 0 42px; |  | ||||||
| 					font-size: 1rem; |  | ||||||
| 					line-height: 38px; |  | ||||||
| 					border: none; |  | ||||||
| 					border-radius: 38px; |  | ||||||
| 					color: var(--fg); |  | ||||||
| 					background: var(--bg); |  | ||||||
| 					-webkit-appearance: textfield; |  | ||||||
| 
 |  | ||||||
| 					&:focus { |  | ||||||
| 						outline: none; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> [data-icon] { |  | ||||||
| 					position: absolute; |  | ||||||
| 					top: 0; |  | ||||||
| 					left: 16px; |  | ||||||
| 					height: 100%; |  | ||||||
| 					pointer-events: none; |  | ||||||
| 					font-size: 16px; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .post { |  | ||||||
| 				width: $post-button-size; |  | ||||||
| 				height: $post-button-size; |  | ||||||
| 				margin-left: $post-button-margin; |  | ||||||
| 				border-radius: 100%; |  | ||||||
| 				font-size: 16px; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .clock { |  | ||||||
| 				margin-left: 8px; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .contents { |  | ||||||
| 		display: flex; |  | ||||||
| 		margin: 0 auto; |  | ||||||
| 		min-width: 0; |  | ||||||
| 
 |  | ||||||
| 		&.wallpaper { |  | ||||||
| 			background: var(--wallpaperOverlay); |  | ||||||
| 			backdrop-filter: blur(4px); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		&.full { |  | ||||||
| 			width: 100%; |  | ||||||
| 
 |  | ||||||
| 			> main { |  | ||||||
| 				width: 100%; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .widgets { |  | ||||||
| 				display: none; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> main { |  | ||||||
| 			width: $main-width; |  | ||||||
| 			min-width: 0; |  | ||||||
| 
 |  | ||||||
| 			> .content { |  | ||||||
| 				> * { |  | ||||||
| 					// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ |  | ||||||
| 					min-height: calc((var(--vh, 1vh) * 100) - #{$header-height}); |  | ||||||
| 					box-sizing: border-box; |  | ||||||
| 					padding: var(--margin); |  | ||||||
| 
 |  | ||||||
| 					&.full { |  | ||||||
| 						padding: 0 var(--margin); |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .powerd-by { |  | ||||||
| 				font-size: 14px; |  | ||||||
| 				text-align: center; |  | ||||||
| 				margin: 32px 0; |  | ||||||
| 				visibility: hidden; |  | ||||||
| 
 |  | ||||||
| 				&.visible { |  | ||||||
| 					visibility: visible; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				&:not(.visible) { |  | ||||||
| 					@media (min-width: 850px) { |  | ||||||
| 						display: none; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				@media (max-width: 500px) { |  | ||||||
| 					margin-top: 16px; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> small { |  | ||||||
| 					display: block; |  | ||||||
| 					margin-top: 8px; |  | ||||||
| 					opacity: 0.5; |  | ||||||
| 
 |  | ||||||
| 					@media (max-width: 500px) { |  | ||||||
| 						margin-top: 4px; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .widgets { |  | ||||||
| 			padding: 0 var(--margin); |  | ||||||
| 			box-shadow: 1px 0 0 0 var(--divider), -1px 0 0 0 var(--divider); |  | ||||||
| 
 |  | ||||||
| 			&.fixed { |  | ||||||
| 				position: sticky; |  | ||||||
| 				overflow: auto; |  | ||||||
| 				// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ |  | ||||||
| 				height: calc((var(--vh, 1vh) * 100) - #{$header-height}); |  | ||||||
| 				top: $header-height; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			&:first-of-type { |  | ||||||
| 				order: -1; |  | ||||||
| 
 |  | ||||||
| 				@media (max-width: $left-widgets-hide-threshold) { |  | ||||||
| 					display: none; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			&.empty { |  | ||||||
| 				display: none; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			@media (max-width: $right-widgets-hide-threshold) { |  | ||||||
| 				display: none; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .container { |  | ||||||
| 				position: sticky; |  | ||||||
| 				height: min-content; |  | ||||||
| 				// ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ |  | ||||||
| 				min-height: calc((var(--vh, 1vh) * 100) - #{$header-height}); |  | ||||||
| 				padding: var(--margin) 0; |  | ||||||
| 				box-sizing: border-box; |  | ||||||
| 
 |  | ||||||
| 				> * { |  | ||||||
| 					margin: var(--margin) 0; |  | ||||||
| 					width: 300px; |  | ||||||
| 
 |  | ||||||
| 					&:first-child { |  | ||||||
| 						margin-top: 0; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					&:last-child { |  | ||||||
| 						margin-bottom: 0; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .add { |  | ||||||
| 				margin: 0 auto; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			.customize-container { |  | ||||||
| 				margin: 8px 0; |  | ||||||
| 
 |  | ||||||
| 				> header { |  | ||||||
| 					position: relative; |  | ||||||
| 					line-height: 32px; |  | ||||||
| 
 |  | ||||||
| 					> .handle { |  | ||||||
| 						padding: 0 8px; |  | ||||||
| 						cursor: move; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					> .remove { |  | ||||||
| 						position: absolute; |  | ||||||
| 						top: 0; |  | ||||||
| 						right: 0; |  | ||||||
| 						padding: 0 8px; |  | ||||||
| 						line-height: 32px; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> div { |  | ||||||
| 					padding: 8px; |  | ||||||
| 
 |  | ||||||
| 					> * { |  | ||||||
| 						pointer-events: none; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .post { |  | ||||||
| 		display: block; |  | ||||||
| 		position: fixed; |  | ||||||
| 		z-index: 1000; |  | ||||||
| 		bottom: 32px; |  | ||||||
| 		right: 32px; |  | ||||||
| 		width: 64px; |  | ||||||
| 		height: 64px; |  | ||||||
| 		border-radius: 100%; |  | ||||||
| 		box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); |  | ||||||
| 		font-size: 22px; |  | ||||||
| 
 |  | ||||||
| 		&.navHidden { |  | ||||||
| 			display: none; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		@media (min-width: ($header-sub-hide-threshold + 1px)) { |  | ||||||
| 			display: none; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .buttons { |  | ||||||
| 		position: fixed; |  | ||||||
| 		z-index: 1000; |  | ||||||
| 		bottom: 0; |  | ||||||
| 		padding: 0 32px 32px 32px; |  | ||||||
| 		display: flex; |  | ||||||
| 		width: 100%; |  | ||||||
| 		box-sizing: border-box; |  | ||||||
| 		background: linear-gradient(0deg, var(--bg), var(--X1)); |  | ||||||
| 
 |  | ||||||
| 		@media (max-width: 500px) { |  | ||||||
| 			padding: 0 16px 16px 16px; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		&:not(.navHidden) { |  | ||||||
| 			display: none; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .button { |  | ||||||
| 			position: relative; |  | ||||||
| 			padding: 0; |  | ||||||
| 			margin: auto; |  | ||||||
| 			width: 64px; |  | ||||||
| 			height: 64px; |  | ||||||
| 			border-radius: 100%; |  | ||||||
| 			box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); |  | ||||||
| 
 |  | ||||||
| 			&:first-child { |  | ||||||
| 				margin-left: 0; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			&:last-child { |  | ||||||
| 				margin-right: 0; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> * { |  | ||||||
| 				font-size: 22px; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			&:disabled { |  | ||||||
| 				cursor: default; |  | ||||||
| 
 |  | ||||||
| 				> * { |  | ||||||
| 					opacity: 0.5; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			&:not(.post) { |  | ||||||
| 				background: var(--panel); |  | ||||||
| 				color: var(--fg); |  | ||||||
| 
 |  | ||||||
| 				&:hover { |  | ||||||
| 					background: var(--X2); |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				> i { |  | ||||||
| 					position: absolute; |  | ||||||
| 					top: 0; |  | ||||||
| 					left: 0; |  | ||||||
| 					color: var(--indicator); |  | ||||||
| 					font-size: 16px; |  | ||||||
| 					animation: blink 1s infinite; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -6,11 +6,11 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { toUnicode } from 'punycode'; | import { toUnicode } from 'punycode'; | ||||||
| import { host } from '../config'; | import { host } from '@/config'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: ['user', 'detail'], | 	props: ['user', 'detail'], | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  |  | ||||||
|  | @ -34,10 +34,11 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import * as tinycolor from 'tinycolor2'; | import * as tinycolor from 'tinycolor2'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			now: new Date(), | 			now: new Date(), | ||||||
|  | @ -127,7 +128,7 @@ export default Vue.extend({ | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		this.enabled = false; | 		this.enabled = false; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,12 +1,12 @@ | ||||||
| <template> | <template> | ||||||
| <div class="swhvrteh" @contextmenu.prevent="() => {}"> | <div class="swhvrteh _popup _shadow" @contextmenu.prevent="() => {}"> | ||||||
| 	<ol class="users" ref="suggests" v-if="type === 'user'"> | 	<ol class="users" ref="suggests" v-if="type === 'user'"> | ||||||
| 		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1" class="user"> | 		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1" class="user"> | ||||||
| 			<img class="avatar" :src="user.avatarUrl"/> | 			<img class="avatar" :src="user.avatarUrl"/> | ||||||
| 			<span class="name"> | 			<span class="name"> | ||||||
| 				<mk-user-name :user="user" :key="user.id"/> | 				<MkUserName :user="user" :key="user.id"/> | ||||||
| 			</span> | 			</span> | ||||||
| 			<span class="username">@{{ user | acct }}</span> | 			<span class="username">@{{ acct(user) }}</span> | ||||||
| 		</li> | 		</li> | ||||||
| 		<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $t('selectUser') }}</li> | 		<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $t('selectUser') }}</li> | ||||||
| 	</ol> | 	</ol> | ||||||
|  | @ -28,12 +28,13 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { emojilist } from '../../misc/emojilist'; | import { emojilist } from '../../misc/emojilist'; | ||||||
| import contains from '../scripts/contains'; | import contains from '@/scripts/contains'; | ||||||
| import { twemojiSvgBase } from '../../misc/twemoji-base'; | import { twemojiSvgBase } from '../../misc/twemoji-base'; | ||||||
| import { getStaticImageUrl } from '../scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | ||||||
| import MkUserSelect from './user-select.vue'; | import { acct } from '@/filters/user'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| type EmojiDef = { | type EmojiDef = { | ||||||
| 	emoji: string; | 	emoji: string; | ||||||
|  | @ -74,7 +75,7 @@ for (const x of lib) { | ||||||
| 
 | 
 | ||||||
| emjdb.sort((a, b) => a.name.length - b.name.length); | emjdb.sort((a, b) => a.name.length - b.name.length); | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		type: { | 		type: { | ||||||
| 			type: String, | 			type: String, | ||||||
|  | @ -91,11 +92,6 @@ export default Vue.extend({ | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		complete: { |  | ||||||
| 			type: Function, |  | ||||||
| 			required: true, |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		close: { | 		close: { | ||||||
| 			type: Function, | 			type: Function, | ||||||
| 			required: true, | 			required: true, | ||||||
|  | @ -110,7 +106,14 @@ export default Vue.extend({ | ||||||
| 			type: Number, | 			type: Number, | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
|  | 
 | ||||||
|  | 		showing: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: true | ||||||
| 		}, | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	emits: ['done', 'closed'], | ||||||
| 
 | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  | @ -135,6 +138,14 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	watch: { | ||||||
|  | 		showing() { | ||||||
|  | 			if (!this.showing) { | ||||||
|  | 				this.$emit('closed'); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
| 	updated() { | 	updated() { | ||||||
| 		this.setPosition(); | 		this.setPosition(); | ||||||
| 	}, | 	}, | ||||||
|  | @ -189,7 +200,7 @@ export default Vue.extend({ | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		this.textarea.removeEventListener('keydown', this.onKeydown); | 		this.textarea.removeEventListener('keydown', this.onKeydown); | ||||||
| 
 | 
 | ||||||
| 		for (const el of Array.from(document.querySelectorAll('body *'))) { | 		for (const el of Array.from(document.querySelectorAll('body *'))) { | ||||||
|  | @ -198,6 +209,11 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
|  | 		complete(type, value) { | ||||||
|  | 			this.$emit('done', { type, value }); | ||||||
|  | 			this.$emit('closed'); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		setPosition() { | 		setPosition() { | ||||||
| 			if (this.x + this.$el.offsetWidth > window.innerWidth) { | 			if (this.x + this.$el.offsetWidth > window.innerWidth) { | ||||||
| 				this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px'; | 				this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px'; | ||||||
|  | @ -236,8 +252,8 @@ export default Vue.extend({ | ||||||
| 					this.users = users; | 					this.users = users; | ||||||
| 					this.fetching = false; | 					this.fetching = false; | ||||||
| 				} else { | 				} else { | ||||||
| 					this.$root.api('users/search', { | 					os.api('users/search-by-username-and-host', { | ||||||
| 						query: this.q, | 						username: this.q, | ||||||
| 						limit: 10, | 						limit: 10, | ||||||
| 						detail: false | 						detail: false | ||||||
| 					}).then(users => { | 					}).then(users => { | ||||||
|  | @ -260,7 +276,7 @@ export default Vue.extend({ | ||||||
| 						this.hashtags = hashtags; | 						this.hashtags = hashtags; | ||||||
| 						this.fetching = false; | 						this.fetching = false; | ||||||
| 					} else { | 					} else { | ||||||
| 						this.$root.api('hashtags/search', { | 						os.api('hashtags/search', { | ||||||
| 							query: this.q, | 							query: this.q, | ||||||
| 							limit: 30 | 							limit: 30 | ||||||
| 						}).then(hashtags => { | 						}).then(hashtags => { | ||||||
|  | @ -374,14 +390,13 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 		chooseUser() { | 		chooseUser() { | ||||||
| 			this.close(); | 			this.close(); | ||||||
| 			const vm = this.$root.new(MkUserSelect, {}); | 			os.selectUser().then(user => { | ||||||
| 			vm.$once('selected', user => { |  | ||||||
| 				this.complete('user', user); | 				this.complete('user', user); | ||||||
| 			}); |  | ||||||
| 			vm.$once('closed', () => { |  | ||||||
| 				this.textarea.focus(); | 				this.textarea.focus(); | ||||||
| 			}); | 			}); | ||||||
| 		} | 		}, | ||||||
|  | 
 | ||||||
|  | 		acct | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  | @ -393,9 +408,6 @@ export default Vue.extend({ | ||||||
| 	max-width: 100%; | 	max-width: 100%; | ||||||
| 	margin-top: calc(1em + 8px); | 	margin-top: calc(1em + 8px); | ||||||
| 	overflow: hidden; | 	overflow: hidden; | ||||||
| 	background: var(--panel); |  | ||||||
| 	border: solid 1px rgba(#000, 0.1); |  | ||||||
| 	border-radius: 4px; |  | ||||||
| 	transition: top 0.1s ease, left 0.1s ease; | 	transition: top 0.1s ease, left 0.1s ease; | ||||||
| 
 | 
 | ||||||
| 	> ol { | 	> ol { | ||||||
|  |  | ||||||
|  | @ -1,17 +1,19 @@ | ||||||
| <template> | <template> | ||||||
| <span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick"> | <span class="eiwwqkts" :class="{ cat }" :title="acct(user)" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick"> | ||||||
| 	<img class="inner" :src="url"/> | 	<img class="inner" :src="url"/> | ||||||
| </span> | </span> | ||||||
| <router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id"> | <router-link class="eiwwqkts" :class="{ cat }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id"> | ||||||
| 	<img class="inner" :src="url"/> | 	<img class="inner" :src="url"/> | ||||||
| </router-link> | </router-link> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { getStaticImageUrl } from '../scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | ||||||
|  | import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; | ||||||
|  | import { acct, userPage } from '../filters/user'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		user: { | 		user: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -30,6 +32,7 @@ export default Vue.extend({ | ||||||
| 			default: false | 			default: false | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  | 	emits: ['click'], | ||||||
| 	computed: { | 	computed: { | ||||||
| 		cat(): boolean { | 		cat(): boolean { | ||||||
| 			return this.user.isCat; | 			return this.user.isCat; | ||||||
|  | @ -42,25 +45,19 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 	watch: { | 	watch: { | ||||||
| 		'user.avatarBlurhash'() { | 		'user.avatarBlurhash'() { | ||||||
| 			this.$el.style.color = this.getBlurhashAvgColor(this.user.avatarBlurhash); | 			if (this.$el == null) return; | ||||||
|  | 			this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash); | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		this.$el.style.color = this.getBlurhashAvgColor(this.user.avatarBlurhash); | 		this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash); | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		getBlurhashAvgColor(s) { |  | ||||||
| 			return typeof s == 'string' |  | ||||||
| 				? '#' + [...s.slice(2, 6)] |  | ||||||
| 						.map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x)) |  | ||||||
| 						.reduce((a, c) => a * 83 + c, 0) |  | ||||||
| 						.toString(16) |  | ||||||
| 						.padStart(6, '0') |  | ||||||
| 				: undefined; |  | ||||||
| 		}, |  | ||||||
| 		onClick(e) { | 		onClick(e) { | ||||||
| 			this.$emit('click', e); | 			this.$emit('click', e); | ||||||
| 		} | 		}, | ||||||
|  | 		acct, | ||||||
|  | 		userPage | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,15 +1,16 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;"> | 	<div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;"> | ||||||
| 		<mk-avatar :user="user" style="width:32px;height:32px;"/> | 		<MkAvatar :user="user" style="width:32px;height:32px;"/> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		userIds: { | 		userIds: { | ||||||
| 			required: true | 			required: true | ||||||
|  | @ -21,7 +22,7 @@ export default Vue.extend({ | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	async created() { | 	async created() { | ||||||
| 		this.us = await this.$root.api('users/show', { | 		this.us = await os.api('users/show', { | ||||||
| 			userIds: this.userIds | 			userIds: this.userIds | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1,12 +1,12 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<span v-if="!available">{{ $t('waiting') }}<mk-ellipsis/></span> | 	<span v-if="!available">{{ $t('waiting') }}<MkEllipsis/></span> | ||||||
| 	<div ref="captcha"></div> | 	<div ref="captcha"></div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| 
 | 
 | ||||||
| type Captcha = { | type Captcha = { | ||||||
| 	render(container: string | Node, options: { | 	render(container: string | Node, options: { | ||||||
|  | @ -28,8 +28,9 @@ declare global { | ||||||
| 	interface Window extends CaptchaContainer { | 	interface Window extends CaptchaContainer { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		provider: { | 		provider: { | ||||||
| 			type: String, | 			type: String, | ||||||
|  | @ -88,7 +89,7 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		this.reset(); | 		this.reset(); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -110,7 +111,7 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		callback(response?: string) { | 		callback(response?: string) { | ||||||
| 			this.$emit('input', typeof response == 'string' ? response : null); | 			this.$emit('update:value', typeof response == 'string' ? response : null); | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -6,23 +6,24 @@ | ||||||
| > | > | ||||||
| 	<template v-if="!wait"> | 	<template v-if="!wait"> | ||||||
| 		<template v-if="isFollowing"> | 		<template v-if="isFollowing"> | ||||||
| 			<span v-if="full">{{ $t('unfollow') }}</span><fa :icon="faMinus"/> | 			<span v-if="full">{{ $t('unfollow') }}</span><Fa :icon="faMinus"/> | ||||||
| 		</template> | 		</template> | ||||||
| 		<template v-else> | 		<template v-else> | ||||||
| 			<span v-if="full">{{ $t('follow') }}</span><fa :icon="faPlus"/> | 			<span v-if="full">{{ $t('follow') }}</span><Fa :icon="faPlus"/> | ||||||
| 		</template> | 		</template> | ||||||
| 	</template> | 	</template> | ||||||
| 	<template v-else> | 	<template v-else> | ||||||
| 		<span v-if="full">{{ $t('processing') }}</span><fa :icon="faSpinner" pulse fixed-width/> | 		<span v-if="full">{{ $t('processing') }}</span><Fa :icon="faSpinner" pulse fixed-width/> | ||||||
| 	</template> | 	</template> | ||||||
| </button> | </button> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faSpinner, faPlus, faMinus, } from '@fortawesome/free-solid-svg-icons'; | import { faSpinner, faPlus, faMinus, } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		channel: { | 		channel: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -49,12 +50,12 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 			try { | 			try { | ||||||
| 				if (this.isFollowing) { | 				if (this.isFollowing) { | ||||||
| 					await this.$root.api('channels/unfollow', { | 					await os.api('channels/unfollow', { | ||||||
| 						channelId: this.channel.id | 						channelId: this.channel.id | ||||||
| 					}); | 					}); | ||||||
| 					this.isFollowing = false; | 					this.isFollowing = false; | ||||||
| 				} else { | 				} else { | ||||||
| 					await this.$root.api('channels/follow', { | 					await os.api('channels/follow', { | ||||||
| 						channelId: this.channel.id | 						channelId: this.channel.id | ||||||
| 					}); | 					}); | ||||||
| 					this.isFollowing = true; | 					this.isFollowing = true; | ||||||
|  |  | ||||||
|  | @ -2,28 +2,42 @@ | ||||||
| <router-link :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1"> | <router-link :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1"> | ||||||
| 	<div class="banner" v-if="channel.bannerUrl" :style="`background-image: url('${channel.bannerUrl}')`"> | 	<div class="banner" v-if="channel.bannerUrl" :style="`background-image: url('${channel.bannerUrl}')`"> | ||||||
| 		<div class="fade"></div> | 		<div class="fade"></div> | ||||||
| 		<div class="name"><fa :icon="faSatelliteDish"/> {{ channel.name }}</div> | 		<div class="name"><Fa :icon="faSatelliteDish"/> {{ channel.name }}</div> | ||||||
| 		<div class="status"> | 		<div class="status"> | ||||||
| 			<div><fa :icon="faUsers" fixed-width/><i18n path="_channel.usersCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.usersCount }}</b></i18n></div> | 			<div> | ||||||
| 			<div><fa :icon="faPencilAlt" fixed-width/><i18n path="_channel.notesCount" tag="span" style="margin-left: 4px;"><b place="n">{{ channel.notesCount }}</b></i18n></div> | 				<Fa :icon="faUsers" fixed-width/> | ||||||
|  | 				<i18n-t keypath="_channel.usersCount" tag="span" style="margin-left: 4px;"> | ||||||
|  | 					<template #n> | ||||||
|  | 						<b>{{ channel.usersCount }}</b> | ||||||
|  | 					</template> | ||||||
|  | 				</i18n-t> | ||||||
|  | 			</div> | ||||||
|  | 			<div> | ||||||
|  | 				<Fa :icon="faPencilAlt" fixed-width/> | ||||||
|  | 				<i18n-t keypath="_channel.notesCount" tag="span" style="margin-left: 4px;"> | ||||||
|  | 					<template #n> | ||||||
|  | 						<b>{{ channel.notesCount }}</b> | ||||||
|  | 					</template> | ||||||
|  | 				</i18n-t> | ||||||
|  | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<article v-if="channel.description"> | 	<article v-if="channel.description"> | ||||||
| 		<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p> | 		<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p> | ||||||
| 	</article> | 	</article> | ||||||
| 	<footer> | 	<footer> | ||||||
| 		<span> | 		<span v-if="channel.lastNotedAt"> | ||||||
| 			{{ $t('updatedAt') }}: <mk-time :time="channel.lastNotedAt"/> | 			{{ $t('updatedAt') }}: <MkTime :time="channel.lastNotedAt"/> | ||||||
| 		</span> | 		</span> | ||||||
| 	</footer> | 	</footer> | ||||||
| </router-link> | </router-link> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faSatelliteDish, faUsers, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; | import { faSatelliteDish, faUsers, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		channel: { | 		channel: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -44,7 +58,6 @@ export default Vue.extend({ | ||||||
| 	display: block; | 	display: block; | ||||||
| 	overflow: hidden; | 	overflow: hidden; | ||||||
| 	width: 100%; | 	width: 100%; | ||||||
| 	border: 1px solid var(--divider); |  | ||||||
| 
 | 
 | ||||||
| 	&:hover { | 	&:hover { | ||||||
| 		text-decoration: none; | 		text-decoration: none; | ||||||
|  |  | ||||||
|  | @ -1,13 +1,14 @@ | ||||||
| <template> | <template> | ||||||
| <x-prism :inline="inline" :language="prismLang">{{ code }}</x-prism> | <XPrism :inline="inline" :language="prismLang">{{ code }}</XPrism> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import 'prismjs'; | import 'prismjs'; | ||||||
| import 'prismjs/themes/prism-okaidia.css'; | import 'prismjs/themes/prism-okaidia.css'; | ||||||
| import XPrism from 'vue-prism-component'; | import XPrism from 'vue-prism-component';import * as os from '@/os'; | ||||||
| export default Vue.extend({ | 
 | ||||||
|  | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XPrism | 		XPrism | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,12 +1,13 @@ | ||||||
| <template> | <template> | ||||||
| <x-code :code="code" :lang="lang" :inline="inline"/> | <XCode :code="code" :lang="lang" :inline="inline"/> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent, defineAsyncComponent } from 'vue'; | ||||||
| export default Vue.extend({ | 
 | ||||||
|  | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XCode: () => import('./code-core.vue').then(m => m.default) | 		XCode: defineAsyncComponent(() => import('./code-core.vue')) | ||||||
| 	}, | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		code: { | 		code: { | ||||||
|  |  | ||||||
|  | @ -1,16 +1,16 @@ | ||||||
| <template> | <template> | ||||||
| <button class="nrvgflfuaxwgkxoynpnumyookecqrrvh _button" @click="toggle"> | <button class="nrvgflfu _button" @click="toggle"> | ||||||
| 	<b>{{ value ? this.$t('_cw.hide') : this.$t('_cw.show') }}</b> | 	<b>{{ value ? $t('_cw.hide') : $t('_cw.show') }}</b> | ||||||
| 	<span v-if="!value">{{ this.label }}</span> | 	<span v-if="!value">{{ label }}</span> | ||||||
| </button> | </button> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { length } from 'stringz'; | import { length } from 'stringz'; | ||||||
| import { concat } from '../../prelude/array'; | import { concat } from '../../prelude/array'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		value: { | 		value: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
|  | @ -36,14 +36,14 @@ export default Vue.extend({ | ||||||
| 		length, | 		length, | ||||||
| 
 | 
 | ||||||
| 		toggle() { | 		toggle() { | ||||||
| 			this.$emit('input', !this.value); | 			this.$emit('update:value', !this.value); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .nrvgflfuaxwgkxoynpnumyookecqrrvh { | .nrvgflfu { | ||||||
| 	display: inline-block; | 	display: inline-block; | ||||||
| 	padding: 4px 8px; | 	padding: 4px 8px; | ||||||
| 	font-size: 0.7em; | 	font-size: 0.7em; | ||||||
|  |  | ||||||
|  | @ -1,22 +1,22 @@ | ||||||
| <template> | <template> | ||||||
| <component :is="$store.state.device.animation ? 'transition-group' : 'div'" class="sqadhkmv _list_" name="list" tag="div" :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'"> | <transition-group class="sqadhkmv _list_" name="list" tag="div" :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'"> | ||||||
| 	<template v-for="(item, i) in items"> | 	<template v-for="(item, i) in items"> | ||||||
| 		<slot :item="item"></slot> | 		<slot :item="item"></slot> | ||||||
| 		<div class="separator" v-if="showDate(i, item)" :key="item.id + '_date'"> | 		<div class="separator" v-if="showDate(i, item)" :key="item.id + '_date'"> | ||||||
| 			<p class="date"> | 			<p class="date"> | ||||||
| 				<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span> | 				<span><Fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span> | ||||||
| 				<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span> | 				<span>{{ getDateText(items[i + 1].createdAt) }}<Fa class="icon" :icon="faAngleDown"/></span> | ||||||
| 			</p> | 			</p> | ||||||
| 		</div> | 		</div> | ||||||
| 	</template> | 	</template> | ||||||
| </component> | </transition-group> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons'; | import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		items: { | 		items: { | ||||||
| 			type: Array, | 			type: Array, | ||||||
|  | @ -82,14 +82,14 @@ export default Vue.extend({ | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	&[data-direction="up"] { | 	&[data-direction="up"] { | ||||||
| 		> .list-enter { | 		> .list-enter-from { | ||||||
| 			opacity: 0; | 			opacity: 0; | ||||||
| 			transform: translateY(64px); | 			transform: translateY(64px); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	&[data-direction="down"] { | 	&[data-direction="down"] { | ||||||
| 		> .list-enter { | 		> .list-enter-from { | ||||||
| 			opacity: 0; | 			opacity: 0; | ||||||
| 			transform: translateY(-64px); | 			transform: translateY(-64px); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -1,20 +1,21 @@ | ||||||
| <template> | <template> | ||||||
| <x-column :menu="menu" :column="column" :is-stacked="isStacked"> | <XColumn :menu="menu" :column="column" :is-stacked="isStacked"> | ||||||
| 	<template #header> | 	<template #header> | ||||||
| 		<fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span> | 		<Fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span> | ||||||
| 	</template> | 	</template> | ||||||
| 
 | 
 | ||||||
| 	<x-timeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/> | 	<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/> | ||||||
| </x-column> | </XColumn> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faSatellite, faCog } from '@fortawesome/free-solid-svg-icons'; | import { faSatellite, faCog } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import XColumn from './column.vue'; | import XColumn from './column.vue'; | ||||||
| import XTimeline from '../timeline.vue'; | import XTimeline from '../timeline.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XColumn, | 		XColumn, | ||||||
| 		XTimeline, | 		XTimeline, | ||||||
|  | @ -59,8 +60,8 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		async setAntenna() { | 		async setAntenna() { | ||||||
| 			const antennas = await this.$root.api('antennas/list'); | 			const antennas = await os.api('antennas/list'); | ||||||
| 			const { canceled, result: antenna } = await this.$root.dialog({ | 			const { canceled, result: antenna } = await os.dialog({ | ||||||
| 				title: this.$t('selectAntenna'), | 				title: this.$t('selectAntenna'), | ||||||
| 				type: null, | 				type: null, | ||||||
| 				select: { | 				select: { | ||||||
|  | @ -72,7 +73,7 @@ export default Vue.extend({ | ||||||
| 				showCancelButton: true | 				showCancelButton: true | ||||||
| 			}); | 			}); | ||||||
| 			if (canceled) return; | 			if (canceled) return; | ||||||
| 			Vue.set(this.column, 'antennaId', antenna.id); | 			this.column.antennaId = antenna.id; | ||||||
| 			this.$store.commit('deviceUser/updateDeckColumn', this.column); | 			this.$store.commit('deviceUser/updateDeckColumn', this.column); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,17 +1,17 @@ | ||||||
| <template> | <template> | ||||||
| <!-- TODO: リファクタの余地がありそう --> | <!-- TODO: リファクタの余地がありそう --> | ||||||
| <x-widgets-column v-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | <XWidgetsColumn v-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||||
| <x-notifications-column v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | <XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||||
| <x-tl-column v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | <XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||||
| <x-list-column v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | <XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||||
| <x-antenna-column v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | <XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||||
| <!-- TODO: <x-tl-column v-else-if="column.type === 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> --> | <!-- TODO: <XTlColumn v-else-if="column.type === 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> --> | ||||||
| <x-mentions-column v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | <XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||||
| <x-direct-column v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | <XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import XTlColumn from './tl-column.vue'; | import XTlColumn from './tl-column.vue'; | ||||||
| import XAntennaColumn from './antenna-column.vue'; | import XAntennaColumn from './antenna-column.vue'; | ||||||
| import XListColumn from './list-column.vue'; | import XListColumn from './list-column.vue'; | ||||||
|  | @ -20,7 +20,7 @@ import XWidgetsColumn from './widgets-column.vue'; | ||||||
| import XMentionsColumn from './mentions-column.vue'; | import XMentionsColumn from './mentions-column.vue'; | ||||||
| import XDirectColumn from './direct-column.vue'; | import XDirectColumn from './direct-column.vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XTlColumn, | 		XTlColumn, | ||||||
| 		XAntennaColumn, | 		XAntennaColumn, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> | <!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため --> | ||||||
| <section class="dnpfarvg _panel _narrow_" :class="{ naked, paged: isMainColumn, _close_: !isMainColumn, active, isStacked, draghover, dragging, dropready }" | <section class="dnpfarvg _panel _narrow_" :class="{ paged: isMainColumn, naked, _close_: !isMainColumn, active, isStacked, draghover, dragging, dropready }" | ||||||
| 	@dragover.prevent.stop="onDragover" | 	@dragover.prevent.stop="onDragover" | ||||||
| 	@dragleave="onDragleave" | 	@dragleave="onDragleave" | ||||||
| 	@drop.prevent.stop="onDrop" | 	@drop.prevent.stop="onDrop" | ||||||
|  | @ -15,15 +15,14 @@ | ||||||
| 		@contextmenu.prevent.stop="onContextmenu" | 		@contextmenu.prevent.stop="onContextmenu" | ||||||
| 	> | 	> | ||||||
| 		<button class="toggleActive _button" @click="toggleActive" v-if="isStacked"> | 		<button class="toggleActive _button" @click="toggleActive" v-if="isStacked"> | ||||||
| 			<template v-if="active"><fa :icon="faAngleUp"/></template> | 			<template v-if="active"><Fa :icon="faAngleUp"/></template> | ||||||
| 			<template v-else><fa :icon="faAngleDown"/></template> | 			<template v-else><Fa :icon="faAngleDown"/></template> | ||||||
| 		</button> | 		</button> | ||||||
| 		<div class="action"> | 		<div class="action"> | ||||||
| 			<slot name="action"></slot> | 			<slot name="action"></slot> | ||||||
| 		</div> | 		</div> | ||||||
| 		<span class="header"><slot name="header"></slot></span> | 		<span class="header"><slot name="header"></slot></span> | ||||||
| 		<button v-if="!isMainColumn" class="menu _button" ref="menu" @click.stop="showMenu"><fa :icon="faCaretDown"/></button> | 		<button v-if="!isMainColumn" class="menu _button" ref="menu" @click.stop="showMenu"><Fa :icon="faCaretDown"/></button> | ||||||
| 		<button v-else-if="$route.name !== 'index'" class="close _button" @click.stop="close"><fa :icon="faTimes"/></button> |  | ||||||
| 	</header> | 	</header> | ||||||
| 	<div ref="body" v-show="active"> | 	<div ref="body" v-show="active"> | ||||||
| 		<slot></slot> | 		<slot></slot> | ||||||
|  | @ -32,11 +31,12 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; | import { faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faArrowRight, faArrowLeft, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faWindowMaximize, faTrashAlt, faWindowRestore } from '@fortawesome/free-regular-svg-icons'; | import { faWindowMaximize, faTrashAlt, faWindowRestore } from '@fortawesome/free-regular-svg-icons'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		column: { | 		column: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -71,7 +71,7 @@ export default Vue.extend({ | ||||||
| 			dragging: false, | 			dragging: false, | ||||||
| 			draghover: false, | 			draghover: false, | ||||||
| 			dropready: false, | 			dropready: false, | ||||||
| 			faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, faTimes, | 			faArrowUp, faArrowDown, faAngleUp, faAngleDown, faCaretDown, | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -86,10 +86,10 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 		keymap(): any { | 		keymap(): any { | ||||||
| 			return { | 			return { | ||||||
| 				'shift+up': () => this.$parent.$emit('parentFocus', 'up'), | 				'shift+up': () => this.$parent.$emit('parent-focus', 'up'), | ||||||
| 				'shift+down': () => this.$parent.$emit('parentFocus', 'down'), | 				'shift+down': () => this.$parent.$emit('parent-focus', 'down'), | ||||||
| 				'shift+left': () => this.$parent.$emit('parentFocus', 'left'), | 				'shift+left': () => this.$parent.$emit('parent-focus', 'left'), | ||||||
| 				'shift+right': () => this.$parent.$emit('parentFocus', 'right'), | 				'shift+right': () => this.$parent.$emit('parent-focus', 'right'), | ||||||
| 			}; | 			}; | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  | @ -100,21 +100,21 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		dragging(v) { | 		dragging(v) { | ||||||
| 			this.$root.$emit(v ? 'deck.column.dragStart' : 'deck.column.dragEnd'); | 			os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'); | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		if (!this.isMainColumn) { | 		if (!this.isMainColumn) { | ||||||
| 			this.$root.$on('deck.column.dragStart', this.onOtherDragStart); | 			os.deckGlobalEvents.on('column.dragStart', this.onOtherDragStart); | ||||||
| 			this.$root.$on('deck.column.dragEnd', this.onOtherDragEnd); | 			os.deckGlobalEvents.on('column.dragEnd', this.onOtherDragEnd); | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		if (!this.isMainColumn) { | 		if (!this.isMainColumn) { | ||||||
| 			this.$root.$off('deck.column.dragStart', this.onOtherDragStart); | 			os.deckGlobalEvents.off('column.dragStart', this.onOtherDragStart); | ||||||
| 			this.$root.$off('deck.column.dragEnd', this.onOtherDragEnd); | 			os.deckGlobalEvents.off('column.dragEnd', this.onOtherDragEnd); | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -137,7 +137,7 @@ export default Vue.extend({ | ||||||
| 				icon: faPencilAlt, | 				icon: faPencilAlt, | ||||||
| 				text: this.$t('rename'), | 				text: this.$t('rename'), | ||||||
| 				action: () => { | 				action: () => { | ||||||
| 					this.$root.dialog({ | 					os.dialog({ | ||||||
| 						title: this.$t('rename'), | 						title: this.$t('rename'), | ||||||
| 						input: { | 						input: { | ||||||
| 							default: this.column.name, | 							default: this.column.name, | ||||||
|  | @ -207,14 +207,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		showMenu() { | 		showMenu() { | ||||||
| 			this.$root.menu({ | 			os.modalMenu(this.getMenu(), this.$refs.menu); | ||||||
| 				items: this.getMenu(), |  | ||||||
| 				source: this.$refs.menu, |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		close() { |  | ||||||
| 			this.$router.push('/'); |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		goTop() { | 		goTop() { | ||||||
|  | @ -232,7 +225,7 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			e.dataTransfer.effectAllowed = 'move'; | 			e.dataTransfer.effectAllowed = 'move'; | ||||||
| 			e.dataTransfer.setData('mk-deck-column', this.column.id); | 			e.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, this.column.id); | ||||||
| 			this.dragging = true; | 			this.dragging = true; | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | @ -254,7 +247,7 @@ export default Vue.extend({ | ||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			const isDeckColumn = e.dataTransfer.types[0] == 'mk-deck-column'; | 			const isDeckColumn = e.dataTransfer.types[0] == _DATA_TRANSFER_DECK_COLUMN_; | ||||||
| 
 | 
 | ||||||
| 			e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; | 			e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none'; | ||||||
| 
 | 
 | ||||||
|  | @ -267,9 +260,9 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 		onDrop(e) { | 		onDrop(e) { | ||||||
| 			this.draghover = false; | 			this.draghover = false; | ||||||
| 			this.$root.$emit('deck.column.dragEnd'); | 			os.deckGlobalEvents.emit('column.dragEnd'); | ||||||
| 
 | 
 | ||||||
| 			const id = e.dataTransfer.getData('mk-deck-column'); | 			const id = e.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_); | ||||||
| 			if (id != null && id != '') { | 			if (id != null && id != '') { | ||||||
| 				this.$store.commit('deviceUser/swapDeckColumn', { | 				this.$store.commit('deviceUser/swapDeckColumn', { | ||||||
| 					a: this.column.id, | 					a: this.column.id, | ||||||
|  | @ -285,9 +278,11 @@ export default Vue.extend({ | ||||||
| .dnpfarvg { | .dnpfarvg { | ||||||
| 	$header-height: 42px; | 	$header-height: 42px; | ||||||
| 
 | 
 | ||||||
|  | 	--section-padding: 10px; | ||||||
|  | 
 | ||||||
| 	height: 100%; | 	height: 100%; | ||||||
| 	overflow: hidden; | 	overflow: hidden; | ||||||
| 	box-shadow: 0 0 0 1px var(--deckColumnBorder); | 	contain: content; | ||||||
| 
 | 
 | ||||||
| 	&.draghover { | 	&.draghover { | ||||||
| 		box-shadow: 0 0 0 2px var(--focus); | 		box-shadow: 0 0 0 2px var(--focus); | ||||||
|  | @ -341,7 +336,6 @@ export default Vue.extend({ | ||||||
| 	&.paged { | 	&.paged { | ||||||
| 		> div { | 		> div { | ||||||
| 			background: var(--bg); | 			background: var(--bg); | ||||||
| 			padding: var(--margin); |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -379,8 +373,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 		> .toggleActive, | 		> .toggleActive, | ||||||
| 		> .action > *, | 		> .action > *, | ||||||
| 		> .menu, | 		> .menu { | ||||||
| 		> .close { |  | ||||||
| 			z-index: 1; | 			z-index: 1; | ||||||
| 			width: $header-height; | 			width: $header-height; | ||||||
| 			line-height: $header-height; | 			line-height: $header-height; | ||||||
|  | @ -408,8 +401,7 @@ export default Vue.extend({ | ||||||
| 			display: none; | 			display: none; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		> .menu, | 		> .menu { | ||||||
| 		> .close { |  | ||||||
| 			margin-left: auto; | 			margin-left: auto; | ||||||
| 			margin-right: -16px; | 			margin-right: -16px; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -1,19 +1,20 @@ | ||||||
| <template> | <template> | ||||||
| <x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu"> | <XColumn :name="name" :column="column" :is-stacked="isStacked" :menu="menu"> | ||||||
| 	<template #header><fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template> | 	<template #header><Fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template> | ||||||
| 
 | 
 | ||||||
| 	<x-notes :pagination="pagination" @before="before()" @after="after()"/> | 	<XNotes :pagination="pagination" @before="before()" @after="after()"/> | ||||||
| </x-column> | </XColumn> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faEnvelope } from '@fortawesome/free-solid-svg-icons'; | import { faEnvelope } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import Progress from '../../scripts/loading'; | import Progress from '@/scripts/loading'; | ||||||
| import XColumn from './column.vue'; | import XColumn from './column.vue'; | ||||||
| import XNotes from '../notes.vue'; | import XNotes from '../notes.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XColumn, | 		XColumn, | ||||||
| 		XNotes | 		XNotes | ||||||
|  |  | ||||||
|  | @ -1,20 +1,21 @@ | ||||||
| <template> | <template> | ||||||
| <x-column :menu="menu" :column="column" :is-stacked="isStacked"> | <XColumn :menu="menu" :column="column" :is-stacked="isStacked"> | ||||||
| 	<template #header> | 	<template #header> | ||||||
| 		<fa :icon="faListUl"/><span style="margin-left: 8px;">{{ column.name }}</span> | 		<Fa :icon="faListUl"/><span style="margin-left: 8px;">{{ column.name }}</span> | ||||||
| 	</template> | 	</template> | ||||||
| 
 | 
 | ||||||
| 	<x-timeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/> | 	<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => $emit('loaded')"/> | ||||||
| </x-column> | </XColumn> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faListUl, faCog } from '@fortawesome/free-solid-svg-icons'; | import { faListUl, faCog } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import XColumn from './column.vue'; | import XColumn from './column.vue'; | ||||||
| import XTimeline from '../timeline.vue'; | import XTimeline from '../timeline.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XColumn, | 		XColumn, | ||||||
| 		XTimeline, | 		XTimeline, | ||||||
|  | @ -59,8 +60,8 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		async setList() { | 		async setList() { | ||||||
| 			const lists = await this.$root.api('users/lists/list'); | 			const lists = await os.api('users/lists/list'); | ||||||
| 			const { canceled, result: list } = await this.$root.dialog({ | 			const { canceled, result: list } = await os.dialog({ | ||||||
| 				title: this.$t('selectList'), | 				title: this.$t('selectList'), | ||||||
| 				type: null, | 				type: null, | ||||||
| 				select: { | 				select: { | ||||||
|  | @ -72,7 +73,7 @@ export default Vue.extend({ | ||||||
| 				showCancelButton: true | 				showCancelButton: true | ||||||
| 			}); | 			}); | ||||||
| 			if (canceled) return; | 			if (canceled) return; | ||||||
| 			Vue.set(this.column, 'listId', list.id); | 			this.column.listId = list.id; | ||||||
| 			this.$store.commit('deviceUser/updateDeckColumn', this.column); | 			this.$store.commit('deviceUser/updateDeckColumn', this.column); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,19 +1,20 @@ | ||||||
| <template> | <template> | ||||||
| <x-column :column="column" :is-stacked="isStacked" :menu="menu"> | <XColumn :column="column" :is-stacked="isStacked" :menu="menu"> | ||||||
| 	<template #header><fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template> | 	<template #header><Fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template> | ||||||
| 
 | 
 | ||||||
| 	<x-notes :pagination="pagination" @before="before()" @after="after()"/> | 	<XNotes :pagination="pagination" @before="before()" @after="after()"/> | ||||||
| </x-column> | </XColumn> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faAt } from '@fortawesome/free-solid-svg-icons'; | import { faAt } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import Progress from '../../scripts/loading'; | import Progress from '@/scripts/loading'; | ||||||
| import XColumn from './column.vue'; | import XColumn from './column.vue'; | ||||||
| import XNotes from '../notes.vue'; | import XNotes from '../notes.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XColumn, | 		XColumn, | ||||||
| 		XNotes | 		XNotes | ||||||
|  |  | ||||||
|  | @ -1,19 +1,20 @@ | ||||||
| <template> | <template> | ||||||
| <x-column :column="column" :is-stacked="isStacked" :menu="menu"> | <XColumn :column="column" :is-stacked="isStacked" :menu="menu"> | ||||||
| 	<template #header><fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template> | 	<template #header><Fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template> | ||||||
| 
 | 
 | ||||||
| 	<x-notifications :include-types="column.includingTypes"/> | 	<XNotifications :include-types="column.includingTypes"/> | ||||||
| </x-column> | </XColumn> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faCog } from '@fortawesome/free-solid-svg-icons'; | import { faCog } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faBell } from '@fortawesome/free-regular-svg-icons'; | import { faBell } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import XColumn from './column.vue'; | import XColumn from './column.vue'; | ||||||
| import XNotifications from '../notifications.vue'; | import XNotifications from '../notifications.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XColumn, | 		XColumn, | ||||||
| 		XNotifications | 		XNotifications | ||||||
|  | @ -42,12 +43,17 @@ export default Vue.extend({ | ||||||
| 			icon: faCog, | 			icon: faCog, | ||||||
| 			text: this.$t('notificationSetting'), | 			text: this.$t('notificationSetting'), | ||||||
| 			action: async () => { | 			action: async () => { | ||||||
| 				this.$root.new(await import('../notification-setting-window.vue').then(m => m.default), { | 				os.popup(await import('@/components/notification-setting-window.vue'), { | ||||||
| 					includingTypes: this.column.includingTypes, | 					includingTypes: this.column.includingTypes, | ||||||
| 				}).$on('ok', async ({ includingTypes }) => { | 				}, { | ||||||
| 					this.$set(this.column, 'includingTypes', includingTypes); | 					done: async (res) => { | ||||||
| 					this.$store.commit('deviceUser/updateDeckColumn', this.column); | 						const { includingTypes } = res; | ||||||
|  | 						this.$store.commit('deviceUser/updateDeckColumn', { | ||||||
|  | 							...this.column, | ||||||
|  | 							includingTypes: includingTypes | ||||||
| 						}); | 						}); | ||||||
|  | 					}, | ||||||
|  | 				}, 'closed'); | ||||||
| 			} | 			} | ||||||
| 		}]; | 		}]; | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,31 +1,32 @@ | ||||||
| <template> | <template> | ||||||
| <x-column :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState"> | <XColumn :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState"> | ||||||
| 	<template #header> | 	<template #header> | ||||||
| 		<fa v-if="column.tl === 'home'" :icon="faHome"/> | 		<Fa v-if="column.tl === 'home'" :icon="faHome"/> | ||||||
| 		<fa v-else-if="column.tl === 'local'" :icon="faComments"/> | 		<Fa v-else-if="column.tl === 'local'" :icon="faComments"/> | ||||||
| 		<fa v-else-if="column.tl === 'social'" :icon="faShareAlt"/> | 		<Fa v-else-if="column.tl === 'social'" :icon="faShareAlt"/> | ||||||
| 		<fa v-else-if="column.tl === 'global'" :icon="faGlobe"/> | 		<Fa v-else-if="column.tl === 'global'" :icon="faGlobe"/> | ||||||
| 		<span style="margin-left: 8px;">{{ column.name }}</span> | 		<span style="margin-left: 8px;">{{ column.name }}</span> | ||||||
| 	</template> | 	</template> | ||||||
| 
 | 
 | ||||||
| 	<div class="iwaalbte" v-if="disabled"> | 	<div class="iwaalbte" v-if="disabled"> | ||||||
| 		<p> | 		<p> | ||||||
| 			<fa :icon="faMinusCircle"/> | 			<Fa :icon="faMinusCircle"/> | ||||||
| 			{{ $t('disabled-timeline.title') }} | 			{{ $t('disabled-timeline.title') }} | ||||||
| 		</p> | 		</p> | ||||||
| 		<p class="desc">{{ $t('disabled-timeline.description') }}</p> | 		<p class="desc">{{ $t('disabled-timeline.description') }}</p> | ||||||
| 	</div> | 	</div> | ||||||
| 	<x-timeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/> | 	<XTimeline v-else-if="column.tl" ref="timeline" :src="column.tl" @after="() => $emit('loaded')" @queue="queueUpdated" @note="onNote" :key="column.tl"/> | ||||||
| </x-column> | </XColumn> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faMinusCircle, faHome, faComments, faShareAlt, faGlobe, faCog } from '@fortawesome/free-solid-svg-icons'; | import { faMinusCircle, faHome, faComments, faShareAlt, faGlobe, faCog } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import XColumn from './column.vue'; | import XColumn from './column.vue'; | ||||||
| import XTimeline from '../timeline.vue'; | import XTimeline from '../timeline.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XColumn, | 		XColumn, | ||||||
| 		XTimeline, | 		XTimeline, | ||||||
|  | @ -78,7 +79,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		async setType() { | 		async setType() { | ||||||
| 			const { canceled, result: src } = await this.$root.dialog({ | 			const { canceled, result: src } = await os.dialog({ | ||||||
| 				title: this.$t('timeline'), | 				title: this.$t('timeline'), | ||||||
| 				type: null, | 				type: null, | ||||||
| 				select: { | 				select: { | ||||||
|  | @ -99,7 +100,7 @@ export default Vue.extend({ | ||||||
| 				} | 				} | ||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 			Vue.set(this.column, 'tl', src); | 			this.column.tl = src; | ||||||
| 			this.$store.commit('deviceUser/updateDeckColumn', this.column); | 			this.$store.commit('deviceUser/updateDeckColumn', this.column); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,47 +1,46 @@ | ||||||
| <template> | <template> | ||||||
| <x-column :menu="menu" :naked="true" :column="column" :is-stacked="isStacked"> | <XColumn :menu="menu" :naked="true" :column="column" :is-stacked="isStacked"> | ||||||
| 	<template #header><fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template> | 	<template #header><Fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template> | ||||||
| 
 | 
 | ||||||
| 	<div class="wtdtxvec"> | 	<div class="wtdtxvec"> | ||||||
| 		<template v-if="edit"> | 		<template v-if="edit"> | ||||||
| 			<header> | 			<header> | ||||||
| 				<mk-select v-model="widgetAdderSelected" style="margin-bottom: var(--margin)"> | 				<MkSelect v-model:value="widgetAdderSelected" style="margin-bottom: var(--margin)"> | ||||||
| 					<template #label>{{ $t('selectWidget') }}</template> | 					<template #label>{{ $t('selectWidget') }}</template> | ||||||
| 					<option v-for="widget in widgets" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option> | 					<option v-for="widget in widgets" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option> | ||||||
| 				</mk-select> | 				</MkSelect> | ||||||
| 				<mk-button inline @click="addWidget" primary><fa :icon="faPlus"/> {{ $t('add') }}</mk-button> | 				<MkButton inline @click="addWidget" primary><Fa :icon="faPlus"/> {{ $t('add') }}</MkButton> | ||||||
| 				<mk-button inline @click="edit = false">{{ $t('close') }}</mk-button> | 				<MkButton inline @click="edit = false">{{ $t('close') }}</MkButton> | ||||||
| 			</header> | 			</header> | ||||||
| 			<x-draggable | 			<XDraggable | ||||||
| 				:list="column.widgets" | 				:list="column.widgets" | ||||||
| 				animation="150" | 				animation="150" | ||||||
| 				@sort="onWidgetSort" | 				@sort="onWidgetSort" | ||||||
| 			> | 			> | ||||||
| 				<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)"> | 				<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)"> | ||||||
| 					<button class="remove _button" @click.prevent.stop="removeWidget(widget)"><fa :icon="faTimes"/></button> | 					<button class="remove _button" @click.prevent.stop="removeWidget(widget)"><Fa :icon="faTimes"/></button> | ||||||
| 					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/> | 					<component :is="`mkw-${widget.name}`" :widget="widget" :setting-callback="setting => settings[widget.id] = setting" :column="column"/> | ||||||
| 				</div> | 				</div> | ||||||
| 			</x-draggable> | 			</XDraggable> | ||||||
| 		</template> | 		</template> | ||||||
| 		<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :column="column"/> | 		<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column"/> | ||||||
| 	</div> | 	</div> | ||||||
| </x-column> | </XColumn> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent, defineAsyncComponent } from 'vue'; | ||||||
| import * as XDraggable from 'vuedraggable'; |  | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; | import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import MkSelect from '../../components/ui/select.vue'; | import MkSelect from '@/components/ui/select.vue'; | ||||||
| import MkButton from '../../components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import XColumn from './column.vue'; | import XColumn from './column.vue'; | ||||||
| import { widgets } from '../../widgets'; | import { widgets } from '../../widgets'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XColumn, | 		XColumn, | ||||||
| 		XDraggable, | 		XDraggable: defineAsyncComponent(() => import('vue-draggable-next').then(x => x.VueDraggableNext)), | ||||||
| 		MkSelect, | 		MkSelect, | ||||||
| 		MkButton, | 		MkButton, | ||||||
| 	}, | 	}, | ||||||
|  | @ -63,6 +62,7 @@ export default Vue.extend({ | ||||||
| 			menu: null, | 			menu: null, | ||||||
| 			widgetAdderSelected: null, | 			widgetAdderSelected: null, | ||||||
| 			widgets, | 			widgets, | ||||||
|  | 			settings: {}, | ||||||
| 			faWindowMaximize, faTimes, faPlus | 			faWindowMaximize, faTimes, faPlus | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
|  | @ -79,7 +79,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		widgetFunc(id) { | 		widgetFunc(id) { | ||||||
| 			this.$refs[id][0].setting(); | 			this.settings[id](); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onWidgetSort() { | 		onWidgetSort() { | ||||||
|  |  | ||||||
|  | @ -1,31 +1,23 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mk-dialog" :class="{ iconOnly }"> | <MkModal ref="modal" @click="done(true)" @closed="$emit('closed')"> | ||||||
| 	<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear> | 	<div class="mk-dialog"> | ||||||
| 		<div class="bg _modalBg" ref="bg" @click="onBgClick" v-if="show"></div> |  | ||||||
| 	</transition> |  | ||||||
| 	<transition :name="$store.state.device.animation ? 'dialog' : ''" appear @after-leave="() => { destroyDom(); }"> |  | ||||||
| 		<div class="main" ref="main" v-if="show"> |  | ||||||
| 			<template v-if="type == 'signin'"> |  | ||||||
| 				<mk-signin/> |  | ||||||
| 			</template> |  | ||||||
| 			<template v-else> |  | ||||||
| 		<div class="icon" v-if="icon"> | 		<div class="icon" v-if="icon"> | ||||||
| 					<fa :icon="icon"/> | 			<Fa :icon="icon"/> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="icon" v-else-if="!input && !select && !user" :class="type"> | 		<div class="icon" v-else-if="!input && !select && !user" :class="type"> | ||||||
| 					<fa :icon="faCheck" v-if="type === 'success'"/> | 			<Fa :icon="faCheck" v-if="type === 'success'"/> | ||||||
| 					<fa :icon="faTimesCircle" v-if="type === 'error'"/> | 			<Fa :icon="faTimesCircle" v-if="type === 'error'"/> | ||||||
| 					<fa :icon="faExclamationTriangle" v-if="type === 'warning'"/> | 			<Fa :icon="faExclamationTriangle" v-if="type === 'warning'"/> | ||||||
| 					<fa :icon="faInfoCircle" v-if="type === 'info'"/> | 			<Fa :icon="faInfoCircle" v-if="type === 'info'"/> | ||||||
| 					<fa :icon="faQuestionCircle" v-if="type === 'question'"/> | 			<Fa :icon="faQuestionCircle" v-if="type === 'question'"/> | ||||||
| 					<fa :icon="faSpinner" pulse v-if="type === 'waiting'"/> | 			<Fa :icon="faSpinner" pulse v-if="type === 'waiting'"/> | ||||||
| 		</div> | 		</div> | ||||||
| 		<header v-if="title" v-html="title"></header> | 		<header v-if="title" v-html="title"></header> | ||||||
| 		<header v-if="title == null && user">{{ $t('enterUsername') }}</header> | 		<header v-if="title == null && user">{{ $t('enterUsername') }}</header> | ||||||
| 		<div class="body" v-if="text" v-html="text"></div> | 		<div class="body" v-if="text" v-html="text"></div> | ||||||
| 				<mk-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></mk-input> | 		<MkInput v-if="input" v-model:value="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></MkInput> | ||||||
| 				<mk-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></mk-input> | 		<MkInput v-if="user" v-model:value="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></MkInput> | ||||||
| 				<mk-select v-if="select" v-model="selectedValue" autofocus> | 		<MkSelect v-if="select" v-model:value="selectedValue" autofocus> | ||||||
| 			<template v-if="select.items"> | 			<template v-if="select.items"> | ||||||
| 				<option v-for="item in select.items" :value="item.value">{{ item.text }}</option> | 				<option v-for="item in select.items" :value="item.value">{{ item.text }}</option> | ||||||
| 			</template> | 			</template> | ||||||
|  | @ -34,36 +26,35 @@ | ||||||
| 					<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option> | 					<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option> | ||||||
| 				</optgroup> | 				</optgroup> | ||||||
| 			</template> | 			</template> | ||||||
| 				</mk-select> | 		</MkSelect> | ||||||
| 				<div class="buttons" v-if="!iconOnly && (showOkButton || showCancelButton) && !actions"> | 		<div class="buttons" v-if="(showOkButton || showCancelButton) && !actions"> | ||||||
| 					<mk-button inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('ok') : $t('gotIt') }}</mk-button> | 			<MkButton inline @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('ok') : $t('gotIt') }}</MkButton> | ||||||
| 					<mk-button inline @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('cancel') }}</mk-button> | 			<MkButton inline @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('cancel') }}</MkButton> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="buttons" v-if="actions"> | 		<div class="buttons" v-if="actions"> | ||||||
| 					<mk-button v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</mk-button> | 			<MkButton v-for="action in actions" inline @click="() => { action.callback(); close(); }" :primary="action.primary" :key="action.text">{{ action.text }}</MkButton> | ||||||
| 		</div> | 		</div> | ||||||
| 			</template> |  | ||||||
| 		</div> |  | ||||||
| 	</transition> |  | ||||||
| 	</div> | 	</div> | ||||||
|  | </MkModal> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faSpinner, faInfoCircle, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons'; | import { faSpinner, faInfoCircle, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons'; | import { faTimesCircle, faQuestionCircle } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import MkButton from './ui/button.vue'; | import MkModal from '@/components/ui/modal.vue'; | ||||||
| import MkInput from './ui/input.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkSelect from './ui/select.vue'; | import MkInput from '@/components/ui/input.vue'; | ||||||
| import MkSignin from './signin.vue'; | import MkSelect from '@/components/ui/select.vue'; | ||||||
| import parseAcct from '../../misc/acct/parse'; | import parseAcct from '../../misc/acct/parse'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
|  | 		MkModal, | ||||||
| 		MkButton, | 		MkButton, | ||||||
| 		MkInput, | 		MkInput, | ||||||
| 		MkSelect, | 		MkSelect, | ||||||
| 		MkSignin, |  | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -107,19 +98,12 @@ export default Vue.extend({ | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			default: true | 			default: true | ||||||
| 		}, | 		}, | ||||||
| 		iconOnly: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			default: false |  | ||||||
| 		}, |  | ||||||
| 		autoClose: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			default: false |  | ||||||
| 		} |  | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	emits: ['done', 'closed'], | ||||||
|  | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			show: true, |  | ||||||
| 			inputValue: this.input && this.input.default ? this.input.default : null, | 			inputValue: this.input && this.input.default ? this.input.default : null, | ||||||
| 			userInputValue: null, | 			userInputValue: null, | ||||||
| 			selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null, | 			selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null, | ||||||
|  | @ -131,63 +115,51 @@ export default Vue.extend({ | ||||||
| 	watch: { | 	watch: { | ||||||
| 		userInputValue() { | 		userInputValue() { | ||||||
| 			if (this.user) { | 			if (this.user) { | ||||||
| 				this.$root.api('users/show', parseAcct(this.userInputValue)).then(u => { | 				os.api('users/show', parseAcct(this.userInputValue)).then(u => { | ||||||
| 					this.canOk = u != null; | 					this.canOk = u != null; | ||||||
| 				}).catch(() => { | 				}).catch(() => { | ||||||
| 					this.canOk = false; | 					this.canOk = false; | ||||||
| 				}); | 				}); | ||||||
| 			} | 			} | ||||||
| 		} | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		if (this.user) this.canOk = false; | 		if (this.user) this.canOk = false; | ||||||
| 
 | 
 | ||||||
| 		if (this.autoClose) { |  | ||||||
| 			setTimeout(() => { |  | ||||||
| 				this.close(); |  | ||||||
| 			}, 1000); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		document.addEventListener('keydown', this.onKeydown); | 		document.addEventListener('keydown', this.onKeydown); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		document.removeEventListener('keydown', this.onKeydown); | 		document.removeEventListener('keydown', this.onKeydown); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
|  | 		done(canceled, result?) { | ||||||
|  | 			this.$emit('done', { canceled, result }); | ||||||
|  | 			this.$refs.modal.close(); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		async ok() { | 		async ok() { | ||||||
| 			if (!this.canOk) return; | 			if (!this.canOk) return; | ||||||
| 			if (!this.showOkButton) return; | 			if (!this.showOkButton) return; | ||||||
| 
 | 
 | ||||||
| 			if (this.user) { | 			if (this.user) { | ||||||
| 				const user = await this.$root.api('users/show', parseAcct(this.userInputValue)); | 				const user = await os.api('users/show', parseAcct(this.userInputValue)); | ||||||
| 				if (user) { | 				if (user) { | ||||||
| 					this.$emit('ok', user); | 					this.done(false, user); | ||||||
| 					this.close(); |  | ||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
| 				const result = | 				const result = | ||||||
| 					this.input ? this.inputValue : | 					this.input ? this.inputValue : | ||||||
| 					this.select ? this.selectedValue : | 					this.select ? this.selectedValue : | ||||||
| 					true; | 					true; | ||||||
| 				this.$emit('ok', result); | 				this.done(false, result); | ||||||
| 				this.close(); |  | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		cancel() { | 		cancel() { | ||||||
| 			this.$emit('cancel'); | 			this.done(true); | ||||||
| 			this.close(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		close() { |  | ||||||
| 			if (!this.show) return; |  | ||||||
| 			this.show = false; |  | ||||||
| 			this.$el.style.pointerEvents = 'none'; |  | ||||||
| 			(this.$refs.bg as any).style.pointerEvents = 'none'; |  | ||||||
| 			(this.$refs.main as any).style.pointerEvents = 'none'; |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onBgClick() { | 		onBgClick() { | ||||||
|  | @ -214,46 +186,12 @@ export default Vue.extend({ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .dialog-enter-active, .dialog-leave-active { |  | ||||||
| 	transition: opacity 0.3s, transform 0.3s !important; |  | ||||||
| } |  | ||||||
| .dialog-enter, .dialog-leave-to { |  | ||||||
| 	opacity: 0; |  | ||||||
| 	transform: scale(0.9); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .bg-fade-enter-active, .bg-fade-leave-active { |  | ||||||
| 	transition: opacity 0.3s !important; |  | ||||||
| } |  | ||||||
| .bg-fade-enter, .bg-fade-leave-to { |  | ||||||
| 	opacity: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .mk-dialog { | .mk-dialog { | ||||||
| 	display: flex; | 	position: relative; | ||||||
| 	align-items: center; |  | ||||||
| 	justify-content: center; |  | ||||||
| 	position: fixed; |  | ||||||
| 	z-index: 30000; |  | ||||||
| 	top: 0; |  | ||||||
| 	left: 0; |  | ||||||
| 	width: 100%; |  | ||||||
| 	height: 100%; |  | ||||||
| 
 |  | ||||||
| 	&.iconOnly > .main { |  | ||||||
| 		min-width: 0; |  | ||||||
| 		width: initial; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .main { |  | ||||||
| 		display: block; |  | ||||||
| 		position: fixed; |  | ||||||
| 		margin: auto; |  | ||||||
| 	padding: 32px; | 	padding: 32px; | ||||||
| 	min-width: 320px; | 	min-width: 320px; | ||||||
| 	max-width: 480px; | 	max-width: 480px; | ||||||
| 	box-sizing: border-box; | 	box-sizing: border-box; | ||||||
| 		width: calc(100% - 32px); |  | ||||||
| 	text-align: center; | 	text-align: center; | ||||||
| 	background: var(--panel); | 	background: var(--panel); | ||||||
| 	border-radius: var(--radius); | 	border-radius: var(--radius); | ||||||
|  | @ -305,5 +243,4 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| } |  | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,20 +1,20 @@ | ||||||
| <template> | <template> | ||||||
| <div class="zdjebgpv" ref="thumbnail"> | <div class="zdjebgpv" ref="thumbnail"> | ||||||
| 	<img-with-blurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/> | 	<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/> | ||||||
| 	<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/> | 	<Fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/> | ||||||
| 	<fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/> | 	<Fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/> | ||||||
| 	<fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/> | 	<Fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/> | ||||||
| 	<fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/> | 	<Fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/> | ||||||
| 	<fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/> | 	<Fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/> | ||||||
| 	<fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/> | 	<Fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/> | ||||||
| 	<fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/> | 	<Fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/> | ||||||
| 	<fa :icon="faFile" class="icon" v-else/> | 	<Fa :icon="faFile" class="icon" v-else/> | ||||||
| 	<fa :icon="faFilm" class="icon-sub" v-if="isThumbnailAvailable && is === 'video'"/> | 	<Fa :icon="faFilm" class="icon-sub" v-if="isThumbnailAvailable && is === 'video'"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { | import { | ||||||
| 	faFile, | 	faFile, | ||||||
| 	faFileAlt, | 	faFileAlt, | ||||||
|  | @ -28,7 +28,7 @@ import { | ||||||
| 	} from '@fortawesome/free-solid-svg-icons'; | 	} from '@fortawesome/free-solid-svg-icons'; | ||||||
| import ImgWithBlurhash from './img-with-blurhash.vue'; | import ImgWithBlurhash from './img-with-blurhash.vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		ImgWithBlurhash | 		ImgWithBlurhash | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,24 +1,34 @@ | ||||||
| <template> | <template> | ||||||
| <x-window ref="window" :width="800" :height="500" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="(type === 'file') && (selected.length === 0)" @ok="ok()"> | <XModalWindow ref="dialog" | ||||||
|  | 	:width="800" | ||||||
|  | 	:height="500" | ||||||
|  | 	:with-ok-button="true" | ||||||
|  | 	:ok-button-disabled="(type === 'file') && (selected.length === 0)" | ||||||
|  | 	@click="cancel()" | ||||||
|  | 	@close="cancel()" | ||||||
|  | 	@ok="ok()" | ||||||
|  | 	@closed="$emit('closed')" | ||||||
|  | > | ||||||
| 	<template #header> | 	<template #header> | ||||||
| 		{{ multiple ? ((type === 'file') ? $t('selectFiles') : $t('selectFolders')) : ((type === 'file') ? $t('selectFile') : $t('selectFolder')) }} | 		{{ multiple ? ((type === 'file') ? $t('selectFiles') : $t('selectFolders')) : ((type === 'file') ? $t('selectFile') : $t('selectFolder')) }} | ||||||
| 		<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ selected.length | number }})</span> | 		<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span> | ||||||
| 	</template> | 	</template> | ||||||
| 	<div> | 	<div> | ||||||
| 		<x-drive :multiple="multiple" @change-selection="onChangeSelection" :select="type"/> | 		<XDrive :multiple="multiple" @changeSelection="onChangeSelection" @selected="ok()" :select="type"/> | ||||||
| 	</div> | 	</div> | ||||||
| </x-window> | </XModalWindow> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import XDrive from './drive.vue'; | import XDrive from './drive.vue'; | ||||||
| import XWindow from './window.vue'; | import XModalWindow from '@/components/ui/modal-window.vue'; | ||||||
|  | import number from '@/filters/number'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XDrive, | 		XDrive, | ||||||
| 		XWindow, | 		XModalWindow, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -33,6 +43,8 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	emits: ['done', 'closed'], | ||||||
|  | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			selected: [] | 			selected: [] | ||||||
|  | @ -41,13 +53,20 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		ok() { | 		ok() { | ||||||
| 			this.$emit('selected', this.selected); | 			this.$emit('done', this.selected); | ||||||
| 			this.$refs.window.close(); | 			this.$refs.dialog.close(); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		cancel() { | ||||||
|  | 			this.$emit('done'); | ||||||
|  | 			this.$refs.dialog.close(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onChangeSelection(xs) { | 		onChangeSelection(xs) { | ||||||
| 			this.selected = xs; | 			this.selected = xs; | ||||||
| 		} | 		}, | ||||||
|  | 
 | ||||||
|  | 		number | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,7 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <div class="ncvczrfv" | <div class="ncvczrfv" | ||||||
| 	:data-is-selected="isSelected" | 	:class="{ isSelected }" | ||||||
| 	@click="onClick" | 	@click="onClick" | ||||||
|  | 	@contextmenu.stop="onContextmenu" | ||||||
| 	draggable="true" | 	draggable="true" | ||||||
| 	@dragstart="onDragstart" | 	@dragstart="onDragstart" | ||||||
| 	@dragend="onDragend" | 	@dragend="onDragend" | ||||||
|  | @ -20,7 +21,7 @@ | ||||||
| 		<p>{{ $t('nsfw') }}</p> | 		<p>{{ $t('nsfw') }}</p> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<x-file-thumbnail class="thumbnail" :file="file" fit="contain"/> | 	<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||||
| 
 | 
 | ||||||
| 	<p class="name"> | 	<p class="name"> | ||||||
| 		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> | 		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> | ||||||
|  | @ -30,17 +31,17 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; | import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import copyToClipboard from '../scripts/copy-to-clipboard'; |  | ||||||
| //import updateAvatar from '../api/update-avatar'; |  | ||||||
| //import updateBanner from '../api/update-banner'; |  | ||||||
| import XFileThumbnail from './drive-file-thumbnail.vue'; |  | ||||||
| import { faDownload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; | import { faDownload, faLink, faICursor, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||||
|  | import MkDriveFileThumbnail from './drive-file-thumbnail.vue'; | ||||||
|  | import bytes from '../filters/bytes'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XFileThumbnail | 		MkDriveFileThumbnail | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -60,6 +61,8 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	emits: ['chosen'], | ||||||
|  | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			isDragging: false | 			isDragging: false | ||||||
|  | @ -72,17 +75,13 @@ export default Vue.extend({ | ||||||
| 			return this.$parent; | 			return this.$parent; | ||||||
| 		}, | 		}, | ||||||
| 		title(): string { | 		title(): string { | ||||||
| 			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.size)}`; | 			return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`; | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		onClick(ev) { | 		getMenu() { | ||||||
| 			if (this.selectMode) { | 			return [{ | ||||||
| 				this.$emit('chosen', this.file); |  | ||||||
| 			} else { |  | ||||||
| 				this.$root.menu({ |  | ||||||
| 					items: [{ |  | ||||||
| 				text: this.$t('rename'), | 				text: this.$t('rename'), | ||||||
| 				icon: faICursor, | 				icon: faICursor, | ||||||
| 				action: this.rename | 				action: this.rename | ||||||
|  | @ -104,16 +103,26 @@ export default Vue.extend({ | ||||||
| 			}, null, { | 			}, null, { | ||||||
| 				text: this.$t('delete'), | 				text: this.$t('delete'), | ||||||
| 				icon: faTrashAlt, | 				icon: faTrashAlt, | ||||||
|  | 				danger: true, | ||||||
| 				action: this.deleteFile | 				action: this.deleteFile | ||||||
| 					}], | 			}]; | ||||||
| 					source: ev.currentTarget || ev.target, | 		}, | ||||||
| 				}); | 
 | ||||||
|  | 		onClick(ev) { | ||||||
|  | 			if (this.selectMode) { | ||||||
|  | 				this.$emit('chosen', this.file); | ||||||
|  | 			} else { | ||||||
|  | 				os.modalMenu(this.getMenu(), ev.currentTarget || ev.target); | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		onContextmenu(e) { | ||||||
|  | 			os.contextMenu(this.getMenu(), e); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		onDragstart(e) { | 		onDragstart(e) { | ||||||
| 			e.dataTransfer.effectAllowed = 'move'; | 			e.dataTransfer.effectAllowed = 'move'; | ||||||
| 			e.dataTransfer.setData('mk_drive_file', JSON.stringify(this.file)); | 			e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file)); | ||||||
| 			this.isDragging = true; | 			this.isDragging = true; | ||||||
| 
 | 
 | ||||||
| 			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる | 			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる | ||||||
|  | @ -127,7 +136,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		rename() { | 		rename() { | ||||||
| 			this.$root.dialog({ | 			os.dialog({ | ||||||
| 				title: this.$t('renameFile'), | 				title: this.$t('renameFile'), | ||||||
| 				input: { | 				input: { | ||||||
| 					placeholder: this.$t('inputNewFileName'), | 					placeholder: this.$t('inputNewFileName'), | ||||||
|  | @ -136,7 +145,7 @@ export default Vue.extend({ | ||||||
| 				} | 				} | ||||||
| 			}).then(({ canceled, result: name }) => { | 			}).then(({ canceled, result: name }) => { | ||||||
| 				if (canceled) return; | 				if (canceled) return; | ||||||
| 				this.$root.api('drive/files/update', { | 				os.api('drive/files/update', { | ||||||
| 					fileId: this.file.id, | 					fileId: this.file.id, | ||||||
| 					name: name | 					name: name | ||||||
| 				}); | 				}); | ||||||
|  | @ -144,7 +153,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		toggleSensitive() { | 		toggleSensitive() { | ||||||
| 			this.$root.api('drive/files/update', { | 			os.api('drive/files/update', { | ||||||
| 				fileId: this.file.id, | 				fileId: this.file.id, | ||||||
| 				isSensitive: !this.file.isSensitive | 				isSensitive: !this.file.isSensitive | ||||||
| 			}); | 			}); | ||||||
|  | @ -152,18 +161,15 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 		copyUrl() { | 		copyUrl() { | ||||||
| 			copyToClipboard(this.file.url); | 			copyToClipboard(this.file.url); | ||||||
| 			this.$root.dialog({ | 			os.success(); | ||||||
| 				type: 'success', |  | ||||||
| 				iconOnly: true, autoClose: true |  | ||||||
| 			}); |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		setAsAvatar() { | 		setAsAvatar() { | ||||||
| 			updateAvatar(this.$root)(this.file); | 			os.updateAvatar(this.file); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		setAsBanner() { | 		setAsBanner() { | ||||||
| 			updateBanner(this.$root)(this.file); | 			os.updateBanner(this.file); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		addApp() { | 		addApp() { | ||||||
|  | @ -171,17 +177,19 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		async deleteFile() { | 		async deleteFile() { | ||||||
| 			const { canceled } = await this.$root.dialog({ | 			const { canceled } = await os.dialog({ | ||||||
| 				type: 'warning', | 				type: 'warning', | ||||||
| 				text: this.$t('driveFileDeleteConfirm', { name: this.file.name }), | 				text: this.$t('driveFileDeleteConfirm', { name: this.file.name }), | ||||||
| 				showCancelButton: true | 				showCancelButton: true | ||||||
| 			}); | 			}); | ||||||
| 			if (canceled) return; | 			if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 			this.$root.api('drive/files/delete', { | 			os.api('drive/files/delete', { | ||||||
| 				fileId: this.file.id | 				fileId: this.file.id | ||||||
| 			}); | 			}); | ||||||
| 		} | 		}, | ||||||
|  | 
 | ||||||
|  | 		bytes | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  | @ -197,6 +205,10 @@ export default Vue.extend({ | ||||||
| 		cursor: pointer; | 		cursor: pointer; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	> * { | ||||||
|  | 		pointer-events: none; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	&:hover { | 	&:hover { | ||||||
| 		background: rgba(#000, 0.05); | 		background: rgba(#000, 0.05); | ||||||
| 
 | 
 | ||||||
|  | @ -233,7 +245,7 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	&[data-is-selected] { | 	&.isSelected { | ||||||
| 		background: var(--accent); | 		background: var(--accent); | ||||||
| 
 | 
 | ||||||
| 		&:hover { | 		&:hover { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <div class="rghtznwe" | <div class="rghtznwe" | ||||||
| 	:data-draghover="draghover" | 	:class="{ draghover }" | ||||||
| 	@click="onClick" | 	@click="onClick" | ||||||
| 	@mouseover="onMouseover" | 	@mouseover="onMouseover" | ||||||
| 	@mouseout="onMouseout" | 	@mouseout="onMouseout" | ||||||
|  | @ -14,8 +14,8 @@ | ||||||
| 	:title="title" | 	:title="title" | ||||||
| > | > | ||||||
| 	<p class="name"> | 	<p class="name"> | ||||||
| 		<template v-if="hover"><fa :icon="faFolderOpen" fixed-width/></template> | 		<template v-if="hover"><Fa :icon="faFolderOpen" fixed-width/></template> | ||||||
| 		<template v-if="!hover"><fa :icon="faFolder" fixed-width/></template> | 		<template v-if="!hover"><Fa :icon="faFolder" fixed-width/></template> | ||||||
| 		{{ folder.name }} | 		{{ folder.name }} | ||||||
| 	</p> | 	</p> | ||||||
| 	<p class="upload" v-if="$store.state.settings.uploadFolder == folder.id"> | 	<p class="upload" v-if="$store.state.settings.uploadFolder == folder.id"> | ||||||
|  | @ -26,10 +26,11 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faFolder, faFolderOpen } from '@fortawesome/free-regular-svg-icons'; | import { faFolder, faFolderOpen } from '@fortawesome/free-regular-svg-icons'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		folder: { | 		folder: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -47,6 +48,8 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	emits: ['chosen'], | ||||||
|  | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			hover: false, | 			hover: false, | ||||||
|  | @ -91,8 +94,8 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			const isFile = e.dataTransfer.items[0].kind == 'file'; | 			const isFile = e.dataTransfer.items[0].kind == 'file'; | ||||||
| 			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; | 			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; | ||||||
| 			const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; | 			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; | ||||||
| 
 | 
 | ||||||
| 			if (isFile || isDriveFile || isDriveFolder) { | 			if (isFile || isDriveFile || isDriveFolder) { | ||||||
| 				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; | 				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; | ||||||
|  | @ -121,11 +124,11 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			//#region ドライブのファイル | 			//#region ドライブのファイル | ||||||
| 			const driveFile = e.dataTransfer.getData('mk_drive_file'); | 			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); | ||||||
| 			if (driveFile != null && driveFile != '') { | 			if (driveFile != null && driveFile != '') { | ||||||
| 				const file = JSON.parse(driveFile); | 				const file = JSON.parse(driveFile); | ||||||
| 				this.browser.removeFile(file.id); | 				this.browser.removeFile(file.id); | ||||||
| 				this.$root.api('drive/files/update', { | 				os.api('drive/files/update', { | ||||||
| 					fileId: file.id, | 					fileId: file.id, | ||||||
| 					folderId: this.folder.id | 					folderId: this.folder.id | ||||||
| 				}); | 				}); | ||||||
|  | @ -133,7 +136,7 @@ export default Vue.extend({ | ||||||
| 			//#endregion | 			//#endregion | ||||||
| 
 | 
 | ||||||
| 			//#region ドライブのフォルダ | 			//#region ドライブのフォルダ | ||||||
| 			const driveFolder = e.dataTransfer.getData('mk_drive_folder'); | 			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); | ||||||
| 			if (driveFolder != null && driveFolder != '') { | 			if (driveFolder != null && driveFolder != '') { | ||||||
| 				const folder = JSON.parse(driveFolder); | 				const folder = JSON.parse(driveFolder); | ||||||
| 
 | 
 | ||||||
|  | @ -141,7 +144,7 @@ export default Vue.extend({ | ||||||
| 				if (folder.id == this.folder.id) return; | 				if (folder.id == this.folder.id) return; | ||||||
| 
 | 
 | ||||||
| 				this.browser.removeFolder(folder.id); | 				this.browser.removeFolder(folder.id); | ||||||
| 				this.$root.api('drive/folders/update', { | 				os.api('drive/folders/update', { | ||||||
| 					folderId: folder.id, | 					folderId: folder.id, | ||||||
| 					parentId: this.folder.id | 					parentId: this.folder.id | ||||||
| 				}).then(() => { | 				}).then(() => { | ||||||
|  | @ -149,15 +152,15 @@ export default Vue.extend({ | ||||||
| 				}).catch(err => { | 				}).catch(err => { | ||||||
| 					switch (err) { | 					switch (err) { | ||||||
| 						case 'detected-circular-definition': | 						case 'detected-circular-definition': | ||||||
| 							this.$root.dialog({ | 							os.dialog({ | ||||||
| 								title: this.$t('unableToProcess'), | 								title: this.$t('unableToProcess'), | ||||||
| 								text: this.$t('circularReferenceFolder') | 								text: this.$t('circularReferenceFolder') | ||||||
| 							}); | 							}); | ||||||
| 							break; | 							break; | ||||||
| 						default: | 						default: | ||||||
| 							this.$root.dialog({ | 							os.dialog({ | ||||||
| 								type: 'error', | 								type: 'error', | ||||||
| 								text: this.$t('error') | 								text: this.$t('somethingHappened') | ||||||
| 							}); | 							}); | ||||||
| 					} | 					} | ||||||
| 				}); | 				}); | ||||||
|  | @ -167,7 +170,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 		onDragstart(e) { | 		onDragstart(e) { | ||||||
| 			e.dataTransfer.effectAllowed = 'move'; | 			e.dataTransfer.effectAllowed = 'move'; | ||||||
| 			e.dataTransfer.setData('mk_drive_folder', JSON.stringify(this.folder)); | 			e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder)); | ||||||
| 			this.isDragging = true; | 			this.isDragging = true; | ||||||
| 
 | 
 | ||||||
| 			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる | 			// 親ブラウザに対して、ドラッグが開始されたフラグを立てる | ||||||
|  | @ -189,7 +192,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		rename() { | 		rename() { | ||||||
| 			this.$root.dialog({ | 			os.dialog({ | ||||||
| 				title: this.$t('renameFolder'), | 				title: this.$t('renameFolder'), | ||||||
| 				input: { | 				input: { | ||||||
| 					placeholder: this.$t('inputNewFolderName'), | 					placeholder: this.$t('inputNewFolderName'), | ||||||
|  | @ -197,7 +200,7 @@ export default Vue.extend({ | ||||||
| 				} | 				} | ||||||
| 			}).then(({ canceled, result: name }) => { | 			}).then(({ canceled, result: name }) => { | ||||||
| 				if (canceled) return; | 				if (canceled) return; | ||||||
| 				this.$root.api('drive/folders/update', { | 				os.api('drive/folders/update', { | ||||||
| 					folderId: this.folder.id, | 					folderId: this.folder.id, | ||||||
| 					name: name | 					name: name | ||||||
| 				}); | 				}); | ||||||
|  | @ -205,7 +208,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		deleteFolder() { | 		deleteFolder() { | ||||||
| 			this.$root.api('drive/folders/delete', { | 			os.api('drive/folders/delete', { | ||||||
| 				folderId: this.folder.id | 				folderId: this.folder.id | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
| 				if (this.$store.state.settings.uploadFolder === this.folder.id) { | 				if (this.$store.state.settings.uploadFolder === this.folder.id) { | ||||||
|  | @ -217,14 +220,14 @@ export default Vue.extend({ | ||||||
| 			}).catch(err => { | 			}).catch(err => { | ||||||
| 				switch(err.id) { | 				switch(err.id) { | ||||||
| 					case 'b0fc8a17-963c-405d-bfbc-859a487295e1': | 					case 'b0fc8a17-963c-405d-bfbc-859a487295e1': | ||||||
| 						this.$root.dialog({ | 						os.dialog({ | ||||||
| 							type: 'error', | 							type: 'error', | ||||||
| 							title: this.$t('unableToDelete'), | 							title: this.$t('unableToDelete'), | ||||||
| 							text: this.$t('hasChildFilesOrFolders') | 							text: this.$t('hasChildFilesOrFolders') | ||||||
| 						}); | 						}); | ||||||
| 						break; | 						break; | ||||||
| 					default: | 					default: | ||||||
| 						this.$root.dialog({ | 						os.dialog({ | ||||||
| 							type: 'error', | 							type: 'error', | ||||||
| 							text: this.$t('unableToDelete') | 							text: this.$t('unableToDelete') | ||||||
| 						}); | 						}); | ||||||
|  | @ -272,7 +275,7 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	&[data-draghover] { | 	&.draghover { | ||||||
| 		&:after { | 		&:after { | ||||||
| 			content: ""; | 			content: ""; | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
|  |  | ||||||
|  | @ -1,22 +1,23 @@ | ||||||
| <template> | <template> | ||||||
| <div class="drylbebk" | <div class="drylbebk" | ||||||
| 	:data-draghover="draghover" | 	:class="{ draghover }" | ||||||
| 	@click="onClick" | 	@click="onClick" | ||||||
| 	@dragover.prevent.stop="onDragover" | 	@dragover.prevent.stop="onDragover" | ||||||
| 	@dragenter="onDragenter" | 	@dragenter="onDragenter" | ||||||
| 	@dragleave="onDragleave" | 	@dragleave="onDragleave" | ||||||
| 	@drop.stop="onDrop" | 	@drop.stop="onDrop" | ||||||
| > | > | ||||||
| 	<i v-if="folder == null"><fa :icon="faCloud"/></i> | 	<i v-if="folder == null"><Fa :icon="faCloud"/></i> | ||||||
| 	<span>{{ folder == null ? $t('drive') : folder.name }}</span> | 	<span>{{ folder == null ? $t('drive') : folder.name }}</span> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faCloud } from '@fortawesome/free-solid-svg-icons'; | import { faCloud } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		folder: { | 		folder: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -58,8 +59,8 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			const isFile = e.dataTransfer.items[0].kind == 'file'; | 			const isFile = e.dataTransfer.items[0].kind == 'file'; | ||||||
| 			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; | 			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; | ||||||
| 			const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; | 			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; | ||||||
| 
 | 
 | ||||||
| 			if (isFile || isDriveFile || isDriveFolder) { | 			if (isFile || isDriveFile || isDriveFolder) { | ||||||
| 				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; | 				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; | ||||||
|  | @ -90,11 +91,11 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			//#region ドライブのファイル | 			//#region ドライブのファイル | ||||||
| 			const driveFile = e.dataTransfer.getData('mk_drive_file'); | 			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); | ||||||
| 			if (driveFile != null && driveFile != '') { | 			if (driveFile != null && driveFile != '') { | ||||||
| 				const file = JSON.parse(driveFile); | 				const file = JSON.parse(driveFile); | ||||||
| 				this.browser.removeFile(file.id); | 				this.browser.removeFile(file.id); | ||||||
| 				this.$root.api('drive/files/update', { | 				os.api('drive/files/update', { | ||||||
| 					fileId: file.id, | 					fileId: file.id, | ||||||
| 					folderId: this.folder ? this.folder.id : null | 					folderId: this.folder ? this.folder.id : null | ||||||
| 				}); | 				}); | ||||||
|  | @ -102,13 +103,13 @@ export default Vue.extend({ | ||||||
| 			//#endregion | 			//#endregion | ||||||
| 
 | 
 | ||||||
| 			//#region ドライブのフォルダ | 			//#region ドライブのフォルダ | ||||||
| 			const driveFolder = e.dataTransfer.getData('mk_drive_folder'); | 			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); | ||||||
| 			if (driveFolder != null && driveFolder != '') { | 			if (driveFolder != null && driveFolder != '') { | ||||||
| 				const folder = JSON.parse(driveFolder); | 				const folder = JSON.parse(driveFolder); | ||||||
| 				// 移動先が自分自身ならreject | 				// 移動先が自分自身ならreject | ||||||
| 				if (this.folder && folder.id == this.folder.id) return; | 				if (this.folder && folder.id == this.folder.id) return; | ||||||
| 				this.browser.removeFolder(folder.id); | 				this.browser.removeFolder(folder.id); | ||||||
| 				this.$root.api('drive/folders/update', { | 				os.api('drive/folders/update', { | ||||||
| 					folderId: folder.id, | 					folderId: folder.id, | ||||||
| 					parentId: this.folder ? this.folder.id : null | 					parentId: this.folder ? this.folder.id : null | ||||||
| 				}); | 				}); | ||||||
|  | @ -125,7 +126,7 @@ export default Vue.extend({ | ||||||
| 		pointer-events: none; | 		pointer-events: none; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	&[data-draghover] { | 	&.draghover { | ||||||
| 		background: #eee; | 		background: #eee; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,34 +2,35 @@ | ||||||
| <div class="yfudmmck"> | <div class="yfudmmck"> | ||||||
| 	<nav> | 	<nav> | ||||||
| 		<div class="path" @contextmenu.prevent.stop="() => {}"> | 		<div class="path" @contextmenu.prevent.stop="() => {}"> | ||||||
| 			<x-nav-folder :class="{ current: folder == null }"/> | 			<XNavFolder :class="{ current: folder == null }"/> | ||||||
| 			<template v-for="f in hierarchyFolders"> | 			<template v-for="f in hierarchyFolders"> | ||||||
| 				<span class="separator" :key="f.id + ':separator'"><fa :icon="faAngleRight"/></span> | 				<span class="separator"><Fa :icon="faAngleRight"/></span> | ||||||
| 				<x-nav-folder :folder="f" :key="f.id"/> | 				<XNavFolder :folder="f"/> | ||||||
| 			</template> | 			</template> | ||||||
| 			<span class="separator" v-if="folder != null"><fa :icon="faAngleRight"/></span> | 			<span class="separator" v-if="folder != null"><Fa :icon="faAngleRight"/></span> | ||||||
| 			<span class="folder current" v-if="folder != null">{{ folder.name }}</span> | 			<span class="folder current" v-if="folder != null">{{ folder.name }}</span> | ||||||
| 		</div> | 		</div> | ||||||
| 	</nav> | 	</nav> | ||||||
| 	<div class="main" :class="{ uploading: uploadings.length > 0, fetching }" | 	<div class="main _section" :class="{ uploading: uploadings.length > 0, fetching }" | ||||||
| 		ref="main" | 		ref="main" | ||||||
| 		@dragover.prevent.stop="onDragover" | 		@dragover.prevent.stop="onDragover" | ||||||
| 		@dragenter="onDragenter" | 		@dragenter="onDragenter" | ||||||
| 		@dragleave="onDragleave" | 		@dragleave="onDragleave" | ||||||
| 		@drop.prevent.stop="onDrop" | 		@drop.prevent.stop="onDrop" | ||||||
|  | 		@contextmenu="onContextmenu" | ||||||
| 	> | 	> | ||||||
| 		<div class="contents" ref="contents"> | 		<div class="contents" ref="contents"> | ||||||
| 			<div class="folders" ref="foldersContainer" v-show="folders.length > 0"> | 			<div class="folders" ref="foldersContainer" v-show="folders.length > 0"> | ||||||
| 				<x-folder v-for="f in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/> | 				<XFolder v-for="f in folders" :key="f.id" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/> | ||||||
| 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> | 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> | ||||||
| 				<div class="padding" v-for="(n, i) in 16" :key="i"></div> | 				<div class="padding" v-for="(n, i) in 16" :key="i"></div> | ||||||
| 				<mk-button ref="moreFolders" v-if="moreFolders">{{ $t('loadMore') }}</mk-button> | 				<MkButton ref="moreFolders" v-if="moreFolders">{{ $t('loadMore') }}</MkButton> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="files" ref="filesContainer" v-show="files.length > 0"> | 			<div class="files" ref="filesContainer" v-show="files.length > 0"> | ||||||
| 				<x-file v-for="file in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/> | 				<XFile v-for="file in files" :key="file.id" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/> | ||||||
| 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> | 				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid --> | ||||||
| 				<div class="padding" v-for="(n, i) in 16" :key="i"></div> | 				<div class="padding" v-for="(n, i) in 16" :key="i"></div> | ||||||
| 				<mk-button ref="loadMoreFiles" @click="fetchMoreFiles" v-show="moreFiles">{{ $t('loadMore') }}</mk-button> | 				<MkButton ref="loadMoreFiles" @click="fetchMoreFiles" v-show="moreFiles">{{ $t('loadMore') }}</MkButton> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching"> | 			<div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching"> | ||||||
| 				<p v-if="draghover">{{ $t('empty-draghover') }}</p> | 				<p v-if="draghover">{{ $t('empty-draghover') }}</p> | ||||||
|  | @ -37,29 +38,28 @@ | ||||||
| 				<p v-if="!draghover && folder != null">{{ $t('emptyFolder') }}</p> | 				<p v-if="!draghover && folder != null">{{ $t('emptyFolder') }}</p> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<mk-loading v-if="fetching"/> | 		<MkLoading v-if="fetching"/> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="dropzone" v-if="draghover"></div> | 	<div class="dropzone" v-if="draghover"></div> | ||||||
| 	<x-uploader ref="uploader" @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/> |  | ||||||
| 	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/> | 	<input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; | import { faAngleRight, faFolderPlus, faICursor, faLink, faUpload } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import XNavFolder from './drive.nav-folder.vue'; | import XNavFolder from './drive.nav-folder.vue'; | ||||||
| import XFolder from './drive.folder.vue'; | import XFolder from './drive.folder.vue'; | ||||||
| import XFile from './drive.file.vue'; | import XFile from './drive.file.vue'; | ||||||
| import XUploader from './uploader.vue'; |  | ||||||
| import MkButton from './ui/button.vue'; | import MkButton from './ui/button.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
|  | import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XNavFolder, | 		XNavFolder, | ||||||
| 		XFolder, | 		XFolder, | ||||||
| 		XFile, | 		XFile, | ||||||
| 		XUploader, |  | ||||||
| 		MkButton, | 		MkButton, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -85,6 +85,8 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'], | ||||||
|  | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			/** | 			/** | ||||||
|  | @ -100,7 +102,7 @@ export default Vue.extend({ | ||||||
| 			hierarchyFolders: [], | 			hierarchyFolders: [], | ||||||
| 			selectedFiles: [], | 			selectedFiles: [], | ||||||
| 			selectedFolders: [], | 			selectedFolders: [], | ||||||
| 			uploadings: [], | 			uploadings: os.uploads, | ||||||
| 			connection: null, | 			connection: null, | ||||||
| 
 | 
 | ||||||
| 			/** | 			/** | ||||||
|  | @ -140,7 +142,7 @@ export default Vue.extend({ | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		this.connection = this.$root.stream.useSharedConnection('drive'); | 		this.connection = os.stream.useSharedConnection('drive'); | ||||||
| 
 | 
 | ||||||
| 		this.connection.on('fileCreated', this.onStreamDriveFileCreated); | 		this.connection.on('fileCreated', this.onStreamDriveFileCreated); | ||||||
| 		this.connection.on('fileUpdated', this.onStreamDriveFileUpdated); | 		this.connection.on('fileUpdated', this.onStreamDriveFileUpdated); | ||||||
|  | @ -164,7 +166,7 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		this.connection.dispose(); | 		this.connection.dispose(); | ||||||
| 		this.ilFilesObserver.disconnect(); | 		this.ilFilesObserver.disconnect(); | ||||||
| 	}, | 	}, | ||||||
|  | @ -204,14 +206,6 @@ export default Vue.extend({ | ||||||
| 			this.removeFolder(folderId); | 			this.removeFolder(folderId); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onChangeUploaderUploads(uploads) { |  | ||||||
| 			this.uploadings = uploads; |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onUploaderUploaded(file) { |  | ||||||
| 			this.addFile(file, true); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onDragover(e): any { | 		onDragover(e): any { | ||||||
| 			// ドラッグ元が自分自身の所有するアイテムだったら | 			// ドラッグ元が自分自身の所有するアイテムだったら | ||||||
| 			if (this.isDragSource) { | 			if (this.isDragSource) { | ||||||
|  | @ -221,8 +215,8 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			const isFile = e.dataTransfer.items[0].kind == 'file'; | 			const isFile = e.dataTransfer.items[0].kind == 'file'; | ||||||
| 			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; | 			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; | ||||||
| 			const isDriveFolder = e.dataTransfer.types[0] == 'mk_drive_folder'; | 			const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_; | ||||||
| 
 | 
 | ||||||
| 			if (isFile || isDriveFile || isDriveFolder) { | 			if (isFile || isDriveFile || isDriveFolder) { | ||||||
| 				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; | 				e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move'; | ||||||
|  | @ -253,12 +247,12 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			//#region ドライブのファイル | 			//#region ドライブのファイル | ||||||
| 			const driveFile = e.dataTransfer.getData('mk_drive_file'); | 			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); | ||||||
| 			if (driveFile != null && driveFile != '') { | 			if (driveFile != null && driveFile != '') { | ||||||
| 				const file = JSON.parse(driveFile); | 				const file = JSON.parse(driveFile); | ||||||
| 				if (this.files.some(f => f.id == file.id)) return; | 				if (this.files.some(f => f.id == file.id)) return; | ||||||
| 				this.removeFile(file.id); | 				this.removeFile(file.id); | ||||||
| 				this.$root.api('drive/files/update', { | 				os.api('drive/files/update', { | ||||||
| 					fileId: file.id, | 					fileId: file.id, | ||||||
| 					folderId: this.folder ? this.folder.id : null | 					folderId: this.folder ? this.folder.id : null | ||||||
| 				}); | 				}); | ||||||
|  | @ -266,7 +260,7 @@ export default Vue.extend({ | ||||||
| 			//#endregion | 			//#endregion | ||||||
| 
 | 
 | ||||||
| 			//#region ドライブのフォルダ | 			//#region ドライブのフォルダ | ||||||
| 			const driveFolder = e.dataTransfer.getData('mk_drive_folder'); | 			const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); | ||||||
| 			if (driveFolder != null && driveFolder != '') { | 			if (driveFolder != null && driveFolder != '') { | ||||||
| 				const folder = JSON.parse(driveFolder); | 				const folder = JSON.parse(driveFolder); | ||||||
| 
 | 
 | ||||||
|  | @ -274,7 +268,7 @@ export default Vue.extend({ | ||||||
| 				if (this.folder && folder.id == this.folder.id) return false; | 				if (this.folder && folder.id == this.folder.id) return false; | ||||||
| 				if (this.folders.some(f => f.id == folder.id)) return false; | 				if (this.folders.some(f => f.id == folder.id)) return false; | ||||||
| 				this.removeFolder(folder.id); | 				this.removeFolder(folder.id); | ||||||
| 				this.$root.api('drive/folders/update', { | 				os.api('drive/folders/update', { | ||||||
| 					folderId: folder.id, | 					folderId: folder.id, | ||||||
| 					parentId: this.folder ? this.folder.id : null | 					parentId: this.folder ? this.folder.id : null | ||||||
| 				}).then(() => { | 				}).then(() => { | ||||||
|  | @ -282,15 +276,15 @@ export default Vue.extend({ | ||||||
| 				}).catch(err => { | 				}).catch(err => { | ||||||
| 					switch (err) { | 					switch (err) { | ||||||
| 						case 'detected-circular-definition': | 						case 'detected-circular-definition': | ||||||
| 							this.$root.dialog({ | 							os.dialog({ | ||||||
| 								title: this.$t('unableToProcess'), | 								title: this.$t('unableToProcess'), | ||||||
| 								text: this.$t('circularReferenceFolder') | 								text: this.$t('circularReferenceFolder') | ||||||
| 							}); | 							}); | ||||||
| 							break; | 							break; | ||||||
| 						default: | 						default: | ||||||
| 							this.$root.dialog({ | 							os.dialog({ | ||||||
| 								type: 'error', | 								type: 'error', | ||||||
| 								text: this.$t('error') | 								text: this.$t('somethingHappened') | ||||||
| 							}); | 							}); | ||||||
| 					} | 					} | ||||||
| 				}); | 				}); | ||||||
|  | @ -303,19 +297,19 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		urlUpload() { | 		urlUpload() { | ||||||
| 			this.$root.dialog({ | 			os.dialog({ | ||||||
| 				title: this.$t('uploadFromUrl'), | 				title: this.$t('uploadFromUrl'), | ||||||
| 				input: { | 				input: { | ||||||
| 					placeholder: this.$t('uploadFromUrlDescription') | 					placeholder: this.$t('uploadFromUrlDescription') | ||||||
| 				} | 				} | ||||||
| 			}).then(({ canceled, result: url }) => { | 			}).then(({ canceled, result: url }) => { | ||||||
| 				if (canceled) return; | 				if (canceled) return; | ||||||
| 				this.$root.api('drive/files/upload_from_url', { | 				os.api('drive/files/upload_from_url', { | ||||||
| 					url: url, | 					url: url, | ||||||
| 					folderId: this.folder ? this.folder.id : undefined | 					folderId: this.folder ? this.folder.id : undefined | ||||||
| 				}); | 				}); | ||||||
| 
 | 
 | ||||||
| 				this.$root.dialog({ | 				os.dialog({ | ||||||
| 					title: this.$t('uploadFromUrlRequested'), | 					title: this.$t('uploadFromUrlRequested'), | ||||||
| 					text: this.$t('uploadFromUrlMayTakeTime') | 					text: this.$t('uploadFromUrlMayTakeTime') | ||||||
| 				}); | 				}); | ||||||
|  | @ -323,14 +317,14 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		createFolder() { | 		createFolder() { | ||||||
| 			this.$root.dialog({ | 			os.dialog({ | ||||||
| 				title: this.$t('createFolder'), | 				title: this.$t('createFolder'), | ||||||
| 				input: { | 				input: { | ||||||
| 					placeholder: this.$t('folderName') | 					placeholder: this.$t('folderName') | ||||||
| 				} | 				} | ||||||
| 			}).then(({ canceled, result: name }) => { | 			}).then(({ canceled, result: name }) => { | ||||||
| 				if (canceled) return; | 				if (canceled) return; | ||||||
| 				this.$root.api('drive/folders/create', { | 				os.api('drive/folders/create', { | ||||||
| 					name: name, | 					name: name, | ||||||
| 					parentId: this.folder ? this.folder.id : undefined | 					parentId: this.folder ? this.folder.id : undefined | ||||||
| 				}).then(folder => { | 				}).then(folder => { | ||||||
|  | @ -340,7 +334,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		renameFolder(folder) { | 		renameFolder(folder) { | ||||||
| 			this.$root.dialog({ | 			os.dialog({ | ||||||
| 				title: this.$t('renameFolder'), | 				title: this.$t('renameFolder'), | ||||||
| 				input: { | 				input: { | ||||||
| 					placeholder: this.$t('inputNewFolderName'), | 					placeholder: this.$t('inputNewFolderName'), | ||||||
|  | @ -348,7 +342,7 @@ export default Vue.extend({ | ||||||
| 				} | 				} | ||||||
| 			}).then(({ canceled, result: name }) => { | 			}).then(({ canceled, result: name }) => { | ||||||
| 				if (canceled) return; | 				if (canceled) return; | ||||||
| 				this.$root.api('drive/folders/update', { | 				os.api('drive/folders/update', { | ||||||
| 					folderId: folder.id, | 					folderId: folder.id, | ||||||
| 					name: name | 					name: name | ||||||
| 				}).then(folder => { | 				}).then(folder => { | ||||||
|  | @ -359,7 +353,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		deleteFolder(folder) { | 		deleteFolder(folder) { | ||||||
| 			this.$root.api('drive/folders/delete', { | 			os.api('drive/folders/delete', { | ||||||
| 				folderId: folder.id | 				folderId: folder.id | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
| 				// 削除時に親フォルダに移動 | 				// 削除時に親フォルダに移動 | ||||||
|  | @ -367,14 +361,14 @@ export default Vue.extend({ | ||||||
| 			}).catch(err => { | 			}).catch(err => { | ||||||
| 				switch(err.id) { | 				switch(err.id) { | ||||||
| 					case 'b0fc8a17-963c-405d-bfbc-859a487295e1': | 					case 'b0fc8a17-963c-405d-bfbc-859a487295e1': | ||||||
| 						this.$root.dialog({ | 						os.dialog({ | ||||||
| 							type: 'error', | 							type: 'error', | ||||||
| 							title: this.$t('unableToDelete'), | 							title: this.$t('unableToDelete'), | ||||||
| 							text: this.$t('hasChildFilesOrFolders') | 							text: this.$t('hasChildFilesOrFolders') | ||||||
| 						}); | 						}); | ||||||
| 						break; | 						break; | ||||||
| 					default: | 					default: | ||||||
| 						this.$root.dialog({ | 						os.dialog({ | ||||||
| 							type: 'error', | 							type: 'error', | ||||||
| 							text: this.$t('unableToDelete') | 							text: this.$t('unableToDelete') | ||||||
| 						}); | 						}); | ||||||
|  | @ -390,7 +384,9 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 		upload(file, folder) { | 		upload(file, folder) { | ||||||
| 			if (folder && typeof folder == 'object') folder = folder.id; | 			if (folder && typeof folder == 'object') folder = folder.id; | ||||||
| 			(this.$refs.uploader as any).upload(file, folder); | 			os.upload(file, folder).then(res => { | ||||||
|  | 				this.addFile(res, true); | ||||||
|  | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		chooseFile(file) { | 		chooseFile(file) { | ||||||
|  | @ -441,7 +437,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 			this.fetching = true; | 			this.fetching = true; | ||||||
| 
 | 
 | ||||||
| 			this.$root.api('drive/folders/show', { | 			os.api('drive/folders/show', { | ||||||
| 				folderId: target | 				folderId: target | ||||||
| 			}).then(folder => { | 			}).then(folder => { | ||||||
| 				this.folder = folder; | 				this.folder = folder; | ||||||
|  | @ -465,7 +461,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 			if (this.folders.some(f => f.id == folder.id)) { | 			if (this.folders.some(f => f.id == folder.id)) { | ||||||
| 				const exist = this.folders.map(f => f.id).indexOf(folder.id); | 				const exist = this.folders.map(f => f.id).indexOf(folder.id); | ||||||
| 				Vue.set(this.folders, exist, folder); | 				this.folders[exist] = folder; | ||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | @ -482,7 +478,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 			if (this.files.some(f => f.id == file.id)) { | 			if (this.files.some(f => f.id == file.id)) { | ||||||
| 				const exist = this.files.map(f => f.id).indexOf(file.id); | 				const exist = this.files.map(f => f.id).indexOf(file.id); | ||||||
| 				Vue.set(this.files, exist, file); | 				this.files[exist] = file; | ||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | @ -543,7 +539,7 @@ export default Vue.extend({ | ||||||
| 			const filesMax = 30; | 			const filesMax = 30; | ||||||
| 
 | 
 | ||||||
| 			// フォルダ一覧取得 | 			// フォルダ一覧取得 | ||||||
| 			this.$root.api('drive/folders', { | 			os.api('drive/folders', { | ||||||
| 				folderId: this.folder ? this.folder.id : null, | 				folderId: this.folder ? this.folder.id : null, | ||||||
| 				limit: foldersMax + 1 | 				limit: foldersMax + 1 | ||||||
| 			}).then(folders => { | 			}).then(folders => { | ||||||
|  | @ -556,7 +552,7 @@ export default Vue.extend({ | ||||||
| 			}); | 			}); | ||||||
| 
 | 
 | ||||||
| 			// ファイル一覧取得 | 			// ファイル一覧取得 | ||||||
| 			this.$root.api('drive/files', { | 			os.api('drive/files', { | ||||||
| 				folderId: this.folder ? this.folder.id : null, | 				folderId: this.folder ? this.folder.id : null, | ||||||
| 				type: this.type, | 				type: this.type, | ||||||
| 				limit: filesMax + 1 | 				limit: filesMax + 1 | ||||||
|  | @ -587,7 +583,7 @@ export default Vue.extend({ | ||||||
| 			const max = 30; | 			const max = 30; | ||||||
| 
 | 
 | ||||||
| 			// ファイル一覧取得 | 			// ファイル一覧取得 | ||||||
| 			this.$root.api('drive/files', { | 			os.api('drive/files', { | ||||||
| 				folderId: this.folder ? this.folder.id : null, | 				folderId: this.folder ? this.folder.id : null, | ||||||
| 				type: this.type, | 				type: this.type, | ||||||
| 				untilId: this.files[this.files.length - 1].id, | 				untilId: this.files[this.files.length - 1].id, | ||||||
|  | @ -602,7 +598,41 @@ export default Vue.extend({ | ||||||
| 				for (const x of files) this.appendFile(x); | 				for (const x of files) this.appendFile(x); | ||||||
| 				this.fetching = false; | 				this.fetching = false; | ||||||
| 			}); | 			}); | ||||||
| 		} | 		}, | ||||||
|  | 
 | ||||||
|  | 		getMenu() { | ||||||
|  | 			return [{ | ||||||
|  | 				text: this.$t('addFile'), | ||||||
|  | 				type: 'label' | ||||||
|  | 			}, { | ||||||
|  | 				text: this.$t('upload'), | ||||||
|  | 				icon: faUpload, | ||||||
|  | 				action: () => { this.selectLocalFile(); } | ||||||
|  | 			}, { | ||||||
|  | 				text: this.$t('fromUrl'), | ||||||
|  | 				icon: faLink, | ||||||
|  | 				action: () => { this.urlUpload(); } | ||||||
|  | 			}, null, { | ||||||
|  | 				text: this.folder ? this.folder.name : this.$t('drive'), | ||||||
|  | 				type: 'label' | ||||||
|  | 			}, this.folder ? { | ||||||
|  | 				text: this.$t('renameFolder'), | ||||||
|  | 				icon: faICursor, | ||||||
|  | 				action: () => { this.renameFolder(this.folder); } | ||||||
|  | 			} : undefined, this.folder ? { | ||||||
|  | 				text: this.$t('deleteFolder'), | ||||||
|  | 				icon: faTrashAlt, | ||||||
|  | 				action: () => { this.deleteFolder(this.folder); } | ||||||
|  | 			} : undefined, { | ||||||
|  | 				text: this.$t('createFolder'), | ||||||
|  | 				icon: faFolderPlus, | ||||||
|  | 				action: () => { this.createFolder(); } | ||||||
|  | 			}]; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		onContextmenu(e) { | ||||||
|  | 			os.contextMenu(this.getMenu(), e); | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  | @ -613,6 +643,8 @@ export default Vue.extend({ | ||||||
| 		display: block; | 		display: block; | ||||||
| 		z-index: 2; | 		z-index: 2; | ||||||
| 		width: 100%; | 		width: 100%; | ||||||
|  | 		padding: 0 8px; | ||||||
|  | 		box-sizing: border-box; | ||||||
| 		overflow: auto; | 		overflow: auto; | ||||||
| 		font-size: 0.9em; | 		font-size: 0.9em; | ||||||
| 		box-shadow: 0 1px 0 var(--divider); | 		box-shadow: 0 1px 0 var(--divider); | ||||||
|  | @ -666,7 +698,6 @@ export default Vue.extend({ | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> .main { | 	> .main { | ||||||
| 		padding: 8px 0; |  | ||||||
| 		overflow: auto; | 		overflow: auto; | ||||||
| 
 | 
 | ||||||
| 		&, * { | 		&, * { | ||||||
|  | @ -734,11 +765,6 @@ export default Vue.extend({ | ||||||
| 		pointer-events: none; | 		pointer-events: none; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> .mk-uploader { |  | ||||||
| 		height: 100px; |  | ||||||
| 		padding: 16px; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> input { | 	> input { | ||||||
| 		display: none; | 		display: none; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }"> | <MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')"> | ||||||
| 	<div class="omfetrab"> | 	<div class="omfetrab _popup"> | ||||||
| 		<header> | 		<header> | ||||||
| 			<button v-for="(category, i) in categories" | 			<button v-for="(category, i) in categories" | ||||||
| 				class="_button" | 				class="_button" | ||||||
|  | @ -8,26 +8,26 @@ | ||||||
| 				:class="{ active: category.isActive }" | 				:class="{ active: category.isActive }" | ||||||
| 				:key="i" | 				:key="i" | ||||||
| 			> | 			> | ||||||
| 				<fa :icon="category.icon" fixed-width/> | 				<Fa :icon="category.icon" fixed-width/> | ||||||
| 			</button> | 			</button> | ||||||
| 		</header> | 		</header> | ||||||
| 
 | 
 | ||||||
| 		<div class="emojis"> | 		<div class="emojis"> | ||||||
| 			<template v-if="categories[0].isActive"> | 			<template v-if="categories[0].isActive"> | ||||||
| 				<header class="category"><fa :icon="faHistory" fixed-width/> {{ $t('recentUsed') }}</header> | 				<header class="category"><Fa :icon="faHistory" fixed-width/> {{ $t('recentUsed') }}</header> | ||||||
| 				<div class="list"> | 				<div class="list"> | ||||||
| 					<button v-for="(emoji, i) in ($store.state.device.recentEmojis || [])" | 					<button v-for="emoji in ($store.state.device.recentEmojis || [])" | ||||||
| 						class="_button" | 						class="_button" | ||||||
| 						:title="emoji.name" | 						:title="emoji.name" | ||||||
| 						@click="chosen(emoji)" | 						@click="chosen(emoji)" | ||||||
| 						:key="i" | 						:key="emoji" | ||||||
| 					> | 					> | ||||||
| 						<mk-emoji v-if="emoji.char != null" :emoji="emoji.char"/> | 						<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/> | ||||||
| 						<img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> | 						<img v-else :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/> | ||||||
| 					</button> | 					</button> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<header class="category"><fa :icon="faAsterisk" fixed-width/> {{ $t('customEmojis') }}</header> | 				<header class="category"><Fa :icon="faAsterisk" fixed-width/> {{ $t('customEmojis') }}</header> | ||||||
| 			</template> | 			</template> | ||||||
| 
 | 
 | ||||||
| 			<template v-if="categories.find(x => x.isActive).name"> | 			<template v-if="categories.find(x => x.isActive).name"> | ||||||
|  | @ -38,7 +38,7 @@ | ||||||
| 						@click="chosen(emoji)" | 						@click="chosen(emoji)" | ||||||
| 						:key="emoji.name" | 						:key="emoji.name" | ||||||
| 					> | 					> | ||||||
| 						<mk-emoji :emoji="emoji.char"/> | 						<MkEmoji :emoji="emoji.char"/> | ||||||
| 					</button> | 					</button> | ||||||
| 				</div> | 				</div> | ||||||
| 			</template> | 			</template> | ||||||
|  | @ -59,29 +59,31 @@ | ||||||
| 			</template> | 			</template> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </x-popup> | </MkModal> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { emojilist } from '../../misc/emojilist'; | import { emojilist } from '../../misc/emojilist'; | ||||||
| import { getStaticImageUrl } from '../scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | ||||||
| import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory, faUser } from '@fortawesome/free-solid-svg-icons'; | import { faAsterisk, faLeaf, faUtensils, faFutbol, faCity, faDice, faGlobe, faHistory, faUser } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons'; | import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { groupByX } from '../../prelude/array'; | import { groupByX } from '../../prelude/array'; | ||||||
| import XPopup from './popup.vue'; | import MkModal from '@/components/ui/modal.vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XPopup, | 		MkModal, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
| 		source: { | 		src: { | ||||||
| 			required: true | 			required: false | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	emits: ['done', 'closed'], | ||||||
|  | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			emojilist, | 			emojilist, | ||||||
|  | @ -162,12 +164,9 @@ export default Vue.extend({ | ||||||
| 			recents = recents.filter((e: any) => getKey(e) !== getKey(emoji)); | 			recents = recents.filter((e: any) => getKey(e) !== getKey(emoji)); | ||||||
| 			recents.unshift(emoji) | 			recents.unshift(emoji) | ||||||
| 			this.$store.commit('device/set', { key: 'recentEmojis', value: recents.splice(0, 16) }); | 			this.$store.commit('device/set', { key: 'recentEmojis', value: recents.splice(0, 16) }); | ||||||
| 			this.$emit('chosen', getKey(emoji)); | 			this.$emit('done', getKey(emoji)); | ||||||
|  | 			this.$refs.modal.close(); | ||||||
| 		}, | 		}, | ||||||
| 
 |  | ||||||
| 		close() { |  | ||||||
| 			this.$refs.popup.close(); |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -6,11 +6,12 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { getStaticImageUrl } from '../scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | ||||||
| import { twemojiSvgBase } from '../../misc/twemoji-base'; | import { twemojiSvgBase } from '../../misc/twemoji-base'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		name: { | 		name: { | ||||||
| 			type: String, | 			type: String, | ||||||
|  |  | ||||||
|  | @ -1,19 +1,19 @@ | ||||||
| <template> | <template> | ||||||
| <transition :name="$store.state.device.animation ? 'zoom' : ''" appear> | <transition :name="$store.state.device.animation ? 'zoom' : ''" appear> | ||||||
| 	<div class="mjndxjcg _panel"> | 	<div class="mjndxjcg"> | ||||||
| 		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> | 		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> | ||||||
| 		<p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p> | 		<p><Fa :icon="faExclamationTriangle"/> {{ $t('somethingHappened') }}</p> | ||||||
| 		<mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button> | 		<MkButton @click="() => $emit('retry')" class="button">{{ $t('retry') }}</MkButton> | ||||||
| 	</div> | 	</div> | ||||||
| </transition> | </transition> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; | import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import MkButton from './ui/button.vue'; | import MkButton from './ui/button.vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkButton, | 		MkButton, | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| <template> | <template> | ||||||
| <span class="mk-file-type-icon"> | <span class="mk-file-type-icon"> | ||||||
| 	<template v-if="kind == 'image'"><fa :icon="faFileImage"/></template> | 	<template v-if="kind == 'image'"><Fa :icon="faFileImage"/></template> | ||||||
| </span> | </span> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faFileImage } from '@fortawesome/free-solid-svg-icons'; | import { faFileImage } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		type: { | 		type: { | ||||||
| 			type: String, | 			type: String, | ||||||
|  |  | ||||||
|  | @ -7,32 +7,33 @@ | ||||||
| > | > | ||||||
| 	<template v-if="!wait"> | 	<template v-if="!wait"> | ||||||
| 		<template v-if="hasPendingFollowRequestFromYou && user.isLocked"> | 		<template v-if="hasPendingFollowRequestFromYou && user.isLocked"> | ||||||
| 			<span v-if="full">{{ $t('followRequestPending') }}</span><fa :icon="faHourglassHalf"/> | 			<span v-if="full">{{ $t('followRequestPending') }}</span><Fa :icon="faHourglassHalf"/> | ||||||
| 		</template> | 		</template> | ||||||
| 		<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 --> | 		<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 --> | ||||||
| 			<span v-if="full">{{ $t('processing') }}</span><fa :icon="faSpinner" pulse/> | 			<span v-if="full">{{ $t('processing') }}</span><Fa :icon="faSpinner" pulse/> | ||||||
| 		</template> | 		</template> | ||||||
| 		<template v-else-if="isFollowing"> | 		<template v-else-if="isFollowing"> | ||||||
| 			<span v-if="full">{{ $t('unfollow') }}</span><fa :icon="faMinus"/> | 			<span v-if="full">{{ $t('unfollow') }}</span><Fa :icon="faMinus"/> | ||||||
| 		</template> | 		</template> | ||||||
| 		<template v-else-if="!isFollowing && user.isLocked"> | 		<template v-else-if="!isFollowing && user.isLocked"> | ||||||
| 			<span v-if="full">{{ $t('followRequest') }}</span><fa :icon="faPlus"/> | 			<span v-if="full">{{ $t('followRequest') }}</span><Fa :icon="faPlus"/> | ||||||
| 		</template> | 		</template> | ||||||
| 		<template v-else-if="!isFollowing && !user.isLocked"> | 		<template v-else-if="!isFollowing && !user.isLocked"> | ||||||
| 			<span v-if="full">{{ $t('follow') }}</span><fa :icon="faPlus"/> | 			<span v-if="full">{{ $t('follow') }}</span><Fa :icon="faPlus"/> | ||||||
| 		</template> | 		</template> | ||||||
| 	</template> | 	</template> | ||||||
| 	<template v-else> | 	<template v-else> | ||||||
| 		<span v-if="full">{{ $t('processing') }}</span><fa :icon="faSpinner" pulse fixed-width/> | 		<span v-if="full">{{ $t('processing') }}</span><Fa :icon="faSpinner" pulse fixed-width/> | ||||||
| 	</template> | 	</template> | ||||||
| </button> | </button> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faSpinner, faPlus, faMinus, faHourglassHalf } from '@fortawesome/free-solid-svg-icons'; | import { faSpinner, faPlus, faMinus, faHourglassHalf } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		user: { | 		user: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -58,7 +59,7 @@ export default Vue.extend({ | ||||||
| 	created() { | 	created() { | ||||||
| 		// 渡されたユーザー情報が不完全な場合 | 		// 渡されたユーザー情報が不完全な場合 | ||||||
| 		if (this.user.isFollowing == null) { | 		if (this.user.isFollowing == null) { | ||||||
| 			this.$root.api('users/show', { | 			os.api('users/show', { | ||||||
| 				userId: this.user.id | 				userId: this.user.id | ||||||
| 			}).then(u => { | 			}).then(u => { | ||||||
| 				this.isFollowing = u.isFollowing; | 				this.isFollowing = u.isFollowing; | ||||||
|  | @ -68,13 +69,13 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		this.connection = this.$root.stream.useSharedConnection('main'); | 		this.connection = os.stream.useSharedConnection('main'); | ||||||
| 
 | 
 | ||||||
| 		this.connection.on('follow', this.onFollowChange); | 		this.connection.on('follow', this.onFollowChange); | ||||||
| 		this.connection.on('unfollow', this.onFollowChange); | 		this.connection.on('unfollow', this.onFollowChange); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		this.connection.dispose(); | 		this.connection.dispose(); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -91,7 +92,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 			try { | 			try { | ||||||
| 				if (this.isFollowing) { | 				if (this.isFollowing) { | ||||||
| 					const { canceled } = await this.$root.dialog({ | 					const { canceled } = await os.dialog({ | ||||||
| 						type: 'warning', | 						type: 'warning', | ||||||
| 						text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }), | 						text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }), | ||||||
| 						showCancelButton: true | 						showCancelButton: true | ||||||
|  | @ -99,21 +100,21 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 					if (canceled) return; | 					if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 					await this.$root.api('following/delete', { | 					await os.api('following/delete', { | ||||||
| 						userId: this.user.id | 						userId: this.user.id | ||||||
| 					}); | 					}); | ||||||
| 				} else { | 				} else { | ||||||
| 					if (this.hasPendingFollowRequestFromYou) { | 					if (this.hasPendingFollowRequestFromYou) { | ||||||
| 						await this.$root.api('following/requests/cancel', { | 						await os.api('following/requests/cancel', { | ||||||
| 							userId: this.user.id | 							userId: this.user.id | ||||||
| 						}); | 						}); | ||||||
| 					} else if (this.user.isLocked) { | 					} else if (this.user.isLocked) { | ||||||
| 						await this.$root.api('following/create', { | 						await os.api('following/create', { | ||||||
| 							userId: this.user.id | 							userId: this.user.id | ||||||
| 						}); | 						}); | ||||||
| 						this.hasPendingFollowRequestFromYou = true; | 						this.hasPendingFollowRequestFromYou = true; | ||||||
| 					} else { | 					} else { | ||||||
| 						await this.$root.api('following/create', { | 						await os.api('following/create', { | ||||||
| 							userId: this.user.id | 							userId: this.user.id | ||||||
| 						}); | 						}); | ||||||
| 						this.hasPendingFollowRequestFromYou = true; | 						this.hasPendingFollowRequestFromYou = true; | ||||||
|  |  | ||||||
|  | @ -1,41 +1,50 @@ | ||||||
| <template> | <template> | ||||||
| <x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false"> | <XModalWindow ref="dialog" | ||||||
|  | 	:width="400" | ||||||
|  | 	:can-close="false" | ||||||
|  | 	:with-ok-button="true" | ||||||
|  | 	:ok-button-disabled="false" | ||||||
|  | 	@click="cancel()" | ||||||
|  | 	@ok="ok()" | ||||||
|  | 	@close="cancel()" | ||||||
|  | 	@closed="$emit('closed')" | ||||||
|  | > | ||||||
| 	<template #header> | 	<template #header> | ||||||
| 		{{ title }} | 		{{ title }} | ||||||
| 	</template> | 	</template> | ||||||
| 	<div class="xkpnjxcv"> | 	<div class="xkpnjxcv _section"> | ||||||
| 		<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item"> | 		<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item"> | ||||||
| 			<mk-input v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"> | 			<MkInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1"> | ||||||
| 				<span v-text="form[item].label || item"></span> | 				<span v-text="form[item].label || item"></span> | ||||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||||
| 			</mk-input> | 			</MkInput> | ||||||
| 			<mk-input v-else-if="form[item].type === 'string' && !item.multiline" v-model="values[item]" type="text"> | 			<MkInput v-else-if="form[item].type === 'string' && !item.multiline" v-model:value="values[item]" type="text"> | ||||||
| 				<span v-text="form[item].label || item"></span> | 				<span v-text="form[item].label || item"></span> | ||||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||||
| 			</mk-input> | 			</MkInput> | ||||||
| 			<mk-textarea v-else-if="form[item].type === 'string' && item.multiline" v-model="values[item]"> | 			<MkTextarea v-else-if="form[item].type === 'string' && item.multiline" v-model:value="values[item]"> | ||||||
| 				<span v-text="form[item].label || item"></span> | 				<span v-text="form[item].label || item"></span> | ||||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||||
| 			</mk-textarea> | 			</MkTextarea> | ||||||
| 			<mk-switch v-else-if="form[item].type === 'boolean'" v-model="values[item]"> | 			<MkSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]"> | ||||||
| 				<span v-text="form[item].label || item"></span> | 				<span v-text="form[item].label || item"></span> | ||||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||||
| 			</mk-switch> | 			</MkSwitch> | ||||||
| 		</label> | 		</label> | ||||||
| 	</div> | 	</div> | ||||||
| </x-window> | </XModalWindow> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import XWindow from './window.vue'; | import XModalWindow from '@/components/ui/modal-window.vue'; | ||||||
| import MkInput from './ui/input.vue'; | import MkInput from './ui/input.vue'; | ||||||
| import MkTextarea from './ui/textarea.vue'; | import MkTextarea from './ui/textarea.vue'; | ||||||
| import MkSwitch from './ui/switch.vue'; | import MkSwitch from './ui/switch.vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XWindow, | 		XModalWindow, | ||||||
| 		MkInput, | 		MkInput, | ||||||
| 		MkTextarea, | 		MkTextarea, | ||||||
| 		MkSwitch, | 		MkSwitch, | ||||||
|  | @ -52,6 +61,8 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	emits: ['done'], | ||||||
|  | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			values: {} | 			values: {} | ||||||
|  | @ -60,15 +71,24 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	created() { | 	created() { | ||||||
| 		for (const item in this.form) { | 		for (const item in this.form) { | ||||||
| 			Vue.set(this.values, item, this.form[item].hasOwnProperty('default') ? this.form[item].default : null); | 			this.values[item] = this.form[item].hasOwnProperty('default') ? this.form[item].default : null; | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		ok() { | 		ok() { | ||||||
| 			this.$emit('ok', this.values); | 			this.$emit('done', { | ||||||
| 			this.$refs.window.close(); | 				result: this.values | ||||||
|  | 			}); | ||||||
|  | 			this.$refs.dialog.close(); | ||||||
| 		}, | 		}, | ||||||
|  | 
 | ||||||
|  | 		cancel() { | ||||||
|  | 			this.$emit('done', { | ||||||
|  | 				canceled: true | ||||||
|  | 			}); | ||||||
|  | 			this.$refs.dialog.close(); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  | @ -77,7 +97,10 @@ export default Vue.extend({ | ||||||
| .xkpnjxcv { | .xkpnjxcv { | ||||||
| 	> label { | 	> label { | ||||||
| 		display: block; | 		display: block; | ||||||
| 		padding: 16px 24px; | 
 | ||||||
|  | 		&:not(:last-child) { | ||||||
|  | 			margin-bottom: 32px; | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | @ -5,9 +5,10 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import * as katex from 'katex'; | import * as katex from 'katex';import * as os from '@/os'; | ||||||
| export default Vue.extend({ | 
 | ||||||
|  | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		formula: { | 		formula: { | ||||||
| 			type: String, | 			type: String, | ||||||
|  |  | ||||||
|  | @ -1,12 +1,13 @@ | ||||||
| <template> | <template> | ||||||
| <x-formula :formula="formula" :block="block" /> | <XFormula :formula="formula" :block="block" /> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent, defineAsyncComponent } from 'vue';import * as os from '@/os'; | ||||||
| export default Vue.extend({ | 
 | ||||||
|  | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XFormula: () => import('./formula-core.vue').then(m => m.default) | 		XFormula: defineAsyncComponent(() => import('./formula-core.vue')) | ||||||
| 	}, | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		formula: { | 		formula: { | ||||||
|  |  | ||||||
|  | @ -1,15 +1,16 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mk-google"> | <div class="mk-google"> | ||||||
| 	<input type="search" v-model="query" :placeholder="q"> | 	<input type="search" v-model="query" :placeholder="q"> | ||||||
| 	<button @click="search"><fa :icon="faSearch"/> {{ $t('search') }}</button> | 	<button @click="search"><Fa :icon="faSearch"/> {{ $t('search') }}</button> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faSearch } from '@fortawesome/free-solid-svg-icons'; | import { faSearch } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: ['q'], | 	props: ['q'], | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  | @ -23,7 +24,7 @@ export default Vue.extend({ | ||||||
| 	methods: { | 	methods: { | ||||||
| 		search() { | 		search() { | ||||||
| 			const engine = this.$store.state.settings.webSearchEngine || | 			const engine = this.$store.state.settings.webSearchEngine || | ||||||
| 				'https://www.google.com/?#q={{query}}'; | 				'https://www.google.com/search?q={{query}}'; | ||||||
| 			const url = engine.replace('{{query}}', this.query) | 			const url = engine.replace('{{query}}', this.query) | ||||||
| 			window.open(url, '_blank'); | 			window.open(url, '_blank'); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -8,16 +8,17 @@ | ||||||
| 		</time> | 		</time> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="content _panel _ghost"> | 	<div class="content _panel _ghost"> | ||||||
| 		<mk-clock/> | 		<MkClock/> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkClock from './analog-clock.vue'; | import MkClock from './analog-clock.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkClock | 		MkClock | ||||||
| 	}, | 	}, | ||||||
|  | @ -48,7 +49,7 @@ export default Vue.extend({ | ||||||
| 		this.tick(); | 		this.tick(); | ||||||
| 		this.clock = setInterval(this.tick, 1000); | 		this.clock = setInterval(this.tick, 1000); | ||||||
| 	}, | 	}, | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		clearInterval(this.clock); | 		clearInterval(this.clock); | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
|  |  | ||||||
							
								
								
									
										73
									
								
								src/client/components/icon-dialog.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/client/components/icon-dialog.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,73 @@ | ||||||
|  | <template> | ||||||
|  | <MkModal ref="modal" @click="type === 'success' ? done() : () => {}" @closed="$emit('closed')"> | ||||||
|  | 	<div class="iuyakobc" :class="type"> | ||||||
|  | 		<Fa class="icon" v-if="type === 'success'" :icon="faCheck"/> | ||||||
|  | 		<Fa class="icon" v-else-if="type === 'waiting'" :icon="faSpinner" pulse/> | ||||||
|  | 	</div> | ||||||
|  | </MkModal> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent } from 'vue'; | ||||||
|  | import { faCheck, faSpinner } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import MkModal from '@/components/ui/modal.vue'; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		MkModal, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	props: { | ||||||
|  | 		type: { | ||||||
|  | 			required: true | ||||||
|  | 		}, | ||||||
|  | 		showing: { | ||||||
|  | 			required: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	emits: ['done', 'closed'], | ||||||
|  | 
 | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			faCheck, faSpinner, | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	watch: { | ||||||
|  | 		showing() { | ||||||
|  | 			if (!this.showing) this.done(); | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		done() { | ||||||
|  | 			this.$emit('done'); | ||||||
|  | 			this.$refs.modal.close(); | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .iuyakobc { | ||||||
|  | 	position: relative; | ||||||
|  | 	padding: 32px; | ||||||
|  | 	box-sizing: border-box; | ||||||
|  | 	text-align: center; | ||||||
|  | 	background: var(--panel); | ||||||
|  | 	border-radius: var(--radius); | ||||||
|  | 	width: initial; | ||||||
|  | 	font-size: 32px; | ||||||
|  | 
 | ||||||
|  | 	&.success { | ||||||
|  | 		color: var(--accent); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	&.waiting { | ||||||
|  | 		> .icon { | ||||||
|  | 			opacity: 0.7; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -1,16 +1,26 @@ | ||||||
| <template> | <template> | ||||||
| <x-modal ref="modal" @closed="() => { $emit('closed'); destroyDom(); }"> | <MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')"> | ||||||
| 	<img class="xubzgfga" ref="img" :src="image.url" :alt="image.name" :title="image.name" @click="close" tabindex="-1"/> | 	<div class="xubzgfga"> | ||||||
| </x-modal> | 		<header>{{ image.name }}</header> | ||||||
|  | 		<img :src="image.url" :alt="image.name" :title="image.name" @click="$refs.modal.close()"/> | ||||||
|  | 		<footer> | ||||||
|  | 			<span>{{ image.type }}</span> | ||||||
|  | 			<span>{{ bytes(image.size) }}</span> | ||||||
|  | 			<span v-if="image.properties?.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span> | ||||||
|  | 		</footer> | ||||||
|  | 	</div> | ||||||
|  | </MkModal> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import XModal from './modal.vue'; | import bytes from '@/filters/bytes'; | ||||||
|  | import number from '@/filters/number'; | ||||||
|  | import MkModal from '@/components/ui/modal.vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XModal, | 		MkModal, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -20,32 +30,50 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	emits: ['closed'], | ||||||
| 		this.$nextTick(() => { |  | ||||||
| 			this.$refs.img.focus(); |  | ||||||
| 		}); |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		close() { | 		bytes, | ||||||
| 			this.$refs.modal.close(); | 		number, | ||||||
| 		}, |  | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .xubzgfga { | .xubzgfga { | ||||||
| 	position: fixed; | 	max-width: 1024px; | ||||||
| 	z-index: 2; | 
 | ||||||
| 	top: 0; | 	> header, | ||||||
| 	right: 0; | 	> footer { | ||||||
| 	bottom: 0; | 		display: inline-block; | ||||||
| 	left: 0; | 		padding: 6px 9px; | ||||||
|  | 		font-size: 90%; | ||||||
|  | 		background: rgba(0, 0, 0, 0.5); | ||||||
|  | 		border-radius: 6px; | ||||||
|  | 		color: #fff; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	> header { | ||||||
|  | 		margin-bottom: 8px; | ||||||
|  | 		opacity: 0.9; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	> img { | ||||||
|  | 		display: block; | ||||||
| 		max-width: 100%; | 		max-width: 100%; | ||||||
| 	max-height: 100%; |  | ||||||
| 	margin: auto; |  | ||||||
| 		cursor: zoom-out; | 		cursor: zoom-out; | ||||||
| 		image-orientation: from-image; | 		image-orientation: from-image; | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	> footer { | ||||||
|  | 		margin-top: 8px; | ||||||
|  | 		opacity: 0.8; | ||||||
|  | 
 | ||||||
|  | 		> span + span { | ||||||
|  | 			margin-left: 0.5em; | ||||||
|  | 			padding-left: 0.5em; | ||||||
|  | 			border-left: solid 1px rgba(255, 255, 255, 0.5); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| <template> | <template> | ||||||
| <div class="xubzgfgb" :title="title"> | <div class="xubzgfgb" :class="{ cover }" :title="title"> | ||||||
| 	<canvas ref="canvas" :width="size" :height="size" :title="title" v-if="!loaded"/> | 	<canvas ref="canvas" :width="size" :height="size" :title="title" v-if="!loaded"/> | ||||||
| 	<img v-if="src" :src="src" :title="title" :alt="alt" @load="onLoad"/> | 	<img v-if="src" :src="src" :title="title" :alt="alt" @load="onLoad"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { decode } from 'blurhash'; | import { decode } from 'blurhash'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		src: { | 		src: { | ||||||
| 			type: String, | 			type: String, | ||||||
|  | @ -35,6 +35,11 @@ export default Vue.extend({ | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: 64 | 			default: 64 | ||||||
| 		}, | 		}, | ||||||
|  | 		cover: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: true, | ||||||
|  | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	data() { | 	data() { | ||||||
|  | @ -49,6 +54,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		draw() { | 		draw() { | ||||||
|  | 			if (this.hash == null) return; | ||||||
| 			const pixels = decode(this.hash, this.size, this.size); | 			const pixels = decode(this.hash, this.size, this.size); | ||||||
| 			const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d'); | 			const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d'); | ||||||
| 			const imageData = ctx!.createImageData(this.size, this.size); | 			const imageData = ctx!.createImageData(this.size, this.size); | ||||||
|  | @ -70,9 +76,23 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	> canvas, | 	> canvas, | ||||||
| 	> img { | 	> img { | ||||||
|  | 		display: block; | ||||||
| 		width: 100%; | 		width: 100%; | ||||||
| 		height: 100%; | 		height: 100%; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	> canvas { | ||||||
|  | 		object-fit: cover; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	> img { | ||||||
|  | 		object-fit: contain; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	&.cover { | ||||||
|  | 		> img { | ||||||
| 			object-fit: cover; | 			object-fit: cover; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import Vue from 'vue'; | import { App } from 'vue'; | ||||||
| 
 | 
 | ||||||
| import mfm from './misskey-flavored-markdown.vue'; | import mfm from './misskey-flavored-markdown.vue'; | ||||||
| import acct from './acct.vue'; | import acct from './acct.vue'; | ||||||
|  | @ -12,14 +12,16 @@ import loading from './loading.vue'; | ||||||
| import error from './error.vue'; | import error from './error.vue'; | ||||||
| import streamIndicator from './stream-indicator.vue'; | import streamIndicator from './stream-indicator.vue'; | ||||||
| 
 | 
 | ||||||
| Vue.component('mfm', mfm); | export default function(app: App) { | ||||||
| Vue.component('mk-acct', acct); | 	app.component('Mfm', mfm); | ||||||
| Vue.component('mk-avatar', avatar); | 	app.component('MkAcct', acct); | ||||||
| Vue.component('mk-emoji', emoji); | 	app.component('MkAvatar', avatar); | ||||||
| Vue.component('mk-user-name', userName); | 	app.component('MkEmoji', emoji); | ||||||
| Vue.component('mk-ellipsis', ellipsis); | 	app.component('MkUserName', userName); | ||||||
| Vue.component('mk-time', time); | 	app.component('MkEllipsis', ellipsis); | ||||||
| Vue.component('mk-url', url); | 	app.component('MkTime', time); | ||||||
| Vue.component('mk-loading', loading); | 	app.component('MkUrl', url); | ||||||
| Vue.component('mk-error', error); | 	app.component('MkLoading', loading); | ||||||
| Vue.component('stream-indicator', streamIndicator); | 	app.component('MkError', error); | ||||||
|  | 	app.component('StreamIndicator', streamIndicator); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,93 +1,93 @@ | ||||||
| <template> | <template> | ||||||
| <div class="zbcjwnqg" v-size="{ max: [550, 1200] }"> | <div class="zbcjwnqg" v-size="{ max: [550, 1000] }"> | ||||||
| 	<div class="stats" v-if="info"> | 	<div class="stats" v-if="info"> | ||||||
| 		<div class="_panel"> | 		<div class="_panel"> | ||||||
| 			<div> | 			<div> | ||||||
| 				<b><fa :icon="faUser"/>{{ $t('users') }}</b> | 				<b><Fa :icon="faUser"/>{{ $t('users') }}</b> | ||||||
| 				<small>{{ $t('local') }}</small> | 				<small>{{ $t('local') }}</small> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div> | 			<div> | ||||||
| 				<dl class="total"> | 				<dl class="total"> | ||||||
| 					<dt>{{ $t('total') }}</dt> | 					<dt>{{ $t('total') }}</dt> | ||||||
| 					<dd>{{ info.originalUsersCount | number }}</dd> | 					<dd>{{ number(info.originalUsersCount) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 				<dl class="diff" :class="{ inc: usersLocalDoD > 0 }"> | 				<dl class="diff" :class="{ inc: usersLocalDoD > 0 }"> | ||||||
| 					<dt>{{ $t('dayOverDayChanges') }}</dt> | 					<dt>{{ $t('dayOverDayChanges') }}</dt> | ||||||
| 					<dd>{{ usersLocalDoD | number }}</dd> | 					<dd>{{ number(usersLocalDoD) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 				<dl class="diff" :class="{ inc: usersLocalWoW > 0 }"> | 				<dl class="diff" :class="{ inc: usersLocalWoW > 0 }"> | ||||||
| 					<dt>{{ $t('weekOverWeekChanges') }}</dt> | 					<dt>{{ $t('weekOverWeekChanges') }}</dt> | ||||||
| 					<dd>{{ usersLocalWoW | number }}</dd> | 					<dd>{{ number(usersLocalWoW) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="_panel"> | 		<div class="_panel"> | ||||||
| 			<div> | 			<div> | ||||||
| 				<b><fa :icon="faUser"/>{{ $t('users') }}</b> | 				<b><Fa :icon="faUser"/>{{ $t('users') }}</b> | ||||||
| 				<small>{{ $t('remote') }}</small> | 				<small>{{ $t('remote') }}</small> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div> | 			<div> | ||||||
| 				<dl class="total"> | 				<dl class="total"> | ||||||
| 					<dt>{{ $t('total') }}</dt> | 					<dt>{{ $t('total') }}</dt> | ||||||
| 					<dd>{{ (info.usersCount - info.originalUsersCount) | number }}</dd> | 					<dd>{{ number((info.usersCount - info.originalUsersCount)) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 				<dl class="diff" :class="{ inc: usersRemoteDoD > 0 }"> | 				<dl class="diff" :class="{ inc: usersRemoteDoD > 0 }"> | ||||||
| 					<dt>{{ $t('dayOverDayChanges') }}</dt> | 					<dt>{{ $t('dayOverDayChanges') }}</dt> | ||||||
| 					<dd>{{ usersRemoteDoD | number }}</dd> | 					<dd>{{ number(usersRemoteDoD) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 				<dl class="diff" :class="{ inc: usersRemoteWoW > 0 }"> | 				<dl class="diff" :class="{ inc: usersRemoteWoW > 0 }"> | ||||||
| 					<dt>{{ $t('weekOverWeekChanges') }}</dt> | 					<dt>{{ $t('weekOverWeekChanges') }}</dt> | ||||||
| 					<dd>{{ usersRemoteWoW | number }}</dd> | 					<dd>{{ number(usersRemoteWoW) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="_panel"> | 		<div class="_panel"> | ||||||
| 			<div> | 			<div> | ||||||
| 				<b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b> | 				<b><Fa :icon="faPencilAlt"/>{{ $t('notes') }}</b> | ||||||
| 				<small>{{ $t('local') }}</small> | 				<small>{{ $t('local') }}</small> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div> | 			<div> | ||||||
| 				<dl class="total"> | 				<dl class="total"> | ||||||
| 					<dt>{{ $t('total') }}</dt> | 					<dt>{{ $t('total') }}</dt> | ||||||
| 					<dd>{{ info.originalNotesCount | number }}</dd> | 					<dd>{{ number(info.originalNotesCount) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 				<dl class="diff" :class="{ inc: notesLocalDoD > 0 }"> | 				<dl class="diff" :class="{ inc: notesLocalDoD > 0 }"> | ||||||
| 					<dt>{{ $t('dayOverDayChanges') }}</dt> | 					<dt>{{ $t('dayOverDayChanges') }}</dt> | ||||||
| 					<dd>{{ notesLocalDoD | number }}</dd> | 					<dd>{{ number(notesLocalDoD) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 				<dl class="diff" :class="{ inc: notesLocalWoW > 0 }"> | 				<dl class="diff" :class="{ inc: notesLocalWoW > 0 }"> | ||||||
| 					<dt>{{ $t('weekOverWeekChanges') }}</dt> | 					<dt>{{ $t('weekOverWeekChanges') }}</dt> | ||||||
| 					<dd>{{ notesLocalWoW | number }}</dd> | 					<dd>{{ number(notesLocalWoW) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="_panel"> | 		<div class="_panel"> | ||||||
| 			<div> | 			<div> | ||||||
| 				<b><fa :icon="faPencilAlt"/>{{ $t('notes') }}</b> | 				<b><Fa :icon="faPencilAlt"/>{{ $t('notes') }}</b> | ||||||
| 				<small>{{ $t('remote') }}</small> | 				<small>{{ $t('remote') }}</small> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div> | 			<div> | ||||||
| 				<dl class="total"> | 				<dl class="total"> | ||||||
| 					<dt>{{ $t('total') }}</dt> | 					<dt>{{ $t('total') }}</dt> | ||||||
| 					<dd>{{ (info.notesCount - info.originalNotesCount) | number }}</dd> | 					<dd>{{ number((info.notesCount - info.originalNotesCount)) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 				<dl class="diff" :class="{ inc: notesRemoteDoD > 0 }"> | 				<dl class="diff" :class="{ inc: notesRemoteDoD > 0 }"> | ||||||
| 					<dt>{{ $t('dayOverDayChanges') }}</dt> | 					<dt>{{ $t('dayOverDayChanges') }}</dt> | ||||||
| 					<dd>{{ notesRemoteDoD | number }}</dd> | 					<dd>{{ number(notesRemoteDoD) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 				<dl class="diff" :class="{ inc: notesRemoteWoW > 0 }"> | 				<dl class="diff" :class="{ inc: notesRemoteWoW > 0 }"> | ||||||
| 					<dt>{{ $t('weekOverWeekChanges') }}</dt> | 					<dt>{{ $t('weekOverWeekChanges') }}</dt> | ||||||
| 					<dd>{{ notesRemoteWoW | number }}</dd> | 					<dd>{{ number(notesRemoteWoW) }}</dd> | ||||||
| 				</dl> | 				</dl> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<section class="_card"> | 	<section class="_card"> | ||||||
| 		<div class="_title" style="position: relative;"><fa :icon="faChartBar"/> {{ $t('statistics') }}<button @click="fetchChart" class="_button" style="position: absolute; right: 0; bottom: 0; top: 0; padding: inherit;"><fa :icon="faSync"/></button></div> | 		<div class="_title" style="position: relative;"><Fa :icon="faChartBar"/> {{ $t('statistics') }}<button @click="fetchChart" class="_button" style="position: absolute; right: 0; bottom: 0; top: 0; padding: inherit;"><Fa :icon="faSync"/></button></div> | ||||||
| 		<div class="_content" style="margin-top: -8px;"> | 		<div class="_content" style="margin-top: -8px;"> | ||||||
| 			<div class="selects" style="display: flex;"> | 			<div class="selects" style="display: flex;"> | ||||||
| 				<mk-select v-model="chartSrc" style="margin: 0; flex: 1;"> | 				<MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;"> | ||||||
| 					<optgroup :label="$t('federation')"> | 					<optgroup :label="$t('federation')"> | ||||||
| 						<option value="federation-instances">{{ $t('_charts.federationInstancesIncDec') }}</option> | 						<option value="federation-instances">{{ $t('_charts.federationInstancesIncDec') }}</option> | ||||||
| 						<option value="federation-instances-total">{{ $t('_charts.federationInstancesTotal') }}</option> | 						<option value="federation-instances-total">{{ $t('_charts.federationInstancesTotal') }}</option> | ||||||
|  | @ -109,11 +109,11 @@ | ||||||
| 						<option value="drive">{{ $t('_charts.storageUsageIncDec') }}</option> | 						<option value="drive">{{ $t('_charts.storageUsageIncDec') }}</option> | ||||||
| 						<option value="drive-total">{{ $t('_charts.storageUsageTotal') }}</option> | 						<option value="drive-total">{{ $t('_charts.storageUsageTotal') }}</option> | ||||||
| 					</optgroup> | 					</optgroup> | ||||||
| 				</mk-select> | 				</MkSelect> | ||||||
| 				<mk-select v-model="chartSpan" style="margin: 0;"> | 				<MkSelect v-model:value="chartSpan" style="margin: 0;"> | ||||||
| 					<option value="hour">{{ $t('perHour') }}</option> | 					<option value="hour">{{ $t('perHour') }}</option> | ||||||
| 					<option value="day">{{ $t('perDay') }}</option> | 					<option value="day">{{ $t('perDay') }}</option> | ||||||
| 				</mk-select> | 				</MkSelect> | ||||||
| 			</div> | 			</div> | ||||||
| 			<canvas ref="chart"></canvas> | 			<canvas ref="chart"></canvas> | ||||||
| 		</div> | 		</div> | ||||||
|  | @ -122,10 +122,11 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent, markRaw } from 'vue'; | ||||||
| import { faChartBar, faUser, faPencilAlt, faSync } from '@fortawesome/free-solid-svg-icons'; | import { faChartBar, faUser, faPencilAlt, faSync } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import Chart from 'chart.js'; | import Chart from 'chart.js'; | ||||||
| import MkSelect from './ui/select.vue'; | import MkSelect from './ui/select.vue'; | ||||||
|  | import number from '@/filters/number'; | ||||||
| 
 | 
 | ||||||
| const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); | const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); | ||||||
| const negate = arr => arr.map(x => -x); | const negate = arr => arr.map(x => -x); | ||||||
|  | @ -136,8 +137,9 @@ const alpha = (hex, a) => { | ||||||
| 	const b = parseInt(result[3], 16); | 	const b = parseInt(result[3], 16); | ||||||
| 	return `rgba(${r}, ${g}, ${b}, ${a})`; | 	return `rgba(${r}, ${g}, ${b}, ${a})`; | ||||||
| }; | }; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkSelect | 		MkSelect | ||||||
| 	}, | 	}, | ||||||
|  | @ -216,7 +218,7 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	async created() { | 	async created() { | ||||||
| 		this.info = await this.$root.api('stats'); | 		this.info = await os.api('stats'); | ||||||
| 
 | 
 | ||||||
| 		this.now = new Date(); | 		this.now = new Date(); | ||||||
| 
 | 
 | ||||||
|  | @ -226,17 +228,17 @@ export default Vue.extend({ | ||||||
| 	methods: { | 	methods: { | ||||||
| 		async fetchChart() { | 		async fetchChart() { | ||||||
| 			const [perHour, perDay] = await Promise.all([Promise.all([ | 			const [perHour, perDay] = await Promise.all([Promise.all([ | ||||||
| 				this.$root.api('charts/federation', { limit: this.chartLimit, span: 'hour' }), | 				os.api('charts/federation', { limit: this.chartLimit, span: 'hour' }), | ||||||
| 				this.$root.api('charts/users', { limit: this.chartLimit, span: 'hour' }), | 				os.api('charts/users', { limit: this.chartLimit, span: 'hour' }), | ||||||
| 				this.$root.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }), | 				os.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }), | ||||||
| 				this.$root.api('charts/notes', { limit: this.chartLimit, span: 'hour' }), | 				os.api('charts/notes', { limit: this.chartLimit, span: 'hour' }), | ||||||
| 				this.$root.api('charts/drive', { limit: this.chartLimit, span: 'hour' }), | 				os.api('charts/drive', { limit: this.chartLimit, span: 'hour' }), | ||||||
| 			]), Promise.all([ | 			]), Promise.all([ | ||||||
| 				this.$root.api('charts/federation', { limit: this.chartLimit, span: 'day' }), | 				os.api('charts/federation', { limit: this.chartLimit, span: 'day' }), | ||||||
| 				this.$root.api('charts/users', { limit: this.chartLimit, span: 'day' }), | 				os.api('charts/users', { limit: this.chartLimit, span: 'day' }), | ||||||
| 				this.$root.api('charts/active-users', { limit: this.chartLimit, span: 'day' }), | 				os.api('charts/active-users', { limit: this.chartLimit, span: 'day' }), | ||||||
| 				this.$root.api('charts/notes', { limit: this.chartLimit, span: 'day' }), | 				os.api('charts/notes', { limit: this.chartLimit, span: 'day' }), | ||||||
| 				this.$root.api('charts/drive', { limit: this.chartLimit, span: 'day' }), | 				os.api('charts/drive', { limit: this.chartLimit, span: 'day' }), | ||||||
| 			])]); | 			])]); | ||||||
| 
 | 
 | ||||||
| 			const chart = { | 			const chart = { | ||||||
|  | @ -279,7 +281,7 @@ export default Vue.extend({ | ||||||
| 			const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; | 			const gridColor = this.$store.state.device.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; | ||||||
| 
 | 
 | ||||||
| 			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | 			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||||
| 			this.chartInstance = new Chart(this.$refs.chart, { | 			this.chartInstance = markRaw(new Chart(this.$refs.chart, { | ||||||
| 				type: 'line', | 				type: 'line', | ||||||
| 				data: { | 				data: { | ||||||
| 					labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), | 					labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), | ||||||
|  | @ -344,7 +346,7 @@ export default Vue.extend({ | ||||||
| 						mode: 'index', | 						mode: 'index', | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			}); | 			})); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		getDate(ago: number) { | 		getDate(ago: number) { | ||||||
|  | @ -622,13 +624,15 @@ export default Vue.extend({ | ||||||
| 				}] | 				}] | ||||||
| 			}; | 			}; | ||||||
| 		}, | 		}, | ||||||
|  | 
 | ||||||
|  | 		number | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .zbcjwnqg { | .zbcjwnqg { | ||||||
| 	&.max-width_1200px { | 	&.max-width_1000px { | ||||||
| 		> .stats { | 		> .stats { | ||||||
| 			grid-template-columns: 1fr 1fr; | 			grid-template-columns: 1fr 1fr; | ||||||
| 			grid-template-rows: 1fr 1fr; | 			grid-template-rows: 1fr 1fr; | ||||||
|  |  | ||||||
|  | @ -5,18 +5,18 @@ | ||||||
| 	:title="url" | 	:title="url" | ||||||
| > | > | ||||||
| 	<slot></slot> | 	<slot></slot> | ||||||
| 	<fa :icon="faExternalLinkSquareAlt" v-if="target === '_blank'" class="icon"/> | 	<Fa :icon="faExternalLinkSquareAlt" v-if="target === '_blank'" class="icon"/> | ||||||
| </component> | </component> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; | import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { url as local } from '../config'; | import { url as local } from '@/config'; | ||||||
| import MkUrlPreview from './url-preview-popup.vue'; | import { isDeviceTouch } from '@/scripts/is-device-touch'; | ||||||
| import { isDeviceTouch } from '../scripts/is-device-touch'; | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		url: { | 		url: { | ||||||
| 			type: String, | 			type: String, | ||||||
|  | @ -36,29 +36,34 @@ export default Vue.extend({ | ||||||
| 			target: self ? null : '_blank', | 			target: self ? null : '_blank', | ||||||
| 			showTimer: null, | 			showTimer: null, | ||||||
| 			hideTimer: null, | 			hideTimer: null, | ||||||
| 			preview: null, | 			checkTimer: null, | ||||||
|  | 			close: null, | ||||||
| 			faExternalLinkSquareAlt | 			faExternalLinkSquareAlt | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		showPreview() { | 		async showPreview() { | ||||||
| 			if (!document.body.contains(this.$el)) return; | 			if (!document.body.contains(this.$el)) return; | ||||||
| 			if (this.preview) return; | 			if (this.close) return; | ||||||
| 
 | 
 | ||||||
| 			this.preview = new MkUrlPreview({ | 			const { dispose } = os.popup(await import('@/components/url-preview-popup.vue'), { | ||||||
| 				parent: this, |  | ||||||
| 				propsData: { |  | ||||||
| 				url: this.url, | 				url: this.url, | ||||||
| 				source: this.$el | 				source: this.$el | ||||||
| 				} | 			}); | ||||||
| 			}).$mount(); |  | ||||||
| 
 | 
 | ||||||
| 			document.body.appendChild(this.preview.$el); | 			this.close = () => { | ||||||
|  | 				dispose(); | ||||||
|  | 			}; | ||||||
|  | 
 | ||||||
|  | 			this.checkTimer = setInterval(() => { | ||||||
|  | 				if (!document.body.contains(this.$el)) this.closePreview(); | ||||||
|  | 			}, 1000); | ||||||
| 		}, | 		}, | ||||||
| 		closePreview() { | 		closePreview() { | ||||||
| 			if (this.preview) { | 			if (this.close) { | ||||||
| 				this.preview.destroyDom(); | 				clearInterval(this.checkTimer); | ||||||
| 				this.preview = null; | 				this.close(); | ||||||
|  | 				this.close = null; | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		onMouseover() { | 		onMouseover() { | ||||||
|  |  | ||||||
|  | @ -5,9 +5,10 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		inline: { | 		inline: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mk-media-banner"> | <div class="mk-media-banner"> | ||||||
| 	<div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false"> | 	<div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false"> | ||||||
| 		<span class="icon"><fa :icon="faExclamationTriangle"/></span> | 		<span class="icon"><Fa :icon="faExclamationTriangle"/></span> | ||||||
| 		<b>{{ $t('sensitive') }}</b> | 		<b>{{ $t('sensitive') }}</b> | ||||||
| 		<span>{{ $t('clickToShow') }}</span> | 		<span>{{ $t('clickToShow') }}</span> | ||||||
| 	</div> | 	</div> | ||||||
|  | @ -19,17 +19,18 @@ | ||||||
| 		:title="media.name" | 		:title="media.name" | ||||||
| 		:download="media.name" | 		:download="media.name" | ||||||
| 	> | 	> | ||||||
| 		<span class="icon"><fa icon="download"/></span> | 		<span class="icon"><Fa icon="download"/></span> | ||||||
| 		<b>{{ media.name }}</b> | 		<b>{{ media.name }}</b> | ||||||
| 	</a> | 	</a> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; | import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		media: { | 		media: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  |  | ||||||
|  | @ -1,34 +1,36 @@ | ||||||
| <template> | <template> | ||||||
| <div class="qjewsnkg" v-if="hide" @click="hide = false"> | <div class="qjewsnkg" v-if="hide" @click="hide = false"> | ||||||
| 	<img-with-blurhash class="bg" :hash="image.blurhash" :title="image.name"/> | 	<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.name"/> | ||||||
| 	<div class="text"> | 	<div class="text"> | ||||||
| 		<div> | 		<div> | ||||||
| 			<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b> | 			<b><Fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b> | ||||||
| 			<span>{{ $t('clickToShow') }}</span> | 			<span>{{ $t('clickToShow') }}</span> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| <div class="gqnyydlz" v-else> | <div class="gqnyydlz" :style="{ background: color }" v-else> | ||||||
| 	<i><fa :icon="faEyeSlash" @click="hide = true"/></i> | 	<i><Fa :icon="faEyeSlash" @click="hide = true"/></i> | ||||||
| 	<a | 	<a | ||||||
| 		:href="image.url" | 		:href="image.url" | ||||||
| 		:title="image.name" | 		:title="image.name" | ||||||
| 		@click.prevent="onClick" | 		@click.prevent="onClick" | ||||||
| 	> | 	> | ||||||
| 		<img-with-blurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name"/> | 		<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name" :cover="false"/> | ||||||
| 		<div class="gif" v-if="image.type === 'image/gif'">GIF</div> | 		<div class="gif" v-if="image.type === 'image/gif'">GIF</div> | ||||||
| 	</a> | 	</a> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; | import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { getStaticImageUrl } from '../scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | ||||||
|  | import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; | ||||||
| import ImageViewer from './image-viewer.vue'; | import ImageViewer from './image-viewer.vue'; | ||||||
| import ImgWithBlurhash from './img-with-blurhash.vue'; | import ImgWithBlurhash from './img-with-blurhash.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		ImgWithBlurhash | 		ImgWithBlurhash | ||||||
| 	}, | 	}, | ||||||
|  | @ -44,8 +46,8 @@ export default Vue.extend({ | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			hide: true, | 			hide: true, | ||||||
| 			faExclamationTriangle, | 			color: null, | ||||||
| 			faEyeSlash | 			faExclamationTriangle, faEyeSlash, | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	computed: { | 	computed: { | ||||||
|  | @ -67,6 +69,9 @@ export default Vue.extend({ | ||||||
| 		// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする | 		// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする | ||||||
| 		this.$watch('image', () => { | 		this.$watch('image', () => { | ||||||
| 			this.hide = this.image.isSensitive && !this.$store.state.device.alwaysShowNsfw; | 			this.hide = this.image.isSensitive && !this.$store.state.device.alwaysShowNsfw; | ||||||
|  | 			if (this.image.blurhash) { | ||||||
|  | 				this.color = extractAvgColorFromBlurhash(this.image.blurhash); | ||||||
|  | 			} | ||||||
| 		}, { | 		}, { | ||||||
| 			deep: true, | 			deep: true, | ||||||
| 			immediate: true, | 			immediate: true, | ||||||
|  | @ -77,12 +82,9 @@ export default Vue.extend({ | ||||||
| 			if (this.$store.state.device.imageNewTab) { | 			if (this.$store.state.device.imageNewTab) { | ||||||
| 				window.open(this.image.url, '_blank'); | 				window.open(this.image.url, '_blank'); | ||||||
| 			} else { | 			} else { | ||||||
| 				const viewer = this.$root.new(ImageViewer, { | 				os.popup(ImageViewer, { | ||||||
| 					image: this.image | 					image: this.image | ||||||
| 				}); | 				}, {}, 'closed'); | ||||||
| 				this.$once('hook:beforeDestroy', () => { |  | ||||||
| 					viewer.close(); |  | ||||||
| 				}); |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -123,6 +125,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| .gqnyydlz { | .gqnyydlz { | ||||||
| 	position: relative; | 	position: relative; | ||||||
|  | 	border: solid 1px var(--divider); | ||||||
| 
 | 
 | ||||||
| 	> i { | 	> i { | ||||||
| 		display: block; | 		display: block; | ||||||
|  |  | ||||||
|  | @ -1,13 +1,11 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mk-media-list"> | <div class="mk-media-list"> | ||||||
| 	<template v-for="media in mediaList.filter(media => !previewable(media))"> | 	<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :media="media" :key="media.id"/> | ||||||
| 		<x-banner :media="media" :key="media.id"/> |  | ||||||
| 	</template> |  | ||||||
| 	<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" ref="gridOuter"> | 	<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" ref="gridOuter"> | ||||||
| 		<div :data-count="mediaList.filter(media => previewable(media)).length" :style="gridInnerStyle"> | 		<div :data-count="mediaList.filter(media => previewable(media)).length" :style="gridInnerStyle"> | ||||||
| 			<template v-for="media in mediaList"> | 			<template v-for="media in mediaList"> | ||||||
| 				<x-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/> | 				<XVideo :video="media" :key="media.id" v-if="media.type.startsWith('video')"/> | ||||||
| 				<x-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/> | 				<XImage :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/> | ||||||
| 			</template> | 			</template> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
|  | @ -15,12 +13,13 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import XBanner from './media-banner.vue'; | import XBanner from './media-banner.vue'; | ||||||
| import XImage from './media-image.vue'; | import XImage from './media-image.vue'; | ||||||
| import XVideo from './media-video.vue'; | import XVideo from './media-video.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XBanner, | 		XBanner, | ||||||
| 		XImage, | 		XImage, | ||||||
|  | @ -46,7 +45,7 @@ export default Vue.extend({ | ||||||
| 		this.size(); | 		this.size(); | ||||||
| 		window.addEventListener('resize', this.size); | 		window.addEventListener('resize', this.size); | ||||||
| 	}, | 	}, | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		window.removeEventListener('resize', this.size); | 		window.removeEventListener('resize', this.size); | ||||||
| 	}, | 	}, | ||||||
| 	activated() { | 	activated() { | ||||||
|  |  | ||||||
|  | @ -1,12 +1,12 @@ | ||||||
| <template> | <template> | ||||||
| <div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="hide" @click="hide = false"> | <div class="icozogqfvdetwohsdglrbswgrejoxbdj" v-if="hide" @click="hide = false"> | ||||||
| 	<div> | 	<div> | ||||||
| 		<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b> | 		<b><Fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b> | ||||||
| 		<span>{{ $t('clickToShow') }}</span> | 		<span>{{ $t('clickToShow') }}</span> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| <div class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else> | <div class="kkjnbbplepmiyuadieoenjgutgcmtsvu" v-else> | ||||||
| 	<i><fa :icon="faEyeSlash" @click="hide = true"/></i> | 	<i><Fa :icon="faEyeSlash" @click="hide = true"/></i> | ||||||
| 	<a | 	<a | ||||||
| 		:href="video.url" | 		:href="video.url" | ||||||
| 		rel="nofollow noopener" | 		rel="nofollow noopener" | ||||||
|  | @ -14,17 +14,18 @@ | ||||||
| 		:style="imageStyle" | 		:style="imageStyle" | ||||||
| 		:title="video.name" | 		:title="video.name" | ||||||
| 	> | 	> | ||||||
| 		<fa :icon="faPlayCircle"/> | 		<Fa :icon="faPlayCircle"/> | ||||||
| 	</a> | 	</a> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faPlayCircle } from '@fortawesome/free-regular-svg-icons'; | import { faPlayCircle } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; | import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		video: { | 		video: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  |  | ||||||
|  | @ -15,12 +15,13 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { toUnicode } from 'punycode'; | import { toUnicode } from 'punycode'; | ||||||
| import { host as localHost } from '../config'; | import { host as localHost } from '@/config'; | ||||||
| import { wellKnownServices } from '../../well-known-services'; | import { wellKnownServices } from '../../well-known-services'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		username: { | 		username: { | ||||||
| 			type: String, | 			type: String, | ||||||
|  |  | ||||||
|  | @ -1,191 +0,0 @@ | ||||||
| <template> |  | ||||||
| <x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap"> |  | ||||||
| 	<div class="rrevdjwt" :class="{ left: align === 'left' }" ref="items"> |  | ||||||
| 		<template v-for="(item, i) in items.filter(item => item !== undefined)"> |  | ||||||
| 			<div v-if="item === null" class="divider" :key="i"></div> |  | ||||||
| 			<span v-else-if="item.type === 'label'" class="label item" :key="i"> |  | ||||||
| 				<span>{{ item.text }}</span> |  | ||||||
| 			</span> |  | ||||||
| 			<router-link v-else-if="item.type === 'link'" :to="item.to" @click.native="close()" :tabindex="i" class="_button item" :key="i"> |  | ||||||
| 				<fa v-if="item.icon" :icon="item.icon" fixed-width/> |  | ||||||
| 				<mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/> |  | ||||||
| 				<span>{{ item.text }}</span> |  | ||||||
| 				<i v-if="item.indicate"><fa :icon="faCircle"/></i> |  | ||||||
| 			</router-link> |  | ||||||
| 			<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item" :key="i"> |  | ||||||
| 				<fa v-if="item.icon" :icon="item.icon" fixed-width/> |  | ||||||
| 				<span>{{ item.text }}</span> |  | ||||||
| 				<i v-if="item.indicate"><fa :icon="faCircle"/></i> |  | ||||||
| 			</a> |  | ||||||
| 			<button v-else-if="item.type === 'user'" @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i"> |  | ||||||
| 				<mk-avatar :user="item.user" class="avatar"/><mk-user-name :user="item.user"/> |  | ||||||
| 				<i v-if="item.indicate"><fa :icon="faCircle"/></i> |  | ||||||
| 			</button> |  | ||||||
| 			<button v-else @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i"> |  | ||||||
| 				<fa v-if="item.icon" :icon="item.icon" fixed-width/> |  | ||||||
| 				<mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/> |  | ||||||
| 				<span>{{ item.text }}</span> |  | ||||||
| 				<i v-if="item.indicate"><fa :icon="faCircle"/></i> |  | ||||||
| 			</button> |  | ||||||
| 		</template> |  | ||||||
| 	</div> |  | ||||||
| </x-popup> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| import { faCircle } from '@fortawesome/free-solid-svg-icons'; |  | ||||||
| import XPopup from './popup.vue'; |  | ||||||
| import { focusPrev, focusNext } from '../scripts/focus'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	components: { |  | ||||||
| 		XPopup |  | ||||||
| 	}, |  | ||||||
| 	props: { |  | ||||||
| 		source: { |  | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 		items: { |  | ||||||
| 			type: Array, |  | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 		align: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		noCenter: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		fixed: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		width: { |  | ||||||
| 			type: Number, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		direction: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		viaKeyboard: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			faCircle |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	computed: { |  | ||||||
| 		keymap(): any { |  | ||||||
| 			return { |  | ||||||
| 				'up|k|shift+tab': this.focusUp, |  | ||||||
| 				'down|j|tab': this.focusDown, |  | ||||||
| 			}; |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	mounted() { |  | ||||||
| 		if (this.viaKeyboard) { |  | ||||||
| 			this.$nextTick(() => { |  | ||||||
| 				focusNext(this.$refs.items.children[0], true); |  | ||||||
| 			}); |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		clicked(fn) { |  | ||||||
| 			fn(); |  | ||||||
| 			this.close(); |  | ||||||
| 		}, |  | ||||||
| 		close() { |  | ||||||
| 			this.$refs.popup.close(); |  | ||||||
| 		}, |  | ||||||
| 		focusUp() { |  | ||||||
| 			focusPrev(document.activeElement); |  | ||||||
| 		}, |  | ||||||
| 		focusDown() { |  | ||||||
| 			focusNext(document.activeElement); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .rrevdjwt { |  | ||||||
| 	padding: 8px 0; |  | ||||||
| 
 |  | ||||||
| 	&.left { |  | ||||||
| 		> .item { |  | ||||||
| 			text-align: left; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .item { |  | ||||||
| 		display: block; |  | ||||||
| 		position: relative; |  | ||||||
| 		padding: 8px 16px; |  | ||||||
| 		width: 100%; |  | ||||||
| 		box-sizing: border-box; |  | ||||||
| 		white-space: nowrap; |  | ||||||
| 		font-size: 0.9em; |  | ||||||
| 		line-height: 20px; |  | ||||||
| 		text-align: center; |  | ||||||
| 		overflow: hidden; |  | ||||||
| 		text-overflow: ellipsis; |  | ||||||
| 
 |  | ||||||
| 		&:hover { |  | ||||||
| 			color: #fff; |  | ||||||
| 			background: var(--accent); |  | ||||||
| 			text-decoration: none; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		&:active { |  | ||||||
| 			color: #fff; |  | ||||||
| 			background: var(--accentDarken); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		&:not(:active):focus { |  | ||||||
| 			box-shadow: 0 0 0 2px var(--focus) inset; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		&.label { |  | ||||||
| 			pointer-events: none; |  | ||||||
| 			font-size: 0.7em; |  | ||||||
| 			padding-bottom: 4px; |  | ||||||
| 
 |  | ||||||
| 			> span { |  | ||||||
| 				opacity: 0.7; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> [data-icon] { |  | ||||||
| 			margin-right: 4px; |  | ||||||
| 			width: 20px; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .avatar { |  | ||||||
| 			margin-right: 4px; |  | ||||||
| 			width: 20px; |  | ||||||
| 			height: 20px; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> i { |  | ||||||
| 			position: absolute; |  | ||||||
| 			top: 5px; |  | ||||||
| 			left: 13px; |  | ||||||
| 			color: var(--indicator); |  | ||||||
| 			font-size: 12px; |  | ||||||
| 			animation: blink 1s infinite; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .divider { |  | ||||||
| 		margin: 8px 0; |  | ||||||
| 		height: 1px; |  | ||||||
| 		background: var(--divider); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,16 +1,18 @@ | ||||||
| import Vue, { VNode } from 'vue'; | import { VNode, defineComponent, h } from 'vue'; | ||||||
| import { MfmForest } from '../../mfm/prelude'; | import { MfmForest } from '../../mfm/prelude'; | ||||||
| import { parse, parsePlain } from '../../mfm/parse'; | import { parse, parsePlain } from '../../mfm/parse'; | ||||||
| import MkUrl from './url.vue'; | import MkUrl from './url.vue'; | ||||||
| import MkLink from './link.vue'; | import MkLink from './link.vue'; | ||||||
| import MkMention from './mention.vue'; | import MkMention from './mention.vue'; | ||||||
|  | import MkEmoji from './emoji.vue'; | ||||||
| import { concat } from '../../prelude/array'; | import { concat } from '../../prelude/array'; | ||||||
| import MkFormula from './formula.vue'; | import MkFormula from './formula.vue'; | ||||||
| import MkCode from './code.vue'; | import MkCode from './code.vue'; | ||||||
| import MkGoogle from './google.vue'; | import MkGoogle from './google.vue'; | ||||||
| import { host } from '../config'; | import { host } from '@/config'; | ||||||
|  | import { RouterLink } from 'vue-router'; | ||||||
| 
 | 
 | ||||||
| export default Vue.component('misskey-flavored-markdown', { | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		text: { | 		text: { | ||||||
| 			type: String, | 			type: String, | ||||||
|  | @ -41,7 +43,7 @@ export default Vue.component('misskey-flavored-markdown', { | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	render(createElement) { | 	render() { | ||||||
| 		if (this.text == null || this.text == '') return; | 		if (this.text == null || this.text == '') return; | ||||||
| 
 | 
 | ||||||
| 		const ast = (this.plain ? parsePlain : parse)(this.text); | 		const ast = (this.plain ? parsePlain : parse)(this.text); | ||||||
|  | @ -53,67 +55,49 @@ export default Vue.component('misskey-flavored-markdown', { | ||||||
| 
 | 
 | ||||||
| 					if (!this.plain) { | 					if (!this.plain) { | ||||||
| 						const x = text.split('\n') | 						const x = text.split('\n') | ||||||
| 							.map(t => t == '' ? [createElement('br')] : [this._v(t), createElement('br')]); // NOTE: this._vはHACK SEE: https://github.com/syuilo/misskey/pull/6399#issuecomment-632820283
 | 							.map(t => t == '' ? [h('br')] : [t, h('br')]); | ||||||
| 						x[x.length - 1].pop(); | 						x[x.length - 1].pop(); | ||||||
| 						return x; | 						return x; | ||||||
| 					} else { | 					} else { | ||||||
| 						return [this._v(text.replace(/\n/g, ' '))]; | 						return [text.replace(/\n/g, ' ')]; | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'bold': { | 				case 'bold': { | ||||||
| 					return [createElement('b', genEl(token.children))]; | 					return [h('b', genEl(token.children))]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'strike': { | 				case 'strike': { | ||||||
| 					return [createElement('del', genEl(token.children))]; | 					return [h('del', genEl(token.children))]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'italic': { | 				case 'italic': { | ||||||
| 					return (createElement as any)('i', { | 					return h('i', { | ||||||
| 						attrs: { |  | ||||||
| 						style: 'font-style: oblique;' | 						style: 'font-style: oblique;' | ||||||
| 						}, |  | ||||||
| 					}, genEl(token.children)); | 					}, genEl(token.children)); | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'big': { | 				case 'big': { | ||||||
| 					return (createElement as any)('strong', { | 					return h('strong', { | ||||||
| 						attrs: { | 						style: `display: inline-block; font-size: 150%;` + (this.$store.state.device.animatedMfm ? 'animation: anime-tada 1s linear infinite both;' : ''), | ||||||
| 							style: `display: inline-block; font-size: 150%;` |  | ||||||
| 						}, |  | ||||||
| 						directives: [this.$store.state.device.animatedMfm ? { |  | ||||||
| 							name: 'animate-css', |  | ||||||
| 							value: { classes: 'tada', iteration: 'infinite' } |  | ||||||
| 						}: {}] |  | ||||||
| 					}, genEl(token.children)); | 					}, genEl(token.children)); | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'small': { | 				case 'small': { | ||||||
| 					return [createElement('small', { | 					return [h('small', { | ||||||
| 						attrs: { |  | ||||||
| 						style: 'opacity: 0.7;' | 						style: 'opacity: 0.7;' | ||||||
| 						}, |  | ||||||
| 					}, genEl(token.children))]; | 					}, genEl(token.children))]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'center': { | 				case 'center': { | ||||||
| 					return [createElement('div', { | 					return [h('div', { | ||||||
| 						attrs: { |  | ||||||
| 						style: 'text-align:center;' | 						style: 'text-align:center;' | ||||||
| 						} |  | ||||||
| 					}, genEl(token.children))]; | 					}, genEl(token.children))]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'motion': { | 				case 'motion': { | ||||||
| 					return (createElement as any)('span', { | 					return h('span', { | ||||||
| 						attrs: { | 						style: 'display: inline-block;' + (this.$store.state.device.animatedMfm ? 'animation: anime-rubberBand 1s linear infinite both;' : ''), | ||||||
| 							style: 'display: inline-block;' |  | ||||||
| 						}, |  | ||||||
| 						directives: [this.$store.state.device.animatedMfm ? { |  | ||||||
| 							name: 'animate-css', |  | ||||||
| 							value: { classes: 'rubberBand', iteration: 'infinite' } |  | ||||||
| 						} : {}] |  | ||||||
| 					}, genEl(token.children)); | 					}, genEl(token.children)); | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
|  | @ -123,163 +107,126 @@ export default Vue.component('misskey-flavored-markdown', { | ||||||
| 						token.node.props.attr == 'alternate' ? 'alternate' : | 						token.node.props.attr == 'alternate' ? 'alternate' : | ||||||
| 						'normal'; | 						'normal'; | ||||||
| 					const style = this.$store.state.device.animatedMfm | 					const style = this.$store.state.device.animatedMfm | ||||||
| 						? `animation: spin 1.5s linear infinite; animation-direction: ${direction};` : ''; | 						? `animation: anime-spin 1.5s linear infinite; animation-direction: ${direction};` : ''; | ||||||
| 					return (createElement as any)('span', { | 					return h('span', { | ||||||
| 						attrs: { |  | ||||||
| 						style: 'display: inline-block;' + style | 						style: 'display: inline-block;' + style | ||||||
| 						}, |  | ||||||
| 					}, genEl(token.children)); | 					}, genEl(token.children)); | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'jump': { | 				case 'jump': { | ||||||
| 					return (createElement as any)('span', { | 					return h('span', { | ||||||
| 						attrs: { | 						style: this.$store.state.device.animatedMfm ? 'display: inline-block; animation: anime-jump 0.75s linear infinite;' : 'display: inline-block;' | ||||||
| 							style: this.$store.state.device.animatedMfm ? 'display: inline-block; animation: jump 0.75s linear infinite;' : 'display: inline-block;' |  | ||||||
| 						}, |  | ||||||
| 					}, genEl(token.children)); | 					}, genEl(token.children)); | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'flip': { | 				case 'flip': { | ||||||
| 					return (createElement as any)('span', { | 					return h('span', { | ||||||
| 						attrs: { |  | ||||||
| 						style: 'display: inline-block; transform: scaleX(-1);' | 						style: 'display: inline-block; transform: scaleX(-1);' | ||||||
| 						}, |  | ||||||
| 					}, genEl(token.children)); | 					}, genEl(token.children)); | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'url': { | 				case 'url': { | ||||||
| 					return [createElement(MkUrl, { | 					return [h(MkUrl, { | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						props: { |  | ||||||
| 						url: token.node.props.url, | 						url: token.node.props.url, | ||||||
| 						rel: 'nofollow noopener', | 						rel: 'nofollow noopener', | ||||||
| 						}, |  | ||||||
| 					})]; | 					})]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'link': { | 				case 'link': { | ||||||
| 					return [createElement(MkLink, { | 					return [h(MkLink, { | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						props: { |  | ||||||
| 						url: token.node.props.url, | 						url: token.node.props.url, | ||||||
| 						rel: 'nofollow noopener', | 						rel: 'nofollow noopener', | ||||||
| 						}, |  | ||||||
| 					}, genEl(token.children))]; | 					}, genEl(token.children))]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'mention': { | 				case 'mention': { | ||||||
| 					return [createElement(MkMention, { | 					return [h(MkMention, { | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						props: { |  | ||||||
| 						host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host, | 						host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host, | ||||||
| 						username: token.node.props.username | 						username: token.node.props.username | ||||||
| 						} |  | ||||||
| 					})]; | 					})]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'hashtag': { | 				case 'hashtag': { | ||||||
| 					return [createElement('router-link', { | 					return [h(RouterLink, { | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						attrs: { |  | ||||||
| 						to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`, | 						to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`, | ||||||
| 						style: 'color:var(--hashtag);' | 						style: 'color:var(--hashtag);' | ||||||
| 						} |  | ||||||
| 					}, `#${token.node.props.hashtag}`)]; | 					}, `#${token.node.props.hashtag}`)]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'blockCode': { | 				case 'blockCode': { | ||||||
| 					return [createElement(MkCode, { | 					return [h(MkCode, { | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						props: { |  | ||||||
| 						code: token.node.props.code, | 						code: token.node.props.code, | ||||||
| 						lang: token.node.props.lang, | 						lang: token.node.props.lang, | ||||||
| 						} |  | ||||||
| 					})]; | 					})]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'inlineCode': { | 				case 'inlineCode': { | ||||||
| 					return [createElement(MkCode, { | 					return [h(MkCode, { | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						props: { |  | ||||||
| 						code: token.node.props.code, | 						code: token.node.props.code, | ||||||
| 						lang: token.node.props.lang, | 						lang: token.node.props.lang, | ||||||
| 						inline: true | 						inline: true | ||||||
| 						} |  | ||||||
| 					})]; | 					})]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'quote': { | 				case 'quote': { | ||||||
| 					if (this.shouldBreak) { | 					if (!this.nowrap) { | ||||||
| 						return [createElement('div', { | 						return [h('div', { | ||||||
| 							attrs: { |  | ||||||
| 							class: 'quote' | 							class: 'quote' | ||||||
| 							} |  | ||||||
| 						}, genEl(token.children))]; | 						}, genEl(token.children))]; | ||||||
| 					} else { | 					} else { | ||||||
| 						return [createElement('span', { | 						return [h('span', { | ||||||
| 							attrs: { |  | ||||||
| 							class: 'quote' | 							class: 'quote' | ||||||
| 							} |  | ||||||
| 						}, genEl(token.children))]; | 						}, genEl(token.children))]; | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'title': { | 				case 'title': { | ||||||
| 					return [createElement('div', { | 					return [h('div', { | ||||||
| 						attrs: { |  | ||||||
| 						class: 'title' | 						class: 'title' | ||||||
| 						} |  | ||||||
| 					}, genEl(token.children))]; | 					}, genEl(token.children))]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'emoji': { | 				case 'emoji': { | ||||||
| 					return [createElement('mk-emoji', { | 					return [h(MkEmoji, { | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						attrs: { |  | ||||||
| 						emoji: token.node.props.emoji, | 						emoji: token.node.props.emoji, | ||||||
| 							name: token.node.props.name | 						name: token.node.props.name, | ||||||
| 						}, |  | ||||||
| 						props: { |  | ||||||
| 						customEmojis: this.customEmojis, | 						customEmojis: this.customEmojis, | ||||||
| 						normal: this.plain | 						normal: this.plain | ||||||
| 						} |  | ||||||
| 					})]; | 					})]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'mathInline': { | 				case 'mathInline': { | ||||||
| 					//const MkFormula = () => import('./formula.vue').then(m => m.default);
 | 					return [h(MkFormula, { | ||||||
| 					return [createElement(MkFormula, { |  | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						props: { |  | ||||||
| 						formula: token.node.props.formula, | 						formula: token.node.props.formula, | ||||||
| 						block: false | 						block: false | ||||||
| 						} |  | ||||||
| 					})]; | 					})]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'mathBlock': { | 				case 'mathBlock': { | ||||||
| 					//const MkFormula = () => import('./formula.vue').then(m => m.default);
 | 					return [h(MkFormula, { | ||||||
| 					return [createElement(MkFormula, { |  | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						props: { |  | ||||||
| 						formula: token.node.props.formula, | 						formula: token.node.props.formula, | ||||||
| 						block: true | 						block: true | ||||||
| 						} |  | ||||||
| 					})]; | 					})]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				case 'search': { | 				case 'search': { | ||||||
| 					//const MkGoogle = () => import('./google.vue').then(m => m.default);
 | 					return [h(MkGoogle, { | ||||||
| 					return [createElement(MkGoogle, { |  | ||||||
| 						key: Math.random(), | 						key: Math.random(), | ||||||
| 						props: { |  | ||||||
| 						q: token.node.props.query | 						q: token.node.props.query | ||||||
| 						} |  | ||||||
| 					})]; | 					})]; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				default: { | 				default: { | ||||||
| 					console.log('unrecognized ast type:', token.node.type); | 					console.error('unrecognized ast type:', token.node.type); | ||||||
| 
 | 
 | ||||||
| 					return []; | 					return []; | ||||||
| 				} | 				} | ||||||
|  | @ -287,6 +234,6 @@ export default Vue.component('misskey-flavored-markdown', { | ||||||
| 		})); | 		})); | ||||||
| 
 | 
 | ||||||
| 		// Parse ast to DOM
 | 		// Parse ast to DOM
 | ||||||
| 		return createElement('span', genEl(ast)); | 		return h('span', genEl(ast)); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -30,10 +30,11 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		src: { | 		src: { | ||||||
| 			type: Array, | 			type: Array, | ||||||
|  | @ -64,7 +65,7 @@ export default Vue.extend({ | ||||||
| 		// Vueが何故かWatchを発動させない場合があるので | 		// Vueが何故かWatchを発動させない場合があるので | ||||||
| 		this.clock = setInterval(this.draw, 1000); | 		this.clock = setInterval(this.draw, 1000); | ||||||
| 	}, | 	}, | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		clearInterval(this.clock); | 		clearInterval(this.clock); | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
|  |  | ||||||
|  | @ -3,10 +3,10 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MfmCore from './mfm'; | import MfmCore from './mfm'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MfmCore | 		MfmCore | ||||||
| 	} | 	} | ||||||
|  | @ -24,7 +24,7 @@ export default Vue.extend({ | ||||||
| 		text-overflow: ellipsis; | 		text-overflow: ellipsis; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	::v-deep .quote { | 	::v-deep(.quote) { | ||||||
| 		display: block; | 		display: block; | ||||||
| 		margin: 8px; | 		margin: 8px; | ||||||
| 		padding: 6px 0 6px 12px; | 		padding: 6px 0 6px 12px; | ||||||
|  | @ -33,15 +33,15 @@ export default Vue.extend({ | ||||||
| 		opacity: 0.7; | 		opacity: 0.7; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	::v-deep pre { | 	::v-deep(pre) { | ||||||
| 		font-size: 0.8em; | 		font-size: 0.8em; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	::v-deep > code { | 	> ::v-deep(code) { | ||||||
| 		word-break: break-all; | 		word-break: break-all; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	::v-deep .title { | 	::v-deep(.title) { | ||||||
| 		text-align: center; | 		text-align: center; | ||||||
| 		border-bottom: solid 1px var(--divider); | 		border-bottom: solid 1px var(--divider); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1,90 +0,0 @@ | ||||||
| <template> |  | ||||||
| <div class="mk-modal" v-hotkey.global="keymap"> |  | ||||||
| 	<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear> |  | ||||||
| 		<div class="bg _modalBg" ref="bg" v-if="show" @click="canClose ? close() : () => {}"></div> |  | ||||||
| 	</transition> |  | ||||||
| 	<transition :name="$store.state.device.animation ? 'modal' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }"> |  | ||||||
| 		<div class="content" ref="content" v-if="show" @click.self="canClose ? close() : () => {}"><slot></slot></div> |  | ||||||
| 	</transition> |  | ||||||
| </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	props: { |  | ||||||
| 		canClose: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false, |  | ||||||
| 			default: true, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			show: true, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	computed: { |  | ||||||
| 		keymap(): any { |  | ||||||
| 			return { |  | ||||||
| 				'esc': this.close, |  | ||||||
| 			}; |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		close() { |  | ||||||
| 			this.show = false; |  | ||||||
| 			(this.$refs.bg as any).style.pointerEvents = 'none'; |  | ||||||
| 			(this.$refs.content as any).style.pointerEvents = 'none'; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .modal-enter-active, .modal-leave-active { |  | ||||||
| 	transition: opacity 0.3s, transform 0.3s !important; |  | ||||||
| } |  | ||||||
| .modal-enter, .modal-leave-to { |  | ||||||
| 	opacity: 0; |  | ||||||
| 	transform: scale(0.9); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .bg-fade-enter-active, .bg-fade-leave-active { |  | ||||||
| 	transition: opacity 0.3s !important; |  | ||||||
| } |  | ||||||
| .bg-fade-enter, .bg-fade-leave-to { |  | ||||||
| 	opacity: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .mk-modal { |  | ||||||
| 	> .bg { |  | ||||||
| 		z-index: 10000; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .content { |  | ||||||
| 		position: fixed; |  | ||||||
| 		z-index: 10000; |  | ||||||
| 		top: 0; |  | ||||||
| 		bottom: 0; |  | ||||||
| 		left: 0; |  | ||||||
| 		right: 0; |  | ||||||
| 		max-width: calc(100% - 16px); |  | ||||||
| 		max-height: calc(100% - 16px); |  | ||||||
| 		overflow: auto; |  | ||||||
| 		margin: auto; |  | ||||||
| 
 |  | ||||||
| 		::v-deep > * { |  | ||||||
| 			position: absolute; |  | ||||||
| 			top: 0; |  | ||||||
| 			bottom: 0; |  | ||||||
| 			left: 0; |  | ||||||
| 			right: 0; |  | ||||||
| 			margin: auto; |  | ||||||
| 			max-height: 100%; |  | ||||||
| 			max-width: 100%; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,33 +1,36 @@ | ||||||
| <template> | <template> | ||||||
| <header class="kkwtjztg"> | <header class="kkwtjztg"> | ||||||
| 	<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"> | 	<router-link class="name" :to="userPage(note.user)" v-user-preview="note.user.id"> | ||||||
| 		<mk-user-name :user="note.user"/> | 		<MkUserName :user="note.user"/> | ||||||
| 	</router-link> | 	</router-link> | ||||||
| 	<span class="is-bot" v-if="note.user.isBot">bot</span> | 	<span class="is-bot" v-if="note.user.isBot">bot</span> | ||||||
| 	<span class="username"><mk-acct :user="note.user"/></span> | 	<span class="username"><MkAcct :user="note.user"/></span> | ||||||
| 	<span class="admin" v-if="note.user.isAdmin"><fa :icon="faBookmark"/></span> | 	<span class="admin" v-if="note.user.isAdmin"><Fa :icon="faBookmark"/></span> | ||||||
| 	<span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><fa :icon="farBookmark"/></span> | 	<span class="moderator" v-if="!note.user.isAdmin && note.user.isModerator"><Fa :icon="farBookmark"/></span> | ||||||
| 	<div class="info"> | 	<div class="info"> | ||||||
| 		<span class="mobile" v-if="note.viaMobile"><fa :icon="faMobileAlt"/></span> | 		<span class="mobile" v-if="note.viaMobile"><Fa :icon="faMobileAlt"/></span> | ||||||
| 		<router-link class="created-at" :to="note | notePage"> | 		<router-link class="created-at" :to="notePage(note)"> | ||||||
| 			<mk-time :time="note.createdAt"/> | 			<MkTime :time="note.createdAt"/> | ||||||
| 		</router-link> | 		</router-link> | ||||||
| 		<span class="visibility" v-if="note.visibility !== 'public'"> | 		<span class="visibility" v-if="note.visibility !== 'public'"> | ||||||
| 			<fa v-if="note.visibility === 'home'" :icon="faHome"/> | 			<Fa v-if="note.visibility === 'home'" :icon="faHome"/> | ||||||
| 			<fa v-if="note.visibility === 'followers'" :icon="faUnlock"/> | 			<Fa v-if="note.visibility === 'followers'" :icon="faUnlock"/> | ||||||
| 			<fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/> | 			<Fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/> | ||||||
| 		</span> | 		</span> | ||||||
| 		<span class="localOnly" v-if="note.localOnly"><fa :icon="faBiohazard"/></span> | 		<span class="localOnly" v-if="note.localOnly"><Fa :icon="faBiohazard"/></span> | ||||||
| 	</div> | 	</div> | ||||||
| </header> | </header> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faHome, faUnlock, faEnvelope, faMobileAlt, faBookmark, faBiohazard } from '@fortawesome/free-solid-svg-icons'; | import { faHome, faUnlock, faEnvelope, faMobileAlt, faBookmark, faBiohazard } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons'; | import { faBookmark as farBookmark } from '@fortawesome/free-regular-svg-icons'; | ||||||
|  | import notePage from '../filters/note'; | ||||||
|  | import { userPage } from '../filters/user'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		note: { | 		note: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -39,6 +42,11 @@ export default Vue.extend({ | ||||||
| 		return { | 		return { | ||||||
| 			faHome, faUnlock, faEnvelope, faMobileAlt, faBookmark, farBookmark, faBiohazard | 			faHome, faUnlock, faEnvelope, faMobileAlt, faBookmark, farBookmark, faBiohazard | ||||||
| 		}; | 		}; | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		notePage, | ||||||
|  | 		userPage | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| <template> | <template> | ||||||
| <div class="yohlumlk"> | <div class="yohlumlk"> | ||||||
| 	<mk-avatar class="avatar" :user="note.user"/> | 	<MkAvatar class="avatar" :user="note.user"/> | ||||||
| 	<div class="main"> | 	<div class="main"> | ||||||
| 		<x-note-header class="header" :note="note" :mini="true"/> | 		<XNoteHeader class="header" :note="note" :mini="true"/> | ||||||
| 		<div class="body"> | 		<div class="body"> | ||||||
| 			<p v-if="note.cw != null" class="cw"> | 			<p v-if="note.cw != null" class="cw"> | ||||||
| 				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span> | 				<span class="text" v-if="note.cw != ''">{{ note.cw }}</span> | ||||||
| 				<x-cw-button v-model="showContent" :note="note"/> | 				<XCwButton v-model:value="showContent" :note="note"/> | ||||||
| 			</p> | 			</p> | ||||||
| 			<div class="content" v-show="note.cw == null || showContent"> | 			<div class="content" v-show="note.cw == null || showContent"> | ||||||
| 				<x-sub-note-content class="text" :note="note"/> | 				<XSubNote-content class="text" :note="note"/> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
|  | @ -17,12 +17,13 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import XNoteHeader from './note-header.vue'; | import XNoteHeader from './note-header.vue'; | ||||||
| import XSubNoteContent from './sub-note-content.vue'; | import XSubNoteContent from './sub-note-content.vue'; | ||||||
| import XCwButton from './cw-button.vue'; | import XCwButton from './cw-button.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XNoteHeader, | 		XNoteHeader, | ||||||
| 		XSubNoteContent, | 		XSubNoteContent, | ||||||
|  |  | ||||||
|  | @ -1,32 +1,33 @@ | ||||||
| <template> | <template> | ||||||
| <div class="wrpstxzv" :class="{ children }" v-size="{ max: [450] }"> | <div class="wrpstxzv" :class="{ children }" v-size="{ max: [450] }"> | ||||||
| 	<div class="main"> | 	<div class="main"> | ||||||
| 		<mk-avatar class="avatar" :user="note.user"/> | 		<MkAvatar class="avatar" :user="note.user"/> | ||||||
| 		<div class="body"> | 		<div class="body"> | ||||||
| 			<x-note-header class="header" :note="note" :mini="true"/> | 			<XNoteHeader class="header" :note="note" :mini="true"/> | ||||||
| 			<div class="body"> | 			<div class="body"> | ||||||
| 				<p v-if="note.cw != null" class="cw"> | 				<p v-if="note.cw != null" class="cw"> | ||||||
| 					<mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" /> | 					<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" /> | ||||||
| 					<x-cw-button v-model="showContent" :note="note"/> | 					<XCwButton v-model:value="showContent" :note="note"/> | ||||||
| 				</p> | 				</p> | ||||||
| 				<div class="content" v-show="note.cw == null || showContent"> | 				<div class="content" v-show="note.cw == null || showContent"> | ||||||
| 					<x-sub-note-content class="text" :note="note"/> | 					<XSubNote-content class="text" :note="note"/> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<x-sub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/> | 	<XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import XNoteHeader from './note-header.vue'; | import XNoteHeader from './note-header.vue'; | ||||||
| import XSubNoteContent from './sub-note-content.vue'; | import XSubNoteContent from './sub-note-content.vue'; | ||||||
| import XCwButton from './cw-button.vue'; | import XCwButton from './cw-button.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	name: 'x-sub', | 	name: 'XSub', | ||||||
| 
 | 
 | ||||||
| 	components: { | 	components: { | ||||||
| 		XNoteHeader, | 		XNoteHeader, | ||||||
|  | @ -65,7 +66,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	created() { | 	created() { | ||||||
| 		if (this.detail) { | 		if (this.detail) { | ||||||
| 			this.$root.api('notes/children', { | 			os.api('notes/children', { | ||||||
| 				noteId: this.note.id, | 				noteId: this.note.id, | ||||||
| 				limit: 5 | 				limit: 5 | ||||||
| 			}).then(replies => { | 			}).then(replies => { | ||||||
|  |  | ||||||
|  | @ -8,95 +8,99 @@ | ||||||
| 	v-hotkey="keymap" | 	v-hotkey="keymap" | ||||||
| 	v-size="{ max: [500, 450, 350, 300] }" | 	v-size="{ max: [500, 450, 350, 300] }" | ||||||
| > | > | ||||||
| 	<x-sub v-for="note in conversation" class="reply-to-more" :key="note.id" :note="note"/> | 	<XSub v-for="note in conversation" class="reply-to-more" :key="note.id" :note="note"/> | ||||||
| 	<x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/> | 	<XSub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/> | ||||||
| 	<div class="info" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div> | 	<div class="info" v-if="pinned"><Fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div> | ||||||
| 	<div class="info" v-if="appearNote._prId_"><fa :icon="faBullhorn"/> {{ $t('promotion') }}<button class="_textButton hide" @click="readPromo()">{{ $t('hideThisNote') }} <fa :icon="faTimes"/></button></div> | 	<div class="info" v-if="appearNote._prId_"><Fa :icon="faBullhorn"/> {{ $t('promotion') }}<button class="_textButton hide" @click="readPromo()">{{ $t('hideThisNote') }} <Fa :icon="faTimes"/></button></div> | ||||||
| 	<div class="info" v-if="appearNote._featuredId_"><fa :icon="faBolt"/> {{ $t('featured') }}</div> | 	<div class="info" v-if="appearNote._featuredId_"><Fa :icon="faBolt"/> {{ $t('featured') }}</div> | ||||||
| 	<div class="renote" v-if="isRenote"> | 	<div class="renote" v-if="isRenote"> | ||||||
| 		<mk-avatar class="avatar" :user="note.user"/> | 		<MkAvatar class="avatar" :user="note.user"/> | ||||||
| 		<fa :icon="faRetweet"/> | 		<Fa :icon="faRetweet"/> | ||||||
| 		<i18n path="renotedBy" tag="span"> | 		<i18n-t keypath="renotedBy" tag="span"> | ||||||
| 			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId" place="user"> | 			<template #user> | ||||||
| 				<mk-user-name :user="note.user"/> | 				<router-link class="name" :to="userPage(note.user)" v-user-preview="note.userId"> | ||||||
|  | 					<MkUserName :user="note.user"/> | ||||||
| 				</router-link> | 				</router-link> | ||||||
| 		</i18n> | 			</template> | ||||||
|  | 		</i18n-t> | ||||||
| 		<div class="info"> | 		<div class="info"> | ||||||
| 			<button class="_button time" @click="showRenoteMenu()" ref="renoteTime"> | 			<button class="_button time" @click="showRenoteMenu()" ref="renoteTime"> | ||||||
| 				<fa class="dropdownIcon" v-if="isMyRenote" :icon="faEllipsisH"/> | 				<Fa class="dropdownIcon" v-if="isMyRenote" :icon="faEllipsisH"/> | ||||||
| 				<mk-time :time="note.createdAt"/> | 				<MkTime :time="note.createdAt"/> | ||||||
| 			</button> | 			</button> | ||||||
| 			<span class="visibility" v-if="note.visibility !== 'public'"> | 			<span class="visibility" v-if="note.visibility !== 'public'"> | ||||||
| 				<fa v-if="note.visibility === 'home'" :icon="faHome"/> | 				<Fa v-if="note.visibility === 'home'" :icon="faHome"/> | ||||||
| 				<fa v-if="note.visibility === 'followers'" :icon="faUnlock"/> | 				<Fa v-if="note.visibility === 'followers'" :icon="faUnlock"/> | ||||||
| 				<fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/> | 				<Fa v-if="note.visibility === 'specified'" :icon="faEnvelope"/> | ||||||
| 			</span> | 			</span> | ||||||
| 			<span class="localOnly" v-if="note.localOnly"><fa :icon="faBiohazard"/></span> | 			<span class="localOnly" v-if="note.localOnly"><Fa :icon="faBiohazard"/></span> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<article class="article"> | 	<article class="article" @contextmenu="onContextmenu"> | ||||||
| 		<mk-avatar class="avatar" :user="appearNote.user"/> | 		<MkAvatar class="avatar" :user="appearNote.user"/> | ||||||
| 		<div class="main"> | 		<div class="main"> | ||||||
| 			<x-note-header class="header" :note="appearNote" :mini="true"/> | 			<XNoteHeader class="header" :note="appearNote" :mini="true"/> | ||||||
| 			<div class="body" ref="noteBody"> | 			<div class="body" ref="noteBody"> | ||||||
| 				<p v-if="appearNote.cw != null" class="cw"> | 				<p v-if="appearNote.cw != null" class="cw"> | ||||||
| 				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> | 					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> | ||||||
| 					<x-cw-button v-model="showContent" :note="appearNote"/> | 					<XCwButton v-model:value="showContent" :note="appearNote"/> | ||||||
| 				</p> | 				</p> | ||||||
| 				<div class="content" v-show="appearNote.cw == null || showContent"> | 				<div class="content" v-show="appearNote.cw == null || showContent"> | ||||||
| 					<div class="text"> | 					<div class="text"> | ||||||
| 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> | 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> | ||||||
| 						<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link> | 						<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><Fa :icon="faReply"/></router-link> | ||||||
| 						<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> | 						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> | ||||||
| 						<a class="rp" v-if="appearNote.renote != null">RN:</a> | 						<a class="rp" v-if="appearNote.renote != null">RN:</a> | ||||||
| 					</div> | 					</div> | ||||||
| 					<div class="files" v-if="appearNote.files.length > 0"> | 					<div class="files" v-if="appearNote.files.length > 0"> | ||||||
| 						<x-media-list :media-list="appearNote.files" :parent-element="noteBody"/> | 						<XMediaList :media-list="appearNote.files" :parent-element="noteBody"/> | ||||||
| 					</div> | 					</div> | ||||||
| 					<x-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/> | 					<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/> | ||||||
| 					<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="detail" class="url-preview"/> | 					<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="detail" class="url-preview"/> | ||||||
| 					<div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div> | 					<div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div> | ||||||
| 				</div> | 				</div> | ||||||
| 				<router-link v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><fa :icon="faSatelliteDish"/> {{ appearNote.channel.name }}</router-link> | 				<router-link v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><Fa :icon="faSatelliteDish"/> {{ appearNote.channel.name }}</router-link> | ||||||
| 			</div> | 			</div> | ||||||
| 			<footer class="footer"> | 			<footer class="footer"> | ||||||
| 				<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/> | 				<XReactionsViewer :note="appearNote" ref="reactionsViewer"/> | ||||||
| 				<button @click="reply()" class="button _button"> | 				<button @click="reply()" class="button _button"> | ||||||
| 					<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template> | 					<template v-if="appearNote.reply"><Fa :icon="faReplyAll"/></template> | ||||||
| 					<template v-else><fa :icon="faReply"/></template> | 					<template v-else><Fa :icon="faReply"/></template> | ||||||
| 					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> | 					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> | ||||||
| 				</button> | 				</button> | ||||||
| 				<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton"> | 				<button v-if="canRenote" @click="renote()" class="button _button" ref="renoteButton"> | ||||||
| 					<fa :icon="faRetweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> | 					<Fa :icon="faRetweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> | ||||||
| 				</button> | 				</button> | ||||||
| 				<button v-else class="button _button"> | 				<button v-else class="button _button"> | ||||||
| 					<fa :icon="faBan"/> | 					<Fa :icon="faBan"/> | ||||||
| 				</button> | 				</button> | ||||||
| 				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> | 				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> | ||||||
| 					<fa :icon="faPlus"/> | 					<Fa :icon="faPlus"/> | ||||||
| 				</button> | 				</button> | ||||||
| 				<button v-if="appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton"> | 				<button v-if="appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton"> | ||||||
| 					<fa :icon="faMinus"/> | 					<Fa :icon="faMinus"/> | ||||||
| 				</button> | 				</button> | ||||||
| 				<button class="button _button" @click="menu()" ref="menuButton"> | 				<button class="button _button" @click="menu()" ref="menuButton"> | ||||||
| 					<fa :icon="faEllipsisH"/> | 					<Fa :icon="faEllipsisH"/> | ||||||
| 				</button> | 				</button> | ||||||
| 			</footer> | 			</footer> | ||||||
| 		</div> | 		</div> | ||||||
| 	</article> | 	</article> | ||||||
| 	<x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/> | 	<XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/> | ||||||
| </div> | </div> | ||||||
| <div v-else class="_panel muted" @click="muted = false"> | <div v-else class="_panel muted" @click="muted = false"> | ||||||
| 	<i18n path="userSaysSomething" tag="small"> | 	<i18n-t keypath="userSaysSomething" tag="small"> | ||||||
| 		<router-link class="name" :to="appearNote.user | userPage" v-user-preview="appearNote.userId" place="name"> | 		<template #name> | ||||||
| 			<mk-user-name :user="appearNote.user"/> | 			<router-link class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId"> | ||||||
|  | 				<MkUserName :user="appearNote.user"/> | ||||||
| 			</router-link> | 			</router-link> | ||||||
| 	</i18n> | 		</template> | ||||||
|  | 	</i18n-t> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue'; | ||||||
| import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons'; | import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; | import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { parse } from '../../mfm/parse'; | import { parse } from '../../mfm/parse'; | ||||||
|  | @ -108,21 +112,24 @@ import XReactionsViewer from './reactions-viewer.vue'; | ||||||
| import XMediaList from './media-list.vue'; | import XMediaList from './media-list.vue'; | ||||||
| import XCwButton from './cw-button.vue'; | import XCwButton from './cw-button.vue'; | ||||||
| import XPoll from './poll.vue'; | import XPoll from './poll.vue'; | ||||||
| import MkUrlPreview from './url-preview.vue'; | import { pleaseLogin } from '@/scripts/please-login'; | ||||||
| import MkReactionPicker from './reaction-picker.vue'; | import { focusPrev, focusNext } from '@/scripts/focus'; | ||||||
| import pleaseLogin from '../scripts/please-login'; | import { url } from '@/config'; | ||||||
| import { focusPrev, focusNext } from '../scripts/focus'; | import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||||
| import { url } from '../config'; | import { checkWordMute } from '@/scripts/check-word-mute'; | ||||||
| import copyToClipboard from '../scripts/copy-to-clipboard'; | import { userPage } from '@/filters/user'; | ||||||
| import { checkWordMute } from '../scripts/check-word-mute'; | import * as os from '@/os'; | ||||||
| import { utils } from '@syuilo/aiscript'; | import { noteActions, noteViewInterruptors } from '@/store'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | function markRawAll(...xs) { | ||||||
| 	model: { | 	for (const x of xs) { | ||||||
| 		prop: 'note', | 		markRaw(x); | ||||||
| 		event: 'updated' | 	} | ||||||
| 	}, | } | ||||||
| 
 | 
 | ||||||
|  | markRawAll(faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug, faSatelliteDish); | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XSub, | 		XSub, | ||||||
| 		XNoteHeader, | 		XNoteHeader, | ||||||
|  | @ -131,7 +138,7 @@ export default Vue.extend({ | ||||||
| 		XMediaList, | 		XMediaList, | ||||||
| 		XCwButton, | 		XCwButton, | ||||||
| 		XPoll, | 		XPoll, | ||||||
| 		MkUrlPreview, | 		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	inject: { | 	inject: { | ||||||
|  | @ -157,6 +164,8 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	emits: ['update:note'], | ||||||
|  | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			connection: null, | 			connection: null, | ||||||
|  | @ -171,6 +180,9 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	computed: { | 	computed: { | ||||||
|  | 		rs() { | ||||||
|  | 			return this.$store.state.settings.reactions; | ||||||
|  | 		}, | ||||||
| 		keymap(): any { | 		keymap(): any { | ||||||
| 			return { | 			return { | ||||||
| 				'r': () => this.reply(true), | 				'r': () => this.reply(true), | ||||||
|  | @ -184,16 +196,16 @@ export default Vue.extend({ | ||||||
| 				'esc': this.blur, | 				'esc': this.blur, | ||||||
| 				'm|o': () => this.menu(true), | 				'm|o': () => this.menu(true), | ||||||
| 				's': this.toggleShowContent, | 				's': this.toggleShowContent, | ||||||
| 				'1': () => this.reactDirectly(this.$store.state.settings.reactions[0]), | 				'1': () => this.reactDirectly(this.rs[0]), | ||||||
| 				'2': () => this.reactDirectly(this.$store.state.settings.reactions[1]), | 				'2': () => this.reactDirectly(this.rs[1]), | ||||||
| 				'3': () => this.reactDirectly(this.$store.state.settings.reactions[2]), | 				'3': () => this.reactDirectly(this.rs[2]), | ||||||
| 				'4': () => this.reactDirectly(this.$store.state.settings.reactions[3]), | 				'4': () => this.reactDirectly(this.rs[3]), | ||||||
| 				'5': () => this.reactDirectly(this.$store.state.settings.reactions[4]), | 				'5': () => this.reactDirectly(this.rs[4]), | ||||||
| 				'6': () => this.reactDirectly(this.$store.state.settings.reactions[5]), | 				'6': () => this.reactDirectly(this.rs[5]), | ||||||
| 				'7': () => this.reactDirectly(this.$store.state.settings.reactions[6]), | 				'7': () => this.reactDirectly(this.rs[6]), | ||||||
| 				'8': () => this.reactDirectly(this.$store.state.settings.reactions[7]), | 				'8': () => this.reactDirectly(this.rs[7]), | ||||||
| 				'9': () => this.reactDirectly(this.$store.state.settings.reactions[8]), | 				'9': () => this.reactDirectly(this.rs[8]), | ||||||
| 				'0': () => this.reactDirectly(this.$store.state.settings.reactions[9]), | 				'0': () => this.reactDirectly(this.rs[9]), | ||||||
| 			}; | 			}; | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | @ -251,22 +263,22 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	async created() { | 	async created() { | ||||||
| 		if (this.$store.getters.isSignedIn) { | 		if (this.$store.getters.isSignedIn) { | ||||||
| 			this.connection = this.$root.stream; | 			this.connection = os.stream; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// plugin | 		// plugin | ||||||
| 		if (this.$store.state.noteViewInterruptors.length > 0) { | 		if (noteViewInterruptors.length > 0) { | ||||||
| 			let result = this.note; | 			let result = this.note; | ||||||
| 			for (const interruptor of this.$store.state.noteViewInterruptors) { | 			for (const interruptor of noteViewInterruptors) { | ||||||
| 				result = utils.valToJs(await interruptor.handler(JSON.parse(JSON.stringify(result)))); | 				result = await interruptor.handler(JSON.parse(JSON.stringify(result))); | ||||||
| 			} | 			} | ||||||
| 			this.$emit('updated', Object.freeze(result)); | 			this.$emit('update:note', Object.freeze(result)); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords); | 		this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords); | ||||||
| 
 | 
 | ||||||
| 		if (this.detail) { | 		if (this.detail) { | ||||||
| 			this.$root.api('notes/children', { | 			os.api('notes/children', { | ||||||
| 				noteId: this.appearNote.id, | 				noteId: this.appearNote.id, | ||||||
| 				limit: 30 | 				limit: 30 | ||||||
| 			}).then(replies => { | 			}).then(replies => { | ||||||
|  | @ -274,7 +286,7 @@ export default Vue.extend({ | ||||||
| 			}); | 			}); | ||||||
| 
 | 
 | ||||||
| 			if (this.appearNote.replyId) { | 			if (this.appearNote.replyId) { | ||||||
| 				this.$root.api('notes/conversation', { | 				os.api('notes/conversation', { | ||||||
| 					noteId: this.appearNote.replyId | 					noteId: this.appearNote.replyId | ||||||
| 				}).then(conversation => { | 				}).then(conversation => { | ||||||
| 					this.conversation = conversation.reverse(); | 					this.conversation = conversation.reverse(); | ||||||
|  | @ -293,7 +305,7 @@ export default Vue.extend({ | ||||||
| 		this.noteBody = this.$refs.noteBody; | 		this.noteBody = this.$refs.noteBody; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		this.decapture(true); | 		this.decapture(true); | ||||||
| 
 | 
 | ||||||
| 		if (this.$store.getters.isSignedIn) { | 		if (this.$store.getters.isSignedIn) { | ||||||
|  | @ -303,7 +315,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		updateAppearNote(v) { | 		updateAppearNote(v) { | ||||||
| 			this.$emit('updated', Object.freeze(this.isRenote ? { | 			this.$emit('update:note', Object.freeze(this.isRenote ? { | ||||||
| 				...this.note, | 				...this.note, | ||||||
| 				renote: { | 				renote: { | ||||||
| 					...this.note.renote, | 					...this.note.renote, | ||||||
|  | @ -316,7 +328,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		readPromo() { | 		readPromo() { | ||||||
| 			(this as any).$root.api('promo/read', { | 			os.api('promo/read', { | ||||||
| 				noteId: this.appearNote.id | 				noteId: this.appearNote.id | ||||||
| 			}); | 			}); | ||||||
| 			this.isDeleted = true; | 			this.isDeleted = true; | ||||||
|  | @ -439,8 +451,8 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		reply(viaKeyboard = false) { | 		reply(viaKeyboard = false) { | ||||||
| 			pleaseLogin(this.$root); | 			pleaseLogin(); | ||||||
| 			this.$root.post({ | 			os.post({ | ||||||
| 				reply: this.appearNote, | 				reply: this.appearNote, | ||||||
| 				animation: !viaKeyboard, | 				animation: !viaKeyboard, | ||||||
| 			}, () => { | 			}, () => { | ||||||
|  | @ -449,14 +461,13 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		renote(viaKeyboard = false) { | 		renote(viaKeyboard = false) { | ||||||
| 			pleaseLogin(this.$root); | 			pleaseLogin(); | ||||||
| 			this.blur(); | 			this.blur(); | ||||||
| 			this.$root.menu({ | 			os.modalMenu([{ | ||||||
| 				items: [{ |  | ||||||
| 				text: this.$t('renote'), | 				text: this.$t('renote'), | ||||||
| 				icon: faRetweet, | 				icon: faRetweet, | ||||||
| 				action: () => { | 				action: () => { | ||||||
| 						(this as any).$root.api('notes/create', { | 					os.api('notes/create', { | ||||||
| 						renoteId: this.appearNote.id | 						renoteId: this.appearNote.id | ||||||
| 					}); | 					}); | ||||||
| 				} | 				} | ||||||
|  | @ -464,42 +475,42 @@ export default Vue.extend({ | ||||||
| 				text: this.$t('quote'), | 				text: this.$t('quote'), | ||||||
| 				icon: faQuoteRight, | 				icon: faQuoteRight, | ||||||
| 				action: () => { | 				action: () => { | ||||||
| 						this.$root.post({ | 					os.post({ | ||||||
| 						renote: this.appearNote, | 						renote: this.appearNote, | ||||||
| 					}); | 					}); | ||||||
| 				} | 				} | ||||||
| 				}] | 			}], this.$refs.renoteButton, { | ||||||
| 				source: this.$refs.renoteButton, |  | ||||||
| 				viaKeyboard | 				viaKeyboard | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		renoteDirectly() { | 		renoteDirectly() { | ||||||
| 			(this as any).$root.api('notes/create', { | 			os.api('notes/create', { | ||||||
| 				renoteId: this.appearNote.id | 				renoteId: this.appearNote.id | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		react(viaKeyboard = false) { | 		react(viaKeyboard = false) { | ||||||
| 			pleaseLogin(this.$root); | 			pleaseLogin(); | ||||||
| 			this.blur(); | 			this.blur(); | ||||||
| 			const picker = this.$root.new(MkReactionPicker, { | 			os.popup(defineAsyncComponent(() => import('@/components/reaction-picker.vue')), { | ||||||
| 				source: this.$refs.reactButton, |  | ||||||
| 				showFocus: viaKeyboard, | 				showFocus: viaKeyboard, | ||||||
| 			}); | 				src: this.$refs.reactButton, | ||||||
| 			picker.$once('chosen', reaction => { | 			}, { | ||||||
| 				this.$root.api('notes/reactions/create', { | 				done: reaction => { | ||||||
|  | 					if (reaction) { | ||||||
|  | 						os.api('notes/reactions/create', { | ||||||
| 							noteId: this.appearNote.id, | 							noteId: this.appearNote.id, | ||||||
| 							reaction: reaction | 							reaction: reaction | ||||||
| 				}).then(() => { |  | ||||||
| 					picker.close(); |  | ||||||
| 						}); | 						}); | ||||||
| 			}); | 					} | ||||||
| 			picker.$once('closed', this.focus); | 					this.focus(); | ||||||
|  | 				}, | ||||||
|  | 			}, 'closed'); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		reactDirectly(reaction) { | 		reactDirectly(reaction) { | ||||||
| 			this.$root.api('notes/reactions/create', { | 			os.api('notes/reactions/create', { | ||||||
| 				noteId: this.appearNote.id, | 				noteId: this.appearNote.id, | ||||||
| 				reaction: reaction | 				reaction: reaction | ||||||
| 			}); | 			}); | ||||||
|  | @ -508,81 +519,67 @@ export default Vue.extend({ | ||||||
| 		undoReact(note) { | 		undoReact(note) { | ||||||
| 			const oldReaction = note.myReaction; | 			const oldReaction = note.myReaction; | ||||||
| 			if (!oldReaction) return; | 			if (!oldReaction) return; | ||||||
| 			this.$root.api('notes/reactions/delete', { | 			os.api('notes/reactions/delete', { | ||||||
| 				noteId: note.id | 				noteId: note.id | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		favorite() { | 		favorite() { | ||||||
| 			pleaseLogin(this.$root); | 			pleaseLogin(); | ||||||
| 			this.$root.api('notes/favorites/create', { | 			os.apiWithDialog('notes/favorites/create', { | ||||||
| 				noteId: this.appearNote.id | 				noteId: this.appearNote.id | ||||||
| 			}).then(() => { |  | ||||||
| 				this.$root.dialog({ |  | ||||||
| 					type: 'success', |  | ||||||
| 					iconOnly: true, autoClose: true |  | ||||||
| 				}); |  | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		del() { | 		del() { | ||||||
| 			this.$root.dialog({ | 			os.dialog({ | ||||||
| 				type: 'warning', | 				type: 'warning', | ||||||
| 				text: this.$t('noteDeleteConfirm'), | 				text: this.$t('noteDeleteConfirm'), | ||||||
| 				showCancelButton: true | 				showCancelButton: true | ||||||
| 			}).then(({ canceled }) => { | 			}).then(({ canceled }) => { | ||||||
| 				if (canceled) return; | 				if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 				this.$root.api('notes/delete', { | 				os.api('notes/delete', { | ||||||
| 					noteId: this.appearNote.id | 					noteId: this.appearNote.id | ||||||
| 				}); | 				}); | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		delEdit() { | 		delEdit() { | ||||||
| 			this.$root.dialog({ | 			os.dialog({ | ||||||
| 				type: 'warning', | 				type: 'warning', | ||||||
| 				text: this.$t('deleteAndEditConfirm'), | 				text: this.$t('deleteAndEditConfirm'), | ||||||
| 				showCancelButton: true | 				showCancelButton: true | ||||||
| 			}).then(({ canceled }) => { | 			}).then(({ canceled }) => { | ||||||
| 				if (canceled) return; | 				if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 				this.$root.api('notes/delete', { | 				os.api('notes/delete', { | ||||||
| 					noteId: this.appearNote.id | 					noteId: this.appearNote.id | ||||||
| 				}); | 				}); | ||||||
| 
 | 
 | ||||||
| 				this.$root.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel }); | 				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel }); | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		toggleFavorite(favorite: boolean) { | 		toggleFavorite(favorite: boolean) { | ||||||
| 			this.$root.api(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { | 			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { | ||||||
| 				noteId: this.appearNote.id | 				noteId: this.appearNote.id | ||||||
| 			}).then(() => { |  | ||||||
| 				this.$root.dialog({ |  | ||||||
| 					type: 'success', |  | ||||||
| 					iconOnly: true, autoClose: true |  | ||||||
| 				}); |  | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		toggleWatch(watch: boolean) { | 		toggleWatch(watch: boolean) { | ||||||
| 			this.$root.api(watch ? 'notes/watching/create' : 'notes/watching/delete', { | 			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', { | ||||||
| 				noteId: this.appearNote.id | 				noteId: this.appearNote.id | ||||||
| 			}).then(() => { |  | ||||||
| 				this.$root.dialog({ |  | ||||||
| 					type: 'success', |  | ||||||
| 					iconOnly: true, autoClose: true |  | ||||||
| 				}); |  | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		async menu(viaKeyboard = false) { | 		getMenu() { | ||||||
| 			let menu; | 			let menu; | ||||||
| 			if (this.$store.getters.isSignedIn) { | 			if (this.$store.getters.isSignedIn) { | ||||||
| 				const state = await this.$root.api('notes/state', { | 				const statePromise = os.api('notes/state', { | ||||||
| 					noteId: this.appearNote.id | 					noteId: this.appearNote.id | ||||||
| 				}); | 				}); | ||||||
|  | 
 | ||||||
| 				menu = [{ | 				menu = [{ | ||||||
| 					type: 'link', | 					type: 'link', | ||||||
| 					icon: faInfoCircle, | 					icon: faInfoCircle, | ||||||
|  | @ -604,7 +601,7 @@ export default Vue.extend({ | ||||||
| 					} | 					} | ||||||
| 				} : undefined, | 				} : undefined, | ||||||
| 				null, | 				null, | ||||||
| 				state.isFavorited ? { | 				statePromise.then(state => state.isFavorited ? { | ||||||
| 					icon: faStar, | 					icon: faStar, | ||||||
| 					text: this.$t('unfavorite'), | 					text: this.$t('unfavorite'), | ||||||
| 					action: () => this.toggleFavorite(false) | 					action: () => this.toggleFavorite(false) | ||||||
|  | @ -612,8 +609,8 @@ export default Vue.extend({ | ||||||
| 					icon: faStar, | 					icon: faStar, | ||||||
| 					text: this.$t('favorite'), | 					text: this.$t('favorite'), | ||||||
| 					action: () => this.toggleFavorite(true) | 					action: () => this.toggleFavorite(true) | ||||||
| 				}, | 				}), | ||||||
| 				this.appearNote.userId != this.$store.state.i.id ? state.isWatching ? { | 				(this.appearNote.userId != this.$store.state.i.id) ? statePromise.then(state => state.isWatching ? { | ||||||
| 					icon: faEyeSlash, | 					icon: faEyeSlash, | ||||||
| 					text: this.$t('unwatch'), | 					text: this.$t('unwatch'), | ||||||
| 					action: () => this.toggleWatch(false) | 					action: () => this.toggleWatch(false) | ||||||
|  | @ -621,7 +618,7 @@ export default Vue.extend({ | ||||||
| 					icon: faEye, | 					icon: faEye, | ||||||
| 					text: this.$t('watch'), | 					text: this.$t('watch'), | ||||||
| 					action: () => this.toggleWatch(true) | 					action: () => this.toggleWatch(true) | ||||||
| 				} : undefined, | 				}) : undefined, | ||||||
| 				this.appearNote.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.appearNote.id) ? { | 				this.appearNote.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.appearNote.id) ? { | ||||||
| 					icon: faThumbtack, | 					icon: faThumbtack, | ||||||
| 					text: this.$t('unpin'), | 					text: this.$t('unpin'), | ||||||
|  | @ -650,6 +647,7 @@ export default Vue.extend({ | ||||||
| 					{ | 					{ | ||||||
| 						icon: faTrashAlt, | 						icon: faTrashAlt, | ||||||
| 						text: this.$t('delete'), | 						text: this.$t('delete'), | ||||||
|  | 						danger: true, | ||||||
| 						action: this.del | 						action: this.del | ||||||
| 					}] | 					}] | ||||||
| 					: [] | 					: [] | ||||||
|  | @ -674,8 +672,8 @@ export default Vue.extend({ | ||||||
| 				.filter(x => x !== undefined); | 				.filter(x => x !== undefined); | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			if (this.$store.state.noteActions.length > 0) { | 			if (noteActions.length > 0) { | ||||||
| 				menu = menu.concat([null, ...this.$store.state.noteActions.map(action => ({ | 				menu = menu.concat([null, ...noteActions.map(action => ({ | ||||||
| 					icon: faPlug, | 					icon: faPlug, | ||||||
| 					text: action.title, | 					text: action.title, | ||||||
| 					action: () => { | 					action: () => { | ||||||
|  | @ -684,27 +682,39 @@ export default Vue.extend({ | ||||||
| 				}))]); | 				}))]); | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			this.$root.menu({ | 			return menu; | ||||||
| 				items: menu, | 		}, | ||||||
| 				source: this.$refs.menuButton, | 
 | ||||||
|  | 		onContextmenu(e) { | ||||||
|  | 			const isLink = (el: HTMLElement) => { | ||||||
|  | 				if (el.tagName === 'A') return true; | ||||||
|  | 				if (el.parentElement) { | ||||||
|  | 					return isLink(el.parentElement); | ||||||
|  | 				} | ||||||
|  | 			}; | ||||||
|  | 			if (isLink(e.target)) return; | ||||||
|  | 			if (window.getSelection().toString() !== '') return; | ||||||
|  | 			os.contextMenu(this.getMenu(), e).then(this.focus); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		menu(viaKeyboard = false) { | ||||||
|  | 			os.modalMenu(this.getMenu(), this.$refs.menuButton, { | ||||||
| 				viaKeyboard | 				viaKeyboard | ||||||
| 			}).then(this.focus); | 			}).then(this.focus); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		showRenoteMenu(viaKeyboard = false) { | 		showRenoteMenu(viaKeyboard = false) { | ||||||
| 			if (!this.isMyRenote) return; | 			if (!this.isMyRenote) return; | ||||||
| 			this.$root.menu({ | 			os.modalMenu([{ | ||||||
| 				items: [{ |  | ||||||
| 				text: this.$t('unrenote'), | 				text: this.$t('unrenote'), | ||||||
| 				icon: faTrashAlt, | 				icon: faTrashAlt, | ||||||
| 				action: () => { | 				action: () => { | ||||||
| 						this.$root.api('notes/delete', { | 					os.api('notes/delete', { | ||||||
| 						noteId: this.note.id | 						noteId: this.note.id | ||||||
| 					}); | 					}); | ||||||
| 					this.isDeleted = true; | 					this.isDeleted = true; | ||||||
| 				} | 				} | ||||||
| 				}], | 			}], this.$refs.renoteTime, { | ||||||
| 				source: this.$refs.renoteTime, |  | ||||||
| 				viaKeyboard: viaKeyboard | 				viaKeyboard: viaKeyboard | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  | @ -715,31 +725,20 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 		copyContent() { | 		copyContent() { | ||||||
| 			copyToClipboard(this.appearNote.text); | 			copyToClipboard(this.appearNote.text); | ||||||
| 			this.$root.dialog({ | 			os.success(); | ||||||
| 				type: 'success', |  | ||||||
| 				iconOnly: true, autoClose: true |  | ||||||
| 			}); |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		copyLink() { | 		copyLink() { | ||||||
| 			copyToClipboard(`${url}/notes/${this.appearNote.id}`); | 			copyToClipboard(`${url}/notes/${this.appearNote.id}`); | ||||||
| 			this.$root.dialog({ | 			os.success(); | ||||||
| 				type: 'success', |  | ||||||
| 				iconOnly: true, autoClose: true |  | ||||||
| 			}); |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		togglePin(pin: boolean) { | 		togglePin(pin: boolean) { | ||||||
| 			this.$root.api(pin ? 'i/pin' : 'i/unpin', { | 			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', { | ||||||
| 				noteId: this.appearNote.id | 				noteId: this.appearNote.id | ||||||
| 			}).then(() => { | 			}, undefined, null, e => { | ||||||
| 				this.$root.dialog({ |  | ||||||
| 					type: 'success', |  | ||||||
| 					iconOnly: true, autoClose: true |  | ||||||
| 				}); |  | ||||||
| 			}).catch(e => { |  | ||||||
| 				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { | 				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { | ||||||
| 					this.$root.dialog({ | 					os.dialog({ | ||||||
| 						type: 'error', | 						type: 'error', | ||||||
| 						text: this.$t('pinLimitExceeded') | 						text: this.$t('pinLimitExceeded') | ||||||
| 					}); | 					}); | ||||||
|  | @ -748,26 +747,16 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		async promote() { | 		async promote() { | ||||||
| 			const { canceled, result: days } = await this.$root.dialog({ | 			const { canceled, result: days } = await os.dialog({ | ||||||
| 				title: this.$t('numberOfDays'), | 				title: this.$t('numberOfDays'), | ||||||
| 				input: { type: 'number' } | 				input: { type: 'number' } | ||||||
| 			}); | 			}); | ||||||
| 
 | 
 | ||||||
| 			if (canceled) return; | 			if (canceled) return; | ||||||
| 
 | 
 | ||||||
| 			this.$root.api('admin/promo/create', { | 			os.apiWithDialog('admin/promo/create', { | ||||||
| 				noteId: this.appearNote.id, | 				noteId: this.appearNote.id, | ||||||
| 				expiresAt: Date.now() + (86400000 * days) | 				expiresAt: Date.now() + (86400000 * days) | ||||||
| 			}).then(() => { |  | ||||||
| 				this.$root.dialog({ |  | ||||||
| 					type: 'success', |  | ||||||
| 					iconOnly: true, autoClose: true |  | ||||||
| 				}); |  | ||||||
| 			}).catch(e => { |  | ||||||
| 				this.$root.dialog({ |  | ||||||
| 					type: 'error', |  | ||||||
| 					text: e |  | ||||||
| 				}); |  | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | @ -785,7 +774,9 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 		focusAfter() { | 		focusAfter() { | ||||||
| 			focusNext(this.$el); | 			focusNext(this.$el); | ||||||
| 		} | 		}, | ||||||
|  | 
 | ||||||
|  | 		userPage | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  | @ -795,10 +786,28 @@ export default Vue.extend({ | ||||||
| 	position: relative; | 	position: relative; | ||||||
| 	transition: box-shadow 0.1s ease; | 	transition: box-shadow 0.1s ease; | ||||||
| 	overflow: hidden; | 	overflow: hidden; | ||||||
|  | 	contain: content; | ||||||
| 
 | 
 | ||||||
| 	&:focus { | 	&:focus { | ||||||
| 		outline: none; | 		outline: none; | ||||||
| 		box-shadow: 0 0 0 3px var(--focus); | 
 | ||||||
|  | 		&:after { | ||||||
|  | 			content: ""; | ||||||
|  | 			pointer-events: none; | ||||||
|  | 			display: block; | ||||||
|  | 			position: absolute; | ||||||
|  | 			z-index: 10; | ||||||
|  | 			top: 0; | ||||||
|  | 			left: 0; | ||||||
|  | 			right: 0; | ||||||
|  | 			bottom: 0; | ||||||
|  | 			margin: auto; | ||||||
|  | 			width: calc(100% - 8px); | ||||||
|  | 			height: calc(100% - 8px); | ||||||
|  | 			border: dashed 1px var(--focus); | ||||||
|  | 			border-radius: var(--radius); | ||||||
|  | 			box-sizing: border-box; | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	&:hover > .article > .main > .footer > .button { | 	&:hover > .article > .main > .footer > .button { | ||||||
|  |  | ||||||
|  | @ -1,42 +1,41 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mk-notes"> | <div class="_list_"> | ||||||
| 	<div class="_fullinfo" v-if="empty"> | 	<div class="_fullinfo" v-if="empty"> | ||||||
| 		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | 		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||||
| 		<div>{{ $t('noNotes') }}</div> | 		<div>{{ $t('noNotes') }}</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<mk-error v-if="error" @retry="init()"/> | 	<MkError v-if="error" @retry="init()"/> | ||||||
| 
 | 
 | ||||||
| 	<div v-show="more && reversed" style="margin-bottom: var(--margin);"> | 	<div v-show="more && reversed" style="margin-bottom: var(--margin);"> | ||||||
| 		<button class="_panel _button" ref="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | 		<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||||
| 			<template v-if="!moreFetching">{{ $t('loadMore') }}</template> | 			<template v-if="!moreFetching">{{ $t('loadMore') }}</template> | ||||||
| 			<template v-if="moreFetching"><mk-loading inline/></template> | 			<template v-if="moreFetching"><MkLoading inline/></template> | ||||||
| 		</button> | 		</button> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<x-list ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> | 	<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> | ||||||
| 		<x-note :note="note" @updated="updated(note, $event)" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/> | 		<XNote :note="note" @update:note="updated(note, $event)" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/> | ||||||
| 	</x-list> | 	</XList> | ||||||
| 
 | 
 | ||||||
| 	<div v-show="more && !reversed" style="margin-top: var(--margin);"> | 	<div v-show="more && !reversed" style="margin-top: var(--margin);"> | ||||||
| 		<button class="_panel _button" ref="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | 		<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||||
| 			<template v-if="!moreFetching">{{ $t('loadMore') }}</template> | 			<template v-if="!moreFetching">{{ $t('loadMore') }}</template> | ||||||
| 			<template v-if="moreFetching"><mk-loading inline/></template> | 			<template v-if="moreFetching"><MkLoading inline/></template> | ||||||
| 		</button> | 		</button> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import paging from '../scripts/paging'; | import paging from '@/scripts/paging'; | ||||||
| import XNote from './note.vue'; | import XNote from './note.vue'; | ||||||
| import XList from './date-separated-list.vue'; | import XList from './date-separated-list.vue'; | ||||||
| import MkButton from './ui/button.vue'; |  | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XNote, XList, MkButton | 		XNote, XList, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mixins: [ | 	mixins: [ | ||||||
|  | @ -68,6 +67,8 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	emits: ['before', 'after'], | ||||||
|  | 
 | ||||||
| 	computed: { | 	computed: { | ||||||
| 		notes(): any[] { | 		notes(): any[] { | ||||||
| 			return this.prop ? this.items.map(item => item[this.prop]) : this.items; | 			return this.prop ? this.items.map(item => item[this.prop]) : this.items; | ||||||
|  | @ -82,9 +83,9 @@ export default Vue.extend({ | ||||||
| 		updated(oldValue, newValue) { | 		updated(oldValue, newValue) { | ||||||
| 			const i = this.notes.findIndex(n => n === oldValue); | 			const i = this.notes.findIndex(n => n === oldValue); | ||||||
| 			if (this.prop) { | 			if (this.prop) { | ||||||
| 				Vue.set(this.items[i], this.prop, newValue); | 				this.items[i][this.prop] = newValue; | ||||||
| 			} else { | 			} else { | ||||||
| 				Vue.set(this.items, i, newValue); | 				this.items[i] = newValue; | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | @ -94,4 +95,3 @@ export default Vue.extend({ | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -1,34 +1,40 @@ | ||||||
| <template> | <template> | ||||||
| <x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()"> | <XModalWindow ref="dialog" | ||||||
|  | 	:width="400" | ||||||
|  | 	:height="450" | ||||||
|  | 	:with-ok-button="true" | ||||||
|  | 	:ok-button-disabled="false" | ||||||
|  | 	@ok="ok()" | ||||||
|  | 	@close="$refs.dialog.close()" | ||||||
|  | 	@closed="$emit('closed')" | ||||||
|  | > | ||||||
| 	<template #header>{{ $t('notificationSetting') }}</template> | 	<template #header>{{ $t('notificationSetting') }}</template> | ||||||
| 	<div class="vv94n3oa"> | 	<div v-if="showGlobalToggle" class="_section"> | ||||||
| 		<div v-if="showGlobalToggle"> | 		<MkSwitch v-model:value="useGlobalSetting"> | ||||||
| 			<mk-switch v-model="useGlobalSetting"> |  | ||||||
| 			{{ $t('useGlobalSetting') }} | 			{{ $t('useGlobalSetting') }} | ||||||
| 			<template #desc>{{ $t('useGlobalSettingDesc') }}</template> | 			<template #desc>{{ $t('useGlobalSettingDesc') }}</template> | ||||||
| 			</mk-switch> | 		</MkSwitch> | ||||||
| 	</div> | 	</div> | ||||||
| 		<div v-if="!useGlobalSetting"> | 	<div v-if="!useGlobalSetting" class="_section"> | ||||||
| 			<mk-info>{{ $t('notificationSettingDesc') }}</mk-info> | 		<MkInfo>{{ $t('notificationSettingDesc') }}</MkInfo> | ||||||
| 			<mk-button inline @click="disableAll">{{ $t('disableAll') }}</mk-button> | 		<MkButton inline @click="disableAll">{{ $t('disableAll') }}</MkButton> | ||||||
| 			<mk-button inline @click="enableAll">{{ $t('enableAll') }}</mk-button> | 		<MkButton inline @click="enableAll">{{ $t('enableAll') }}</MkButton> | ||||||
| 			<mk-switch v-for="type in notificationTypes" :key="type" v-model="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</mk-switch> | 		<MkSwitch v-for="type in notificationTypes" :key="type" v-model:value="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</MkSwitch> | ||||||
| 	</div> | 	</div> | ||||||
| 	</div> | </XModalWindow> | ||||||
| </x-window> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue, { PropType } from 'vue'; | import { defineComponent, PropType } from 'vue'; | ||||||
| import XWindow from './window.vue'; | import XModalWindow from '@/components/ui/modal-window.vue'; | ||||||
| import MkSwitch from './ui/switch.vue'; | import MkSwitch from './ui/switch.vue'; | ||||||
| import MkInfo from './ui/info.vue'; | import MkInfo from './ui/info.vue'; | ||||||
| import MkButton from './ui/button.vue'; | import MkButton from './ui/button.vue'; | ||||||
| import { notificationTypes } from '../../types'; | import { notificationTypes } from '../../types'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XWindow, | 		XModalWindow, | ||||||
| 		MkSwitch, | 		MkSwitch, | ||||||
| 		MkInfo, | 		MkInfo, | ||||||
| 		MkButton | 		MkButton | ||||||
|  | @ -48,6 +54,8 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	emits: ['done', 'closed'], | ||||||
|  | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			typesMap: {} as Record<typeof notificationTypes[number], boolean>, | 			typesMap: {} as Record<typeof notificationTypes[number], boolean>, | ||||||
|  | @ -60,7 +68,7 @@ export default Vue.extend({ | ||||||
| 		this.useGlobalSetting = this.includingTypes === null && this.showGlobalToggle; | 		this.useGlobalSetting = this.includingTypes === null && this.showGlobalToggle; | ||||||
| 
 | 
 | ||||||
| 		for (const type of this.notificationTypes) { | 		for (const type of this.notificationTypes) { | ||||||
| 			Vue.set(this.typesMap, type, this.includingTypes === null || this.includingTypes.includes(type)); | 			this.typesMap[type] = this.includingTypes === null || this.includingTypes.includes(type); | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -69,8 +77,8 @@ export default Vue.extend({ | ||||||
| 			const includingTypes = this.useGlobalSetting ? null : (Object.keys(this.typesMap) as typeof notificationTypes[number][]) | 			const includingTypes = this.useGlobalSetting ? null : (Object.keys(this.typesMap) as typeof notificationTypes[number][]) | ||||||
| 				.filter(type => this.typesMap[type]); | 				.filter(type => this.typesMap[type]); | ||||||
| 
 | 
 | ||||||
| 			this.$emit('ok', { includingTypes }); | 			this.$emit('done', { includingTypes }); | ||||||
| 			this.$refs.window.close(); | 			this.$refs.dialog.close(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		disableAll() { | 		disableAll() { | ||||||
|  | @ -87,12 +95,3 @@ export default Vue.extend({ | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .vv94n3oa { |  | ||||||
| 	> div { |  | ||||||
| 		border-top: solid 1px var(--divider); |  | ||||||
| 		padding: 24px; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
|  | @ -1,71 +1,75 @@ | ||||||
| <template> | <template> | ||||||
| <div class="qglefbjs" :class="notification.type" v-size="{ max: [500, 600] }"> | <div class="qglefbjs" :class="notification.type" v-size="{ max: [500, 600] }"> | ||||||
| 	<div class="head"> | 	<div class="head"> | ||||||
| 		<mk-avatar v-if="notification.user" class="icon" :user="notification.user"/> | 		<MkAvatar v-if="notification.user" class="icon" :user="notification.user"/> | ||||||
| 		<img v-else class="icon" :src="notification.icon" alt=""/> | 		<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/> | ||||||
| 		<div class="sub-icon" :class="notification.type"> | 		<div class="sub-icon" :class="notification.type"> | ||||||
| 			<fa :icon="faPlus" v-if="notification.type === 'follow'"/> | 			<Fa :icon="faPlus" v-if="notification.type === 'follow'"/> | ||||||
| 			<fa :icon="faClock" v-else-if="notification.type === 'receiveFollowRequest'"/> | 			<Fa :icon="faClock" v-else-if="notification.type === 'receiveFollowRequest'"/> | ||||||
| 			<fa :icon="faCheck" v-else-if="notification.type === 'followRequestAccepted'"/> | 			<Fa :icon="faCheck" v-else-if="notification.type === 'followRequestAccepted'"/> | ||||||
| 			<fa :icon="faIdCardAlt" v-else-if="notification.type === 'groupInvited'"/> | 			<Fa :icon="faIdCardAlt" v-else-if="notification.type === 'groupInvited'"/> | ||||||
| 			<fa :icon="faRetweet" v-else-if="notification.type === 'renote'"/> | 			<Fa :icon="faRetweet" v-else-if="notification.type === 'renote'"/> | ||||||
| 			<fa :icon="faReply" v-else-if="notification.type === 'reply'"/> | 			<Fa :icon="faReply" v-else-if="notification.type === 'reply'"/> | ||||||
| 			<fa :icon="faAt" v-else-if="notification.type === 'mention'"/> | 			<Fa :icon="faAt" v-else-if="notification.type === 'mention'"/> | ||||||
| 			<fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/> | 			<Fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/> | ||||||
| 			<fa :icon="faPollH" v-else-if="notification.type === 'pollVote'"/> | 			<Fa :icon="faPollH" v-else-if="notification.type === 'pollVote'"/> | ||||||
| 			<x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :custom-emojis="notification.note.emojis" :no-style="true"/> | 			<XReactionIcon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :custom-emojis="notification.note.emojis" :no-style="true"/> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="tail"> | 	<div class="tail"> | ||||||
| 		<header> | 		<header> | ||||||
| 			<router-link v-if="notification.user" class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link> | 			<router-link v-if="notification.user" class="name" :to="userPage(notification.user)" v-user-preview="notification.user.id"><MkUserName :user="notification.user"/></router-link> | ||||||
| 			<span v-else>{{ notification.header }}</span> | 			<span v-else>{{ notification.header }}</span> | ||||||
| 			<mk-time :time="notification.createdAt" v-if="withTime"/> | 			<MkTime :time="notification.createdAt" v-if="withTime"/> | ||||||
| 		</header> | 		</header> | ||||||
| 		<router-link v-if="notification.type === 'reaction'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> | 		<router-link v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
| 			<fa :icon="faQuoteLeft"/> | 			<Fa :icon="faQuoteLeft"/> | ||||||
| 			<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | ||||||
| 			<fa :icon="faQuoteRight"/> | 			<Fa :icon="faQuoteRight"/> | ||||||
| 		</router-link> | 		</router-link> | ||||||
| 		<router-link v-if="notification.type === 'renote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)"> | 		<router-link v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)"> | ||||||
| 			<fa :icon="faQuoteLeft"/> | 			<Fa :icon="faQuoteLeft"/> | ||||||
| 			<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.renote.emojis"/> | ||||||
| 			<fa :icon="faQuoteRight"/> | 			<Fa :icon="faQuoteRight"/> | ||||||
| 		</router-link> | 		</router-link> | ||||||
| 		<router-link v-if="notification.type === 'reply'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> | 		<router-link v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
| 			<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | ||||||
| 		</router-link> | 		</router-link> | ||||||
| 		<router-link v-if="notification.type === 'mention'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> | 		<router-link v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
| 			<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | ||||||
| 		</router-link> | 		</router-link> | ||||||
| 		<router-link v-if="notification.type === 'quote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> | 		<router-link v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
| 			<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | ||||||
| 		</router-link> | 		</router-link> | ||||||
| 		<router-link v-if="notification.type === 'pollVote'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> | 		<router-link v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> | ||||||
| 			<fa :icon="faQuoteLeft"/> | 			<Fa :icon="faQuoteLeft"/> | ||||||
| 			<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | 			<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/> | ||||||
| 			<fa :icon="faQuoteRight"/> | 			<Fa :icon="faQuoteRight"/> | ||||||
| 		</router-link> | 		</router-link> | ||||||
| 		<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $t('youGotNewFollower') }}<div v-if="full"><mk-follow-button :user="notification.user" :full="true"/></div></span> | 		<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ $t('youGotNewFollower') }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> | ||||||
| 		<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span> | 		<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span> | ||||||
| 		<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span> | 		<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span> | ||||||
| 		<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $t('groupInvited') }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $t('reject') }}</button></div></span> | 		<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $t('groupInvited') }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $t('reject') }}</button></div></span> | ||||||
| 		<span v-if="notification.type === 'app'" class="text"> | 		<span v-if="notification.type === 'app'" class="text"> | ||||||
| 			<mfm :text="notification.body" :nowrap="!full"/> | 			<Mfm :text="notification.body" :nowrap="!full"/> | ||||||
| 		</span> | 		</span> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faIdCardAlt, faPlus, faQuoteLeft, faQuoteRight, faRetweet, faReply, faAt, faCheck, faPollH } from '@fortawesome/free-solid-svg-icons'; | import { faIdCardAlt, faPlus, faQuoteLeft, faQuoteRight, faRetweet, faReply, faAt, faCheck, faPollH } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faClock } from '@fortawesome/free-regular-svg-icons'; | import { faClock } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import noteSummary from '../../misc/get-note-summary'; | import noteSummary from '../../misc/get-note-summary'; | ||||||
| import XReactionIcon from './reaction-icon.vue'; | import XReactionIcon from './reaction-icon.vue'; | ||||||
| import MkFollowButton from './follow-button.vue'; | import MkFollowButton from './follow-button.vue'; | ||||||
|  | import notePage from '../filters/note'; | ||||||
|  | import { userPage } from '../filters/user'; | ||||||
|  | import { locale } from '../i18n'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XReactionIcon, MkFollowButton | 		XReactionIcon, MkFollowButton | ||||||
| 	}, | 	}, | ||||||
|  | @ -87,7 +91,7 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			getNoteSummary: (text: string) => noteSummary(text, this.$root.i18n.messages[this.$root.i18n.locale]), | 			getNoteSummary: (text: string) => noteSummary(text, locale), | ||||||
| 			followRequestDone: false, | 			followRequestDone: false, | ||||||
| 			groupInviteDone: false, | 			groupInviteDone: false, | ||||||
| 			connection: null, | 			connection: null, | ||||||
|  | @ -100,7 +104,7 @@ export default Vue.extend({ | ||||||
| 		if (!this.notification.isRead) { | 		if (!this.notification.isRead) { | ||||||
| 			this.readObserver = new IntersectionObserver((entries, observer) => { | 			this.readObserver = new IntersectionObserver((entries, observer) => { | ||||||
| 				if (!entries.some(entry => entry.isIntersecting)) return; | 				if (!entries.some(entry => entry.isIntersecting)) return; | ||||||
| 				this.$root.stream.send('readNotification', { | 				os.stream.send('readNotification', { | ||||||
| 					id: this.notification.id | 					id: this.notification.id | ||||||
| 				}); | 				}); | ||||||
| 				entries.map(({ target }) => observer.unobserve(target)); | 				entries.map(({ target }) => observer.unobserve(target)); | ||||||
|  | @ -108,12 +112,12 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 			this.readObserver.observe(this.$el); | 			this.readObserver.observe(this.$el); | ||||||
| 
 | 
 | ||||||
| 			this.connection = this.$root.stream.useSharedConnection('main'); | 			this.connection = os.stream.useSharedConnection('main'); | ||||||
| 			this.connection.on('readAllNotifications', () => this.readObserver.unobserve(this.$el)); | 			this.connection.on('readAllNotifications', () => this.readObserver.unobserve(this.$el)); | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		if (!this.notification.isRead) { | 		if (!this.notification.isRead) { | ||||||
| 			this.readObserver.unobserve(this.$el); | 			this.readObserver.unobserve(this.$el); | ||||||
| 			this.connection.dispose(); | 			this.connection.dispose(); | ||||||
|  | @ -123,24 +127,22 @@ export default Vue.extend({ | ||||||
| 	methods: { | 	methods: { | ||||||
| 		acceptFollowRequest() { | 		acceptFollowRequest() { | ||||||
| 			this.followRequestDone = true; | 			this.followRequestDone = true; | ||||||
| 			this.$root.api('following/requests/accept', { userId: this.notification.user.id }); | 			os.api('following/requests/accept', { userId: this.notification.user.id }); | ||||||
| 		}, | 		}, | ||||||
| 		rejectFollowRequest() { | 		rejectFollowRequest() { | ||||||
| 			this.followRequestDone = true; | 			this.followRequestDone = true; | ||||||
| 			this.$root.api('following/requests/reject', { userId: this.notification.user.id }); | 			os.api('following/requests/reject', { userId: this.notification.user.id }); | ||||||
| 		}, | 		}, | ||||||
| 		acceptGroupInvitation() { | 		acceptGroupInvitation() { | ||||||
| 			this.groupInviteDone = true; | 			this.groupInviteDone = true; | ||||||
| 			this.$root.api('users/groups/invitations/accept', { invitationId: this.notification.invitation.id }); | 			os.apiWithDialog('users/groups/invitations/accept', { invitationId: this.notification.invitation.id }); | ||||||
| 			this.$root.dialog({ |  | ||||||
| 				type: 'success', |  | ||||||
| 				iconOnly: true, autoClose: true |  | ||||||
| 			}); |  | ||||||
| 		}, | 		}, | ||||||
| 		rejectGroupInvitation() { | 		rejectGroupInvitation() { | ||||||
| 			this.groupInviteDone = true; | 			this.groupInviteDone = true; | ||||||
| 			this.$root.api('users/groups/invitations/reject', { invitationId: this.notification.invitation.id }); | 			os.api('users/groups/invitations/reject', { invitationId: this.notification.invitation.id }); | ||||||
| 		}, | 		}, | ||||||
|  | 		notePage, | ||||||
|  | 		userPage | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  | @ -153,6 +155,7 @@ export default Vue.extend({ | ||||||
| 	font-size: 0.9em; | 	font-size: 0.9em; | ||||||
| 	overflow-wrap: break-word; | 	overflow-wrap: break-word; | ||||||
| 	display: flex; | 	display: flex; | ||||||
|  | 	contain: content; | ||||||
| 
 | 
 | ||||||
| 	&.max-width_600px { | 	&.max-width_600px { | ||||||
| 		padding: 16px; | 		padding: 16px; | ||||||
|  |  | ||||||
|  | @ -1,30 +1,31 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mfcuwfyp"> | <div class="mfcuwfyp"> | ||||||
| 	<x-list class="notifications" :items="items" v-slot="{ item: notification }"> | 	<XList class="notifications" :items="items" v-slot="{ item: notification }"> | ||||||
| 		<x-note v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @updated="noteUpdated(notification.note, $event)" :key="notification.id"/> | 		<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/> | ||||||
| 		<x-notification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/> | 		<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/> | ||||||
| 	</x-list> | 	</XList> | ||||||
| 
 | 
 | ||||||
| 	<button class="_panel _button" ref="loadMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | 	<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||||
| 		<template v-if="!moreFetching">{{ $t('loadMore') }}</template> | 		<template v-if="!moreFetching">{{ $t('loadMore') }}</template> | ||||||
| 		<template v-if="moreFetching"><mk-loading inline/></template> | 		<template v-if="moreFetching"><MkLoading inline/></template> | ||||||
| 	</button> | 	</button> | ||||||
| 
 | 
 | ||||||
| 	<p class="empty" v-if="empty">{{ $t('noNotifications') }}</p> | 	<p class="empty" v-if="empty">{{ $t('noNotifications') }}</p> | ||||||
| 
 | 
 | ||||||
| 	<mk-error v-if="error" @retry="init()"/> | 	<MkError v-if="error" @retry="init()"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue, { PropType } from 'vue'; | import { defineComponent, PropType } from 'vue'; | ||||||
| import paging from '../scripts/paging'; | import paging from '@/scripts/paging'; | ||||||
| import XNotification from './notification.vue'; | import XNotification from './notification.vue'; | ||||||
| import XList from './date-separated-list.vue'; | import XList from './date-separated-list.vue'; | ||||||
| import XNote from './note.vue'; | import XNote from './note.vue'; | ||||||
| import { notificationTypes } from '../../types'; | import { notificationTypes } from '../../types'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XNotification, | 		XNotification, | ||||||
| 		XList, | 		XList, | ||||||
|  | @ -63,22 +64,30 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	watch: { | 	watch: { | ||||||
| 		includeTypes() { | 		includeTypes: { | ||||||
|  | 			handler() { | ||||||
| 				this.reload(); | 				this.reload(); | ||||||
| 			}, | 			}, | ||||||
| 		'$store.state.i.mutingNotificationTypes'() { | 			deep: true | ||||||
|  | 		}, | ||||||
|  | 		// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $store.state.i が更新されると、 | ||||||
|  | 		// mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す | ||||||
|  | 		'$store.state.i.mutingNotificationTypes': { | ||||||
|  | 			handler() { | ||||||
| 				if (this.includeTypes === null) { | 				if (this.includeTypes === null) { | ||||||
| 					this.reload(); | 					this.reload(); | ||||||
| 				} | 				} | ||||||
|  | 			}, | ||||||
|  | 			deep: true | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		this.connection = this.$root.stream.useSharedConnection('main'); | 		this.connection = os.stream.useSharedConnection('main'); | ||||||
| 		this.connection.on('notification', this.onNotification); | 		this.connection.on('notification', this.onNotification); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		this.connection.dispose(); | 		this.connection.dispose(); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -86,7 +95,7 @@ export default Vue.extend({ | ||||||
| 		onNotification(notification) { | 		onNotification(notification) { | ||||||
| 			const isMuted = !this.allIncludeTypes.includes(notification.type); | 			const isMuted = !this.allIncludeTypes.includes(notification.type); | ||||||
| 			if (isMuted || document.visibilityState === 'visible') { | 			if (isMuted || document.visibilityState === 'visible') { | ||||||
| 				this.$root.stream.send('readNotification', { | 				os.stream.send('readNotification', { | ||||||
| 					id: notification.id | 					id: notification.id | ||||||
| 				}); | 				}); | ||||||
| 			} | 			} | ||||||
|  | @ -101,10 +110,10 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 		noteUpdated(oldValue, newValue) { | 		noteUpdated(oldValue, newValue) { | ||||||
| 			const i = this.items.findIndex(n => n.note === oldValue); | 			const i = this.items.findIndex(n => n.note === oldValue); | ||||||
| 			Vue.set(this.items, i, { | 			this.items[i] = { | ||||||
| 				...this.items[i], | 				...this.items[i], | ||||||
| 				note: newValue | 				note: newValue | ||||||
| 			}); | 			}; | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -8,22 +8,27 @@ | ||||||
| 		<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p> | 		<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p> | ||||||
| 		<footer> | 		<footer> | ||||||
| 			<img class="icon" :src="page.user.avatarUrl"/> | 			<img class="icon" :src="page.user.avatarUrl"/> | ||||||
| 			<p>{{ page.user | userName }}</p> | 			<p>{{ userName(page.user) }}</p> | ||||||
| 		</footer> | 		</footer> | ||||||
| 	</article> | 	</article> | ||||||
| </router-link> | </router-link> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
|  | import { userName } from '../filters/user'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		page: { | 		page: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
| 			required: true | 			required: true | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
|  | 	methods: { | ||||||
|  | 		userName | ||||||
|  | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										86
									
								
								src/client/components/page-window.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/client/components/page-window.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | ||||||
|  | <template> | ||||||
|  | <XWindow ref="window" :initial-width="400" :initial-height="450" :can-resize="true" @closed="$emit('closed')"> | ||||||
|  | 	<template #header> | ||||||
|  | 		<XHeader :info="pageInfo" :with-back="false"/> | ||||||
|  | 	</template> | ||||||
|  | 	<template #buttons> | ||||||
|  | 		<button class="_button" @click="expand" v-tooltip="$t('showInPage')"><Fa :icon="faExpandAlt"/></button> | ||||||
|  | 		<button class="_button" @click="popout" v-tooltip="$t('popout')"><Fa :icon="faExternalLinkAlt"/></button> | ||||||
|  | 	</template> | ||||||
|  | 	<div style="min-height: 100%; background: var(--bg);"> | ||||||
|  | 		<component :is="component" v-bind="props" :ref="changePage"/> | ||||||
|  | 	</div> | ||||||
|  | </XWindow> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, markRaw } from 'vue'; | ||||||
|  | import { faExternalLinkAlt, faExpandAlt } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import XWindow from '@/components/ui/window.vue'; | ||||||
|  | import XHeader from '@/ui/_common_/header.vue'; | ||||||
|  | import { popout } from '@/scripts/popout'; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		XWindow, | ||||||
|  | 		XHeader, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	props: { | ||||||
|  | 		initialUrl: { | ||||||
|  | 			type: String, | ||||||
|  | 			required: true, | ||||||
|  | 		}, | ||||||
|  | 		initialComponent: { | ||||||
|  | 			type: Object, | ||||||
|  | 			required: true, | ||||||
|  | 		}, | ||||||
|  | 		initialProps: { | ||||||
|  | 			type: Object, | ||||||
|  | 			required: false, | ||||||
|  | 			default: {}, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	emits: ['closed'], | ||||||
|  | 
 | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			pageInfo: null, | ||||||
|  | 			url: this.initialUrl, | ||||||
|  | 			component: this.initialComponent, | ||||||
|  | 			props: this.initialProps, | ||||||
|  | 			faExternalLinkAlt, faExpandAlt, | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	provide() { | ||||||
|  | 		return { | ||||||
|  | 			navHook: (url, component, props) => { | ||||||
|  | 				this.url = url; | ||||||
|  | 				this.component = markRaw(component); | ||||||
|  | 				this.props = props; | ||||||
|  | 			} | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		changePage(page) { | ||||||
|  | 			if (page == null) return; | ||||||
|  | 			if (page.INFO) { | ||||||
|  | 				this.pageInfo = page.INFO; | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		expand() { | ||||||
|  | 			this.$router.push(this.url); | ||||||
|  | 			this.$refs.window.close(); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		popout() { | ||||||
|  | 			popout(this.url, this.$el); | ||||||
|  | 			this.$refs.window.close(); | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import XText from './page.text.vue'; | import XText from './page.text.vue'; | ||||||
| import XSection from './page.section.vue'; | import XSection from './page.section.vue'; | ||||||
| import XImage from './page.image.vue'; | import XImage from './page.image.vue'; | ||||||
|  | @ -19,7 +19,7 @@ import XCounter from './page.counter.vue'; | ||||||
| import XRadioButton from './page.radio-button.vue'; | import XRadioButton from './page.radio-button.vue'; | ||||||
| import XCanvas from './page.canvas.vue'; | import XCanvas from './page.canvas.vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas | 		XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<mk-button class="kudkigyw" @click="click()" :primary="value.primary">{{ hpml.interpolate(value.text) }}</mk-button> | 	<MkButton class="kudkigyw" @click="click()" :primary="value.primary">{{ hpml.interpolate(value.text) }}</MkButton> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkButton from '../ui/button.vue'; | import MkButton from '../ui/button.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkButton | 		MkButton | ||||||
| 	}, | 	}, | ||||||
|  | @ -24,14 +25,14 @@ export default Vue.extend({ | ||||||
| 		click() { | 		click() { | ||||||
| 			if (this.value.action === 'dialog') { | 			if (this.value.action === 'dialog') { | ||||||
| 				this.hpml.eval(); | 				this.hpml.eval(); | ||||||
| 				this.$root.dialog({ | 				os.dialog({ | ||||||
| 					text: this.hpml.interpolate(this.value.content) | 					text: this.hpml.interpolate(this.value.content) | ||||||
| 				}); | 				}); | ||||||
| 			} else if (this.value.action === 'resetRandom') { | 			} else if (this.value.action === 'resetRandom') { | ||||||
| 				this.hpml.updateRandomSeed(Math.random()); | 				this.hpml.updateRandomSeed(Math.random()); | ||||||
| 				this.hpml.eval(); | 				this.hpml.eval(); | ||||||
| 			} else if (this.value.action === 'pushEvent') { | 			} else if (this.value.action === 'pushEvent') { | ||||||
| 				this.$root.api('page-push', { | 				os.api('page-push', { | ||||||
| 					pageId: this.hpml.page.id, | 					pageId: this.hpml.page.id, | ||||||
| 					event: this.value.event, | 					event: this.value.event, | ||||||
| 					...(this.value.var ? { | 					...(this.value.var ? { | ||||||
|  | @ -39,7 +40,7 @@ export default Vue.extend({ | ||||||
| 					} : {}) | 					} : {}) | ||||||
| 				}); | 				}); | ||||||
| 
 | 
 | ||||||
| 				this.$root.dialog({ | 				os.dialog({ | ||||||
| 					type: 'success', | 					type: 'success', | ||||||
| 					text: this.hpml.interpolate(this.value.message) | 					text: this.hpml.interpolate(this.value.message) | ||||||
| 				}); | 				}); | ||||||
|  |  | ||||||
|  | @ -5,9 +5,10 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		value: { | 		value: { | ||||||
| 			required: true | 			required: true | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<mk-button class="llumlmnx" @click="click()">{{ hpml.interpolate(value.text) }}</mk-button> | 	<MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(value.text) }}</MkButton> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkButton from '../ui/button.vue'; | import MkButton from '../ui/button.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkButton | 		MkButton | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,13 +1,14 @@ | ||||||
| <template> | <template> | ||||||
| <div v-show="hpml.vars[value.var]"> | <div v-show="hpml.vars[value.var]"> | ||||||
| 	<x-block v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h"/> | 	<XBlock v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		value: { | 		value: { | ||||||
| 			required: true | 			required: true | ||||||
|  |  | ||||||
|  | @ -5,9 +5,10 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		value: { | 		value: { | ||||||
| 			required: true | 			required: true | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<mk-input class="kudkigyw" v-model="v" type="number">{{ hpml.interpolate(value.text) }}</mk-input> | 	<MkInput class="kudkigyw" v-model:value="v" type="number">{{ hpml.interpolate(value.text) }}</MkInput> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkInput from '../ui/input.vue'; | import MkInput from '../ui/input.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkInput | 		MkInput | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,18 +1,19 @@ | ||||||
| <template> | <template> | ||||||
| <div class="ngbfujlo"> | <div class="ngbfujlo"> | ||||||
| 	<mk-textarea :value="text" readonly style="margin: 0;"></mk-textarea> | 	<MkTextarea :value="text" readonly style="margin: 0;"></MkTextarea> | ||||||
| 	<mk-button class="button" primary @click="post()" :disabled="posting || posted"><fa v-if="posted" :icon="faCheck"/><fa v-else :icon="faPaperPlane"/></mk-button> | 	<MkButton class="button" primary @click="post()" :disabled="posting || posted"><Fa v-if="posted" :icon="faCheck"/><Fa v-else :icon="faPaperPlane"/></MkButton> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faCheck, faPaperPlane } from '@fortawesome/free-solid-svg-icons'; | import { faCheck, faPaperPlane } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import MkTextarea from '../ui/textarea.vue'; | import MkTextarea from '../ui/textarea.vue'; | ||||||
| import MkButton from '../ui/button.vue'; | import MkButton from '../ui/button.vue'; | ||||||
| import { apiUrl } from '../../config'; | import { apiUrl } from '@/config'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkTextarea, | 		MkTextarea, | ||||||
| 		MkButton, | 		MkButton, | ||||||
|  | @ -44,7 +45,7 @@ export default Vue.extend({ | ||||||
| 	methods: { | 	methods: { | ||||||
| 		upload() { | 		upload() { | ||||||
| 			return new Promise((ok) => { | 			return new Promise((ok) => { | ||||||
| 				const dialog = this.$root.dialog({ | 				const dialog = os.dialog({ | ||||||
| 					type: 'waiting', | 					type: 'waiting', | ||||||
| 					text: this.$t('uploading') + '...', | 					text: this.$t('uploading') + '...', | ||||||
| 					showOkButton: false, | 					showOkButton: false, | ||||||
|  | @ -75,15 +76,11 @@ export default Vue.extend({ | ||||||
| 		async post() { | 		async post() { | ||||||
| 			this.posting = true; | 			this.posting = true; | ||||||
| 			const file = this.value.attachCanvasImage ? await this.upload() : null; | 			const file = this.value.attachCanvasImage ? await this.upload() : null; | ||||||
| 			this.$root.api('notes/create', { | 			os.apiWithDialog('notes/create', { | ||||||
| 				text: this.text === '' ? null : this.text, | 				text: this.text === '' ? null : this.text, | ||||||
| 				fileIds: file ? [file.id] : undefined, | 				fileIds: file ? [file.id] : undefined, | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
| 				this.posted = true; | 				this.posted = true; | ||||||
| 				this.$root.dialog({ |  | ||||||
| 					type: 'success', |  | ||||||
| 					iconOnly: true, autoClose: true |  | ||||||
| 				}); |  | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1,15 +1,16 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<div>{{ hpml.interpolate(value.title) }}</div> | 	<div>{{ hpml.interpolate(value.title) }}</div> | ||||||
| 	<mk-radio v-for="x in value.values" v-model="v" :value="x" :key="x">{{ x }}</mk-radio> | 	<MkRadio v-for="x in value.values" v-model:value="v" :value="x" :key="x">{{ x }}</MkRadio> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkRadio from '../ui/radio.vue'; | import MkRadio from '../ui/radio.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkRadio | 		MkRadio | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -3,15 +3,16 @@ | ||||||
| 	<component :is="'h' + h">{{ value.title }}</component> | 	<component :is="'h' + h">{{ value.title }}</component> | ||||||
| 
 | 
 | ||||||
| 	<div class="children"> | 	<div class="children"> | ||||||
| 		<x-block v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h + 1"/> | 		<XBlock v-for="child in value.children" :value="child" :page="page" :hpml="hpml" :key="child.id" :h="h + 1"/> | ||||||
| 	</div> | 	</div> | ||||||
| </section> | </section> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		value: { | 		value: { | ||||||
| 			required: true | 			required: true | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| <template> | <template> | ||||||
| <div class="hkcxmtwj"> | <div class="hkcxmtwj"> | ||||||
| 	<mk-switch v-model="v">{{ hpml.interpolate(value.text) }}</mk-switch> | 	<MkSwitch v-model:value="v">{{ hpml.interpolate(value.text) }}</MkSwitch> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkSwitch from '../ui/switch.vue'; | import MkSwitch from '../ui/switch.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkSwitch | 		MkSwitch | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<mk-input class="kudkigyw" v-model="v" type="text">{{ hpml.interpolate(value.text) }}</mk-input> | 	<MkInput class="kudkigyw" v-model:value="v" type="text">{{ hpml.interpolate(value.text) }}</MkInput> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkInput from '../ui/input.vue'; | import MkInput from '../ui/input.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkInput | 		MkInput | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,16 +1,19 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mrdgzndn"> | <div class="mrdgzndn"> | ||||||
| 	<mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/> | 	<Mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/> | ||||||
| 	<mk-url-preview v-for="url in urls" :url="url" :key="url" class="url"/> | 	<MkUrlPreview v-for="url in urls" :url="url" :key="url" class="url"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineAsyncComponent, defineComponent } from 'vue'; | ||||||
| import { parse } from '../../../mfm/parse'; | import { parse } from '../../../mfm/parse'; | ||||||
| import { unique } from '../../../prelude/array'; | import { unique } from '../../../prelude/array'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), | ||||||
|  | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		value: { | 		value: { | ||||||
| 			required: true | 			required: true | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<mk-textarea v-model="v">{{ hpml.interpolate(value.text) }}</mk-textarea> | 	<MkTextarea v-model:value="v">{{ hpml.interpolate(value.text) }}</MkTextarea> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkTextarea from '../ui/textarea.vue'; | import MkTextarea from '../ui/textarea.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkTextarea | 		MkTextarea | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,12 +1,13 @@ | ||||||
| <template> | <template> | ||||||
| <mk-textarea :value="text" readonly></mk-textarea> | <MkTextarea :value="text" readonly></MkTextarea> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkTextarea from '../ui/textarea.vue'; | import MkTextarea from '../ui/textarea.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkTextarea | 		MkTextarea | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,19 +1,20 @@ | ||||||
| <template> | <template> | ||||||
| <div class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }" v-if="hpml"> | <div class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }" v-if="hpml"> | ||||||
| 	<x-block v-for="child in page.content" :value="child" @input="v => updateBlock(v)" :page="page" :hpml="hpml" :key="child.id" :h="2"/> | 	<XBlock v-for="child in page.content" :value="child" @update:value="v => updateBlock(v)" :page="page" :hpml="hpml" :key="child.id" :h="2"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { parse } from '@syuilo/aiscript'; | import { parse } from '@syuilo/aiscript'; | ||||||
| import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons'; | import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faHeart } from '@fortawesome/free-regular-svg-icons'; | import { faHeart } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import XBlock from './page.block.vue'; | import XBlock from './page.block.vue'; | ||||||
| import { Hpml } from '../../scripts/hpml/evaluator'; | import { Hpml } from '@/scripts/hpml/evaluator'; | ||||||
| import { url } from '../../config'; | import { url } from '@/config'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XBlock | 		XBlock | ||||||
| 	}, | 	}, | ||||||
|  | @ -33,7 +34,7 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	created() { | 	created() { | ||||||
| 		this.hpml = new Hpml(this, this.page, { | 		this.hpml = new Hpml(this.page, { | ||||||
| 			randomSeed: Math.random(), | 			randomSeed: Math.random(), | ||||||
| 			visitor: this.$store.state.i, | 			visitor: this.$store.state.i, | ||||||
| 			url: url, | 			url: url, | ||||||
|  | @ -49,7 +50,7 @@ export default Vue.extend({ | ||||||
| 					ast = parse(this.page.script); | 					ast = parse(this.page.script); | ||||||
| 				} catch (e) { | 				} catch (e) { | ||||||
| 					console.error(e); | 					console.error(e); | ||||||
| 					/*this.$root.dialog({ | 					/*os.dialog({ | ||||||
| 						type: 'error', | 						type: 'error', | ||||||
| 						text: 'Syntax error :(' | 						text: 'Syntax error :(' | ||||||
| 					});*/ | 					});*/ | ||||||
|  | @ -59,7 +60,7 @@ export default Vue.extend({ | ||||||
| 					this.hpml.eval(); | 					this.hpml.eval(); | ||||||
| 				}).catch(e => { | 				}).catch(e => { | ||||||
| 					console.error(e); | 					console.error(e); | ||||||
| 					/*this.$root.dialog({ | 					/*os.dialog({ | ||||||
| 						type: 'error', | 						type: 'error', | ||||||
| 						text: e | 						text: e | ||||||
| 					});*/ | 					});*/ | ||||||
|  | @ -70,7 +71,7 @@ export default Vue.extend({ | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeDestroy() { | 	beforeUnmount() { | ||||||
| 		if (this.hpml.aiscript) this.hpml.aiscript.abort(); | 		if (this.hpml.aiscript) this.hpml.aiscript.abort(); | ||||||
| 	}, | 	}, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -8,14 +8,16 @@ | ||||||
| 				calcMode="spline" | 				calcMode="spline" | ||||||
| 				keyTimes="0; 1" | 				keyTimes="0; 1" | ||||||
| 				keySplines="0.165, 0.84, 0.44, 1" | 				keySplines="0.165, 0.84, 0.44, 1" | ||||||
| 					repeatCount="1" /> | 				repeatCount="1" | ||||||
|  | 			/> | ||||||
| 			<animate attributeName="stroke-width" | 			<animate attributeName="stroke-width" | ||||||
| 				begin="0s" dur="0.5s" | 				begin="0s" dur="0.5s" | ||||||
| 				values="16; 0" | 				values="16; 0" | ||||||
| 				calcMode="spline" | 				calcMode="spline" | ||||||
| 				keyTimes="0; 1" | 				keyTimes="0; 1" | ||||||
| 				keySplines="0.3, 0.61, 0.355, 1" | 				keySplines="0.3, 0.61, 0.355, 1" | ||||||
| 					repeatCount="1" /> | 				repeatCount="1" | ||||||
|  | 			/> | ||||||
| 		</circle> | 		</circle> | ||||||
| 		<g fill="none" fill-rule="evenodd"> | 		<g fill="none" fill-rule="evenodd"> | ||||||
| 			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color"> | 			<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color"> | ||||||
|  | @ -25,21 +27,24 @@ | ||||||
| 					calcMode="spline" | 					calcMode="spline" | ||||||
| 					keyTimes="0; 1" | 					keyTimes="0; 1" | ||||||
| 					keySplines="0.165, 0.84, 0.44, 1" | 					keySplines="0.165, 0.84, 0.44, 1" | ||||||
| 						repeatCount="1" /> | 					repeatCount="1" | ||||||
|  | 				/> | ||||||
| 				<animate attributeName="cx" | 				<animate attributeName="cx" | ||||||
| 					begin="0s" dur="0.8s" | 					begin="0s" dur="0.8s" | ||||||
| 					:values="`${particle.xA}; ${particle.xB}`" | 					:values="`${particle.xA}; ${particle.xB}`" | ||||||
| 					calcMode="spline" | 					calcMode="spline" | ||||||
| 					keyTimes="0; 1" | 					keyTimes="0; 1" | ||||||
| 					keySplines="0.3, 0.61, 0.355, 1" | 					keySplines="0.3, 0.61, 0.355, 1" | ||||||
| 						repeatCount="1" /> | 					repeatCount="1" | ||||||
|  | 				/> | ||||||
| 				<animate attributeName="cy" | 				<animate attributeName="cy" | ||||||
| 					begin="0s" dur="0.8s" | 					begin="0s" dur="0.8s" | ||||||
| 					:values="`${particle.yA}; ${particle.yB}`" | 					:values="`${particle.yA}; ${particle.yB}`" | ||||||
| 					calcMode="spline" | 					calcMode="spline" | ||||||
| 					keyTimes="0; 1" | 					keyTimes="0; 1" | ||||||
| 					keySplines="0.3, 0.61, 0.355, 1" | 					keySplines="0.3, 0.61, 0.355, 1" | ||||||
| 						repeatCount="1" /> | 					repeatCount="1" | ||||||
|  | 				/> | ||||||
| 			</circle> | 			</circle> | ||||||
| 		</g> | 		</g> | ||||||
| 	</svg> | 	</svg> | ||||||
|  | @ -47,9 +52,9 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		x: { | 		x: { | ||||||
| 			type: Number, | 			type: Number, | ||||||
|  | @ -60,6 +65,7 @@ export default Vue.extend({ | ||||||
| 			required: true | 			required: true | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  | 	emits: ['end'], | ||||||
| 	data() { | 	data() { | ||||||
| 		const particles = []; | 		const particles = []; | ||||||
| 		const origin = 64; | 		const origin = 64; | ||||||
|  | @ -85,7 +91,7 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		setTimeout(() => { | 		setTimeout(() => { | ||||||
| 			this.destroyDom(); | 			this.$emit('end'); | ||||||
| 		}, 1100); | 		}, 1100); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,47 +1,47 @@ | ||||||
| <template> | <template> | ||||||
| <div class="zmdxowus"> | <div class="zmdxowus"> | ||||||
| 	<p class="caution" v-if="choices.length < 2"> | 	<p class="caution" v-if="choices.length < 2"> | ||||||
| 		<fa :icon="faExclamationTriangle"/>{{ $t('_poll.noOnlyOneChoice') }} | 		<Fa :icon="faExclamationTriangle"/>{{ $t('_poll.noOnlyOneChoice') }} | ||||||
| 	</p> | 	</p> | ||||||
| 	<ul ref="choices"> | 	<ul ref="choices"> | ||||||
| 		<li v-for="(choice, i) in choices" :key="i"> | 		<li v-for="(choice, i) in choices" :key="i"> | ||||||
| 			<mk-input class="input" :value="choice" @input="onInput(i, $event)"> | 			<MkInput class="input" :value="choice" @update:value="onInput(i, $event)"> | ||||||
| 				<span>{{ $t('_poll.choiceN', { n: i + 1 }) }}</span> | 				<span>{{ $t('_poll.choiceN', { n: i + 1 }) }}</span> | ||||||
| 			</mk-input> | 			</MkInput> | ||||||
| 			<button @click="remove(i)" class="_button"> | 			<button @click="remove(i)" class="_button"> | ||||||
| 				<fa :icon="faTimes"/> | 				<Fa :icon="faTimes"/> | ||||||
| 			</button> | 			</button> | ||||||
| 		</li> | 		</li> | ||||||
| 	</ul> | 	</ul> | ||||||
| 	<mk-button class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</mk-button> | 	<MkButton class="add" v-if="choices.length < 10" @click="add">{{ $t('add') }}</MkButton> | ||||||
| 	<mk-button class="add" v-else disabled>{{ $t('_poll.noMore') }}</mk-button> | 	<MkButton class="add" v-else disabled>{{ $t('_poll.noMore') }}</MkButton> | ||||||
| 	<section> | 	<section> | ||||||
| 		<mk-switch v-model="multiple">{{ $t('_poll.canMultipleVote') }}</mk-switch> | 		<MkSwitch v-model:value="multiple">{{ $t('_poll.canMultipleVote') }}</MkSwitch> | ||||||
| 		<div> | 		<div> | ||||||
| 			<mk-select v-model="expiration"> | 			<MkSelect v-model:value="expiration"> | ||||||
| 				<template #label>{{ $t('_poll.expiration') }}</template> | 				<template #label>{{ $t('_poll.expiration') }}</template> | ||||||
| 				<option value="infinite">{{ $t('_poll.infinite') }}</option> | 				<option value="infinite">{{ $t('_poll.infinite') }}</option> | ||||||
| 				<option value="at">{{ $t('_poll.at') }}</option> | 				<option value="at">{{ $t('_poll.at') }}</option> | ||||||
| 				<option value="after">{{ $t('_poll.after') }}</option> | 				<option value="after">{{ $t('_poll.after') }}</option> | ||||||
| 			</mk-select> | 			</MkSelect> | ||||||
| 			<section v-if="expiration === 'at'"> | 			<section v-if="expiration === 'at'"> | ||||||
| 				<mk-input v-model="atDate" type="date" class="input"> | 				<MkInput v-model:value="atDate" type="date" class="input"> | ||||||
| 					<span>{{ $t('_poll.deadlineDate') }}</span> | 					<span>{{ $t('_poll.deadlineDate') }}</span> | ||||||
| 				</mk-input> | 				</MkInput> | ||||||
| 				<mk-input v-model="atTime" type="time" class="input"> | 				<MkInput v-model:value="atTime" type="time" class="input"> | ||||||
| 					<span>{{ $t('_poll.deadlineTime') }}</span> | 					<span>{{ $t('_poll.deadlineTime') }}</span> | ||||||
| 				</mk-input> | 				</MkInput> | ||||||
| 			</section> | 			</section> | ||||||
| 			<section v-if="expiration === 'after'"> | 			<section v-if="expiration === 'after'"> | ||||||
| 				<mk-input v-model="after" type="number" class="input"> | 				<MkInput v-model:value="after" type="number" class="input"> | ||||||
| 					<span>{{ $t('_poll.duration') }}</span> | 					<span>{{ $t('_poll.duration') }}</span> | ||||||
| 				</mk-input> | 				</MkInput> | ||||||
| 				<mk-select v-model="unit"> | 				<MkSelect v-model:value="unit"> | ||||||
| 					<option value="second">{{ $t('_time.second') }}</option> | 					<option value="second">{{ $t('_time.second') }}</option> | ||||||
| 					<option value="minute">{{ $t('_time.minute') }}</option> | 					<option value="minute">{{ $t('_time.minute') }}</option> | ||||||
| 					<option value="hour">{{ $t('_time.hour') }}</option> | 					<option value="hour">{{ $t('_time.hour') }}</option> | ||||||
| 					<option value="day">{{ $t('_time.day') }}</option> | 					<option value="day">{{ $t('_time.day') }}</option> | ||||||
| 				</mk-select> | 				</MkSelect> | ||||||
| 			</section> | 			</section> | ||||||
| 		</div> | 		</div> | ||||||
| 	</section> | 	</section> | ||||||
|  | @ -49,23 +49,33 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faExclamationTriangle, faTimes } from '@fortawesome/free-solid-svg-icons'; | import { faExclamationTriangle, faTimes } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { erase } from '../../prelude/array'; |  | ||||||
| import { addTime } from '../../prelude/time'; | import { addTime } from '../../prelude/time'; | ||||||
| import { formatDateTimeString } from '../../misc/format-time-string'; | import { formatDateTimeString } from '../../misc/format-time-string'; | ||||||
| import MkInput from './ui/input.vue'; | import MkInput from './ui/input.vue'; | ||||||
| import MkSelect from './ui/select.vue'; | import MkSelect from './ui/select.vue'; | ||||||
| import MkSwitch from './ui/switch.vue'; | import MkSwitch from './ui/switch.vue'; | ||||||
| import MkButton from './ui/button.vue'; | import MkButton from './ui/button.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkInput, | 		MkInput, | ||||||
| 		MkSelect, | 		MkSelect, | ||||||
| 		MkSwitch, | 		MkSwitch, | ||||||
| 		MkButton, | 		MkButton, | ||||||
| 	}, | 	}, | ||||||
|  | 
 | ||||||
|  | 	props: { | ||||||
|  | 		poll: { | ||||||
|  | 			type: Object, | ||||||
|  | 			required: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	emits: ['updated'], | ||||||
|  | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			choices: ['', ''], | 			choices: ['', ''], | ||||||
|  | @ -78,20 +88,66 @@ export default Vue.extend({ | ||||||
| 			faExclamationTriangle, faTimes | 			faExclamationTriangle, faTimes | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
|  | 
 | ||||||
| 	watch: { | 	watch: { | ||||||
| 		choices() { | 		poll: { | ||||||
| 			this.$emit('updated'); | 			handler(poll) { | ||||||
|  | 				if (poll == null) return; | ||||||
|  | 				if (poll.choices.length == 0) return; | ||||||
|  | 				this.choices = poll.choices; | ||||||
|  | 				if (poll.choices.length == 1) this.choices = this.choices.concat(''); | ||||||
|  | 				this.multiple = poll.multiple; | ||||||
|  | 				if (poll.expiresAt) { | ||||||
|  | 					this.expiration = 'at'; | ||||||
|  | 					this.atDate = this.atTime = poll.expiresAt; | ||||||
|  | 				} else if (typeof poll.expiredAfter === 'number') { | ||||||
|  | 					this.expiration = 'after'; | ||||||
|  | 					this.after = poll.expiredAfter; | ||||||
|  | 				} else { | ||||||
|  | 					this.expiration = 'infinite'; | ||||||
| 				} | 				} | ||||||
| 			}, | 			}, | ||||||
|  | 			deep: true, | ||||||
|  | 			immediate: true | ||||||
|  | 		}, | ||||||
|  | 		choices: { | ||||||
|  | 			handler() { | ||||||
|  | 				this.$emit('updated', this.get()); | ||||||
|  | 			}, | ||||||
|  | 			deep: true | ||||||
|  | 		}, | ||||||
|  | 		multiple: { | ||||||
|  | 			handler() { | ||||||
|  | 				this.$emit('updated', this.get()); | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		expiration: { | ||||||
|  | 			handler() { | ||||||
|  | 				this.$emit('updated', this.get()); | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		atDate: { | ||||||
|  | 			handler() { | ||||||
|  | 				this.$emit('updated', this.get()); | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		after: { | ||||||
|  | 			handler() { | ||||||
|  | 				this.$emit('updated', this.get()); | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		onInput(i, e) { | 		onInput(i, e) { | ||||||
| 			Vue.set(this.choices, i, e); | 			this.choices[i] = e; | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		add() { | 		add() { | ||||||
| 			this.choices.push(''); | 			this.choices.push(''); | ||||||
| 			this.$nextTick(() => { | 			this.$nextTick(() => { | ||||||
| 				(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus(); | 				// TODO | ||||||
|  | 				//(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus(); | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | @ -116,29 +172,14 @@ export default Vue.extend({ | ||||||
| 			}; | 			}; | ||||||
| 
 | 
 | ||||||
| 			return { | 			return { | ||||||
| 				choices: erase('', this.choices), | 				choices: this.choices, | ||||||
| 				multiple: this.multiple, | 				multiple: this.multiple, | ||||||
| 				...( | 				...( | ||||||
| 					this.expiration === 'at' ? { expiresAt: at() } : | 					this.expiration === 'at' ? { expiresAt: at() } : | ||||||
| 					this.expiration === 'after' ? { expiredAfter: after() } : {}) | 					this.expiration === 'after' ? { expiredAfter: after() } : {} | ||||||
|  | 				) | ||||||
| 			}; | 			}; | ||||||
| 		}, | 		}, | ||||||
| 
 |  | ||||||
| 		set(data) { |  | ||||||
| 			if (data.choices.length == 0) return; |  | ||||||
| 			this.choices = data.choices; |  | ||||||
| 			if (data.choices.length == 1) this.choices = this.choices.concat(''); |  | ||||||
| 			this.multiple = data.multiple; |  | ||||||
| 			if (data.expiresAt) { |  | ||||||
| 				this.expiration = 'at'; |  | ||||||
| 				this.atDate = this.atTime = data.expiresAt; |  | ||||||
| 			} else if (typeof data.expiredAfter === 'number') { |  | ||||||
| 				this.expiration = 'after'; |  | ||||||
| 				this.after = data.expiredAfter; |  | ||||||
| 			} else { |  | ||||||
| 				this.expiration = 'infinite'; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| <template> | <template> | ||||||
| <div class="tivcixzd" :data-done="closed || isVoted"> | <div class="tivcixzd" :class="{ done: closed || isVoted }"> | ||||||
| 	<ul> | 	<ul> | ||||||
| 		<li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }"> | 		<li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }"> | ||||||
| 			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> | 			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> | ||||||
| 			<span> | 			<span> | ||||||
| 				<template v-if="choice.isVoted"><fa :icon="faCheck"/></template> | 				<template v-if="choice.isVoted"><Fa :icon="faCheck"/></template> | ||||||
| 				<mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/> | 				<Mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/> | ||||||
| 				<span class="votes" v-if="showResult">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span> | 				<span class="votes" v-if="showResult">({{ $t('_poll.votesCount', { n: choice.votes }) }})</span> | ||||||
| 			</span> | 			</span> | ||||||
| 		</li> | 		</li> | ||||||
|  | @ -22,11 +22,12 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faCheck } from '@fortawesome/free-solid-svg-icons'; | import { faCheck } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { sum } from '../../prelude/array'; | import { sum } from '../../prelude/array'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		note: { | 		note: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -85,7 +86,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 		vote(id) { | 		vote(id) { | ||||||
| 			if (this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return; | 			if (this.closed || !this.poll.multiple && this.poll.choices.some(c => c.isVoted)) return; | ||||||
| 			this.$root.api('notes/polls/vote', { | 			os.api('notes/polls/vote', { | ||||||
| 				noteId: this.note.id, | 				noteId: this.note.id, | ||||||
| 				choice: id | 				choice: id | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
|  | @ -153,7 +154,7 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	&[data-done] { | 	&.done { | ||||||
| 		> ul > li { | 		> ul > li { | ||||||
| 			cursor: default; | 			cursor: default; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,148 +0,0 @@ | ||||||
| <template> |  | ||||||
| <div class="mk-popup" v-hotkey.global="keymap"> |  | ||||||
| 	<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear> |  | ||||||
| 		<div class="bg _modalBg" ref="bg" @click="close()" v-if="show"></div> |  | ||||||
| 	</transition> |  | ||||||
| 	<transition :name="$store.state.device.animation ? 'popup' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }"> |  | ||||||
| 		<div class="content" :class="{ fixed }" ref="content" v-if="show" :style="{ width: width ? width + 'px' : 'auto' }"><slot></slot></div> |  | ||||||
| 	</transition> |  | ||||||
| </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
| 
 |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	props: { |  | ||||||
| 		source: { |  | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 		noCenter: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		fixed: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		width: { |  | ||||||
| 			type: Number, |  | ||||||
| 			required: false |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			show: true, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	computed: { |  | ||||||
| 		keymap(): any { |  | ||||||
| 			return { |  | ||||||
| 				'esc': this.close, |  | ||||||
| 			}; |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	mounted() { |  | ||||||
| 		this.$nextTick(() => { |  | ||||||
| 			const popover = this.$refs.content as any; |  | ||||||
| 
 |  | ||||||
| 			const rect = this.source.getBoundingClientRect(); |  | ||||||
| 			const width = popover.offsetWidth; |  | ||||||
| 			const height = popover.offsetHeight; |  | ||||||
| 
 |  | ||||||
| 			let left; |  | ||||||
| 			let top; |  | ||||||
| 
 |  | ||||||
| 			if (this.$root.isMobile && !this.noCenter) { |  | ||||||
| 				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2); |  | ||||||
| 				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + (this.source.offsetHeight / 2); |  | ||||||
| 				left = (x - (width / 2)); |  | ||||||
| 				top = (y - (height / 2)); |  | ||||||
| 				popover.style.transformOrigin = 'center'; |  | ||||||
| 			} else { |  | ||||||
| 				const x = rect.left + (this.fixed ? 0 : window.pageXOffset) + (this.source.offsetWidth / 2); |  | ||||||
| 				const y = rect.top + (this.fixed ? 0 : window.pageYOffset) + this.source.offsetHeight; |  | ||||||
| 				left = (x - (width / 2)); |  | ||||||
| 				top = y; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if (this.fixed) { |  | ||||||
| 				if (left + width > window.innerWidth) { |  | ||||||
| 					left = window.innerWidth - width; |  | ||||||
| 					popover.style.transformOrigin = 'center'; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				if (top + height > window.innerHeight) { |  | ||||||
| 					top = window.innerHeight - height; |  | ||||||
| 					popover.style.transformOrigin = 'center'; |  | ||||||
| 				} |  | ||||||
| 			} else { |  | ||||||
| 				if (left + width - window.pageXOffset > window.innerWidth) { |  | ||||||
| 					left = window.innerWidth - width + window.pageXOffset; |  | ||||||
| 					popover.style.transformOrigin = 'center'; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				if (top + height - window.pageYOffset > window.innerHeight) { |  | ||||||
| 					top = window.innerHeight - height + window.pageYOffset; |  | ||||||
| 					popover.style.transformOrigin = 'center'; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if (top < 0) { |  | ||||||
| 				top = 0; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			if (left < 0) { |  | ||||||
| 				left = 0; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			popover.style.left = left + 'px'; |  | ||||||
| 			popover.style.top = top + 'px'; |  | ||||||
| 		}); |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		close() { |  | ||||||
| 			this.show = false; |  | ||||||
| 			if (this.$refs.bg) (this.$refs.bg as any).style.pointerEvents = 'none'; |  | ||||||
| 			if (this.$refs.content) (this.$refs.content as any).style.pointerEvents = 'none'; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .popup-enter-active, .popup-leave-active { |  | ||||||
| 	transition: opacity 0.3s, transform 0.3s !important; |  | ||||||
| } |  | ||||||
| .popup-enter, .popup-leave-to { |  | ||||||
| 	opacity: 0; |  | ||||||
| 	transform: scale(0.9); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .bg-fade-enter-active, .bg-fade-leave-active { |  | ||||||
| 	transition: opacity 0.3s !important; |  | ||||||
| } |  | ||||||
| .bg-fade-enter, .bg-fade-leave-to { |  | ||||||
| 	opacity: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .mk-popup { |  | ||||||
| 	> .bg { |  | ||||||
| 		z-index: 10000; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .content { |  | ||||||
| 		position: absolute; |  | ||||||
| 		z-index: 10001; |  | ||||||
| 		background: var(--panel); |  | ||||||
| 		border-radius: 8px; |  | ||||||
| 		box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15); |  | ||||||
| 		overflow: hidden; |  | ||||||
| 		transform-origin: center top; |  | ||||||
| 
 |  | ||||||
| 		&.fixed { |  | ||||||
| 			position: fixed; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | @ -1,28 +1,28 @@ | ||||||
| <template> | <template> | ||||||
| <div class="skeikyzd" v-show="files.length != 0"> | <div class="skeikyzd" v-show="files.length != 0"> | ||||||
| 	<x-draggable class="files" :list="files" animation="150" delay="100" delayOnTouchOnly="true"> | 	<XDraggable class="files" :list="files" animation="150" delay="100" delay-on-touch-only="true"> | ||||||
| 		<div v-for="file in files" :key="file.id" @click="showFileMenu(file, $event)" @contextmenu.prevent="showFileMenu(file, $event)"> | 		<div v-for="file in files" :key="file.id" @click="showFileMenu(file, $event)" @contextmenu.prevent="showFileMenu(file, $event)"> | ||||||
| 			<x-file-thumbnail :data-id="file.id" class="thumbnail" :file="file" fit="cover"/> | 			<MkDriveFileThumbnail :data-id="file.id" class="thumbnail" :file="file" fit="cover"/> | ||||||
| 			<div class="sensitive" v-if="file.isSensitive"> | 			<div class="sensitive" v-if="file.isSensitive"> | ||||||
| 				<fa class="icon" :icon="faExclamationTriangle"/> | 				<Fa class="icon" :icon="faExclamationTriangle"/> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</x-draggable> | 	</XDraggable> | ||||||
| 	<p class="remain">{{ 4 - files.length }}/4</p> | 	<p class="remain">{{ 4 - files.length }}/4</p> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent, defineAsyncComponent } from 'vue'; | ||||||
| import * as XDraggable from 'vuedraggable'; |  | ||||||
| import { faTimesCircle, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; | import { faTimesCircle, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { faExclamationTriangle, faICursor } from '@fortawesome/free-solid-svg-icons'; | import { faExclamationTriangle, faICursor } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import XFileThumbnail from './drive-file-thumbnail.vue' | import MkDriveFileThumbnail from './drive-file-thumbnail.vue' | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XDraggable, | 		XDraggable: defineAsyncComponent(() => import('vue-draggable-next').then(x => x.VueDraggableNext)), | ||||||
| 		XFileThumbnail | 		MkDriveFileThumbnail | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -36,6 +36,8 @@ export default Vue.extend({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	emits: ['updated', 'detach'], | ||||||
|  | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			menu: null as Promise<null> | null, | 			menu: null as Promise<null> | null, | ||||||
|  | @ -48,21 +50,21 @@ export default Vue.extend({ | ||||||
| 		detachMedia(id) { | 		detachMedia(id) { | ||||||
| 			if (this.detachMediaFn) { | 			if (this.detachMediaFn) { | ||||||
| 				this.detachMediaFn(id); | 				this.detachMediaFn(id); | ||||||
| 			} else if (this.$parent.detachMedia) { | 			} else { | ||||||
| 				this.$parent.detachMedia(id); | 				this.$emit('detach', id); | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		toggleSensitive(file) { | 		toggleSensitive(file) { | ||||||
| 			this.$root.api('drive/files/update', { | 			os.api('drive/files/update', { | ||||||
| 				fileId: file.id, | 				fileId: file.id, | ||||||
| 				isSensitive: !file.isSensitive | 				isSensitive: !file.isSensitive | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
| 				file.isSensitive = !file.isSensitive; | 				file.isSensitive = !file.isSensitive; | ||||||
| 				this.$parent.updateMedia(file); | 				this.$emit('updated', file); | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 		async rename(file) { | 		async rename(file) { | ||||||
| 			const { canceled, result } = await this.$root.dialog({ | 			const { canceled, result } = await os.dialog({ | ||||||
| 				title: this.$t('enterFileName'), | 				title: this.$t('enterFileName'), | ||||||
| 				input: { | 				input: { | ||||||
| 					default: file.name | 					default: file.name | ||||||
|  | @ -70,18 +72,17 @@ export default Vue.extend({ | ||||||
| 				allowEmpty: false | 				allowEmpty: false | ||||||
| 			}); | 			}); | ||||||
| 			if (canceled) return; | 			if (canceled) return; | ||||||
| 			this.$root.api('drive/files/update', { | 			os.api('drive/files/update', { | ||||||
| 				fileId: file.id, | 				fileId: file.id, | ||||||
| 				name: result | 				name: result | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
| 				file.name = result; | 				file.name = result; | ||||||
| 				this.$parent.updateMedia(file); | 				this.$emit('updated', file); | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 		showFileMenu(file, ev: MouseEvent) { | 		showFileMenu(file, ev: MouseEvent) { | ||||||
| 			if (this.menu) return; | 			if (this.menu) return; | ||||||
| 			this.menu = this.$root.menu({ | 			this.menu = os.modalMenu([{ | ||||||
| 				items: [{ |  | ||||||
| 				text: this.$t('renameFile'), | 				text: this.$t('renameFile'), | ||||||
| 				icon: faICursor, | 				icon: faICursor, | ||||||
| 				action: () => { this.rename(file) } | 				action: () => { this.rename(file) } | ||||||
|  | @ -93,9 +94,7 @@ export default Vue.extend({ | ||||||
| 				text: this.$t('attachCancel'), | 				text: this.$t('attachCancel'), | ||||||
| 				icon: faTimesCircle, | 				icon: faTimesCircle, | ||||||
| 				action: () => { this.detachMedia(file.id) } | 				action: () => { this.detachMedia(file.id) } | ||||||
| 				}], | 			}], ev.currentTarget || ev.target).then(() => this.menu = null); | ||||||
| 				source: ev.currentTarget || ev.target |  | ||||||
| 			}).then(() => this.menu = null); |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | @ -103,7 +102,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .skeikyzd { | .skeikyzd { | ||||||
| 	padding: 4px; | 	padding: 8px 16px; | ||||||
| 	position: relative; | 	position: relative; | ||||||
| 
 | 
 | ||||||
| 	> .files { | 	> .files { | ||||||
|  | @ -114,7 +113,9 @@ export default Vue.extend({ | ||||||
| 			position: relative; | 			position: relative; | ||||||
| 			width: 64px; | 			width: 64px; | ||||||
| 			height: 64px; | 			height: 64px; | ||||||
| 			margin: 4px; | 			margin-right: 4px; | ||||||
|  | 			border-radius: 4px; | ||||||
|  | 			overflow: hidden; | ||||||
| 			cursor: move; | 			cursor: move; | ||||||
| 
 | 
 | ||||||
| 			&:hover > .remove { | 			&:hover > .remove { | ||||||
|  |  | ||||||
|  | @ -1,156 +1,19 @@ | ||||||
| <template> | <template> | ||||||
| <div class="ulveipgl"> | <MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')" :position="'top'"> | ||||||
| 	<transition :name="$store.state.device.animation ? 'form-fade' : ''" appear @after-leave="$emit('closed');"> | 	<MkPostForm @done="$refs.modal.close()" @esc="$refs.modal.close()" v-bind="$attrs"/> | ||||||
| 		<div class="bg _modalBg" ref="bg" v-if="show" @click="close()"></div> | </MkModal> | ||||||
| 	</transition> |  | ||||||
| 	<div class="main" ref="main" @click.self="close()" @keydown="onKeydown"> |  | ||||||
| 		<transition :name="$store.state.device.animation ? 'form' : ''" appear |  | ||||||
| 			@after-leave="destroyDom" |  | ||||||
| 		> |  | ||||||
| 			<x-post-form ref="form" |  | ||||||
| 				v-if="show" |  | ||||||
| 				:reply="reply" |  | ||||||
| 				:renote="renote" |  | ||||||
| 				:mention="mention" |  | ||||||
| 				:specified="specified" |  | ||||||
| 				:initial-text="initialText" |  | ||||||
| 				:initial-note="initialNote" |  | ||||||
| 				:instant="instant" |  | ||||||
| 				:channel="channel" |  | ||||||
| 				@posted="onPosted" |  | ||||||
| 				@cancel="onCanceled" |  | ||||||
| 				style="border-radius: var(--radius);"/> |  | ||||||
| 		</transition> |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import XPostForm from './post-form.vue'; | import MkModal from '@/components/ui/modal.vue'; | ||||||
|  | import MkPostForm from '@/components/post-form.vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XPostForm | 		MkModal, | ||||||
|  | 		MkPostForm, | ||||||
| 	}, | 	}, | ||||||
| 
 | 	emits: ['closed'], | ||||||
| 	props: { |  | ||||||
| 		reply: { |  | ||||||
| 			type: Object, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		renote: { |  | ||||||
| 			type: Object, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		mention: { |  | ||||||
| 			type: Object, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		specified: { |  | ||||||
| 			type: Object, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		initialText: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		initialNote: { |  | ||||||
| 			type: Object, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		instant: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false, |  | ||||||
| 			default: false |  | ||||||
| 		}, |  | ||||||
| 		channel: { |  | ||||||
| 			type: Object, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			show: true |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		focus() { |  | ||||||
| 			this.$refs.form.focus(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		close() { |  | ||||||
| 			this.show = false; |  | ||||||
| 			(this.$refs.bg as any).style.pointerEvents = 'none'; |  | ||||||
| 			(this.$refs.main as any).style.pointerEvents = 'none'; |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onPosted() { |  | ||||||
| 			this.$emit('posted'); |  | ||||||
| 			this.close(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onCanceled() { |  | ||||||
| 			this.$emit('cancel'); |  | ||||||
| 			this.close(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onKeydown(e) { |  | ||||||
| 			if (e.which === 27) { // Esc |  | ||||||
| 				e.preventDefault(); |  | ||||||
| 				e.stopPropagation(); |  | ||||||
| 				this.close(); |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| .form-enter-active, .form-leave-active { |  | ||||||
| 	transition: opacity 0.3s, transform 0.3s !important; |  | ||||||
| } |  | ||||||
| .form-enter, .form-leave-to { |  | ||||||
| 	opacity: 0; |  | ||||||
| 	transform: scale(0.9); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .form-fade-enter-active, .form-fade-leave-active { |  | ||||||
| 	transition: opacity 0.3s !important; |  | ||||||
| } |  | ||||||
| .form-fade-enter, .form-fade-leave-to { |  | ||||||
| 	opacity: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ulveipgl { |  | ||||||
| 	> .bg { |  | ||||||
| 		z-index: 10000; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .main { |  | ||||||
| 		display: block; |  | ||||||
| 		position: fixed; |  | ||||||
| 		z-index: 10000; |  | ||||||
| 		top: 32px; |  | ||||||
| 		left: 0; |  | ||||||
| 		right: 0; |  | ||||||
| 		height: calc(100% - 64px); |  | ||||||
| 		width: 500px; |  | ||||||
| 		max-width: calc(100% - 16px); |  | ||||||
| 		overflow: auto; |  | ||||||
| 		margin: 0 auto 0 auto; |  | ||||||
| 
 |  | ||||||
| 		@media (max-width: 550px) { |  | ||||||
| 			top: 16px; |  | ||||||
| 			height: calc(100% - 32px); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		@media (max-width: 520px) { |  | ||||||
| 			top: 8px; |  | ||||||
| 			height: calc(100% - 16px); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
|  | @ -1,84 +1,84 @@ | ||||||
| <template> | <template> | ||||||
| <div class="gafaadew" | <div class="gafaadew" :class="{ modal, _popup: modal }" | ||||||
|  | 	v-size="{ max: [500] }" | ||||||
| 	@dragover.stop="onDragover" | 	@dragover.stop="onDragover" | ||||||
| 	@dragenter="onDragenter" | 	@dragenter="onDragenter" | ||||||
| 	@dragleave="onDragleave" | 	@dragleave="onDragleave" | ||||||
| 	@drop.stop="onDrop" | 	@drop.stop="onDrop" | ||||||
| > | > | ||||||
| 	<header> | 	<header> | ||||||
| 		<button v-if="!fixed" class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button> | 		<button v-if="!fixed" class="cancel _button" @click="cancel"><Fa :icon="faTimes"/></button> | ||||||
| 		<div> | 		<div> | ||||||
| 			<span class="local-only" v-if="localOnly" v-text="$t('_visibility.localOnly')" /> |  | ||||||
| 			<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span> | 			<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span> | ||||||
|  | 			<span class="local-only" v-if="localOnly"><Fa :icon="faBiohazard"/></span> | ||||||
| 			<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$t('visibility')" :disabled="channel != null"> | 			<button class="_button visibility" @click="setVisibility" ref="visibilityButton" v-tooltip="$t('visibility')" :disabled="channel != null"> | ||||||
| 				<span v-if="visibility === 'public'"><fa :icon="faGlobe"/></span> | 				<span v-if="visibility === 'public'"><Fa :icon="faGlobe"/></span> | ||||||
| 				<span v-if="visibility === 'home'"><fa :icon="faHome"/></span> | 				<span v-if="visibility === 'home'"><Fa :icon="faHome"/></span> | ||||||
| 				<span v-if="visibility === 'followers'"><fa :icon="faUnlock"/></span> | 				<span v-if="visibility === 'followers'"><Fa :icon="faUnlock"/></span> | ||||||
| 				<span v-if="visibility === 'specified'"><fa :icon="faEnvelope"/></span> | 				<span v-if="visibility === 'specified'"><Fa :icon="faEnvelope"/></span> | ||||||
| 			</button> | 			</button> | ||||||
| 			<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<fa :icon="reply ? faReply : renote ? faQuoteRight : faPaperPlane"/></button> | 			<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<Fa :icon="reply ? faReply : renote ? faQuoteRight : faPaperPlane"/></button> | ||||||
| 		</div> | 		</div> | ||||||
| 	</header> | 	</header> | ||||||
| 	<div class="form" :class="{ fixed }"> | 	<div class="form" :class="{ fixed }"> | ||||||
| 		<x-note-preview class="preview" v-if="reply" :note="reply"/> | 		<XNotePreview class="preview" v-if="reply" :note="reply"/> | ||||||
| 		<x-note-preview class="preview" v-if="renote" :note="renote"/> | 		<XNotePreview class="preview" v-if="renote" :note="renote"/> | ||||||
| 		<div class="with-quote" v-if="quoteId"><fa icon="quote-left"/> {{ $t('quoteAttached') }}<button @click="quoteId = null"><fa icon="times"/></button></div> | 		<div class="with-quote" v-if="quoteId"><Fa icon="quote-left"/> {{ $t('quoteAttached') }}<button @click="quoteId = null"><Fa icon="times"/></button></div> | ||||||
| 		<div v-if="visibility === 'specified'" class="to-specified"> | 		<div v-if="visibility === 'specified'" class="to-specified"> | ||||||
| 			<span style="margin-right: 8px;">{{ $t('recipient') }}</span> | 			<span style="margin-right: 8px;">{{ $t('recipient') }}</span> | ||||||
| 			<div class="visibleUsers"> | 			<div class="visibleUsers"> | ||||||
| 				<span v-for="u in visibleUsers" :key="u.id"> | 				<span v-for="u in visibleUsers" :key="u.id"> | ||||||
| 					<mk-acct :user="u"/> | 					<MkAcct :user="u"/> | ||||||
| 					<button class="_button" @click="removeVisibleUser(u)"><fa :icon="faTimes"/></button> | 					<button class="_button" @click="removeVisibleUser(u)"><Fa :icon="faTimes"/></button> | ||||||
| 				</span> | 				</span> | ||||||
| 				<button @click="addVisibleUser" class="_buttonPrimary"><fa :icon="faPlus" fixed-width/></button> | 				<button @click="addVisibleUser" class="_buttonPrimary"><Fa :icon="faPlus" fixed-width/></button> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$t('annotation')" v-autocomplete="{ model: 'cw' }" @keydown="onKeydown"> | 		<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$t('annotation')" @keydown="onKeydown"> | ||||||
| 		<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }" @keydown="onKeydown" @paste="onPaste"></textarea> | 		<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste"></textarea> | ||||||
| 		<x-post-form-attaches class="attaches" :files="files"/> | 		<XPostFormAttaches class="attaches" :files="files" @updated="updateMedia" @detach="detachMedia"/> | ||||||
| 		<x-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> | 		<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/> | ||||||
| 		<x-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> |  | ||||||
| 		<footer> | 		<footer> | ||||||
| 			<button class="_button" @click="chooseFileFrom" v-tooltip="$t('attachFile')"><fa :icon="faPhotoVideo"/></button> | 			<button class="_button" @click="chooseFileFrom" v-tooltip="$t('attachFile')"><Fa :icon="faPhotoVideo"/></button> | ||||||
| 			<button class="_button" @click="poll = !poll" :class="{ active: poll }" v-tooltip="$t('poll')"><fa :icon="faPollH"/></button> | 			<button class="_button" @click="togglePoll" :class="{ active: poll }" v-tooltip="$t('poll')"><Fa :icon="faPollH"/></button> | ||||||
| 			<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$t('useCw')"><fa :icon="faEyeSlash"/></button> | 			<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$t('useCw')"><Fa :icon="faEyeSlash"/></button> | ||||||
| 			<button class="_button" @click="insertMention" v-tooltip="$t('mention')"><fa :icon="faAt"/></button> | 			<button class="_button" @click="insertMention" v-tooltip="$t('mention')"><Fa :icon="faAt"/></button> | ||||||
| 			<button class="_button" @click="insertEmoji" v-tooltip="$t('emoji')"><fa :icon="faLaughSquint"/></button> | 			<button class="_button" @click="insertEmoji" v-tooltip="$t('emoji')"><Fa :icon="faLaughSquint"/></button> | ||||||
| 			<button class="_button" @click="showActions" v-tooltip="$t('plugin')" v-if="$store.state.postFormActions.length > 0"><fa :icon="faPlug"/></button> | 			<button class="_button" @click="showActions" v-tooltip="$t('plugin')" v-if="postFormActions.length > 0"><Fa :icon="faPlug"/></button> | ||||||
| 		</footer> | 		</footer> | ||||||
| 		<input ref="file" class="file _button" type="file" multiple="multiple" @change="onChangeFile"/> |  | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent, defineAsyncComponent } from 'vue'; | ||||||
| import { faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons'; | import { faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons'; | import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import insertTextAtCursor from 'insert-text-at-cursor'; | import insertTextAtCursor from 'insert-text-at-cursor'; | ||||||
| import { length } from 'stringz'; | import { length } from 'stringz'; | ||||||
| import { toASCII } from 'punycode'; | import { toASCII } from 'punycode'; | ||||||
| import MkVisibilityChooser from './visibility-chooser.vue'; |  | ||||||
| import MkUserSelect from './user-select.vue'; |  | ||||||
| import XNotePreview from './note-preview.vue'; | import XNotePreview from './note-preview.vue'; | ||||||
| import { parse } from '../../mfm/parse'; | import { parse } from '../../mfm/parse'; | ||||||
| import { host, url } from '../config'; | import { host, url } from '@/config'; | ||||||
| import { erase, unique } from '../../prelude/array'; | import { erase, unique } from '../../prelude/array'; | ||||||
| import extractMentions from '../../misc/extract-mentions'; | import extractMentions from '../../misc/extract-mentions'; | ||||||
| import getAcct from '../../misc/acct/render'; | import getAcct from '../../misc/acct/render'; | ||||||
| import { formatTimeString } from '../../misc/format-time-string'; | import { formatTimeString } from '../../misc/format-time-string'; | ||||||
| import { selectDriveFile } from '../scripts/select-drive-file'; | import { Autocomplete } from '@/scripts/autocomplete'; | ||||||
| import { noteVisibilities } from '../../types'; | import { noteVisibilities } from '../../types'; | ||||||
| import { utils } from '@syuilo/aiscript'; | import * as os from '@/os'; | ||||||
|  | import { selectFile } from '@/scripts/select-file'; | ||||||
|  | import { notePostInterruptors, postFormActions } from '@/store'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XNotePreview, | 		XNotePreview, | ||||||
| 		XUploader: () => import('./uploader.vue').then(m => m.default), | 		XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')), | ||||||
| 		XPostFormAttaches: () => import('./post-form-attaches.vue').then(m => m.default), | 		XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')) | ||||||
| 		XPollEditor: () => import('./poll-editor.vue').then(m => m.default) |  | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	inject: ['modal'], | ||||||
|  | 
 | ||||||
| 	props: { | 	props: { | ||||||
| 		reply: { | 		reply: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -117,19 +117,22 @@ export default Vue.extend({ | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
| 		} |  | ||||||
| 		}, | 		}, | ||||||
|  | 		autofocus: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: true | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	emits: ['posted', 'done', 'esc'], | ||||||
| 
 | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			posting: false, | 			posting: false, | ||||||
| 			text: '', | 			text: '', | ||||||
| 			files: [], | 			files: [], | ||||||
| 			uploadings: [], | 			poll: null, | ||||||
| 			poll: false, |  | ||||||
| 			pollChoices: [], |  | ||||||
| 			pollMultiple: false, |  | ||||||
| 			pollExpiration: [], |  | ||||||
| 			useCw: false, | 			useCw: false, | ||||||
| 			cw: null, | 			cw: null, | ||||||
| 			localOnly: false, | 			localOnly: false, | ||||||
|  | @ -139,7 +142,8 @@ export default Vue.extend({ | ||||||
| 			draghover: false, | 			draghover: false, | ||||||
| 			quoteId: null, | 			quoteId: null, | ||||||
| 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), | 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), | ||||||
| 			faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard, faPlug | 			postFormActions, | ||||||
|  | 			faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -190,7 +194,7 @@ export default Vue.extend({ | ||||||
| 			return !this.posting && | 			return !this.posting && | ||||||
| 				(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) && | 				(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) && | ||||||
| 				(length(this.text.trim()) <= this.max) && | 				(length(this.text.trim()) <= this.max) && | ||||||
| 				(!this.poll || this.pollChoices.length >= 2); | 				(!this.poll || this.poll.choices.length >= 2); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		max(): number { | 		max(): number { | ||||||
|  | @ -246,14 +250,14 @@ export default Vue.extend({ | ||||||
| 		if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) { | 		if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) { | ||||||
| 			this.visibility = this.reply.visibility; | 			this.visibility = this.reply.visibility; | ||||||
| 			if (this.reply.visibility === 'specified') { | 			if (this.reply.visibility === 'specified') { | ||||||
| 				this.$root.api('users/show', { | 				os.api('users/show', { | ||||||
| 					userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$store.state.i.id && uid !== this.reply.userId) | 					userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$store.state.i.id && uid !== this.reply.userId) | ||||||
| 				}).then(users => { | 				}).then(users => { | ||||||
| 					this.visibleUsers.push(...users); | 					this.visibleUsers.push(...users); | ||||||
| 				}); | 				}); | ||||||
| 
 | 
 | ||||||
| 				if (this.reply.userId !== this.$store.state.i.id) { | 				if (this.reply.userId !== this.$store.state.i.id) { | ||||||
| 					this.$root.api('users/show', { userId: this.reply.userId }).then(user => { | 					os.api('users/show', { userId: this.reply.userId }).then(user => { | ||||||
| 						this.visibleUsers.push(user); | 						this.visibleUsers.push(user); | ||||||
| 					}); | 					}); | ||||||
| 				} | 				} | ||||||
|  | @ -271,15 +275,21 @@ export default Vue.extend({ | ||||||
| 			this.cw = this.reply.cw; | 			this.cw = this.reply.cw; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		if (this.autofocus) { | ||||||
| 			this.focus(); | 			this.focus(); | ||||||
| 
 | 
 | ||||||
| 			this.$nextTick(() => { | 			this.$nextTick(() => { | ||||||
| 				this.focus(); | 				this.focus(); | ||||||
| 			}); | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// TODO: detach when unmount | ||||||
|  | 		new Autocomplete(this.$refs.text, this, { model: 'text' }); | ||||||
|  | 		new Autocomplete(this.$refs.cw, this, { model: 'cw' }); | ||||||
| 
 | 
 | ||||||
| 		this.$nextTick(() => { | 		this.$nextTick(() => { | ||||||
| 			// 書きかけの投稿を復元 | 			// 書きかけの投稿を復元 | ||||||
| 			if (!this.instant && !this.mention) { | 			if (!this.instant && !this.mention && !this.specified) { | ||||||
| 				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey]; | 				const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey]; | ||||||
| 				if (draft) { | 				if (draft) { | ||||||
| 					this.text = draft.data.text; | 					this.text = draft.data.text; | ||||||
|  | @ -289,10 +299,7 @@ export default Vue.extend({ | ||||||
| 					this.localOnly = draft.data.localOnly; | 					this.localOnly = draft.data.localOnly; | ||||||
| 					this.files = (draft.data.files || []).filter(e => e); | 					this.files = (draft.data.files || []).filter(e => e); | ||||||
| 					if (draft.data.poll) { | 					if (draft.data.poll) { | ||||||
| 						this.poll = true; | 						this.poll = draft.data.poll; | ||||||
| 						this.$nextTick(() => { |  | ||||||
| 							(this.$refs.poll as any).set(draft.data.poll); |  | ||||||
| 						}); |  | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | @ -305,13 +312,7 @@ export default Vue.extend({ | ||||||
| 				this.cw = init.cw; | 				this.cw = init.cw; | ||||||
| 				this.useCw = init.cw != null; | 				this.useCw = init.cw != null; | ||||||
| 				if (init.poll) { | 				if (init.poll) { | ||||||
| 					this.poll = true; | 					this.poll = init.poll; | ||||||
| 					this.$nextTick(() => { |  | ||||||
| 						(this.$refs.poll as any).set({ |  | ||||||
| 							choices: init.poll.choices.map(c => c.text), |  | ||||||
| 							multiple: init.poll.multiple |  | ||||||
| 						}); |  | ||||||
| 					}); |  | ||||||
| 				} | 				} | ||||||
| 				this.visibility = init.visibility; | 				this.visibility = init.visibility; | ||||||
| 				this.localOnly = init.localOnly; | 				this.localOnly = init.localOnly; | ||||||
|  | @ -328,11 +329,24 @@ export default Vue.extend({ | ||||||
| 			this.$watch('useCw', () => this.saveDraft()); | 			this.$watch('useCw', () => this.saveDraft()); | ||||||
| 			this.$watch('cw', () => this.saveDraft()); | 			this.$watch('cw', () => this.saveDraft()); | ||||||
| 			this.$watch('poll', () => this.saveDraft()); | 			this.$watch('poll', () => this.saveDraft()); | ||||||
| 			this.$watch('files', () => this.saveDraft()); | 			this.$watch('files', () => this.saveDraft(), { deep: true }); | ||||||
| 			this.$watch('visibility', () => this.saveDraft()); | 			this.$watch('visibility', () => this.saveDraft()); | ||||||
| 			this.$watch('localOnly', () => this.saveDraft()); | 			this.$watch('localOnly', () => this.saveDraft()); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		togglePoll() { | ||||||
|  | 			if (this.poll) { | ||||||
|  | 				this.poll = null; | ||||||
|  | 			} else { | ||||||
|  | 				this.poll = { | ||||||
|  | 					choices: ['', ''], | ||||||
|  | 					multiple: false, | ||||||
|  | 					expiresAt: null, | ||||||
|  | 					expiredAfter: null, | ||||||
|  | 				}; | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		trimmedLength(text: string) { | 		trimmedLength(text: string) { | ||||||
| 			return length(text.trim()); | 			return length(text.trim()); | ||||||
| 		}, | 		}, | ||||||
|  | @ -346,85 +360,50 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		chooseFileFrom(ev) { | 		chooseFileFrom(ev) { | ||||||
| 			this.$root.menu({ | 			selectFile(ev.currentTarget || ev.target, this.$t('attachFile'), true).then(files => { | ||||||
| 				items: [{ |  | ||||||
| 					type: 'label', |  | ||||||
| 					text: this.$t('attachFile'), |  | ||||||
| 				}, { |  | ||||||
| 					text: this.$t('upload'), |  | ||||||
| 					icon: faUpload, |  | ||||||
| 					action: () => { this.chooseFileFromPc() } |  | ||||||
| 				}, { |  | ||||||
| 					text: this.$t('fromDrive'), |  | ||||||
| 					icon: faCloud, |  | ||||||
| 					action: () => { this.chooseFileFromDrive() } |  | ||||||
| 				}, { |  | ||||||
| 					text: this.$t('fromUrl'), |  | ||||||
| 					icon: faLink, |  | ||||||
| 					action: () => { this.chooseFileFromUrl() } |  | ||||||
| 				}], |  | ||||||
| 				source: ev.currentTarget || ev.target |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		chooseFileFromPc() { |  | ||||||
| 			(this.$refs.file as any).click(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		chooseFileFromDrive() { |  | ||||||
| 			selectDriveFile(this.$root, true).then(files => { |  | ||||||
| 				for (const file of files) { | 				for (const file of files) { | ||||||
| 					this.attachMedia(file); | 					this.files.push(file); | ||||||
| 				} | 				} | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		attachMedia(driveFile) { |  | ||||||
| 			this.files.push(driveFile); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		detachMedia(id) { | 		detachMedia(id) { | ||||||
| 			this.files = this.files.filter(x => x.id != id); | 			this.files = this.files.filter(x => x.id != id); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		updateMedia(file) { | 		updateMedia(file) { | ||||||
| 			Vue.set(this.files, this.files.findIndex(x => x.id === file.id), file); | 			this.files[this.files.findIndex(x => x.id === file.id)] = file; | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onChangeFile() { |  | ||||||
| 			for (const x of Array.from((this.$refs.file as any).files)) this.upload(x); |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		upload(file: File, name?: string) { | 		upload(file: File, name?: string) { | ||||||
| 			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name); | 			os.upload(file, this.$store.state.settings.uploadFolder, name).then(res => { | ||||||
|  | 				this.files.push(res); | ||||||
|  | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onChangeUploadings(uploads) { | 		onPollUpdate(poll) { | ||||||
| 			this.$emit('change-uploadings', uploads); | 			this.poll = poll; | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onPollUpdate() { |  | ||||||
| 			const got = this.$refs.poll.get(); |  | ||||||
| 			this.pollChoices = got.choices; |  | ||||||
| 			this.pollMultiple = got.multiple; |  | ||||||
| 			this.pollExpiration = [got.expiration, got.expiresAt || got.expiredAfter]; |  | ||||||
| 			this.saveDraft(); | 			this.saveDraft(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		setVisibility() { | 		async setVisibility() { | ||||||
| 			if (this.channel) { | 			if (this.channel) { | ||||||
| 				// TODO: information dialog | 				// TODO: information dialog | ||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 			const w = this.$root.new(MkVisibilityChooser, { | 
 | ||||||
| 				source: this.$refs.visibilityButton, | 			os.popup(await import('./visibility-picker.vue'), { | ||||||
| 				currentVisibility: this.visibility, | 				currentVisibility: this.visibility, | ||||||
| 				currentLocalOnly: this.localOnly | 				currentLocalOnly: this.localOnly, | ||||||
| 			}); | 				src: this.$refs.visibilityButton | ||||||
| 			w.$once('chosen', ({ visibility, localOnly }) => { | 			}, { | ||||||
|  | 				changeVisibility: visibility => { | ||||||
| 					this.applyVisibility(visibility); | 					this.applyVisibility(visibility); | ||||||
|  | 				}, | ||||||
|  | 				changeLocalOnly: localOnly => { | ||||||
| 					this.localOnly = localOnly; | 					this.localOnly = localOnly; | ||||||
| 			}); | 				} | ||||||
|  | 			}, 'closed'); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		applyVisibility(v: string) { | 		applyVisibility(v: string) { | ||||||
|  | @ -432,8 +411,7 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		addVisibleUser() { | 		addVisibleUser() { | ||||||
| 			const vm = this.$root.new(MkUserSelect, {}); | 			os.selectUser().then(user => { | ||||||
| 			vm.$once('selected', user => { |  | ||||||
| 				this.visibleUsers.push(user); | 				this.visibleUsers.push(user); | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  | @ -445,12 +423,13 @@ export default Vue.extend({ | ||||||
| 		clear() { | 		clear() { | ||||||
| 			this.text = ''; | 			this.text = ''; | ||||||
| 			this.files = []; | 			this.files = []; | ||||||
| 			this.poll = false; | 			this.poll = null; | ||||||
| 			this.quoteId = null; | 			this.quoteId = null; | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		onKeydown(e) { | 		onKeydown(e) { | ||||||
| 			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); | 			if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post(); | ||||||
|  | 			if (e.which === 27) this.$emit('esc'); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		async onPaste(e: ClipboardEvent) { | 		async onPaste(e: ClipboardEvent) { | ||||||
|  | @ -469,7 +448,7 @@ export default Vue.extend({ | ||||||
| 			if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) { | 			if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) { | ||||||
| 				e.preventDefault(); | 				e.preventDefault(); | ||||||
| 
 | 
 | ||||||
| 				this.$root.dialog({ | 				os.dialog({ | ||||||
| 					type: 'info', | 					type: 'info', | ||||||
| 					text: this.$t('quoteQuestion'), | 					text: this.$t('quoteQuestion'), | ||||||
| 					showCancelButton: true | 					showCancelButton: true | ||||||
|  | @ -487,7 +466,7 @@ export default Vue.extend({ | ||||||
| 		onDragover(e) { | 		onDragover(e) { | ||||||
| 			if (!e.dataTransfer.items[0]) return; | 			if (!e.dataTransfer.items[0]) return; | ||||||
| 			const isFile = e.dataTransfer.items[0].kind == 'file'; | 			const isFile = e.dataTransfer.items[0].kind == 'file'; | ||||||
| 			const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file'; | 			const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_; | ||||||
| 			if (isFile || isDriveFile) { | 			if (isFile || isDriveFile) { | ||||||
| 				e.preventDefault(); | 				e.preventDefault(); | ||||||
| 				this.draghover = true; | 				this.draghover = true; | ||||||
|  | @ -514,7 +493,7 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			//#region ドライブのファイル | 			//#region ドライブのファイル | ||||||
| 			const driveFile = e.dataTransfer.getData('mk_drive_file'); | 			const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); | ||||||
| 			if (driveFile != null && driveFile != '') { | 			if (driveFile != null && driveFile != '') { | ||||||
| 				const file = JSON.parse(driveFile); | 				const file = JSON.parse(driveFile); | ||||||
| 				this.files.push(file); | 				this.files.push(file); | ||||||
|  | @ -537,7 +516,7 @@ export default Vue.extend({ | ||||||
| 					visibility: this.visibility, | 					visibility: this.visibility, | ||||||
| 					localOnly: this.localOnly, | 					localOnly: this.localOnly, | ||||||
| 					files: this.files, | 					files: this.files, | ||||||
| 					poll: this.poll && this.$refs.poll ? (this.$refs.poll as any).get() : undefined | 					poll: this.poll | ||||||
| 				} | 				} | ||||||
| 			}; | 			}; | ||||||
| 
 | 
 | ||||||
|  | @ -559,29 +538,30 @@ export default Vue.extend({ | ||||||
| 				replyId: this.reply ? this.reply.id : undefined, | 				replyId: this.reply ? this.reply.id : undefined, | ||||||
| 				renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined, | 				renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined, | ||||||
| 				channelId: this.channel ? this.channel.id : undefined, | 				channelId: this.channel ? this.channel.id : undefined, | ||||||
| 				poll: this.poll ? (this.$refs.poll as any).get() : undefined, | 				poll: this.poll, | ||||||
| 				cw: this.useCw ? this.cw || '' : undefined, | 				cw: this.useCw ? this.cw || '' : undefined, | ||||||
| 				localOnly: this.localOnly, | 				localOnly: this.localOnly, | ||||||
| 				visibility: this.visibility, | 				visibility: this.visibility, | ||||||
| 				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined, | 				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined, | ||||||
| 				viaMobile: this.$root.isMobile | 				viaMobile: os.isMobile | ||||||
| 			}; | 			}; | ||||||
| 
 | 
 | ||||||
| 			// plugin | 			// plugin | ||||||
| 			if (this.$store.state.notePostInterruptors.length > 0) { | 			if (notePostInterruptors.length > 0) { | ||||||
| 				for (const interruptor of this.$store.state.notePostInterruptors) { | 				for (const interruptor of notePostInterruptors) { | ||||||
| 					data = utils.valToJs(await interruptor.handler(JSON.parse(JSON.stringify(data)))); | 					data = await interruptor.handler(JSON.parse(JSON.stringify(data))); | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			this.posting = true; | 			this.posting = true; | ||||||
| 			this.$root.api('notes/create', data).then(() => { | 			os.api('notes/create', data).then(() => { | ||||||
| 				this.clear(); | 				this.clear(); | ||||||
| 				this.deleteDraft(); | 				this.deleteDraft(); | ||||||
| 				this.$emit('posted'); | 				this.$emit('posted'); | ||||||
| 			}).catch(err => { | 			}).catch(err => { | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
| 				this.posting = false; | 				this.posting = false; | ||||||
|  | 				this.$emit('done'); | ||||||
| 			}); | 			}); | ||||||
| 
 | 
 | ||||||
| 			if (this.text && this.text != '') { | 			if (this.text && this.text != '') { | ||||||
|  | @ -592,28 +572,23 @@ export default Vue.extend({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		cancel() { | 		cancel() { | ||||||
| 			this.$emit('cancel'); | 			this.$emit('done'); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		insertMention() { | 		insertMention() { | ||||||
| 			const vm = this.$root.new(MkUserSelect, {}); | 			os.selectUser().then(user => { | ||||||
| 			vm.$once('selected', user => { | 				insertTextAtCursor(this.$refs.text, '@' + getAcct(user) + ' '); | ||||||
| 				insertTextAtCursor(this.$refs.text, getAcct(user) + ' '); |  | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		async insertEmoji(ev) { | 		async insertEmoji(ev) { | ||||||
| 			const vm = this.$root.new(await import('./emoji-picker.vue').then(m => m.default), { | 			os.pickEmoji(ev.currentTarget || ev.target).then(emoji => { | ||||||
| 				source: ev.currentTarget || ev.target |  | ||||||
| 			}).$once('chosen', emoji => { |  | ||||||
| 				insertTextAtCursor(this.$refs.text, emoji); | 				insertTextAtCursor(this.$refs.text, emoji); | ||||||
| 				vm.close(); |  | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		showActions(ev) { | 		showActions(ev) { | ||||||
| 			this.$root.menu({ | 			os.modalMenu(postFormActions.map(action => ({ | ||||||
| 				items: this.$store.state.postFormActions.map(action => ({ |  | ||||||
| 				text: action.title, | 				text: action.title, | ||||||
| 				action: () => { | 				action: () => { | ||||||
| 					action.handler({ | 					action.handler({ | ||||||
|  | @ -622,9 +597,7 @@ export default Vue.extend({ | ||||||
| 						if (key === 'text') { this.text = value; } | 						if (key === 'text') { this.text = value; } | ||||||
| 					}); | 					}); | ||||||
| 				} | 				} | ||||||
| 				})), | 			})), ev.currentTarget || ev.target); | ||||||
| 				source: ev.currentTarget || ev.target, |  | ||||||
| 			}); |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | @ -632,26 +605,22 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .gafaadew { | .gafaadew { | ||||||
| 	background: var(--panel); | 	position: relative; | ||||||
|  | 
 | ||||||
|  | 	&.modal { | ||||||
|  | 		width: 100%; | ||||||
|  | 		max-width: 520px; | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	> header { | 	> header { | ||||||
| 		z-index: 1000; | 		z-index: 1000; | ||||||
| 		height: 66px; | 		height: 66px; | ||||||
| 
 | 
 | ||||||
| 		@media (max-width: 500px) { |  | ||||||
| 			height: 50px; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .cancel { | 		> .cancel { | ||||||
| 			padding: 0; | 			padding: 0; | ||||||
| 			font-size: 20px; | 			font-size: 20px; | ||||||
| 			width: 64px; | 			width: 64px; | ||||||
| 			line-height: 66px; | 			line-height: 66px; | ||||||
| 
 |  | ||||||
| 			@media (max-width: 500px) { |  | ||||||
| 				width: 50px; |  | ||||||
| 				line-height: 50px; |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		> div { | 		> div { | ||||||
|  | @ -662,10 +631,6 @@ export default Vue.extend({ | ||||||
| 			> .text-count { | 			> .text-count { | ||||||
| 				opacity: 0.7; | 				opacity: 0.7; | ||||||
| 				line-height: 66px; | 				line-height: 66px; | ||||||
| 
 |  | ||||||
| 				@media (max-width: 500px) { |  | ||||||
| 					line-height: 50px; |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			> .visibility { | 			> .visibility { | ||||||
|  | @ -678,8 +643,9 @@ export default Vue.extend({ | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			 | 			 | ||||||
| 			.local-only { | 			> .local-only { | ||||||
| 				margin: 0 8px; | 				margin: 0 0 0 12px; | ||||||
|  | 				opacity: 0.7; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			> .submit { | 			> .submit { | ||||||
|  | @ -690,10 +656,6 @@ export default Vue.extend({ | ||||||
| 				vertical-align: bottom; | 				vertical-align: bottom; | ||||||
| 				border-radius: 4px; | 				border-radius: 4px; | ||||||
| 
 | 
 | ||||||
| 				@media (max-width: 500px) { |  | ||||||
| 					margin: 8px; |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				&:disabled { | 				&:disabled { | ||||||
| 					opacity: 0.7; | 					opacity: 0.7; | ||||||
| 				} | 				} | ||||||
|  | @ -706,13 +668,6 @@ export default Vue.extend({ | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> .form { | 	> .form { | ||||||
| 		max-width: 500px; |  | ||||||
| 		margin: 0 auto; |  | ||||||
| 
 |  | ||||||
| 		&.fixed { |  | ||||||
| 			max-width: unset; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .preview { | 		> .preview { | ||||||
| 			padding: 16px; | 			padding: 16px; | ||||||
| 		} | 		} | ||||||
|  | @ -741,10 +696,6 @@ export default Vue.extend({ | ||||||
| 			overflow: auto; | 			overflow: auto; | ||||||
| 			white-space: nowrap; | 			white-space: nowrap; | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: 500px) { |  | ||||||
| 				padding: 6px 16px; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .visibleUsers { | 			> .visibleUsers { | ||||||
| 				display: inline; | 				display: inline; | ||||||
| 				top: -1px; | 				top: -1px; | ||||||
|  | @ -782,10 +733,6 @@ export default Vue.extend({ | ||||||
| 			color: var(--fg); | 			color: var(--fg); | ||||||
| 			font-family: inherit; | 			font-family: inherit; | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: 500px) { |  | ||||||
| 				padding: 0 16px; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			&:focus { | 			&:focus { | ||||||
| 				outline: none; | 				outline: none; | ||||||
| 			} | 			} | ||||||
|  | @ -806,31 +753,14 @@ export default Vue.extend({ | ||||||
| 			min-width: 100%; | 			min-width: 100%; | ||||||
| 			min-height: 90px; | 			min-height: 90px; | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: 500px) { |  | ||||||
| 				min-height: 80px; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			&.withCw { | 			&.withCw { | ||||||
| 				padding-top: 8px; | 				padding-top: 8px; | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		> .mk-uploader { |  | ||||||
| 			margin: 8px 0 0 0; |  | ||||||
| 			padding: 8px; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .file { |  | ||||||
| 			display: none; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> footer { | 		> footer { | ||||||
| 			padding: 0 16px 16px 16px; | 			padding: 0 16px 16px 16px; | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: 500px) { |  | ||||||
| 				padding: 0 8px 8px 8px; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> button { | 			> button { | ||||||
| 				display: inline-block; | 				display: inline-block; | ||||||
| 				padding: 0; | 				padding: 0; | ||||||
|  | @ -850,5 +780,45 @@ export default Vue.extend({ | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	&.max-width_500px { | ||||||
|  | 		> header { | ||||||
|  | 			height: 50px; | ||||||
|  | 
 | ||||||
|  | 			> .cancel { | ||||||
|  | 				width: 50px; | ||||||
|  | 				line-height: 50px; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			> div { | ||||||
|  | 				> .text-count { | ||||||
|  | 					line-height: 50px; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .submit { | ||||||
|  | 					margin: 8px; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		> .form { | ||||||
|  | 			> .to-specified { | ||||||
|  | 				padding: 6px 16px; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			> .cw, | ||||||
|  | 			> .text { | ||||||
|  | 				padding: 0 16px; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			> .text { | ||||||
|  | 				min-height: 80px; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			> footer { | ||||||
|  | 				padding: 0 8px 8px 8px; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| <template> | <template> | ||||||
| <mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :customEmojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/> | <MkEmoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :customEmojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue';import * as os from '@/os'; | ||||||
| export default Vue.extend({ | 
 | ||||||
|  | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
| 		reaction: { | 		reaction: { | ||||||
| 			type: String, | 			type: String, | ||||||
|  |  | ||||||
|  | @ -1,31 +1,28 @@ | ||||||
| <template> | <template> | ||||||
| <x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap"> | <MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')"> | ||||||
| 	<div class="rdfaahpb"> | 	<div class="rdfaahpb _popup" v-hotkey="keymap"> | ||||||
| 		<div class="buttons" ref="buttons" :class="{ showFocus }"> | 		<div class="buttons" ref="buttons" :class="{ showFocus }"> | ||||||
| 			<button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :tabindex="i + 1" :title="reaction" v-particle><x-reaction-icon :reaction="reaction"/></button> | 			<button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :tabindex="i + 1" :title="reaction" v-particle><XReactionIcon :reaction="reaction"/></button> | ||||||
| 		</div> | 		</div> | ||||||
| 		<input class="text" v-model.trim="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }"> | 		<input class="text" ref="text" v-model.trim="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText"> | ||||||
| 	</div> | 	</div> | ||||||
| </x-popup> | </MkModal> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { emojiRegex } from '../../misc/emoji-regex'; | import { emojiRegex } from '../../misc/emoji-regex'; | ||||||
| import XReactionIcon from './reaction-icon.vue'; | import XReactionIcon from '@/components/reaction-icon.vue'; | ||||||
| import XPopup from './popup.vue'; | import MkModal from '@/components/ui/modal.vue'; | ||||||
|  | import { Autocomplete } from '@/scripts/autocomplete'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XPopup, |  | ||||||
| 		XReactionIcon, | 		XReactionIcon, | ||||||
|  | 		MkModal, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
| 		source: { |  | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		reactions: { | 		reactions: { | ||||||
| 			required: false | 			required: false | ||||||
| 		}, | 		}, | ||||||
|  | @ -35,7 +32,13 @@ export default Vue.extend({ | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
| 		}, | 		}, | ||||||
|  | 
 | ||||||
|  | 		src: { | ||||||
|  | 			required: false | ||||||
| 		}, | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	emits: ['done', 'closed'], | ||||||
| 
 | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  | @ -70,21 +73,30 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 	watch: { | 	watch: { | ||||||
| 		focus(i) { | 		focus(i) { | ||||||
| 			this.$refs.buttons.children[i].focus(); | 			this.$refs.buttons.children[i].focus({ | ||||||
|  | 				preventScroll: true | ||||||
|  | 			}); | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
|  | 		this.$nextTick(() => { | ||||||
| 			this.focus = 0; | 			this.focus = 0; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		// TODO: detach when unmount | ||||||
|  | 		new Autocomplete(this.$refs.text, this, { model: 'text' }); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		close() { | 		close() { | ||||||
| 			this.$refs.popup.close(); | 			this.$emit('done'); | ||||||
|  | 			this.$refs.modal.close(); | ||||||
| 		}, | 		}, | ||||||
| 	 | 	 | ||||||
| 		react(reaction) { | 		react(reaction) { | ||||||
| 			this.$emit('chosen', reaction); | 			this.$emit('done', reaction); | ||||||
|  | 			this.$refs.modal.close(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		reactText() { | 		reactText() { | ||||||
|  | @ -136,6 +148,7 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 		&.showFocus { | 		&.showFocus { | ||||||
| 			> button:focus { | 			> button:focus { | ||||||
|  | 				position: relative; | ||||||
| 				z-index: 1; | 				z-index: 1; | ||||||
| 
 | 
 | ||||||
| 				&:after { | 				&:after { | ||||||
|  |  | ||||||
|  | @ -1,28 +1,36 @@ | ||||||
| <template> | <template> | ||||||
| <mk-tooltip :source="source" ref="tooltip"> | <MkTooltip :source="source" ref="tooltip" @closed="$emit('closed')"> | ||||||
|  | 	<div class="bqxuuuey"> | ||||||
|  | 		<div class="info"> | ||||||
|  | 			<div>{{ reaction.replace('@.', '') }}</div> | ||||||
|  | 			<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon"/> | ||||||
|  | 		</div> | ||||||
| 		<template v-if="users.length <= 10"> | 		<template v-if="users.length <= 10"> | ||||||
| 			<b v-for="u in users" :key="u.id" style="margin-right: 12px;"> | 			<b v-for="u in users" :key="u.id" style="margin-right: 12px;"> | ||||||
| 			<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> | 				<MkAvatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> | ||||||
| 			<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> | 				<MkUserName :user="u" :nowrap="false" style="line-height: 24px;"/> | ||||||
| 			</b> | 			</b> | ||||||
| 		</template> | 		</template> | ||||||
| 		<template v-if="10 < users.length"> | 		<template v-if="10 < users.length"> | ||||||
| 			<b v-for="u in users" :key="u.id" style="margin-right: 12px;"> | 			<b v-for="u in users" :key="u.id" style="margin-right: 12px;"> | ||||||
| 			<mk-avatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> | 				<MkAvatar :user="u" style="width: 24px; height: 24px; margin-right: 2px;"/> | ||||||
| 			<mk-user-name :user="u" :nowrap="false" style="line-height: 24px;"/> | 				<MkUserName :user="u" :nowrap="false" style="line-height: 24px;"/> | ||||||
| 			</b> | 			</b> | ||||||
| 			<span slot="omitted">+{{ count - 10 }}</span> | 			<span slot="omitted">+{{ count - 10 }}</span> | ||||||
| 		</template> | 		</template> | ||||||
| </mk-tooltip> | 	</div> | ||||||
|  | </MkTooltip> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import MkTooltip from './ui/tooltip.vue'; | import MkTooltip from './ui/tooltip.vue'; | ||||||
|  | import XReactionIcon from './reaction-icon.vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkTooltip | 		MkTooltip, | ||||||
|  | 		XReactionIcon | ||||||
| 	}, | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		reaction: { | 		reaction: { | ||||||
|  | @ -37,15 +45,30 @@ export default Vue.extend({ | ||||||
| 			type: Number, | 			type: Number, | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
|  | 		emojis: { | ||||||
|  | 			type: Array, | ||||||
|  | 			required: true, | ||||||
|  | 		}, | ||||||
| 		source: { | 		source: { | ||||||
| 			required: true, | 			required: true, | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 	emits: ['closed'], | ||||||
| 	methods: { |  | ||||||
| 		close() { |  | ||||||
| 			this.$refs.tooltip.close(); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }) | }) | ||||||
| </script> | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .bqxuuuey { | ||||||
|  | 	> .info { | ||||||
|  | 		padding: 0 0 8px 0; | ||||||
|  | 		text-align: center; | ||||||
|  | 
 | ||||||
|  | 		> .icon { | ||||||
|  | 			display: block; | ||||||
|  | 			width: 60px; | ||||||
|  | 			height: 60px; | ||||||
|  | 			margin: 0 auto; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  |  | ||||||
|  | @ -4,24 +4,25 @@ | ||||||
| 	:class="{ reacted: note.myReaction == reaction, canToggle }" | 	:class="{ reacted: note.myReaction == reaction, canToggle }" | ||||||
| 	@click="toggleReaction(reaction)" | 	@click="toggleReaction(reaction)" | ||||||
| 	v-if="count > 0" | 	v-if="count > 0" | ||||||
| 	@touchstart="onMouseover" | 	@touchstart.passive="onMouseover" | ||||||
| 	@mouseover="onMouseover" | 	@mouseover="onMouseover" | ||||||
| 	@mouseleave="onMouseleave" | 	@mouseleave="onMouseleave" | ||||||
| 	@touchend="onMouseleave" | 	@touchend="onMouseleave" | ||||||
| 	ref="reaction" | 	ref="reaction" | ||||||
| 	v-particle="canToggle" | 	v-particle="canToggle" | ||||||
| > | > | ||||||
| 	<x-reaction-icon :reaction="reaction" :custom-emojis="note.emojis" ref="icon"/> | 	<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/> | ||||||
| 	<span>{{ count }}</span> | 	<span>{{ count }}</span> | ||||||
| </button> | </button> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import { defineComponent, ref } from 'vue'; | ||||||
| import XDetails from './reactions-viewer.details.vue'; | import XDetails from '@/components/reactions-viewer.details.vue'; | ||||||
| import XReactionIcon from './reaction-icon.vue'; | import XReactionIcon from '@/components/reaction-icon.vue'; | ||||||
|  | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XReactionIcon | 		XReactionIcon | ||||||
| 	}, | 	}, | ||||||
|  | @ -45,7 +46,7 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			details: null, | 			close: null, | ||||||
| 			detailsTimeoutId: null, | 			detailsTimeoutId: null, | ||||||
| 			isHovering: false | 			isHovering: false | ||||||
| 		}; | 		}; | ||||||
|  | @ -58,7 +59,7 @@ export default Vue.extend({ | ||||||
| 	watch: { | 	watch: { | ||||||
| 		count(newCount, oldCount) { | 		count(newCount, oldCount) { | ||||||
| 			if (oldCount < newCount) this.anime(); | 			if (oldCount < newCount) this.anime(); | ||||||
| 			if (this.details != null) this.openDetails(); | 			if (this.close != null) this.openDetails(); | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	mounted() { | 	mounted() { | ||||||
|  | @ -70,18 +71,18 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 			const oldReaction = this.note.myReaction; | 			const oldReaction = this.note.myReaction; | ||||||
| 			if (oldReaction) { | 			if (oldReaction) { | ||||||
| 				this.$root.api('notes/reactions/delete', { | 				os.api('notes/reactions/delete', { | ||||||
| 					noteId: this.note.id | 					noteId: this.note.id | ||||||
| 				}).then(() => { | 				}).then(() => { | ||||||
| 					if (oldReaction !== this.reaction) { | 					if (oldReaction !== this.reaction) { | ||||||
| 						this.$root.api('notes/reactions/create', { | 						os.api('notes/reactions/create', { | ||||||
| 							noteId: this.note.id, | 							noteId: this.note.id, | ||||||
| 							reaction: this.reaction | 							reaction: this.reaction | ||||||
| 						}); | 						}); | ||||||
| 					} | 					} | ||||||
| 				}); | 				}); | ||||||
| 			} else { | 			} else { | ||||||
| 				this.$root.api('notes/reactions/create', { | 				os.api('notes/reactions/create', { | ||||||
| 					noteId: this.note.id, | 					noteId: this.note.id, | ||||||
| 					reaction: this.reaction | 					reaction: this.reaction | ||||||
| 				}); | 				}); | ||||||
|  | @ -99,7 +100,7 @@ export default Vue.extend({ | ||||||
| 			this.closeDetails(); | 			this.closeDetails(); | ||||||
| 		}, | 		}, | ||||||
| 		openDetails() { | 		openDetails() { | ||||||
| 			this.$root.api('notes/reactions', { | 			os.api('notes/reactions', { | ||||||
| 				noteId: this.note.id, | 				noteId: this.note.id, | ||||||
| 				type: this.reaction, | 				type: this.reaction, | ||||||
| 				limit: 11 | 				limit: 11 | ||||||
|  | @ -110,18 +111,26 @@ export default Vue.extend({ | ||||||
| 
 | 
 | ||||||
| 				this.closeDetails(); | 				this.closeDetails(); | ||||||
| 				if (!this.isHovering) return; | 				if (!this.isHovering) return; | ||||||
| 				this.details = this.$root.new(XDetails, { | 
 | ||||||
|  | 				const showing = ref(true); | ||||||
|  | 				os.popup(XDetails, { | ||||||
|  | 					showing, | ||||||
| 					reaction: this.reaction, | 					reaction: this.reaction, | ||||||
|  | 					emojis: this.note.emojis, | ||||||
| 					users, | 					users, | ||||||
| 					count: this.count, | 					count: this.count, | ||||||
| 					source: this.$refs.reaction | 					source: this.$refs.reaction | ||||||
| 				}); | 				}, {}, 'closed'); | ||||||
|  | 
 | ||||||
|  | 				this.close = () => { | ||||||
|  | 					showing.value = false; | ||||||
|  | 				}; | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 		closeDetails() { | 		closeDetails() { | ||||||
| 			if (this.details != null) { | 			if (this.close != null) { | ||||||
| 				this.details.close(); | 				this.close(); | ||||||
| 				this.details = null; | 				this.close = null; | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		anime() { | 		anime() { | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		
		Reference in a new issue