mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 22:40: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,8 +23,10 @@
|
|||||||
:key="workspace.id"
|
:key="workspace.id"
|
||||||
class="w-full h-full"
|
class="w-full h-full"
|
||||||
>
|
>
|
||||||
|
<UContextMenu :items="getWorkspaceContextMenuItems(workspace.id)">
|
||||||
<div
|
<div
|
||||||
class="w-full h-full relative"
|
class="w-full h-full relative"
|
||||||
|
:style="getWorkspaceBackgroundStyle(workspace)"
|
||||||
@click.self.stop="handleDesktopClick"
|
@click.self.stop="handleDesktopClick"
|
||||||
@mousedown.left.self="handleAreaSelectStart"
|
@mousedown.left.self="handleAreaSelectStart"
|
||||||
@dragover.prevent="handleDragOver"
|
@dragover.prevent="handleDragOver"
|
||||||
@ -44,12 +46,16 @@
|
|||||||
|
|
||||||
<div
|
<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="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'"
|
:class="
|
||||||
|
showLeftSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<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="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'"
|
:class="
|
||||||
|
showRightSnapZone ? 'w-1/2 bg-blue-500/20 border-2' : 'w-0'
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Area Selection Box -->
|
<!-- Area Selection Box -->
|
||||||
@ -208,6 +214,7 @@
|
|||||||
</HaexWindow>
|
</HaexWindow>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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