feature: window overview

This commit is contained in:
2025-10-24 13:17:29 +02:00
parent 4f839aa856
commit 252b8711de
13 changed files with 934 additions and 1208 deletions

View File

@ -24,7 +24,7 @@
"@nuxt/ui": "4.0.0",
"@nuxtjs/i18n": "10.0.6",
"@pinia/nuxt": "^0.11.2",
"@tailwindcss/vite": "^4.1.15",
"@tailwindcss/vite": "^4.1.16",
"@tauri-apps/api": "^2.9.0",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2",
@ -38,23 +38,24 @@
"@vueuse/core": "^13.9.0",
"@vueuse/gesture": "^2.0.0",
"@vueuse/nuxt": "^13.9.0",
"drizzle-orm": "^0.44.6",
"drizzle-orm": "^0.44.7",
"eslint": "^9.38.0",
"fuse.js": "^7.1.0",
"nuxt": "^4.1.3",
"nuxt-zod-i18n": "^1.12.1",
"swiper": "^12.0.3",
"tailwindcss": "^4.1.15",
"tailwindcss": "^4.1.16",
"vue": "^3.5.22",
"vue-router": "^4.6.3",
"zod": "^3.25.76"
},
"devDependencies": {
"@iconify-json/hugeicons": "^1.2.17",
"@iconify/json": "^2.2.398",
"@iconify-json/lucide": "^1.2.70",
"@iconify/json": "^2.2.399",
"@iconify/tailwind4": "^1.0.6",
"@libsql/client": "^0.15.15",
"@tauri-apps/cli": "^2.9.0",
"@tauri-apps/cli": "^2.9.1",
"@types/node": "^24.9.1",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "^3.5.22",

737
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -437,6 +437,17 @@ impl ExtensionManager {
&extracted.manifest.version,
)?;
// If extension version already exists, remove it completely before installing
if extensions_dir.exists() {
eprintln!(
"Extension version already exists at {}, removing old version",
extensions_dir.display()
);
std::fs::remove_dir_all(&extensions_dir).map_err(|e| {
ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e)
})?;
}
std::fs::create_dir_all(&extensions_dir).map_err(|e| {
ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e)
})?;

View File

