use window system

This commit is contained in:
2025-10-20 19:14:05 +02:00
parent a291619f63
commit 2b8f1781f3
51 changed files with 6687 additions and 2070 deletions

View File

@ -0,0 +1,13 @@
{
"contextMenu": {
"open": "Öffnen",
"removeFromDesktop": "Von Desktop entfernen",
"uninstall": "Deinstallieren"
},
"confirmUninstall": {
"title": "Erweiterung deinstallieren",
"message": "Möchten Sie die Erweiterung '{name}' wirklich deinstallieren? Diese Aktion kann nicht rückgängig gemacht werden.",
"confirm": "Deinstallieren",
"cancel": "Abbrechen"
}
}

View File

@ -0,0 +1,13 @@
{
"contextMenu": {
"open": "Open",
"removeFromDesktop": "Remove from Desktop",
"uninstall": "Uninstall"
},
"confirmUninstall": {
"title": "Uninstall Extension",
"message": "Do you really want to uninstall the extension '{name}'? This action cannot be undone.",
"confirm": "Uninstall",
"cancel": "Cancel"
}
}

295
src/stores/desktop/index.ts Normal file
View File

@ -0,0 +1,295 @@
import { eq } from 'drizzle-orm'
import { haexDesktopItems } from '~~/src-tauri/database/schemas'
import type {
InsertHaexDesktopItems,
SelectHaexDesktopItems,
} from '~~/src-tauri/database/schemas'
import de from './de.json'
import en from './en.json'
export type DesktopItemType = 'extension' | 'file' | 'folder'
export interface IDesktopItem extends SelectHaexDesktopItems {
label?: string
icon?: string
}
export const useDesktopStore = defineStore('desktopStore', () => {
const { currentVault } = storeToRefs(useVaultStore())
const workspaceStore = useWorkspaceStore()
const { currentWorkspace } = storeToRefs(workspaceStore)
const { $i18n } = useNuxtApp()
$i18n.setLocaleMessage('de', {
desktop: de,
})
$i18n.setLocaleMessage('en', { desktop: en })
const desktopItems = ref<IDesktopItem[]>([])
const selectedItemIds = ref<Set<string>>(new Set())
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
} 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,
) => {
if (!currentVault.value?.drizzle) {
throw new Error('Kein Vault geöffnet')
}
if (!currentWorkspace.value) {
throw new Error('Kein Workspace aktiv')
}
try {
const newItem: InsertHaexDesktopItems = {
workspaceId: currentWorkspace.value.id,
itemType: itemType,
referenceId: referenceId,
positionX: positionX,
positionY: positionY,
}
const result = await currentVault.value.drizzle
.insert(haexDesktopItems)
.values(newItem)
.returning()
if (result.length > 0 && result[0]) {
desktopItems.value.push(result[0])
return result[0]
}
} catch (error) {
console.error('Fehler beim Hinzufügen des Desktop-Items:', error)
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) {
desktopItems.value[index] = result[0]
}
}
} 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) => item.itemType === itemType && item.referenceId === referenceId,
)
}
const openDesktopItem = (
itemType: DesktopItemType,
referenceId: string,
sourcePosition?: { x: number; y: number; width: number; height: number },
) => {
if (itemType === 'extension') {
const windowManager = useWindowManagerStore()
const extensionsStore = useExtensionsStore()
const extension = extensionsStore.availableExtensions.find(
(ext) => ext.id === referenceId,
)
if (extension) {
windowManager.openWindow(
'extension',
extension.id,
extension.name,
extension.icon || undefined,
undefined, // Use default viewport-aware width
undefined, // Use default viewport-aware height
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)
}
return [
[
{
label: $i18n.t('desktop.contextMenu.open'),
icon: 'i-heroicons-arrow-top-right-on-square',
onSelect: handleOpen,
},
],
[
{
label: $i18n.t('desktop.contextMenu.removeFromDesktop'),
icon: 'i-heroicons-x-mark',
onSelect: async () => {
await removeDesktopItemAsync(id)
},
},
{
label: $i18n.t('desktop.contextMenu.uninstall'),
icon: 'i-heroicons-trash',
onSelect: onUninstall,
},
],
]
}
return {
desktopItems,
selectedItemIds,
selectedItems,
loadDesktopItemsAsync,
addDesktopItemAsync,
updateDesktopItemPositionAsync,
removeDesktopItemAsync,
getDesktopItemByReference,
getContextMenuItems,
openDesktopItem,
uninstallDesktopItem,
toggleSelection,
clearSelection,
isItemSelected,
}
})

View File

@ -0,0 +1,312 @@
import { defineAsyncComponent, type Component } from 'vue'
export interface IWindow {
id: string
workspaceId: string // Window belongs to a specific workspace
type: 'system' | 'extension'
sourceId: string // extensionId or systemWindowId (depends on type)
title: string
icon?: string
x: number
y: number
width: number
height: number
isMinimized: boolean
zIndex: number
// Animation source position (icon position)
sourceX?: number
sourceY?: number
sourceWidth?: number
sourceHeight?: number
// Animation state
isOpening?: boolean
isClosing?: boolean
}
export interface SystemWindowDefinition {
id: string
name: string
icon: string
component: Component
defaultWidth: number
defaultHeight: number
resizable?: boolean
singleton?: boolean // Nur eine Instanz erlaubt?
}
export const useWindowManagerStore = defineStore('windowManager', () => {
const workspaceStore = useWorkspaceStore()
const { currentWorkspace, workspaces } = storeToRefs(workspaceStore)
const windows = ref<IWindow[]>([])
const activeWindowId = ref<string | null>(null)
const nextZIndex = ref(100)
// System Windows Registry
const systemWindows: Record<string, SystemWindowDefinition> = {
settings: {
id: 'settings',
name: 'Settings',
icon: 'i-mdi-cog',
component: defineAsyncComponent(
() => import('@/components/haex/system/settings.vue'),
) as Component,
defaultWidth: 800,
defaultHeight: 600,
resizable: true,
singleton: true,
},
marketplace: {
id: 'marketplace',
name: 'Marketplace',
icon: 'i-mdi-store',
component: defineAsyncComponent(
() => import('@/components/haex/system/marketplace.vue'),
),
defaultWidth: 1000,
defaultHeight: 700,
resizable: true,
singleton: false,
},
}
const getSystemWindow = (id: string): SystemWindowDefinition | undefined => {
return systemWindows[id]
}
const getAllSystemWindows = (): SystemWindowDefinition[] => {
return Object.values(systemWindows)
}
// Window animation settings
const windowAnimationDuration = ref(600) // in milliseconds (matches Tailwind duration-600)
// Get windows for current workspace only
const currentWorkspaceWindows = computed(() => {
if (!currentWorkspace.value) return []
return windows.value.filter(
(w) => w.workspaceId === currentWorkspace.value?.id,
)
})
const windowsByWorkspaceId = (workspaceId: string) =>
computed(() =>
windows.value.filter((window) => window.workspaceId === workspaceId),
)
const moveWindowsToWorkspace = (
fromWorkspaceId: string,
toWorkspaceId: string,
) => {
const windowsFrom = windowsByWorkspaceId(fromWorkspaceId)
windowsFrom.value.forEach((window) => (window.workspaceId = toWorkspaceId))
}
const openWindow = (
type: 'system' | 'extension',
sourceId: string,
workspaceId: string,
title?: string,
icon?: string,
width?: number,
height?: number,
sourcePosition?: { x: number; y: number; width: number; height: number },
) => {
const workspace = workspaces.value.find((w) => w.id === workspaceId)
if (!workspace) {
console.error('Cannot open window: No active workspace')
return
}
// System Window specific handling
if (type === 'system') {
const systemWindowDef = getSystemWindow(sourceId)
if (!systemWindowDef) {
console.error(`System window '${sourceId}' not found in registry`)
return
}
// Singleton check: If already open, activate existing window
if (systemWindowDef.singleton) {
const existingWindow = windows.value.find(
(w) => w.type === 'system' && w.sourceId === sourceId,
)
if (existingWindow) {
activateWindow(existingWindow.id)
return existingWindow.id
}
}
// Use system window defaults
title = title ?? systemWindowDef.name
icon = icon ?? systemWindowDef.icon
width = width ?? systemWindowDef.defaultWidth
height = height ?? systemWindowDef.defaultHeight
}
// Create new window
const windowId = crypto.randomUUID()
// Calculate viewport-aware size
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
const isMobile = viewportWidth < 768 // Tailwind md breakpoint
// Default size based on viewport
const defaultWidth = isMobile ? Math.floor(viewportWidth * 0.95) : 800
const defaultHeight = isMobile ? Math.floor(viewportHeight * 0.85) : 600
const windowWidth = width ?? defaultWidth
const windowHeight = height ?? defaultHeight
// Calculate centered position with cascading offset (only count windows in current workspace)
const offset = currentWorkspaceWindows.value.length * 30
const centerX = Math.max(0, (viewportWidth - windowWidth) / 2)
const centerY = Math.max(0, (viewportHeight - windowHeight) / 2)
const x = Math.min(centerX + offset, viewportWidth - windowWidth)
const y = Math.min(centerY + offset, viewportHeight - windowHeight)
const newWindow: IWindow = {
id: windowId,
workspaceId: workspace.id,
type,
sourceId,
title: title!,
icon,
x,
y,
width: windowWidth,
height: windowHeight,
isMinimized: false,
zIndex: nextZIndex.value++,
sourceX: sourcePosition?.x,
sourceY: sourcePosition?.y,
sourceWidth: sourcePosition?.width,
sourceHeight: sourcePosition?.height,
isOpening: true,
isClosing: false,
}
windows.value.push(newWindow)
activeWindowId.value = windowId
// Remove opening flag after animation
setTimeout(() => {
const window = windows.value.find((w) => w.id === windowId)
if (window) {
window.isOpening = false
}
}, windowAnimationDuration.value)
return windowId
}
/*****************************************************************************************************
* TODO: Momentan werden die Fenster einfach nur geschlossen.
* In Zukunft sollte aber vorher ein close event an die Erweiterungen via postMessage geschickt werden,
* so dass die Erweiterungen darauf reagieren können, um eventuell ungespeicherte Daten zu sichern
*****************************************************************************************************/
const closeWindow = (windowId: string) => {
const window = windows.value.find((w) => w.id === windowId)
if (!window) return
// Start closing animation
window.isClosing = true
// Remove window after animation completes
setTimeout(() => {
const index = windows.value.findIndex((w) => w.id === windowId)
if (index !== -1) {
windows.value.splice(index, 1)
// If closed window was active, activate the topmost window
if (activeWindowId.value === windowId) {
if (windows.value.length > 0) {
const topWindow = windows.value.reduce((max, w) =>
w.zIndex > max.zIndex ? w : max,
)
activeWindowId.value = topWindow.id
} else {
activeWindowId.value = null
}
}
}
}, windowAnimationDuration.value)
}
const minimizeWindow = (windowId: string) => {
const window = windows.value.find((w) => w.id === windowId)
if (window) {
window.isMinimized = true
}
}
const restoreWindow = (windowId: string) => {
const window = windows.value.find((w) => w.id === windowId)
if (window) {
window.isMinimized = false
activateWindow(windowId)
}
}
const activateWindow = (windowId: string) => {
const window = windows.value.find((w) => w.id === windowId)
if (window) {
window.zIndex = nextZIndex.value++
activeWindowId.value = windowId
}
}
const updateWindowPosition = (windowId: string, x: number, y: number) => {
const window = windows.value.find((w) => w.id === windowId)
if (window) {
window.x = x
window.y = y
}
}
const updateWindowSize = (
windowId: string,
width: number,
height: number,
) => {
const window = windows.value.find((w) => w.id === windowId)
if (window) {
window.width = width
window.height = height
}
}
const isWindowActive = (windowId: string) => {
return activeWindowId.value === windowId
}
const getVisibleWindows = computed(() => {
return currentWorkspaceWindows.value.filter((w) => !w.isMinimized)
})
const getMinimizedWindows = computed(() => {
return currentWorkspaceWindows.value.filter((w) => w.isMinimized)
})
return {
activateWindow,
activeWindowId,
closeWindow,
currentWorkspaceWindows,
getAllSystemWindows,
getMinimizedWindows,
getSystemWindow,
getVisibleWindows,
isWindowActive,
minimizeWindow,
moveWindowsToWorkspace,
openWindow,
restoreWindow,
updateWindowPosition,
updateWindowSize,
windowAnimationDuration,
windows,
windowsByWorkspaceId,
}
})

View File

@ -0,0 +1,189 @@
import { asc, eq } from 'drizzle-orm'
import {
haexWorkspaces,
type InsertHaexWorkspaces,
type SelectHaexWorkspaces,
} from '~~/src-tauri/database/schemas'
import type { Swiper } from 'swiper/types'
export type IWorkspace = SelectHaexWorkspaces
export const useWorkspaceStore = defineStore('workspaceStore', () => {
const vaultStore = useVaultStore()
const windowStore = useWindowManagerStore()
const { currentVault } = storeToRefs(vaultStore)
const swiperInstance = ref<Swiper | null>(null)
const allowSwipe = ref(true)
// Workspace Overview Mode (GNOME-style)
const isOverviewMode = ref(false)
const workspaces = ref<IWorkspace[]>([])
const currentWorkspaceIndex = ref(0)
// Load workspaces from database
const loadWorkspacesAsync = async () => {
if (!currentVault.value?.drizzle) {
console.error('Kein Vault geöffnet')
return
}
try {
const items = await currentVault.value.drizzle
.select()
.from(haexWorkspaces)
.orderBy(asc(haexWorkspaces.position))
workspaces.value = items
// Create default workspace if none exist
if (items.length === 0) {
await addWorkspaceAsync('Workspace 1')
}
} catch (error) {
console.error('Fehler beim Laden der Workspaces:', error)
throw error
}
}
const currentWorkspace = computed(() => {
return workspaces.value[currentWorkspaceIndex.value]
})
const addWorkspaceAsync = async (name?: string) => {
if (!currentVault.value?.drizzle) {
throw new Error('Kein Vault geöffnet')
}
try {
const newIndex = workspaces.value.length + 1
const newWorkspace: InsertHaexWorkspaces = {
name: name || `Workspace ${newIndex}`,
position: workspaces.value.length,
}
const result = await currentVault.value.drizzle
.insert(haexWorkspaces)
.values(newWorkspace)
.returning()
if (result.length > 0 && result[0]) {
workspaces.value.push(result[0])
currentWorkspaceIndex.value = workspaces.value.length - 1
return result[0]
}
} catch (error) {
console.error('Fehler beim Hinzufügen des Workspace:', error)
throw error
}
}
const closeWorkspaceAsync = async (workspaceId: string) => {
const openWindows = windowStore.windowsByWorkspaceId(workspaceId)
for (const window of openWindows.value) {
windowStore.closeWindow(window.id)
}
return await removeWorkspaceAsync(workspaceId)
}
const removeWorkspaceAsync = async (workspaceId: string) => {
// Don't allow removing the last workspace
if (workspaces.value.length <= 1) return
if (!currentVault.value?.drizzle) {
throw new Error('Kein Vault geöffnet')
}
const index = workspaces.value.findIndex((ws) => ws.id === workspaceId)
if (index === -1) return
try {
await currentVault.value.drizzle
.delete(haexWorkspaces)
.where(eq(haexWorkspaces.id, workspaceId))
workspaces.value.splice(index, 1)
// Adjust current index if needed
if (currentWorkspaceIndex.value >= workspaces.value.length) {
currentWorkspaceIndex.value = workspaces.value.length - 1
}
} catch (error) {
console.error('Fehler beim Entfernen des Workspace:', error)
throw error
}
}
const switchToWorkspace = (index: number) => {
if (index >= 0 && index < workspaces.value.length) {
currentWorkspaceIndex.value = index
}
}
const switchToNext = () => {
if (currentWorkspaceIndex.value < workspaces.value.length - 1) {
currentWorkspaceIndex.value++
}
}
const switchToPrevious = () => {
if (currentWorkspaceIndex.value > 0) {
currentWorkspaceIndex.value--
}
}
const renameWorkspaceAsync = async (workspaceId: string, newName: string) => {
if (!currentVault.value?.drizzle) {
throw new Error('Kein Vault geöffnet')
}
try {
const result = await currentVault.value.drizzle
.update(haexWorkspaces)
.set({ name: newName })
.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 Umbenennen des Workspace:', error)
throw error
}
}
const slideToWorkspace = (index: number) => {
if (swiperInstance.value) {
swiperInstance.value.slideTo(index)
}
isOverviewMode.value = false
}
return {
addWorkspaceAsync,
allowSwipe,
closeWorkspaceAsync,
currentWorkspace,
currentWorkspaceIndex,
isOverviewMode,
slideToWorkspace,
loadWorkspacesAsync,
removeWorkspaceAsync,
renameWorkspaceAsync,
swiperInstance,
switchToNext,
switchToPrevious,
switchToWorkspace,
workspaces,
}
})