enhance(client): Sync widgets (#8512)
* feature: sync widgets among devices * fix * nanka iroiro * classic.widgets.vueの機能をuniversal.widgets.vueに統合 * 左右のウィジェット編集状態を同期するように * 左右やカラム間でウィジェットを行き来できるように * MkWidgetsをCSS Module化 * set min-height: 100px; * fix deck widget * Update packages/client/src/ui/deck/widgets-column.vue Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> * merge * Update classic.vue * Delete classic.widgets.vue Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
		
							parent
							
								
									6c4fa1bc8b
								
							
						
					
					
						commit
						e3f2845cf8
					
				
					 8 changed files with 125 additions and 155 deletions
				
			
		|  | @ -1 +1 @@ | |||
| Subproject commit cf3ce27b2eb8417233072e3d6d2fb7c5356c2364 | ||||
| Subproject commit 0179793ec891856d6f37a3be16ba4c22f67a81b5 | ||||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <div class="vjoppmmu"> | ||||
| <div :class="$style.root"> | ||||
| 	<template v-if="edit"> | ||||
| 		<header> | ||||
| 		<header :class="$style['edit-header']"> | ||||
| 			<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" class="mk-widget-select"> | ||||
| 				<template #label>{{ i18n.ts.selectWidget }}</template> | ||||
| 				<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option> | ||||
|  | @ -14,23 +14,34 @@ | |||
| 			item-key="id" | ||||
| 			handle=".handle" | ||||
| 			:animation="150" | ||||
| 			:group="{ name: 'SortableMkWidgets' }" | ||||
| 			@update:model-value="v => emit('updateWidgets', v)" | ||||
| 			:class="$style['edit-editing']" | ||||
| 		> | ||||
| 			<template #item="{element}"> | ||||
| 				<div class="customize-container"> | ||||
| 					<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button> | ||||
| 					<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button> | ||||
| 				<div :class="[$style.widget, $style['customize-container']]"> | ||||
| 					<button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button> | ||||
| 					<button :class="$style['customize-container-remove']" class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button> | ||||
| 					<div class="handle"> | ||||
| 						<component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :widget="element" @update-props="updateWidget(element.id, $event)"/> | ||||
| 						<component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @update-props="updateWidget(element.id, $event)"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 		</Sortable> | ||||
| 	</template> | ||||
| 	<component :is="`mkw-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" class="widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> | ||||
| 	<component :is="`mkw-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| export type Widget = { | ||||
| 	name: string; | ||||
| 	id: string; | ||||
| 	data: Record<string, any>; | ||||
| }; | ||||
| export type DefaultStoredWidget = { | ||||
| 	place: string | null; | ||||
| } & Widget; | ||||
| </script> | ||||
| <script lang="ts" setup> | ||||
| import { defineAsyncComponent, reactive, ref, computed } from 'vue'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | @ -43,12 +54,6 @@ import { deepClone } from '@/scripts/clone'; | |||
| 
 | ||||
| const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); | ||||
| 
 | ||||
