wip
This commit is contained in:
		
							parent
							
								
									6a9187c1ba
								
							
						
					
					
						commit
						911dedf3d7
					
				
					 11 changed files with 315 additions and 233 deletions
				
			
		|  | @ -97,6 +97,7 @@ cantRenote: "この投稿はRenoteできません。" | ||||||
| cantReRenote: "RenoteをRenoteすることはできません。" | cantReRenote: "RenoteをRenoteすることはできません。" | ||||||
| quote: "引用" | quote: "引用" | ||||||
| pinnedNote: "ピン留めされたノート" | pinnedNote: "ピン留めされたノート" | ||||||
|  | pinned: "ピン留め" | ||||||
| you: "あなた" | you: "あなた" | ||||||
| clickToShow: "クリックして表示" | clickToShow: "クリックして表示" | ||||||
| sensitive: "閲覧注意" | sensitive: "閲覧注意" | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <transition name="zoom-in-top" appear @after-leave="$emit('closed')"> | <transition name="tooltip" appear @after-leave="$emit('closed')"> | ||||||
| 	<div class="buebdbiu _acrylic _shadow" v-if="showing"> | 	<div class="buebdbiu _acrylic _shadow" v-show="showing" ref="content"> | ||||||
| 		<slot>{{ text }}</slot> | 		<slot>{{ text }}</slot> | ||||||
| 	</div> | 	</div> | ||||||
| </transition> | </transition> | ||||||
|  | @ -35,19 +35,43 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 			const rect = this.source.getBoundingClientRect(); | 			const rect = this.source.getBoundingClientRect(); | ||||||
| 
 | 
 | ||||||
| 			let x = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); | 			const contentWidth = this.$refs.content.offsetWidth; | ||||||
| 			let y = rect.top + window.pageYOffset + this.source.offsetHeight; | 			const contentHeight = this.$refs.content.offsetHeight; | ||||||
| 
 | 
 | ||||||
| 			x -= (this.$el.offsetWidth / 2); | 			let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); | ||||||
|  | 			let top = rect.top + window.pageYOffset + this.source.offsetHeight; | ||||||
| 
 | 
 | ||||||
| 			this.$el.style.left = x + 'px'; | 			left -= (this.$el.offsetWidth / 2); | ||||||
| 			this.$el.style.top = y + 'px'; | 
 | ||||||
|  | 			if (left + contentWidth - window.pageXOffset > window.innerWidth) { | ||||||
|  | 				left = window.innerWidth - contentWidth + window.pageXOffset - 1; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if (top + contentHeight - window.pageYOffset > window.innerHeight) { | ||||||
|  | 				top = rect.top + window.pageYOffset - contentHeight; | ||||||
|  | 				this.$refs.content.style.transformOrigin = 'center bottom'; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			this.$el.style.left = left + 'px'; | ||||||
|  | 			this.$el.style.top = top + 'px'; | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
| }) | }) | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  | .tooltip-enter-active, | ||||||
|  | .tooltip-leave-active { | ||||||
|  | 	opacity: 1; | ||||||
|  | 	transform: scale(1); | ||||||
|  | 	transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1), opacity 200ms cubic-bezier(0.23, 1, 0.32, 1); | ||||||
|  | } | ||||||
|  | .tooltip-enter-from, | ||||||
|  | .tooltip-leave-active { | ||||||
|  | 	opacity: 0; | ||||||
|  | 	transform: scale(0.75); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .buebdbiu { | .buebdbiu { | ||||||
| 	position: absolute; | 	position: absolute; | ||||||
| 	z-index: 11000; | 	z-index: 11000; | ||||||
|  | @ -57,6 +81,6 @@ export default defineComponent({ | ||||||
| 	text-align: center; | 	text-align: center; | ||||||
| 	border-radius: 4px; | 	border-radius: 4px; | ||||||
| 	pointer-events: none; | 	pointer-events: none; | ||||||
| 	transform-origin: center -16px; | 	transform-origin: center top; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
							
								
								
									
										153
									
								
								src/client/components/widgets.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/client/components/widgets.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,153 @@ | ||||||
|  | <template> | ||||||
|  | <div class="vjoppmmu"> | ||||||
|  | 	<template v-if="edit"> | ||||||
|  | 		<header> | ||||||
|  | 			<MkSelect v-model:value="widgetAdderSelected" style="margin-bottom: var(--margin)"> | ||||||
|  | 				<template #label>{{ $ts.selectWidget }}</template> | ||||||
|  | 				<option v-for="widget in widgetDefs" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option> | ||||||
|  | 			</MkSelect> | ||||||
|  | 			<MkButton inline @click="addWidget" primary><Fa :icon="faPlus"/> {{ $ts.add }}</MkButton> | ||||||
|  | 			<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton> | ||||||
|  | 		</header> | ||||||
|  | 		<XDraggable | ||||||
|  | 			v-model="_widgets" | ||||||
|  | 			item-key="id" | ||||||
|  | 			animation="150" | ||||||
|  | 		> | ||||||
|  | 			<template #item="{element}"> | ||||||
|  | 				<div class="customize-container"> | ||||||
|  | 					<button class="config _button" @click.prevent.stop="configWidget(element.id)"><Fa :icon="faCog"/></button> | ||||||
|  | 					<button class="remove _button" @click.prevent.stop="removeWidget(element)"><Fa :icon="faTimes"/></button> | ||||||
|  | 					<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" :column="column" @updateProps="updateWidget(element.id, $event)"/> | ||||||
|  | 				</div> | ||||||
|  | 			</template> | ||||||
|  | 		</XDraggable> | ||||||
|  | 	</template> | ||||||
|  | 	<component v-else class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column" @updateProps="updateWidget(widget.id, $event)"/> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, defineAsyncComponent } from 'vue'; | ||||||
|  | import { v4 as uuid } from 'uuid'; | ||||||
|  | import { faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  | import MkSelect from '@/components/ui/select.vue'; | ||||||
|  | import MkButton from '@/components/ui/button.vue'; | ||||||
|  | import { widgets as widgetDefs } from '@/widgets'; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), | ||||||
|  | 		MkSelect, | ||||||
|  | 		MkButton, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	props: { | ||||||
|  | 		widgets: { | ||||||
|  | 			required: true, | ||||||
|  | 		}, | ||||||
|  | 		edit: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: true, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'], | ||||||
|  | 
 | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			widgetAdderSelected: null, | ||||||
|  | 			widgetDefs, | ||||||
|  | 			settings: {}, | ||||||
|  | 			faTimes, faPlus, faCog | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	computed: { | ||||||
|  | 		_widgets: { | ||||||
|  | 			get() { | ||||||
|  | 				return this.widgets; | ||||||
|  | 			}, | ||||||
|  | 			set(value) { | ||||||
|  | 				this.$emit('updateWidgets', value); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		configWidget(id) { | ||||||
|  | 			this.settings[id](); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		addWidget() { | ||||||
|  | 			if (this.widgetAdderSelected == null) return; | ||||||
|  | 
 | ||||||
|  | 			this.$emit('addWidget', { | ||||||
|  | 				name: this.widgetAdderSelected, | ||||||
|  | 				id: uuid(), | ||||||
|  | 				data: {} | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			this.widgetAdderSelected = null; | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		removeWidget(widget) { | ||||||
|  | 			this.$emit('removeWidget', widget); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		updateWidget(id, data) { | ||||||
|  | 			this.$emit('updateWidget', { id, data }); | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .vjoppmmu { | ||||||
|  | 	> header { | ||||||
|  | 		margin: 16px 0; | ||||||
|  | 
 | ||||||
|  | 		> * { | ||||||
|  | 			width: 100%; | ||||||
|  | 			padding: 4px; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	> .widget, .customize-container { | ||||||
|  | 		margin: var(--margin) 0; | ||||||
|  | 
 | ||||||
|  | 		&:first-of-type { | ||||||
|  | 			margin-top: 0; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.customize-container { | ||||||
|  | 		position: relative; | ||||||
|  | 		cursor: move; | ||||||
|  | 
 | ||||||
|  | 		> *:not(.remove):not(.config) { | ||||||
|  | 			pointer-events: none; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		> .config, | ||||||
|  | 		> .remove { | ||||||
|  | 			position: absolute; | ||||||
|  | 			z-index: 10000; | ||||||
|  | 			top: 8px; | ||||||
|  | 			width: 32px; | ||||||
|  | 			height: 32px; | ||||||
|  | 			color: #fff; | ||||||
|  | 			background: rgba(#000, 0.7); | ||||||
|  | 			border-radius: 4px; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		> .config { | ||||||
|  | 			right: 8px + 8px + 32px; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		> .remove { | ||||||
|  | 			right: 8px; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -4,6 +4,7 @@ import { popup } from '@/os'; | ||||||
| 
 | 
 | ||||||
| const start = isDeviceTouch ? 'touchstart' : 'mouseover'; | const start = isDeviceTouch ? 'touchstart' : 'mouseover'; | ||||||
| const end = isDeviceTouch ? 'touchend' : 'mouseleave'; | const end = isDeviceTouch ? 'touchend' : 'mouseleave'; | ||||||
|  | const delay = 100; | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
| 	mounted(el: HTMLElement, binding, vn) { | 	mounted(el: HTMLElement, binding, vn) { | ||||||
|  | @ -47,13 +48,13 @@ export default { | ||||||
| 		el.addEventListener(start, () => { | 		el.addEventListener(start, () => { | ||||||
| 			clearTimeout(self.showTimer); | 			clearTimeout(self.showTimer); | ||||||
| 			clearTimeout(self.hideTimer); | 			clearTimeout(self.hideTimer); | ||||||
| 			self.showTimer = setTimeout(show, 300); | 			self.showTimer = setTimeout(show, delay); | ||||||
| 		}, { passive: true }); | 		}, { passive: true }); | ||||||
| 
 | 
 | ||||||
| 		el.addEventListener(end, () => { | 		el.addEventListener(end, () => { | ||||||
| 			clearTimeout(self.showTimer); | 			clearTimeout(self.showTimer); | ||||||
| 			clearTimeout(self.hideTimer); | 			clearTimeout(self.hideTimer); | ||||||
| 			self.hideTimer = setTimeout(self.close, 300); | 			self.hideTimer = setTimeout(self.close, delay); | ||||||
| 		}, { passive: true }); | 		}, { passive: true }); | ||||||
| 
 | 
 | ||||||
| 		el.addEventListener('click', () => { | 		el.addEventListener('click', () => { | ||||||
|  |  | ||||||
|  | @ -488,19 +488,6 @@ hr { | ||||||
| 	transform: scale(0.9); | 	transform: scale(0.9); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .zoom-in-top-enter-active, |  | ||||||
| .zoom-in-top-leave-active { |  | ||||||
| 	opacity: 1; |  | ||||||
| 	transform: scaleY(1); |  | ||||||
| 	transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); |  | ||||||
| 	transform-origin: center top; |  | ||||||
| } |  | ||||||
| .zoom-in-top-enter-from, |  | ||||||
| .zoom-in-top-leave-active { |  | ||||||
| 	opacity: 0; |  | ||||||
| 	transform: scaleY(0); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| @keyframes blink { | @keyframes blink { | ||||||
| 	0% { opacity: 1; transform: scale(1); } | 	0% { opacity: 1; transform: scale(1); } | ||||||
| 	30% { opacity: 1; transform: scale(1); } | 	30% { opacity: 1; transform: scale(1); } | ||||||
|  |  | ||||||
|  | @ -10,10 +10,10 @@ | ||||||
| 				</button> | 				</button> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="right"> | 			<div class="right"> | ||||||
| 				<MkA class="item" to="/my/messaging"><Fa class="icon" :icon="faComments"/><i v-if="$i.hasUnreadMessagingMessage"><Fa :icon="faCircle"/></i></MkA> | 				<MkA class="item" to="/my/messaging" v-tooltip="$ts.messaging"><Fa class="icon" :icon="faComments"/><i v-if="$i.hasUnreadMessagingMessage"><Fa :icon="faCircle"/></i></MkA> | ||||||
| 				<MkA class="item" to="/my/messages"><Fa class="icon" :icon="faEnvelope"/><i v-if="$i.hasUnreadSpecifiedNotes"><Fa :icon="faCircle"/></i></MkA> | 				<MkA class="item" to="/my/messages" v-tooltip="$ts.directNotes"><Fa class="icon" :icon="faEnvelope"/><i v-if="$i.hasUnreadSpecifiedNotes"><Fa :icon="faCircle"/></i></MkA> | ||||||
| 				<MkA class="item" to="/my/mentions"><Fa class="icon" :icon="faAt"/><i v-if="$i.hasUnreadMentions"><Fa :icon="faCircle"/></i></MkA> | 				<MkA class="item" to="/my/mentions" v-tooltip="$ts.mentions"><Fa class="icon" :icon="faAt"/><i v-if="$i.hasUnreadMentions"><Fa :icon="faCircle"/></i></MkA> | ||||||
| 				<MkA class="item" to="/my/notifications"><Fa class="icon" :icon="faBell"/><i v-if="$i.hasUnreadNotification"><Fa :icon="faCircle"/></i></MkA> | 				<MkA class="item" to="/my/notifications" v-tooltip="$ts.notifications"><Fa class="icon" :icon="faBell"/><i v-if="$i.hasUnreadNotification"><Fa :icon="faCircle"/></i></MkA> | ||||||
| 			</div> | 			</div> | ||||||
| 		</header> | 		</header> | ||||||
| 		<div class="body"> | 		<div class="body"> | ||||||
|  | @ -63,10 +63,10 @@ | ||||||
| 				</button> | 				</button> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div class="right"> | 			<div class="right"> | ||||||
| 				<button class="_button item search" @click="search"> | 				<button class="_button item search" @click="search" v-tooltip="$ts.search"> | ||||||
| 					<Fa :icon="faSearch"/> | 					<Fa :icon="faSearch"/> | ||||||
| 				</button> | 				</button> | ||||||
| 				<MkA class="item" to="/settings"><Fa class="icon" :icon="faCog"/></MkA> | 				<MkA class="item" to="/settings" v-tooltip="$ts.settings"><Fa class="icon" :icon="faCog"/></MkA> | ||||||
| 			</div> | 			</div> | ||||||
| 		</footer> | 		</footer> | ||||||
| 	</div> | 	</div> | ||||||
|  | @ -97,11 +97,12 @@ | ||||||
| 			</div> | 			</div> | ||||||
| 
 | 
 | ||||||
| 			<div class="right"> | 			<div class="right"> | ||||||
|  | 				<div class="instance">{{ instanceName }}</div> | ||||||
| 				<XHeaderClock class="clock"/> | 				<XHeaderClock class="clock"/> | ||||||
| 				<button class="_button button search" @click="search"> | 				<button class="_button button search" @click="search" v-tooltip="$ts.search"> | ||||||
| 					<Fa :icon="faSearch"/> | 					<Fa :icon="faSearch"/> | ||||||
| 				</button> | 				</button> | ||||||
| 				<button class="_button button follow" v-if="tl.startsWith('channel:') && currentChannel" :class="{ followed: currentChannel.isFollowing }" @click="toggleChannelFollow"> | 				<button class="_button button follow" v-if="tl.startsWith('channel:') && currentChannel" :class="{ followed: currentChannel.isFollowing }" @click="toggleChannelFollow" v-tooltip="currentChannel.isFollowing ? $ts.unfollow : $ts.follow"> | ||||||
| 					<Fa v-if="currentChannel.isFollowing" :icon="faStar"/> | 					<Fa v-if="currentChannel.isFollowing" :icon="faStar"/> | ||||||
| 					<Fa v-else :icon="farStar"/> | 					<Fa v-else :icon="farStar"/> | ||||||
| 				</button> | 				</button> | ||||||
|  | @ -121,6 +122,9 @@ | ||||||
| 	</main> | 	</main> | ||||||
| 
 | 
 | ||||||
| 	<XSide class="side" ref="side"/> | 	<XSide class="side" ref="side"/> | ||||||
|  | 	<div class="side"> | ||||||
|  | 		<XWidgets/> | ||||||
|  | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<XCommon/> | 	<XCommon/> | ||||||
| </div> | </div> | ||||||
|  | @ -132,6 +136,7 @@ import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faColumns, fa | ||||||
| import { faBell, faStar as farStar, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons'; | import { faBell, faStar as farStar, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { instanceName, url } from '@/config'; | import { instanceName, url } from '@/config'; | ||||||
| import XSidebar from '@/components/sidebar.vue'; | import XSidebar from '@/components/sidebar.vue'; | ||||||
|  | import XWidgets from './widgets.vue'; | ||||||
| import XCommon from '../_common_/common.vue'; | import XCommon from '../_common_/common.vue'; | ||||||
| import XSide from './side.vue'; | import XSide from './side.vue'; | ||||||
| import XTimeline from './timeline.vue'; | import XTimeline from './timeline.vue'; | ||||||
|  | @ -147,6 +152,7 @@ export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XCommon, | 		XCommon, | ||||||
| 		XSidebar, | 		XSidebar, | ||||||
|  | 		XWidgets, | ||||||
| 		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる | 		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる | ||||||
| 		XTimeline, | 		XTimeline, | ||||||
| 		XPostForm, | 		XPostForm, | ||||||
|  | @ -187,6 +193,7 @@ export default defineComponent({ | ||||||
| 			featuredChannels: null, | 			featuredChannels: null, | ||||||
| 			currentChannel: null, | 			currentChannel: null, | ||||||
| 			menuDef: sidebarDef, | 			menuDef: sidebarDef, | ||||||
|  | 			instanceName, | ||||||
| 			faLayerGroup, faBars, faBell, faHome, faCircle, faPencilAlt, faShareAlt, faSatelliteDish, faListUl, faSatellite, faCog, faSearch, faPlus, faStar, farStar, faAt, faLink, faEllipsisH, faGlobe, faComments, faEnvelope, | 			faLayerGroup, faBars, faBell, faHome, faCircle, faPencilAlt, faShareAlt, faSatelliteDish, faListUl, faSatellite, faCog, faSearch, faPlus, faStar, farStar, faAt, faLink, faEllipsisH, faGlobe, faComments, faEnvelope, | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
|  | @ -310,8 +317,7 @@ export default defineComponent({ | ||||||
| 	$ui-font-size: 1em; // TODO: どこかに集約したい | 	$ui-font-size: 1em; // TODO: どこかに集約したい | ||||||
| 
 | 
 | ||||||
| 	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ | 	// ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ | ||||||
| 	min-height: calc(var(--vh, 1vh) * 100); | 	height: calc(var(--vh, 1vh) * 100); | ||||||
| 	box-sizing: border-box; |  | ||||||
| 	display: flex; | 	display: flex; | ||||||
| 
 | 
 | ||||||
| 	> .nav { | 	> .nav { | ||||||
|  | @ -518,6 +524,10 @@ export default defineComponent({ | ||||||
| 				margin-left: auto; | 				margin-left: auto; | ||||||
| 				padding-left: 8px; | 				padding-left: 8px; | ||||||
| 
 | 
 | ||||||
|  | 				> .instance { | ||||||
|  | 					margin-right: 16px; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
| 				> .clock { | 				> .clock { | ||||||
| 					margin-right: 16px; | 					margin-right: 16px; | ||||||
| 				} | 				} | ||||||
|  | @ -552,6 +562,7 @@ export default defineComponent({ | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> .side { | 	> .side { | ||||||
|  | 		width: 350px; | ||||||
| 		border-left: solid 1px var(--divider); | 		border-left: solid 1px var(--divider); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -120,12 +120,8 @@ export default defineComponent({ | ||||||
| 	--section-padding: 16px; | 	--section-padding: 16px; | ||||||
| 	--margin: var(--marginHalf); | 	--margin: var(--marginHalf); | ||||||
| 
 | 
 | ||||||
| 	width: 390px; |  | ||||||
| 
 |  | ||||||
| 	> .container { | 	> .container { | ||||||
| 		position: fixed; | 		height: 100%; | ||||||
| 		width: 390px; |  | ||||||
| 		height: 100vh; |  | ||||||
| 		overflow: auto; | 		overflow: auto; | ||||||
| 		box-sizing: border-box; | 		box-sizing: border-box; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								src/client/ui/chat/store.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/client/ui/chat/store.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | import { markRaw } from 'vue'; | ||||||
|  | import { Storage } from '../../pizzax'; | ||||||
|  | 
 | ||||||
|  | export const store = markRaw(new Storage('chatUi', { | ||||||
|  | 	widgets: { | ||||||
|  | 		where: 'account', | ||||||
|  | 		default: [] as { | ||||||
|  | 			name: string; | ||||||
|  | 			id: string; | ||||||
|  | 			data: Record<string, any>; | ||||||
|  | 		}[] | ||||||
|  | 	}, | ||||||
|  | })); | ||||||
							
								
								
									
										61
									
								
								src/client/ui/chat/widgets.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/client/ui/chat/widgets.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | ||||||
|  | <template> | ||||||
|  | <div class="qydbhufi"> | ||||||
|  | 	<XWidgets :edit="edit" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/> | ||||||
|  | 
 | ||||||
|  | 	<button v-if="edit" @click="edit = false" class="_textButton" style="font-size: 0.9em;">{{ $ts.editWidgetsExit }}</button> | ||||||
|  | 	<button v-else @click="edit = true" class="_textButton" style="font-size: 0.9em;">{{ $ts.editWidgets }}</button> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, defineAsyncComponent } from 'vue'; | ||||||
|  | import XWidgets from '@/components/widgets.vue'; | ||||||
|  | import { store } from './store.ts'; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		XWidgets, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			edit: false, | ||||||
|  | 			widgets: store.reactiveState.widgets | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	methods: { | ||||||
|  | 		addWidget(widget) { | ||||||
|  | 			store.set('widgets', [widget, ...store.state.widgets]); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		removeWidget(widget) { | ||||||
|  | 			store.set('widgets', store.state.widgets.filter(w => w.id != widget.id)); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		updateWidget({ id, data }) { | ||||||
|  | 			store.set('widgets', store.state.widgets.map(w => w.id === id ? { | ||||||
|  | 				...w, | ||||||
|  | 				data: data | ||||||
|  | 			} : w)); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		updateWidgets(widgets) { | ||||||
|  | 			store.set('widgets', widgets); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .qydbhufi { | ||||||
|  | 	height: 100%; | ||||||
|  | 	box-sizing: border-box; | ||||||
|  | 	overflow: auto; | ||||||
|  | 	padding: var(--margin); | ||||||
|  | 
 | ||||||
|  | 	::v-deep(._panel) { | ||||||
|  | 		box-shadow: none; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -3,49 +3,22 @@ | ||||||
| 	<template #header><Fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template> | 	<template #header><Fa :icon="faWindowMaximize" style="margin-right: 8px;"/>{{ column.name }}</template> | ||||||
| 
 | 
 | ||||||
| 	<div class="wtdtxvec"> | 	<div class="wtdtxvec"> | ||||||
| 		<template v-if="edit"> | 		<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/> | ||||||
| 			<header> |  | ||||||
| 				<MkSelect v-model:value="widgetAdderSelected" style="margin-bottom: var(--margin)"> |  | ||||||
| 					<template #label>{{ $ts.selectWidget }}</template> |  | ||||||
| 					<option v-for="widget in widgets" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option> |  | ||||||
| 				</MkSelect> |  | ||||||
| 				<MkButton inline @click="addWidget" primary><Fa :icon="faPlus"/> {{ $ts.add }}</MkButton> |  | ||||||
| 				<MkButton inline @click="edit = false">{{ $ts.close }}</MkButton> |  | ||||||
| 			</header> |  | ||||||
| 			<XDraggable |  | ||||||
| 				v-model="_widgets" |  | ||||||
| 				item-key="id" |  | ||||||
| 				animation="150" |  | ||||||
| 			> |  | ||||||
| 				<template #item="{element}"> |  | ||||||
| 					<div class="customize-container" @click="widgetFunc(element.id)"> |  | ||||||
| 						<button class="remove _button" @click.prevent.stop="removeWidget(element)"><Fa :icon="faTimes"/></button> |  | ||||||
| 						<component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" :column="column" @updateProps="saveWidget(element.id, $event)"/> |  | ||||||
| 					</div> |  | ||||||
| 				</template> |  | ||||||
| 			</XDraggable> |  | ||||||
| 		</template> |  | ||||||
| 		<component v-else class="widget" v-for="widget in column.widgets" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" :column="column" @updateProps="saveWidget(widget.id, $event)"/> |  | ||||||
| 	</div> | 	</div> | ||||||
| </XColumn> | </XColumn> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, defineAsyncComponent } from 'vue'; | import { defineComponent, defineAsyncComponent } from 'vue'; | ||||||
| import { v4 as uuid } from 'uuid'; |  | ||||||
| import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; | import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import MkSelect from '@/components/ui/select.vue'; | import XWidgets from '@/components/widgets.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; |  | ||||||
| import XColumn from './column.vue'; | import XColumn from './column.vue'; | ||||||
| import { widgets } from '../../widgets'; |  | ||||||
| import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store'; | import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XColumn, | 		XColumn, | ||||||
| 		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), | 		XWidgets, | ||||||
| 		MkSelect, |  | ||||||
| 		MkButton, |  | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -62,49 +35,27 @@ export default defineComponent({ | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			edit: false, | 			edit: false, | ||||||
| 			widgetAdderSelected: null, |  | ||||||
| 			widgets, |  | ||||||
| 			settings: {}, |  | ||||||
| 			faWindowMaximize, faTimes, faPlus | 			faWindowMaximize, faTimes, faPlus | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	computed: { |  | ||||||
| 		_widgets: { |  | ||||||
| 			get() { |  | ||||||
| 				return this.column.widgets; |  | ||||||
| 			}, |  | ||||||
| 			set(value) { |  | ||||||
| 				setColumnWidgets(this.column.id, value); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { | 	methods: { | ||||||
| 		widgetFunc(id) { | 		addWidget(widget) { | ||||||
| 			this.settings[id](); | 			addColumnWidget(this.column.id, widget); | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		addWidget() { |  | ||||||
| 			if (this.widgetAdderSelected == null) return; |  | ||||||
| 
 |  | ||||||
| 			addColumnWidget(this.column.id, { |  | ||||||
| 				name: this.widgetAdderSelected, |  | ||||||
| 				id: uuid(), |  | ||||||
| 				data: {} |  | ||||||
| 			}); |  | ||||||
| 
 |  | ||||||
| 			this.widgetAdderSelected = null; |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		removeWidget(widget) { | 		removeWidget(widget) { | ||||||
| 			removeColumnWidget(this.column.id, widget); | 			removeColumnWidget(this.column.id, widget); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		saveWidget(id, data) { | 		updateWidget({ id, data }) { | ||||||
| 			updateColumnWidget(this.column.id, id, data); | 			updateColumnWidget(this.column.id, id, data); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		updateWidgets(widgets) { | ||||||
|  | 			setColumnWidgets(this.column.id, widgets); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		func() { | 		func() { | ||||||
| 			this.edit = !this.edit; | 			this.edit = !this.edit; | ||||||
| 		} | 		} | ||||||
|  | @ -114,46 +65,12 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .wtdtxvec { | .wtdtxvec { | ||||||
| 	._panel { | 	--margin: 8px; | ||||||
|  | 
 | ||||||
|  | 	padding: 0 var(--margin); | ||||||
|  | 
 | ||||||
|  | 	::v-deep(._panel) { | ||||||
| 		box-shadow: none; | 		box-shadow: none; | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	> header { |  | ||||||
| 		padding: 16px; |  | ||||||
| 
 |  | ||||||
| 		> * { |  | ||||||
| 			width: 100%; |  | ||||||
| 			padding: 4px; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	> .widget, .customize-container { |  | ||||||
| 		margin: 8px; |  | ||||||
| 
 |  | ||||||
| 		&:first-of-type { |  | ||||||
| 			margin-top: 0; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	.customize-container { |  | ||||||
| 		position: relative; |  | ||||||
| 		cursor: move; |  | ||||||
| 
 |  | ||||||
| 		> *:not(.remove) { |  | ||||||
| 			pointer-events: none; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .remove { |  | ||||||
| 			position: absolute; |  | ||||||
| 			z-index: 2; |  | ||||||
| 			top: 8px; |  | ||||||
| 			right: 8px; |  | ||||||
| 			width: 32px; |  | ||||||
| 			height: 32px; |  | ||||||
| 			color: #fff; |  | ||||||
| 			background: rgba(#000, 0.7); |  | ||||||
| 			border-radius: 4px; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,46 +1,21 @@ | ||||||
| <template> | <template> | ||||||
| <div class="efzpzdvf"> | <div class="efzpzdvf"> | ||||||
| 	<template v-if="editMode"> | 	<XWidgets :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> | ||||||
| 		<MkButton primary @click="addWidget" class="add"><Fa :icon="faPlus"/></MkButton> | 
 | ||||||
| 		<XDraggable | 	<button v-if="editMode" @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button> | ||||||
| 			v-model="widgets" | 	<button v-else @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button> | ||||||
| 			item-key="id" |  | ||||||
| 			handle=".handle" |  | ||||||
| 			animation="150" |  | ||||||
| 			class="sortable" |  | ||||||
| 		> |  | ||||||
| 			<template #item="{element}"> |  | ||||||
| 				<div class="customize-container _panel"> |  | ||||||
| 					<header> |  | ||||||
| 						<span class="handle"><Fa :icon="faBars"/></span>{{ $t('_widgets.' + element.name) }}<button class="remove _button" @click="removeWidget(element)"><Fa :icon="faTimes"/></button> |  | ||||||
| 					</header> |  | ||||||
| 					<div @click="widgetFunc(element.id)"> |  | ||||||
| 						<component class="_inContainer_ _forceContainerFull_" :is="`mkw-${element.name}`" :widget="element" :ref="element.id" :setting-callback="setting => settings[element.id] = setting" @updateProps="saveWidget(element.id, $event)"/> |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 			</template> |  | ||||||
| 		</XDraggable> |  | ||||||
| 		<button @click="editMode = false" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faCheck"/> {{ $ts.editWidgetsExit }}</button> |  | ||||||
| 	</template> |  | ||||||
| 	<template v-else> |  | ||||||
| 		<component v-for="widget in widgets" class="_inContainer_ _forceContainerFull_" :is="`mkw-${widget.name}`" :key="widget.id" :widget="widget" @updateProps="saveWidget(widget.id, $event)"/> |  | ||||||
| 		<button @click="editMode = true" class="_textButton" style="font-size: 0.9em;"><Fa :icon="faPencilAlt"/> {{ $ts.editWidgets }}</button> |  | ||||||
| 	</template> |  | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, defineAsyncComponent } from 'vue'; | import { defineComponent, defineAsyncComponent } from 'vue'; | ||||||
| import { v4 as uuid } from 'uuid'; |  | ||||||
| import { faPencilAlt, faPlus, faBars, faTimes, faCheck } from '@fortawesome/free-solid-svg-icons'; | import { faPencilAlt, faPlus, faBars, faTimes, faCheck } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { widgets } from '@/widgets'; | import XWidgets from '@/components/widgets.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import MkButton from '@/components/ui/button.vue'; |  | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkButton, | 		XWidgets | ||||||
| 		XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)), |  | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	emits: ['mounted'], | 	emits: ['mounted'], | ||||||
|  | @ -48,62 +23,35 @@ export default defineComponent({ | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			editMode: false, | 			editMode: false, | ||||||
| 			settings: {}, |  | ||||||
| 			faPencilAlt, faPlus, faBars, faTimes, faCheck, | 			faPencilAlt, faPlus, faBars, faTimes, faCheck, | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	computed: { |  | ||||||
| 		widgets: { |  | ||||||
| 			get() { |  | ||||||
| 				return this.$store.reactiveState.widgets.value; |  | ||||||
| 			}, |  | ||||||
| 			set(value) { |  | ||||||
| 				this.$store.set('widgets', value); |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		this.$emit('mounted', this.$el); | 		this.$emit('mounted', this.$el); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		widgetFunc(id) { | 		addWidget(widget) { | ||||||
| 			this.settings[id](); | 			this.$store.set('widgets', [{ | ||||||
| 		}, | 				...widget, | ||||||
| 
 |  | ||||||
| 		async addWidget() { |  | ||||||
| 			const { canceled, result: widget } = await os.dialog({ |  | ||||||
| 				type: null, |  | ||||||
| 				title: this.$ts.chooseWidget, |  | ||||||
| 				select: { |  | ||||||
| 					items: widgets.map(widget => ({ |  | ||||||
| 						value: widget, |  | ||||||
| 						text: this.$t('_widgets.' + widget), |  | ||||||
| 					})) |  | ||||||
| 				}, |  | ||||||
| 				showCancelButton: true |  | ||||||
| 			}); |  | ||||||
| 			if (canceled) return; |  | ||||||
| 
 |  | ||||||
| 			this.$store.set('widgets', [...this.$store.state.widgets, { |  | ||||||
| 				name: widget, |  | ||||||
| 				id: uuid(), |  | ||||||
| 				place: null, | 				place: null, | ||||||
| 				data: {} | 			}, ...this.$store.state.widgets]); | ||||||
| 			}]); |  | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		removeWidget(widget) { | 		removeWidget(widget) { | ||||||
| 			this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id)); | 			this.$store.set('widgets', this.$store.state.widgets.filter(w => w.id != widget.id)); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		saveWidget(id, data) { | 		updateWidget({ id, data }) { | ||||||
| 			this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? { | 			this.$store.set('widgets', this.$store.state.widgets.map(w => w.id === id ? { | ||||||
| 				...w, | 				...w, | ||||||
| 				data: data | 				data: data | ||||||
| 			} : w)); | 			} : w)); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		updateWidgets(widgets) { | ||||||
|  | 			this.$store.set('widgets', widgets); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | @ -129,35 +77,5 @@ export default defineComponent({ | ||||||
| 	> .add { | 	> .add { | ||||||
| 		margin: 0 auto; | 		margin: 0 auto; | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	.customize-container { |  | ||||||
| 		margin: 8px 0; |  | ||||||
| 
 |  | ||||||
| 		> header { |  | ||||||
| 			position: relative; |  | ||||||
| 			line-height: 32px; |  | ||||||
| 
 |  | ||||||
| 			> .handle { |  | ||||||
| 				padding: 0 8px; |  | ||||||
| 				cursor: move; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .remove { |  | ||||||
| 				position: absolute; |  | ||||||
| 				top: 0; |  | ||||||
| 				right: 0; |  | ||||||
| 				padding: 0 8px; |  | ||||||
| 				line-height: 32px; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> div { |  | ||||||
| 			padding: 8px; |  | ||||||
| 
 |  | ||||||
| 			> * { |  | ||||||
| 				pointer-events: none; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue