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