Merge branch 'develop' into img-max
This commit is contained in:
		
							
								
								
									
										31
									
								
								packages/frontend/src/components/MkChannelList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								packages/frontend/src/components/MkChannelList.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| <template> | ||||
| <MkPagination :pagination="pagination"> | ||||
| 	<template #empty> | ||||
| 		<div class="_fullinfo"> | ||||
| 			<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
| 			<div>{{ i18n.ts.notFound }}</div> | ||||
| 		</div> | ||||
| 	</template> | ||||
|  | ||||
| 	<template #default="{ items }"> | ||||
| 		<MkChannelPreview v-for="item in items" :key="item.id" class="_margin" :channel="extractor(item)"/> | ||||
| 	</template> | ||||
| </MkPagination> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import MkChannelPreview from '@/components/MkChannelPreview.vue'; | ||||
| import MkPagination, { Paging } from '@/components/MkPagination.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	pagination: Paging; | ||||
| 	noGap?: boolean; | ||||
| 	extractor?: (item: any) => any; | ||||
| }>(), { | ||||
| 	extractor: (item) => item, | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| </style> | ||||
| @@ -82,6 +82,7 @@ export default defineComponent({ | ||||
| 			omitted: null, | ||||
| 			ignoreOmit: false, | ||||
| 			defaultStore, | ||||
| 			i18n, | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
|   | ||||
| @@ -439,7 +439,6 @@ defineExpose({ | ||||
|  | ||||
| 	&.asDrawer { | ||||
| 		width: 100% !important; | ||||
| 		padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; | ||||
|  | ||||
| 		> .emojis { | ||||
| 			::v-deep(section) { | ||||
| @@ -498,6 +497,10 @@ defineExpose({ | ||||
| 		background: transparent; | ||||
| 		color: var(--fg); | ||||
|  | ||||
| 		&:not(:focus):not(.filled) { | ||||
| 			margin-bottom: env(safe-area-inset-bottom, 0px); | ||||
| 		} | ||||
|  | ||||
| 		&:not(.filled) { | ||||
| 			order: 1; | ||||
| 			z-index: 2; | ||||
|   | ||||
| @@ -31,7 +31,7 @@ | ||||
| import { onMounted } from 'vue'; | ||||
| import * as misskey from 'misskey-js'; | ||||
| import VuePlyr from 'vue-plyr'; | ||||
| import { ColdDeviceStorage } from '@/store'; | ||||
| import { soundConfigStore } from '@/scripts/sound'; | ||||
| import 'vue-plyr/dist/vue-plyr.css'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | ||||
| @@ -44,11 +44,11 @@ const audioEl = $shallowRef<HTMLAudioElement | null>(); | ||||
| let hide = $ref(true); | ||||
|  | ||||
| function volumechange() { | ||||
| 	if (audioEl) ColdDeviceStorage.set('mediaVolume', audioEl.volume); | ||||
| 	if (audioEl) soundConfigStore.set('mediaVolume', audioEl.volume); | ||||
| } | ||||
|  | ||||
| onMounted(() => { | ||||
| 	if (audioEl) audioEl.volume = ColdDeviceStorage.get('mediaVolume'); | ||||
| 	if (audioEl) audioEl.volume = soundConfigStore.state.mediaVolume; | ||||
| }); | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -1124,16 +1124,16 @@ defineExpose({ | ||||
| 	display: grid; | ||||
| 	grid-auto-flow: row; | ||||
| 	grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); | ||||
| 	grid-auto-rows: 46px; | ||||
| 	grid-auto-rows: 40px; | ||||
| } | ||||
|  | ||||
| .footerRight { | ||||
| 	flex: 0.3; | ||||
| 	flex: 0; | ||||
| 	margin-left: auto; | ||||
| 	display: grid; | ||||
| 	grid-auto-flow: row; | ||||
| 	grid-template-columns: repeat(auto-fill, minmax(42px, 1fr)); | ||||
| 	grid-auto-rows: 46px; | ||||
| 	grid-auto-rows: 40px; | ||||
| 	direction: rtl; | ||||
| } | ||||
|  | ||||
| @@ -1198,13 +1198,21 @@ defineExpose({ | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @container (max-width: 330px) { | ||||
| @container (max-width: 350px) { | ||||
| 	.footer { | ||||
| 		font-size: 0.9em; | ||||
| 	} | ||||
|  | ||||
| 	.footerLeft { | ||||
| 		grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); | ||||
| 	} | ||||
|  | ||||
| 	.footerRight { | ||||
| 		grid-template-columns: repeat(auto-fill, minmax(38px, 1fr)); | ||||
| 	} | ||||
|  | ||||
| 	.headerRight { | ||||
| 		gap: 0; | ||||
| 	} | ||||
|  | ||||
| 	.footer { | ||||
| 		font-size: 14px; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -83,7 +83,7 @@ const choseAd = (): Ad | null => { | ||||
| }; | ||||
|  | ||||
| const chosen = ref(choseAd()); | ||||
| const shouldHide = $ref($i && $i.policies.canHideAds && (props.specify == null)); | ||||
| const shouldHide = $ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); | ||||
|  | ||||
| function reduceFrequency(): void { | ||||
| 	if (chosen.value == null) return; | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { computed } from 'vue'; | ||||
| import { getStaticImageUrl } from '@/scripts/media-proxy'; | ||||
| import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { customEmojis } from '@/custom-emojis'; | ||||
|  | ||||
| @@ -15,25 +15,38 @@ const props = defineProps<{ | ||||
| 	noStyle?: boolean; | ||||
| 	host?: string | null; | ||||
| 	url?: string; | ||||
| 	useOriginalSize?: boolean; | ||||
| }>(); | ||||
|  | ||||
| const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substr(1, props.name.length - 2) : props.name).replace('@.', '')); | ||||
| const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@'))); | ||||
|  | ||||
| const rawUrl = computed(() => { | ||||
| 	if (props.url) { | ||||
| 		return props.url; | ||||
| 	} | ||||
| 	if (props.host == null && !customEmojiName.value.includes('@')) { | ||||
| 	if (isLocal.value) { | ||||
| 		return customEmojis.value.find(x => x.name === customEmojiName.value)?.url ?? null; | ||||
| 	} | ||||
| 	return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`; | ||||
| }); | ||||
|  | ||||
| const url = computed(() => | ||||
| 	defaultStore.reactiveState.disableShowingAnimatedImages.value && rawUrl.value | ||||
| 		? getStaticImageUrl(rawUrl.value) | ||||
| 		: rawUrl.value, | ||||
| ); | ||||
| const url = computed(() => { | ||||
| 	if (rawUrl.value == null) return null; | ||||
|  | ||||
| 	const proxied = | ||||
| 		(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value)) | ||||
| 			? rawUrl.value | ||||
| 			: getProxiedImageUrl( | ||||
| 				rawUrl.value, | ||||
| 				props.useOriginalSize ? undefined : 'emoji', | ||||
| 				false, | ||||
| 				true, | ||||
| 			); | ||||
| 	return defaultStore.reactiveState.disableShowingAnimatedImages.value | ||||
| 		? getStaticImageUrl(proxied) | ||||
| 		: proxied; | ||||
| }); | ||||
|  | ||||
| const alt = computed(() => `:${customEmojiName.value}:`); | ||||
| let errored = $ref(url.value == null); | ||||
|   | ||||
| @@ -51,6 +51,10 @@ export default defineComponent({ | ||||
| 			type: Object, | ||||
| 			default: null, | ||||
| 		}, | ||||
| 		rootScale: { | ||||
| 			type: Number, | ||||
| 			default: 1, | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| 	render() { | ||||
| @@ -65,7 +69,12 @@ export default defineComponent({ | ||||
|  | ||||
| 		const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm; | ||||
|  | ||||
| 		const genEl = (ast: mfm.MfmNode[]) => ast.map((token): VNode | string | (VNode | string)[] => { | ||||
| 		/** | ||||
| 		 * Gen Vue Elements from MFM AST | ||||
| 		 * @param ast MFM AST | ||||
| 		 * @param scale How times large the text is | ||||
| 		 */ | ||||
| 		const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => { | ||||
| 			switch (token.type) { | ||||
| 				case 'text': { | ||||
| 					const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); | ||||
| @@ -84,17 +93,17 @@ export default defineComponent({ | ||||
| 				} | ||||
|  | ||||
| 				case 'bold': { | ||||
| 					return [h('b', genEl(token.children))]; | ||||
| 					return [h('b', genEl(token.children, scale))]; | ||||
| 				} | ||||
|  | ||||
| 				case 'strike': { | ||||
| 					return [h('del', genEl(token.children))]; | ||||
| 					return [h('del', genEl(token.children, scale))]; | ||||
| 				} | ||||
|  | ||||
| 				case 'italic': { | ||||
| 					return h('i', { | ||||
| 						style: 'font-style: oblique;', | ||||
| 					}, genEl(token.children)); | ||||
| 					}, genEl(token.children, scale)); | ||||
| 				} | ||||
|  | ||||
| 				case 'fn': { | ||||
| @@ -155,17 +164,17 @@ export default defineComponent({ | ||||
| 						case 'x2': { | ||||
| 							return h('span', { | ||||
| 								class: defaultStore.state.advancedMfm ? 'mfm-x2' : '', | ||||
| 							}, genEl(token.children)); | ||||
| 							}, genEl(token.children, scale * 2)); | ||||
| 						} | ||||
| 						case 'x3': { | ||||
| 							return h('span', { | ||||
| 								class: defaultStore.state.advancedMfm ? 'mfm-x3' : '', | ||||
| 							}, genEl(token.children)); | ||||
| 							}, genEl(token.children, scale * 3)); | ||||
| 						} | ||||
| 						case 'x4': { | ||||
| 							return h('span', { | ||||
| 								class: defaultStore.state.advancedMfm ? 'mfm-x4' : '', | ||||
| 							}, genEl(token.children)); | ||||
| 							}, genEl(token.children, scale * 4)); | ||||
| 						} | ||||
| 						case 'font': { | ||||
| 							const family = | ||||
| @@ -182,7 +191,7 @@ export default defineComponent({ | ||||
| 						case 'blur': { | ||||
| 							return h('span', { | ||||
| 								class: '_mfm_blur_', | ||||
| 							}, genEl(token.children)); | ||||
| 							}, genEl(token.children, scale)); | ||||
| 						} | ||||
| 						case 'rainbow': { | ||||
| 							const speed = validTime(token.props.args.speed) ?? '1s'; | ||||
| @@ -191,9 +200,9 @@ export default defineComponent({ | ||||
| 						} | ||||
| 						case 'sparkle': { | ||||
| 							if (!useAnim) { | ||||
| 								return genEl(token.children); | ||||
| 								return genEl(token.children, scale); | ||||
| 							} | ||||
| 							return h(MkSparkle, {}, genEl(token.children)); | ||||
| 							return h(MkSparkle, {}, genEl(token.children, scale)); | ||||
| 						} | ||||
| 						case 'rotate': { | ||||
| 							const degrees = parseFloat(token.props.args.deg ?? '90'); | ||||
| @@ -214,7 +223,8 @@ export default defineComponent({ | ||||
| 							} | ||||
| 							const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5); | ||||
| 							const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5); | ||||
| 							style = `transform: scale(${x}, ${y});`; | ||||
| 							style = `transform: scale(${x}, ${y});`;  | ||||
| 							scale = scale * Math.max(x, y); | ||||
| 							break; | ||||
| 						} | ||||
| 						case 'fg': { | ||||
| @@ -231,24 +241,24 @@ export default defineComponent({ | ||||
| 						} | ||||
| 					} | ||||
| 					if (style == null) { | ||||
| 						return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']); | ||||
| 						return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); | ||||
| 					} else { | ||||
| 						return h('span', { | ||||
| 							style: 'display: inline-block; ' + style, | ||||
| 						}, genEl(token.children)); | ||||
| 						}, genEl(token.children, scale)); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				case 'small': { | ||||
| 					return [h('small', { | ||||
| 						style: 'opacity: 0.7;', | ||||
| 					}, genEl(token.children))]; | ||||
| 					}, genEl(token.children, scale))]; | ||||
| 				} | ||||
|  | ||||
| 				case 'center': { | ||||
| 					return [h('div', { | ||||
| 						style: 'text-align:center;', | ||||
| 					}, genEl(token.children))]; | ||||
| 					}, genEl(token.children, scale))]; | ||||
| 				} | ||||
|  | ||||
| 				case 'url': { | ||||
| @@ -264,7 +274,7 @@ export default defineComponent({ | ||||
| 						key: Math.random(), | ||||
| 						url: token.props.url, | ||||
| 						rel: 'nofollow noopener', | ||||
| 					}, genEl(token.children))]; | ||||
| 					}, genEl(token.children, scale))]; | ||||
| 				} | ||||
|  | ||||
| 				case 'mention': { | ||||
| @@ -303,11 +313,11 @@ export default defineComponent({ | ||||
| 					if (!this.nowrap) { | ||||
| 						return [h('div', { | ||||
| 							style: QUOTE_STYLE, | ||||
| 						}, genEl(token.children))]; | ||||
| 						}, genEl(token.children, scale))]; | ||||
| 					} else { | ||||
| 						return [h('span', { | ||||
| 							style: QUOTE_STYLE, | ||||
| 						}, genEl(token.children))]; | ||||
| 						}, genEl(token.children, scale))]; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| @@ -319,6 +329,7 @@ export default defineComponent({ | ||||
| 							name: token.props.name, | ||||
| 							normal: this.plain, | ||||
| 							host: null, | ||||
| 							useOriginalSize: scale >= 2.5, | ||||
| 						})]; | ||||
| 					} else { | ||||
| 						// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||||
| @@ -332,6 +343,7 @@ export default defineComponent({ | ||||
| 								url: this.emojiUrls ? this.emojiUrls[token.props.name] : null, | ||||
| 								normal: this.plain, | ||||
| 								host: this.author.host, | ||||
| 								useOriginalSize: scale >= 2.5, | ||||
| 							})]; | ||||
| 						} | ||||
| 					} | ||||
| @@ -360,7 +372,7 @@ export default defineComponent({ | ||||
| 				} | ||||
|  | ||||
| 				case 'plain': { | ||||
| 					return [h('span', genEl(token.children))]; | ||||
| 					return [h('span', genEl(token.children, scale))]; | ||||
| 				} | ||||
|  | ||||
| 				default: { | ||||
| @@ -373,6 +385,6 @@ export default defineComponent({ | ||||
| 		}).flat(Infinity) as (VNode | string)[]; | ||||
|  | ||||
| 		// Parse ast to DOM | ||||
| 		return h('span', genEl(ast)); | ||||
| 		return h('span', genEl(ast, this.rootScale)); | ||||
| 	}, | ||||
| }); | ||||
|   | ||||
| @@ -2,6 +2,23 @@ | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="700"> | ||||
| 		<div v-if="tab === 'search'"> | ||||
| 			<div class="_gaps"> | ||||
| 				<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search"> | ||||
| 					<template #prefix><i class="ti ti-search"></i></template> | ||||
| 				</MkInput> | ||||
| 				<MkRadios v-model="searchType" @update:model-value="search()"> | ||||
| 					<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option> | ||||
| 					<option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option> | ||||
| 				</MkRadios> | ||||
| 				<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton> | ||||
| 			</div> | ||||
|  | ||||
| 			<MkFoldableSection v-if="channelPagination"> | ||||
| 				<template #header>{{ i18n.ts.searchResult }}</template> | ||||
| 				<MkChannelList :key="key" :pagination="channelPagination"/> | ||||
| 			</MkFoldableSection> | ||||
| 		</div> | ||||
| 		<div v-if="tab === 'featured'"> | ||||
| 			<MkPagination v-slot="{items}" :pagination="featuredPagination"> | ||||
| 				<MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> | ||||
| @@ -28,17 +45,35 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { computed } from 'vue'; | ||||
| import { computed, onMounted } from 'vue'; | ||||
| import MkChannelPreview from '@/components/MkChannelPreview.vue'; | ||||
| import MkChannelList from '@/components/MkChannelList.vue'; | ||||
| import MkPagination from '@/components/MkPagination.vue'; | ||||
| import MkInput from '@/components/MkInput.vue'; | ||||
| import MkRadios from '@/components/MkRadios.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import MkFoldableSection from '@/components/MkFoldableSection.vue'; | ||||
| import { useRouter } from '@/router'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { i18n } from '@/i18n'; | ||||
|  | ||||
| const router = useRouter(); | ||||
|  | ||||
| let tab = $ref('featured'); | ||||
| const props = defineProps<{ | ||||
| 	query: string; | ||||
| 	type?: string; | ||||
| }>(); | ||||
|  | ||||
| let key = $ref(''); | ||||
| let tab = $ref('search'); | ||||
| let searchQuery = $ref(''); | ||||
| let searchType = $ref('nameAndDescription'); | ||||
| let channelPagination = $ref(); | ||||
|  | ||||
| onMounted(() => { | ||||
| 	searchQuery = props.query ?? ''; | ||||
| 	searchType = props.type ?? 'nameAndDescription'; | ||||
| }); | ||||
|  | ||||
| const featuredPagination = { | ||||
| 	endpoint: 'channels/featured' as const, | ||||
| @@ -58,6 +93,25 @@ const ownedPagination = { | ||||
| 	limit: 10, | ||||
| }; | ||||
|  | ||||
| async function search() { | ||||
| 	const query = searchQuery.toString().trim(); | ||||
|  | ||||
| 	if (query == null || query === '') return; | ||||
|  | ||||
| 	const type = searchType.toString().trim(); | ||||
|  | ||||
| 	channelPagination = { | ||||
| 		endpoint: 'channels/search', | ||||
| 		limit: 10, | ||||
| 		params: { | ||||
| 			query: searchQuery, | ||||
| 			type: type, | ||||
| 		}, | ||||
| 	}; | ||||
|  | ||||
| 	key = query + type; | ||||
| } | ||||
|  | ||||
| function create() { | ||||
| 	router.push('/channels/new'); | ||||
| } | ||||
| @@ -69,6 +123,10 @@ const headerActions = $computed(() => [{ | ||||
| }]); | ||||
|  | ||||
| const headerTabs = $computed(() => [{ | ||||
| 	key: 'search', | ||||
| 	title: i18n.ts.search, | ||||
| 	icon: 'ti ti-search', | ||||
| }, { | ||||
| 	key: 'featured', | ||||
| 	title: i18n.ts._channel.featured, | ||||
| 	icon: 'ti ti-comet', | ||||
|   | ||||
| @@ -66,7 +66,7 @@ const recentPostsPagination = { | ||||
| }; | ||||
| const popularPostsPagination = { | ||||
| 	endpoint: 'gallery/featured' as const, | ||||
| 	limit: 5, | ||||
| 	noPaging: true, | ||||
| }; | ||||
| const myPostsPagination = { | ||||
| 	endpoint: 'i/gallery/posts' as const, | ||||
|   | ||||
| @@ -8,27 +8,29 @@ | ||||
| 			</div> | ||||
| 		</template> | ||||
| 		<template #default="{items}"> | ||||
| 			<div v-for="token in items" :key="token.id" class="_panel bfomjevm"> | ||||
| 				<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/> | ||||
| 				<div class="body"> | ||||
| 					<div class="name">{{ token.name }}</div> | ||||
| 					<div class="description">{{ token.description }}</div> | ||||
| 					<MkKeyValue oneline> | ||||
| 						<template #key>{{ i18n.ts.installedDate }}</template> | ||||
| 						<template #value><MkTime :time="token.createdAt"/></template> | ||||
| 					</MkKeyValue> | ||||
| 					<MkKeyValue oneline> | ||||
| 						<template #key>{{ i18n.ts.lastUsedDate }}</template> | ||||
| 						<template #value><MkTime :time="token.lastUsedAt"/></template> | ||||
| 					</MkKeyValue> | ||||
| 					<details> | ||||
| 						<summary>{{ i18n.ts.details }}</summary> | ||||
| 						<ul> | ||||
| 							<li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li> | ||||
| 						</ul> | ||||
| 					</details> | ||||
| 					<div class="actions"> | ||||
| 						<MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton> | ||||
| 			<div class="_gaps"> | ||||
| 				<div v-for="token in items" :key="token.id" class="_panel bfomjevm"> | ||||
| 					<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/> | ||||
| 					<div class="body"> | ||||
| 						<div class="name">{{ token.name }}</div> | ||||
| 						<div class="description">{{ token.description }}</div> | ||||
| 						<MkKeyValue oneline> | ||||
| 							<template #key>{{ i18n.ts.installedDate }}</template> | ||||
| 							<template #value><MkTime :time="token.createdAt"/></template> | ||||
| 						</MkKeyValue> | ||||
| 						<MkKeyValue oneline> | ||||
| 							<template #key>{{ i18n.ts.lastUsedDate }}</template> | ||||
| 							<template #value><MkTime :time="token.lastUsedAt"/></template> | ||||
| 						</MkKeyValue> | ||||
| 						<details> | ||||
| 							<summary>{{ i18n.ts.details }}</summary> | ||||
| 							<ul> | ||||
| 								<li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li> | ||||
| 							</ul> | ||||
| 						</details> | ||||
| 						<div class="actions"> | ||||
| 							<MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| @@ -51,6 +53,7 @@ const list = ref<any>(null); | ||||
| const pagination = { | ||||
| 	endpoint: 'i/apps' as const, | ||||
| 	limit: 100, | ||||
| 	noPaging: true, | ||||
| 	params: { | ||||
| 		sort: '+lastUsedAt', | ||||
| 	}, | ||||
|   | ||||
| @@ -61,6 +61,7 @@ | ||||
| 				<MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch> | ||||
| 				<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch> | ||||
| 				<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch> | ||||
| 				<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch> | ||||
| 			</div> | ||||
| 			<div> | ||||
| 				<MkRadios v-model="emojiStyle"> | ||||
| @@ -163,6 +164,7 @@ const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm')); | ||||
| const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle')); | ||||
| const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer')); | ||||
| const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages')); | ||||
| const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds')); | ||||
| const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages')); | ||||
| const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab')); | ||||
| const nsfw = computed(defaultStore.makeGetterSetter('nsfw')); | ||||
|   | ||||
| @@ -400,7 +400,7 @@ function menu(ev: MouseEvent, profileId: string) { | ||||
| 		icon: 'ti ti-device-floppy', | ||||
| 		action: () => save(profileId), | ||||
| 	}, null, { | ||||
| 		text: ts._preferencesBackups.delete, | ||||
| 		text: ts.delete, | ||||
| 		icon: 'ti ti-trash', | ||||
| 		action: () => deleteProfile(profileId), | ||||
| 		danger: true, | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
| 	<FormSection> | ||||
| 		<template #label>{{ i18n.ts.sounds }}</template> | ||||
| 		<div class="_gaps_s"> | ||||
| 			<MkFolder v-for="type in Object.keys(sounds)" :key="type"> | ||||
| 			<MkFolder v-for="type in soundsKeys" :key="type"> | ||||
| 				<template #label>{{ i18n.t('_sfx.' + type) }}</template> | ||||
| 				<template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template> | ||||
|  | ||||
| @@ -21,51 +21,44 @@ | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { computed, ref } from 'vue'; | ||||
| import { Ref, computed, ref } from 'vue'; | ||||
| import XSound from './sounds.sound.vue'; | ||||
| import MkRange from '@/components/MkRange.vue'; | ||||
| import MkButton from '@/components/MkButton.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import MkFolder from '@/components/MkFolder.vue'; | ||||
| import { ColdDeviceStorage } from '@/store'; | ||||
| import { soundConfigStore } from '@/scripts/sound'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
|  | ||||
| const masterVolume = computed({ | ||||
| 	get: () => { | ||||
| 		return ColdDeviceStorage.get('sound_masterVolume'); | ||||
| 	}, | ||||
| 	set: (value) => { | ||||
| 		ColdDeviceStorage.set('sound_masterVolume', value); | ||||
| 	}, | ||||
| const masterVolume = computed(soundConfigStore.makeGetterSetter('sound_masterVolume')); | ||||
|  | ||||
| const soundsKeys = ['note', 'noteMy', 'notification', 'chat', 'chatBg', 'antenna', 'channel'] as const; | ||||
|  | ||||
| const sounds = ref<Record<typeof soundsKeys[number], Ref<any>>>({ | ||||
| 	note: soundConfigStore.reactiveState.sound_note, | ||||
| 	noteMy: soundConfigStore.reactiveState.sound_noteMy, | ||||
| 	notification: soundConfigStore.reactiveState.sound_notification, | ||||
| 	chat: soundConfigStore.reactiveState.sound_chat, | ||||
| 	chatBg: soundConfigStore.reactiveState.sound_chatBg, | ||||
| 	antenna: soundConfigStore.reactiveState.sound_antenna, | ||||
| 	channel: soundConfigStore.reactiveState.sound_channel, | ||||
| }); | ||||
|  | ||||
| const volumeIcon = computed(() => masterVolume.value === 0 ? 'ti ti-volume-3' : 'ti ti-volume'); | ||||
|  | ||||
| const sounds = ref({ | ||||
| 	note: ColdDeviceStorage.get('sound_note'), | ||||
| 	noteMy: ColdDeviceStorage.get('sound_noteMy'), | ||||
| 	notification: ColdDeviceStorage.get('sound_notification'), | ||||
| 	chat: ColdDeviceStorage.get('sound_chat'), | ||||
| 	chatBg: ColdDeviceStorage.get('sound_chatBg'), | ||||
| 	antenna: ColdDeviceStorage.get('sound_antenna'), | ||||
| 	channel: ColdDeviceStorage.get('sound_channel'), | ||||
| }); | ||||
|  | ||||
| async function updated(type, sound) { | ||||
| async function updated(type: keyof typeof sounds.value, sound) { | ||||
| 	const v = { | ||||
| 		type: sound.type, | ||||
| 		volume: sound.volume, | ||||
| 	}; | ||||
|  | ||||
| 	ColdDeviceStorage.set('sound_' + type, v); | ||||
| 	soundConfigStore.set(`sound_${type}`, v); | ||||
| 	sounds.value[type] = v; | ||||
| } | ||||
|  | ||||
| function reset() { | ||||
| 	for (const sound of Object.keys(sounds.value)) { | ||||
| 		const v = ColdDeviceStorage.default['sound_' + sound]; | ||||
| 		ColdDeviceStorage.set('sound_' + sound, v); | ||||
| 	for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) { | ||||
| 		const v = soundConfigStore.def[`sound_${sound}`].default; | ||||
| 		soundConfigStore.set(`sound_${sound}`, v); | ||||
| 		sounds.value[sound] = v; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -7,18 +7,20 @@ | ||||
| 	<FormSection> | ||||
| 		<MkPagination :pagination="pagination"> | ||||
| 			<template #default="{items}"> | ||||
| 				<FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`" class="_margin"> | ||||
| 					<template #icon> | ||||
| 						<i v-if="webhook.active === false" class="ti ti-player-pause"></i> | ||||
| 						<i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i> | ||||
| 						<i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i> | ||||
| 						<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i> | ||||
| 					</template> | ||||
| 					{{ webhook.name || webhook.url }} | ||||
| 					<template #suffix> | ||||
| 						<MkTime v-if="webhook.latestSentAt" :time="webhook.latestSentAt"></MkTime> | ||||
| 					</template> | ||||
| 				</FormLink> | ||||
| 				<div class="_gaps"> | ||||
| 					<FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`"> | ||||
| 						<template #icon> | ||||
| 							<i v-if="webhook.active === false" class="ti ti-player-pause"></i> | ||||
| 							<i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i> | ||||
| 							<i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i> | ||||
| 							<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i> | ||||
| 						</template> | ||||
| 						{{ webhook.name || webhook.url }} | ||||
| 						<template #suffix> | ||||
| 							<MkTime v-if="webhook.latestSentAt" :time="webhook.latestSentAt"></MkTime> | ||||
| 						</template> | ||||
| 					</FormLink> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 		</MkPagination> | ||||
| 	</FormSection> | ||||
| @@ -35,7 +37,8 @@ import { i18n } from '@/i18n'; | ||||
|  | ||||
| const pagination = { | ||||
| 	endpoint: 'i/webhooks/list' as const, | ||||
| 	limit: 10, | ||||
| 	limit: 100, | ||||
| 	noPaging: true, | ||||
| }; | ||||
|  | ||||
| const headerActions = $computed(() => []); | ||||
|   | ||||
							
								
								
									
										74
									
								
								packages/frontend/src/pages/user/home.stories.impl.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								packages/frontend/src/pages/user/home.stories.impl.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| /* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import { rest } from 'msw'; | ||||
| import { userDetailed } from '../../../.storybook/fakes'; | ||||
| import { commonHandlers } from '../../../.storybook/mocks'; | ||||
| import home_ from './home.vue'; | ||||
| export const Default = { | ||||
| 	render(args) { | ||||
| 		return { | ||||
| 			components: { | ||||
| 				home_, | ||||
| 			}, | ||||
| 			setup() { | ||||
| 				return { | ||||
| 					args, | ||||
| 				}; | ||||
| 			}, | ||||
| 			computed: { | ||||
| 				props() { | ||||
| 					return { | ||||
| 						...this.args, | ||||
| 					}; | ||||
| 				}, | ||||
| 			}, | ||||
| 			template: '<home_ v-bind="props" />', | ||||
| 		}; | ||||
| 	}, | ||||
| 	args: { | ||||
| 		user: userDetailed(), | ||||
| 		disableNotes: false, | ||||
| 	}, | ||||
| 	parameters: { | ||||
| 		layout: 'fullscreen', | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/users/notes', (req, res, ctx) => { | ||||
| 					return res(ctx.json([])); | ||||
| 				}), | ||||
| 				rest.get('/api/charts/user/notes', (req, res, ctx) => { | ||||
| 					const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300); | ||||
| 					return res(ctx.json({ | ||||
| 						total: Array.from({ length }, () => 0), | ||||
| 						inc: Array.from({ length }, () => 0), | ||||
| 						dec: Array.from({ length }, () => 0), | ||||
| 						diffs: { | ||||
| 							normal: Array.from({ length }, () => 0), | ||||
| 							reply: Array.from({ length }, () => 0), | ||||
| 							renote: Array.from({ length }, () => 0), | ||||
| 							withFile: Array.from({ length }, () => 0), | ||||
| 						}, | ||||
| 					})); | ||||
| 				}), | ||||
| 				rest.get('/api/charts/user/pv', (req, res, ctx) => { | ||||
| 					const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300); | ||||
| 					return res(ctx.json({ | ||||
| 						upv: { | ||||
| 							user: Array.from({ length }, () => 0), | ||||
| 							visitor: Array.from({ length }, () => 0), | ||||
| 						}, | ||||
| 						pv: { | ||||
| 							user: Array.from({ length }, () => 0), | ||||
| 							visitor: Array.from({ length }, () => 0), | ||||
| 						}, | ||||
| 					})); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
| 		chromatic: { | ||||
| 			// `XActivity` is not compatible with Chromatic for now | ||||
| 			disableSnapshot: true, | ||||
| 		}, | ||||
| 	}, | ||||
| } satisfies StoryObj<typeof home_>; | ||||
| @@ -2,7 +2,7 @@ import { query } from '@/scripts/url'; | ||||
| import { url } from '@/config'; | ||||
| import { instance } from '@/instance'; | ||||
|  | ||||
| export function getProxiedImageUrl(imageUrl: string, type?: 'preview', mustOrigin: boolean = false): string { | ||||
| export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin: boolean = false, noFallback: boolean = false): string { | ||||
| 	const localProxy = `${url}/proxy`; | ||||
|  | ||||
| 	if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { | ||||
| @@ -15,7 +15,7 @@ export function getProxiedImageUrl(imageUrl: string, type?: 'preview', mustOrigi | ||||
| 		: 'image.webp' | ||||
| 	}?${query({ | ||||
| 		url: imageUrl, | ||||
| 		fallback: '1', | ||||
| 		...(!noFallback ? { 'fallback': '1' } : {}), | ||||
| 		...(type ? { [type]: '1' } : {}), | ||||
| 		...(mustOrigin ? { origin: '1' } : {}), | ||||
| 	})}`; | ||||
|   | ||||
| @@ -1,4 +1,56 @@ | ||||
| import { ColdDeviceStorage } from '@/store'; | ||||
| import { markRaw } from 'vue'; | ||||
| import { Storage } from '@/pizzax'; | ||||
|  | ||||
| export const soundConfigStore = markRaw(new Storage('sound', { | ||||
| 	mediaVolume: { | ||||
| 		where: 'device', | ||||
| 		default: 0.5 | ||||
| 	}, | ||||
| 	sound_masterVolume: { | ||||
| 		where: 'device', | ||||
| 		default: 0.3 | ||||
| 	}, | ||||
| 	sound_note: { | ||||
| 		where: 'account', | ||||
| 		default: { type: 'syuilo/n-aec', volume: 1 } | ||||
| 	}, | ||||
| 	sound_noteMy: { | ||||
| 		where: 'account', | ||||
| 		default: { type: 'syuilo/n-cea-4va', volume: 1 } | ||||
| 	}, | ||||
| 	sound_notification: { | ||||
| 		where: 'account', | ||||
| 		default: { type: 'syuilo/n-ea', volume: 1 } | ||||
| 	}, | ||||
| 	sound_chat: { | ||||
| 		where: 'account', | ||||
| 		default: { type: 'syuilo/pope1', volume: 1 } | ||||
| 	}, | ||||
| 	sound_chatBg: { | ||||
| 		where: 'account', | ||||
| 		default: { type: 'syuilo/waon', volume: 1 } | ||||
| 	}, | ||||
| 	sound_antenna: { | ||||
| 		where: 'account', | ||||
| 		default: { type: 'syuilo/triple', volume: 1 } | ||||
| 	}, | ||||
| 	sound_channel: { | ||||
| 		where: 'account', | ||||
| 		default: { type: 'syuilo/square-pico', volume: 1 } | ||||
| 	}, | ||||
| })); | ||||
|  | ||||
| await soundConfigStore.ready; | ||||
|  | ||||
| //#region サウンドのColdDeviceStorage => indexedDBのマイグレーション | ||||
| for (const target of Object.keys(soundConfigStore.state) as Array<keyof typeof soundConfigStore.state>) { | ||||
| 	const value = localStorage.getItem(`miux:${target}`); | ||||
| 	if (value) { | ||||
| 		soundConfigStore.set(target, JSON.parse(value) as typeof soundConfigStore.def[typeof target]['default']); | ||||
| 		localStorage.removeItem(`miux:${target}`); | ||||
| 	} | ||||
| } | ||||
| //#endregion | ||||
|  | ||||
| const cache = new Map<string, HTMLAudioElement>(); | ||||
|  | ||||
| @@ -67,19 +119,20 @@ export function getAudio(file: string, useCache = true): HTMLAudioElement { | ||||
| } | ||||
|  | ||||
| export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement { | ||||
| 	const masterVolume = ColdDeviceStorage.get('sound_masterVolume'); | ||||
| 	const masterVolume = soundConfigStore.state.sound_masterVolume; | ||||
| 	audio.volume = masterVolume - ((1 - volume) * masterVolume); | ||||
| 	return audio; | ||||
| } | ||||
|  | ||||
| export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notification') { | ||||
| 	const sound = ColdDeviceStorage.get(`sound_${type}`); | ||||
| 	const sound = soundConfigStore.state[`sound_${type}`]; | ||||
| 	if (_DEV_) console.log('play', type, sound); | ||||
| 	if (sound.type == null) return; | ||||
| 	playFile(sound.type, sound.volume); | ||||
| } | ||||
|  | ||||
| export function playFile(file: string, volume: number) { | ||||
| 	const masterVolume = ColdDeviceStorage.get('sound_masterVolume'); | ||||
| 	const masterVolume = soundConfigStore.state.sound_masterVolume; | ||||
| 	if (masterVolume === 0) return; | ||||
|  | ||||
| 	const audio = setVolume(getAudio(file), volume); | ||||
|   | ||||
| @@ -298,6 +298,10 @@ export const defaultStore = markRaw(new Storage('base', { | ||||
| 		where: 'device', | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	forceShowAds: { | ||||
| 		where: 'device', | ||||
| 		default: false, | ||||
| 	}, | ||||
| 	aiChanMode: { | ||||
| 		where: 'device', | ||||
| 		default: false, | ||||
| @@ -343,15 +347,6 @@ export class ColdDeviceStorage { | ||||
| 		darkTheme, | ||||
| 		syncDeviceDarkMode: true, | ||||
| 		plugins: [] as Plugin[], | ||||
| 		mediaVolume: 0.5, | ||||
| 		sound_masterVolume: 0.5, | ||||
| 		sound_note: { type: 'syuilo/n-eca', volume: 0.5 }, | ||||
| 		sound_noteMy: { type: 'syuilo/n-cea-4va', volume: 0.5 }, | ||||
| 		sound_notification: { type: 'syuilo/n-ea', volume: 0.5 }, | ||||
| 		sound_chat: { type: 'syuilo/pope1', volume: 0.5 }, | ||||
| 		sound_chatBg: { type: 'syuilo/waon', volume: 0.5 }, | ||||
| 		sound_antenna: { type: 'syuilo/triple', volume: 0.5 }, | ||||
| 		sound_channel: { type: 'syuilo/square-pico', volume: 0.5 }, | ||||
| 	}; | ||||
|  | ||||
| 	public static watchers: Watcher[] = []; | ||||
|   | ||||
| @@ -1,17 +1,18 @@ | ||||
| import { post } from '@/os'; | ||||
| import { api, post } from '@/os'; | ||||
| import { $i, login } from '@/account'; | ||||
| import { getAccountFromId } from '@/scripts/get-account-from-id'; | ||||
| import { mainRouter } from '@/router'; | ||||
| import { deepClone } from '@/scripts/clone'; | ||||
|  | ||||
| export function swInject() { | ||||
| 	navigator.serviceWorker.addEventListener('message', ev => { | ||||
| 	navigator.serviceWorker.addEventListener('message', async ev => { | ||||
| 		if (_DEV_) { | ||||
| 			console.log('sw msg', ev.data); | ||||
| 		} | ||||
|  | ||||
| 		if (ev.data.type !== 'order') return; | ||||
|  | ||||
| 		if (ev.data.loginId !== $i?.id) { | ||||
| 		if (ev.data.loginId && ev.data.loginId !== $i?.id) { | ||||
| 			return getAccountFromId(ev.data.loginId).then(account => { | ||||
| 				if (!account) return; | ||||
| 				return login(account.token, ev.data.url); | ||||
| @@ -19,8 +20,18 @@ export function swInject() { | ||||
| 		} | ||||
|  | ||||
| 		switch (ev.data.order) { | ||||
| 			case 'post': | ||||
| 				return post(ev.data.options); | ||||
| 			case 'post': { | ||||
| 				const props = deepClone(ev.data.options); | ||||
| 				// プッシュ通知から来たreply,renoteはtruncateBodyが通されているため、 | ||||
| 				// 完全なノートを取得しなおす | ||||
| 				if (props.reply) { | ||||
| 					props.reply = await api('notes/show', { noteId: props.reply.id }); | ||||
| 				} | ||||
| 				if (props.renote) { | ||||
| 					props.renote = await api('notes/show', { noteId: props.renote.id }); | ||||
| 				} | ||||
| 				return post(props); | ||||
| 			} | ||||
| 			case 'push': | ||||
| 				if (mainRouter.currentRoute.value.path === ev.data.url) { | ||||
| 					return window.scroll({ top: 0, behavior: 'smooth' }); | ||||
|   | ||||
| @@ -250,6 +250,7 @@ onMounted(() => { | ||||
| 		> .widgets { | ||||
| 			//--panelBorder: none; | ||||
| 			width: 300px; | ||||
| 			padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px)); | ||||
|  | ||||
| 			@media (max-width: $widgets-hide-threshold) { | ||||
| 				display: none; | ||||
| @@ -304,7 +305,7 @@ onMounted(() => { | ||||
| 		right: 0; | ||||
| 		z-index: 1001; | ||||
| 		height: 100dvh; | ||||
| 		padding: var(--margin); | ||||
| 		padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)); | ||||
| 		box-sizing: border-box; | ||||
| 		overflow: auto; | ||||
| 		background: var(--bg); | ||||
|   | ||||
| @@ -296,7 +296,7 @@ $widgets-hide-threshold: 1090px; | ||||
| } | ||||
|  | ||||
| .widgets { | ||||
| 	padding: 0 var(--margin); | ||||
| 	padding: 0 var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)); | ||||
| 	border-left: solid 0.5px var(--divider); | ||||
| 	background: var(--bg); | ||||
|  | ||||
| @@ -329,7 +329,7 @@ $widgets-hide-threshold: 1090px; | ||||
| 	right: 0; | ||||
| 	z-index: 1001; | ||||
| 	height: 100dvh; | ||||
| 	padding: var(--margin) !important; | ||||
| 	padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important; | ||||
| 	box-sizing: border-box; | ||||
| 	overflow: auto; | ||||
| 	overscroll-behavior: contain; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| 	<XWidgets :class="$style.widgets" :edit="editMode" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> | ||||
|  | ||||
| 	<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button> | ||||
| 	<button v-else class="_textButton mk-widget-edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button> | ||||
| 	<button v-else class="_textButton mk-widget-edit" :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -91,4 +91,8 @@ function updateWidgets(thisWidgets) { | ||||
| .widgets { | ||||
| 	width: 300px; | ||||
| } | ||||
|  | ||||
| .edit { | ||||
| 	width: 100%; | ||||
| } | ||||
| </style> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 tamaina
					tamaina