| type Widget = { | ||||
| 	name: string; | ||||
| 	id: string; | ||||
| 	data: Record<string, any>; | ||||
| }; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	widgets: Widget[]; | ||||
| 	edit: boolean; | ||||
|  | @ -109,20 +114,12 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { | |||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .vjoppmmu { | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	container-type: inline-size; | ||||
| 
 | ||||
| 	> header { | ||||
| 		margin: 16px 0; | ||||
| 
 | ||||
| 		> * { | ||||
| 			width: 100%; | ||||
| 			padding: 4px; | ||||
| 		} | ||||
| } | ||||
| 
 | ||||
| 	> .widget, .customize-container { | ||||
| .widget { | ||||
| 	contain: content; | ||||
| 	margin: var(--margin) 0; | ||||
| 
 | ||||
|  | @ -131,12 +128,27 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| .edit { | ||||
| 	&-header { | ||||
| 		margin: 16px 0; | ||||
| 
 | ||||
| 		> * { | ||||
| 			width: 100%; | ||||
| 			padding: 4px; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	&-editing { | ||||
| 		min-height: 100px; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .customize-container { | ||||
| 	position: relative; | ||||
| 	cursor: move; | ||||
| 
 | ||||
| 		> .config, | ||||
| 		> .remove { | ||||
| 	&-config, | ||||
| 	&-remove { | ||||
| 		position: absolute; | ||||
| 		z-index: 10000; | ||||
| 		top: 8px; | ||||
|  | @ -147,19 +159,21 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { | |||
| 		border-radius: 4px; | ||||
| 	} | ||||
| 
 | ||||
| 		> .config { | ||||
| 	&-config { | ||||
| 		right: 8px + 8px + 32px; | ||||
| 	} | ||||
| 
 | ||||
| 		> .remove { | ||||
| 	&-remove { | ||||
| 		right: 8px; | ||||
| 	} | ||||
| 
 | ||||
| 		> .handle { | ||||
| 			> .widget { | ||||
| 	&-handle { | ||||
| 
 | ||||
| 		&-widget { | ||||
| 			pointer-events: none; | ||||
| 		}  | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| } | ||||
| 
 | ||||
| </style> | ||||
|  |  | |||
|  | @ -122,7 +122,7 @@ export const defaultStore = markRaw(new Storage('base', { | |||
| 		}[], | ||||
| 	}, | ||||
| 	widgets: { | ||||
| 		where: 'deviceAccount', | ||||
| 		where: 'account', | ||||
| 		default: [] as { | ||||
| 			name: string; | ||||
| 			id: string; | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
| 			<XSidebar/> | ||||
| 		</div> | ||||
| 		<div v-else ref="widgetsLeft" class="widgets left"> | ||||
| 			<XWidgets :place="'left'" @mounted="attachSticky(widgetsLeft)"/> | ||||
| 			<XWidgets place="left" @mounted="attachSticky(widgetsLeft)"/> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<main class="main" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu"> | ||||
|  | @ -17,7 +17,7 @@ | |||
| 		</main> | ||||
| 
 | ||||
| 		<div v-if="isDesktop" ref="widgetsRight" class="widgets right"> | ||||
| 			<XWidgets :place="null" @mounted="attachSticky(widgetsRight)"/> | ||||
| 			<XWidgets :place="showMenuOnTop ? 'right' : null" @mounted="attachSticky(widgetsRight)"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
|  | @ -52,7 +52,7 @@ import { PageMetadata, provideMetadataReceiver, setPageMetadata } from '@/script | |||
| import { defaultStore } from '@/store'; | ||||
| import { i18n } from '@/i18n'; | ||||
| const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue')); | ||||
| const XWidgets = defineAsyncComponent(() => import('./classic.widgets.vue')); | ||||
| const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); | ||||
| 
 | ||||
| const DESKTOP_THRESHOLD = 1100; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,84 +0,0 @@ | |||
| <template> | ||||
| <div class="ddiqwdnk"> | ||||
| 	<XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value.filter(w => w.place === place)" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> | ||||
| 	<MkAd class="a" :prefer="['square']"/> | ||||
| 
 | ||||
| 	<button v-if="editMode" class="_textButton edit" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ $ts.editWidgetsExit }}</button> | ||||
| 	<button v-else class="_textButton edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ $ts.editWidgets }}</button> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, defineAsyncComponent } from 'vue'; | ||||
| import XWidgets from '@/components/MkWidgets.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XWidgets, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		place: { | ||||
| 			type: String, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['mounted'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			editMode: false, | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$emit('mounted', this.$el); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		addWidget(widget) { | ||||
| 			this.$store.set('widgets', [{ | ||||
| 				...widget, | ||||
| 				place: this.place, | ||||
| 			}, ...this.$store.state.widgets]); | ||||
| 		}, | ||||
| 
 | ||||
| 		removeWidget(widget) { | ||||
| 			this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id !== widget.id)); | ||||
| 		}, | ||||
| 
 | ||||
| 		updateWidget({ id, data }) { | ||||
| 			this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? { | ||||
| 				...w, | ||||
| 				data, | ||||
| 			} : w)); | ||||
| 		}, | ||||
| 
 | ||||
| 		updateWidgets(widgets) { | ||||
| 			this.$store.set('widgets', [ | ||||
| 				...this.$store.state.widgets.filter(w => w.place !== this.place), | ||||
| 				...widgets, | ||||
| 			]); | ||||
| 		}, | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .ddiqwdnk { | ||||
| 	position: sticky; | ||||
| 	height: min-content; | ||||
| 	box-sizing: border-box; | ||||
| 	padding-bottom: 8px; | ||||
| 
 | ||||
| 	> .widgets, | ||||
| 	> .a { | ||||
| 		width: 300px; | ||||
| 	} | ||||
| 
 | ||||
| 	> .edit { | ||||
| 		display: block; | ||||
| 		margin: 16px auto; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -4,7 +4,7 @@ | |||
| 
 | ||||
| 	<div class="wtdtxvec"> | ||||
| 		<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" class="intro">{{ i18n.ts._deck.widgetsIntroduction }}</div> | ||||
| 		<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/> | ||||
| 		<XWidgets :edit="edit" :widgets="column.widgets ?? []" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/> | ||||
| 	</div> | ||||
| </XColumn> | ||||
| </template> | ||||
|  |  | |||
|  | @ -273,7 +273,7 @@ const wallpaper = localStorage.getItem('wallpaper') != null; | |||
| 		right: 0; | ||||
| 		z-index: 1001; | ||||
| 		height: 100dvh; | ||||
| 		padding: var(--margin); | ||||
| 		padding: var(--margin) !important; | ||||
| 		box-sizing: border-box; | ||||
| 		overflow: auto; | ||||
| 		overscroll-behavior: contain; | ||||
|  |  | |||
|  | @ -1,25 +1,44 @@ | |||
| <template> | ||||
| <div class="efzpzdvf"> | ||||
| 	<XWidgets :edit="editMode" :widgets="defaultStore.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> | ||||
| <div class="efzpzdvf" :class="{ universal: !classic, classic }"> | ||||
| 	<XWidgets :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> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| let editMode = $ref(false); | ||||
| </script> | ||||
| <script lang="ts" setup> | ||||
| import { onMounted } from 'vue'; | ||||
| import XWidgets from '@/components/MkWidgets.vue'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { defaultStore } from '@/store'; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	// null = 全てのウィジェットを表示 | ||||
| 	// left = place: leftだけを表示 | ||||
| 	// right = rightとnullを表示 | ||||
| 	place?: 'left' | null | 'right'; | ||||
| 	classic?: boolean; | ||||
| }>(), { | ||||
| 	place: null, | ||||
| 	classic: false, | ||||
| }); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'mounted', el: Element): void; | ||||
| 	(ev: 'mounted', el?: Element): void; | ||||
| }>(); | ||||
| 
 | ||||
| let editMode = $ref(false); | ||||
| let rootEl = $ref<HTMLDivElement>(); | ||||
| 
 | ||||
| const widgets = $computed(() => { | ||||
| 	if (props.place === null) return defaultStore.reactiveState.widgets.value; | ||||
| 	if (props.place === 'left') return defaultStore.reactiveState.widgets.value.filter(w => w.place === 'left'); | ||||
| 	return defaultStore.reactiveState.widgets.value.filter(w => w.place !== 'left'); | ||||
| }); | ||||
| 
 | ||||
| onMounted(() => { | ||||
| 	emit('mounted', rootEl); | ||||
| }); | ||||
|  | @ -27,7 +46,7 @@ onMounted(() => { | |||
| function addWidget(widget) { | ||||
| 	defaultStore.set('widgets', [{ | ||||
| 		...widget, | ||||
| 		place: null, | ||||
| 		place: props.place, | ||||
| 	}, ...defaultStore.state.widgets]); | ||||
| } | ||||
| 
 | ||||
|  | @ -39,11 +58,26 @@ function updateWidget({ id, data }) { | |||
| 	defaultStore.set('widgets', defaultStore.state.widgets.map(w => w.id === id ? { | ||||
| 		...w, | ||||
| 		data, | ||||
| 		place: props.place, | ||||
| 	} : w)); | ||||
| } | ||||
| 
 | ||||
| function updateWidgets(widgets) { | ||||
| 	defaultStore.set('widgets', widgets); | ||||
| function updateWidgets(thisWidgets) { | ||||
| 	if (props.place === null) { | ||||
| 		defaultStore.set('widgets', thisWidgets); | ||||
| 		return; | ||||
| 	} | ||||
| 	if (props.place === 'left') { | ||||
| 		defaultStore.set('widgets', [ | ||||
| 			...thisWidgets.map(w => ({ ...w, place: 'left' })), | ||||
| 			...defaultStore.state.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)), | ||||
| 		]); | ||||
| 		return; | ||||
| 	} | ||||
| 	defaultStore.set('widgets', [ | ||||
| 		...defaultStore.state.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)), | ||||
| 		...thisWidgets.map(w => ({ ...w, place: 'right' })), | ||||
| 	]); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
|  | @ -52,11 +86,17 @@ function updateWidgets(widgets) { | |||
| 	position: sticky; | ||||
| 	height: min-content; | ||||
| 	min-height: 100vh; | ||||
| 	padding: var(--margin) 0; | ||||
| 	box-sizing: border-box; | ||||
| 
 | ||||
| 	&.universal { | ||||
| 		padding: var(--margin) 0; | ||||
| 
 | ||||
| 		> * { | ||||
| 			margin: var(--margin) 0; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> * { | ||||
| 		width: 300px; | ||||
| 
 | ||||
| 		&:first-child { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue