mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 22:20:51 +01:00
- Add haexSyncBackends table with CRDT support for multi-backend sync - Implement useSyncBackendsStore for managing sync server configurations - Fix desktop icon grid snapping for all icon sizes (small to extra-large) - Add Supabase client dependency for future sync implementation - Generate database migration for sync_backends table
462 lines
13 KiB
TypeScript
462 lines
13 KiB
TypeScript
import { eq } from 'drizzle-orm'
|
|
import { haexDesktopItems, haexDevices } from '~/database/schemas'
|
|
import type {
|
|
InsertHaexDesktopItems,
|
|
SelectHaexDesktopItems,
|
|
} from '~/database/schemas'
|
|
import {
|
|
DesktopIconSizePreset,
|
|
iconSizePresetValues,
|
|
} from '~/stores/vault/settings'
|
|
import de from './de.json'
|
|
import en from './en.json'
|
|
|
|
export type DesktopItemType = 'extension' | 'file' | 'folder' | 'system'
|
|
|
|
export interface IDesktopItem extends SelectHaexDesktopItems {
|
|
label?: string
|
|
icon?: string
|
|
referenceId: string // Computed: extensionId or systemWindowId
|
|
}
|
|
|
|
export const useDesktopStore = defineStore('desktopStore', () => {
|
|
const { currentVault } = storeToRefs(useVaultStore())
|
|
const workspaceStore = useWorkspaceStore()
|
|
const { currentWorkspace } = storeToRefs(workspaceStore)
|
|
const { $i18n } = useNuxtApp()
|
|
const deviceStore = useDeviceStore()
|
|
const settingsStore = useVaultSettingsStore()
|
|
|
|
$i18n.setLocaleMessage('de', { desktop: de })
|
|
$i18n.setLocaleMessage('en', { desktop: en })
|
|
|
|
const desktopItems = ref<IDesktopItem[]>([])
|
|
const selectedItemIds = ref<Set<string>>(new Set())
|
|
|
|
// Desktop Grid Settings (stored in DB per device)
|
|
const iconSizePreset = ref<DesktopIconSizePreset>(
|
|
DesktopIconSizePreset.medium,
|
|
)
|
|
|
|
// Get device internal ID from DB
|
|
const getDeviceInternalIdAsync = async () => {
|
|
if (!deviceStore.deviceId || !currentVault.value?.drizzle) return undefined
|
|
|
|
const device = await currentVault.value.drizzle.query.haexDevices.findFirst(
|
|
{
|
|
where: eq(haexDevices.deviceId, deviceStore.deviceId),
|
|
},
|
|
)
|
|
|
|
return device?.id ? device.id : undefined
|
|
}
|
|
|
|
// Sync icon size from DB
|
|
const syncDesktopIconSizeAsync = async () => {
|
|
const deviceInternalId = await getDeviceInternalIdAsync()
|
|
if (!deviceInternalId) return
|
|
|
|
const preset =
|
|
await settingsStore.syncDesktopIconSizeAsync(deviceInternalId)
|
|
iconSizePreset.value = preset
|
|
}
|
|
|
|
// Update icon size in DB
|
|
const updateDesktopIconSizeAsync = async (preset: DesktopIconSizePreset) => {
|
|
const deviceInternalId = await getDeviceInternalIdAsync()
|
|
if (!deviceInternalId) return
|
|
|
|
await settingsStore.updateDesktopIconSizeAsync(deviceInternalId, preset)
|
|
iconSizePreset.value = preset
|
|
}
|
|
|
|
const effectiveIconSize = computed(() => {
|
|
return iconSizePresetValues[iconSizePreset.value]
|
|
})
|
|
|
|
const iconPadding = 30
|
|
|
|
// Calculate grid cell size based on icon size
|
|
const gridCellSize = computed(() => {
|
|
// Add padding around icon (30px extra for spacing)
|
|
return effectiveIconSize.value + iconPadding
|
|
})
|
|
|
|
// Snap position to grid (centers icon in cell)
|
|
// iconWidth and iconHeight are optional - if provided, they're used for centering
|
|
const snapToGrid = (
|
|
x: number,
|
|
y: number,
|
|
iconWidth?: number,
|
|
iconHeight?: number,
|
|
) => {
|
|
const cellSize = gridCellSize.value
|
|
const halfCell = cellSize / 2
|
|
|
|
// Use provided dimensions or fall back to the effective icon size (not cell size!)
|
|
const actualIconWidth = iconWidth || effectiveIconSize.value
|
|
const actualIconHeight = iconHeight || effectiveIconSize.value
|
|
|
|
// Calculate which grid cell the position falls into
|
|
// Add half the icon size to x/y to get the center point for snapping
|
|
const centerX = x + actualIconWidth / 2
|
|
const centerY = y + actualIconHeight / 2
|
|
|
|
// Find nearest grid cell center
|
|
// Grid cells are centered at: halfCell, halfCell + cellSize, halfCell + 2*cellSize, ...
|
|
// Which is: halfCell + (n * cellSize) for n = 0, 1, 2, ...
|
|
const col = Math.round((centerX - halfCell) / cellSize)
|
|
const row = Math.round((centerY - halfCell) / cellSize)
|
|
|
|
// Calculate the center of the target grid cell
|
|
const gridCenterX = halfCell + col * cellSize
|
|
const gridCenterY = halfCell + row * cellSize
|
|
|
|
// Calculate the top-left position that centers the icon in the cell
|
|
const snappedX = gridCenterX - actualIconWidth / 2
|
|
const snappedY = gridCenterY - actualIconHeight / 2
|
|
|
|
return {
|
|
x: snappedX,
|
|
y: snappedY,
|
|
}
|
|
}
|
|
|
|
const loadDesktopItemsAsync = async () => {
|
|
if (!currentVault.value?.drizzle) {
|
|
console.error('Kein Vault geöffnet')
|
|
return
|
|
}
|
|
|
|
if (!currentWorkspace.value) {
|
|
console.error('Kein Workspace aktiv')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const items = await currentVault.value.drizzle
|
|
.select()
|
|
.from(haexDesktopItems)
|
|
.where(eq(haexDesktopItems.workspaceId, currentWorkspace.value.id))
|
|
|
|
desktopItems.value = items.map((item) => ({
|
|
...item,
|
|
referenceId:
|
|
item.itemType === 'extension'
|
|
? item.extensionId!
|
|
: item.systemWindowId!,
|
|
}))
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Desktop-Items:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const addDesktopItemAsync = async (
|
|
itemType: DesktopItemType,
|
|
referenceId: string,
|
|
positionX: number = 0,
|
|
positionY: number = 0,
|
|
workspaceId?: string,
|
|
) => {
|
|
if (!currentVault.value?.drizzle) {
|
|
throw new Error('Kein Vault geöffnet')
|
|
}
|
|
|
|
const targetWorkspaceId = workspaceId || currentWorkspace.value?.id
|
|
if (!targetWorkspaceId) {
|
|
throw new Error('Kein Workspace aktiv')
|
|
}
|
|
|
|
try {
|
|
const newItem: InsertHaexDesktopItems = {
|
|
workspaceId: targetWorkspaceId,
|
|
itemType: itemType,
|
|
extensionId: itemType === 'extension' ? referenceId : null,
|
|
systemWindowId:
|
|
itemType === 'system' || itemType === 'file' || itemType === 'folder'
|
|
? referenceId
|
|
: null,
|
|
positionX: positionX,
|
|
positionY: positionY,
|
|
}
|
|
|
|
const result = await currentVault.value.drizzle
|
|
.insert(haexDesktopItems)
|
|
.values(newItem)
|
|
.returning()
|
|
|
|
if (result.length > 0 && result[0]) {
|
|
const itemWithRef = {
|
|
...result[0],
|
|
referenceId:
|
|
itemType === 'extension'
|
|
? result[0].extensionId!
|
|
: result[0].systemWindowId!,
|
|
}
|
|
desktopItems.value.push(itemWithRef)
|
|
return itemWithRef
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Hinzufügen des Desktop-Items:', {
|
|
error,
|
|
itemType,
|
|
referenceId,
|
|
workspaceId: targetWorkspaceId,
|
|
position: { x: positionX, y: positionY },
|
|
})
|
|
|
|
// Log full error details
|
|
if (error && typeof error === 'object') {
|
|
console.error('Full error object:', JSON.stringify(error, null, 2))
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const updateDesktopItemPositionAsync = async (
|
|
id: string,
|
|
positionX: number,
|
|
positionY: number,
|
|
) => {
|
|
if (!currentVault.value?.drizzle) {
|
|
throw new Error('Kein Vault geöffnet')
|
|
}
|
|
|
|
try {
|
|
const result = await currentVault.value.drizzle
|
|
.update(haexDesktopItems)
|
|
.set({
|
|
positionX: positionX,
|
|
positionY: positionY,
|
|
})
|
|
.where(eq(haexDesktopItems.id, id))
|
|
.returning()
|
|
|
|
if (result.length > 0 && result[0]) {
|
|
const index = desktopItems.value.findIndex((item) => item.id === id)
|
|
if (index !== -1) {
|
|
const item = result[0]
|
|
desktopItems.value[index] = {
|
|
...item,
|
|
referenceId:
|
|
item.itemType === 'extension'
|
|
? item.extensionId!
|
|
: item.systemWindowId!,
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Aktualisieren der Position:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const removeDesktopItemAsync = async (id: string) => {
|
|
console.log('removeDesktopItemAsync', id)
|
|
if (!currentVault.value?.drizzle) {
|
|
throw new Error('Kein Vault geöffnet')
|
|
}
|
|
|
|
try {
|
|
// Soft delete using haexTombstone
|
|
await currentVault.value.drizzle
|
|
.delete(haexDesktopItems)
|
|
.where(eq(haexDesktopItems.id, id))
|
|
|
|
desktopItems.value = desktopItems.value.filter((item) => item.id !== id)
|
|
} catch (error) {
|
|
console.error('Fehler beim Entfernen des Desktop-Items:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const getDesktopItemByReference = (
|
|
itemType: DesktopItemType,
|
|
referenceId: string,
|
|
) => {
|
|
return desktopItems.value.find((item) => {
|
|
if (item.itemType !== itemType) return false
|
|
if (itemType === 'extension') {
|
|
return item.extensionId === referenceId
|
|
} else {
|
|
return item.systemWindowId === referenceId
|
|
}
|
|
})
|
|
}
|
|
|
|
const openDesktopItem = (
|
|
itemType: DesktopItemType,
|
|
referenceId: string,
|
|
sourcePosition?: { x: number; y: number; width: number; height: number },
|
|
) => {
|
|
const windowManager = useWindowManagerStore()
|
|
|
|
if (itemType === 'system') {
|
|
const systemWindow = windowManager
|
|
.getAllSystemWindows()
|
|
.find((win) => win.id === referenceId)
|
|
|
|
if (systemWindow) {
|
|
windowManager.openWindowAsync({
|
|
sourceId: systemWindow.id,
|
|
type: 'system',
|
|
icon: systemWindow.icon,
|
|
title: systemWindow.name,
|
|
sourcePosition,
|
|
})
|
|
}
|
|
} else if (itemType === 'extension') {
|
|
const extensionsStore = useExtensionsStore()
|
|
|
|
const extension = extensionsStore.availableExtensions.find(
|
|
(ext) => ext.id === referenceId,
|
|
)
|
|
|
|
if (extension) {
|
|
windowManager.openWindowAsync({
|
|
sourceId: extension.id,
|
|
type: 'extension',
|
|
icon: extension.icon,
|
|
title: extension.name,
|
|
sourcePosition,
|
|
})
|
|
}
|
|
}
|
|
// Für später: file und folder handling
|
|
}
|
|
|
|
const uninstallDesktopItem = async (
|
|
id: string,
|
|
itemType: DesktopItemType,
|
|
referenceId: string,
|
|
) => {
|
|
if (itemType === 'extension') {
|
|
try {
|
|
const extensionsStore = useExtensionsStore()
|
|
const extension = extensionsStore.availableExtensions.find(
|
|
(ext) => ext.id === referenceId,
|
|
)
|
|
if (!extension) {
|
|
console.error('Extension nicht gefunden')
|
|
return
|
|
}
|
|
|
|
// Uninstall the extension
|
|
await extensionsStore.removeExtensionAsync(
|
|
extension.publicKey,
|
|
extension.name,
|
|
extension.version,
|
|
)
|
|
|
|
// Reload extensions after uninstall
|
|
await extensionsStore.loadExtensionsAsync()
|
|
|
|
// Remove desktop item
|
|
await removeDesktopItemAsync(id)
|
|
} catch (error) {
|
|
console.error('Fehler beim Deinstallieren:', error)
|
|
}
|
|
}
|
|
// Für später: file und folder handling
|
|
}
|
|
|
|
const toggleSelection = (id: string, ctrlKey: boolean = false) => {
|
|
if (ctrlKey) {
|
|
// Mit Ctrl: Toggle einzelnes Element
|
|
if (selectedItemIds.value.has(id)) {
|
|
selectedItemIds.value.delete(id)
|
|
} else {
|
|
selectedItemIds.value.add(id)
|
|
}
|
|
} else {
|
|
// Ohne Ctrl: Nur dieses Element auswählen
|
|
selectedItemIds.value.clear()
|
|
selectedItemIds.value.add(id)
|
|
}
|
|
}
|
|
|
|
const clearSelection = () => {
|
|
selectedItemIds.value.clear()
|
|
}
|
|
|
|
const isItemSelected = (id: string) => {
|
|
return selectedItemIds.value.has(id)
|
|
}
|
|
|
|
const selectedItems = computed(() => {
|
|
return desktopItems.value.filter((item) =>
|
|
selectedItemIds.value.has(item.id),
|
|
)
|
|
})
|
|
|
|
const getContextMenuItems = (
|
|
id: string,
|
|
itemType: DesktopItemType,
|
|
referenceId: string,
|
|
onUninstall: () => void,
|
|
) => {
|
|
const handleOpen = () => {
|
|
openDesktopItem(itemType, referenceId)
|
|
}
|
|
|
|
// Build second menu group based on item type
|
|
const secondGroup = [
|
|
{
|
|
label: $i18n.t('desktop.contextMenu.removeFromDesktop'),
|
|
icon: 'i-heroicons-x-mark',
|
|
onSelect: async () => {
|
|
await removeDesktopItemAsync(id)
|
|
},
|
|
},
|
|
]
|
|
|
|
// Only show uninstall option for extensions
|
|
if (itemType === 'extension') {
|
|
secondGroup.push({
|
|
label: $i18n.t('desktop.contextMenu.uninstall'),
|
|
icon: 'i-heroicons-trash',
|
|
onSelect: async () => {
|
|
onUninstall()
|
|
},
|
|
})
|
|
}
|
|
|
|
return [
|
|
[
|
|
{
|
|
label: $i18n.t('desktop.contextMenu.open'),
|
|
icon: 'i-heroicons-arrow-top-right-on-square',
|
|
onSelect: handleOpen,
|
|
},
|
|
],
|
|
secondGroup,
|
|
]
|
|
}
|
|
|
|
return {
|
|
desktopItems,
|
|
selectedItemIds,
|
|
selectedItems,
|
|
loadDesktopItemsAsync,
|
|
addDesktopItemAsync,
|
|
updateDesktopItemPositionAsync,
|
|
removeDesktopItemAsync,
|
|
getDesktopItemByReference,
|
|
getContextMenuItems,
|
|
openDesktopItem,
|
|
uninstallDesktopItem,
|
|
toggleSelection,
|
|
clearSelection,
|
|
isItemSelected,
|
|
// Grid settings
|
|
iconSizePreset,
|
|
syncDesktopIconSizeAsync,
|
|
updateDesktopIconSizeAsync,
|
|
effectiveIconSize,
|
|
gridCellSize,
|
|
snapToGrid,
|
|
}
|
|
})
|