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: "いいね解除しますか?" | ||||
| fullView: "フルビュー" | ||||
| quitFullView: "フルビュー解除" | ||||
| addDescription: "説明を追加" | ||||
| userPagePinTip: "個々のノートのメニューから「ピン留め」を選択することで、ここにノートを表示しておくことができます。" | ||||
| notSpecifiedMentionWarning: "宛先に含まれていないメンションがあります" | ||||
| info: "情報" | ||||
| userInfo: "ユーザー情報" | ||||
| unknown: "不明" | ||||
| 
 | ||||
| _email: | ||||
|   _follow: | ||||
|  | @ -884,6 +890,7 @@ _theme: | |||
|   install: "テーマのインストール" | ||||
|   manage: "テーマの管理" | ||||
|   code: "テーマコード" | ||||
|   description: "説明" | ||||
|   installed: "{name}をインストールしました" | ||||
|   installedThemes: "インストールされたテーマ" | ||||
|   builtinThemes: "標準のテーマ" | ||||
|  |  | |||
							
								
								
									
										94
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										94
									
								
								package.json
									
										
									
									
									
								
							|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	"name": "misskey", | ||||
| 	"author": "syuilo <syuilotan@yahoo.co.jp>", | ||||
| 	"version": "12.75.1", | ||||
| 	"version": "12.76.1-beta.1", | ||||
| 	"codename": "indigo", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
|  | @ -37,7 +37,7 @@ | |||
| 		"lodash": "^4.17.20" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@babel/plugin-transform-runtime": "7.13.10", | ||||
| 		"@babel/plugin-transform-runtime": "7.13.15", | ||||
| 		"@elastic/elasticsearch": "7.11.0", | ||||
| 		"@fortawesome/fontawesome-svg-core": "1.2.35", | ||||
| 		"@fortawesome/free-brands-svg-icons": "5.15.3", | ||||
|  | @ -49,7 +49,7 @@ | |||
| 		"@koa/router": "9.0.1", | ||||
| 		"@sentry/browser": "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", | ||||
| 		"@types/bcryptjs": "2.4.2", | ||||
| 		"@types/bull": "3.15.0", | ||||
|  | @ -62,7 +62,7 @@ | |||
| 		"@types/gulp-replace": "0.0.31", | ||||
| 		"@types/is-url": "1.2.28", | ||||
| 		"@types/js-yaml": "4.0.0", | ||||
| 		"@types/jsdom": "16.2.7", | ||||
| 		"@types/jsdom": "16.2.10", | ||||
| 		"@types/jsonld": "1.5.5", | ||||
| 		"@types/katex": "0.11.0", | ||||
| 		"@types/koa": "2.13.1", | ||||
|  | @ -77,10 +77,10 @@ | |||
| 		"@types/koa__multer": "2.0.2", | ||||
| 		"@types/koa__router": "8.0.4", | ||||
| 		"@types/markdown-it": "12.0.1", | ||||
| 		"@types/matter-js": "0.14.10", | ||||
| 		"@types/mocha": "8.2.1", | ||||
| 		"@types/node": "14.14.35", | ||||
| 		"@types/node-fetch": "2.5.8", | ||||
| 		"@types/matter-js": "0.14.11", | ||||
| 		"@types/mocha": "8.2.2", | ||||
| 		"@types/node": "14.14.41", | ||||
| 		"@types/node-fetch": "2.5.10", | ||||
| 		"@types/nodemailer": "6.4.1", | ||||
| 		"@types/nprogress": "0.2.0", | ||||
| 		"@types/oauth": "0.9.1", | ||||
|  | @ -97,7 +97,7 @@ | |||
| 		"@types/request-stats": "3.0.0", | ||||
| 		"@types/rimraf": "3.0.0", | ||||
| 		"@types/seedrandom": "2.4.28", | ||||
| 		"@types/sharp": "0.27.1", | ||||
| 		"@types/sharp": "0.28.0", | ||||
| 		"@types/sinonjs__fake-timers": "6.0.2", | ||||
| 		"@types/speakeasy": "2.0.5", | ||||
| 		"@types/throttle-debounce": "2.1.0", | ||||
|  | @ -105,39 +105,39 @@ | |||
| 		"@types/tmp": "0.2.0", | ||||
| 		"@types/uuid": "8.3.0", | ||||
| 		"@types/web-push": "3.3.0", | ||||
| 		"@types/webpack": "4.41.26", | ||||
| 		"@types/webpack-stream": "3.2.11", | ||||
| 		"@types/webpack": "5.28.0", | ||||
| 		"@types/webpack-stream": "3.2.12", | ||||
| 		"@types/websocket": "1.0.2", | ||||
| 		"@types/ws": "7.4.0", | ||||
| 		"@typescript-eslint/parser": "4.18.0", | ||||
| 		"@vue/compiler-sfc": "3.0.8", | ||||
| 		"@types/ws": "7.4.1", | ||||
| 		"@typescript-eslint/parser": "4.22.0", | ||||
| 		"@vue/compiler-sfc": "3.0.11", | ||||
| 		"abort-controller": "3.0.0", | ||||
| 		"apexcharts": "3.26.0", | ||||
| 		"autobind-decorator": "2.4.0", | ||||
| 		"autosize": "4.0.2", | ||||
| 		"autwh": "0.1.0", | ||||
| 		"aws-sdk": "2.867.0", | ||||
| 		"aws-sdk": "2.887.0", | ||||
| 		"bcryptjs": "2.4.3", | ||||
| 		"blurhash": "1.1.3", | ||||
| 		"broadcast-channel": "3.5.3", | ||||
| 		"bull": "3.21.1", | ||||
| 		"bull": "3.22.0", | ||||
| 		"cafy": "15.2.1", | ||||
| 		"cbor": "7.0.4", | ||||
| 		"cbor": "7.0.5", | ||||
| 		"chalk": "4.1.0", | ||||
| 		"chart.js": "2.9.4", | ||||
| 		"cli-highlight": "2.1.10", | ||||
| 		"commander": "4.1.1", | ||||
| 		"concurrently": "6.0.0", | ||||
| 		"cli-highlight": "2.1.11", | ||||
| 		"commander": "7.2.0", | ||||
| 		"concurrently": "6.0.2", | ||||
| 		"content-disposition": "0.5.3", | ||||
| 		"core-js": "3.9.1", | ||||
| 		"core-js": "3.10.1", | ||||
| 		"crc-32": "1.2.0", | ||||
| 		"css-loader": "5.1.3", | ||||
| 		"cssnano": "4.1.10", | ||||
| 		"css-loader": "5.2.1", | ||||
| 		"cssnano": "5.0.1", | ||||
| 		"dateformat": "4.5.1", | ||||
| 		"diskusage": "1.1.3", | ||||
| 		"escape-regexp": "0.0.1", | ||||
| 		"eslint": "7.22.0", | ||||
| 		"eslint-plugin-vue": "7.7.0", | ||||
| 		"eslint": "7.24.0", | ||||
| 		"eslint-plugin-vue": "7.9.0", | ||||
| 		"eventemitter3": "4.0.7", | ||||
| 		"feed": "4.2.2", | ||||
| 		"fibers": "5.0.0", | ||||
|  | @ -156,17 +156,17 @@ | |||
| 		"http-proxy-agent": "4.0.1", | ||||
| 		"http-signature": "1.3.5", | ||||
| 		"https-proxy-agent": "5.0.0", | ||||
| 		"idb-keyval": "5.0.4", | ||||
| 		"idb-keyval": "5.0.5", | ||||
| 		"insert-text-at-cursor": "0.3.0", | ||||
| 		"is-root": "2.1.0", | ||||
| 		"is-svg": "4.3.1", | ||||
| 		"js-yaml": "4.0.0", | ||||
| 		"jsdom": "16.5.1", | ||||
| 		"js-yaml": "4.1.0", | ||||
| 		"jsdom": "16.5.3", | ||||
| 		"json5": "2.2.0", | ||||
| 		"json5-loader": "4.0.1", | ||||
| 		"jsonld": "4.0.1", | ||||
| 		"jsrsasign": "8.0.20", | ||||
| 		"katex": "0.13.0", | ||||
| 		"katex": "0.13.2", | ||||
| 		"koa": "2.13.1", | ||||
| 		"koa-bodyparser": "4.3.0", | ||||
| 		"koa-favicon": "2.1.0", | ||||
|  | @ -178,10 +178,10 @@ | |||
| 		"koa-views": "7.0.1", | ||||
| 		"langmap": "0.0.16", | ||||
| 		"lookup-dns-cache": "2.1.0", | ||||
| 		"markdown-it": "12.0.4", | ||||
| 		"markdown-it": "12.0.5", | ||||
| 		"markdown-it-anchor": "7.1.0", | ||||
| 		"matter-js": "0.16.1", | ||||
| 		"mfm-js": "0.14.0", | ||||
| 		"matter-js": "0.17.1", | ||||
| 		"mfm-js": "0.15.0", | ||||
| 		"mocha": "8.3.2", | ||||
| 		"moji": "0.5.1", | ||||
| 		"ms": "2.1.3", | ||||
|  | @ -192,23 +192,23 @@ | |||
| 		"object-assign-deep": "0.4.0", | ||||
| 		"os-utils": "0.0.14", | ||||
| 		"parse5": "6.0.1", | ||||
| 		"pg": "8.5.1", | ||||
| 		"pg": "8.6.0", | ||||
| 		"portscanner": "2.2.0", | ||||
| 		"postcss": "8.2.8", | ||||
| 		"postcss": "8.2.10", | ||||
| 		"postcss-loader": "5.2.0", | ||||
| 		"prismjs": "1.23.0", | ||||
| 		"probe-image-size": "7.0.1", | ||||
| 		"probe-image-size": "7.1.0", | ||||
| 		"promise-limit": "2.7.0", | ||||
| 		"promise-sequential": "1.1.1", | ||||
| 		"pug": "3.0.2", | ||||
| 		"punycode": "2.1.1", | ||||
| 		"pureimage": "0.2.7", | ||||
| 		"pureimage": "0.3.2", | ||||
| 		"qrcode": "1.4.4", | ||||
| 		"random-seed": "0.3.0", | ||||
| 		"ratelimiter": "3.4.1", | ||||
| 		"re2": "1.15.9", | ||||
| 		"reconnecting-websocket": "4.4.0", | ||||
| 		"redis": "3.0.2", | ||||
| 		"redis": "3.1.1", | ||||
| 		"redis-lock": "0.1.4", | ||||
| 		"reflect-metadata": "0.1.13", | ||||
| 		"regenerator-runtime": "0.13.7", | ||||
|  | @ -221,32 +221,32 @@ | |||
| 		"sass": "1.32.8", | ||||
| 		"sass-loader": "11.0.1", | ||||
| 		"seedrandom": "3.0.5", | ||||
| 		"sharp": "0.27.2", | ||||
| 		"sharp": "0.28.1", | ||||
| 		"speakeasy": "2.0.0", | ||||
| 		"stringz": "2.1.0", | ||||
| 		"style-loader": "2.0.0", | ||||
| 		"summaly": "2.4.0", | ||||
| 		"syslog-pro": "1.0.0", | ||||
| 		"systeminformation": "5.6.7", | ||||
| 		"systeminformation": "5.6.12", | ||||
| 		"syuilo-password-strength": "0.0.1", | ||||
| 		"textarea-caret": "3.1.0", | ||||
| 		"three": "0.117.1", | ||||
| 		"throttle-debounce": "3.0.1", | ||||
| 		"tinycolor2": "1.4.2", | ||||
| 		"tmp": "0.2.1", | ||||
| 		"ts-loader": "8.0.18", | ||||
| 		"ts-loader": "8.1.0", | ||||
| 		"ts-node": "9.1.1", | ||||
| 		"tsc-alias": "1.2.8", | ||||
| 		"tsc-alias": "1.2.9", | ||||
| 		"tsconfig-paths": "3.9.0", | ||||
| 		"tslint": "6.1.3", | ||||
| 		"tslint-sonarts": "1.9.0", | ||||
| 		"typeorm": "0.2.31", | ||||
| 		"typescript": "4.2.3", | ||||
| 		"typeorm": "0.2.32", | ||||
| 		"typescript": "4.2.4", | ||||
| 		"ulid": "2.3.0", | ||||
| 		"uuid": "8.3.2", | ||||
| 		"v-debounce": "0.1.2", | ||||
| 		"vanilla-tilt": "1.7.0", | ||||
| 		"vue": "3.0.8", | ||||
| 		"vue": "3.0.11", | ||||
| 		"vue-color": "2.8.1", | ||||
| 		"vue-json-pretty": "1.7.1", | ||||
| 		"vue-loader": "16.1.2", | ||||
|  | @ -256,9 +256,9 @@ | |||
| 		"vue-svg-loader": "0.17.0-beta.2", | ||||
| 		"vuedraggable": "4.0.1", | ||||
| 		"web-push": "3.4.4", | ||||
| 		"webpack": "5.27.2", | ||||
| 		"webpack-cli": "4.5.0", | ||||
| 		"websocket": "1.0.33", | ||||
| 		"webpack": "5.33.2", | ||||
| 		"webpack-cli": "4.6.0", | ||||
| 		"websocket": "1.0.34", | ||||
| 		"ws": "7.4.4", | ||||
| 		"xev": "2.0.1" | ||||
| 	}, | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| import * as program from 'commander'; | ||||
| import { Command } from 'commander'; | ||||
| import config from '@/config'; | ||||
| 
 | ||||
| const program = new Command(); | ||||
| 
 | ||||
| program | ||||
| 	.version(config.version) | ||||
| 	.option('--no-daemons', 'Disable daemon processes (for debbuging)') | ||||
|  |  | |||
|  | @ -18,17 +18,20 @@ export default defineComponent({ | |||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		} | ||||
| 		}, | ||||
| 		noGap: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		focus() { | ||||
| 			this.$slots.default[0].elm.focus(); | ||||
| 		} | ||||
| 		}, | ||||
| 
 | ||||
| 	render() { | ||||
| 		const getDateText = (time: string) => { | ||||
| 		getDateText(time: string) { | ||||
| 			const date = new Date(time).getDate(); | ||||
| 			const month = new Date(time).getMonth() + 1; | ||||
| 			return this.$t('monthAndDay', { | ||||
|  | @ -36,17 +39,19 @@ export default defineComponent({ | |||
| 				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 ? { | ||||
| 			class: 'sqadhkmv' + (noGap ? ' _block' : ''), | ||||
| 			class: 'sqadhkmv' + (this.noGap ? ' noGap _block' : ''), | ||||
| 			name: 'list', | ||||
| 			tag: 'div', | ||||
| 			'data-direction': this.direction, | ||||
| 			'data-reversed': this.reversed ? 'true' : 'false', | ||||
| 		} : { | ||||
| 			class: 'sqadhkmv', | ||||
| 			class: 'sqadhkmv' + (this.noGap ? ' noGap _block' : ''), | ||||
| 		}, this.items.map((item, i) => { | ||||
| 			const el = this.$slots.default({ | ||||
| 				item: item | ||||
|  | @ -72,10 +77,10 @@ export default defineComponent({ | |||
| 							class: 'icon', | ||||
| 							icon: faAngleUp, | ||||
| 						}), | ||||
| 						getDateText(item.createdAt) | ||||
| 						this.getDateText(item.createdAt) | ||||
| 					]), | ||||
| 					h('span', [ | ||||
| 						getDateText(this.items[i + 1].createdAt), | ||||
| 						this.getDateText(this.items[i + 1].createdAt), | ||||
| 						h(FontAwesomeIcon, { | ||||
| 							class: 'icon', | ||||
| 							icon: faAngleDown, | ||||
|  | @ -152,9 +157,8 @@ export default defineComponent({ | |||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| ._noGap_ .sqadhkmv { | ||||
| 	&.noGap { | ||||
| 		> * { | ||||
| 			margin: 0 !important; | ||||
| 			border: none; | ||||
|  | @ -166,6 +170,7 @@ export default defineComponent({ | |||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| ._inContainer_ .sqadhkmv > * { | ||||
| 	margin: 0 !important; | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ import { | |||
| 	faFileArchive, | ||||
| 	faFilm | ||||
| 	} 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'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|  |  | |||
|  | @ -24,9 +24,12 @@ export default defineComponent({ | |||
| 	--formXPadding: 32px; | ||||
| 	--formYPadding: 32px; | ||||
| 
 | ||||
| 	font-size: 95%; | ||||
| 	line-height: 1.3em; | ||||
| 	background: var(--bg); | ||||
| 	padding: var(--formYPadding) var(--formXPadding); | ||||
| 	max-width: 750px; | ||||
| 	margin: 0 auto; | ||||
| 
 | ||||
| 	&:not(.wide).max-width_400px { | ||||
| 		--formXPadding: 0px; | ||||
|  | @ -40,16 +43,16 @@ export default defineComponent({ | |||
| 			} | ||||
| 
 | ||||
| 			._form_group { | ||||
| 				> * { | ||||
| 					&:not(:first-child) { | ||||
| 				> *:not(._formNoConcat) { | ||||
| 					&:not(:last-child):not(._formNoConcatPrev) { | ||||
| 						&._formPanel, ._formPanel { | ||||
| 							border-top: none; | ||||
| 							border-bottom: solid 0.5px var(--divider); | ||||
| 						} | ||||
| 					} | ||||
| 
 | ||||
| 					&:not(:last-child) { | ||||
| 					&:not(:first-child):not(._formNoConcatNext) { | ||||
| 						&._formPanel, ._formPanel { | ||||
| 							border-bottom: solid 0.5px var(--divider); | ||||
| 							border-top: none; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <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="main _form_group"> | ||||
| 	<div class="main _form_group" ref="child"> | ||||
| 		<slot></slot> | ||||
| 	</div> | ||||
| 	<div class="_formCaption"><slot name="caption"></slot></div> | ||||
|  | @ -9,33 +9,69 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { defineComponent, onMounted, ref } from 'vue'; | ||||
| 
 | ||||
| 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> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .vrtktovg { | ||||
| 	> .main { | ||||
| 		> ::v-deep(*) { | ||||
| 		> ::v-deep(*):not(._formNoConcat) { | ||||
| 			&: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 { | ||||
| 					border-bottom: solid 0.5px var(--divider); | ||||
| 					border-bottom-left-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; | ||||
| 	padding: 14px 16px; | ||||
| 
 | ||||
| 	> .key { | ||||
| 		margin-right: 12px; | ||||
| 		white-space: nowrap; | ||||
| 	} | ||||
| 
 | ||||
| 	> .value { | ||||
| 		margin-left: auto; | ||||
| 		opacity: 0.7; | ||||
| 		text-overflow: ellipsis; | ||||
| 		white-space: nowrap; | ||||
| 		overflow: hidden; | ||||
| 	} | ||||
| } | ||||
| </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> | ||||
| <div class="yxspomdl" :class="{ inline }"> | ||||
| <div class="yxspomdl" :class="{ inline, colored }"> | ||||
| 	<div class="ring"></div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import * as os from '@client/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
|  | @ -14,6 +13,11 @@ export default defineComponent({ | |||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		colored: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: true | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | @ -32,6 +36,11 @@ export default defineComponent({ | |||
| .yxspomdl { | ||||
| 	padding: 32px; | ||||
| 	text-align: center; | ||||
| 	cursor: wait; | ||||
| 
 | ||||
| 	&.colored { | ||||
| 		color: var(--accent); | ||||
| 	} | ||||
| 
 | ||||
| 	&.inline { | ||||
| 		display: inline; | ||||
|  | @ -41,15 +50,23 @@ export default defineComponent({ | |||
| 			width: 32px; | ||||
| 			height: 32px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .ring { | ||||
| 			&:before, | ||||
| 			&:after { | ||||
| 				width: 32px; | ||||
| 				height: 32px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .ring { | ||||
| 		position: relative; | ||||
| 		display: inline-block; | ||||
| 		opacity: 0.7; | ||||
| 		vertical-align: middle; | ||||
| 	} | ||||
| 
 | ||||
| 	> .ring:after { | ||||
| 		&:before, | ||||
| 		&:after { | ||||
| 			content: " "; | ||||
| 			display: block; | ||||
| 			box-sizing: border-box; | ||||
|  | @ -57,8 +74,19 @@ export default defineComponent({ | |||
| 			height: 48px; | ||||
| 			border-radius: 50%; | ||||
| 			border: solid 4px; | ||||
| 		} | ||||
| 
 | ||||
| 		&: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> | ||||
|  |  | |||
|  | @ -71,6 +71,7 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .xubzgfgb { | ||||
| 	position: relative; | ||||
| 	width: 100%; | ||||
| 	height: 100%; | ||||
| 
 | ||||
|  | @ -82,6 +83,7 @@ export default defineComponent({ | |||
| 	} | ||||
| 
 | ||||
| 	> canvas { | ||||
| 		position: absolute; | ||||
| 		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 { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash'; | ||||
| 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'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
|  |  | |||
|  | @ -58,10 +58,13 @@ export default defineComponent({ | |||
| 					const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); | ||||
| 
 | ||||
| 					if (!this.plain) { | ||||
| 						const x = text.split('\n') | ||||
| 							.map(t => t == '' ? [h('br')] : [t, h('br')]); | ||||
| 						x[x.length - 1].pop(); | ||||
| 						return x; | ||||
| 						const res = []; | ||||
| 						for (const t of text.split('\n')) { | ||||
| 							res.push(h('br')); | ||||
| 							res.push(t); | ||||
| 						} | ||||
| 						res.shift(); | ||||
| 						return res; | ||||
| 					} else { | ||||
| 						return [text.replace(/\n/g, ' ')]; | ||||
| 					} | ||||
|  |  | |||
|  | @ -1,12 +1,15 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<div class="_fullinfo" v-if="empty"> | ||||
| <transition name="fade" mode="out-in"> | ||||
| 	<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"/> | ||||
| 		<div>{{ $ts.noNotes }}</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> | ||||
|  | @ -14,7 +17,7 @@ | |||
| 			</MkButton> | ||||
| 		</div> | ||||
| 
 | ||||
| 	<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> | ||||
| 		<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap"> | ||||
| 			<XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/> | ||||
| 		</XList> | ||||
| 
 | ||||
|  | @ -25,6 +28,7 @@ | |||
| 			</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -55,11 +59,15 @@ export default defineComponent({ | |||
| 		pagination: { | ||||
| 			required: true | ||||
| 		}, | ||||
| 
 | ||||
| 		prop: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		} | ||||
| 		}, | ||||
| 		noGap: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['before', 'after'], | ||||
|  | @ -90,3 +98,14 @@ export default defineComponent({ | |||
| 	} | ||||
| }); | ||||
| </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,7 +1,13 @@ | |||
| <template> | ||||
| <div class="mfcuwfyp _noGap_"> | ||||
| 	<div class="_magnet"></div> | ||||
| 	<XList class="notifications" :items="items" v-slot="{ item: notification }"> | ||||
| <transition name="fade" mode="out-in"> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 
 | ||||
| 	<MkError v-else-if="error" @retry="init()"/> | ||||
| 
 | ||||
| 	<p class="mfcuwfyp" v-else-if="empty">{{ $ts.noNotifications }}</p> | ||||
| 
 | ||||
| 	<div v-else class="_magnetParent"> | ||||
| 		<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> | ||||
|  | @ -10,11 +16,8 @@ | |||
| 			<template v-if="!moreFetching">{{ $ts.loadMore }}</template> | ||||
| 			<template v-if="moreFetching"><MkLoading inline/></template> | ||||
| 		</button> | ||||
| 
 | ||||
| 	<p class="empty" v-if="empty">{{ $ts.noNotifications }}</p> | ||||
| 
 | ||||
| 	<MkError v-if="error" @retry="init()"/> | ||||
| 	</div> | ||||
| </transition> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -121,17 +124,19 @@ export default defineComponent({ | |||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
| 	transition: opacity 0.125s ease; | ||||
| } | ||||
| .fade-enter-from, | ||||
| .fade-leave-to { | ||||
| 	opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .mfcuwfyp { | ||||
| 	> .empty { | ||||
| 	margin: 0; | ||||
| 	padding: 16px; | ||||
| 	text-align: center; | ||||
| 	color: var(--fg); | ||||
| } | ||||
| 
 | ||||
| 	> .placeholder { | ||||
| 		padding: 32px; | ||||
| 		opacity: 0.3; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ | |||
| 				<button @click="addVisibleUser" class="_buttonPrimary"><Fa :icon="faPlus" fixed-width/></button> | ||||
| 			</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"> | ||||
| 		<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"/> | ||||
|  | @ -71,12 +72,14 @@ import { selectFile } from '@client/scripts/select-file'; | |||
| import { notePostInterruptors, postFormActions } from '@client/store'; | ||||
| import { isMobile } from '@client/scripts/is-mobile'; | ||||
| import { throttle } from 'throttle-debounce'; | ||||
| import MkInfo from '@client/components/ui/info.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XNotePreview, | ||||
| 		XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')), | ||||
| 		XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')) | ||||
| 		XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')), | ||||
| 		MkInfo, | ||||
| 	}, | ||||
| 
 | ||||
| 	inject: ['modal'], | ||||
|  | @ -143,6 +146,7 @@ export default defineComponent({ | |||
| 			autocomplete: null, | ||||
| 			draghover: false, | ||||
| 			quoteId: null, | ||||
| 			hasNotSpecifiedMentions: false, | ||||
| 			recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'), | ||||
| 			imeText: '', | ||||
| 			typing: throttle(3000, () => { | ||||
|  | @ -214,6 +218,18 @@ export default defineComponent({ | |||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		text() { | ||||
| 			this.checkMissingMention(); | ||||
| 		}, | ||||
| 		visibleUsers: { | ||||
| 			handler() { | ||||
| 				this.checkMissingMention(); | ||||
| 			}, | ||||
| 			deep: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		if (this.initialText) { | ||||
| 			this.text = this.initialText; | ||||
|  | @ -338,6 +354,32 @@ export default defineComponent({ | |||
| 			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() { | ||||
| 			if (this.poll) { | ||||
| 				this.poll = null; | ||||
|  | @ -741,6 +783,10 @@ export default defineComponent({ | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .hasNotSpecifiedMentions { | ||||
| 			margin: 0 20px 16px 20px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .cw, | ||||
| 		> .text { | ||||
| 			display: block; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <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> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
| 		<div class="title"><slot name="header"></slot></div> | ||||
| 		<div class="sub"> | ||||
| 			<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-else><Fa :icon="faAngleDown"/></template> | ||||
| 			</button> | ||||
|  | @ -16,8 +16,11 @@ | |||
| 		@leave="leave" | ||||
| 		@after-leave="afterLeave" | ||||
| 	> | ||||
| 		<div v-show="showBody"> | ||||
| 		<div v-show="showBody" class="content" :class="{ omitted }" ref="content"> | ||||
| 			<slot></slot> | ||||
| 			<button v-if="omitted" class="fade _button" @click="() => { ignoreOmit = true; omitted = false; }"> | ||||
| 				<span>{{ $ts.showMore }}</span> | ||||
| 			</button> | ||||
| 		</div> | ||||
| 	</transition> | ||||
| </div> | ||||
|  | @ -39,7 +42,7 @@ export default defineComponent({ | |||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		bodyTogglable: { | ||||
| 		foldable: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
|  | @ -54,10 +57,17 @@ export default defineComponent({ | |||
| 			required: false, | ||||
| 			default: false | ||||
| 		}, | ||||
| 		maxHeight: { | ||||
| 			type: Number, | ||||
| 			required: false, | ||||
| 			default: null | ||||
| 		}, | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			showBody: this.expanded, | ||||
| 			omitted: null, | ||||
| 			ignoreOmit: false, | ||||
| 			faAngleUp, faAngleDown | ||||
| 		}; | ||||
| 	}, | ||||
|  | @ -73,10 +83,23 @@ export default defineComponent({ | |||
| 		}, { | ||||
| 			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: { | ||||
| 		toggleContent(show: boolean) { | ||||
| 			if (!this.bodyTogglable) return; | ||||
| 			if (!this.foldable) return; | ||||
| 			this.showBody = show; | ||||
| 		}, | ||||
| 
 | ||||
|  | @ -127,7 +150,7 @@ export default defineComponent({ | |||
| 		display: flex; | ||||
| 		flex-direction: column; | ||||
| 
 | ||||
| 		> div { | ||||
| 		> .content { | ||||
| 			overflow: auto; | ||||
| 		} | ||||
| 	} | ||||
|  | @ -169,12 +192,35 @@ export default defineComponent({ | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> div { | ||||
| 		> ::v-deep(._content) { | ||||
| 			padding: 24px; | ||||
| 	> .content { | ||||
| 		&.omitted { | ||||
| 			position: relative; | ||||
| 			max-height: var(--maxHeight); | ||||
| 			overflow: hidden; | ||||
| 
 | ||||
| 			& + ._content { | ||||
| 				border-top: solid 0.5px var(--divider); | ||||
| 			> .fade { | ||||
| 				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 { | ||||
| 			> ::v-deep(._content) { | ||||
| 				padding: 16px; | ||||
| 			} | ||||
| 		> .content { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
| 
 | ||||
| 	<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> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ | |||
| 			<header><span>{{ $ts.exploreFediverse }}</span></header> | ||||
| 		</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> | ||||
| 
 | ||||
| 			<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> | ||||
| 		<div class="_section" style="padding: 0 var(--margin);"> | ||||
| 			<div class="_content"> | ||||
| 				<MkContainer :body-togglable="false" class="_gap"> | ||||
| 				<MkContainer :foldable="false" class="_gap"> | ||||
| 					<template #header><Fa :icon="faMicrochip"/>{{ $ts.cpuAndMemory }}</template> | ||||
| 					<!-- | ||||
| 					<template #func> | ||||
|  | @ -27,7 +27,7 @@ | |||
| 					</div> | ||||
| 				</MkContainer> | ||||
| 
 | ||||
| 				<MkContainer :body-togglable="false" class="_gap"> | ||||
| 				<MkContainer :foldable="false" class="_gap"> | ||||
| 					<template #header><Fa :icon="faHdd"/> {{ $ts.disk }}</template> | ||||
| 					<!-- | ||||
| 					<template #func> | ||||
|  | @ -50,7 +50,7 @@ | |||
| 					</div> | ||||
| 				</MkContainer> | ||||
| 
 | ||||
| 				<MkContainer :body-togglable="false" class="_gap"> | ||||
| 				<MkContainer :foldable="false" class="_gap"> | ||||
| 					<template #header><Fa :icon="faExchangeAlt"/> {{ $ts.network }}</template> | ||||
| 					<!-- | ||||
| 					<template #func> | ||||
|  | @ -78,7 +78,7 @@ | |||
| 		<template #header><Fa :icon="faClipboardList"/> {{ $ts.jobQueue }}</template> | ||||
| 
 | ||||
| 		<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> | ||||
| 
 | ||||
| 				<div class="_content"> | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
| 		<div class="sboqnrfi" :style="{ gridTemplateRows: overviewHeight }"> | ||||
| 			<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> | ||||
| 
 | ||||
| 				<div class="_content"> | ||||
|  | @ -19,7 +19,7 @@ | |||
| 				</div> | ||||
| 			</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> | ||||
| 
 | ||||
| 				<div class="_content" v-if="dbInfo"> | ||||
|  |  | |||
|  | @ -85,6 +85,8 @@ export default defineComponent({ | |||
| 	display: flex; | ||||
| 
 | ||||
| 	> .avatar { | ||||
| 		position: sticky; | ||||
| 		top: calc(var(--stickyTop, 0px) + 16px); | ||||
| 		display: block; | ||||
| 		width: 54px; | ||||
| 		height: 54px; | ||||
|  | @ -274,6 +276,11 @@ export default defineComponent({ | |||
| 				background: $me-balloon-color; | ||||
| 				text-align: left; | ||||
| 
 | ||||
| 				::selection { | ||||
| 					color: var(--accent); | ||||
| 					background-color: #fff; | ||||
| 				}  | ||||
| 
 | ||||
| 				&.noText { | ||||
| 					background: transparent; | ||||
| 				} | ||||
|  |  | |||
|  | @ -1,265 +1,261 @@ | |||
| <template> | ||||
| <div class="mwysmxbg"> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_content"> | ||||
| 			<p>{{ $ts._mfm.intro }}</p> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.mention }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="_isolated">{{ $ts._mfm.intro }}</div> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.mention }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.mentionDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_mention"/> | ||||
| 				<MkTextarea v-model:value="preview_mention"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.hashtag }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.hashtag }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.hashtagDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_hashtag"/> | ||||
| 				<MkTextarea v-model:value="preview_hashtag"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.url }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.url }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.urlDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_url"/> | ||||
| 				<MkTextarea v-model:value="preview_url"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.link }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.link }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.linkDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_link"/> | ||||
| 				<MkTextarea v-model:value="preview_link"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.emoji }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.emoji }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.emojiDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_emoji"/> | ||||
| 				<MkTextarea v-model:value="preview_emoji"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.bold }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.bold }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.boldDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_bold"/> | ||||
| 				<MkTextarea v-model:value="preview_bold"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.small }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.small }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.smallDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_small"/> | ||||
| 				<MkTextarea v-model:value="preview_small"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.quote }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.quote }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.quoteDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_quote"/> | ||||
| 				<MkTextarea v-model:value="preview_quote"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.center }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.center }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.centerDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_center"/> | ||||
| 				<MkTextarea v-model:value="preview_center"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.inlineCode }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.inlineCode }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.inlineCodeDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_inlineCode"/> | ||||
| 				<MkTextarea v-model:value="preview_inlineCode"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.blockCode }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.blockCode }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.blockCodeDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_blockCode"/> | ||||
| 				<MkTextarea v-model:value="preview_blockCode"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.inlineMath }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.inlineMath }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.inlineMathDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_inlineMath"/> | ||||
| 				<MkTextarea v-model:value="preview_inlineMath"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.search }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.search }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.searchDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_search"/> | ||||
| 				<MkTextarea v-model:value="preview_search"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.flip }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.flip }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.flipDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_flip"/> | ||||
| 				<MkTextarea v-model:value="preview_flip"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.font }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.font }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.fontDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_font"/> | ||||
| 				<MkTextarea v-model:value="preview_font"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.x2 }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.x2 }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.x2Description }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_x2"/> | ||||
| 				<MkTextarea v-model:value="preview_x2"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.x3 }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.x3 }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.x3Description }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_x3"/> | ||||
| 				<MkTextarea v-model:value="preview_x3"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.x4 }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.x4 }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.x4Description }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_x4"/> | ||||
| 				<MkTextarea v-model:value="preview_x4"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.blur }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.blur }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.blurDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_blur"/> | ||||
| 				<MkTextarea v-model:value="preview_blur"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.jelly }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.jelly }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.jellyDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_jelly"/> | ||||
| 				<MkTextarea v-model:value="preview_jelly"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.tada }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.tada }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.tadaDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_tada"/> | ||||
| 				<MkTextarea v-model:value="preview_tada"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.jump }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.jump }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.jumpDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_jump"/> | ||||
| 				<MkTextarea v-model:value="preview_jump"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.bounce }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.bounce }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.bounceDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_bounce"/> | ||||
| 				<MkTextarea v-model:value="preview_bounce"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.spin }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.spin }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.spinDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_spin"/> | ||||
| 				<MkTextarea v-model:value="preview_spin"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.shake }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.shake }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.shakeDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_shake"/> | ||||
| 				<MkTextarea v-model:value="preview_shake"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_title">{{ $ts._mfm.twitch }}</div> | ||||
| 		<div class="_content"> | ||||
| 	<div class="section _block"> | ||||
| 		<div class="title">{{ $ts._mfm.twitch }}</div> | ||||
| 		<div class="content"> | ||||
| 			<p>{{ $ts._mfm.twitchDescription }}</p> | ||||
| 			<div class="preview _panel"> | ||||
| 			<div class="preview"> | ||||
| 				<Mfm :text="preview_twitch"/> | ||||
| 				<MkTextarea v-model:value="preview_twitch"><span>MFM</span></MkTextarea> | ||||
| 			</div> | ||||
|  | @ -318,8 +314,29 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .mwysmxbg { | ||||
| 	.preview { | ||||
| 	> .section { | ||||
| 		> .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> | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| <div class="fcuexfpr _root"> | ||||
| 	<div v-if="note" class="note" v-anim> | ||||
| 		<div class="_gap" v-if="showNext"> | ||||
| 			<XNotes class="_content _noGap_" :pagination="next"/> | ||||
| 			<XNotes class="_content" :pagination="next" :no-gap="true"/> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<div class="main _gap"> | ||||
|  | @ -25,7 +25,7 @@ | |||
| 		</div> | ||||
| 
 | ||||
| 		<div class="_gap" v-if="showPrev"> | ||||
| 			<XNotes class="_content _noGap_" :pagination="prev"/> | ||||
| 			<XNotes class="_content" :pagination="prev" :no-gap="true"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
| 		<MkButton inline @click="del" class="delete" v-if="pageId && !readonly"><Fa :icon="faTrashAlt"/> {{ $ts.delete }}</MkButton> | ||||
| 	</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> | ||||
| 		<div style="padding: 16px;"> | ||||
| 			<MkInput v-model:value="title"> | ||||
|  | @ -44,7 +44,7 @@ | |||
| 		</div> | ||||
| 	</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> | ||||
| 		<div style="padding: 16px;"> | ||||
| 			<XBlocks class="content" v-model:value="content" :hpml="hpml"/> | ||||
|  | @ -53,7 +53,7 @@ | |||
| 		</div> | ||||
| 	</MkContainer> | ||||
| 
 | ||||
| 	<MkContainer :body-togglable="true" class="_gap"> | ||||
| 	<MkContainer :foldable="true" class="_gap"> | ||||
| 		<template #header><Fa :icon="faMagic"/> {{ $ts._pages.variables }}</template> | ||||
| 		<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"> | ||||
|  | @ -74,7 +74,7 @@ | |||
| 		</div> | ||||
| 	</MkContainer> | ||||
| 
 | ||||
| 	<MkContainer :body-togglable="true" :expanded="true" class="_gap"> | ||||
| 	<MkContainer :foldable="true" :expanded="true" class="_gap"> | ||||
| 		<template #header><Fa :icon="faCode"/> {{ $ts.script }}</template> | ||||
| 		<div> | ||||
| 			<MkTextarea class="_code" v-model:value="script"/> | ||||
|  |  | |||
|  | @ -1,8 +1,6 @@ | |||
| <template> | ||||
| <div class="xcukqgmh _root" v-if="page" :key="page.id" v-size="{ max: [450] }"> | ||||
| 	<div class="_magnet"></div> | ||||
| 
 | ||||
| 	<div class="_block main"> | ||||
| <div class="xcukqgmh _root _magnetParent" v-if="page" :key="page.id" v-size="{ max: [450] }"> | ||||
| 	<div class="_block _magnetChild main"> | ||||
| 		<!-- | ||||
| 		<div class="header"> | ||||
| 			<h1>{{ page.title }}</h1> | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 		<MkButton style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><Fa :icon="faPlay"/></MkButton> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<MkContainer :body-togglable="true" class="_gap"> | ||||
| 	<MkContainer :foldable="true" class="_gap"> | ||||
| 		<template #header><Fa fixed-width/>{{ $ts.output }}</template> | ||||
| 		<div class="bepmlvbi"> | ||||
| 			<div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div> | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| <template> | ||||
| <FormBase> | ||||
| 	<FormSwitch v-model:value="titlebar">{{ $ts.showTitlebar }}</FormSwitch> | ||||
| 	<FormSwitch v-model:value="showFixedPostForm">{{ $ts.showFixedPostForm }}</FormSwitch> | ||||
| 
 | ||||
| 	<FormSelect v-model:value="lang"> | ||||
|  | @ -137,7 +136,6 @@ export default defineComponent({ | |||
| 		useOsNativeEmojis: defaultStore.makeGetterSetter('useOsNativeEmojis'), | ||||
| 		disableShowingAnimatedImages: defaultStore.makeGetterSetter('disableShowingAnimatedImages'), | ||||
| 		loadRawImages: defaultStore.makeGetterSetter('loadRawImages'), | ||||
| 		titlebar: defaultStore.makeGetterSetter('titlebar'), | ||||
| 		imageNewTab: defaultStore.makeGetterSetter('imageNewTab'), | ||||
| 		nsfw: defaultStore.makeGetterSetter('nsfw'), | ||||
| 		disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'), | ||||
|  | @ -182,10 +180,6 @@ export default defineComponent({ | |||
| 			this.reloadAsk(); | ||||
| 		}, | ||||
| 
 | ||||
| 		titlebar() { | ||||
| 			this.reloadAsk(); | ||||
| 		}, | ||||
| 
 | ||||
| 		instanceTicker() { | ||||
| 			this.reloadAsk(); | ||||
| 		}, | ||||
|  |  | |||
|  | @ -13,6 +13,9 @@ | |||
| 		<FormInput readonly :value="selectedTheme.author"> | ||||
| 			<span>{{ $ts.author }}</span> | ||||
| 		</FormInput> | ||||
| 		<FormTextarea readonly :value="selectedTheme.desc" v-if="selectedTheme.desc"> | ||||
| 			<span>{{ $ts._theme.description }}</span> | ||||
| 		</FormTextarea> | ||||
| 		<FormTextarea readonly tall :value="selectedThemeCode"> | ||||
| 			<span>{{ $ts._theme.code }}</span> | ||||
| 			<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $ts.copy }}</button></template> | ||||
|  | @ -94,6 +97,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 		uninstall() { | ||||
| 			removeTheme(this.selectedTheme); | ||||
| 			this.installedThemes = this.installedThemes.filter(t => t.id !== this.selectedThemeId); | ||||
| 			this.selectedThemeId = null; | ||||
| 			os.success(); | ||||
| 		}, | ||||
|  |  | |||
|  | @ -35,6 +35,7 @@ | |||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<FormGroup v-if="codeEnabled"> | ||||
| 		<FormTextarea v-model:value="themeCode" tall> | ||||
| 			<span>{{ $ts._theme.code }}</span> | ||||
|  | @ -42,6 +43,14 @@ | |||
| 		<FormButton @click="applyThemeCode" primary>{{ $ts.apply }}</FormButton> | ||||
| 	</FormGroup> | ||||
| 	<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> | ||||
| 		<FormButton @click="showPreview"><Fa :icon="faEye"/> {{ $ts.preview }}</FormButton> | ||||
| 		<FormButton @click="saveAs" primary><Fa :icon="faSave"/> {{ $ts.saveAs }}</FormButton> | ||||
|  | @ -88,6 +97,8 @@ export default defineComponent({ | |||
| 				props: lightTheme.props | ||||
| 			} as Theme, | ||||
| 			codeEnabled: false, | ||||
| 			descriptionEnabled: false, | ||||
| 			description: null, | ||||
| 			themeCode: null, | ||||
| 			bgColors: [ | ||||
| 				{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' }, | ||||
|  | @ -218,6 +229,7 @@ export default defineComponent({ | |||
| 			this.theme.id = uuid(); | ||||
| 			this.theme.name = name; | ||||
| 			this.theme.author = `@${this.$i.username}@${toUnicode(host)}`; | ||||
| 			if (this.description) this.theme.desc = this.description; | ||||
| 			addTheme(this.theme); | ||||
| 			applyTheme(this.theme); | ||||
| 			if (this.$store.state.darkMode) { | ||||
|  |  | |||
|  | @ -1,11 +1,10 @@ | |||
| <template> | ||||
| <div class="cmuxhskf _root" 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="cmuxhskf _root _magnetParent" v-hotkey.global="keymap"> | ||||
| 	<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"/> | ||||
| 	<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/> | ||||
| 	<div class="tabs _block"> | ||||
| 	<div class="tabs _block _magnetChild"> | ||||
| 		<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 = '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, | ||||
| 			menuOpened: false, | ||||
| 			queue: 0, | ||||
| 			width: 0, | ||||
| 			[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 				title: this.$ts.timeline, | ||||
| 				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: { | ||||
| 		before() { | ||||
| 			Progress.start(); | ||||
|  | @ -140,7 +134,6 @@ export default defineComponent({ | |||
| 		}, | ||||
| 
 | ||||
| 		queueUpdated(q) { | ||||
| 			if (this.$el.offsetWidth !== 0) this.width = this.$el.offsetWidth; | ||||
| 			this.queue = q; | ||||
| 		}, | ||||
| 
 | ||||
|  | @ -223,8 +216,10 @@ export default defineComponent({ | |||
| <style lang="scss" scoped> | ||||
| .cmuxhskf { | ||||
| 	> .new { | ||||
| 		position: fixed; | ||||
| 		position: sticky; | ||||
| 		top: calc(var(--stickyTop, 0px) + 16px); | ||||
| 		z-index: 1000; | ||||
| 		width: 100%; | ||||
| 
 | ||||
| 		> button { | ||||
| 			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> | ||||
| <MkContainer> | ||||
| <MkContainer :max-height="300" :foldable="true"> | ||||
| 	<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $ts.images }}</template> | ||||
| 	<div class="ujigsodd"> | ||||
| 		<MkLoading v-if="fetching"/> | ||||
| 		<div class="stream" v-if="!fetching && images.length > 0"> | ||||
| 			<MkA v-for="image in images" | ||||
| 				class="img" | ||||
| 				:style="`background-image: url(${thumbnail(image.file)})`" | ||||
| 				:to="notePage(image.note)" | ||||
| 			></MkA> | ||||
| 				:key="image.id" | ||||
| 			> | ||||
| 				<ImgWithBlurhash :hash="image.blurhash" :src="thumbnail(image.file)" :alt="image.name" :title="image.name"/> | ||||
| 			</MkA> | ||||
| 		</div> | ||||
| 		<p class="empty" v-if="!fetching && images.length == 0">{{ $ts.nothing }}</p> | ||||
| 	</div> | ||||
|  | @ -22,10 +24,12 @@ import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; | |||
| import notePage from '../../filters/note'; | ||||
| import * as os from '@client/os'; | ||||
| import MkContainer from '@client/components/ui/container.vue'; | ||||
| import ImgWithBlurhash from '@client/components/img-with-blurhash.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		ImgWithBlurhash, | ||||
| 	}, | ||||
| 	props: { | ||||
| 		user: { | ||||
|  | @ -52,18 +56,16 @@ export default defineComponent({ | |||
| 			userId: this.user.id, | ||||
| 			fileType: image, | ||||
| 			excludeNsfw: this.$store.state.nsfw !== 'ignore', | ||||
| 			limit: 9, | ||||
| 			limit: 10, | ||||
| 		}).then(notes => { | ||||
| 			for (const note of notes) { | ||||
| 				for (const file of note.files) { | ||||
| 					if (this.images.length < 9) { | ||||
| 					this.images.push({ | ||||
| 						note, | ||||
| 						file | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 			} | ||||
| 			this.fetching = false; | ||||
| 		}); | ||||
| 	}, | ||||
|  | @ -83,20 +85,14 @@ export default defineComponent({ | |||
| 	padding: 8px; | ||||
| 
 | ||||
| 	> .stream { | ||||
| 		display: flex; | ||||
| 		justify-content: center; | ||||
| 		flex-wrap: wrap; | ||||
| 		display: grid; | ||||
| 		grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | ||||
| 		grid-gap: 6px; | ||||
| 
 | ||||
| 		> .img { | ||||
| 			flex: 1 1 33%; | ||||
| 			width: 33%; | ||||
| 			height: 90px; | ||||
| 			box-sizing: border-box; | ||||
| 			background-position: center center; | ||||
| 			background-size: cover; | ||||
| 			background-clip: content-box; | ||||
| 			border: solid 2px transparent; | ||||
| 			height: 128px; | ||||
| 			border-radius: 6px; | ||||
| 			overflow: clip; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 		<option value="replies">{{ $ts.notesAndReplies }}</option> | ||||
| 		<option value="files">{{ $ts.withFiles }}</option> | ||||
| 	</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> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -198,6 +198,7 @@ | |||
| 					<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"/> | ||||
| 					</div> | ||||
| 					<MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo> | ||||
| 					<XPhotos :user="user" :key="user.id"/> | ||||
| 					<XActivity :user="user" :key="user.id"/> | ||||
| 				</div> | ||||
|  | @ -229,6 +230,7 @@ import MkContainer from '@client/components/ui/container.vue'; | |||
| import MkFolder from '@client/components/ui/folder.vue'; | ||||
| import MkRemoteCaution from '@client/components/remote-caution.vue'; | ||||
| import MkTab from '@client/components/tab.vue'; | ||||
| import MkInfo from '@client/components/ui/info.vue'; | ||||
| import Progress from '@client/scripts/loading'; | ||||
| import parseAcct from '@/misc/acct/parse'; | ||||
| import { getScrollPosition } from '@client/scripts/scroll'; | ||||
|  | @ -247,6 +249,7 @@ export default defineComponent({ | |||
| 		MkRemoteCaution, | ||||
| 		MkFolder, | ||||
| 		MkTab, | ||||
| 		MkInfo, | ||||
| 		XFollowList: defineAsyncComponent(() => import('./follow-list.vue')), | ||||
| 		XClips: defineAsyncComponent(() => import('./clips.vue')), | ||||
| 		XPages: defineAsyncComponent(() => import('./pages.vue')), | ||||
|  | @ -276,6 +279,7 @@ export default defineComponent({ | |||
| 				share: { | ||||
| 					title: this.user.name, | ||||
| 				}, | ||||
| 				menu: () => getUserMenu(this.user), | ||||
| 			} : null), | ||||
| 			user: null, | ||||
| 			error: null, | ||||
|  |  | |||
|  | @ -82,6 +82,9 @@ export const router = createRouter({ | |||
| 		{ path: '/instance/abuses', component: page('instance/abuses') }, | ||||
| 		{ 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: '/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/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) }, | ||||
| 		{ 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 { i18n } from '@client/i18n'; | ||||
| import copyToClipboard from '@client/scripts/copy-to-clipboard'; | ||||
|  | @ -126,6 +126,12 @@ export function getUserMenu(user) { | |||
| 		action: () => { | ||||
| 			copyToClipboard(`@${user.username}@${user.host || host}`); | ||||
| 		} | ||||
| 	}, { | ||||
| 		icon: faInfoCircle, | ||||
| 		text: i18n.locale.info, | ||||
| 		action: () => { | ||||
| 			os.pageWindow(`/user-info/${user.id}`); | ||||
| 		} | ||||
| 	}, { | ||||
| 		icon: faEnvelope, | ||||
| 		text: i18n.locale.sendMessage, | ||||
|  |  | |||
|  | @ -184,10 +184,6 @@ export const defaultStore = markRaw(new Storage('base', { | |||
| 		where: 'device', | ||||
| 		default: 'full' as 'full' | 'icon' | ||||
| 	}, | ||||
| 	titlebar: { | ||||
| 		where: 'device', | ||||
| 		default: true | ||||
| 	}, | ||||
| 	reportError: { | ||||
| 		where: 'device', | ||||
| 		default: false | ||||
|  |  | |||
|  | @ -26,6 +26,7 @@ html { | |||
| 	background-position: center; | ||||
| 	color: var(--fg); | ||||
| 	overflow: auto; | ||||
| 	overflow-wrap: break-word; | ||||
| 	font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; | ||||
| 	line-height: 1.35; | ||||
| 	text-size-adjust: 100%; | ||||
|  | @ -88,10 +89,6 @@ html._themeChanging_ { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| body { | ||||
| 	overflow-wrap: break-word; | ||||
| } | ||||
| 
 | ||||
| html, body { | ||||
| 	margin: 0; | ||||
| 	padding: 0; | ||||
|  | @ -366,10 +363,6 @@ hr { | |||
| 		border-radius: var(--radius); | ||||
| 	} | ||||
| 
 | ||||
| 	._magnet { | ||||
| 		margin-bottom: calc(var(--margin) * -1); | ||||
| 	}	 | ||||
| 
 | ||||
| 	@media (max-width: 500px) { | ||||
| 		._root { | ||||
| 			--root-margin: 0; | ||||
|  | @ -377,6 +370,12 @@ hr { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| ._magnetParent { | ||||
| 	._magnetChild:not(* + ._magnetChild) { | ||||
| 		margin-top: 0; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| ._narrow_ ._card { | ||||
| 	> ._title { | ||||
| 		padding: 16px; | ||||
|  | @ -456,7 +455,7 @@ hr { | |||
| } | ||||
| 
 | ||||
| ._monospace { | ||||
| 	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; | ||||
| 	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; | ||||
| } | ||||
| 
 | ||||
| ._code { | ||||
|  |  | |||
|  | @ -16,8 +16,6 @@ | |||
| 		panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)', | ||||
| 		panelHeaderBg: '@panel', | ||||
| 		panelHeaderDivider: '@divider', | ||||
| 		infoFg: '@accent', | ||||
| 		infoBg: 'rgb(0, 0, 0)', | ||||
| 		header: ':alpha<0.7<@panel', | ||||
| 		navBg: '#363636', | ||||
| 		renote: '@accent', | ||||
|  |  | |||
|  | @ -5,6 +5,8 @@ | |||
| 		<div>{{ $ts.noNotes }}</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 
 | ||||
| 	<MkError v-if="error" @retry="init()"/> | ||||
| 
 | ||||
| 	<div v-show="more && reversed" style="margin-bottom: var(--margin);"> | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
| 		</div> | ||||
| 
 | ||||
| 		<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"/> | ||||
| 			</header> | ||||
| 			<div class="content" :class="{ _flat_: !fullView }"> | ||||
|  | @ -350,6 +350,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 		> .widgets { | ||||
| 			//--panelShadow: none; | ||||
| 			width: 300px; | ||||
| 
 | ||||
| 			@media (max-width: $widgets-hide-threshold) { | ||||
| 				display: none; | ||||
|  |  | |||
|  | @ -2,8 +2,8 @@ | |||
| <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"/> | ||||
| 
 | ||||
| 	<button v-if="editMode" @click="editMode = false" class="_textButton" 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-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 edit" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -62,18 +62,11 @@ export default defineComponent({ | |||
| 	position: sticky; | ||||
| 	height: min-content; | ||||
| 	box-sizing: border-box; | ||||
| 	padding-bottom: 8px; | ||||
| 
 | ||||
| 	> * { | ||||
| 		margin: var(--margin) 0; | ||||
| 		width: 300px; | ||||
| 
 | ||||
| 		&:first-child { | ||||
| 			margin-top: 0; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .add { | ||||
| 		margin: 0 auto; | ||||
| 	> .edit { | ||||
| 		display: block; | ||||
| 		margin: 16px auto; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -2,8 +2,8 @@ | |||
| <div class="mk-app" :class="{ wallpaper }"> | ||||
| 	<XSidebar ref="nav" class="sidebar"/> | ||||
| 
 | ||||
| 	<div class="contents" ref="contents" :class="{ withHeader: $store.state.titlebar }" @contextmenu.stop="onContextmenu"> | ||||
| 		<header v-if="$store.state.titlebar" class="header" ref="header" @click="onHeaderClick"> | ||||
| 	<div class="contents" ref="contents" @contextmenu.stop="onContextmenu"> | ||||
| 		<header class="header" ref="header" @click="onHeaderClick"> | ||||
| 			<XHeader :info="pageInfo"/> | ||||
| 		</header> | ||||
| 		<main ref="main"> | ||||
|  | @ -260,10 +260,7 @@ export default defineComponent({ | |||
| 		width: 100%; | ||||
| 		min-width: 0; | ||||
| 		--stickyTop: #{$header-height}; | ||||
| 
 | ||||
| 		&.withHeader { | ||||
| 		padding-top: $header-height; | ||||
| 		} | ||||
| 
 | ||||
| 		> .header { | ||||
| 			position: fixed; | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <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> | ||||
| 
 | ||||
| 	<div class="wbrkwalb"> | ||||
|  | @ -42,7 +42,7 @@ export default defineComponent({ | |||
| 		MkContainer, MkMiniChart | ||||
| 	}, | ||||
| 	props: { | ||||
| 		bodyTogglable: { | ||||
| 		foldable: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
| 			default: false | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
| 		</div> | ||||
| 	</div> | ||||
| 	<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> | ||||
| 				<div>Process</div> | ||||
|  |  | |||
|  | @ -195,6 +195,7 @@ export class UserRepository extends Repository<User> { | |||
| 
 | ||||
| 			...(opts.detail ? { | ||||
| 				url: profile!.url, | ||||
| 				uri: user.uri, | ||||
| 				createdAt: user.createdAt.toISOString(), | ||||
| 				updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, | ||||
| 				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