@ -10,7 +10,6 @@
:initial-slide="currentWorkspaceIndex"
:speed="300"
:touch-angle="45"
:threshold="10"
:no-swiping="true"
no-swiping-class="no-swipe"
:allow-touch-move="allowSwipe"
@ -79,37 +78,95 @@
<!-- Windows for this workspace -->
<template
v-for="(window, index) in getWorkspaceWindows(workspace.id)"
v-for="window in getWorkspaceWindows(workspace.id)"
:key="window.id"
>
<!-- Wrapper for Overview Mode Click/Drag -->
<!-- Desktop container for when overview is closed -->
<div
v-if="false"
:style="
getOverviewWindowGridStyle(
index,
getWorkspaceWindows(workspace.id).length,
)
"
class="absolute cursor-pointer group"
:draggable="true"
@dragstart="handleOverviewWindowDragStart($event, window.id)"
@dragend="handleOverviewWindowDragEnd"
@click="handleOverviewWindowClick(window.id)"
>
<!-- Overlay for click/drag events (prevents interaction with window content) -->
<div
class="absolute inset-0 z-[100] bg-transparent group-hover:ring-4 group-hover:ring-purple-500 rounded-xl transition-all"
/>
:id="`desktop-container-${window.id}`"
class="absolute"
/>
<!-- Window with dynamic teleport -->
<Teleport
:to="
windowManager.showWindowOverview
? `#window-preview-${window.id}`
: `#desktop-container-${window.id}`
"
>
<template
v-if="
windowManager.showWindowOverview &&
overviewWindowState[window.id]
"
>
<div
class="absolute origin-top-left"
:style="{
transform: `scale(${overviewWindowState[window.id]!.scale})`,
width: `${overviewWindowState[window.id]!.width}px`,
height: `${overviewWindowState[window.id]!.height}px`,
}"
>
<HaexWindow
v-show="
windowManager.showWindowOverview || !window.isMinimized
"
:id="window.id"
:title="window.title"
:icon="window.icon"
v-model:x="overviewWindowState[window.id]!.x"
v-model:y="overviewWindowState[window.id]!.y"
v-model:width="overviewWindowState[window.id]!.width"
v-model:height="overviewWindowState[window.id]!.height"
: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"
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>
</template>
<HaexWindow
v-else
v-show="windowManager.showWindowOverview || !window.isMinimized"
:id="window.id"
:title="window.title"
:icon="window.icon"
:initial-x="window.x"
:initial-y="window.y"
:initial-width="window.width"
:initial-height="window.height"
v-model:x="window.x"
v-model:y="window.y"
v-model:width="window.width"
v-model:height="window.height"
:is-active="windowManager.isWindowActive(window.id)"
:source-x="window.sourceX"
:source-y="window.sourceY"
@ -117,7 +174,7 @@
:source-height="window.sourceHeight"
:is-opening="window.isOpening"
:is-closing="window.isClosing"
class="no-swipe pointer-events-none"
class="no-swipe"
@close="windowManager.closeWindow(window.id)"
@minimize="windowManager.minimizeWindow(window.id)"
@activate="windowManager.activateWindow(window.id)"
@ -131,7 +188,6 @@
@drag-start="handleWindowDragStart(window.id)"
@drag-end="handleWindowDragEnd"
>
{{ window }}
<!-- System Window: Render Vue Component -->
<component
:is="getSystemWindowComponent(window.sourceId)"
@ -145,56 +201,15 @@
:window-id="window.id"
/>
</HaexWindow>
</div>
<!-- Normal Mode (non-overview) -->
<HaexWindow
:id="window.id"
:title="window.title"
:icon="window.icon"
:initial-x="window.x"
:initial-y="window.y"
:initial-width="window.width"
:initial-height="window.height"
: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"
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>
</Teleport>
</template>
</div>
</SwiperSlide>
</Swiper>
<!-- Window Overview Modal -->
<HaexWindowOverview />
<!-- Workspace Drawer -->
<UDrawer
v-model:open="isOverviewMode"
@ -202,8 +217,6 @@
:dismissible="false"
:overlay="false"
:modal="false"
should-scale-background
set-background-color-on-scale
title="Workspaces"
description="Workspaces"
>
@ -252,17 +265,12 @@ import type { Swiper as SwiperType } from 'swiper'
import 'swiper/css'
import 'swiper/css/navigation'
import { eq } from 'drizzle-orm'
import { haexDesktopItems } from '~~/src-tauri/database/schemas'
const SwiperNavigation = Navigation
const desktopStore = useDesktopStore()
const extensionsStore = useExtensionsStore()
const windowManager = useWindowManagerStore()
const workspaceStore = useWorkspaceStore()
const { currentVault } = storeToRefs(useVaultStore())
const { desktopItems } = storeToRefs(desktopStore)
const { availableExtensions } = storeToRefs(extensionsStore)
const {
@ -315,7 +323,6 @@ const currentDraggedReferenceId = ref<string>()
// Window drag state for snap zones
const isWindowDragging = ref(false)
const currentDraggingWindowId = ref<string | null>(null)
const snapEdgeThreshold = 50 // pixels from edge to show snap zone
// Computed visibility for snap zones (uses mouseX from above)
@ -329,27 +336,6 @@ const showRightSnapZone = computed(() => {
return mouseX.value >= viewportWidth - snapEdgeThreshold
})
// Dropzone refs
/* const removeDropzoneEl = ref<HTMLElement>()
const uninstallDropzoneEl = ref<HTMLElement>() */
// Setup dropzones with VueUse
/* const { isOverDropZone: isOverRemoveZone } = useDropZone(removeDropzoneEl, {
onDrop: () => {
if (currentDraggedItemId.value) {
handleRemoveFromDesktop(currentDraggedItemId.value)
}
},
}) */
/* const { isOverDropZone: isOverUninstallZone } = useDropZone(uninstallDropzoneEl, {
onDrop: () => {
if (currentDraggedItemType.value && currentDraggedReferenceId.value) {
handleUninstall(currentDraggedItemType.value, currentDraggedReferenceId.value)
}
},
}) */
// Get icons for a specific workspace
const getWorkspaceIcons = (workspaceId: string) => {
return desktopItems.value
@ -393,11 +379,9 @@ const getWorkspaceIcons = (workspaceId: string) => {
})
}
// Get windows for a specific workspace
// Get windows for a specific workspace (including minimized for teleport)
const getWorkspaceWindows = (workspaceId: string) => {
return windowManager.windows.filter(
(w) => w.workspaceId === workspaceId && !w.isMinimized,
)
return windowManager.windows.filter((w) => w.workspaceId === workspaceId)
}
// Get Vue Component for system window
@ -431,29 +415,6 @@ const handleDragEnd = async () => {
allowSwipe.value = true // Re-enable Swiper after drag
}
// Move desktop item to different workspace
const moveItemToWorkspace = async (
itemId: string,
targetWorkspaceId: string,
) => {
const item = desktopItems.value.find((i) => i.id === itemId)
if (!item) return
try {
if (!currentVault.value?.drizzle) return
await currentVault.value.drizzle
.update(haexDesktopItems)
.set({ workspaceId: targetWorkspaceId })
.where(eq(haexDesktopItems.id, itemId))
// Update local state
item.workspaceId = targetWorkspaceId
} catch (error) {
console.error('Fehler beim Verschieben des Items:', error)
}
}
const handleDesktopClick = () => {
// Only clear selection if it was a simple click, not an area selection
// Check if we just finished an area selection (box size > threshold)
@ -470,30 +431,23 @@ const handleDesktopClick = () => {
}
const handleWindowDragStart = (windowId: string) => {
console.log('[Desktop] handleWindowDragStart:', windowId)
isWindowDragging.value = true
currentDraggingWindowId.value = windowId
windowManager.draggingWindowId = windowId // Set in store for workspace cards
console.log(
'[Desktop] draggingWindowId set to:',
windowManager.draggingWindowId,
)
allowSwipe.value = false // Disable Swiper during window drag
}
const handleWindowDragEnd = async () => {
// Window handles snapping itself, we just need to cleanup state
console.log('[Desktop] handleWindowDragEnd')
isWindowDragging.value = false
currentDraggingWindowId.value = null
windowManager.draggingWindowId = null // Clear from store
allowSwipe.value = true // Re-enable Swiper after drag
}
// Move window to different workspace
const moveWindowToWorkspace = async (
windowId: string,
targetWorkspaceId: string,
) => {
const window = windowManager.windows.find((w) => w.id === windowId)
if (!window) return
// Update window's workspaceId
window.workspaceId = targetWorkspaceId
}
// Area selection handlers
const handleAreaSelectStart = (e: MouseEvent) => {
if (!desktopEl.value) return
@ -579,13 +533,7 @@ const handleAddWorkspace = async () => {
})
}
const handleSwitchToWorkspace = (index: number) => {
if (swiperInstance.value) {
swiperInstance.value.slideTo(index)
}
}
const handleRemoveWorkspace = async () => {
/* const handleRemoveWorkspace = async () => {
if (!currentWorkspace.value || workspaces.value.length <= 1) return
const currentIndex = currentWorkspaceIndex.value
@ -600,13 +548,6 @@ const handleRemoveWorkspace = async () => {
})
}
// Drawer handlers
const handleSwitchToWorkspaceFromDrawer = (index: number) => {
handleSwitchToWorkspace(index)
// Close drawer after switch
isOverviewMode.value = false
}
const handleDropWindowOnWorkspace = async (
event: DragEvent,
targetWorkspaceId: string,
@ -616,116 +557,58 @@ const handleDropWindowOnWorkspace = async (
if (windowId) {
await moveWindowToWorkspace(windowId, targetWorkspaceId)
}
}
} */
// Overview Mode: Calculate grid positions and scale for windows
const getOverviewWindowGridStyle = (index: number, totalWindows: number) => {
if (!viewportWidth.value || !viewportHeight.value) {
return {}
}
// Calculate preview dimensions for window overview
const MIN_PREVIEW_WIDTH = 300 // 50% increase from 200
const MAX_PREVIEW_WIDTH = 600 // 50% increase from 400
const MIN_PREVIEW_HEIGHT = 225 // 50% increase from 150
const MAX_PREVIEW_HEIGHT = 450 // 50% increase from 300
// Determine grid layout based on number of windows
let cols = 1
let rows = 1
// Store window state for overview (position only, size stays original)
const overviewWindowState = reactive<
Record<
string,
{ x: number; y: number; width: number; height: number; scale: number }
>
>({})
if (totalWindows === 1) {
cols = 1
rows = 1
} else if (totalWindows === 2) {
cols = 2
rows = 1
} else if (totalWindows <= 4) {
cols = 2
rows = 2
} else if (totalWindows <= 6) {
cols = 3
rows = 2
} else if (totalWindows <= 9) {
cols = 3
rows = 3
} else {
cols = 4
rows = Math.ceil(totalWindows / 4)
}
// Calculate scale and card dimensions for each window
watch(
() => windowManager.showWindowOverview,
(isOpen) => {
if (isOpen) {
// Calculate scale for each window
windowManager.windows.forEach((window) => {
const scaleX = MAX_PREVIEW_WIDTH / window.width
const scaleY = MAX_PREVIEW_HEIGHT / window.height
const scale = Math.min(scaleX, scaleY, 1) // Never scale up, only down
// Calculate grid cell position
const col = index % cols
const row = Math.floor(index / cols)
// Ensure minimum card size
const scaledWidth = window.width * scale
const scaledHeight = window.height * scale
// Padding and gap
const padding = 40 // px from viewport edges
const gap = 30 // px between windows
let finalScale = scale
if (scaledWidth < MIN_PREVIEW_WIDTH) {
finalScale = MIN_PREVIEW_WIDTH / window.width
}
if (scaledHeight < MIN_PREVIEW_HEIGHT) {
finalScale = Math.max(finalScale, MIN_PREVIEW_HEIGHT / window.height)
}
// Available space
const availableWidth = viewportWidth.value - padding * 2 - gap * (cols - 1)
const availableHeight = viewportHeight.value - padding * 2 - gap * (rows - 1)
// Cell dimensions
const cellWidth = availableWidth / cols
const cellHeight = availableHeight / rows
// Window aspect ratio (assume 16:9 or use actual window dimensions)
const windowAspectRatio = 16 / 9
// Calculate scale to fit window in cell
const targetWidth = cellWidth
const targetHeight = cellHeight
const targetAspect = targetWidth / targetHeight
let scale = 0.25 // Default scale
let scaledWidth = 800 * scale
let scaledHeight = 600 * scale
if (targetAspect > windowAspectRatio) {
// Cell is wider than window aspect ratio - fit by height
scaledHeight = Math.min(targetHeight, 600 * 0.4)
scale = scaledHeight / 600
scaledWidth = 800 * scale
} else {
// Cell is taller than window aspect ratio - fit by width
scaledWidth = Math.min(targetWidth, 800 * 0.4)
scale = scaledWidth / 800
scaledHeight = 600 * scale
}
// Calculate position to center window in cell
const cellX = padding + col * (cellWidth + gap)
const cellY = padding + row * (cellHeight + gap)
// Center window in cell
const x = cellX + (cellWidth - scaledWidth) / 2
const y = cellY + (cellHeight - scaledHeight) / 2
return {
transform: `scale(${scale})`,
transformOrigin: 'top left',
left: `${x / scale}px`,
top: `${y / scale}px`,
width: '800px',
height: '600px',
zIndex: 91,
transition: 'all 0.3s ease',
}
}
// Overview Mode handlers
const handleOverviewWindowClick = (windowId: string) => {
// Activate the window
windowManager.activateWindow(windowId)
// Close overview mode
isOverviewMode.value = false
}
const handleOverviewWindowDragStart = (event: DragEvent, windowId: string) => {
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('windowId', windowId)
}
}
const handleOverviewWindowDragEnd = () => {
// Cleanup after drag
}
overviewWindowState[window.id] = {
x: 0,
y: 0,
width: window.width, // Keep original width
height: window.height, // Keep original height
scale: finalScale, // Store the scale factor
}
})
}
},
{ immediate: true },
)
// Disable Swiper in overview mode
watch(isOverviewMode, (newValue) => {

View File

@ -1,480 +0,0 @@
<template>
<div
ref="windowEl"
:style="windowStyle"
:class="[
'absolute backdrop-blur-xl rounded-xl shadow-2xl overflow-hidden',
'border border-gray-200 dark:border-gray-700 transition-all ease-out duration-600',
'flex flex-col',
isActive ? 'z-50' : 'z-10',
]"
@mousedown="handleActivate"
>
<!-- Window Titlebar -->
<div
ref="titlebarEl"
class="grid grid-cols-3 items-center px-3 py-1 bg-white/80 dark:bg-gray-800/80 border-b border-gray-200/50 dark:border-gray-700/50 cursor-move select-none touch-none"
@dblclick="handleMaximize"
>
<!-- Left: Icon -->
<div class="flex items-center gap-2">
<img
v-if="icon"
:src="icon"
:alt="title"
class="w-5 h-5 object-contain flex-shrink-0"
/>
</div>
<!-- Center: Title -->
<div class="flex items-center justify-center">
<span
class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate max-w-full"
>
{{ title }}
</span>
</div>
<!-- Right: Window Controls -->
<div class="flex items-center gap-1 justify-end">
<button
class="w-8 h-8 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center transition-colors"
@click.stop="handleMinimize"
>
<UIcon
name="i-heroicons-minus"
class="w-4 h-4 text-gray-600 dark:text-gray-400"
/>
</button>
<button
class="w-8 h-8 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 flex items-center justify-center transition-colors"
@click.stop="handleMaximize"
>
<UIcon
:name="
isMaximized
? 'i-heroicons-arrows-pointing-in'
: 'i-heroicons-arrows-pointing-out'
"
class="w-4 h-4 text-gray-600 dark:text-gray-400"
/>
</button>
<button
class="w-8 h-8 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 flex items-center justify-center transition-colors group"
@click.stop="handleClose"
>
<UIcon
name="i-heroicons-x-mark"
class="w-4 h-4 text-gray-600 dark:text-gray-400 group-hover:text-red-600 dark:group-hover:text-red-400"
/>
</button>
</div>
</div>
<!-- Window Content -->
<div
:class="[
'flex-1 overflow-auto relative ',
isDragging || isResizing ? 'pointer-events-none select-none' : '',
]"
>
<slot />
</div>
<!-- Resize Handles -->
<template v-if="!isMaximized">
<div
class="absolute top-0 left-0 w-2 h-2 cursor-nw-resize shrink-0"
@mousedown.left.stop="handleResizeStart('nw', $event)"
/>
<div
class="absolute top-0 right-0 w-2 h-2 cursor-ne-resize shrink-0"
@mousedown.left.stop="handleResizeStart('ne', $event)"
/>
<div
class="absolute bottom-0 left-0 w-2 h-2 cursor-sw-resize shrink-0"
@mousedown.left.stop="handleResizeStart('sw', $event)"
/>
<div
class="absolute bottom-0 right-0 w-2 h-2 cursor-se-resize shrink-0"
@mousedown.left.stop="handleResizeStart('se', $event)"
/>
<div
class="absolute top-0 left-2 right-2 h-2 cursor-n-resize shrink-0"
@mousedown.left.stop="handleResizeStart('n', $event)"
/>
<div
class="absolute bottom-0 left-2 right-2 h-2 cursor-s-resize shrink-0"
@mousedown.left.stop="handleResizeStart('s', $event)"
/>
<div
class="absolute left-0 top-2 bottom-2 w-2 cursor-w-resize bg-red-300 overflow-visible"
@mousedown.left.stop="handleResizeStart('w', $event)"
/>
<div
class="absolute right-0 top-2 bottom-2 w-2 cursor-e-resize shrink-0"
@mousedown.left.stop="handleResizeStart('e', $event)"
/>
</template>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
id: string
title: string
icon?: string | null
initialX?: number
initialY?: number
initialWidth?: number
initialHeight?: number
isActive?: boolean
sourceX?: number
sourceY?: number
sourceWidth?: number
sourceHeight?: number
isOpening?: boolean
isClosing?: boolean
}>()
const emit = defineEmits<{
close: []
minimize: []
activate: []
positionChanged: [x: number, y: number]
sizeChanged: [width: number, height: number]
dragStart: []
dragEnd: []
}>()
const windowEl = ref<HTMLElement>()
const titlebarEl = useTemplateRef('titlebarEl')
// Inject viewport size from parent desktop
const viewportSize = inject<{
width: Ref<number>
height: Ref<number>
}>('viewportSize')
// Window state
const x = ref(props.initialX ?? 100)
const y = ref(props.initialY ?? 100)
const width = ref(props.initialWidth ?? 800)
const height = ref(props.initialHeight ?? 600)
const isMaximized = ref(false) // Don't start maximized
// Store initial position/size for restore
const preMaximizeState = ref({
x: props.initialX ?? 100,
y: props.initialY ?? 100,
width: props.initialWidth ?? 800,
height: props.initialHeight ?? 600,
})
// Dragging state
const isDragging = ref(false)
const dragStartX = ref(0)
const dragStartY = ref(0)
// Resizing state
const isResizing = ref(false)
const resizeDirection = ref<string>('')
const resizeStartX = ref(0)
const resizeStartY = ref(0)
const resizeStartWidth = ref(0)
const resizeStartHeight = ref(0)
const resizeStartPosX = ref(0)
const resizeStartPosY = ref(0)
// Snap settings
const snapEdgeThreshold = 50 // pixels from edge to trigger snap
const { x: mouseX } = useMouse()
// Setup drag with useDrag composable (supports mouse + touch)
useDrag(
({ movement: [mx, my], first, last }) => {
if (isMaximized.value) return
if (first) {
// Drag started - save initial position
isDragging.value = true
dragStartX.value = x.value
dragStartY.value = y.value
emit('dragStart')
return // Don't update position on first event
}
if (last) {
// Drag ended - apply snapping
isDragging.value = false
const viewportBounds = getViewportBounds()
if (viewportBounds) {
const viewportWidth = viewportBounds.width
const viewportHeight = viewportBounds.height
if (mouseX.value <= snapEdgeThreshold) {
// Snap to left half
x.value = 0
y.value = 0
width.value = viewportWidth / 2
height.value = viewportHeight
isMaximized.value = false
} else if (mouseX.value >= viewportWidth - snapEdgeThreshold) {
// Snap to right half
x.value = viewportWidth / 2
y.value = 0
width.value = viewportWidth / 2
height.value = viewportHeight
isMaximized.value = false
} else {
// Normal snap back to viewport
snapToViewport()
}
}
emit('positionChanged', x.value, y.value)
emit('sizeChanged', width.value, height.value)
emit('dragEnd')
return
}
// Dragging (not first, not last)
const newX = dragStartX.value + mx
const newY = dragStartY.value + my
// Apply constraints during drag
const constrained = constrainToViewportDuringDrag(newX, newY)
x.value = constrained.x
y.value = constrained.y
},
{
domTarget: titlebarEl,
eventOptions: { passive: false },
pointer: { touch: true },
drag: {
threshold: 10, // 10px threshold prevents accidental drags and improves performance
filterTaps: true, // Filter out taps (clicks) vs drags
delay: 0, // No delay for immediate response
},
},
)
const windowStyle = computed(() => {
const baseStyle: Record<string, string> = {}
// Opening animation: start from icon position
if (
props.isOpening &&
props.sourceX !== undefined &&
props.sourceY !== undefined
) {
baseStyle.left = `${props.sourceX}px`
baseStyle.top = `${props.sourceY}px`
baseStyle.width = `${props.sourceWidth || 100}px`
baseStyle.height = `${props.sourceHeight || 100}px`
baseStyle.opacity = '0'
baseStyle.transform = 'scale(0.3)'
}
// Closing animation: shrink to icon position
else if (
props.isClosing &&
props.sourceX !== undefined &&
props.sourceY !== undefined
) {
baseStyle.left = `${props.sourceX}px`
baseStyle.top = `${props.sourceY}px`
baseStyle.width = `${props.sourceWidth || 100}px`
baseStyle.height = `${props.sourceHeight || 100}px`
baseStyle.opacity = '0'
baseStyle.transform = 'scale(0.3)'
}
// Normal state
else if (isMaximized.value) {
baseStyle.left = '0px'
baseStyle.top = '0px'
baseStyle.width = '100%'
baseStyle.height = '100%'
baseStyle.borderRadius = '0'
baseStyle.opacity = '1'
baseStyle.transform = 'scale(1)'
} else {
baseStyle.left = `${x.value}px`
baseStyle.top = `${y.value}px`
baseStyle.width = `${width.value}px`
baseStyle.height = `${height.value}px`
baseStyle.opacity = '1'
baseStyle.transform = 'scale(1)'
}
// Performance optimization: hint browser about transforms
if (isDragging.value || isResizing.value) {
baseStyle.willChange = 'transform, width, height'
}
return baseStyle
})
const getViewportBounds = () => {
// Use reactive viewport size from parent if available
if (viewportSize) {
return {
width: viewportSize.width.value,
height: viewportSize.height.value,
}
}
// Fallback to parent element measurement
if (!windowEl.value?.parentElement) return null
const parent = windowEl.value.parentElement
return {
width: parent.clientWidth,
height: parent.clientHeight,
}
}
const constrainToViewportDuringDrag = (newX: number, newY: number) => {
const bounds = getViewportBounds()
if (!bounds) return { x: newX, y: newY }
const windowWidth = width.value
const windowHeight = height.value
// Allow max 1/3 of window to go outside viewport during drag
const maxOffscreenX = windowWidth / 3
const maxOffscreenY = windowHeight / 3
const maxX = bounds.width - windowWidth + maxOffscreenX
const minX = -maxOffscreenX
const maxY = bounds.height - windowHeight + maxOffscreenY
const minY = -maxOffscreenY
const constrainedX = Math.max(minX, Math.min(maxX, newX))
const constrainedY = Math.max(minY, Math.min(maxY, newY))
return { x: constrainedX, y: constrainedY }
}
const constrainToViewportFully = (
newX: number,
newY: number,
newWidth?: number,
newHeight?: number,
) => {
const bounds = getViewportBounds()
if (!bounds) return { x: newX, y: newY }
const windowWidth = newWidth ?? width.value
const windowHeight = newHeight ?? height.value
// Keep entire window within viewport
const maxX = bounds.width - windowWidth
const minX = 0
const maxY = bounds.height - windowHeight
const minY = 0
const constrainedX = Math.max(minX, Math.min(maxX, newX))
const constrainedY = Math.max(minY, Math.min(maxY, newY))
return { x: constrainedX, y: constrainedY }
}
const snapToViewport = () => {
const bounds = getViewportBounds()
if (!bounds) return
const constrained = constrainToViewportFully(x.value, y.value)
x.value = constrained.x
y.value = constrained.y
}
const handleActivate = () => {
emit('activate')
}
const handleClose = () => {
emit('close')
}
const handleMinimize = () => {
emit('minimize')
}
const handleMaximize = () => {
if (isMaximized.value) {
// Restore
x.value = preMaximizeState.value.x
y.value = preMaximizeState.value.y
width.value = preMaximizeState.value.width
height.value = preMaximizeState.value.height
isMaximized.value = false
} else {
// Maximize
preMaximizeState.value = {
x: x.value,
y: y.value,
width: width.value,
height: height.value,
}
isMaximized.value = true
}
}
// Window resizing
const handleResizeStart = (direction: string, e: MouseEvent) => {
isResizing.value = true
resizeDirection.value = direction
resizeStartX.value = e.clientX
resizeStartY.value = e.clientY
resizeStartWidth.value = width.value
resizeStartHeight.value = height.value
resizeStartPosX.value = x.value
resizeStartPosY.value = y.value
}
// Global mouse move handler (for resizing only, dragging handled by useDrag)
useEventListener(window, 'mousemove', (e: MouseEvent) => {
if (isResizing.value) {
const deltaX = e.clientX - resizeStartX.value
const deltaY = e.clientY - resizeStartY.value
const dir = resizeDirection.value
// Handle width changes
if (dir.includes('e')) {
width.value = Math.max(300, resizeStartWidth.value + deltaX)
} else if (dir.includes('w')) {
const newWidth = Math.max(300, resizeStartWidth.value - deltaX)
const widthDiff = resizeStartWidth.value - newWidth
x.value = resizeStartPosX.value + widthDiff
width.value = newWidth
}
// Handle height changes
if (dir.includes('s')) {
height.value = Math.max(200, resizeStartHeight.value + deltaY)
} else if (dir.includes('n')) {
const newHeight = Math.max(200, resizeStartHeight.value - deltaY)
const heightDiff = resizeStartHeight.value - newHeight
y.value = resizeStartPosY.value + heightDiff
height.value = newHeight
}
}
})
// Global mouse up handler (for resizing only, dragging handled by useDrag)
useEventListener(window, 'mouseup', () => {
if (isResizing.value) {
globalThis.getSelection()?.removeAllRanges()
isResizing.value = false
// Snap back to viewport after resize ends
snapToViewport()
emit('positionChanged', x.value, y.value)
emit('sizeChanged', width.value, height.value)
}
})
</script>

View File

@ -155,8 +155,15 @@ const onOpenDatabase = async () => {
)
} catch (error) {
open.value = false
console.error('handleError', error, typeof error)
add({ color: 'error', description: `${error}` })
if (error?.details?.reason === 'file is not a database') {
add({
color: 'error',
title: t('error.password.title'),
description: t('error.password.description'),
})
} else {
add({ color: 'error', description: JSON.stringify(error) })
}
}
}
</script>
@ -170,7 +177,9 @@ de:
open: Vault öffnen
description: Öffne eine vorhandene Vault
error:
open: Vault konnte nicht geöffnet werden. \n Vermutlich ist das Passwort falsch
password:
title: Vault konnte nicht geöffnet werden
description: Bitte üperprüfe das Passwort
en:
open: Unlock
@ -180,5 +189,7 @@ en:
vault:
open: Open Vault
error:
open: Vault couldn't be opened. \n The password is probably wrong
password:
title: Vault couldn't be opened
description: Please check your password
</i18n>

View File

@ -7,7 +7,7 @@
'border border-gray-200 dark:border-gray-700 transition-all ease-out duration-600 ',
'flex flex-col @container',
{ 'select-none': isResizingOrDragging },
isActive ? 'z-50' : 'z-10',
isActive ? 'z-100' : 'z-50',
]"
@mousedown="handleActivate"
>
@ -95,10 +95,6 @@ const props = defineProps<{
id: string
title: string
icon?: string | null
initialX?: number
initialY?: number
initialWidth?: number
initialHeight?: number
isActive?: boolean
sourceX?: number
sourceY?: number
@ -118,6 +114,12 @@ const emit = defineEmits<{
dragEnd: []
}>()
// Use defineModel for x, y, width, height
const x = defineModel<number>('x', { default: 100 })
const y = defineModel<number>('y', { default: 100 })
const width = defineModel<number>('width', { default: 800 })
const height = defineModel<number>('height', { default: 600 })
const windowEl = useTemplateRef('windowEl')
const titlebarEl = useTemplateRef('titlebarEl')
@ -126,20 +128,14 @@ const viewportSize = inject<{
width: Ref<number>
height: Ref<number>
}>('viewportSize')
// Window state
const x = ref(props.initialX ?? 100)
const y = ref(props.initialY ?? 100)
const width = ref(props.initialWidth ?? 800)
const height = ref(props.initialHeight ?? 600)
const isMaximized = ref(false) // Don't start maximized
// Store initial position/size for restore
const preMaximizeState = ref({
x: props.initialX ?? 100,
y: props.initialY ?? 100,
width: props.initialWidth ?? 800,
height: props.initialHeight ?? 600,
x: x.value,
y: y.value,
width: width.value,
height: height.value,
})
// Dragging state
@ -161,10 +157,6 @@ const isResizingOrDragging = computed(
() => isResizing.value || isDragging.value,
)
// Snap settings
const snapEdgeThreshold = 50 // pixels from edge to trigger snap
const { x: mouseX } = useMouse()
// Setup drag with useDrag composable (supports mouse + touch)
useDrag(
({ movement: [mx, my], first, last }) => {
@ -180,34 +172,8 @@ useDrag(
}
if (last) {
// Drag ended - apply snapping
// Drag ended
isDragging.value = false
const viewportBounds = getViewportBounds()
if (viewportBounds) {
const viewportWidth = viewportBounds.width
const viewportHeight = viewportBounds.height
if (mouseX.value <= snapEdgeThreshold) {
// Snap to left half
x.value = 0
y.value = 0
width.value = viewportWidth / 2
height.value = viewportHeight
isMaximized.value = false
} else if (mouseX.value >= viewportWidth - snapEdgeThreshold) {
// Snap to right half
x.value = viewportWidth / 2
y.value = 0
width.value = viewportWidth / 2
height.value = viewportHeight
isMaximized.value = false
} else {
// Normal snap back to viewport
snapToViewport()
}
}
globalThis.getSelection()?.removeAllRanges()
emit('positionChanged', x.value, y.value)
emit('sizeChanged', width.value, height.value)
@ -229,7 +195,6 @@ useDrag(
eventOptions: { passive: false },
pointer: { touch: true },
drag: {
threshold: 10, // 10px threshold prevents accidental drags and improves performance
filterTaps: true, // Filter out taps (clicks) vs drags
delay: 0, // No delay for immediate response
},
@ -265,22 +230,18 @@ const windowStyle = computed(() => {
baseStyle.opacity = '0'
baseStyle.transform = 'scale(0.3)'
}
// Normal state
else if (isMaximized.value) {
baseStyle.left = '0px'
baseStyle.top = '0px'
baseStyle.width = '100%'
baseStyle.height = '100%'
baseStyle.borderRadius = '0'
baseStyle.opacity = '1'
//baseStyle.transform = 'scale(1)'
} else {
// Normal state (maximized windows now use actual pixel dimensions)
else {
baseStyle.left = `${x.value}px`
baseStyle.top = `${y.value}px`
baseStyle.width = `${width.value}px`
baseStyle.height = `${height.value}px`
baseStyle.opacity = '1'
//baseStyle.transform = 'scale(1)'
// Remove border-radius when maximized
if (isMaximized.value) {
baseStyle.borderRadius = '0'
}
}
// Performance optimization: hint browser about transforms
@ -318,38 +279,18 @@ const constrainToViewportDuringDrag = (newX: number, newY: number) => {
const windowWidth = width.value
const windowHeight = height.value
// Allow max 1/3 of window to go outside viewport during drag
// Allow sides and bottom to go out more
const maxOffscreenX = windowWidth / 3
const maxOffscreenY = windowHeight / 3
const maxOffscreenBottom = windowHeight / 3
// For X axis: allow 1/3 to go outside on both sides
const maxX = bounds.width - windowWidth + maxOffscreenX
const minX = -maxOffscreenX
const maxY = bounds.height - windowHeight + maxOffscreenY
const minY = -maxOffscreenY
const constrainedX = Math.max(minX, Math.min(maxX, newX))
const constrainedY = Math.max(minY, Math.min(maxY, newY))
return { x: constrainedX, y: constrainedY }
}
const constrainToViewportFully = (
newX: number,
newY: number,
newWidth?: number,
newHeight?: number,
) => {
const bounds = getViewportBounds()
if (!bounds) return { x: newX, y: newY }
const windowWidth = newWidth ?? width.value
const windowHeight = newHeight ?? height.value
// Keep entire window within viewport
const maxX = bounds.width - windowWidth
const minX = 0
const maxY = bounds.height - windowHeight
// For Y axis: HARD constraint at top (y=0), never allow window to go above header
const minY = 0
// Bottom: allow 1/3 to go outside
const maxY = bounds.height - windowHeight + maxOffscreenBottom
const constrainedX = Math.max(minX, Math.min(maxX, newX))
const constrainedY = Math.max(minY, Math.min(maxY, newY))
@ -357,15 +298,6 @@ const constrainToViewportFully = (
return { x: constrainedX, y: constrainedY }
}
const snapToViewport = () => {
const bounds = getViewportBounds()
if (!bounds) return
const constrained = constrainToViewportFully(x.value, y.value)
x.value = constrained.x
y.value = constrained.y
}
const handleActivate = () => {
emit('activate')
}
@ -387,14 +319,25 @@ const handleMaximize = () => {
height.value = preMaximizeState.value.height
isMaximized.value = false
} else {
// Maximize
// Maximize - set position and size to viewport dimensions
preMaximizeState.value = {
x: x.value,
y: y.value,
width: width.value,
height: height.value,
}
isMaximized.value = true
// Get viewport bounds (desktop container, already excludes header)
const bounds = getViewportBounds()
if (bounds && bounds.width > 0 && bounds.height > 0) {
x.value = 0
y.value = 0
width.value = bounds.width
height.value = bounds.height
isMaximized.value = true
}
console.log('handleMaximize', preMaximizeState, bounds)
}
}
@ -402,8 +345,30 @@ const handleMaximize = () => {
const handleResizeStart = (direction: string, e: MouseEvent | TouchEvent) => {
isResizing.value = true
resizeDirection.value = direction
resizeStartX.value = e.clientX
resizeStartY.value = e.clientY
let clientX: number
let clientY: number
if ('touches' in e) {
// Es ist ein TouchEvent
const touch = e.touches[0] // Hole den ersten Touch
// Prüfe, ob 'touch' existiert (ist undefined, wenn e.touches leer ist)
if (touch) {
clientX = touch.clientX
clientY = touch.clientY
} else {
// Ungültiges Start-Event (kein Finger). Abbruch.
isResizing.value = false
return
}
} else {
// Es ist ein MouseEvent
clientX = e.clientX
clientY = e.clientY
}
resizeStartX.value = clientX
resizeStartY.value = clientY
resizeStartWidth.value = width.value
resizeStartHeight.value = height.value
resizeStartPosX.value = x.value
@ -446,9 +411,6 @@ useEventListener(window, 'mouseup', () => {
globalThis.getSelection()?.removeAllRanges()
isResizing.value = false
// Snap back to viewport after resize ends
snapToViewport()
emit('positionChanged', x.value, y.value)
emit('sizeChanged', width.value, height.value)
}

View File

@ -0,0 +1,219 @@
<template>
<UModal
v-model:open="localShowWindowOverview"
title=""
fullscreen
>
<template #content>
<div class="flex flex-col h-full">
<!-- Header -->
<div
class="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700"
>
<h3 class="text-2xl font-bold">Window Overview</h3>
<UButton
icon="i-heroicons-x-mark"
color="neutral"
variant="ghost"
@click="localShowWindowOverview = false"
/>
</div>
<!-- Scrollable Content -->
<div class="flex-1 overflow-y-auto p-6 justify-center flex">
<!-- Window Thumbnails Flex Layout -->
<div
v-if="windows.length > 0"
class="flex flex-wrap gap-6 justify-start items-start"
>
<div
v-for="window in windows"
:key="window.id"
class="relative group cursor-pointer"
>
<!-- Window Title Bar -->
<div class="flex items-center gap-3 mb-2 px-2">
<UIcon
v-if="window.icon"
:name="window.icon"
class="size-5 shrink-0"
/>
<div class="flex-1 min-w-0">
<p class="font-semibold text-sm truncate">
{{ window.title }}
</p>
</div>
<!-- Minimized Badge -->
<UBadge
v-if="window.isMinimized"
color="info"
size="xs"
>
Minimized
</UBadge>
</div>
<!-- Scaled Window Preview Container / Teleport Target -->
<div
:id="`window-preview-${window.id}`"
class="relative bg-gray-100 dark:bg-gray-900 rounded-lg overflow-hidden border-2 border-gray-200 dark:border-gray-700 group-hover:border-primary-500 transition-all shadow-lg"
:style="getCardStyle(window)"
@click="handleRestoreAndActivateWindow(window.id)"
>
<!-- Hover Overlay -->
<div
class="absolute inset-0 bg-primary-500/10 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-40"
/>
</div>
</div>
</div>
<!-- Empty State -->
<div
v-else
class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400"
>
<UIcon
name="i-heroicons-window"
class="size-16 mb-4"
/>
<p class="text-lg font-medium">No windows open</p>
<p class="text-sm">
Open an extension or system window to see it here
</p>
</div>
</div>
</div>
</template>
</UModal>
</template>
<script setup lang="ts">
const windowManager = useWindowManagerStore()
const workspaceStore = useWorkspaceStore()
const { showWindowOverview, windows } = storeToRefs(windowManager)
// Local computed for two-way binding with UModal
const localShowWindowOverview = computed({
get: () => showWindowOverview.value,
set: (value) => {
showWindowOverview.value = value
},
})
const handleRestoreAndActivateWindow = (windowId: string) => {
const window = windowManager.windows.find((w) => w.id === windowId)
if (!window) return
// Switch to the workspace where this window is located
if (window.workspaceId) {
workspaceStore.slideToWorkspace(window.workspaceId)
}
// If window is minimized, restore it first
if (window.isMinimized) {
windowManager.restoreWindow(windowId)
} else {
// If not minimized, just activate it
windowManager.activateWindow(windowId)
}
// Close the overview
localShowWindowOverview.value = false
}
// Store original window sizes and positions to restore after overview closes
const originalWindowState = ref<
Map<string, { width: number; height: number; x: number; y: number }>
>(new Map())
// Min/Max dimensions for preview cards
const MIN_PREVIEW_WIDTH = 300
const MAX_PREVIEW_WIDTH = 600
const MIN_PREVIEW_HEIGHT = 225
const MAX_PREVIEW_HEIGHT = 450
// Calculate card size and scale based on window dimensions
const getCardStyle = (window: (typeof windows.value)[0]) => {
const scaleX = MAX_PREVIEW_WIDTH / window.width
const scaleY = MAX_PREVIEW_HEIGHT / window.height
const scale = Math.min(scaleX, scaleY, 1) // Never scale up, only down
// Calculate scaled dimensions
const scaledWidth = window.width * scale
const scaledHeight = window.height * scale
// Ensure minimum card size
let finalScale = scale
if (scaledWidth < MIN_PREVIEW_WIDTH) {
finalScale = MIN_PREVIEW_WIDTH / window.width
}
if (scaledHeight < MIN_PREVIEW_HEIGHT) {
finalScale = Math.max(finalScale, MIN_PREVIEW_HEIGHT / window.height)
}
const cardWidth = window.width * finalScale
const cardHeight = window.height * finalScale
return {
width: `${cardWidth}px`,
height: `${cardHeight}px`,
'--window-scale': finalScale, // CSS variable for scale
}
}
// Watch for overview closing to restore windows
watch(localShowWindowOverview, async (isOpen, wasOpen) => {
if (!isOpen && wasOpen) {
console.log('[WindowOverview] Overview closed, restoring windows...')
// Restore original window state
for (const window of windows.value) {
const originalState = originalWindowState.value.get(window.id)
if (originalState) {
console.log(
`[WindowOverview] Restoring window ${window.id} to:`,
originalState,
)
windowManager.updateWindowSize(
window.id,
originalState.width,
originalState.height,
)
windowManager.updateWindowPosition(
window.id,
originalState.x,
originalState.y,
)
}
}
originalWindowState.value.clear()
}
})
// Watch for overview opening to store original state
watch(
() => localShowWindowOverview.value && windows.value.length,
(shouldStore) => {
if (shouldStore && originalWindowState.value.size === 0) {
console.log('[WindowOverview] Storing original window states...')
for (const window of windows.value) {
console.log(`[WindowOverview] Window ${window.id}:`, {
originalSize: { width: window.width, height: window.height },
originalPos: { x: window.x, y: window.y },
})
originalWindowState.value.set(window.id, {
width: window.width,
height: window.height,
x: window.x,
y: window.y,
})
}
}
},
)
</script>

View File

@ -1,10 +1,12 @@
<template>
<UCard
ref="cardEl"
class="cursor-pointer transition-all h-32 w-72 shrink-0 group duration-500"
:class="[
workspace.id === currentWorkspace?.id
? 'ring-2 ring-secondary bg-secondary/10'
: 'hover:ring-2 hover:ring-gray-300',
isDragOver ? 'ring-4 ring-primary bg-primary/20 scale-105' : '',
]"
@click="workspaceStore.slideToWorkspace(workspace.id)"
>
@ -27,9 +29,70 @@
</template>
<script setup lang="ts">
defineProps<{ workspace: IWorkspace }>()
const props = defineProps<{ workspace: IWorkspace }>()
const workspaceStore = useWorkspaceStore()
const windowManager = useWindowManagerStore()
const { currentWorkspace } = storeToRefs(workspaceStore)
const cardEl = useTemplateRef('cardEl')
const isDragOver = ref(false)
// Use mouse position to detect if over card
const { x: mouseX, y: mouseY } = useMouse()
// Check if mouse is over this card while dragging
watchEffect(() => {
if (!windowManager.draggingWindowId || !cardEl.value?.$el) {
isDragOver.value = false
return
}
// Get card bounding box
const rect = cardEl.value.$el.getBoundingClientRect()
// Check if mouse is within card bounds
const isOver =
mouseX.value >= rect.left &&
mouseX.value <= rect.right &&
mouseY.value >= rect.top &&
mouseY.value <= rect.bottom
isDragOver.value = isOver
})
// Handle drop when drag ends - check BEFORE draggingWindowId is cleared
let wasOverThisCard = false
watchEffect(() => {
if (isDragOver.value && windowManager.draggingWindowId) {
wasOverThisCard = true
}
})
watch(
() => windowManager.draggingWindowId,
(newValue, oldValue) => {
// Drag ended (from something to null)
if (oldValue && !newValue && wasOverThisCard) {
console.log(
'[WorkspaceCard] Drop detected! Moving window to workspace:',
props.workspace.name,
)
const window = windowManager.windows.find((w) => w.id === oldValue)
if (window) {
window.workspaceId = props.workspace.id
window.x = 0
window.y = 0
// Switch to the workspace after dropping
//workspaceStore.slideToWorkspace(props.workspace.id)
}
wasOverThisCard = false
} else if (!newValue) {
// Drag ended but not over this card
wasOverThisCard = false
}
},
)
</script>

View File

@ -37,6 +37,22 @@
size="lg"
>
</UButton>
<UButton
color="neutral"
variant="outline"
:block="isSmallScreen"
@click="showWindowOverview = !showWindowOverview"
icon="i-heroicons-squares-2x2"
size="lg"
>
<template #trailing v-if="openWindowsCount > 0">
<UBadge
:label="openWindowsCount.toString()"
color="primary"
size="xs"
/>
</template>
</UButton>
<HaexExtensionLauncher :block="isSmallScreen" />
</template>
</UPageHeader>
@ -54,6 +70,10 @@ const { currentVaultName } = storeToRefs(useVaultStore())
const { isSmallScreen } = storeToRefs(useUiStore())
const { isOverviewMode } = storeToRefs(useWorkspaceStore())
const { showWindowOverview, openWindowsCount } = storeToRefs(
useWindowManagerStore(),
)
</script>
<i18n lang="yaml">

View File

@ -39,6 +39,15 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
const activeWindowId = ref<string | null>(null)
const nextZIndex = ref(100)
// Window Overview State
const showWindowOverview = ref(false)
// Computed: Count of all open windows (including minimized)
const openWindowsCount = computed(() => windows.value.length)
// Window Dragging State (for drag & drop to workspaces)
const draggingWindowId = ref<string | null>(null)
// System Windows Registry
const systemWindows: Record<string, SystemWindowDefinition> = {
developer: {
@ -332,6 +341,7 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
activeWindowId,
closeWindow,
currentWorkspaceWindows,
draggingWindowId,
getAllSystemWindows,
getMinimizedWindows,
getSystemWindow,
@ -340,7 +350,9 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
minimizeWindow,
moveWindowsToWorkspace,
openWindowAsync,
openWindowsCount,
restoreWindow,
showWindowOverview,
updateWindowPosition,
updateWindowSize,
windowAnimationDuration,

View File

@ -136,6 +136,8 @@ const drizzleCallback = (async (
params: unknown[],
method: 'get' | 'run' | 'all' | 'values',
) => {
// Wir MÜSSEN 'any[]' verwenden, um Drizzle's Typ zu erfüllen.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let rows: any[] = []
try {
@ -179,7 +181,8 @@ const drizzleCallback = (async (
console.log('drizzleCallback rows', rows)
if (method === 'get') {
return rows.length > 0 ? { rows: rows[0] } : { rows }
return { rows: rows.slice(0, 1) }
//return rows.length > 0 ? { rows: rows[0] } : { rows }
}
return { rows }
}) satisfies AsyncRemoteCallback