mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 14:10:52 +01:00
feature: window overview
This commit is contained in:
11
package.json
11
package.json
@ -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
737
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -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)
|
||||
})?;
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
219
src/components/haex/window/overview.vue
Normal file
219
src/components/haex/window/overview.vue
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user