From 43b080eff52d7de5d691797d6ad539b6757c37c9 Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Thu, 27 Jul 2023 15:20:23 -0300 Subject: [PATCH] Working. Lacks commiting. --- src/App.vue | 118 +++++--- src/assets/defaults.ts | 69 ++++- src/components/EditMenu.vue | 400 ++++++++++++------------- src/components/WidgetHugger.vue | 505 +++++++++++++------------------- src/stores/widgetManager.ts | 154 ++++++++-- src/styles/global.css | 18 ++ src/types/widgets.ts | 16 + src/views/WidgetsView.vue | 30 +- 8 files changed, 699 insertions(+), 611 deletions(-) diff --git a/src/App.vue b/src/App.vue index fc5fbbb21..8eb477de8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,52 +1,6 @@ @@ -477,15 +334,6 @@ const optionsBtnTopStyle = computed(() => `${48 - constrain(widget.value.positio left: calc(50% - 16px); bottom: calc(50% - 16px); } -.resizer.allowResizing { - width: 10px; - height: 10px; - cursor: se-resize; - user-select: none; - position: absolute; - left: 100%; - top: 100%; -} .options-btn { display: none; position: absolute; @@ -496,4 +344,71 @@ const optionsBtnTopStyle = computed(() => `${48 - constrain(widget.value.positio filter: drop-shadow(0.5px 0.5px 0.5px black); display: v-bind('mouseOverWidgetStyle'); } + +.resize-handle { + position: absolute; + width: 10px; + height: 10px; + background: white; + border: 1px solid black; + z-index: 2; + opacity: 0; + transition: opacity 0.3s ease; +} + +.resize-handle.hoveringWidgetOrOverlay.allowResizing { + opacity: 1; +} + +.resize-handle.top-left.allowResizing { + top: -5px; + left: -5px; + cursor: nwse-resize; +} + +.resize-handle.top-right.allowResizing { + top: -5px; + right: -5px; + cursor: nesw-resize; +} + +.resize-handle.bottom-left.allowResizing { + bottom: -5px; + left: -5px; + cursor: nesw-resize; +} + +.resize-handle.bottom-right.allowResizing { + bottom: -5px; + right: -5px; + cursor: nwse-resize; +} + +.resize-handle.left.allowResizing { + top: 50%; + left: -5px; + transform: translateY(-50%); + cursor: ew-resize; +} + +.resize-handle.right.allowResizing { + top: 50%; + right: -5px; + transform: translateY(-50%); + cursor: ew-resize; +} + +.resize-handle.top.allowResizing { + top: -5px; + left: 50%; + transform: translateX(-50%); + cursor: ns-resize; +} + +.resize-handle.bottom.allowResizing { + bottom: -5px; + left: 50%; + transform: translateX(-50%); + cursor: ns-resize; +} diff --git a/src/stores/widgetManager.ts b/src/stores/widgetManager.ts index fcc9a6996..8a8245a3f 100644 --- a/src/stores/widgetManager.ts +++ b/src/stores/widgetManager.ts @@ -1,21 +1,29 @@ import '@/libs/cosmos' import { useStorage } from '@vueuse/core' +import { saveAs } from 'file-saver' import { defineStore } from 'pinia' +import Swal from 'sweetalert2' import { v4 as uuid4 } from 'uuid' -import { ref } from 'vue' +import { computed, ref } from 'vue' import { widgetProfile, widgetProfiles } from '@/assets/defaults' import { miniWidgetsProfile } from '@/assets/defaults' import * as Words from '@/libs/funny-name/words' -import type { Layer, Profile, Widget, WidgetType } from '@/types/widgets' +import { isEqual } from '@/libs/utils' +import type { Point2D, SizeRect2D } from '@/types/general' +import { type Layer, type Profile, type Widget, type WidgetType, isLayer, isProfile } from '@/types/widgets' export const useWidgetManagerStore = defineStore('widget-manager', () => { const editingMode = ref(false) + const showGrid = ref(true) + const gridInterval = ref(0.01) const currentProfile = useStorage('cockpit-current-profile', widgetProfile) const currentMiniWidgetsProfile = useStorage('cockpit-mini-widgets-profile', miniWidgetsProfile) const savedProfiles = useStorage('cockpit-saved-profiles', widgetProfiles) + const currentLayer = computed(() => currentProfile.value.layers[0]) + /** * Get layer where given widget is at * @returns { Layer } @@ -79,6 +87,28 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => { savedProfiles.value = widgetProfiles } + const exportCurrentProfile = (): void => { + const blob = new Blob([JSON.stringify(currentProfile.value)], { type: 'text/plain;charset=utf-8' }) + saveAs(blob, `cockpit-widget-profile.json`) + } + + const importProfile = (e: Event): void => { + const reader = new FileReader() + reader.onload = (event: Event) => { + // @ts-ignore: We know the event type and need refactor of the event typing + const contents = event.target.result + const maybeProfile = JSON.parse(contents) + if (!isProfile(maybeProfile)) { + Swal.fire({ icon: 'error', text: 'Invalid profile file.', timer: 3000 }) + return + } + const newProfile = saveProfile(maybeProfile) + loadProfile(newProfile) + } + // @ts-ignore: We know the event type and need refactor of the event typing + reader.readAsText(e.target.files[0]) + } + /** * Adds new layer to the store, with a randomly generated hash with UUID4 pattern */ @@ -99,6 +129,47 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => { currentProfile.value.layers.splice(index, 1) } + /** + * Rename a layer + * @param { Layer } layer - Layer + * @param { string } name - New name of the layer + */ + function renameLayer(layer: Layer, name: string): void { + const index = currentProfile.value.layers.indexOf(layer) + currentProfile.value.layers[index].name = name + } + + /** + * Select a layer to be used + * @param { Layer } layer - Layer + */ + const selectLayer = (layer: Layer): void => { + const index = currentProfile.value.layers.indexOf(layer) + currentProfile.value.layers.splice(index, 1) + currentProfile.value.layers.unshift(layer) + } + + const exportCurrentLayer = (): void => { + const blob = new Blob([JSON.stringify(currentLayer.value)], { type: 'text/plain;charset=utf-8' }) + saveAs(blob, `cockpit-widget-layer.json`) + } + + const importLayer = (e: Event): void => { + const reader = new FileReader() + reader.onload = (event: Event) => { + // @ts-ignore: We know the event type and need refactor of the event typing + const contents = event.target.result + const maybeLayer = JSON.parse(contents) + if (!isLayer(maybeLayer)) { + Swal.fire({ icon: 'error', text: 'Invalid layer file.', timer: 3000 }) + return + } + currentProfile.value.layers.unshift(maybeLayer) + } + // @ts-ignore: We know the event type and need refactor of the event typing + reader.readAsText(e.target.files[0]) + } + /** * Add widget with given type to given layer * @param { WidgetType } widgetType - Type of the widget @@ -108,12 +179,18 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => { const widgetHash = uuid4() layer.widgets.unshift({ hash: widgetHash, - name: widgetHash, + name: widgetType, component: widgetType, position: { x: 0.4, y: 0.32 }, size: { width: 0.2, height: 0.36 }, options: {}, - managerVars: { timesMounted: 0 }, + managerVars: { + timesMounted: 0, + lastNonMaximizedX: 0.4, + lastNonMaximizedY: 0.32, + lastNonMaximizedWidth: 0.2, + lastNonMaximizedHeight: 0.36, + }, }) } @@ -127,42 +204,71 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => { layer.widgets.splice(index, 1) } - /** - * Send widget to the beggining (front) of the widgets list - * @param { Widget } widget - Widget - */ - function bringWidgetFront(widget: Widget): void { - const layer = layerFromWidget(widget) - const index = layer.widgets.indexOf(widget) - layer.widgets.splice(index, 1) - layer.widgets.unshift(widget) + const fullScreenPosition = { x: 0, y: 0 } + const fullScreenSize = { width: 1, height: 1 } + const defaultRestoredPosition: Point2D = { x: 0.15, y: 0.15 } + const defaultRestoredSize: SizeRect2D = { width: 0.7, height: 0.7 } + + const toggleFullScreen = (widget: Widget): void => { + if (!isFullScreen(widget)) { + widget.managerVars.lastNonMaximizedX = widget.position.x + widget.managerVars.lastNonMaximizedY = widget.position.y + widget.managerVars.lastNonMaximizedWidth = widget.size.width + widget.managerVars.lastNonMaximizedHeight = widget.size.height + widget.position = fullScreenPosition + widget.size = fullScreenSize + return + } + + if (widget.managerVars.lastNonMaximizedX === 0) { + widget.managerVars.lastNonMaximizedX = defaultRestoredPosition.x + } + if (widget.managerVars.lastNonMaximizedY === fullScreenPosition.y) { + widget.managerVars.lastNonMaximizedY = defaultRestoredPosition.y + } + if (widget.managerVars.lastNonMaximizedWidth === fullScreenSize.width) { + widget.managerVars.lastNonMaximizedWidth = defaultRestoredSize.width + } + if (widget.managerVars.lastNonMaximizedHeight === fullScreenSize.height) { + widget.managerVars.lastNonMaximizedHeight = defaultRestoredSize.height + } + widget.position = { + x: widget.managerVars.lastNonMaximizedX, + y: widget.managerVars.lastNonMaximizedY, + } + widget.size = { + width: widget.managerVars.lastNonMaximizedWidth, + height: widget.managerVars.lastNonMaximizedHeight, + } } - /** - * Send widget to the end (back) of the widgets list - * @param { Widget } widget - Widget - */ - function sendWidgetBack(widget: Widget): void { - const layer = layerFromWidget(widget) - const index = layer.widgets.indexOf(widget) - layer.widgets.splice(index, 1) - layer.widgets.push(widget) + const isFullScreen = (widget: Widget): boolean => { + return isEqual(widget.position, fullScreenPosition) && isEqual(widget.size, fullScreenSize) } return { editingMode, + showGrid, + gridInterval, currentProfile, + currentLayer, currentMiniWidgetsProfile, savedProfiles, loadProfile, saveProfile, resetCurrentProfile, resetSavedProfiles, + exportCurrentProfile, + importProfile, addLayer, deleteLayer, + renameLayer, + selectLayer, + exportCurrentLayer, + importLayer, addWidget, deleteWidget, - bringWidgetFront, - sendWidgetBack, + toggleFullScreen, + isFullScreen, } }) diff --git a/src/styles/global.css b/src/styles/global.css index 80493b30a..e889996f1 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -24,3 +24,21 @@ .v-leave-to { opacity: 0; } + +::-webkit-scrollbar { + width: 0.5em; + height: 0.5em; +} +::-webkit-scrollbar-track { + background-color: hsla(0, 0%, 0%, 0.1); + border-radius: 100vw; + margin-block: 0.5rem; + margin-inline: 0.5rem; +} +::-webkit-scrollbar-thumb { + background-color: hsla(0, 0%, 0%, 0.2); + border-radius: 100vw; +} +::-webkit-scrollbar-thumb:hover { + background-color: hsla(0, 0%, 0%, 0.411); +} diff --git a/src/types/widgets.ts b/src/types/widgets.ts index 042f76618..ba0147b17 100644 --- a/src/types/widgets.ts +++ b/src/types/widgets.ts @@ -53,6 +53,22 @@ export type Widget = { * Number of times the widget was mounted */ timesMounted: number + /** + * Last widget X position when it wasn't maximized + */ + lastNonMaximizedX: number + /** + * Last widget Y position when it wasn't maximized + */ + lastNonMaximizedY: number + /** + * Last widget width when it wasn't maximized + */ + lastNonMaximizedWidth: number + /** + * Last widget height when it wasn't maximized + */ + lastNonMaximizedHeight: number } } diff --git a/src/views/WidgetsView.vue b/src/views/WidgetsView.vue index 84287a867..21f313a59 100644 --- a/src/views/WidgetsView.vue +++ b/src/views/WidgetsView.vue @@ -1,6 +1,5 @@