Merge branch 'develop' into nav-animation
This commit is contained in:
		
						commit
						06d33ec520
					
				
					 53 changed files with 2297 additions and 1133 deletions
				
			
		|  | @ -716,6 +716,12 @@ goBack: "戻る" | ||||||
| unlikeConfirm: "いいね解除しますか?" | unlikeConfirm: "いいね解除しますか?" | ||||||
| fullView: "フルビュー" | fullView: "フルビュー" | ||||||
| quitFullView: "フルビュー解除" | quitFullView: "フルビュー解除" | ||||||
|  | addDescription: "説明を追加" | ||||||
|  | userPagePinTip: "個々のノートのメニューから「ピン留め」を選択することで、ここにノートを表示しておくことができます。" | ||||||
|  | notSpecifiedMentionWarning: "宛先に含まれていないメンションがあります" | ||||||
|  | info: "情報" | ||||||
|  | userInfo: "ユーザー情報" | ||||||
|  | unknown: "不明" | ||||||
| 
 | 
 | ||||||
| _email: | _email: | ||||||
|   _follow: |   _follow: | ||||||
|  | @ -884,6 +890,7 @@ _theme: | ||||||
|   install: "テーマのインストール" |   install: "テーマのインストール" | ||||||
|   manage: "テーマの管理" |   manage: "テーマの管理" | ||||||
|   code: "テーマコード" |   code: "テーマコード" | ||||||
|  |   description: "説明" | ||||||
|   installed: "{name}をインストールしました" |   installed: "{name}をインストールしました" | ||||||
|   installedThemes: "インストールされたテーマ" |   installedThemes: "インストールされたテーマ" | ||||||
|   builtinThemes: "標準のテーマ" |   builtinThemes: "標準のテーマ" | ||||||
|  |  | ||||||
							
								
								
									
										94
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										94
									
								
								package.json
									
										
									
									
									
								
							|  | @ -1,7 +1,7 @@ | ||||||
| { | { | ||||||
| 	"name": "misskey", | 	"name": "misskey", | ||||||
| 	"author": "syuilo <syuilotan@yahoo.co.jp>", | 	"author": "syuilo <syuilotan@yahoo.co.jp>", | ||||||
| 	"version": "12.75.1", | 	"version": "12.76.1-beta.1", | ||||||
| 	"codename": "indigo", | 	"codename": "indigo", | ||||||
| 	"repository": { | 	"repository": { | ||||||
| 		"type": "git", | 		"type": "git", | ||||||
|  | @ -37,7 +37,7 @@ | ||||||
| 		"lodash": "^4.17.20" | 		"lodash": "^4.17.20" | ||||||
| 	}, | 	}, | ||||||
| 	"dependencies": { | 	"dependencies": { | ||||||
| 		"@babel/plugin-transform-runtime": "7.13.10", | 		"@babel/plugin-transform-runtime": "7.13.15", | ||||||
| 		"@elastic/elasticsearch": "7.11.0", | 		"@elastic/elasticsearch": "7.11.0", | ||||||
| 		"@fortawesome/fontawesome-svg-core": "1.2.35", | 		"@fortawesome/fontawesome-svg-core": "1.2.35", | ||||||
| 		"@fortawesome/free-brands-svg-icons": "5.15.3", | 		"@fortawesome/free-brands-svg-icons": "5.15.3", | ||||||
|  | @ -49,7 +49,7 @@ | ||||||
| 		"@koa/router": "9.0.1", | 		"@koa/router": "9.0.1", | ||||||
| 		"@sentry/browser": "5.29.2", | 		"@sentry/browser": "5.29.2", | ||||||
| 		"@sentry/tracing": "5.29.2", | 		"@sentry/tracing": "5.29.2", | ||||||
| 		"@sinonjs/fake-timers": "7.0.2", | 		"@sinonjs/fake-timers": "7.0.5", | ||||||
| 		"@syuilo/aiscript": "0.11.1", | 		"@syuilo/aiscript": "0.11.1", | ||||||
| 		"@types/bcryptjs": "2.4.2", | 		"@types/bcryptjs": "2.4.2", | ||||||
| 		"@types/bull": "3.15.0", | 		"@types/bull": "3.15.0", | ||||||
|  | @ -62,7 +62,7 @@ | ||||||
| 		"@types/gulp-replace": "0.0.31", | 		"@types/gulp-replace": "0.0.31", | ||||||
| 		"@types/is-url": "1.2.28", | 		"@types/is-url": "1.2.28", | ||||||
| 		"@types/js-yaml": "4.0.0", | 		"@types/js-yaml": "4.0.0", | ||||||
| 		"@types/jsdom": "16.2.7", | 		"@types/jsdom": "16.2.10", | ||||||
| 		"@types/jsonld": "1.5.5", | 		"@types/jsonld": "1.5.5", | ||||||
| 		"@types/katex": "0.11.0", | 		"@types/katex": "0.11.0", | ||||||
| 		"@types/koa": "2.13.1", | 		"@types/koa": "2.13.1", | ||||||
|  | @ -77,10 +77,10 @@ | ||||||
| 		"@types/koa__multer": "2.0.2", | 		"@types/koa__multer": "2.0.2", | ||||||
| 		"@types/koa__router": "8.0.4", | 		"@types/koa__router": "8.0.4", | ||||||
| 		"@types/markdown-it": "12.0.1", | 		"@types/markdown-it": "12.0.1", | ||||||
| 		"@types/matter-js": "0.14.10", | 		"@types/matter-js": "0.14.11", | ||||||
| 		"@types/mocha": "8.2.1", | 		"@types/mocha": "8.2.2", | ||||||
| 		"@types/node": "14.14.35", | 		"@types/node": "14.14.41", | ||||||
| 		"@types/node-fetch": "2.5.8", | 		"@types/node-fetch": "2.5.10", | ||||||
| 		"@types/nodemailer": "6.4.1", | 		"@types/nodemailer": "6.4.1", | ||||||
| 		"@types/nprogress": "0.2.0", | 		"@types/nprogress": "0.2.0", | ||||||
| 		"@types/oauth": "0.9.1", | 		"@types/oauth": "0.9.1", | ||||||
|  | @ -97,7 +97,7 @@ | ||||||
| 		"@types/request-stats": "3.0.0", | 		"@types/request-stats": "3.0.0", | ||||||
| 		"@types/rimraf": "3.0.0", | 		"@types/rimraf": "3.0.0", | ||||||
| 		"@types/seedrandom": "2.4.28", | 		"@types/seedrandom": "2.4.28", | ||||||
| 		"@types/sharp": "0.27.1", | 		"@types/sharp": "0.28.0", | ||||||
| 		"@types/sinonjs__fake-timers": "6.0.2", | 		"@types/sinonjs__fake-timers": "6.0.2", | ||||||
| 		"@types/speakeasy": "2.0.5", | 		"@types/speakeasy": "2.0.5", | ||||||
| 		"@types/throttle-debounce": "2.1.0", | 		"@types/throttle-debounce": "2.1.0", | ||||||
|  | @ -105,39 +105,39 @@ | ||||||
| 		"@types/tmp": "0.2.0", | 		"@types/tmp": "0.2.0", | ||||||
| 		"@types/uuid": "8.3.0", | 		"@types/uuid": "8.3.0", | ||||||
| 		"@types/web-push": "3.3.0", | 		"@types/web-push": "3.3.0", | ||||||
| 		"@types/webpack": "4.41.26", | 		"@types/webpack": "5.28.0", | ||||||
| 		"@types/webpack-stream": "3.2.11", | 		"@types/webpack-stream": "3.2.12", | ||||||
| 		"@types/websocket": "1.0.2", | 		"@types/websocket": "1.0.2", | ||||||
| 		"@types/ws": "7.4.0", | 		"@types/ws": "7.4.1", | ||||||
| 		"@typescript-eslint/parser": "4.18.0", | 		"@typescript-eslint/parser": "4.22.0", | ||||||
| 		"@vue/compiler-sfc": "3.0.8", | 		"@vue/compiler-sfc": "3.0.11", | ||||||
| 		"abort-controller": "3.0.0", | 		"abort-controller": "3.0.0", | ||||||
| 		"apexcharts": "3.26.0", | 		"apexcharts": "3.26.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.867.0", | 		"aws-sdk": "2.887.0", | ||||||
| 		"bcryptjs": "2.4.3", | 		"bcryptjs": "2.4.3", | ||||||
| 		"blurhash": "1.1.3", | 		"blurhash": "1.1.3", | ||||||
| 		"broadcast-channel": "3.5.3", | 		"broadcast-channel": "3.5.3", | ||||||
| 		"bull": "3.21.1", | 		"bull": "3.22.0", | ||||||
| 		"cafy": "15.2.1", | 		"cafy": "15.2.1", | ||||||
| 		"cbor": "7.0.4", | 		"cbor": "7.0.5", | ||||||
| 		"chalk": "4.1.0", | 		"chalk": "4.1.0", | ||||||
| 		"chart.js": "2.9.4", | 		"chart.js": "2.9.4", | ||||||
| 		"cli-highlight": "2.1.10", | 		"cli-highlight": "2.1.11", | ||||||
| 		"commander": "4.1.1", | 		"commander": "7.2.0", | ||||||
| 		"concurrently": "6.0.0", | 		"concurrently": "6.0.2", | ||||||
| 		"content-disposition": "0.5.3", | 		"content-disposition": "0.5.3", | ||||||
| 		"core-js": "3.9.1", | 		"core-js": "3.10.1", | ||||||
| 		"crc-32": "1.2.0", | 		"crc-32": "1.2.0", | ||||||
| 		"css-loader": "5.1.3", | 		"css-loader": "5.2.1", | ||||||
| 		"cssnano": "4.1.10", | 		"cssnano": "5.0.1", | ||||||
| 		"dateformat": "4.5.1", | 		"dateformat": "4.5.1", | ||||||
| 		"diskusage": "1.1.3", | 		"diskusage": "1.1.3", | ||||||
| 		"escape-regexp": "0.0.1", | 		"escape-regexp": "0.0.1", | ||||||
| 		"eslint": "7.22.0", | 		"eslint": "7.24.0", | ||||||
| 		"eslint-plugin-vue": "7.7.0", | 		"eslint-plugin-vue": "7.9.0", | ||||||
| 		"eventemitter3": "4.0.7", | 		"eventemitter3": "4.0.7", | ||||||
| 		"feed": "4.2.2", | 		"feed": "4.2.2", | ||||||
| 		"fibers": "5.0.0", | 		"fibers": "5.0.0", | ||||||
|  | @ -156,17 +156,17 @@ | ||||||
| 		"http-proxy-agent": "4.0.1", | 		"http-proxy-agent": "4.0.1", | ||||||
| 		"http-signature": "1.3.5", | 		"http-signature": "1.3.5", | ||||||
| 		"https-proxy-agent": "5.0.0", | 		"https-proxy-agent": "5.0.0", | ||||||
| 		"idb-keyval": "5.0.4", | 		"idb-keyval": "5.0.5", | ||||||
| 		"insert-text-at-cursor": "0.3.0", | 		"insert-text-at-cursor": "0.3.0", | ||||||
| 		"is-root": "2.1.0", | 		"is-root": "2.1.0", | ||||||
| 		"is-svg": "4.3.1", | 		"is-svg": "4.3.1", | ||||||
| 		"js-yaml": "4.0.0", | 		"js-yaml": "4.1.0", | ||||||
| 		"jsdom": "16.5.1", | 		"jsdom": "16.5.3", | ||||||
| 		"json5": "2.2.0", | 		"json5": "2.2.0", | ||||||
| 		"json5-loader": "4.0.1", | 		"json5-loader": "4.0.1", | ||||||
| 		"jsonld": "4.0.1", | 		"jsonld": "4.0.1", | ||||||
| 		"jsrsasign": "8.0.20", | 		"jsrsasign": "8.0.20", | ||||||
| 		"katex": "0.13.0", | 		"katex": "0.13.2", | ||||||
| 		"koa": "2.13.1", | 		"koa": "2.13.1", | ||||||
| 		"koa-bodyparser": "4.3.0", | 		"koa-bodyparser": "4.3.0", | ||||||
| 		"koa-favicon": "2.1.0", | 		"koa-favicon": "2.1.0", | ||||||
|  | @ -178,10 +178,10 @@ | ||||||
| 		"koa-views": "7.0.1", | 		"koa-views": "7.0.1", | ||||||
| 		"langmap": "0.0.16", | 		"langmap": "0.0.16", | ||||||
| 		"lookup-dns-cache": "2.1.0", | 		"lookup-dns-cache": "2.1.0", | ||||||
| 		"markdown-it": "12.0.4", | 		"markdown-it": "12.0.5", | ||||||
| 		"markdown-it-anchor": "7.1.0", | 		"markdown-it-anchor": "7.1.0", | ||||||
| 		"matter-js": "0.16.1", | 		"matter-js": "0.17.1", | ||||||
| 		"mfm-js": "0.14.0", | 		"mfm-js": "0.15.0", | ||||||
| 		"mocha": "8.3.2", | 		"mocha": "8.3.2", | ||||||
| 		"moji": "0.5.1", | 		"moji": "0.5.1", | ||||||
| 		"ms": "2.1.3", | 		"ms": "2.1.3", | ||||||
|  | @ -192,23 +192,23 @@ | ||||||
| 		"object-assign-deep": "0.4.0", | 		"object-assign-deep": "0.4.0", | ||||||
| 		"os-utils": "0.0.14", | 		"os-utils": "0.0.14", | ||||||
| 		"parse5": "6.0.1", | 		"parse5": "6.0.1", | ||||||
| 		"pg": "8.5.1", | 		"pg": "8.6.0", | ||||||
| 		"portscanner": "2.2.0", | 		"portscanner": "2.2.0", | ||||||
| 		"postcss": "8.2.8", | 		"postcss": "8.2.10", | ||||||
| 		"postcss-loader": "5.2.0", | 		"postcss-loader": "5.2.0", | ||||||
| 		"prismjs": "1.23.0", | 		"prismjs": "1.23.0", | ||||||
| 		"probe-image-size": "7.0.1", | 		"probe-image-size": "7.1.0", | ||||||
| 		"promise-limit": "2.7.0", | 		"promise-limit": "2.7.0", | ||||||
| 		"promise-sequential": "1.1.1", | 		"promise-sequential": "1.1.1", | ||||||
| 		"pug": "3.0.2", | 		"pug": "3.0.2", | ||||||
| 		"punycode": "2.1.1", | 		"punycode": "2.1.1", | ||||||
| 		"pureimage": "0.2.7", | 		"pureimage": "0.3.2", | ||||||
| 		"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.9", | 		"re2": "1.15.9", | ||||||
| 		"reconnecting-websocket": "4.4.0", | 		"reconnecting-websocket": "4.4.0", | ||||||
| 		"redis": "3.0.2", | 		"redis": "3.1.1", | ||||||
| 		"redis-lock": "0.1.4", | 		"redis-lock": "0.1.4", | ||||||
| 		"reflect-metadata": "0.1.13", | 		"reflect-metadata": "0.1.13", | ||||||
| 		"regenerator-runtime": "0.13.7", | 		"regenerator-runtime": "0.13.7", | ||||||
|  | @ -221,32 +221,32 @@ | ||||||
| 		"sass": "1.32.8", | 		"sass": "1.32.8", | ||||||
| 		"sass-loader": "11.0.1", | 		"sass-loader": "11.0.1", | ||||||
| 		"seedrandom": "3.0.5", | 		"seedrandom": "3.0.5", | ||||||
| 		"sharp": "0.27.2", | 		"sharp": "0.28.1", | ||||||
| 		"speakeasy": "2.0.0", | 		"speakeasy": "2.0.0", | ||||||
| 		"stringz": "2.1.0", | 		"stringz": "2.1.0", | ||||||
| 		"style-loader": "2.0.0", | 		"style-loader": "2.0.0", | ||||||
| 		"summaly": "2.4.0", | 		"summaly": "2.4.0", | ||||||
| 		"syslog-pro": "1.0.0", | 		"syslog-pro": "1.0.0", | ||||||
| 		"systeminformation": "5.6.7", | 		"systeminformation": "5.6.12", | ||||||
| 		"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", | ||||||
| 		"throttle-debounce": "3.0.1", | 		"throttle-debounce": "3.0.1", | ||||||
| 		"tinycolor2": "1.4.2", | 		"tinycolor2": "1.4.2", | ||||||
| 		"tmp": "0.2.1", | 		"tmp": "0.2.1", | ||||||
| 		"ts-loader": "8.0.18", | 		"ts-loader": "8.1.0", | ||||||
| 		"ts-node": "9.1.1", | 		"ts-node": "9.1.1", | ||||||
| 		"tsc-alias": "1.2.8", | 		"tsc-alias": "1.2.9", | ||||||
| 		"tsconfig-paths": "3.9.0", | 		"tsconfig-paths": "3.9.0", | ||||||
| 		"tslint": "6.1.3", | 		"tslint": "6.1.3", | ||||||
| 		"tslint-sonarts": "1.9.0", | 		"tslint-sonarts": "1.9.0", | ||||||
| 		"typeorm": "0.2.31", | 		"typeorm": "0.2.32", | ||||||
| 		"typescript": "4.2.3", | 		"typescript": "4.2.4", | ||||||
| 		"ulid": "2.3.0", | 		"ulid": "2.3.0", | ||||||
| 		"uuid": "8.3.2", | 		"uuid": "8.3.2", | ||||||
| 		"v-debounce": "0.1.2", | 		"v-debounce": "0.1.2", | ||||||
| 		"vanilla-tilt": "1.7.0", | 		"vanilla-tilt": "1.7.0", | ||||||
| 		"vue": "3.0.8", | 		"vue": "3.0.11", | ||||||
| 		"vue-color": "2.8.1", | 		"vue-color": "2.8.1", | ||||||
| 		"vue-json-pretty": "1.7.1", | 		"vue-json-pretty": "1.7.1", | ||||||
| 		"vue-loader": "16.1.2", | 		"vue-loader": "16.1.2", | ||||||
|  | @ -256,9 +256,9 @@ | ||||||
| 		"vue-svg-loader": "0.17.0-beta.2", | 		"vue-svg-loader": "0.17.0-beta.2", | ||||||
| 		"vuedraggable": "4.0.1", | 		"vuedraggable": "4.0.1", | ||||||
| 		"web-push": "3.4.4", | 		"web-push": "3.4.4", | ||||||
| 		"webpack": "5.27.2", | 		"webpack": "5.33.2", | ||||||
| 		"webpack-cli": "4.5.0", | 		"webpack-cli": "4.6.0", | ||||||
| 		"websocket": "1.0.33", | 		"websocket": "1.0.34", | ||||||
| 		"ws": "7.4.4", | 		"ws": "7.4.4", | ||||||
| 		"xev": "2.0.1" | 		"xev": "2.0.1" | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| import * as program from 'commander'; | import { Command } from 'commander'; | ||||||
| import config from '@/config'; | import config from '@/config'; | ||||||
| 
 | 
 | ||||||
|  | const program = new Command(); | ||||||
|  | 
 | ||||||
| program | program | ||||||
| 	.version(config.version) | 	.version(config.version) | ||||||
| 	.option('--no-daemons', 'Disable daemon processes (for debbuging)') | 	.option('--no-daemons', 'Disable daemon processes (for debbuging)') | ||||||
|  |  | ||||||
|  | @ -18,17 +18,20 @@ export default defineComponent({ | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
| 		} | 		}, | ||||||
|  | 		noGap: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		focus() { | 		focus() { | ||||||
| 			this.$slots.default[0].elm.focus(); | 			this.$slots.default[0].elm.focus(); | ||||||
| 		} | 		}, | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	render() { | 		getDateText(time: string) { | ||||||
| 		const getDateText = (time: string) => { |  | ||||||
| 			const date = new Date(time).getDate(); | 			const date = new Date(time).getDate(); | ||||||
| 			const month = new Date(time).getMonth() + 1; | 			const month = new Date(time).getMonth() + 1; | ||||||
| 			return this.$t('monthAndDay', { | 			return this.$t('monthAndDay', { | ||||||
|  | @ -36,17 +39,19 @@ export default defineComponent({ | ||||||
| 				day: date.toString() | 				day: date.toString() | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
|  | 	}, | ||||||
| 
 | 
 | ||||||
| 		const noGap = [...document.querySelectorAll('._noGap_')].some(el => el.contains(this.$parent.$el)); | 	render() { | ||||||
|  | 		if (this.items.length === 0) return; | ||||||
| 
 | 
 | ||||||
| 		return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? { | 		return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? { | ||||||
| 			class: 'sqadhkmv' + (noGap ? ' _block' : ''), | 			class: 'sqadhkmv' + (this.noGap ? ' noGap _block' : ''), | ||||||
| 			name: 'list', | 			name: 'list', | ||||||
| 			tag: 'div', | 			tag: 'div', | ||||||
| 			'data-direction': this.direction, | 			'data-direction': this.direction, | ||||||
| 			'data-reversed': this.reversed ? 'true' : 'false', | 			'data-reversed': this.reversed ? 'true' : 'false', | ||||||
| 		} : { | 		} : { | ||||||
| 			class: 'sqadhkmv', | 			class: 'sqadhkmv' + (this.noGap ? ' noGap _block' : ''), | ||||||
| 		}, this.items.map((item, i) => { | 		}, this.items.map((item, i) => { | ||||||
| 			const el = this.$slots.default({ | 			const el = this.$slots.default({ | ||||||
| 				item: item | 				item: item | ||||||
|  | @ -72,10 +77,10 @@ export default defineComponent({ | ||||||
| 							class: 'icon', | 							class: 'icon', | ||||||
| 							icon: faAngleUp, | 							icon: faAngleUp, | ||||||
| 						}), | 						}), | ||||||
| 						getDateText(item.createdAt) | 						this.getDateText(item.createdAt) | ||||||
| 					]), | 					]), | ||||||
| 					h('span', [ | 					h('span', [ | ||||||
| 						getDateText(this.items[i + 1].createdAt), | 						this.getDateText(this.items[i + 1].createdAt), | ||||||
| 						h(FontAwesomeIcon, { | 						h(FontAwesomeIcon, { | ||||||
| 							class: 'icon', | 							class: 'icon', | ||||||
| 							icon: faAngleDown, | 							icon: faAngleDown, | ||||||
|  | @ -152,17 +157,17 @@ export default defineComponent({ | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| ._noGap_ .sqadhkmv { | 	&.noGap { | ||||||
| 	> * { | 		> * { | ||||||
| 		margin: 0 !important; | 			margin: 0 !important; | ||||||
| 		border: none; | 			border: none; | ||||||
| 		border-radius: 0; | 			border-radius: 0; | ||||||
| 		box-shadow: none; | 			box-shadow: none; | ||||||
| 
 | 
 | ||||||
| 		&:not(:last-child) { | 			&:not(:last-child) { | ||||||
| 			border-bottom: solid 0.5px var(--divider); | 				border-bottom: solid 0.5px var(--divider); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ import { | ||||||
| 	faFileArchive, | 	faFileArchive, | ||||||
| 	faFilm | 	faFilm | ||||||
| 	} from '@fortawesome/free-solid-svg-icons'; | 	} from '@fortawesome/free-solid-svg-icons'; | ||||||
| import ImgWithBlurhash from './img-with-blurhash.vue'; | import ImgWithBlurhash from '@client/components/img-with-blurhash.vue'; | ||||||
| import { ColdDeviceStorage } from '@client/store'; | import { ColdDeviceStorage } from '@client/store'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|  |  | ||||||
|  | @ -24,9 +24,12 @@ export default defineComponent({ | ||||||
| 	--formXPadding: 32px; | 	--formXPadding: 32px; | ||||||
| 	--formYPadding: 32px; | 	--formYPadding: 32px; | ||||||
| 
 | 
 | ||||||
|  | 	font-size: 95%; | ||||||
| 	line-height: 1.3em; | 	line-height: 1.3em; | ||||||
| 	background: var(--bg); | 	background: var(--bg); | ||||||
| 	padding: var(--formYPadding) var(--formXPadding); | 	padding: var(--formYPadding) var(--formXPadding); | ||||||
|  | 	max-width: 750px; | ||||||
|  | 	margin: 0 auto; | ||||||
| 
 | 
 | ||||||
| 	&:not(.wide).max-width_400px { | 	&:not(.wide).max-width_400px { | ||||||
| 		--formXPadding: 0px; | 		--formXPadding: 0px; | ||||||
|  | @ -40,16 +43,16 @@ export default defineComponent({ | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			._form_group { | 			._form_group { | ||||||
| 				> * { | 				> *:not(._formNoConcat) { | ||||||
| 					&:not(:first-child) { | 					&:not(:last-child):not(._formNoConcatPrev) { | ||||||
| 						&._formPanel, ._formPanel { | 						&._formPanel, ._formPanel { | ||||||
| 							border-top: none; | 							border-bottom: solid 0.5px var(--divider); | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					&:not(:last-child) { | 					&:not(:first-child):not(._formNoConcatNext) { | ||||||
| 						&._formPanel, ._formPanel { | 						&._formPanel, ._formPanel { | ||||||
| 							border-bottom: solid 0.5px var(--divider); | 							border-top: none; | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <div class="vrtktovg _formItem" v-size="{ max: [500] }"> | <div class="vrtktovg _formItem _formNoConcat" v-size="{ max: [500] }" v-sticky-container> | ||||||
| 	<div class="_formLabel"><slot name="label"></slot></div> | 	<div class="_formLabel"><slot name="label"></slot></div> | ||||||
| 	<div class="main _form_group"> | 	<div class="main _form_group" ref="child"> | ||||||
| 		<slot></slot> | 		<slot></slot> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_formCaption"><slot name="caption"></slot></div> | 	<div class="_formCaption"><slot name="caption"></slot></div> | ||||||
|  | @ -9,33 +9,69 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent, onMounted, ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|  | 	setup(props, context) { | ||||||
|  | 		const child = ref<HTMLElement | null>(null); | ||||||
|  | 
 | ||||||
|  | 		const scanChild = () => { | ||||||
|  | 			if (child.value == null) return; | ||||||
|  | 			const els = Array.from(child.value.children); | ||||||
|  | 			for (let i = 0; i < els.length; i++) { | ||||||
|  | 				const el = els[i]; | ||||||
|  | 				if (el.classList.contains('_formNoConcat')) { | ||||||
|  | 					if (els[i - 1]) els[i - 1].classList.add('_formNoConcatPrev'); | ||||||
|  | 					if (els[i + 1]) els[i + 1].classList.add('_formNoConcatNext'); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		onMounted(() => { | ||||||
|  | 			scanChild(); | ||||||
|  | 
 | ||||||
|  | 			const observer = new MutationObserver(records => { | ||||||
|  | 				scanChild(); | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			observer.observe(child.value, { | ||||||
|  | 				childList: true, | ||||||
|  | 				subtree: false, | ||||||
|  | 				attributes: false, | ||||||
|  | 				characterData: false, | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			child | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .vrtktovg { | .vrtktovg { | ||||||
| 	> .main { | 	> .main { | ||||||
| 		> ::v-deep(*) { | 		> ::v-deep(*):not(._formNoConcat) { | ||||||
| 			margin: 0; | 			&:not(._formNoConcatNext) { | ||||||
| 
 | 				margin: 0; | ||||||
| 			&:not(:first-child) { |  | ||||||
| 				&._formPanel, ._formPanel { |  | ||||||
| 					border-top: none; |  | ||||||
| 					border-top-left-radius: 0; |  | ||||||
| 					border-top-right-radius: 0; |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			&:not(:last-child) { | 			&:not(:last-child):not(._formNoConcatPrev) { | ||||||
| 				&._formPanel, ._formPanel { | 				&._formPanel, ._formPanel { | ||||||
| 					border-bottom: solid 0.5px var(--divider); | 					border-bottom: solid 0.5px var(--divider); | ||||||
| 					border-bottom-left-radius: 0; | 					border-bottom-left-radius: 0; | ||||||
| 					border-bottom-right-radius: 0; | 					border-bottom-right-radius: 0; | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
|  | 			&:not(:first-child):not(._formNoConcatNext) { | ||||||
|  | 				&._formPanel, ._formPanel { | ||||||
|  | 					border-top: none; | ||||||
|  | 					border-top-left-radius: 0; | ||||||
|  | 					border-top-right-radius: 0; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -22,9 +22,17 @@ export default defineComponent({ | ||||||
| 	align-items: center; | 	align-items: center; | ||||||
| 	padding: 14px 16px; | 	padding: 14px 16px; | ||||||
| 
 | 
 | ||||||
|  | 	> .key { | ||||||
|  | 		margin-right: 12px; | ||||||
|  | 		white-space: nowrap; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	> .value { | 	> .value { | ||||||
| 		margin-left: auto; | 		margin-left: auto; | ||||||
| 		opacity: 0.7; | 		opacity: 0.7; | ||||||
|  | 		text-overflow: ellipsis; | ||||||
|  | 		white-space: nowrap; | ||||||
|  | 		overflow: hidden; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
							
								
								
									
										102
									
								
								src/client/components/form/object-view.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/client/components/form/object-view.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,102 @@ | ||||||
|  | <template> | ||||||
|  | <FormGroup class="_formItem"> | ||||||
|  | 	<template #label><slot></slot></template> | ||||||
|  | 	<div class="drooglns _formItem" :class="{ tall }"> | ||||||
|  | 		<div class="input _formPanel"> | ||||||
|  | 			<textarea class="_monospace" | ||||||
|  | 				v-model="v" | ||||||
|  | 				readonly | ||||||
|  | 				:spellcheck="false" | ||||||
|  | 			></textarea> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | 	<template #caption><slot name="desc"></slot></template> | ||||||
|  | </FormGroup> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, ref, toRefs, watch } from 'vue'; | ||||||
|  | import * as JSON5 from 'json5'; | ||||||
|  | import './form.scss'; | ||||||
|  | import FormGroup from './group.vue'; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		FormGroup, | ||||||
|  | 	}, | ||||||
|  | 	props: { | ||||||
|  | 		value: { | ||||||
|  | 			required: false | ||||||
|  | 		}, | ||||||
|  | 		tall: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		}, | ||||||
|  | 		pre: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		}, | ||||||
|  | 		manualSave: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	setup(props, context) { | ||||||
|  | 		const { value } = toRefs(props); | ||||||
|  | 		const v = ref(''); | ||||||
|  | 
 | ||||||
|  | 		watch(() => value, newValue => { | ||||||
|  | 			v.value = JSON5.stringify(newValue.value, null, '\t'); | ||||||
|  | 		}, { | ||||||
|  | 			immediate: true | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			v, | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .drooglns { | ||||||
|  | 	position: relative; | ||||||
|  | 
 | ||||||
|  | 	> .input { | ||||||
|  | 		position: relative; | ||||||
|  | 	 | ||||||
|  | 		> textarea { | ||||||
|  | 			display: block; | ||||||
|  | 			width: 100%; | ||||||
|  | 			min-width: 100%; | ||||||
|  | 			max-width: 100%; | ||||||
|  | 			min-height: 130px; | ||||||
|  | 			margin: 0; | ||||||
|  | 			padding: 16px; | ||||||
|  | 			box-sizing: border-box; | ||||||
|  | 			font: inherit; | ||||||
|  | 			font-weight: normal; | ||||||
|  | 			font-size: 1em; | ||||||
|  | 			background: transparent; | ||||||
|  | 			border: none; | ||||||
|  | 			border-radius: 0; | ||||||
|  | 			outline: none; | ||||||
|  | 			box-shadow: none; | ||||||
|  | 			color: var(--fg); | ||||||
|  | 			tab-size: 2; | ||||||
|  | 			white-space: pre; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	&.tall { | ||||||
|  | 		> .input { | ||||||
|  | 			> textarea { | ||||||
|  | 				min-height: 200px; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										87
									
								
								src/client/components/form/suspense.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/client/components/form/suspense.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,87 @@ | ||||||
|  | <template> | ||||||
|  | <transition name="fade" mode="out-in"> | ||||||
|  | 	<div class="_formItem" v-if="pending"> | ||||||
|  | 		<div class="_formPanel"> | ||||||
|  | 			<MkLoading/> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | 	<div v-else-if="resolved"> | ||||||
|  | 		<slot :result="result"></slot> | ||||||
|  | 	</div> | ||||||
|  | 	<div class="_formItem" v-else> | ||||||
|  | 		<div class="_formPanel"> | ||||||
|  | 			error! | ||||||
|  | 			<button @click="retry">retry</button> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | </transition> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, PropType, ref, watch } from 'vue'; | ||||||
|  | import './form.scss'; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  | 	props: { | ||||||
|  | 		p: { | ||||||
|  | 			type: Function as PropType<() => Promise<any>>, | ||||||
|  | 			required: true, | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	setup(props, context) { | ||||||
|  | 		const pending = ref(true); | ||||||
|  | 		const resolved = ref(false); | ||||||
|  | 		const rejected = ref(false); | ||||||
|  | 		const result = ref(null); | ||||||
|  | 
 | ||||||
|  | 		const process = () => { | ||||||
|  | 			if (props.p == null) { | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			const promise = props.p(); | ||||||
|  | 			pending.value = true; | ||||||
|  | 			resolved.value = false; | ||||||
|  | 			rejected.value = false; | ||||||
|  | 			promise.then((_result) => { | ||||||
|  | 				pending.value = false; | ||||||
|  | 				resolved.value = true; | ||||||
|  | 				result.value = _result; | ||||||
|  | 			}); | ||||||
|  | 			promise.catch(() => { | ||||||
|  | 				pending.value = false; | ||||||
|  | 				rejected.value = true; | ||||||
|  | 			}); | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		watch(() => props.p, () => { | ||||||
|  | 			process(); | ||||||
|  | 		}, { | ||||||
|  | 			immediate: true | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		const retry = () => { | ||||||
|  | 			process(); | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			pending, | ||||||
|  | 			resolved, | ||||||
|  | 			rejected, | ||||||
|  | 			result, | ||||||
|  | 			retry, | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .fade-enter-active, | ||||||
|  | .fade-leave-active { | ||||||
|  | 	transition: opacity 0.125s ease; | ||||||
|  | } | ||||||
|  | .fade-enter-from, | ||||||
|  | .fade-leave-to { | ||||||
|  | 	opacity: 0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -1,12 +1,11 @@ | ||||||
| <template> | <template> | ||||||
| <div class="yxspomdl" :class="{ inline }"> | <div class="yxspomdl" :class="{ inline, colored }"> | ||||||
| 	<div class="ring"></div> | 	<div class="ring"></div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import * as os from '@client/os'; |  | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -14,6 +13,11 @@ export default defineComponent({ | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
|  | 		}, | ||||||
|  | 		colored: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | @ -32,6 +36,11 @@ export default defineComponent({ | ||||||
| .yxspomdl { | .yxspomdl { | ||||||
| 	padding: 32px; | 	padding: 32px; | ||||||
| 	text-align: center; | 	text-align: center; | ||||||
|  | 	cursor: wait; | ||||||
|  | 
 | ||||||
|  | 	&.colored { | ||||||
|  | 		color: var(--accent); | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	&.inline { | 	&.inline { | ||||||
| 		display: inline; | 		display: inline; | ||||||
|  | @ -41,24 +50,43 @@ export default defineComponent({ | ||||||
| 			width: 32px; | 			width: 32px; | ||||||
| 			height: 32px; | 			height: 32px; | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		> .ring { | ||||||
|  | 			&:before, | ||||||
|  | 			&:after { | ||||||
|  | 				width: 32px; | ||||||
|  | 				height: 32px; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> .ring { | 	> .ring { | ||||||
|  | 		position: relative; | ||||||
| 		display: inline-block; | 		display: inline-block; | ||||||
| 		opacity: 0.7; |  | ||||||
| 		vertical-align: middle; | 		vertical-align: middle; | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	> .ring:after { | 		&:before, | ||||||
| 		content: " "; | 		&:after { | ||||||
| 		display: block; | 			content: " "; | ||||||
| 		box-sizing: border-box; | 			display: block; | ||||||
| 		width: 48px; | 			box-sizing: border-box; | ||||||
| 		height: 48px; | 			width: 48px; | ||||||
| 		border-radius: 50%; | 			height: 48px; | ||||||
| 		border: solid 4px; | 			border-radius: 50%; | ||||||
| 		border-color: currentColor transparent transparent transparent; | 			border: solid 4px; | ||||||
| 		animation: ring 0.5s linear infinite; | 		} | ||||||
|  | 
 | ||||||
|  | 		&:before { | ||||||
|  | 			border-color: currentColor; | ||||||
|  | 			opacity: 0.3; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		&:after { | ||||||
|  | 			position: absolute; | ||||||
|  | 			top: 0; | ||||||
|  | 			border-color: currentColor transparent transparent transparent; | ||||||
|  | 			animation: ring 0.5s linear infinite; | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -71,6 +71,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .xubzgfgb { | .xubzgfgb { | ||||||
|  | 	position: relative; | ||||||
| 	width: 100%; | 	width: 100%; | ||||||
| 	height: 100%; | 	height: 100%; | ||||||
| 
 | 
 | ||||||
|  | @ -82,6 +83,7 @@ export default defineComponent({ | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> canvas { | 	> canvas { | ||||||
|  | 		position: absolute; | ||||||
| 		object-fit: cover; | 		object-fit: cover; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-i | ||||||
| import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; | import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; | ||||||
| import { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash'; | import { extractAvgColorFromBlurhash } from '@client/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 '@client/components/img-with-blurhash.vue'; | ||||||
| import * as os from '@client/os'; | import * as os from '@client/os'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|  |  | ||||||
|  | @ -58,10 +58,13 @@ export default defineComponent({ | ||||||
| 					const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); | 					const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); | ||||||
| 
 | 
 | ||||||
| 					if (!this.plain) { | 					if (!this.plain) { | ||||||
| 						const x = text.split('\n') | 						const res = []; | ||||||
| 							.map(t => t == '' ? [h('br')] : [t, h('br')]); | 						for (const t of text.split('\n')) { | ||||||
| 						x[x.length - 1].pop(); | 							res.push(h('br')); | ||||||
| 						return x; | 							res.push(t); | ||||||
|  | 						} | ||||||
|  | 						res.shift(); | ||||||
|  | 						return res; | ||||||
| 					} else { | 					} else { | ||||||
| 						return [text.replace(/\n/g, ' ')]; | 						return [text.replace(/\n/g, ' ')]; | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
|  | @ -1,30 +1,34 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <transition name="fade" mode="out-in"> | ||||||
| 	<div class="_fullinfo" v-if="empty"> | 	<MkLoading v-if="fetching"/> | ||||||
|  | 
 | ||||||
|  | 	<MkError v-else-if="error" @retry="init()"/> | ||||||
|  | 
 | ||||||
|  | 	<div class="_fullinfo" v-else-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>{{ $ts.noNotes }}</div> | 		<div>{{ $ts.noNotes }}</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<MkError v-if="error" @retry="init()"/> | 	<div v-else> | ||||||
|  | 		<div v-show="more && reversed" style="margin-bottom: var(--margin);"> | ||||||
|  | 			<MkButton style="margin: 0 auto;" @click="fetchMoreFeature" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||||
|  | 				<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||||
|  | 				<template v-if="moreFetching"><MkLoading inline/></template> | ||||||
|  | 			</MkButton> | ||||||
|  | 		</div> | ||||||
| 
 | 
 | ||||||
| 	<div v-show="more && reversed" style="margin-bottom: var(--margin);"> | 		<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap"> | ||||||
| 		<MkButton style="margin: 0 auto;" @click="fetchMoreFeature" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | 			<XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/> | ||||||
| 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | 		</XList> | ||||||
| 			<template v-if="moreFetching"><MkLoading inline/></template> | 
 | ||||||
| 		</MkButton> | 		<div v-show="more && !reversed" style="margin-top: var(--margin);"> | ||||||
|  | 			<MkButton style="margin: 0 auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||||
|  | 				<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||||
|  | 				<template v-if="moreFetching"><MkLoading inline/></template> | ||||||
|  | 			</MkButton> | ||||||
|  | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | </transition> | ||||||
| 	<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> |  | ||||||
| 		<XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/> |  | ||||||
| 	</XList> |  | ||||||
| 
 |  | ||||||
| 	<div v-show="more && !reversed" style="margin-top: var(--margin);"> |  | ||||||
| 		<MkButton style="margin: 0 auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> |  | ||||||
| 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> |  | ||||||
| 			<template v-if="moreFetching"><MkLoading inline/></template> |  | ||||||
| 		</MkButton> |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -55,11 +59,15 @@ export default defineComponent({ | ||||||
| 		pagination: { | 		pagination: { | ||||||
| 			required: true | 			required: true | ||||||
| 		}, | 		}, | ||||||
| 
 |  | ||||||
| 		prop: { | 		prop: { | ||||||
| 			type: String, | 			type: String, | ||||||
| 			required: false | 			required: false | ||||||
| 		} | 		}, | ||||||
|  | 		noGap: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	emits: ['before', 'after'], | 	emits: ['before', 'after'], | ||||||
|  | @ -90,3 +98,14 @@ export default defineComponent({ | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .fade-enter-active, | ||||||
|  | .fade-leave-active { | ||||||
|  | 	transition: opacity 0.125s ease; | ||||||
|  | } | ||||||
|  | .fade-enter-from, | ||||||
|  | .fade-leave-to { | ||||||
|  | 	opacity: 0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  |  | ||||||
|  | @ -1,20 +1,23 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mfcuwfyp _noGap_"> | <transition name="fade" mode="out-in"> | ||||||
| 	<div class="_magnet"></div> | 	<MkLoading v-if="fetching"/> | ||||||
| 	<XList class="notifications" :items="items" v-slot="{ item: notification }"> |  | ||||||
| 		<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/> |  | ||||||
| 		<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/> |  | ||||||
| 	</XList> |  | ||||||
| 
 | 
 | ||||||
| 	<button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | 	<MkError v-else-if="error" @retry="init()"/> | ||||||
| 		<template v-if="!moreFetching">{{ $ts.loadMore }}</template> |  | ||||||
| 		<template v-if="moreFetching"><MkLoading inline/></template> |  | ||||||
| 	</button> |  | ||||||
| 
 | 
 | ||||||
| 	<p class="empty" v-if="empty">{{ $ts.noNotifications }}</p> | 	<p class="mfcuwfyp" v-else-if="empty">{{ $ts.noNotifications }}</p> | ||||||
| 
 | 
 | ||||||
| 	<MkError v-if="error" @retry="init()"/> | 	<div v-else class="_magnetParent"> | ||||||
| </div> | 		<XList class="notifications _magnetChild" :items="items" v-slot="{ item: notification }" :no-gap="true"> | ||||||
|  | 			<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/> | ||||||
|  | 			<XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/> | ||||||
|  | 		</XList> | ||||||
|  | 
 | ||||||
|  | 		<button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> | ||||||
|  | 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||||
|  | 			<template v-if="moreFetching"><MkLoading inline/></template> | ||||||
|  | 		</button> | ||||||
|  | 	</div> | ||||||
|  | </transition> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -121,17 +124,19 @@ export default defineComponent({ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .mfcuwfyp { | .fade-enter-active, | ||||||
| 	> .empty { | .fade-leave-active { | ||||||
| 		margin: 0; | 	transition: opacity 0.125s ease; | ||||||
| 		padding: 16px; | } | ||||||
| 		text-align: center; | .fade-enter-from, | ||||||
| 		color: var(--fg); | .fade-leave-to { | ||||||
| 	} | 	opacity: 0; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 	> .placeholder { | .mfcuwfyp { | ||||||
| 		padding: 32px; | 	margin: 0; | ||||||
| 		opacity: 0.3; | 	padding: 16px; | ||||||
| 	} | 	text-align: center; | ||||||
|  | 	color: var(--fg); | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -34,6 +34,7 @@ | ||||||
| 				<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> | ||||||
|  | 		<MkInfo warn v-if="hasNotSpecifiedMentions" class="hasNotSpecifiedMentions">{{ $ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ $ts.add }}</button></MkInfo> | ||||||
| 		<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$ts.annotation" @keydown="onKeydown"> | 		<input v-show="useCw" ref="cw" class="cw" v-model="cw" :placeholder="$ts.annotation" @keydown="onKeydown"> | ||||||
| 		<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd" /> | 		<textarea v-model="text" class="text" :class="{ withCw: useCw }" ref="text" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd" /> | ||||||
| 		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> | 		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/> | ||||||
|  | @ -71,12 +72,14 @@ import { selectFile } from '@client/scripts/select-file'; | ||||||
| import { notePostInterruptors, postFormActions } from '@client/store'; | import { notePostInterruptors, postFormActions } from '@client/store'; | ||||||
| import { isMobile } from '@client/scripts/is-mobile'; | import { isMobile } from '@client/scripts/is-mobile'; | ||||||
| import { throttle } from 'throttle-debounce'; | import { throttle } from 'throttle-debounce'; | ||||||
|  | import MkInfo from '@client/components/ui/info.vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XNotePreview, | 		XNotePreview, | ||||||
| 		XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')), | 		XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')), | ||||||
| 		XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')) | 		XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')), | ||||||
|  | 		MkInfo, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	inject: ['modal'], | 	inject: ['modal'], | ||||||
|  | @ -143,6 +146,7 @@ export default defineComponent({ | ||||||
| 			autocomplete: null, | 			autocomplete: null, | ||||||
| 			draghover: false, | 			draghover: false, | ||||||
| 			quoteId: null, | 			quoteId: null, | ||||||
|  | 			hasNotSpecifiedMentions: false, | ||||||
| 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), | 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), | ||||||
| 			imeText: '', | 			imeText: '', | ||||||
| 			typing: throttle(3000, () => { | 			typing: throttle(3000, () => { | ||||||
|  | @ -214,6 +218,18 @@ export default defineComponent({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | 	watch: { | ||||||
|  | 		text() { | ||||||
|  | 			this.checkMissingMention(); | ||||||
|  | 		}, | ||||||
|  | 		visibleUsers: { | ||||||
|  | 			handler() { | ||||||
|  | 				this.checkMissingMention(); | ||||||
|  | 			}, | ||||||
|  | 			deep: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		if (this.initialText) { | 		if (this.initialText) { | ||||||
| 			this.text = this.initialText; | 			this.text = this.initialText; | ||||||
|  | @ -338,6 +354,32 @@ export default defineComponent({ | ||||||
| 			this.$watch('localOnly', () => this.saveDraft()); | 			this.$watch('localOnly', () => this.saveDraft()); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		checkMissingMention() { | ||||||
|  | 			if (this.visibility === 'specified') { | ||||||
|  | 				const ast = mfm.parse(this.text); | ||||||
|  | 
 | ||||||
|  | 				for (const x of extractMentions(ast)) { | ||||||
|  | 					if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) { | ||||||
|  | 						this.hasNotSpecifiedMentions = true; | ||||||
|  | 						return; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				this.hasNotSpecifiedMentions = false; | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		addMissingMention() { | ||||||
|  | 			const ast = mfm.parse(this.text); | ||||||
|  | 
 | ||||||
|  | 			for (const x of extractMentions(ast)) { | ||||||
|  | 				if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) { | ||||||
|  | 					os.api('users/show', { username: x.username, host: x.host }).then(user => { | ||||||
|  | 						this.visibleUsers.push(user); | ||||||
|  | 					}); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		togglePoll() { | 		togglePoll() { | ||||||
| 			if (this.poll) { | 			if (this.poll) { | ||||||
| 				this.poll = null; | 				this.poll = null; | ||||||
|  | @ -741,6 +783,10 @@ export default defineComponent({ | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		> .hasNotSpecifiedMentions { | ||||||
|  | 			margin: 0 20px 16px 20px; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		> .cw, | 		> .cw, | ||||||
| 		> .text { | 		> .text { | ||||||
| 			display: block; | 			display: block; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <template> | ||||||
| <XNotes :class="{ _noGap_: !$store.state.showGapBetweenNotesInTimeline }" ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/> | <XNotes :no-gap="!$store.state.showGapBetweenNotesInTimeline" ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 		<div class="title"><slot name="header"></slot></div> | 		<div class="title"><slot name="header"></slot></div> | ||||||
| 		<div class="sub"> | 		<div class="sub"> | ||||||
| 			<slot name="func"></slot> | 			<slot name="func"></slot> | ||||||
| 			<button class="_button" v-if="bodyTogglable" @click="() => showBody = !showBody"> | 			<button class="_button" v-if="foldable" @click="() => showBody = !showBody"> | ||||||
| 				<template v-if="showBody"><Fa :icon="faAngleUp"/></template> | 				<template v-if="showBody"><Fa :icon="faAngleUp"/></template> | ||||||
| 				<template v-else><Fa :icon="faAngleDown"/></template> | 				<template v-else><Fa :icon="faAngleDown"/></template> | ||||||
| 			</button> | 			</button> | ||||||
|  | @ -16,8 +16,11 @@ | ||||||
| 		@leave="leave" | 		@leave="leave" | ||||||
| 		@after-leave="afterLeave" | 		@after-leave="afterLeave" | ||||||
| 	> | 	> | ||||||
| 		<div v-show="showBody"> | 		<div v-show="showBody" class="content" :class="{ omitted }" ref="content"> | ||||||
| 			<slot></slot> | 			<slot></slot> | ||||||
|  | 			<button v-if="omitted" class="fade _button" @click="() => { ignoreOmit = true; omitted = false; }"> | ||||||
|  | 				<span>{{ $ts.showMore }}</span> | ||||||
|  | 			</button> | ||||||
| 		</div> | 		</div> | ||||||
| 	</transition> | 	</transition> | ||||||
| </div> | </div> | ||||||
|  | @ -39,7 +42,7 @@ export default defineComponent({ | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
| 		}, | 		}, | ||||||
| 		bodyTogglable: { | 		foldable: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
|  | @ -54,10 +57,17 @@ export default defineComponent({ | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
| 		}, | 		}, | ||||||
|  | 		maxHeight: { | ||||||
|  | 			type: Number, | ||||||
|  | 			required: false, | ||||||
|  | 			default: null | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			showBody: this.expanded, | 			showBody: this.expanded, | ||||||
|  | 			omitted: null, | ||||||
|  | 			ignoreOmit: false, | ||||||
| 			faAngleUp, faAngleDown | 			faAngleUp, faAngleDown | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
|  | @ -73,10 +83,23 @@ export default defineComponent({ | ||||||
| 		}, { | 		}, { | ||||||
| 			immediate: true | 			immediate: true | ||||||
| 		}); | 		}); | ||||||
|  | 
 | ||||||
|  | 		this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px'); | ||||||
|  | 
 | ||||||
|  | 		const calcOmit = () => { | ||||||
|  | 			if (this.omitted || this.ignoreOmit || this.maxHeight == null) return; | ||||||
|  | 			const height = this.$refs.content.offsetHeight; | ||||||
|  | 			this.omitted = height > this.maxHeight; | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		calcOmit(); | ||||||
|  | 		new ResizeObserver((entries, observer) => { | ||||||
|  | 			calcOmit(); | ||||||
|  | 		}).observe(this.$refs.content); | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		toggleContent(show: boolean) { | 		toggleContent(show: boolean) { | ||||||
| 			if (!this.bodyTogglable) return; | 			if (!this.foldable) return; | ||||||
| 			this.showBody = show; | 			this.showBody = show; | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | @ -127,7 +150,7 @@ export default defineComponent({ | ||||||
| 		display: flex; | 		display: flex; | ||||||
| 		flex-direction: column; | 		flex-direction: column; | ||||||
| 
 | 
 | ||||||
| 		> div { | 		> .content { | ||||||
| 			overflow: auto; | 			overflow: auto; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -169,12 +192,35 @@ export default defineComponent({ | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> div { | 	> .content { | ||||||
| 		> ::v-deep(._content) { | 		&.omitted { | ||||||
| 			padding: 24px; | 			position: relative; | ||||||
|  | 			max-height: var(--maxHeight); | ||||||
|  | 			overflow: hidden; | ||||||
| 
 | 
 | ||||||
| 			& + ._content { | 			> .fade { | ||||||
| 				border-top: solid 0.5px var(--divider); | 				display: block; | ||||||
|  | 				position: absolute; | ||||||
|  | 				bottom: 0; | ||||||
|  | 				left: 0; | ||||||
|  | 				width: 100%; | ||||||
|  | 				height: 64px; | ||||||
|  | 				background: linear-gradient(0deg, var(--panel), var(--X15)); | ||||||
|  | 
 | ||||||
|  | 				> span { | ||||||
|  | 					display: inline-block; | ||||||
|  | 					background: var(--panel); | ||||||
|  | 					padding: 6px 10px; | ||||||
|  | 					font-size: 0.8em; | ||||||
|  | 					border-radius: 999px; | ||||||
|  | 					box-shadow: 0 2px 6px rgb(0 0 0 / 20%); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				&:hover { | ||||||
|  | 					> span { | ||||||
|  | 						background: var(--panelHighlight); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -187,10 +233,7 @@ export default defineComponent({ | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		> div { | 		> .content { | ||||||
| 			> ::v-deep(._content) { |  | ||||||
| 				padding: 16px; |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ | ||||||
| 
 | 
 | ||||||
| 	<XPostForm :channel="channel" class="post-form _content _panel _gap" fixed v-if="$i"/> | 	<XPostForm :channel="channel" class="post-form _content _panel _gap" fixed v-if="$i"/> | ||||||
| 
 | 
 | ||||||
| 	<XTimeline class="_content _gap _noGap_" src="channel" :key="channelId" :channel="channelId" @before="before" @after="after"/> | 	<XTimeline class="_content _gap" src="channel" :key="channelId" :channel="channelId" @before="before" @after="after"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ | ||||||
| 			<header><span>{{ $ts.exploreFediverse }}</span></header> | 			<header><span>{{ $ts.exploreFediverse }}</span></header> | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
| 		<MkFolder :body-togglable="true" :expanded="false" ref="tags" class="_gap"> | 		<MkFolder :foldable="true" :expanded="false" ref="tags" class="_gap"> | ||||||
| 			<template #header><Fa :icon="faHashtag" fixed-width style="margin-right: 0.5em;"/>{{ $ts.popularTags }}</template> | 			<template #header><Fa :icon="faHashtag" fixed-width style="margin-right: 0.5em;"/>{{ $ts.popularTags }}</template> | ||||||
| 
 | 
 | ||||||
| 			<div class="vxjfqztj"> | 			<div class="vxjfqztj"> | ||||||
|  |  | ||||||
							
								
								
									
										427
									
								
								src/client/pages/instance-info.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										427
									
								
								src/client/pages/instance-info.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,427 @@ | ||||||
|  | <template> | ||||||
|  | <FormBase> | ||||||
|  | 	<FormGroup v-if="instance"> | ||||||
|  | 		<template #label>{{ instance.host }}</template> | ||||||
|  | 		<FormGroup> | ||||||
|  | 			<div class="_formItem"> | ||||||
|  | 				<div class="_formPanel fnfelxur"> | ||||||
|  | 					<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>Name</template> | ||||||
|  | 				<template #value><span class="_monospace">{{ instance.name || `(${$ts.unknown})` }}</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 
 | ||||||
|  | 		<FormGroup> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>{{ $ts.software }}</template> | ||||||
|  | 				<template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }}</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>{{ $ts.version }}</template> | ||||||
|  | 				<template #value><span class="_monospace">{{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 		<FormGroup> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>{{ $ts.administrator }}</template> | ||||||
|  | 				<template #value><span class="_monospace">{{ instance.maintainerName || `(${$ts.unknown})` }}</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>{{ $ts.contact }}</template> | ||||||
|  | 				<template #value><span class="_monospace">{{ instance.maintainerEmail || `(${$ts.unknown})` }}</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 		<FormGroup> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>{{ $ts.latestRequestSentAt }}</template> | ||||||
|  | 				<template #value><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>{{ $ts.latestStatus }}</template> | ||||||
|  | 				<template #value>{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>{{ $ts.latestRequestReceivedAt }}</template> | ||||||
|  | 				<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 		<FormGroup> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>Open Registrations</template> | ||||||
|  | 				<template #value>{{ instance.openRegistrations ? $ts.yes : $ts.no }}</template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 		<div class="_formItem"> | ||||||
|  | 			<div class="_formLabel">{{ $ts.statistics }}</div> | ||||||
|  | 			<div class="_formPanel cmhjzshl"> | ||||||
|  | 				<div class="selects"> | ||||||
|  | 					<MkSelect v-model:value="chartSrc" style="margin: 0; flex: 1;"> | ||||||
|  | 						<option value="requests">{{ $ts._instanceCharts.requests }}</option> | ||||||
|  | 						<option value="users">{{ $ts._instanceCharts.users }}</option> | ||||||
|  | 						<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option> | ||||||
|  | 						<option value="notes">{{ $ts._instanceCharts.notes }}</option> | ||||||
|  | 						<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option> | ||||||
|  | 						<option value="ff">{{ $ts._instanceCharts.ff }}</option> | ||||||
|  | 						<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option> | ||||||
|  | 						<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option> | ||||||
|  | 						<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option> | ||||||
|  | 						<option value="drive-files">{{ $ts._instanceCharts.files }}</option> | ||||||
|  | 						<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option> | ||||||
|  | 					</MkSelect> | ||||||
|  | 					<MkSelect v-model:value="chartSpan" style="margin: 0;"> | ||||||
|  | 						<option value="hour">{{ $ts.perHour }}</option> | ||||||
|  | 						<option value="day">{{ $ts.perDay }}</option> | ||||||
|  | 					</MkSelect> | ||||||
|  | 				</div> | ||||||
|  | 				<div class="chart"> | ||||||
|  | 					<canvas :ref="setChart"></canvas> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 		<FormGroup> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>{{ $ts.registeredAt }}</template> | ||||||
|  | 				<template #value><MkTime mode="detail" :time="instance.caughtAt"/></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 		<FormObjectView tall :value="instance"> | ||||||
|  | 			<span>Raw</span> | ||||||
|  | 		</FormObjectView> | ||||||
|  | 	</FormGroup> | ||||||
|  | </FormBase> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineAsyncComponent, defineComponent } from 'vue'; | ||||||
|  | import { faExternalLinkAlt, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import Chart from 'chart.js'; | ||||||
|  | import FormObjectView from '@client/components/form/object-view.vue'; | ||||||
|  | import FormTextarea from '@client/components/form/textarea.vue'; | ||||||
|  | import FormLink from '@client/components/form/link.vue'; | ||||||
|  | import FormBase from '@client/components/form/base.vue'; | ||||||
|  | import FormGroup from '@client/components/form/group.vue'; | ||||||
|  | import FormButton from '@client/components/form/button.vue'; | ||||||
|  | import FormKeyValueView from '@client/components/form/key-value-view.vue'; | ||||||
|  | import FormSuspense from '@client/components/form/suspense.vue'; | ||||||
|  | import MkSelect from '@client/components/ui/select.vue'; | ||||||
|  | import * as os from '@client/os'; | ||||||
|  | import number from '@client/filters/number'; | ||||||
|  | import bytes from '@client/filters/bytes'; | ||||||
|  | import * as symbols from '@client/symbols'; | ||||||
|  | import { url } from '@client/config'; | ||||||
|  | 
 | ||||||
|  | const chartLimit = 90; | ||||||
|  | const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b)); | ||||||
|  | const negate = arr => arr.map(x => -x); | ||||||
|  | const alpha = hex => { | ||||||
|  | 	const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; | ||||||
|  | 	const r = parseInt(result[1], 16); | ||||||
|  | 	const g = parseInt(result[2], 16); | ||||||
|  | 	const b = parseInt(result[3], 16); | ||||||
|  | 	return `rgba(${r}, ${g}, ${b}, 0.1)`; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		FormBase, | ||||||
|  | 		FormTextarea, | ||||||
|  | 		FormObjectView, | ||||||
|  | 		FormButton, | ||||||
|  | 		FormLink, | ||||||
|  | 		FormGroup, | ||||||
|  | 		FormKeyValueView, | ||||||
|  | 		FormSuspense, | ||||||
|  | 		MkSelect, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	props: { | ||||||
|  | 		host: { | ||||||
|  | 			type: String, | ||||||
|  | 			required: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			[symbols.PAGE_INFO]: { | ||||||
|  | 				title: this.$ts.instanceInfo, | ||||||
|  | 				icon: faInfoCircle, | ||||||
|  | 				actions: [{ | ||||||
|  | 					text: `https://${this.host}`, | ||||||
|  | 					icon: faExternalLinkAlt, | ||||||
|  | 					handler: () => { | ||||||
|  | 						window.open(`https://${this.host}`, '_blank'); | ||||||
|  | 					} | ||||||
|  | 				}], | ||||||
|  | 			}, | ||||||
|  | 			instance: null, | ||||||
|  | 			now: null, | ||||||
|  | 			canvas: null, | ||||||
|  | 			chart: null, | ||||||
|  | 			chartInstance: null, | ||||||
|  | 			chartSrc: 'requests', | ||||||
|  | 			chartSpan: 'hour', | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	computed: { | ||||||
|  | 		data(): any { | ||||||
|  | 			if (this.chart == null) return null; | ||||||
|  | 			switch (this.chartSrc) { | ||||||
|  | 				case 'requests': return this.requestsChart(); | ||||||
|  | 				case 'users': return this.usersChart(false); | ||||||
|  | 				case 'users-total': return this.usersChart(true); | ||||||
|  | 				case 'notes': return this.notesChart(false); | ||||||
|  | 				case 'notes-total': return this.notesChart(true); | ||||||
|  | 				case 'ff': return this.ffChart(false); | ||||||
|  | 				case 'ff-total': return this.ffChart(true); | ||||||
|  | 				case 'drive-usage': return this.driveUsageChart(false); | ||||||
|  | 				case 'drive-usage-total': return this.driveUsageChart(true); | ||||||
|  | 				case 'drive-files': return this.driveFilesChart(false); | ||||||
|  | 				case 'drive-files-total': return this.driveFilesChart(true); | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		stats(): any[] { | ||||||
|  | 			const stats = | ||||||
|  | 				this.chartSpan == 'day' ? this.chart.perDay : | ||||||
|  | 				this.chartSpan == 'hour' ? this.chart.perHour : | ||||||
|  | 				null; | ||||||
|  | 
 | ||||||
|  | 			return stats; | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	watch: { | ||||||
|  | 		chartSrc() { | ||||||
|  | 			this.renderChart(); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		chartSpan() { | ||||||
|  | 			this.renderChart(); | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	mounted() { | ||||||
|  | 		this.fetch(); | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		number, | ||||||
|  | 		bytes, | ||||||
|  | 
 | ||||||
|  | 		async fetch() { | ||||||
|  | 			this.instance = await os.api('federation/show-instance', { | ||||||
|  | 				host: this.host | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			this.now = new Date(); | ||||||
|  | 
 | ||||||
|  | 			const [perHour, perDay] = await Promise.all([ | ||||||
|  | 				os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }), | ||||||
|  | 				os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }), | ||||||
|  | 			]); | ||||||
|  | 
 | ||||||
|  | 			const chart = { | ||||||
|  | 				perHour: perHour, | ||||||
|  | 				perDay: perDay | ||||||
|  | 			}; | ||||||
|  | 
 | ||||||
|  | 			this.chart = chart; | ||||||
|  | 
 | ||||||
|  | 			this.renderChart(); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		setChart(el) { | ||||||
|  | 			this.canvas = el; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		renderChart() { | ||||||
|  | 			if (this.chartInstance) { | ||||||
|  | 				this.chartInstance.destroy(); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg'); | ||||||
|  | 			this.chartInstance = new Chart(this.canvas, { | ||||||
|  | 				type: 'line', | ||||||
|  | 				data: { | ||||||
|  | 					labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(), | ||||||
|  | 					datasets: this.data.series.map(x => ({ | ||||||
|  | 						label: x.name, | ||||||
|  | 						data: x.data.slice().reverse(), | ||||||
|  | 						pointRadius: 0, | ||||||
|  | 						lineTension: 0, | ||||||
|  | 						borderWidth: 2, | ||||||
|  | 						borderColor: x.color, | ||||||
|  | 						backgroundColor: alpha(x.color), | ||||||
|  | 					})) | ||||||
|  | 				}, | ||||||
|  | 				options: { | ||||||
|  | 					aspectRatio: 2.5, | ||||||
|  | 					layout: { | ||||||
|  | 						padding: { | ||||||
|  | 							left: 16, | ||||||
|  | 							right: 16, | ||||||
|  | 							top: 16, | ||||||
|  | 							bottom: 16 | ||||||
|  | 						} | ||||||
|  | 					}, | ||||||
|  | 					legend: { | ||||||
|  | 						position: 'bottom', | ||||||
|  | 						labels: { | ||||||
|  | 							boxWidth: 16, | ||||||
|  | 						} | ||||||
|  | 					}, | ||||||
|  | 					scales: { | ||||||
|  | 						xAxes: [{ | ||||||
|  | 							gridLines: { | ||||||
|  | 								display: false | ||||||
|  | 							}, | ||||||
|  | 							ticks: { | ||||||
|  | 								display: false | ||||||
|  | 							} | ||||||
|  | 						}], | ||||||
|  | 						yAxes: [{ | ||||||
|  | 							position: 'right', | ||||||
|  | 							ticks: { | ||||||
|  | 								display: false | ||||||
|  | 							} | ||||||
|  | 						}] | ||||||
|  | 					}, | ||||||
|  | 					tooltips: { | ||||||
|  | 						intersect: false, | ||||||
|  | 						mode: 'index', | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		getDate(ago: number) { | ||||||
|  | 			const y = this.now.getFullYear(); | ||||||
|  | 			const m = this.now.getMonth(); | ||||||
|  | 			const d = this.now.getDate(); | ||||||
|  | 			const h = this.now.getHours(); | ||||||
|  | 
 | ||||||
|  | 			return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		format(arr) { | ||||||
|  | 			return arr; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		requestsChart(): any { | ||||||
|  | 			return { | ||||||
|  | 				series: [{ | ||||||
|  | 					name: 'In', | ||||||
|  | 					color: '#008FFB', | ||||||
|  | 					data: this.format(this.stats.requests.received) | ||||||
|  | 				}, { | ||||||
|  | 					name: 'Out (succ)', | ||||||
|  | 					color: '#00E396', | ||||||
|  | 					data: this.format(this.stats.requests.succeeded) | ||||||
|  | 				}, { | ||||||
|  | 					name: 'Out (fail)', | ||||||
|  | 					color: '#FEB019', | ||||||
|  | 					data: this.format(this.stats.requests.failed) | ||||||
|  | 				}] | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		usersChart(total: boolean): any { | ||||||
|  | 			return { | ||||||
|  | 				series: [{ | ||||||
|  | 					name: 'Users', | ||||||
|  | 					color: '#008FFB', | ||||||
|  | 					data: this.format(total | ||||||
|  | 						? this.stats.users.total | ||||||
|  | 						: sum(this.stats.users.inc, negate(this.stats.users.dec)) | ||||||
|  | 					) | ||||||
|  | 				}] | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		notesChart(total: boolean): any { | ||||||
|  | 			return { | ||||||
|  | 				series: [{ | ||||||
|  | 					name: 'Notes', | ||||||
|  | 					color: '#008FFB', | ||||||
|  | 					data: this.format(total | ||||||
|  | 						? this.stats.notes.total | ||||||
|  | 						: sum(this.stats.notes.inc, negate(this.stats.notes.dec)) | ||||||
|  | 					) | ||||||
|  | 				}] | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		ffChart(total: boolean): any { | ||||||
|  | 			return { | ||||||
|  | 				series: [{ | ||||||
|  | 					name: 'Following', | ||||||
|  | 					color: '#008FFB', | ||||||
|  | 					data: this.format(total | ||||||
|  | 						? this.stats.following.total | ||||||
|  | 						: sum(this.stats.following.inc, negate(this.stats.following.dec)) | ||||||
|  | 					) | ||||||
|  | 				}, { | ||||||
|  | 					name: 'Followers', | ||||||
|  | 					color: '#00E396', | ||||||
|  | 					data: this.format(total | ||||||
|  | 						? this.stats.followers.total | ||||||
|  | 						: sum(this.stats.followers.inc, negate(this.stats.followers.dec)) | ||||||
|  | 					) | ||||||
|  | 				}] | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		driveUsageChart(total: boolean): any { | ||||||
|  | 			return { | ||||||
|  | 				bytes: true, | ||||||
|  | 				series: [{ | ||||||
|  | 					name: 'Drive usage', | ||||||
|  | 					color: '#008FFB', | ||||||
|  | 					data: this.format(total | ||||||
|  | 						? this.stats.drive.totalUsage | ||||||
|  | 						: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage)) | ||||||
|  | 					) | ||||||
|  | 				}] | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		driveFilesChart(total: boolean): any { | ||||||
|  | 			return { | ||||||
|  | 				series: [{ | ||||||
|  | 					name: 'Drive files', | ||||||
|  | 					color: '#008FFB', | ||||||
|  | 					data: this.format(total | ||||||
|  | 						? this.stats.drive.totalFiles | ||||||
|  | 						: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles)) | ||||||
|  | 					) | ||||||
|  | 				}] | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .fnfelxur { | ||||||
|  | 	padding: 16px; | ||||||
|  | 
 | ||||||
|  | 	> img { | ||||||
|  | 		display: block; | ||||||
|  | 		margin: auto; | ||||||
|  | 		height: 64px; | ||||||
|  | 		border-radius: 8px; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .cmhjzshl { | ||||||
|  | 	> .selects { | ||||||
|  | 		display: flex; | ||||||
|  | 		padding: 16px; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 		<template #header><Fa :icon="faHeartbeat"/> {{ $ts.metrics }}</template> | 		<template #header><Fa :icon="faHeartbeat"/> {{ $ts.metrics }}</template> | ||||||
| 		<div class="_section" style="padding: 0 var(--margin);"> | 		<div class="_section" style="padding: 0 var(--margin);"> | ||||||
| 			<div class="_content"> | 			<div class="_content"> | ||||||
| 				<MkContainer :body-togglable="false" class="_gap"> | 				<MkContainer :foldable="false" class="_gap"> | ||||||
| 					<template #header><Fa :icon="faMicrochip"/>{{ $ts.cpuAndMemory }}</template> | 					<template #header><Fa :icon="faMicrochip"/>{{ $ts.cpuAndMemory }}</template> | ||||||
| 					<!-- | 					<!-- | ||||||
| 					<template #func> | 					<template #func> | ||||||
|  | @ -27,7 +27,7 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				</MkContainer> | 				</MkContainer> | ||||||
| 
 | 
 | ||||||
| 				<MkContainer :body-togglable="false" class="_gap"> | 				<MkContainer :foldable="false" class="_gap"> | ||||||
| 					<template #header><Fa :icon="faHdd"/> {{ $ts.disk }}</template> | 					<template #header><Fa :icon="faHdd"/> {{ $ts.disk }}</template> | ||||||
| 					<!-- | 					<!-- | ||||||
| 					<template #func> | 					<template #func> | ||||||
|  | @ -50,7 +50,7 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				</MkContainer> | 				</MkContainer> | ||||||
| 
 | 
 | ||||||
| 				<MkContainer :body-togglable="false" class="_gap"> | 				<MkContainer :foldable="false" class="_gap"> | ||||||
| 					<template #header><Fa :icon="faExchangeAlt"/> {{ $ts.network }}</template> | 					<template #header><Fa :icon="faExchangeAlt"/> {{ $ts.network }}</template> | ||||||
| 					<!-- | 					<!-- | ||||||
| 					<template #func> | 					<template #func> | ||||||
|  | @ -78,7 +78,7 @@ | ||||||
| 		<template #header><Fa :icon="faClipboardList"/> {{ $ts.jobQueue }}</template> | 		<template #header><Fa :icon="faClipboardList"/> {{ $ts.jobQueue }}</template> | ||||||
| 
 | 
 | ||||||
| 		<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }"> | 		<div class="vkyrmkwb" :style="{ gridTemplateRows: queueHeight }"> | ||||||
| 			<MkContainer :body-togglable="false" :scrollable="true" :resize-base-el="() => $el"> | 			<MkContainer :foldable="false" :scrollable="true" :resize-base-el="() => $el"> | ||||||
| 				<template #header><Fa :icon="faExclamationTriangle"/> {{ $ts.delayed }}</template> | 				<template #header><Fa :icon="faExclamationTriangle"/> {{ $ts.delayed }}</template> | ||||||
| 
 | 
 | ||||||
| 				<div class="_content"> | 				<div class="_content"> | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
| 		<div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }"> | 		<div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }"> | ||||||
| 			<MkInstanceStats :chart-limit="300" :detailed="true" class="_gap" ref="stats"/> | 			<MkInstanceStats :chart-limit="300" :detailed="true" class="_gap" ref="stats"/> | ||||||
| 
 | 
 | ||||||
| 			<MkContainer :body-togglable="true" class="_gap"> | 			<MkContainer :foldable="true" class="_gap"> | ||||||
| 				<template #header><Fa :icon="faInfoCircle"/>{{ $ts.instanceInfo }}</template> | 				<template #header><Fa :icon="faInfoCircle"/>{{ $ts.instanceInfo }}</template> | ||||||
| 
 | 
 | ||||||
| 				<div class="_content"> | 				<div class="_content"> | ||||||
|  | @ -19,7 +19,7 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 			</MkContainer> | 			</MkContainer> | ||||||
| 			 | 			 | ||||||
| 			<MkContainer :body-togglable="true" :scrollable="true" class="_gap" style="height: 300px;"> | 			<MkContainer :foldable="true" :scrollable="true" class="_gap" style="height: 300px;"> | ||||||
| 				<template #header><Fa :icon="faDatabase"/>{{ $ts.database }}</template> | 				<template #header><Fa :icon="faDatabase"/>{{ $ts.database }}</template> | ||||||
| 
 | 
 | ||||||
| 				<div class="_content" v-if="dbInfo"> | 				<div class="_content" v-if="dbInfo"> | ||||||
|  |  | ||||||
|  | @ -85,6 +85,8 @@ export default defineComponent({ | ||||||
| 	display: flex; | 	display: flex; | ||||||
| 
 | 
 | ||||||
| 	> .avatar { | 	> .avatar { | ||||||
|  | 		position: sticky; | ||||||
|  | 		top: calc(var(--stickyTop, 0px) + 16px); | ||||||
| 		display: block; | 		display: block; | ||||||
| 		width: 54px; | 		width: 54px; | ||||||
| 		height: 54px; | 		height: 54px; | ||||||
|  | @ -274,6 +276,11 @@ export default defineComponent({ | ||||||
| 				background: $me-balloon-color; | 				background: $me-balloon-color; | ||||||
| 				text-align: left; | 				text-align: left; | ||||||
| 
 | 
 | ||||||
|  | 				::selection { | ||||||
|  | 					color: var(--accent); | ||||||
|  | 					background-color: #fff; | ||||||
|  | 				}  | ||||||
|  | 
 | ||||||
| 				&.noText { | 				&.noText { | ||||||
| 					background: transparent; | 					background: transparent; | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | @ -1,265 +1,261 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mwysmxbg"> | <div class="mwysmxbg"> | ||||||
| 	<div class="_section"> | 	<div class="_isolated">{{ $ts._mfm.intro }}</div> | ||||||
| 		<div class="_content"> | 	<div class="section _block"> | ||||||
| 			<p>{{ $ts._mfm.intro }}</p> | 		<div class="title">{{ $ts._mfm.mention }}</div> | ||||||
| 		</div> | 		<div class="content"> | ||||||
| 	</div> |  | ||||||
| 	<div class="_section"> |  | ||||||
| 		<div class="_title">{{ $ts._mfm.mention }}</div> |  | ||||||
| 		<div class="_content"> |  | ||||||
| 			<p>{{ $ts._mfm.mentionDescription }}</p> | 			<p>{{ $ts._mfm.mentionDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_mention"/> | 				<Mfm :text="preview_mention"/> | ||||||
| 				<MkTextarea v-model:value="preview_mention"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_mention"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.hashtag }}</div> | 		<div class="title">{{ $ts._mfm.hashtag }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.hashtagDescription }}</p> | 			<p>{{ $ts._mfm.hashtagDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_hashtag"/> | 				<Mfm :text="preview_hashtag"/> | ||||||
| 				<MkTextarea v-model:value="preview_hashtag"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_hashtag"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.url }}</div> | 		<div class="title">{{ $ts._mfm.url }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.urlDescription }}</p> | 			<p>{{ $ts._mfm.urlDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_url"/> | 				<Mfm :text="preview_url"/> | ||||||
| 				<MkTextarea v-model:value="preview_url"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_url"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.link }}</div> | 		<div class="title">{{ $ts._mfm.link }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.linkDescription }}</p> | 			<p>{{ $ts._mfm.linkDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_link"/> | 				<Mfm :text="preview_link"/> | ||||||
| 				<MkTextarea v-model:value="preview_link"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_link"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.emoji }}</div> | 		<div class="title">{{ $ts._mfm.emoji }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.emojiDescription }}</p> | 			<p>{{ $ts._mfm.emojiDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_emoji"/> | 				<Mfm :text="preview_emoji"/> | ||||||
| 				<MkTextarea v-model:value="preview_emoji"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_emoji"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.bold }}</div> | 		<div class="title">{{ $ts._mfm.bold }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.boldDescription }}</p> | 			<p>{{ $ts._mfm.boldDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_bold"/> | 				<Mfm :text="preview_bold"/> | ||||||
| 				<MkTextarea v-model:value="preview_bold"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_bold"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.small }}</div> | 		<div class="title">{{ $ts._mfm.small }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.smallDescription }}</p> | 			<p>{{ $ts._mfm.smallDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_small"/> | 				<Mfm :text="preview_small"/> | ||||||
| 				<MkTextarea v-model:value="preview_small"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_small"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.quote }}</div> | 		<div class="title">{{ $ts._mfm.quote }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.quoteDescription }}</p> | 			<p>{{ $ts._mfm.quoteDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_quote"/> | 				<Mfm :text="preview_quote"/> | ||||||
| 				<MkTextarea v-model:value="preview_quote"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_quote"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.center }}</div> | 		<div class="title">{{ $ts._mfm.center }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.centerDescription }}</p> | 			<p>{{ $ts._mfm.centerDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_center"/> | 				<Mfm :text="preview_center"/> | ||||||
| 				<MkTextarea v-model:value="preview_center"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_center"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.inlineCode }}</div> | 		<div class="title">{{ $ts._mfm.inlineCode }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.inlineCodeDescription }}</p> | 			<p>{{ $ts._mfm.inlineCodeDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_inlineCode"/> | 				<Mfm :text="preview_inlineCode"/> | ||||||
| 				<MkTextarea v-model:value="preview_inlineCode"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_inlineCode"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.blockCode }}</div> | 		<div class="title">{{ $ts._mfm.blockCode }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.blockCodeDescription }}</p> | 			<p>{{ $ts._mfm.blockCodeDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_blockCode"/> | 				<Mfm :text="preview_blockCode"/> | ||||||
| 				<MkTextarea v-model:value="preview_blockCode"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_blockCode"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.inlineMath }}</div> | 		<div class="title">{{ $ts._mfm.inlineMath }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.inlineMathDescription }}</p> | 			<p>{{ $ts._mfm.inlineMathDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_inlineMath"/> | 				<Mfm :text="preview_inlineMath"/> | ||||||
| 				<MkTextarea v-model:value="preview_inlineMath"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_inlineMath"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.search }}</div> | 		<div class="title">{{ $ts._mfm.search }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.searchDescription }}</p> | 			<p>{{ $ts._mfm.searchDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_search"/> | 				<Mfm :text="preview_search"/> | ||||||
| 				<MkTextarea v-model:value="preview_search"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_search"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.flip }}</div> | 		<div class="title">{{ $ts._mfm.flip }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.flipDescription }}</p> | 			<p>{{ $ts._mfm.flipDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_flip"/> | 				<Mfm :text="preview_flip"/> | ||||||
| 				<MkTextarea v-model:value="preview_flip"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_flip"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.font }}</div> | 		<div class="title">{{ $ts._mfm.font }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.fontDescription }}</p> | 			<p>{{ $ts._mfm.fontDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_font"/> | 				<Mfm :text="preview_font"/> | ||||||
| 				<MkTextarea v-model:value="preview_font"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_font"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.x2 }}</div> | 		<div class="title">{{ $ts._mfm.x2 }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.x2Description }}</p> | 			<p>{{ $ts._mfm.x2Description }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_x2"/> | 				<Mfm :text="preview_x2"/> | ||||||
| 				<MkTextarea v-model:value="preview_x2"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_x2"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.x3 }}</div> | 		<div class="title">{{ $ts._mfm.x3 }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.x3Description }}</p> | 			<p>{{ $ts._mfm.x3Description }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_x3"/> | 				<Mfm :text="preview_x3"/> | ||||||
| 				<MkTextarea v-model:value="preview_x3"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_x3"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.x4 }}</div> | 		<div class="title">{{ $ts._mfm.x4 }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.x4Description }}</p> | 			<p>{{ $ts._mfm.x4Description }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_x4"/> | 				<Mfm :text="preview_x4"/> | ||||||
| 				<MkTextarea v-model:value="preview_x4"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_x4"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.blur }}</div> | 		<div class="title">{{ $ts._mfm.blur }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.blurDescription }}</p> | 			<p>{{ $ts._mfm.blurDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_blur"/> | 				<Mfm :text="preview_blur"/> | ||||||
| 				<MkTextarea v-model:value="preview_blur"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_blur"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.jelly }}</div> | 		<div class="title">{{ $ts._mfm.jelly }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.jellyDescription }}</p> | 			<p>{{ $ts._mfm.jellyDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_jelly"/> | 				<Mfm :text="preview_jelly"/> | ||||||
| 				<MkTextarea v-model:value="preview_jelly"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_jelly"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.tada }}</div> | 		<div class="title">{{ $ts._mfm.tada }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.tadaDescription }}</p> | 			<p>{{ $ts._mfm.tadaDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_tada"/> | 				<Mfm :text="preview_tada"/> | ||||||
| 				<MkTextarea v-model:value="preview_tada"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_tada"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.jump }}</div> | 		<div class="title">{{ $ts._mfm.jump }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.jumpDescription }}</p> | 			<p>{{ $ts._mfm.jumpDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_jump"/> | 				<Mfm :text="preview_jump"/> | ||||||
| 				<MkTextarea v-model:value="preview_jump"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_jump"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.bounce }}</div> | 		<div class="title">{{ $ts._mfm.bounce }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.bounceDescription }}</p> | 			<p>{{ $ts._mfm.bounceDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_bounce"/> | 				<Mfm :text="preview_bounce"/> | ||||||
| 				<MkTextarea v-model:value="preview_bounce"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_bounce"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.spin }}</div> | 		<div class="title">{{ $ts._mfm.spin }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.spinDescription }}</p> | 			<p>{{ $ts._mfm.spinDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_spin"/> | 				<Mfm :text="preview_spin"/> | ||||||
| 				<MkTextarea v-model:value="preview_spin"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_spin"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.shake }}</div> | 		<div class="title">{{ $ts._mfm.shake }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.shakeDescription }}</p> | 			<p>{{ $ts._mfm.shakeDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_shake"/> | 				<Mfm :text="preview_shake"/> | ||||||
| 				<MkTextarea v-model:value="preview_shake"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_shake"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="_section"> | 	<div class="section _block"> | ||||||
| 		<div class="_title">{{ $ts._mfm.twitch }}</div> | 		<div class="title">{{ $ts._mfm.twitch }}</div> | ||||||
| 		<div class="_content"> | 		<div class="content"> | ||||||
| 			<p>{{ $ts._mfm.twitchDescription }}</p> | 			<p>{{ $ts._mfm.twitchDescription }}</p> | ||||||
| 			<div class="preview _panel"> | 			<div class="preview"> | ||||||
| 				<Mfm :text="preview_twitch"/> | 				<Mfm :text="preview_twitch"/> | ||||||
| 				<MkTextarea v-model:value="preview_twitch"><span>MFM</span></MkTextarea> | 				<MkTextarea v-model:value="preview_twitch"><span>MFM</span></MkTextarea> | ||||||
| 			</div> | 			</div> | ||||||
|  | @ -318,8 +314,29 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .mwysmxbg { | .mwysmxbg { | ||||||
| 	.preview { | 	> .section { | ||||||
| 		padding: 16px; | 		> .title { | ||||||
|  | 			position: sticky; | ||||||
|  | 			z-index: 1; | ||||||
|  | 			top: var(--stickyTop, 0px); | ||||||
|  | 			padding: 16px; | ||||||
|  | 			font-weight: bold; | ||||||
|  | 			-webkit-backdrop-filter: blur(10px); | ||||||
|  | 			backdrop-filter: blur(10px); | ||||||
|  | 			background-color: var(--X16); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		> .content { | ||||||
|  | 			> p { | ||||||
|  | 				margin: 0; | ||||||
|  | 				padding: 16px; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			> .preview { | ||||||
|  | 				border-top: solid 0.5px var(--divider); | ||||||
|  | 				padding: 16px; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| <div class="fcuexfpr _root"> | <div class="fcuexfpr _root"> | ||||||
| 	<div v-if="note" class="note" v-anim> | 	<div v-if="note" class="note" v-anim> | ||||||
| 		<div class="_gap" v-if="showNext"> | 		<div class="_gap" v-if="showNext"> | ||||||
| 			<XNotes class="_content _noGap_" :pagination="next"/> | 			<XNotes class="_content" :pagination="next" :no-gap="true"/> | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
| 		<div class="main _gap"> | 		<div class="main _gap"> | ||||||
|  | @ -25,7 +25,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
| 		<div class="_gap" v-if="showPrev"> | 		<div class="_gap" v-if="showPrev"> | ||||||
| 			<XNotes class="_content _noGap_" :pagination="prev"/> | 			<XNotes class="_content" :pagination="prev" :no-gap="true"/> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
| 		<MkButton inline @click="del" class="delete" v-if="pageId && !readonly"><Fa :icon="faTrashAlt"/> {{ $ts.delete }}</MkButton> | 		<MkButton inline @click="del" class="delete" v-if="pageId && !readonly"><Fa :icon="faTrashAlt"/> {{ $ts.delete }}</MkButton> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<MkContainer :body-togglable="true" :expanded="true" class="_gap"> | 	<MkContainer :foldable="true" :expanded="true" class="_gap"> | ||||||
| 		<template #header><Fa :icon="faCog"/> {{ $ts._pages.pageSetting }}</template> | 		<template #header><Fa :icon="faCog"/> {{ $ts._pages.pageSetting }}</template> | ||||||
| 		<div style="padding: 16px;"> | 		<div style="padding: 16px;"> | ||||||
| 			<MkInput v-model:value="title"> | 			<MkInput v-model:value="title"> | ||||||
|  | @ -44,7 +44,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 	</MkContainer> | 	</MkContainer> | ||||||
| 
 | 
 | ||||||
| 	<MkContainer :body-togglable="true" :expanded="true" class="_gap"> | 	<MkContainer :foldable="true" :expanded="true" class="_gap"> | ||||||
| 		<template #header><Fa :icon="faStickyNote"/> {{ $ts._pages.contents }}</template> | 		<template #header><Fa :icon="faStickyNote"/> {{ $ts._pages.contents }}</template> | ||||||
| 		<div style="padding: 16px;"> | 		<div style="padding: 16px;"> | ||||||
| 			<XBlocks class="content" v-model:value="content" :hpml="hpml"/> | 			<XBlocks class="content" v-model:value="content" :hpml="hpml"/> | ||||||
|  | @ -53,7 +53,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 	</MkContainer> | 	</MkContainer> | ||||||
| 
 | 
 | ||||||
| 	<MkContainer :body-togglable="true" class="_gap"> | 	<MkContainer :foldable="true" class="_gap"> | ||||||
| 		<template #header><Fa :icon="faMagic"/> {{ $ts._pages.variables }}</template> | 		<template #header><Fa :icon="faMagic"/> {{ $ts._pages.variables }}</template> | ||||||
| 		<div class="qmuvgica"> | 		<div class="qmuvgica"> | ||||||
| 			<XDraggable tag="div" class="variables" v-show="variables.length > 0" v-model="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> | 			<XDraggable tag="div" class="variables" v-show="variables.length > 0" v-model="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5"> | ||||||
|  | @ -74,7 +74,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 	</MkContainer> | 	</MkContainer> | ||||||
| 
 | 
 | ||||||
| 	<MkContainer :body-togglable="true" :expanded="true" class="_gap"> | 	<MkContainer :foldable="true" :expanded="true" class="_gap"> | ||||||
| 		<template #header><Fa :icon="faCode"/> {{ $ts.script }}</template> | 		<template #header><Fa :icon="faCode"/> {{ $ts.script }}</template> | ||||||
| 		<div> | 		<div> | ||||||
| 			<MkTextarea class="_code" v-model:value="script"/> | 			<MkTextarea class="_code" v-model:value="script"/> | ||||||
|  |  | ||||||
|  | @ -1,8 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <div class="xcukqgmh _root" v-if="page" :key="page.id" v-size="{ max: [450] }"> | <div class="xcukqgmh _root _magnetParent" v-if="page" :key="page.id" v-size="{ max: [450] }"> | ||||||
| 	<div class="_magnet"></div> | 	<div class="_block _magnetChild main"> | ||||||
| 
 |  | ||||||
| 	<div class="_block main"> |  | ||||||
| 		<!-- | 		<!-- | ||||||
| 		<div class="header"> | 		<div class="header"> | ||||||
| 			<h1>{{ page.title }}</h1> | 			<h1>{{ page.title }}</h1> | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| 		<MkButton style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><Fa :icon="faPlay"/></MkButton> | 		<MkButton style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><Fa :icon="faPlay"/></MkButton> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<MkContainer :body-togglable="true" class="_gap"> | 	<MkContainer :foldable="true" class="_gap"> | ||||||
| 		<template #header><Fa fixed-width/>{{ $ts.output }}</template> | 		<template #header><Fa fixed-width/>{{ $ts.output }}</template> | ||||||
| 		<div class="bepmlvbi"> | 		<div class="bepmlvbi"> | ||||||
| 			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div> | 			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| <template> | <template> | ||||||
| <FormBase> | <FormBase> | ||||||
| 	<FormSwitch v-model:value="titlebar">{{ $ts.showTitlebar }}</FormSwitch> |  | ||||||
| 	<FormSwitch v-model:value="showFixedPostForm">{{ $ts.showFixedPostForm }}</FormSwitch> | 	<FormSwitch v-model:value="showFixedPostForm">{{ $ts.showFixedPostForm }}</FormSwitch> | ||||||
| 
 | 
 | ||||||
| 	<FormSelect v-model:value="lang"> | 	<FormSelect v-model:value="lang"> | ||||||
|  | @ -137,7 +136,6 @@ export default defineComponent({ | ||||||
| 		useOsNativeEmojis: defaultStore.makeGetterSetter('useOsNativeEmojis'), | 		useOsNativeEmojis: defaultStore.makeGetterSetter('useOsNativeEmojis'), | ||||||
| 		disableShowingAnimatedImages: defaultStore.makeGetterSetter('disableShowingAnimatedImages'), | 		disableShowingAnimatedImages: defaultStore.makeGetterSetter('disableShowingAnimatedImages'), | ||||||
| 		loadRawImages: defaultStore.makeGetterSetter('loadRawImages'), | 		loadRawImages: defaultStore.makeGetterSetter('loadRawImages'), | ||||||
| 		titlebar: defaultStore.makeGetterSetter('titlebar'), |  | ||||||
| 		imageNewTab: defaultStore.makeGetterSetter('imageNewTab'), | 		imageNewTab: defaultStore.makeGetterSetter('imageNewTab'), | ||||||
| 		nsfw: defaultStore.makeGetterSetter('nsfw'), | 		nsfw: defaultStore.makeGetterSetter('nsfw'), | ||||||
| 		disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'), | 		disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'), | ||||||
|  | @ -182,10 +180,6 @@ export default defineComponent({ | ||||||
| 			this.reloadAsk(); | 			this.reloadAsk(); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		titlebar() { |  | ||||||
| 			this.reloadAsk(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		instanceTicker() { | 		instanceTicker() { | ||||||
| 			this.reloadAsk(); | 			this.reloadAsk(); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | @ -13,6 +13,9 @@ | ||||||
| 		<FormInput readonly :value="selectedTheme.author"> | 		<FormInput readonly :value="selectedTheme.author"> | ||||||
| 			<span>{{ $ts.author }}</span> | 			<span>{{ $ts.author }}</span> | ||||||
| 		</FormInput> | 		</FormInput> | ||||||
|  | 		<FormTextarea readonly :value="selectedTheme.desc" v-if="selectedTheme.desc"> | ||||||
|  | 			<span>{{ $ts._theme.description }}</span> | ||||||
|  | 		</FormTextarea> | ||||||
| 		<FormTextarea readonly tall :value="selectedThemeCode"> | 		<FormTextarea readonly tall :value="selectedThemeCode"> | ||||||
| 			<span>{{ $ts._theme.code }}</span> | 			<span>{{ $ts._theme.code }}</span> | ||||||
| 			<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template> | 			<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template> | ||||||
|  | @ -94,6 +97,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 		uninstall() { | 		uninstall() { | ||||||
| 			removeTheme(this.selectedTheme); | 			removeTheme(this.selectedTheme); | ||||||
|  | 			this.installedThemes = this.installedThemes.filter(t => t.id !== this.selectedThemeId); | ||||||
| 			this.selectedThemeId = null; | 			this.selectedThemeId = null; | ||||||
| 			os.success(); | 			os.success(); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | @ -35,6 +35,7 @@ | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
|  | 
 | ||||||
| 	<FormGroup v-if="codeEnabled"> | 	<FormGroup v-if="codeEnabled"> | ||||||
| 		<FormTextarea v-model:value="themeCode" tall> | 		<FormTextarea v-model:value="themeCode" tall> | ||||||
| 			<span>{{ $ts._theme.code }}</span> | 			<span>{{ $ts._theme.code }}</span> | ||||||
|  | @ -42,6 +43,14 @@ | ||||||
| 		<FormButton @click="applyThemeCode" primary>{{ $ts.apply }}</FormButton> | 		<FormButton @click="applyThemeCode" primary>{{ $ts.apply }}</FormButton> | ||||||
| 	</FormGroup> | 	</FormGroup> | ||||||
| 	<FormButton v-else @click="codeEnabled = true"><Fa :icon="faCode"/> {{ $ts.editCode }}</FormButton> | 	<FormButton v-else @click="codeEnabled = true"><Fa :icon="faCode"/> {{ $ts.editCode }}</FormButton> | ||||||
|  | 
 | ||||||
|  | 	<FormGroup v-if="descriptionEnabled"> | ||||||
|  | 		<FormTextarea v-model:value="description"> | ||||||
|  | 			<span>{{ $ts._theme.description }}</span> | ||||||
|  | 		</FormTextarea> | ||||||
|  | 	</FormGroup> | ||||||
|  | 	<FormButton v-else @click="descriptionEnabled = true">{{ $ts.addDescription }}</FormButton> | ||||||
|  | 
 | ||||||
| 	<FormGroup> | 	<FormGroup> | ||||||
| 		<FormButton @click="showPreview"><Fa :icon="faEye"/> {{ $ts.preview }}</FormButton> | 		<FormButton @click="showPreview"><Fa :icon="faEye"/> {{ $ts.preview }}</FormButton> | ||||||
| 		<FormButton @click="saveAs" primary><Fa :icon="faSave"/> {{ $ts.saveAs }}</FormButton> | 		<FormButton @click="saveAs" primary><Fa :icon="faSave"/> {{ $ts.saveAs }}</FormButton> | ||||||
|  | @ -88,6 +97,8 @@ export default defineComponent({ | ||||||
| 				props: lightTheme.props | 				props: lightTheme.props | ||||||
| 			} as Theme, | 			} as Theme, | ||||||
| 			codeEnabled: false, | 			codeEnabled: false, | ||||||
|  | 			descriptionEnabled: false, | ||||||
|  | 			description: null, | ||||||
| 			themeCode: null, | 			themeCode: null, | ||||||
| 			bgColors: [ | 			bgColors: [ | ||||||
| 				{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' }, | 				{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' }, | ||||||
|  | @ -218,6 +229,7 @@ export default defineComponent({ | ||||||
| 			this.theme.id = uuid(); | 			this.theme.id = uuid(); | ||||||
| 			this.theme.name = name; | 			this.theme.name = name; | ||||||
| 			this.theme.author = `@${this.$i.username}@${toUnicode(host)}`; | 			this.theme.author = `@${this.$i.username}@${toUnicode(host)}`; | ||||||
|  | 			if (this.description) this.theme.desc = this.description; | ||||||
| 			addTheme(this.theme); | 			addTheme(this.theme); | ||||||
| 			applyTheme(this.theme); | 			applyTheme(this.theme); | ||||||
| 			if (this.$store.state.darkMode) { | 			if (this.$store.state.darkMode) { | ||||||
|  |  | ||||||
|  | @ -1,11 +1,10 @@ | ||||||
| <template> | <template> | ||||||
| <div class="cmuxhskf _root" v-hotkey.global="keymap"> | <div class="cmuxhskf _root _magnetParent" v-hotkey.global="keymap"> | ||||||
| 	<div class="new" v-if="queue > 0" :style="{ width: width + 'px' }"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> | 	<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> | ||||||
| 
 | 
 | ||||||
| 	<div class="_magnet"></div> |  | ||||||
| 	<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/> | 	<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/> | ||||||
| 	<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/> | 	<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/> | ||||||
| 	<div class="tabs _block"> | 	<div class="tabs _block _magnetChild"> | ||||||
| 		<div class="left"> | 		<div class="left"> | ||||||
| 			<button class="_button tab" @click="() => { src = 'home'; saveSrc(); }" :class="{ active: src === 'home' }" v-tooltip="$ts._timelines.home"><Fa :icon="faHome"/></button> | 			<button class="_button tab" @click="() => { src = 'home'; saveSrc(); }" :class="{ active: src === 'home' }" v-tooltip="$ts._timelines.home"><Fa :icon="faHome"/></button> | ||||||
| 			<button class="_button tab" @click="() => { src = 'local'; saveSrc(); }" :class="{ active: src === 'local' }" v-tooltip="$ts._timelines.local" v-if="isLocalTimelineAvailable"><Fa :icon="faComments"/></button> | 			<button class="_button tab" @click="() => { src = 'local'; saveSrc(); }" :class="{ active: src === 'local' }" v-tooltip="$ts._timelines.local" v-if="isLocalTimelineAvailable"><Fa :icon="faComments"/></button> | ||||||
|  | @ -64,7 +63,6 @@ export default defineComponent({ | ||||||
| 			channel: null, | 			channel: null, | ||||||
| 			menuOpened: false, | 			menuOpened: false, | ||||||
| 			queue: 0, | 			queue: 0, | ||||||
| 			width: 0, |  | ||||||
| 			[symbols.PAGE_INFO]: computed(() => ({ | 			[symbols.PAGE_INFO]: computed(() => ({ | ||||||
| 				title: this.$ts.timeline, | 				title: this.$ts.timeline, | ||||||
| 				icon: this.src === 'local' ? faComments : this.src === 'social' ? faShareAlt : this.src === 'global' ? faGlobe : faHome, | 				icon: this.src === 'local' ? faComments : this.src === 'social' ? faShareAlt : this.src === 'global' ? faGlobe : faHome, | ||||||
|  | @ -126,10 +124,6 @@ export default defineComponent({ | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { |  | ||||||
| 		this.width = this.$el.offsetWidth; |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { | 	methods: { | ||||||
| 		before() { | 		before() { | ||||||
| 			Progress.start(); | 			Progress.start(); | ||||||
|  | @ -140,7 +134,6 @@ export default defineComponent({ | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		queueUpdated(q) { | 		queueUpdated(q) { | ||||||
| 			if (this.$el.offsetWidth !== 0) this.width = this.$el.offsetWidth; |  | ||||||
| 			this.queue = q; | 			this.queue = q; | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | @ -223,8 +216,10 @@ export default defineComponent({ | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .cmuxhskf { | .cmuxhskf { | ||||||
| 	> .new { | 	> .new { | ||||||
| 		position: fixed; | 		position: sticky; | ||||||
|  | 		top: calc(var(--stickyTop, 0px) + 16px); | ||||||
| 		z-index: 1000; | 		z-index: 1000; | ||||||
|  | 		width: 100%; | ||||||
| 
 | 
 | ||||||
| 		> button { | 		> button { | ||||||
| 			display: block; | 			display: block; | ||||||
|  |  | ||||||
							
								
								
									
										122
									
								
								src/client/pages/user-ap-info.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/client/pages/user-ap-info.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,122 @@ | ||||||
|  | <template> | ||||||
|  | <FormBase> | ||||||
|  | 	<FormSuspense :p="apPromiseFactory" v-slot="{ result: ap }"> | ||||||
|  | 		<FormGroup> | ||||||
|  | 			<template #label>ActivityPub</template> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>Type</template> | ||||||
|  | 				<template #value><span class="_monospace">{{ ap.type }}</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>URI</template> | ||||||
|  | 				<template #value><span class="_monospace">{{ ap.id }}</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>URL</template> | ||||||
|  | 				<template #value><span class="_monospace">{{ ap.url }}</span></template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 			<FormGroup> | ||||||
|  | 				<FormKeyValueView> | ||||||
|  | 					<template #key>Inbox</template> | ||||||
|  | 					<template #value><span class="_monospace">{{ ap.inbox }}</span></template> | ||||||
|  | 				</FormKeyValueView> | ||||||
|  | 				<FormKeyValueView> | ||||||
|  | 					<template #key>Shared Inbox</template> | ||||||
|  | 					<template #value><span class="_monospace">{{ ap.sharedInbox }}</span></template> | ||||||
|  | 				</FormKeyValueView> | ||||||
|  | 				<FormKeyValueView> | ||||||
|  | 					<template #key>Outbox</template> | ||||||
|  | 					<template #value><span class="_monospace">{{ ap.outbox }}</span></template> | ||||||
|  | 				</FormKeyValueView> | ||||||
|  | 			</FormGroup> | ||||||
|  | 			<FormTextarea readonly tall code pre :value="ap.publicKey.publicKeyPem"> | ||||||
|  | 				<span>Public Key</span> | ||||||
|  | 			</FormTextarea> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>Discoverable</template> | ||||||
|  | 				<template #value>{{ ap.discoverable ? $ts.yes : $ts.no }}</template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 			<FormKeyValueView> | ||||||
|  | 				<template #key>ManuallyApprovesFollowers</template> | ||||||
|  | 				<template #value>{{ ap.manuallyApprovesFollowers ? $ts.yes : $ts.no }}</template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 			<FormObjectView tall :value="ap"> | ||||||
|  | 				<span>Raw</span> | ||||||
|  | 			</FormObjectView> | ||||||
|  | 			<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> | ||||||
|  | 			<FormKeyValueView v-else> | ||||||
|  | 				<template #key>{{ $ts.instanceInfo }}</template> | ||||||
|  | 				<template #value>(Local user)</template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 	</FormSuspense> | ||||||
|  | </FormBase> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineAsyncComponent, defineComponent } from 'vue'; | ||||||
|  | import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import FormObjectView from '@client/components/form/object-view.vue'; | ||||||
|  | import FormTextarea from '@client/components/form/textarea.vue'; | ||||||
|  | import FormLink from '@client/components/form/link.vue'; | ||||||
|  | import FormBase from '@client/components/form/base.vue'; | ||||||
|  | import FormGroup from '@client/components/form/group.vue'; | ||||||
|  | import FormButton from '@client/components/form/button.vue'; | ||||||
|  | import FormKeyValueView from '@client/components/form/key-value-view.vue'; | ||||||
|  | import FormSuspense from '@client/components/form/suspense.vue'; | ||||||
|  | import * as os from '@client/os'; | ||||||
|  | import number from '@client/filters/number'; | ||||||
|  | import bytes from '@client/filters/bytes'; | ||||||
|  | import * as symbols from '@client/symbols'; | ||||||
|  | import { url } from '@client/config'; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		FormBase, | ||||||
|  | 		FormTextarea, | ||||||
|  | 		FormObjectView, | ||||||
|  | 		FormButton, | ||||||
|  | 		FormLink, | ||||||
|  | 		FormGroup, | ||||||
|  | 		FormKeyValueView, | ||||||
|  | 		FormSuspense, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	props: { | ||||||
|  | 		userId: { | ||||||
|  | 			type: String, | ||||||
|  | 			required: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			[symbols.PAGE_INFO]: { | ||||||
|  | 				title: this.$ts.userInfo, | ||||||
|  | 				icon: faInfoCircle | ||||||
|  | 			}, | ||||||
|  | 			user: null, | ||||||
|  | 			apPromiseFactory: null, | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	mounted() { | ||||||
|  | 		this.fetch(); | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		number, | ||||||
|  | 		bytes, | ||||||
|  | 
 | ||||||
|  | 		async fetch() { | ||||||
|  | 			this.user = await os.api('users/show', { | ||||||
|  | 				userId: this.userId | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			this.apPromiseFactory = () => os.api('ap/get', { | ||||||
|  | 				uri: this.user.uri || `${url}/users/${this.user.id}` | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
							
								
								
									
										96
									
								
								src/client/pages/user-info.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/client/pages/user-info.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,96 @@ | ||||||
|  | <template> | ||||||
|  | <FormBase> | ||||||
|  | 	<FormGroup v-if="user"> | ||||||
|  | 		<template #label><MkAcct :user="user"/></template> | ||||||
|  | 
 | ||||||
|  | 		<FormKeyValueView> | ||||||
|  | 			<template #key>ID</template> | ||||||
|  | 			<template #value><span class="_monospace">{{ user.id }}</span></template> | ||||||
|  | 		</FormKeyValueView> | ||||||
|  | 
 | ||||||
|  | 		<FormGroup> | ||||||
|  | 			<FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink> | ||||||
|  | 
 | ||||||
|  | 			<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink> | ||||||
|  | 			<FormKeyValueView v-else> | ||||||
|  | 				<template #key>{{ $ts.instanceInfo }}</template> | ||||||
|  | 				<template #value>(Local user)</template> | ||||||
|  | 			</FormKeyValueView> | ||||||
|  | 		</FormGroup> | ||||||
|  | 
 | ||||||
|  | 		<FormObjectView tall :value="user"> | ||||||
|  | 			<span>Raw</span> | ||||||
|  | 		</FormObjectView> | ||||||
|  | 	</FormGroup> | ||||||
|  | </FormBase> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { computed, defineAsyncComponent, defineComponent } from 'vue'; | ||||||
|  | import { faExternalLinkAlt, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import FormObjectView from '@client/components/form/object-view.vue'; | ||||||
|  | import FormTextarea from '@client/components/form/textarea.vue'; | ||||||
|  | import FormLink from '@client/components/form/link.vue'; | ||||||
|  | import FormBase from '@client/components/form/base.vue'; | ||||||
|  | import FormGroup from '@client/components/form/group.vue'; | ||||||
|  | import FormButton from '@client/components/form/button.vue'; | ||||||
|  | import FormKeyValueView from '@client/components/form/key-value-view.vue'; | ||||||
|  | import FormSuspense from '@client/components/form/suspense.vue'; | ||||||
|  | import * as os from '@client/os'; | ||||||
|  | import number from '@client/filters/number'; | ||||||
|  | import bytes from '@client/filters/bytes'; | ||||||
|  | import * as symbols from '@client/symbols'; | ||||||
|  | import { url } from '@client/config'; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		FormBase, | ||||||
|  | 		FormTextarea, | ||||||
|  | 		FormObjectView, | ||||||
|  | 		FormButton, | ||||||
|  | 		FormLink, | ||||||
|  | 		FormGroup, | ||||||
|  | 		FormKeyValueView, | ||||||
|  | 		FormSuspense, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	props: { | ||||||
|  | 		userId: { | ||||||
|  | 			type: String, | ||||||
|  | 			required: true | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			[symbols.PAGE_INFO]: computed(() => ({ | ||||||
|  | 				title: this.$ts.userInfo, | ||||||
|  | 				icon: faInfoCircle, | ||||||
|  | 				actions: this.user ? [this.user.url ? { | ||||||
|  | 					text: this.user.url, | ||||||
|  | 					icon: faExternalLinkAlt, | ||||||
|  | 					handler: () => { | ||||||
|  | 						window.open(this.user.url, '_blank'); | ||||||
|  | 					} | ||||||
|  | 				} : undefined].filter(x => x !== undefined) : [], | ||||||
|  | 			})), | ||||||
|  | 			user: null, | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	mounted() { | ||||||
|  | 		this.fetch(); | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		number, | ||||||
|  | 		bytes, | ||||||
|  | 
 | ||||||
|  | 		async fetch() { | ||||||
|  | 			this.user = await os.api('users/show', { | ||||||
|  | 				userId: this.userId | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | @ -1,14 +1,16 @@ | ||||||
| <template> | <template> | ||||||
| <MkContainer> | <MkContainer :max-height="300" :foldable="true"> | ||||||
| 	<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $ts.images }}</template> | 	<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $ts.images }}</template> | ||||||
| 	<div class="ujigsodd"> | 	<div class="ujigsodd"> | ||||||
| 		<MkLoading v-if="fetching"/> | 		<MkLoading v-if="fetching"/> | ||||||
| 		<div class="stream" v-if="!fetching && images.length > 0"> | 		<div class="stream" v-if="!fetching && images.length > 0"> | ||||||
| 			<MkA v-for="image in images" | 			<MkA v-for="image in images" | ||||||
| 				class="img" | 				class="img" | ||||||
| 				:style="`background-image: url(${thumbnail(image.file)})`" |  | ||||||
| 				:to="notePage(image.note)" | 				:to="notePage(image.note)" | ||||||
| 			></MkA> | 				:key="image.id" | ||||||
|  | 			> | ||||||
|  | 				<ImgWithBlurhash :hash="image.blurhash" :src="thumbnail(image.file)" :alt="image.name" :title="image.name"/> | ||||||
|  | 			</MkA> | ||||||
| 		</div> | 		</div> | ||||||
| 		<p class="empty" v-if="!fetching && images.length == 0">{{ $ts.nothing }}</p> | 		<p class="empty" v-if="!fetching && images.length == 0">{{ $ts.nothing }}</p> | ||||||
| 	</div> | 	</div> | ||||||
|  | @ -22,10 +24,12 @@ import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; | ||||||
| import notePage from '../../filters/note'; | import notePage from '../../filters/note'; | ||||||
| import * as os from '@client/os'; | import * as os from '@client/os'; | ||||||
| import MkContainer from '@client/components/ui/container.vue'; | import MkContainer from '@client/components/ui/container.vue'; | ||||||
|  | import ImgWithBlurhash from '@client/components/img-with-blurhash.vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkContainer, | 		MkContainer, | ||||||
|  | 		ImgWithBlurhash, | ||||||
| 	}, | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		user: { | 		user: { | ||||||
|  | @ -52,16 +56,14 @@ export default defineComponent({ | ||||||
| 			userId: this.user.id, | 			userId: this.user.id, | ||||||
| 			fileType: image, | 			fileType: image, | ||||||
| 			excludeNsfw: this.$store.state.nsfw !== 'ignore', | 			excludeNsfw: this.$store.state.nsfw !== 'ignore', | ||||||
| 			limit: 9, | 			limit: 10, | ||||||
| 		}).then(notes => { | 		}).then(notes => { | ||||||
| 			for (const note of notes) { | 			for (const note of notes) { | ||||||
| 				for (const file of note.files) { | 				for (const file of note.files) { | ||||||
| 					if (this.images.length < 9) { | 					this.images.push({ | ||||||
| 						this.images.push({ | 						note, | ||||||
| 							note, | 						file | ||||||
| 							file | 					}); | ||||||
| 						}); |  | ||||||
| 					} |  | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			this.fetching = false; | 			this.fetching = false; | ||||||
|  | @ -83,20 +85,14 @@ export default defineComponent({ | ||||||
| 	padding: 8px; | 	padding: 8px; | ||||||
| 
 | 
 | ||||||
| 	> .stream { | 	> .stream { | ||||||
| 		display: flex; | 		display: grid; | ||||||
| 		justify-content: center; | 		grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | ||||||
| 		flex-wrap: wrap; | 		grid-gap: 6px; | ||||||
| 
 | 
 | ||||||
| 		> .img { | 		> .img { | ||||||
| 			flex: 1 1 33%; | 			height: 128px; | ||||||
| 			width: 33%; |  | ||||||
| 			height: 90px; |  | ||||||
| 			box-sizing: border-box; |  | ||||||
| 			background-position: center center; |  | ||||||
| 			background-size: cover; |  | ||||||
| 			background-clip: content-box; |  | ||||||
| 			border: solid 2px transparent; |  | ||||||
| 			border-radius: 6px; | 			border-radius: 6px; | ||||||
|  | 			overflow: clip; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,7 +5,7 @@ | ||||||
| 		<option value="replies">{{ $ts.notesAndReplies }}</option> | 		<option value="replies">{{ $ts.notesAndReplies }}</option> | ||||||
| 		<option value="files">{{ $ts.withFiles }}</option> | 		<option value="files">{{ $ts.withFiles }}</option> | ||||||
| 	</MkTab> | 	</MkTab> | ||||||
| 	<XNotes ref="timeline" class="_noGap_" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/> | 	<XNotes ref="timeline" :no-gap="true" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -198,6 +198,7 @@ | ||||||
| 					<div v-if="user.pinnedNotes.length > 0"> | 					<div v-if="user.pinnedNotes.length > 0"> | ||||||
| 						<XNote v-for="note in user.pinnedNotes" class="note _block" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/> | 						<XNote v-for="note in user.pinnedNotes" class="note _block" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/> | ||||||
| 					</div> | 					</div> | ||||||
|  | 					<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo> | ||||||
| 					<XPhotos :user="user" :key="user.id"/> | 					<XPhotos :user="user" :key="user.id"/> | ||||||
| 					<XActivity :user="user" :key="user.id"/> | 					<XActivity :user="user" :key="user.id"/> | ||||||
| 				</div> | 				</div> | ||||||
|  | @ -229,6 +230,7 @@ import MkContainer from '@client/components/ui/container.vue'; | ||||||
| import MkFolder from '@client/components/ui/folder.vue'; | import MkFolder from '@client/components/ui/folder.vue'; | ||||||
| import MkRemoteCaution from '@client/components/remote-caution.vue'; | import MkRemoteCaution from '@client/components/remote-caution.vue'; | ||||||
| import MkTab from '@client/components/tab.vue'; | import MkTab from '@client/components/tab.vue'; | ||||||
|  | import MkInfo from '@client/components/ui/info.vue'; | ||||||
| import Progress from '@client/scripts/loading'; | import Progress from '@client/scripts/loading'; | ||||||
| import parseAcct from '@/misc/acct/parse'; | import parseAcct from '@/misc/acct/parse'; | ||||||
| import { getScrollPosition } from '@client/scripts/scroll'; | import { getScrollPosition } from '@client/scripts/scroll'; | ||||||
|  | @ -247,6 +249,7 @@ export default defineComponent({ | ||||||
| 		MkRemoteCaution, | 		MkRemoteCaution, | ||||||
| 		MkFolder, | 		MkFolder, | ||||||
| 		MkTab, | 		MkTab, | ||||||
|  | 		MkInfo, | ||||||
| 		XFollowList: defineAsyncComponent(() => import('./follow-list.vue')), | 		XFollowList: defineAsyncComponent(() => import('./follow-list.vue')), | ||||||
| 		XClips: defineAsyncComponent(() => import('./clips.vue')), | 		XClips: defineAsyncComponent(() => import('./clips.vue')), | ||||||
| 		XPages: defineAsyncComponent(() => import('./pages.vue')), | 		XPages: defineAsyncComponent(() => import('./pages.vue')), | ||||||
|  | @ -276,6 +279,7 @@ export default defineComponent({ | ||||||
| 				share: { | 				share: { | ||||||
| 					title: this.user.name, | 					title: this.user.name, | ||||||
| 				}, | 				}, | ||||||
|  | 				menu: () => getUserMenu(this.user), | ||||||
| 			} : null), | 			} : null), | ||||||
| 			user: null, | 			user: null, | ||||||
| 			error: null, | 			error: null, | ||||||
|  |  | ||||||
|  | @ -82,6 +82,9 @@ export const router = createRouter({ | ||||||
| 		{ path: '/instance/abuses', component: page('instance/abuses') }, | 		{ path: '/instance/abuses', component: page('instance/abuses') }, | ||||||
| 		{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) }, | 		{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) }, | ||||||
| 		{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) }, | 		{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) }, | ||||||
|  | 		{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) }, | ||||||
|  | 		{ path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) }, | ||||||
|  | 		{ path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) }, | ||||||
| 		{ path: '/games/reversi', component: page('reversi/index') }, | 		{ path: '/games/reversi', component: page('reversi/index') }, | ||||||
| 		{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) }, | 		{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) }, | ||||||
| 		{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') }, | 		{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') }, | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; | import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug, faExclamationCircle, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons'; | import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { i18n } from '@client/i18n'; | import { i18n } from '@client/i18n'; | ||||||
| import copyToClipboard from '@client/scripts/copy-to-clipboard'; | import copyToClipboard from '@client/scripts/copy-to-clipboard'; | ||||||
|  | @ -126,6 +126,12 @@ export function getUserMenu(user) { | ||||||
| 		action: () => { | 		action: () => { | ||||||
| 			copyToClipboard(`@${user.username}@${user.host || host}`); | 			copyToClipboard(`@${user.username}@${user.host || host}`); | ||||||
| 		} | 		} | ||||||
|  | 	}, { | ||||||
|  | 		icon: faInfoCircle, | ||||||
|  | 		text: i18n.locale.info, | ||||||
|  | 		action: () => { | ||||||
|  | 			os.pageWindow(`/user-info/${user.id}`); | ||||||
|  | 		} | ||||||
| 	}, { | 	}, { | ||||||
| 		icon: faEnvelope, | 		icon: faEnvelope, | ||||||
| 		text: i18n.locale.sendMessage, | 		text: i18n.locale.sendMessage, | ||||||
|  |  | ||||||
|  | @ -184,10 +184,6 @@ export const defaultStore = markRaw(new Storage('base', { | ||||||
| 		where: 'device', | 		where: 'device', | ||||||
| 		default: 'full' as 'full' | 'icon' | 		default: 'full' as 'full' | 'icon' | ||||||
| 	}, | 	}, | ||||||
| 	titlebar: { |  | ||||||
| 		where: 'device', |  | ||||||
| 		default: true |  | ||||||
| 	}, |  | ||||||
| 	reportError: { | 	reportError: { | ||||||
| 		where: 'device', | 		where: 'device', | ||||||
| 		default: false | 		default: false | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ html { | ||||||
| 	background-position: center; | 	background-position: center; | ||||||
| 	color: var(--fg); | 	color: var(--fg); | ||||||
| 	overflow: auto; | 	overflow: auto; | ||||||
|  | 	overflow-wrap: break-word; | ||||||
| 	font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; | 	font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; | ||||||
| 	line-height: 1.35; | 	line-height: 1.35; | ||||||
| 	text-size-adjust: 100%; | 	text-size-adjust: 100%; | ||||||
|  | @ -88,10 +89,6 @@ html._themeChanging_ { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| body { |  | ||||||
| 	overflow-wrap: break-word; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| html, body { | html, body { | ||||||
| 	margin: 0; | 	margin: 0; | ||||||
| 	padding: 0; | 	padding: 0; | ||||||
|  | @ -366,10 +363,6 @@ hr { | ||||||
| 		border-radius: var(--radius); | 		border-radius: var(--radius); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	._magnet { |  | ||||||
| 		margin-bottom: calc(var(--margin) * -1); |  | ||||||
| 	}	 |  | ||||||
| 
 |  | ||||||
| 	@media (max-width: 500px) { | 	@media (max-width: 500px) { | ||||||
| 		._root { | 		._root { | ||||||
| 			--root-margin: 0; | 			--root-margin: 0; | ||||||
|  | @ -377,6 +370,12 @@ hr { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | ._magnetParent { | ||||||
|  | 	._magnetChild:not(* + ._magnetChild) { | ||||||
|  | 		margin-top: 0; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| ._narrow_ ._card { | ._narrow_ ._card { | ||||||
| 	> ._title { | 	> ._title { | ||||||
| 		padding: 16px; | 		padding: 16px; | ||||||
|  | @ -456,7 +455,7 @@ hr { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ._monospace { | ._monospace { | ||||||
| 	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; | 	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ._code { | ._code { | ||||||
|  |  | ||||||
|  | @ -16,8 +16,6 @@ | ||||||
| 		panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)', | 		panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)', | ||||||
| 		panelHeaderBg: '@panel', | 		panelHeaderBg: '@panel', | ||||||
| 		panelHeaderDivider: '@divider', | 		panelHeaderDivider: '@divider', | ||||||
| 		infoFg: '@accent', |  | ||||||
| 		infoBg: 'rgb(0, 0, 0)', |  | ||||||
| 		header: ':alpha<0.7<@panel', | 		header: ':alpha<0.7<@panel', | ||||||
| 		navBg: '#363636', | 		navBg: '#363636', | ||||||
| 		renote: '@accent', | 		renote: '@accent', | ||||||
|  |  | ||||||
|  | @ -5,6 +5,8 @@ | ||||||
| 		<div>{{ $ts.noNotes }}</div> | 		<div>{{ $ts.noNotes }}</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
|  | 	<MkLoading v-if="fetching"/> | ||||||
|  | 
 | ||||||
| 	<MkError 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);"> | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
| 		<main class="main _panel" @contextmenu.stop="onContextmenu"> | 		<main class="main _panel" @contextmenu.stop="onContextmenu"> | ||||||
| 			<header v-if="$store.state.titlebar" class="header" @click="onHeaderClick"> | 			<header class="header" @click="onHeaderClick"> | ||||||
| 				<XHeader :info="pageInfo"/> | 				<XHeader :info="pageInfo"/> | ||||||
| 			</header> | 			</header> | ||||||
| 			<div class="content" :class="{ _flat_: !fullView }"> | 			<div class="content" :class="{ _flat_: !fullView }"> | ||||||
|  | @ -350,6 +350,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 		> .widgets { | 		> .widgets { | ||||||
| 			//--panelShadow: none; | 			//--panelShadow: none; | ||||||
|  | 			width: 300px; | ||||||
| 
 | 
 | ||||||
| 			@media (max-width: $widgets-hide-threshold) { | 			@media (max-width: $widgets-hide-threshold) { | ||||||
| 				display: none; | 				display: none; | ||||||
|  |  | ||||||
|  | @ -2,8 +2,8 @@ | ||||||
| <div class="efzpzdvf"> | <div class="efzpzdvf"> | ||||||
| 	<XWidgets :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> | 	<XWidgets :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> | ||||||
| 
 | 
 | ||||||
| 	<button v-if="editMode" @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button> | 	<button v-if="editMode" @click="editMode = false" class="_textButton edit" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button> | ||||||
| 	<button v-else @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button> | 	<button v-else @click="editMode = true" class="_textButton edit" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -62,18 +62,11 @@ export default defineComponent({ | ||||||
| 	position: sticky; | 	position: sticky; | ||||||
| 	height: min-content; | 	height: min-content; | ||||||
| 	box-sizing: border-box; | 	box-sizing: border-box; | ||||||
|  | 	padding-bottom: 8px; | ||||||
| 
 | 
 | ||||||
| 	> * { | 	> .edit { | ||||||
| 		margin: var(--margin) 0; | 		display: block; | ||||||
| 		width: 300px; | 		margin: 16px auto; | ||||||
| 
 |  | ||||||
| 		&:first-child { |  | ||||||
| 			margin-top: 0; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .add { |  | ||||||
| 		margin: 0 auto; |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -2,8 +2,8 @@ | ||||||
| <div class="mk-app" :class="{ wallpaper }"> | <div class="mk-app" :class="{ wallpaper }"> | ||||||
| 	<XSidebar ref="nav" class="sidebar"/> | 	<XSidebar ref="nav" class="sidebar"/> | ||||||
| 
 | 
 | ||||||
| 	<div class="contents" ref="contents" :class="{ withHeader: $store.state.titlebar }" @contextmenu.stop="onContextmenu"> | 	<div class="contents" ref="contents" @contextmenu.stop="onContextmenu"> | ||||||
| 		<header v-if="$store.state.titlebar" class="header" ref="header" @click="onHeaderClick"> | 		<header class="header" ref="header" @click="onHeaderClick"> | ||||||
| 			<XHeader :info="pageInfo"/> | 			<XHeader :info="pageInfo"/> | ||||||
| 		</header> | 		</header> | ||||||
| 		<main ref="main"> | 		<main ref="main"> | ||||||
|  | @ -260,10 +260,7 @@ export default defineComponent({ | ||||||
| 		width: 100%; | 		width: 100%; | ||||||
| 		min-width: 0; | 		min-width: 0; | ||||||
| 		--stickyTop: #{$header-height}; | 		--stickyTop: #{$header-height}; | ||||||
| 
 | 		padding-top: $header-height; | ||||||
| 		&.withHeader { |  | ||||||
| 			padding-top: $header-height; |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		> .header { | 		> .header { | ||||||
| 			position: fixed; | 			position: fixed; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <template> | ||||||
| <MkContainer :show-header="props.showHeader" :body-togglable="bodyTogglable" :scrollable="scrollable"> | <MkContainer :show-header="props.showHeader" :foldable="foldable" :scrollable="scrollable"> | ||||||
| 	<template #header><Fa :icon="faGlobe"/>{{ $ts._widgets.federation }}</template> | 	<template #header><Fa :icon="faGlobe"/>{{ $ts._widgets.federation }}</template> | ||||||
| 
 | 
 | ||||||
| 	<div class="wbrkwalb"> | 	<div class="wbrkwalb"> | ||||||
|  | @ -42,7 +42,7 @@ export default defineComponent({ | ||||||
| 		MkContainer, MkMiniChart | 		MkContainer, MkMiniChart | ||||||
| 	}, | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		bodyTogglable: { | 		foldable: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="deliver"> | 	<div class="deliver"> | ||||||
| 		<div class="label">Deliver queue<Fa :icon="faExclamationTriangle" v-if="inbox.waiting > 0" class="icon"/></div> | 		<div class="label">Deliver queue<Fa :icon="faExclamationTriangle" v-if="deliver.waiting > 0" class="icon"/></div> | ||||||
| 		<div class="values"> | 		<div class="values"> | ||||||
| 			<div> | 			<div> | ||||||
| 				<div>Process</div> | 				<div>Process</div> | ||||||
|  |  | ||||||
|  | @ -195,6 +195,7 @@ export class UserRepository extends Repository<User> { | ||||||
| 
 | 
 | ||||||
| 			...(opts.detail ? { | 			...(opts.detail ? { | ||||||
| 				url: profile!.url, | 				url: profile!.url, | ||||||
|  | 				uri: user.uri, | ||||||
| 				createdAt: user.createdAt.toISOString(), | 				createdAt: user.createdAt.toISOString(), | ||||||
| 				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, | 				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, | ||||||
| 				bannerUrl: user.bannerUrl, | 				bannerUrl: user.bannerUrl, | ||||||
|  |  | ||||||
							
								
								
									
										38
									
								
								src/server/api/endpoints/ap/get.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/server/api/endpoints/ap/get.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import define from '../../define'; | ||||||
|  | import Resolver from '../../../../remote/activitypub/resolver'; | ||||||
|  | import { ApiError } from '../../error'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['federation'], | ||||||
|  | 
 | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': 'URIを指定してActivityPubオブジェクトを参照します。', | ||||||
|  | 		'en-US': 'Browse to the ActivityPub object by specifying the URI.' | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	requireCredential: false as const, | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		uri: { | ||||||
|  | 			validator: $.str, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'ActivityPubオブジェクトのURI' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	res: { | ||||||
|  | 		type: 'object' as const, | ||||||
|  | 		optional: false as const, nullable: false as const, | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps) => { | ||||||
|  | 	const resolver = new Resolver(); | ||||||
|  | 	const object = await resolver.resolve(ps.uri); | ||||||
|  | 	return object; | ||||||
|  | }); | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue