mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 22:20:51 +01:00
Add workspace background customization and fix launcher drawer drag
- Add workspace background image support with file-based storage - Store background images in $APPLOCALDATA/files directory - Save file paths in database (text column in haex_workspaces) - Use convertFileSrc for secure asset:// URL conversion - Add context menu to workspaces with "Hintergrund ändern" option - Implement background management in settings - File selection dialog for PNG, JPG, JPEG, WebP images - Copy selected images to app data directory - Remove background with file cleanup - Multilingual UI (German/English) - Fix launcher drawer drag interference - Add :handle-only="true" to UDrawer to restrict drag to handle - Simplify drag handlers (removed complex state tracking) - Items can now be dragged to desktop without drawer interference - Extend Tauri asset protocol scope to include $APPLOCALDATA/** for background image loading
This commit is contained in:
@ -64,7 +64,7 @@
|
|||||||
},
|
},
|
||||||
"assetProtocol": {
|
"assetProtocol": {
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"scope": ["$APPDATA", "$RESOURCE"]
|
"scope": ["$APPDATA", "$RESOURCE", "$APPLOCALDATA/**"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -23,191 +23,198 @@
|
|||||||
:key="workspace.id"
|
:key="workspace.id"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
>
|
>
|
||||||
<div
|
<UContextMenu :items="getWorkspaceContextMenuItems(workspace.id)">
|
||||||
class="w-full h-full relative"
|
|
||||||
@click.self.stop="handleDesktopClick"
|
|
||||||
@mousedown.left.self="handleAreaSelectStart"
|
|
||||||
@dragover.prevent="handleDragOver"
|
|
||||||
@drop.prevent="handleDrop($event, workspace.id)"
|
|
||||||
>
|
|
||||||
<!-- Grid Pattern Background -->
|
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 pointer-events-none opacity-30"
|
class="w-full h-full relative"
|
||||||
:style="{
|
:style="getWorkspaceBackgroundStyle(workspace)"
|
||||||
backgroundImage:
|
@click.self.stop="handleDesktopClick"
|
||||||
'linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)',
|
@mousedown.left.self="handleAreaSelectStart"
|
||||||
backgroundSize: '32px 32px',
|
@dragover.prevent="handleDragOver"
|
||||||
}"
|
@drop.prevent="handleDrop($event, workspace.id)"
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Snap Dropzones (only visible when window drag near edge) -->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="absolute left-0 top-0 bottom-0 border-blue-500 pointer-events-none backdrop-blur-sm z-50 transition-all duration-500 ease-in-out"
|
|
||||||
:class="showLeftSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="absolute right-0 top-0 bottom-0 border-blue-500 pointer-events-none backdrop-blur-sm z-50 transition-all duration-500 ease-in-out"
|
|
||||||
:class="showRightSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Area Selection Box -->
|
|
||||||
<div
|
|
||||||
v-if="isAreaSelecting"
|
|
||||||
class="absolute bg-blue-500/20 border-2 border-blue-500 pointer-events-none z-30"
|
|
||||||
:style="selectionBoxStyle"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Icons for this workspace -->
|
|
||||||
<HaexDesktopIcon
|
|
||||||
v-for="item in getWorkspaceIcons(workspace.id)"
|
|
||||||
:id="item.id"
|
|
||||||
:key="item.id"
|
|
||||||
:item-type="item.itemType"
|
|
||||||
:reference-id="item.referenceId"
|
|
||||||
:initial-x="item.positionX"
|
|
||||||
:initial-y="item.positionY"
|
|
||||||
:label="item.label"
|
|
||||||
:icon="item.icon"
|
|
||||||
class="no-swipe"
|
|
||||||
@position-changed="handlePositionChanged"
|
|
||||||
@drag-start="handleDragStart"
|
|
||||||
@drag-end="handleDragEnd"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Windows for this workspace -->
|
|
||||||
<template
|
|
||||||
v-for="window in getWorkspaceWindows(workspace.id)"
|
|
||||||
:key="window.id"
|
|
||||||
>
|
>
|
||||||
<!-- Overview Mode: Teleport to window preview -->
|
<!-- Grid Pattern Background -->
|
||||||
<Teleport
|
<div
|
||||||
v-if="
|
class="absolute inset-0 pointer-events-none opacity-30"
|
||||||
windowManager.showWindowOverview &&
|
:style="{
|
||||||
overviewWindowState.has(window.id)
|
backgroundImage:
|
||||||
"
|
'linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 0, 0, 0.1) 1px, transparent 1px)',
|
||||||
:to="`#window-preview-${window.id}`"
|
backgroundSize: '32px 32px',
|
||||||
>
|
}"
|
||||||
<div
|
/>
|
||||||
class="absolute origin-top-left"
|
|
||||||
:style="{
|
|
||||||
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
|
||||||
width: `${overviewWindowState.get(window.id)!.width}px`,
|
|
||||||
height: `${overviewWindowState.get(window.id)!.height}px`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<HaexWindow
|
|
||||||
v-show="
|
|
||||||
windowManager.showWindowOverview || !window.isMinimized
|
|
||||||
"
|
|
||||||
:id="window.id"
|
|
||||||
v-model:x="overviewWindowState.get(window.id)!.x"
|
|
||||||
v-model:y="overviewWindowState.get(window.id)!.y"
|
|
||||||
v-model:width="overviewWindowState.get(window.id)!.width"
|
|
||||||
v-model:height="overviewWindowState.get(window.id)!.height"
|
|
||||||
:title="window.title"
|
|
||||||
:icon="window.icon"
|
|
||||||
:is-active="windowManager.isWindowActive(window.id)"
|
|
||||||
:source-x="window.sourceX"
|
|
||||||
:source-y="window.sourceY"
|
|
||||||
:source-width="window.sourceWidth"
|
|
||||||
:source-height="window.sourceHeight"
|
|
||||||
:is-opening="window.isOpening"
|
|
||||||
:is-closing="window.isClosing"
|
|
||||||
:warning-level="
|
|
||||||
window.type === 'extension' &&
|
|
||||||
availableExtensions.find(
|
|
||||||
(ext) => ext.id === window.sourceId,
|
|
||||||
)?.devServerUrl
|
|
||||||
? 'warning'
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
class="no-swipe"
|
|
||||||
@close="windowManager.closeWindow(window.id)"
|
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
|
||||||
@activate="windowManager.activateWindow(window.id)"
|
|
||||||
@position-changed="
|
|
||||||
(x, y) =>
|
|
||||||
windowManager.updateWindowPosition(window.id, x, y)
|
|
||||||
"
|
|
||||||
@size-changed="
|
|
||||||
(width, height) =>
|
|
||||||
windowManager.updateWindowSize(window.id, width, height)
|
|
||||||
"
|
|
||||||
@drag-start="handleWindowDragStart(window.id)"
|
|
||||||
@drag-end="handleWindowDragEnd"
|
|
||||||
>
|
|
||||||
<!-- System Window: Render Vue Component -->
|
|
||||||
<component
|
|
||||||
:is="getSystemWindowComponent(window.sourceId)"
|
|
||||||
v-if="window.type === 'system'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Extension Window: Render iFrame -->
|
<!-- Snap Dropzones (only visible when window drag near edge) -->
|
||||||
<HaexDesktopExtensionFrame
|
|
||||||
v-else
|
|
||||||
:extension-id="window.sourceId"
|
|
||||||
:window-id="window.id"
|
|
||||||
/>
|
|
||||||
</HaexWindow>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
|
|
||||||
<!-- Desktop Mode: Render directly in workspace -->
|
<div
|
||||||
<HaexWindow
|
class="absolute left-0 top-0 bottom-0 border-blue-500 pointer-events-none backdrop-blur-sm z-50 transition-all duration-500 ease-in-out"
|
||||||
v-else
|
:class="
|
||||||
v-show="windowManager.showWindowOverview || !window.isMinimized"
|
showLeftSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'
|
||||||
:id="window.id"
|
|
||||||
v-model:x="window.x"
|
|
||||||
v-model:y="window.y"
|
|
||||||
v-model:width="window.width"
|
|
||||||
v-model:height="window.height"
|
|
||||||
:title="window.title"
|
|
||||||
:icon="window.icon"
|
|
||||||
:is-active="windowManager.isWindowActive(window.id)"
|
|
||||||
:source-x="window.sourceX"
|
|
||||||
:source-y="window.sourceY"
|
|
||||||
:source-width="window.sourceWidth"
|
|
||||||
:source-height="window.sourceHeight"
|
|
||||||
:is-opening="window.isOpening"
|
|
||||||
:is-closing="window.isClosing"
|
|
||||||
:warning-level="
|
|
||||||
window.type === 'extension' &&
|
|
||||||
availableExtensions.find((ext) => ext.id === window.sourceId)
|
|
||||||
?.devServerUrl
|
|
||||||
? 'warning'
|
|
||||||
: undefined
|
|
||||||
"
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute right-0 top-0 bottom-0 border-blue-500 pointer-events-none backdrop-blur-sm z-50 transition-all duration-500 ease-in-out"
|
||||||
|
:class="
|
||||||
|
showRightSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Area Selection Box -->
|
||||||
|
<div
|
||||||
|
v-if="isAreaSelecting"
|
||||||
|
class="absolute bg-blue-500/20 border-2 border-blue-500 pointer-events-none z-30"
|
||||||
|
:style="selectionBoxStyle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Icons for this workspace -->
|
||||||
|
<HaexDesktopIcon
|
||||||
|
v-for="item in getWorkspaceIcons(workspace.id)"
|
||||||
|
:id="item.id"
|
||||||
|
:key="item.id"
|
||||||
|
:item-type="item.itemType"
|
||||||
|
:reference-id="item.referenceId"
|
||||||
|
:initial-x="item.positionX"
|
||||||
|
:initial-y="item.positionY"
|
||||||
|
:label="item.label"
|
||||||
|
:icon="item.icon"
|
||||||
class="no-swipe"
|
class="no-swipe"
|
||||||
@close="windowManager.closeWindow(window.id)"
|
@position-changed="handlePositionChanged"
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
@drag-start="handleDragStart"
|
||||||
@activate="windowManager.activateWindow(window.id)"
|
@drag-end="handleDragEnd"
|
||||||
@position-changed="
|
/>
|
||||||
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
|
|
||||||
"
|
|
||||||
@size-changed="
|
|
||||||
(width, height) =>
|
|
||||||
windowManager.updateWindowSize(window.id, width, height)
|
|
||||||
"
|
|
||||||
@drag-start="handleWindowDragStart(window.id)"
|
|
||||||
@drag-end="handleWindowDragEnd"
|
|
||||||
>
|
|
||||||
<!-- System Window: Render Vue Component -->
|
|
||||||
<component
|
|
||||||
:is="getSystemWindowComponent(window.sourceId)"
|
|
||||||
v-if="window.type === 'system'"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Extension Window: Render iFrame -->
|
<!-- Windows for this workspace -->
|
||||||
<HaexDesktopExtensionFrame
|
<template
|
||||||
|
v-for="window in getWorkspaceWindows(workspace.id)"
|
||||||
|
:key="window.id"
|
||||||
|
>
|
||||||
|
<!-- Overview Mode: Teleport to window preview -->
|
||||||
|
<Teleport
|
||||||
|
v-if="
|
||||||
|
windowManager.showWindowOverview &&
|
||||||
|
overviewWindowState.has(window.id)
|
||||||
|
"
|
||||||
|
:to="`#window-preview-${window.id}`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute origin-top-left"
|
||||||
|
:style="{
|
||||||
|
transform: `scale(${overviewWindowState.get(window.id)!.scale})`,
|
||||||
|
width: `${overviewWindowState.get(window.id)!.width}px`,
|
||||||
|
height: `${overviewWindowState.get(window.id)!.height}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<HaexWindow
|
||||||
|
v-show="
|
||||||
|
windowManager.showWindowOverview || !window.isMinimized
|
||||||
|
"
|
||||||
|
:id="window.id"
|
||||||
|
v-model:x="overviewWindowState.get(window.id)!.x"
|
||||||
|
v-model:y="overviewWindowState.get(window.id)!.y"
|
||||||
|
v-model:width="overviewWindowState.get(window.id)!.width"
|
||||||
|
v-model:height="overviewWindowState.get(window.id)!.height"
|
||||||
|
:title="window.title"
|
||||||
|
:icon="window.icon"
|
||||||
|
:is-active="windowManager.isWindowActive(window.id)"
|
||||||
|
:source-x="window.sourceX"
|
||||||
|
:source-y="window.sourceY"
|
||||||
|
:source-width="window.sourceWidth"
|
||||||
|
:source-height="window.sourceHeight"
|
||||||
|
:is-opening="window.isOpening"
|
||||||
|
:is-closing="window.isClosing"
|
||||||
|
:warning-level="
|
||||||
|
window.type === 'extension' &&
|
||||||
|
availableExtensions.find(
|
||||||
|
(ext) => ext.id === window.sourceId,
|
||||||
|
)?.devServerUrl
|
||||||
|
? 'warning'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
class="no-swipe"
|
||||||
|
@close="windowManager.closeWindow(window.id)"
|
||||||
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
|
@activate="windowManager.activateWindow(window.id)"
|
||||||
|
@position-changed="
|
||||||
|
(x, y) =>
|
||||||
|
windowManager.updateWindowPosition(window.id, x, y)
|
||||||
|
"
|
||||||
|
@size-changed="
|
||||||
|
(width, height) =>
|
||||||
|
windowManager.updateWindowSize(window.id, width, height)
|
||||||
|
"
|
||||||
|
@drag-start="handleWindowDragStart(window.id)"
|
||||||
|
@drag-end="handleWindowDragEnd"
|
||||||
|
>
|
||||||
|
<!-- System Window: Render Vue Component -->
|
||||||
|
<component
|
||||||
|
:is="getSystemWindowComponent(window.sourceId)"
|
||||||
|
v-if="window.type === 'system'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Extension Window: Render iFrame -->
|
||||||
|
<HaexDesktopExtensionFrame
|
||||||
|
v-else
|
||||||
|
:extension-id="window.sourceId"
|
||||||
|
:window-id="window.id"
|
||||||
|
/>
|
||||||
|
</HaexWindow>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<!-- Desktop Mode: Render directly in workspace -->
|
||||||
|
<HaexWindow
|
||||||
v-else
|
v-else
|
||||||
:extension-id="window.sourceId"
|
v-show="windowManager.showWindowOverview || !window.isMinimized"
|
||||||
:window-id="window.id"
|
:id="window.id"
|
||||||
/>
|
v-model:x="window.x"
|
||||||
</HaexWindow>
|
v-model:y="window.y"
|
||||||
</template>
|
v-model:width="window.width"
|
||||||
</div>
|
v-model:height="window.height"
|
||||||
|
:title="window.title"
|
||||||
|
:icon="window.icon"
|
||||||
|
:is-active="windowManager.isWindowActive(window.id)"
|
||||||
|
:source-x="window.sourceX"
|
||||||
|
:source-y="window.sourceY"
|
||||||
|
:source-width="window.sourceWidth"
|
||||||
|
:source-height="window.sourceHeight"
|
||||||
|
:is-opening="window.isOpening"
|
||||||
|
:is-closing="window.isClosing"
|
||||||
|
:warning-level="
|
||||||
|
window.type === 'extension' &&
|
||||||
|
availableExtensions.find((ext) => ext.id === window.sourceId)
|
||||||
|
?.devServerUrl
|
||||||
|
? 'warning'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
class="no-swipe"
|
||||||
|
@close="windowManager.closeWindow(window.id)"
|
||||||
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
|
@activate="windowManager.activateWindow(window.id)"
|
||||||
|
@position-changed="
|
||||||
|
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
|
||||||
|
"
|
||||||
|
@size-changed="
|
||||||
|
(width, height) =>
|
||||||
|
windowManager.updateWindowSize(window.id, width, height)
|
||||||
|
"
|
||||||
|
@drag-start="handleWindowDragStart(window.id)"
|
||||||
|
@drag-end="handleWindowDragEnd"
|
||||||
|
>
|
||||||
|
<!-- System Window: Render Vue Component -->
|
||||||
|
<component
|
||||||
|
:is="getSystemWindowComponent(window.sourceId)"
|
||||||
|
v-if="window.type === 'system'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Extension Window: Render iFrame -->
|
||||||
|
<HaexDesktopExtensionFrame
|
||||||
|
v-else
|
||||||
|
:extension-id="window.sourceId"
|
||||||
|
:window-id="window.id"
|
||||||
|
/>
|
||||||
|
</HaexWindow>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</UContextMenu>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
|
||||||
@ -239,6 +246,8 @@ const {
|
|||||||
allowSwipe,
|
allowSwipe,
|
||||||
isOverviewMode,
|
isOverviewMode,
|
||||||
} = storeToRefs(workspaceStore)
|
} = storeToRefs(workspaceStore)
|
||||||
|
const { getWorkspaceBackgroundStyle, getWorkspaceContextMenuItems } =
|
||||||
|
workspaceStore
|
||||||
|
|
||||||
const { x: mouseX } = useMouse()
|
const { x: mouseX } = useMouse()
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,9 @@
|
|||||||
direction="right"
|
direction="right"
|
||||||
:title="t('launcher.title')"
|
:title="t('launcher.title')"
|
||||||
:description="t('launcher.description')"
|
:description="t('launcher.description')"
|
||||||
|
:overlay="false"
|
||||||
|
:modal="false"
|
||||||
|
:handle-only="true"
|
||||||
:ui="{
|
:ui="{
|
||||||
content: 'w-dvw max-w-md sm:max-w-fit',
|
content: 'w-dvw max-w-md sm:max-w-fit',
|
||||||
}"
|
}"
|
||||||
@ -30,7 +33,7 @@
|
|||||||
size="lg"
|
size="lg"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:ui="{
|
:ui="{
|
||||||
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible cursor-grab active:cursor-grabbing',
|
base: 'size-24 flex flex-wrap text-sm items-center justify-center overflow-visible cursor-grab',
|
||||||
leadingIcon: 'size-10',
|
leadingIcon: 'size-10',
|
||||||
label: 'w-full',
|
label: 'w-full',
|
||||||
}"
|
}"
|
||||||
@ -241,10 +244,6 @@ const handleDragStart = (event: DragEvent, item: LauncherItem) => {
|
|||||||
event.dataTransfer.setDragImage(dragImage, 20, 20)
|
event.dataTransfer.setDragImage(dragImage, 20, 20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDragEnd = () => {
|
|
||||||
// Cleanup if needed
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
|
|||||||
@ -33,6 +33,20 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2">{{ t('workspaceBackground.label') }}</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UiButton
|
||||||
|
:label="t('workspaceBackground.choose')"
|
||||||
|
@click="selectBackgroundImage"
|
||||||
|
/>
|
||||||
|
<UiButton
|
||||||
|
v-if="currentWorkspace?.background"
|
||||||
|
:label="t('workspaceBackground.remove.label')"
|
||||||
|
color="error"
|
||||||
|
@click="removeBackgroundImage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="h-full"/>
|
<div class="h-full"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -40,6 +54,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Locale } from 'vue-i18n'
|
import type { Locale } from 'vue-i18n'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { readFile, writeFile, mkdir, exists, remove } from '@tauri-apps/plugin-fs'
|
||||||
|
import { appLocalDataDir, join } from '@tauri-apps/api/path'
|
||||||
|
|
||||||
const { t, setLocale } = useI18n()
|
const { t, setLocale } = useI18n()
|
||||||
|
|
||||||
@ -77,6 +94,10 @@ const { requestNotificationPermissionAsync } = useNotificationStore()
|
|||||||
const { deviceName } = storeToRefs(useDeviceStore())
|
const { deviceName } = storeToRefs(useDeviceStore())
|
||||||
const { updateDeviceNameAsync, readDeviceNameAsync } = useDeviceStore()
|
const { updateDeviceNameAsync, readDeviceNameAsync } = useDeviceStore()
|
||||||
|
|
||||||
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
const { currentWorkspace } = storeToRefs(workspaceStore)
|
||||||
|
const { updateWorkspaceBackgroundAsync } = workspaceStore
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await readDeviceNameAsync()
|
await readDeviceNameAsync()
|
||||||
})
|
})
|
||||||
@ -92,6 +113,72 @@ const onUpdateDeviceNameAsync = async () => {
|
|||||||
add({ description: t('deviceName.update.error'), color: 'error' })
|
add({ description: t('deviceName.update.error'), color: 'error' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectBackgroundImage = async () => {
|
||||||
|
if (!currentWorkspace.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const selected = await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [{
|
||||||
|
name: 'Images',
|
||||||
|
extensions: ['png', 'jpg', 'jpeg', 'webp']
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (selected && typeof selected === 'string') {
|
||||||
|
const fileData = await readFile(selected)
|
||||||
|
|
||||||
|
// Create files directory if it doesn't exist
|
||||||
|
const appDataPath = await appLocalDataDir()
|
||||||
|
const filesDir = await join(appDataPath, 'files')
|
||||||
|
|
||||||
|
if (!await exists(filesDir)) {
|
||||||
|
await mkdir(filesDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename for the background image
|
||||||
|
const ext = selected.split('.').pop()?.toLowerCase() || 'png'
|
||||||
|
const fileName = `workspace-${currentWorkspace.value.id}-background.${ext}`
|
||||||
|
const targetPath = await join(filesDir, fileName)
|
||||||
|
|
||||||
|
// Copy file to app data directory
|
||||||
|
await writeFile(targetPath, fileData)
|
||||||
|
|
||||||
|
// Store the absolute file path in database
|
||||||
|
await updateWorkspaceBackgroundAsync(currentWorkspace.value.id, targetPath)
|
||||||
|
add({ description: t('workspaceBackground.update.success'), color: 'success' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error selecting background:', error)
|
||||||
|
add({ description: t('workspaceBackground.update.error'), color: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeBackgroundImage = async () => {
|
||||||
|
if (!currentWorkspace.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Delete the background file if it exists
|
||||||
|
if (currentWorkspace.value.background) {
|
||||||
|
try {
|
||||||
|
// The background field contains the absolute file path
|
||||||
|
if (await exists(currentWorkspace.value.background)) {
|
||||||
|
await remove(currentWorkspace.value.background)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Could not delete background file:', err)
|
||||||
|
// Continue anyway to clear the database entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateWorkspaceBackgroundAsync(currentWorkspace.value.id, null)
|
||||||
|
add({ description: t('workspaceBackground.remove.success'), color: 'success' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing background:', error)
|
||||||
|
add({ description: t('workspaceBackground.remove.error'), color: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
@ -112,6 +199,16 @@ de:
|
|||||||
update:
|
update:
|
||||||
success: Gerätename wurde erfolgreich aktualisiert
|
success: Gerätename wurde erfolgreich aktualisiert
|
||||||
error: Gerätename konnte nich aktualisiert werden
|
error: Gerätename konnte nich aktualisiert werden
|
||||||
|
workspaceBackground:
|
||||||
|
label: Workspace-Hintergrund
|
||||||
|
choose: Bild auswählen
|
||||||
|
update:
|
||||||
|
success: Hintergrund erfolgreich aktualisiert
|
||||||
|
error: Fehler beim Aktualisieren des Hintergrunds
|
||||||
|
remove:
|
||||||
|
label: Hintergrund entfernen
|
||||||
|
success: Hintergrund erfolgreich entfernt
|
||||||
|
error: Fehler beim Entfernen des Hintergrunds
|
||||||
en:
|
en:
|
||||||
language: Language
|
language: Language
|
||||||
design: Design
|
design: Design
|
||||||
@ -129,4 +226,14 @@ en:
|
|||||||
update:
|
update:
|
||||||
success: Device name has been successfully updated
|
success: Device name has been successfully updated
|
||||||
error: Device name could not be updated
|
error: Device name could not be updated
|
||||||
|
workspaceBackground:
|
||||||
|
label: Workspace Background
|
||||||
|
choose: Choose Image
|
||||||
|
update:
|
||||||
|
success: Background successfully updated
|
||||||
|
error: Error updating background
|
||||||
|
remove:
|
||||||
|
label: Remove Background
|
||||||
|
success: Background successfully removed
|
||||||
|
error: Error removing background
|
||||||
</i18n>
|
</i18n>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
type AnySQLiteColumn,
|
type AnySQLiteColumn,
|
||||||
type SQLiteColumnBuilderBase,
|
type SQLiteColumnBuilderBase,
|
||||||
} from 'drizzle-orm/sqlite-core'
|
} from 'drizzle-orm/sqlite-core'
|
||||||
import tableNames from '~/database/tableNames.json'
|
import tableNames from '@/database/tableNames.json'
|
||||||
|
|
||||||
const crdtColumnNames = {
|
const crdtColumnNames = {
|
||||||
haexTimestamp: 'haex_timestamp',
|
haexTimestamp: 'haex_timestamp',
|
||||||
@ -137,6 +137,7 @@ export const haexWorkspaces = sqliteTable(
|
|||||||
position: integer(tableNames.haex.workspaces.columns.position)
|
position: integer(tableNames.haex.workspaces.columns.position)
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
|
background: text(),
|
||||||
}),
|
}),
|
||||||
(table) => [unique().on(table.position)],
|
(table) => [unique().on(table.position)],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
type SelectHaexWorkspaces,
|
type SelectHaexWorkspaces,
|
||||||
} from '~/database/schemas'
|
} from '~/database/schemas'
|
||||||
import type { Swiper } from 'swiper/types'
|
import type { Swiper } from 'swiper/types'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
export type IWorkspace = SelectHaexWorkspaces
|
export type IWorkspace = SelectHaexWorkspaces
|
||||||
|
|
||||||
@ -203,12 +204,86 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
|||||||
isOverviewMode.value = false
|
isOverviewMode.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateWorkspaceBackgroundAsync = async (
|
||||||
|
workspaceId: string,
|
||||||
|
base64Image: string | null,
|
||||||
|
) => {
|
||||||
|
if (!currentVault.value?.drizzle) {
|
||||||
|
throw new Error('Kein Vault geöffnet')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await currentVault.value.drizzle
|
||||||
|
.update(haexWorkspaces)
|
||||||
|
.set({ background: base64Image })
|
||||||
|
.where(eq(haexWorkspaces.id, workspaceId))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
if (result.length > 0 && result[0]) {
|
||||||
|
const index = workspaces.value.findIndex((ws) => ws.id === workspaceId)
|
||||||
|
if (index !== -1) {
|
||||||
|
workspaces.value[index] = result[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Aktualisieren des Workspace-Hintergrunds:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getWorkspaceBackgroundStyle = (workspace: IWorkspace) => {
|
||||||
|
if (!workspace.background) return {}
|
||||||
|
|
||||||
|
// The background field contains the absolute file path
|
||||||
|
// Convert it to an asset URL
|
||||||
|
const assetUrl = convertFileSrc(workspace.background)
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundImage: `url(${assetUrl})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getWorkspaceContextMenuItems = (workspaceId: string) => {
|
||||||
|
const windowManager = useWindowManagerStore()
|
||||||
|
|
||||||
|
return [[
|
||||||
|
{
|
||||||
|
label: 'Hintergrund ändern',
|
||||||
|
icon: 'i-mdi-image',
|
||||||
|
onSelect: async () => {
|
||||||
|
// Store the workspace ID for settings to use
|
||||||
|
currentWorkspaceIndex.value = workspaces.value.findIndex(
|
||||||
|
(ws) => ws.id === workspaceId,
|
||||||
|
)
|
||||||
|
// Get settings window info
|
||||||
|
const settingsWindow = windowManager.getAllSystemWindows()
|
||||||
|
.find((win) => win.id === 'settings')
|
||||||
|
|
||||||
|
if (settingsWindow) {
|
||||||
|
await windowManager.openWindowAsync({
|
||||||
|
type: 'system',
|
||||||
|
sourceId: settingsWindow.id,
|
||||||
|
title: settingsWindow.name,
|
||||||
|
icon: settingsWindow.icon || undefined,
|
||||||
|
workspaceId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addWorkspaceAsync,
|
addWorkspaceAsync,
|
||||||
allowSwipe,
|
allowSwipe,
|
||||||
closeWorkspaceAsync,
|
closeWorkspaceAsync,
|
||||||
currentWorkspace,
|
currentWorkspace,
|
||||||
currentWorkspaceIndex,
|
currentWorkspaceIndex,
|
||||||
|
getWorkspaceBackgroundStyle,
|
||||||
|
getWorkspaceContextMenuItems,
|
||||||
isOverviewMode,
|
isOverviewMode,
|
||||||
slideToWorkspace,
|
slideToWorkspace,
|
||||||
loadWorkspacesAsync,
|
loadWorkspacesAsync,
|
||||||
@ -218,6 +293,7 @@ export const useWorkspaceStore = defineStore('workspaceStore', () => {
|
|||||||
switchToNext,
|
switchToNext,
|
||||||
switchToPrevious,
|
switchToPrevious,
|
||||||
switchToWorkspace,
|
switchToWorkspace,
|
||||||
|
updateWorkspaceBackgroundAsync,
|
||||||
workspaces,
|
workspaces,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user