mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-16 22:20:51 +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",
|
"@nuxt/ui": "4.0.0",
|
||||||
"@nuxtjs/i18n": "10.0.6",
|
"@nuxtjs/i18n": "10.0.6",
|
||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
"@tailwindcss/vite": "^4.1.15",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@tauri-apps/api": "^2.9.0",
|
"@tauri-apps/api": "^2.9.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
"@tauri-apps/plugin-fs": "^2.4.2",
|
||||||
@ -38,23 +38,24 @@
|
|||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^13.9.0",
|
||||||
"@vueuse/gesture": "^2.0.0",
|
"@vueuse/gesture": "^2.0.0",
|
||||||
"@vueuse/nuxt": "^13.9.0",
|
"@vueuse/nuxt": "^13.9.0",
|
||||||
"drizzle-orm": "^0.44.6",
|
"drizzle-orm": "^0.44.7",
|
||||||
"eslint": "^9.38.0",
|
"eslint": "^9.38.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"nuxt": "^4.1.3",
|
"nuxt": "^4.1.3",
|
||||||
"nuxt-zod-i18n": "^1.12.1",
|
"nuxt-zod-i18n": "^1.12.1",
|
||||||
"swiper": "^12.0.3",
|
"swiper": "^12.0.3",
|
||||||
"tailwindcss": "^4.1.15",
|
"tailwindcss": "^4.1.16",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3",
|
"vue-router": "^4.6.3",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/hugeicons": "^1.2.17",
|
"@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",
|
"@iconify/tailwind4": "^1.0.6",
|
||||||
"@libsql/client": "^0.15.15",
|
"@libsql/client": "^0.15.15",
|
||||||
"@tauri-apps/cli": "^2.9.0",
|
"@tauri-apps/cli": "^2.9.1",
|
||||||
"@types/node": "^24.9.1",
|
"@types/node": "^24.9.1",
|
||||||
"@vitejs/plugin-vue": "6.0.1",
|
"@vitejs/plugin-vue": "6.0.1",
|
||||||
"@vue/compiler-sfc": "^3.5.22",
|
"@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,
|
&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| {
|
std::fs::create_dir_all(&extensions_dir).map_err(|e| {
|
||||||
ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e)
|
ExtensionError::filesystem_with_path(extensions_dir.display().to_string(), e)
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@ -10,7 +10,6 @@
|
|||||||
:initial-slide="currentWorkspaceIndex"
|
:initial-slide="currentWorkspaceIndex"
|
||||||
:speed="300"
|
:speed="300"
|
||||||
:touch-angle="45"
|
:touch-angle="45"
|
||||||
:threshold="10"
|
|
||||||
:no-swiping="true"
|
:no-swiping="true"
|
||||||
no-swiping-class="no-swipe"
|
no-swiping-class="no-swipe"
|
||||||
:allow-touch-move="allowSwipe"
|
:allow-touch-move="allowSwipe"
|
||||||
@ -79,37 +78,48 @@
|
|||||||
|
|
||||||
<!-- Windows for this workspace -->
|
<!-- Windows for this workspace -->
|
||||||
<template
|
<template
|
||||||
v-for="(window, index) in getWorkspaceWindows(workspace.id)"
|
v-for="window in getWorkspaceWindows(workspace.id)"
|
||||||
:key="window.id"
|
:key="window.id"
|
||||||
>
|
>
|
||||||
<!-- Wrapper for Overview Mode Click/Drag -->
|
<!-- Desktop container for when overview is closed -->
|
||||||
<div
|
<div
|
||||||
v-if="false"
|
:id="`desktop-container-${window.id}`"
|
||||||
:style="
|
class="absolute"
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 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
|
<HaexWindow
|
||||||
|
v-show="
|
||||||
|
windowManager.showWindowOverview || !window.isMinimized
|
||||||
|
"
|
||||||
:id="window.id"
|
:id="window.id"
|
||||||
:title="window.title"
|
:title="window.title"
|
||||||
:icon="window.icon"
|
:icon="window.icon"
|
||||||
:initial-x="window.x"
|
v-model:x="overviewWindowState[window.id]!.x"
|
||||||
:initial-y="window.y"
|
v-model:y="overviewWindowState[window.id]!.y"
|
||||||
:initial-width="window.width"
|
v-model:width="overviewWindowState[window.id]!.width"
|
||||||
:initial-height="window.height"
|
v-model:height="overviewWindowState[window.id]!.height"
|
||||||
:is-active="windowManager.isWindowActive(window.id)"
|
:is-active="windowManager.isWindowActive(window.id)"
|
||||||
:source-x="window.sourceX"
|
:source-x="window.sourceX"
|
||||||
:source-y="window.sourceY"
|
:source-y="window.sourceY"
|
||||||
@ -117,12 +127,13 @@
|
|||||||
:source-height="window.sourceHeight"
|
:source-height="window.sourceHeight"
|
||||||
:is-opening="window.isOpening"
|
:is-opening="window.isOpening"
|
||||||
:is-closing="window.isClosing"
|
:is-closing="window.isClosing"
|
||||||
class="no-swipe pointer-events-none"
|
class="no-swipe"
|
||||||
@close="windowManager.closeWindow(window.id)"
|
@close="windowManager.closeWindow(window.id)"
|
||||||
@minimize="windowManager.minimizeWindow(window.id)"
|
@minimize="windowManager.minimizeWindow(window.id)"
|
||||||
@activate="windowManager.activateWindow(window.id)"
|
@activate="windowManager.activateWindow(window.id)"
|
||||||
@position-changed="
|
@position-changed="
|
||||||
(x, y) => windowManager.updateWindowPosition(window.id, x, y)
|
(x, y) =>
|
||||||
|
windowManager.updateWindowPosition(window.id, x, y)
|
||||||
"
|
"
|
||||||
@size-changed="
|
@size-changed="
|
||||||
(width, height) =>
|
(width, height) =>
|
||||||
@ -131,7 +142,6 @@
|
|||||||
@drag-start="handleWindowDragStart(window.id)"
|
@drag-start="handleWindowDragStart(window.id)"
|
||||||
@drag-end="handleWindowDragEnd"
|
@drag-end="handleWindowDragEnd"
|
||||||
>
|
>
|
||||||
{{ window }}
|
|
||||||
<!-- System Window: Render Vue Component -->
|
<!-- System Window: Render Vue Component -->
|
||||||
<component
|
<component
|
||||||
:is="getSystemWindowComponent(window.sourceId)"
|
:is="getSystemWindowComponent(window.sourceId)"
|
||||||
@ -146,16 +156,17 @@
|
|||||||
/>
|
/>
|
||||||
</HaexWindow>
|
</HaexWindow>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
<!-- Normal Mode (non-overview) -->
|
|
||||||
<HaexWindow
|
<HaexWindow
|
||||||
|
v-else
|
||||||
|
v-show="windowManager.showWindowOverview || !window.isMinimized"
|
||||||
:id="window.id"
|
:id="window.id"
|
||||||
:title="window.title"
|
:title="window.title"
|
||||||
:icon="window.icon"
|
:icon="window.icon"
|
||||||
:initial-x="window.x"
|
v-model:x="window.x"
|
||||||
:initial-y="window.y"
|
v-model:y="window.y"
|
||||||
:initial-width="window.width"
|
v-model:width="window.width"
|
||||||
:initial-height="window.height"
|
v-model:height="window.height"
|
||||||
:is-active="windowManager.isWindowActive(window.id)"
|
:is-active="windowManager.isWindowActive(window.id)"
|
||||||
:source-x="window.sourceX"
|
:source-x="window.sourceX"
|
||||||
:source-y="window.sourceY"
|
:source-y="window.sourceY"
|
||||||
@ -190,11 +201,15 @@
|
|||||||
:window-id="window.id"
|
:window-id="window.id"
|
||||||
/>
|
/>
|
||||||
</HaexWindow>
|
</HaexWindow>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
|
||||||
|
<!-- Window Overview Modal -->
|
||||||
|
<HaexWindowOverview />
|
||||||
|
|
||||||
<!-- Workspace Drawer -->
|
<!-- Workspace Drawer -->
|
||||||
<UDrawer
|
<UDrawer
|
||||||
v-model:open="isOverviewMode"
|
v-model:open="isOverviewMode"
|
||||||
@ -202,8 +217,6 @@
|
|||||||
:dismissible="false"
|
:dismissible="false"
|
||||||
:overlay="false"
|
:overlay="false"
|
||||||
:modal="false"
|
:modal="false"
|
||||||
should-scale-background
|
|
||||||
set-background-color-on-scale
|
|
||||||
title="Workspaces"
|
title="Workspaces"
|
||||||
description="Workspaces"
|
description="Workspaces"
|
||||||
>
|
>
|
||||||
@ -252,17 +265,12 @@ import type { Swiper as SwiperType } from 'swiper'
|
|||||||
import 'swiper/css'
|
import 'swiper/css'
|
||||||
import 'swiper/css/navigation'
|
import 'swiper/css/navigation'
|
||||||
|
|
||||||
import { eq } from 'drizzle-orm'
|
|
||||||
import { haexDesktopItems } from '~~/src-tauri/database/schemas'
|
|
||||||
|
|
||||||
const SwiperNavigation = Navigation
|
const SwiperNavigation = Navigation
|
||||||
|
|
||||||
const desktopStore = useDesktopStore()
|
const desktopStore = useDesktopStore()
|
||||||
const extensionsStore = useExtensionsStore()
|
const extensionsStore = useExtensionsStore()
|
||||||
const windowManager = useWindowManagerStore()
|
const windowManager = useWindowManagerStore()
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
|
||||||
const { currentVault } = storeToRefs(useVaultStore())
|
|
||||||
const { desktopItems } = storeToRefs(desktopStore)
|
const { desktopItems } = storeToRefs(desktopStore)
|
||||||
const { availableExtensions } = storeToRefs(extensionsStore)
|
const { availableExtensions } = storeToRefs(extensionsStore)
|
||||||
const {
|
const {
|
||||||
@ -315,7 +323,6 @@ const currentDraggedReferenceId = ref<string>()
|
|||||||
|
|
||||||
// Window drag state for snap zones
|
// Window drag state for snap zones
|
||||||
const isWindowDragging = ref(false)
|
const isWindowDragging = ref(false)
|
||||||
const currentDraggingWindowId = ref<string | null>(null)
|
|
||||||
const snapEdgeThreshold = 50 // pixels from edge to show snap zone
|
const snapEdgeThreshold = 50 // pixels from edge to show snap zone
|
||||||
|
|
||||||
// Computed visibility for snap zones (uses mouseX from above)
|
// Computed visibility for snap zones (uses mouseX from above)
|
||||||
@ -329,27 +336,6 @@ const showRightSnapZone = computed(() => {
|
|||||||
return mouseX.value >= viewportWidth - snapEdgeThreshold
|
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
|
// Get icons for a specific workspace
|
||||||
const getWorkspaceIcons = (workspaceId: string) => {
|
const getWorkspaceIcons = (workspaceId: string) => {
|
||||||
return desktopItems.value
|
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) => {
|
const getWorkspaceWindows = (workspaceId: string) => {
|
||||||
return windowManager.windows.filter(
|
return windowManager.windows.filter((w) => w.workspaceId === workspaceId)
|
||||||
(w) => w.workspaceId === workspaceId && !w.isMinimized,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Vue Component for system window
|
// Get Vue Component for system window
|
||||||
@ -431,29 +415,6 @@ const handleDragEnd = async () => {
|
|||||||
allowSwipe.value = true // Re-enable Swiper after drag
|
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 = () => {
|
const handleDesktopClick = () => {
|
||||||
// Only clear selection if it was a simple click, not an area selection
|
// Only clear selection if it was a simple click, not an area selection
|
||||||
// Check if we just finished an area selection (box size > threshold)
|
// Check if we just finished an area selection (box size > threshold)
|
||||||
@ -470,30 +431,23 @@ const handleDesktopClick = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleWindowDragStart = (windowId: string) => {
|
const handleWindowDragStart = (windowId: string) => {
|
||||||
|
console.log('[Desktop] handleWindowDragStart:', windowId)
|
||||||
isWindowDragging.value = true
|
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
|
allowSwipe.value = false // Disable Swiper during window drag
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleWindowDragEnd = async () => {
|
const handleWindowDragEnd = async () => {
|
||||||
// Window handles snapping itself, we just need to cleanup state
|
console.log('[Desktop] handleWindowDragEnd')
|
||||||
isWindowDragging.value = false
|
isWindowDragging.value = false
|
||||||
currentDraggingWindowId.value = null
|
windowManager.draggingWindowId = null // Clear from store
|
||||||
allowSwipe.value = true // Re-enable Swiper after drag
|
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
|
// Area selection handlers
|
||||||
const handleAreaSelectStart = (e: MouseEvent) => {
|
const handleAreaSelectStart = (e: MouseEvent) => {
|
||||||
if (!desktopEl.value) return
|
if (!desktopEl.value) return
|
||||||
@ -579,13 +533,7 @@ const handleAddWorkspace = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSwitchToWorkspace = (index: number) => {
|
/* const handleRemoveWorkspace = async () => {
|
||||||
if (swiperInstance.value) {
|
|
||||||
swiperInstance.value.slideTo(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveWorkspace = async () => {
|
|
||||||
if (!currentWorkspace.value || workspaces.value.length <= 1) return
|
if (!currentWorkspace.value || workspaces.value.length <= 1) return
|
||||||
|
|
||||||
const currentIndex = currentWorkspaceIndex.value
|
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 (
|
const handleDropWindowOnWorkspace = async (
|
||||||
event: DragEvent,
|
event: DragEvent,
|
||||||
targetWorkspaceId: string,
|
targetWorkspaceId: string,
|
||||||
@ -616,116 +557,58 @@ const handleDropWindowOnWorkspace = async (
|
|||||||
if (windowId) {
|
if (windowId) {
|
||||||
await moveWindowToWorkspace(windowId, targetWorkspaceId)
|
await moveWindowToWorkspace(windowId, targetWorkspaceId)
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
|
|
||||||
// Overview Mode: Calculate grid positions and scale for windows
|
// Overview Mode: Calculate grid positions and scale for windows
|
||||||
const getOverviewWindowGridStyle = (index: number, totalWindows: number) => {
|
// Calculate preview dimensions for window overview
|
||||||
if (!viewportWidth.value || !viewportHeight.value) {
|
const MIN_PREVIEW_WIDTH = 300 // 50% increase from 200
|
||||||
return {}
|
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
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
>
|
||||||
|
>({})
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Ensure minimum card size
|
||||||
|
const scaledWidth = window.width * scale
|
||||||
|
const scaledHeight = window.height * scale
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine grid layout based on number of windows
|
overviewWindowState[window.id] = {
|
||||||
let cols = 1
|
x: 0,
|
||||||
let rows = 1
|
y: 0,
|
||||||
|
width: window.width, // Keep original width
|
||||||
if (totalWindows === 1) {
|
height: window.height, // Keep original height
|
||||||
cols = 1
|
scale: finalScale, // Store the scale factor
|
||||||
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 grid cell position
|
|
||||||
const col = index % cols
|
|
||||||
const row = Math.floor(index / cols)
|
|
||||||
|
|
||||||
// Padding and gap
|
|
||||||
const padding = 40 // px from viewport edges
|
|
||||||
const gap = 30 // px between windows
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
// Disable Swiper in overview mode
|
// Disable Swiper in overview mode
|
||||||
watch(isOverviewMode, (newValue) => {
|
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) {
|
} catch (error) {
|
||||||
open.value = false
|
open.value = false
|
||||||
console.error('handleError', error, typeof error)
|
if (error?.details?.reason === 'file is not a database') {
|
||||||
add({ color: 'error', description: `${error}` })
|
add({
|
||||||
|
color: 'error',
|
||||||
|
title: t('error.password.title'),
|
||||||
|
description: t('error.password.description'),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
add({ color: 'error', description: JSON.stringify(error) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -170,7 +177,9 @@ de:
|
|||||||
open: Vault öffnen
|
open: Vault öffnen
|
||||||
description: Öffne eine vorhandene Vault
|
description: Öffne eine vorhandene Vault
|
||||||
error:
|
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:
|
en:
|
||||||
open: Unlock
|
open: Unlock
|
||||||
@ -180,5 +189,7 @@ en:
|
|||||||
vault:
|
vault:
|
||||||
open: Open Vault
|
open: Open Vault
|
||||||
error:
|
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>
|
</i18n>
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
'border border-gray-200 dark:border-gray-700 transition-all ease-out duration-600 ',
|
'border border-gray-200 dark:border-gray-700 transition-all ease-out duration-600 ',
|
||||||
'flex flex-col @container',
|
'flex flex-col @container',
|
||||||
{ 'select-none': isResizingOrDragging },
|
{ 'select-none': isResizingOrDragging },
|
||||||
isActive ? 'z-50' : 'z-10',
|
isActive ? 'z-100' : 'z-50',
|
||||||
]"
|
]"
|
||||||
@mousedown="handleActivate"
|
@mousedown="handleActivate"
|
||||||
>
|
>
|
||||||
@ -95,10 +95,6 @@ const props = defineProps<{
|
|||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
icon?: string | null
|
icon?: string | null
|
||||||
initialX?: number
|
|
||||||
initialY?: number
|
|
||||||
initialWidth?: number
|
|
||||||
initialHeight?: number
|
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
sourceX?: number
|
sourceX?: number
|
||||||
sourceY?: number
|
sourceY?: number
|
||||||
@ -118,6 +114,12 @@ const emit = defineEmits<{
|
|||||||
dragEnd: []
|
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 windowEl = useTemplateRef('windowEl')
|
||||||
const titlebarEl = useTemplateRef('titlebarEl')
|
const titlebarEl = useTemplateRef('titlebarEl')
|
||||||
|
|
||||||
@ -126,20 +128,14 @@ const viewportSize = inject<{
|
|||||||
width: Ref<number>
|
width: Ref<number>
|
||||||
height: Ref<number>
|
height: Ref<number>
|
||||||
}>('viewportSize')
|
}>('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
|
const isMaximized = ref(false) // Don't start maximized
|
||||||
|
|
||||||
// Store initial position/size for restore
|
// Store initial position/size for restore
|
||||||
const preMaximizeState = ref({
|
const preMaximizeState = ref({
|
||||||
x: props.initialX ?? 100,
|
x: x.value,
|
||||||
y: props.initialY ?? 100,
|
y: y.value,
|
||||||
width: props.initialWidth ?? 800,
|
width: width.value,
|
||||||
height: props.initialHeight ?? 600,
|
height: height.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Dragging state
|
// Dragging state
|
||||||
@ -161,10 +157,6 @@ const isResizingOrDragging = computed(
|
|||||||
() => isResizing.value || isDragging.value,
|
() => 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)
|
// Setup drag with useDrag composable (supports mouse + touch)
|
||||||
useDrag(
|
useDrag(
|
||||||
({ movement: [mx, my], first, last }) => {
|
({ movement: [mx, my], first, last }) => {
|
||||||
@ -180,34 +172,8 @@ useDrag(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (last) {
|
if (last) {
|
||||||
// Drag ended - apply snapping
|
// Drag ended
|
||||||
isDragging.value = false
|
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()
|
globalThis.getSelection()?.removeAllRanges()
|
||||||
emit('positionChanged', x.value, y.value)
|
emit('positionChanged', x.value, y.value)
|
||||||
emit('sizeChanged', width.value, height.value)
|
emit('sizeChanged', width.value, height.value)
|
||||||
@ -229,7 +195,6 @@ useDrag(
|
|||||||
eventOptions: { passive: false },
|
eventOptions: { passive: false },
|
||||||
pointer: { touch: true },
|
pointer: { touch: true },
|
||||||
drag: {
|
drag: {
|
||||||
threshold: 10, // 10px threshold prevents accidental drags and improves performance
|
|
||||||
filterTaps: true, // Filter out taps (clicks) vs drags
|
filterTaps: true, // Filter out taps (clicks) vs drags
|
||||||
delay: 0, // No delay for immediate response
|
delay: 0, // No delay for immediate response
|
||||||
},
|
},
|
||||||
@ -265,22 +230,18 @@ const windowStyle = computed(() => {
|
|||||||
baseStyle.opacity = '0'
|
baseStyle.opacity = '0'
|
||||||
baseStyle.transform = 'scale(0.3)'
|
baseStyle.transform = 'scale(0.3)'
|
||||||
}
|
}
|
||||||
// Normal state
|
// Normal state (maximized windows now use actual pixel dimensions)
|
||||||
else if (isMaximized.value) {
|
else {
|
||||||
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.left = `${x.value}px`
|
||||||
baseStyle.top = `${y.value}px`
|
baseStyle.top = `${y.value}px`
|
||||||
baseStyle.width = `${width.value}px`
|
baseStyle.width = `${width.value}px`
|
||||||
baseStyle.height = `${height.value}px`
|
baseStyle.height = `${height.value}px`
|
||||||
baseStyle.opacity = '1'
|
baseStyle.opacity = '1'
|
||||||
//baseStyle.transform = 'scale(1)'
|
|
||||||
|
// Remove border-radius when maximized
|
||||||
|
if (isMaximized.value) {
|
||||||
|
baseStyle.borderRadius = '0'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Performance optimization: hint browser about transforms
|
// Performance optimization: hint browser about transforms
|
||||||
@ -318,38 +279,18 @@ const constrainToViewportDuringDrag = (newX: number, newY: number) => {
|
|||||||
const windowWidth = width.value
|
const windowWidth = width.value
|
||||||
const windowHeight = height.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 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 maxX = bounds.width - windowWidth + maxOffscreenX
|
||||||
const minX = -maxOffscreenX
|
const minX = -maxOffscreenX
|
||||||
const maxY = bounds.height - windowHeight + maxOffscreenY
|
|
||||||
const minY = -maxOffscreenY
|
|
||||||
|
|
||||||
const constrainedX = Math.max(minX, Math.min(maxX, newX))
|
// For Y axis: HARD constraint at top (y=0), never allow window to go above header
|
||||||
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 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 constrainedX = Math.max(minX, Math.min(maxX, newX))
|
||||||
const constrainedY = Math.max(minY, Math.min(maxY, newY))
|
const constrainedY = Math.max(minY, Math.min(maxY, newY))
|
||||||
@ -357,15 +298,6 @@ const constrainToViewportFully = (
|
|||||||
return { x: constrainedX, y: constrainedY }
|
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 = () => {
|
const handleActivate = () => {
|
||||||
emit('activate')
|
emit('activate')
|
||||||
}
|
}
|
||||||
@ -387,23 +319,56 @@ const handleMaximize = () => {
|
|||||||
height.value = preMaximizeState.value.height
|
height.value = preMaximizeState.value.height
|
||||||
isMaximized.value = false
|
isMaximized.value = false
|
||||||
} else {
|
} else {
|
||||||
// Maximize
|
// Maximize - set position and size to viewport dimensions
|
||||||
preMaximizeState.value = {
|
preMaximizeState.value = {
|
||||||
x: x.value,
|
x: x.value,
|
||||||
y: y.value,
|
y: y.value,
|
||||||
width: width.value,
|
width: width.value,
|
||||||
height: height.value,
|
height: height.value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
isMaximized.value = true
|
||||||
}
|
}
|
||||||
|
console.log('handleMaximize', preMaximizeState, bounds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window resizing
|
// Window resizing
|
||||||
const handleResizeStart = (direction: string, e: MouseEvent | TouchEvent) => {
|
const handleResizeStart = (direction: string, e: MouseEvent | TouchEvent) => {
|
||||||
isResizing.value = true
|
isResizing.value = true
|
||||||
resizeDirection.value = direction
|
resizeDirection.value = direction
|
||||||
resizeStartX.value = e.clientX
|
let clientX: number
|
||||||
resizeStartY.value = e.clientY
|
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
|
resizeStartWidth.value = width.value
|
||||||
resizeStartHeight.value = height.value
|
resizeStartHeight.value = height.value
|
||||||
resizeStartPosX.value = x.value
|
resizeStartPosX.value = x.value
|
||||||
@ -446,9 +411,6 @@ useEventListener(window, 'mouseup', () => {
|
|||||||
globalThis.getSelection()?.removeAllRanges()
|
globalThis.getSelection()?.removeAllRanges()
|
||||||
isResizing.value = false
|
isResizing.value = false
|
||||||
|
|
||||||
// Snap back to viewport after resize ends
|
|
||||||
snapToViewport()
|
|
||||||
|
|
||||||
emit('positionChanged', x.value, y.value)
|
emit('positionChanged', x.value, y.value)
|
||||||
emit('sizeChanged', width.value, height.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>
|
<template>
|
||||||
<UCard
|
<UCard
|
||||||
|
ref="cardEl"
|
||||||
class="cursor-pointer transition-all h-32 w-72 shrink-0 group duration-500"
|
class="cursor-pointer transition-all h-32 w-72 shrink-0 group duration-500"
|
||||||
:class="[
|
:class="[
|
||||||
workspace.id === currentWorkspace?.id
|
workspace.id === currentWorkspace?.id
|
||||||
? 'ring-2 ring-secondary bg-secondary/10'
|
? 'ring-2 ring-secondary bg-secondary/10'
|
||||||
: 'hover:ring-2 hover:ring-gray-300',
|
: 'hover:ring-2 hover:ring-gray-300',
|
||||||
|
isDragOver ? 'ring-4 ring-primary bg-primary/20 scale-105' : '',
|
||||||
]"
|
]"
|
||||||
@click="workspaceStore.slideToWorkspace(workspace.id)"
|
@click="workspaceStore.slideToWorkspace(workspace.id)"
|
||||||
>
|
>
|
||||||
@ -27,9 +29,70 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{ workspace: IWorkspace }>()
|
const props = defineProps<{ workspace: IWorkspace }>()
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
const windowManager = useWindowManagerStore()
|
||||||
|
|
||||||
const { currentWorkspace } = storeToRefs(workspaceStore)
|
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>
|
</script>
|
||||||
|
|||||||
@ -37,6 +37,22 @@
|
|||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
</UButton>
|
</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" />
|
<HaexExtensionLauncher :block="isSmallScreen" />
|
||||||
</template>
|
</template>
|
||||||
</UPageHeader>
|
</UPageHeader>
|
||||||
@ -54,6 +70,10 @@ const { currentVaultName } = storeToRefs(useVaultStore())
|
|||||||
const { isSmallScreen } = storeToRefs(useUiStore())
|
const { isSmallScreen } = storeToRefs(useUiStore())
|
||||||
|
|
||||||
const { isOverviewMode } = storeToRefs(useWorkspaceStore())
|
const { isOverviewMode } = storeToRefs(useWorkspaceStore())
|
||||||
|
|
||||||
|
const { showWindowOverview, openWindowsCount } = storeToRefs(
|
||||||
|
useWindowManagerStore(),
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i18n lang="yaml">
|
<i18n lang="yaml">
|
||||||
|
|||||||
@ -39,6 +39,15 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
|
|||||||
const activeWindowId = ref<string | null>(null)
|
const activeWindowId = ref<string | null>(null)
|
||||||
const nextZIndex = ref(100)
|
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
|
// System Windows Registry
|
||||||
const systemWindows: Record<string, SystemWindowDefinition> = {
|
const systemWindows: Record<string, SystemWindowDefinition> = {
|
||||||
developer: {
|
developer: {
|
||||||
@ -332,6 +341,7 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
|
|||||||
activeWindowId,
|
activeWindowId,
|
||||||
closeWindow,
|
closeWindow,
|
||||||
currentWorkspaceWindows,
|
currentWorkspaceWindows,
|
||||||
|
draggingWindowId,
|
||||||
getAllSystemWindows,
|
getAllSystemWindows,
|
||||||
getMinimizedWindows,
|
getMinimizedWindows,
|
||||||
getSystemWindow,
|
getSystemWindow,
|
||||||
@ -340,7 +350,9 @@ export const useWindowManagerStore = defineStore('windowManager', () => {
|
|||||||
minimizeWindow,
|
minimizeWindow,
|
||||||
moveWindowsToWorkspace,
|
moveWindowsToWorkspace,
|
||||||
openWindowAsync,
|
openWindowAsync,
|
||||||
|
openWindowsCount,
|
||||||
restoreWindow,
|
restoreWindow,
|
||||||
|
showWindowOverview,
|
||||||
updateWindowPosition,
|
updateWindowPosition,
|
||||||
updateWindowSize,
|
updateWindowSize,
|
||||||
windowAnimationDuration,
|
windowAnimationDuration,
|
||||||
|
|||||||
@ -136,6 +136,8 @@ const drizzleCallback = (async (
|
|||||||
params: unknown[],
|
params: unknown[],
|
||||||
method: 'get' | 'run' | 'all' | 'values',
|
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[] = []
|
let rows: any[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -179,7 +181,8 @@ const drizzleCallback = (async (
|
|||||||
console.log('drizzleCallback rows', rows)
|
console.log('drizzleCallback rows', rows)
|
||||||
|
|
||||||
if (method === 'get') {
|
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 }
|
return { rows }
|
||||||
}) satisfies AsyncRemoteCallback
|
}) satisfies AsyncRemoteCallback
|
||||||
|
|||||||
Reference in New Issue
Block a user