add desktop

This commit is contained in:
2025-10-16 20:56:21 +02:00
parent 033c9135c6
commit a291619f63
27 changed files with 1755 additions and 124 deletions

View File

@ -0,0 +1,119 @@
<template>
<UContextMenu :items="contextMenuItems">
<div
ref="draggableEl"
:style="style"
class="select-none cursor-grab active:cursor-grabbing"
@pointerdown="handlePointerDown"
@pointermove="handlePointerMove"
@pointerup="handlePointerUp"
@dblclick="handleOpen"
>
<div class="flex flex-col items-center gap-1 p-2">
<div
class="w-16 h-16 flex items-center justify-center bg-white/90 dark:bg-gray-800/90 rounded-lg shadow-lg hover:shadow-xl transition-shadow"
>
<img v-if="icon" :src="icon" :alt="label" class="w-12 h-12 object-contain" />
<Icon v-else name="i-heroicons-puzzle-piece-solid" class="w-12 h-12 text-gray-500" />
</div>
<span
class="text-xs text-center max-w-20 truncate bg-white/80 dark:bg-gray-800/80 px-2 py-1 rounded shadow"
>
{{ label }}
</span>
</div>
</div>
</UContextMenu>
</template>
<script setup lang="ts">
const props = defineProps<{
id: string
itemType: 'extension' | 'file' | 'folder'
referenceId: string
initialX: number
initialY: number
label: string
icon?: string
}>()
const emit = defineEmits<{
positionChanged: [id: string, x: number, y: number]
open: [itemType: string, referenceId: string]
uninstall: [itemType: string, referenceId: string]
dragStart: [id: string, itemType: string, referenceId: string]
dragEnd: []
}>()
const desktopStore = useDesktopStore()
const contextMenuItems = computed(() =>
desktopStore.getContextMenuItems(
props.id,
props.itemType,
props.referenceId,
handleOpen,
handleUninstall,
),
)
const draggableEl = ref<HTMLElement>()
const x = ref(props.initialX)
const y = ref(props.initialY)
const isDragging = ref(false)
const offsetX = ref(0)
const offsetY = ref(0)
const style = computed(() => ({
position: 'absolute' as const,
left: `${x.value}px`,
top: `${y.value}px`,
touchAction: 'none' as const,
}))
const handlePointerDown = (e: PointerEvent) => {
if (!draggableEl.value || !draggableEl.value.parentElement) return
isDragging.value = true
emit('dragStart', props.id, props.itemType, props.referenceId)
// Get parent offset to convert from viewport coordinates to parent-relative coordinates
const parentRect = draggableEl.value.parentElement.getBoundingClientRect()
// Calculate offset from mouse position to current element position (in parent coordinates)
offsetX.value = e.clientX - parentRect.left - x.value
offsetY.value = e.clientY - parentRect.top - y.value
draggableEl.value.setPointerCapture(e.pointerId)
}
const handlePointerMove = (e: PointerEvent) => {
if (!isDragging.value || !draggableEl.value?.parentElement) return
const parentRect = draggableEl.value.parentElement.getBoundingClientRect()
const newX = e.clientX - parentRect.left - offsetX.value
const newY = e.clientY - parentRect.top - offsetY.value
x.value = newX
y.value = newY
}
const handlePointerUp = (e: PointerEvent) => {
if (!isDragging.value) return
isDragging.value = false
if (draggableEl.value) {
draggableEl.value.releasePointerCapture(e.pointerId)
}
emit('dragEnd')
emit('positionChanged', props.id, x.value, y.value)
}
const handleOpen = () => {
emit('open', props.itemType, props.referenceId)
}
const handleUninstall = () => {
emit('uninstall', props.itemType, props.referenceId)
}
</script>

View File

@ -0,0 +1,250 @@
<template>
<div
class="w-full h-full relative overflow-hidden bg-gradient-to-br from-blue-50 to-blue-100 dark:from-gray-900 dark:to-gray-800"
>
<!-- Dropzones (only visible during drag) -->
<Transition name="slide-down">
<div
v-if="isDragging"
class="absolute top-0 left-0 right-0 flex gap-2 p-4 z-50"
>
<!-- Remove from Desktop Dropzone -->
<div
ref="removeDropzoneEl"
class="flex-1 h-20 flex items-center justify-center gap-2 rounded-lg border-2 border-dashed transition-all"
:class="
isOverRemoveZone
? 'bg-orange-500/20 border-orange-500 dark:bg-orange-400/20 dark:border-orange-400'
: 'border-orange-500/50 dark:border-orange-400/50'
"
>
<Icon
name="i-heroicons-x-mark"
class="w-6 h-6"
:class="
isOverRemoveZone
? 'text-orange-700 dark:text-orange-300'
: 'text-orange-600 dark:text-orange-400'
"
/>
<span
class="font-semibold"
:class="
isOverRemoveZone
? 'text-orange-700 dark:text-orange-300'
: 'text-orange-600 dark:text-orange-400'
"
>
Von Desktop entfernen
</span>
</div>
<!-- Uninstall Dropzone -->
<div
ref="uninstallDropzoneEl"
class="flex-1 h-20 flex items-center justify-center gap-2 rounded-lg border-2 border-dashed transition-all"
:class="
isOverUninstallZone
? 'bg-red-500/20 border-red-500 dark:bg-red-400/20 dark:border-red-400'
: 'border-red-500/50 dark:border-red-400/50'
"
>
<Icon
name="i-heroicons-trash"
class="w-6 h-6"
:class="
isOverUninstallZone
? 'text-red-700 dark:text-red-300'
: 'text-red-600 dark:text-red-400'
"
/>
<span
class="font-semibold"
:class="
isOverUninstallZone
? 'text-red-700 dark:text-red-300'
: 'text-red-600 dark:text-red-400'
"
>
Deinstallieren
</span>
</div>
</div>
</Transition>
<HaexDesktopIcon
v-for="item in desktopItemIcons"
:key="item.id"
:id="item.id"
:item-type="item.itemType"
:reference-id="item.referenceId"
:initial-x="item.positionX"
:initial-y="item.positionY"
:label="item.label"
:icon="item.icon"
@position-changed="handlePositionChanged"
@open="handleOpen"
@drag-start="handleDragStart"
@drag-end="handleDragEnd"
@uninstall="handleUninstall"
/>
</div>
</template>
<script setup lang="ts">
const desktopStore = useDesktopStore()
const extensionsStore = useExtensionsStore()
const router = useRouter()
const { desktopItems } = storeToRefs(desktopStore)
const { availableExtensions } = storeToRefs(extensionsStore)
// Drag state
const isDragging = ref(false)
const currentDraggedItemId = ref<string>()
const currentDraggedItemType = ref<string>()
const currentDraggedReferenceId = ref<string>()
// 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)
}
},
})
interface DesktopItemIcon extends IDesktopItem {
label: string
icon?: string
}
const desktopItemIcons = computed<DesktopItemIcon[]>(() => {
return desktopItems.value.map((item) => {
if (item.itemType === 'extension') {
const extension = availableExtensions.value.find(
(ext) => ext.id === item.referenceId,
)
return {
...item,
label: extension?.name || 'Unknown',
icon: extension?.icon || '',
}
}
if (item.itemType === 'file') {
// Für später: file handling
return {
...item,
label: item.referenceId,
icon: undefined,
}
}
if (item.itemType === 'folder') {
// Für später: folder handling
return {
...item,
label: item.referenceId,
icon: undefined,
}
}
return {
...item,
label: item.referenceId,
icon: undefined,
}
})
})
const handlePositionChanged = async (id: string, x: number, y: number) => {
try {
await desktopStore.updateDesktopItemPositionAsync(id, x, y)
} catch (error) {
console.error('Fehler beim Speichern der Position:', error)
}
}
const localePath = useLocalePath()
const handleOpen = (itemType: string, referenceId: string) => {
if (itemType === 'extension') {
router.push(
localePath({
name: 'extension',
params: { extensionId: referenceId },
})
)
}
// Für später: file und folder handling
}
const handleDragStart = (id: string, itemType: string, referenceId: string) => {
isDragging.value = true
currentDraggedItemId.value = id
currentDraggedItemType.value = itemType
currentDraggedReferenceId.value = referenceId
}
const handleDragEnd = () => {
isDragging.value = false
currentDraggedItemId.value = undefined
currentDraggedItemType.value = undefined
currentDraggedReferenceId.value = undefined
}
const handleUninstall = async (itemType: string, referenceId: string) => {
if (itemType === 'extension') {
try {
const extension = availableExtensions.value.find((ext) => ext.id === referenceId)
if (extension) {
await extensionsStore.removeExtensionAsync(
extension.publicKey,
extension.name,
extension.version,
)
// Reload extensions after uninstall
await extensionsStore.loadExtensionsAsync()
}
} catch (error) {
console.error('Fehler beim Deinstallieren:', error)
}
}
// Für später: file und folder handling
}
onMounted(async () => {
await desktopStore.loadDesktopItemsAsync()
})
</script>
<style scoped>
.slide-down-enter-active,
.slide-down-leave-active {
transition: all 0.3s ease;
}
.slide-down-enter-from {
opacity: 0;
transform: translateY(-100%);
}
.slide-down-leave-to {
opacity: 0;
transform: translateY(-100%);
}
</style>

View File

@ -67,6 +67,12 @@
</div>
</UCard>
<!-- Add to Desktop Option -->
<UCheckbox
v-model="addToDesktop"
:label="t('addToDesktop')"
/>
<!-- Permissions Section -->
<div class="flex flex-col gap-4">
<h4 class="text-lg font-semibold">
@ -140,6 +146,7 @@ const open = defineModel<boolean>('open', { default: false })
const preview = defineModel<ExtensionPreview | null>('preview', {
default: null,
})
const addToDesktop = ref(true)
const databasePermissions = computed({
get: () => preview.value?.editable_permissions?.database || [],
@ -217,7 +224,10 @@ const permissionAccordionItems = computed(() => {
return items
})
const emit = defineEmits(['deny', 'confirm'])
const emit = defineEmits<{
deny: []
confirm: [addToDesktop: boolean]
}>()
const onDeny = () => {
open.value = false
@ -226,7 +236,7 @@ const onDeny = () => {
const onConfirm = () => {
open.value = false
emit('confirm')
emit('confirm', addToDesktop.value)
}
</script>
@ -235,6 +245,7 @@ de:
title: Erweiterung installieren
version: Version
author: Autor
addToDesktop: Zum Desktop hinzufügen
signature:
valid: Signatur verifiziert
invalid: Signatur ungültig
@ -249,6 +260,7 @@ en:
title: Install Extension
version: Version
author: Author
addToDesktop: Add to Desktop
signature:
valid: Signature verified
invalid: Invalid signature

View File

@ -422,7 +422,7 @@ async function handleContextMethodAsync(request: ExtensionRequest) {
return {
theme: contextGetters.getTheme(),
locale: contextGetters.getLocale(),
platform: detectPlatform(),
platform: contextGetters.getPlatform(),
}
default:
@ -430,13 +430,6 @@ async function handleContextMethodAsync(request: ExtensionRequest) {
}
}
function detectPlatform(): 'desktop' | 'mobile' | 'tablet' {
const width = window.innerWidth
if (width < 768) return 'mobile'
if (width < 1024) return 'tablet'
return 'desktop'
}
// ==========================================
// Storage Methods
// ==========================================

View File

@ -1,6 +1,5 @@
<template>
<div class="">
<!-- class="" -->
<div class="flex flex-col w-full h-full overflow-hidden">
<UPageHeader
as="header"
:ui="{
@ -33,7 +32,7 @@
</template>
</UPageHeader>
<main class="overflow-scroll flex bg-elevated">
<main class="flex-1 overflow-hidden bg-elevated">
<NuxtPage />
</main>
</div>

View File

@ -1,5 +1,5 @@
<template>
<div class="bg-default isolate h-dvh">
<div class="bg-default isolate w-dvw h-dvh flex flex-col">
<slot />
</div>
</template>

View File

@ -100,7 +100,11 @@ const { syncLastVaultsAsync, removeVaultAsync } = useLastVaultStore()
const { lastVaults } = storeToRefs(useLastVaultStore())
onMounted(async () => {
await syncLastVaultsAsync()
try {
await syncLastVaultsAsync()
} catch (error) {
console.error('ERROR: ', error)
}
})
const onSelectLocale = async (locale: Locale) => {

View File

@ -1,10 +1,5 @@
<template>
<div
:ui="{
root: ['h-full w-full bg-elevated lg:flex'],
center: ['h-full w-full'],
}"
>
<div class="w-full h-full overflow-y-auto">
<NuxtLayout name="app">
<NuxtPage />
</NuxtLayout>
@ -57,14 +52,18 @@ const { setDeviceIdIfNotExistsAsync, addDeviceNameAsync } = useDeviceStore()
const { deviceId } = storeToRefs(useDeviceStore())
onMounted(async () => {
await setDeviceIdIfNotExistsAsync()
await loadExtensionsAsync()
await readNotificationsAsync()
try {
await setDeviceIdIfNotExistsAsync()
await loadExtensionsAsync()
await readNotificationsAsync()
if (!(await isKnownDeviceAsync())) {
console.log('not known device')
newDeviceName.value = hostname.value ?? 'unknown'
showNewDeviceDialog.value = true
if (!(await isKnownDeviceAsync())) {
console.log('not known device')
newDeviceName.value = hostname.value ?? 'unknown'
showNewDeviceDialog.value = true
}
} catch (error) {
console.error('vault mount error:', error)
}
})

View File

@ -206,7 +206,7 @@ import {
} from '~/config/constants'
definePageMeta({
name: 'haexExtension',
name: 'extension',
})
const { t } = useI18n()

View File

@ -104,7 +104,7 @@
<HaexExtensionDialogInstall
v-model:open="showConfirmation"
:preview="preview"
@confirm="addExtensionAsync"
@confirm="(addToDesktop) => addExtensionAsync(addToDesktop)"
/>
<HaexExtensionDialogRemove
@ -130,6 +130,7 @@ definePageMeta({
const { t } = useI18n()
const extensionStore = useExtensionsStore()
const desktopStore = useDesktopStore()
const showConfirmation = ref(false)
const openOverwriteDialog = ref(false)
@ -388,18 +389,23 @@ const onSelectExtensionAsync = async () => {
}
}
const addExtensionAsync = async () => {
const addExtensionAsync = async (addToDesktop: boolean = false) => {
try {
console.log(
'preview.value?.editable_permissions',
preview.value?.editable_permissions,
)
await extensionStore.installAsync(
const extensionId = await extensionStore.installAsync(
extension.path,
preview.value?.editable_permissions,
)
await extensionStore.loadExtensionsAsync()
// Add to desktop if requested
if (addToDesktop && extensionId) {
await desktopStore.addDesktopItemAsync('extension', extensionId, 50, 50)
}
add({
color: 'success',
title: t('extension.success.title', {

View File

@ -1,11 +1,6 @@
<template>
<div class="w-full">
<div class="h-screen bg-amber-300 flex-1 flex-wrap">
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
</div>
<div class="h-screen bg-teal-300 flex-1">
abbbbbbbbbbbbbbbbbbbbb availableThemes:{{ uiStore.availableThemes }}
</div>
<div class="w-full h-full flex items-center justify-center">
<HaexDesktop />
</div>
</template>
@ -13,6 +8,4 @@
definePageMeta({
name: 'vaultOverview',
})
const uiStore = useUiStore()
</script>

View File

@ -57,14 +57,14 @@ function interceptConsole(level: 'log' | 'info' | 'warn' | 'error' | 'debug') {
}
export default defineNuxtPlugin(() => {
// Install console interceptors immediately
interceptConsole('log')
interceptConsole('info')
interceptConsole('warn')
interceptConsole('error')
interceptConsole('debug')
// TEMPORARILY DISABLED - Console interceptor causes too many logs
// interceptConsole('log')
// interceptConsole('info')
// interceptConsole('warn')
// interceptConsole('error')
// interceptConsole('debug')
console.log('[HaexHub] Global console interceptor installed')
// console.log('[HaexHub] Global console interceptor installed')
return {
provide: {

170
src/stores/vault/desktop.ts Normal file
View File

@ -0,0 +1,170 @@
import { eq } from 'drizzle-orm'
import { haexDesktopItems } from '~~/src-tauri/database/schemas'
import type {
InsertHaexDesktopItems,
SelectHaexDesktopItems,
} from '~~/src-tauri/database/schemas'
export type DesktopItemType = 'extension' | 'file' | 'folder'
export interface IDesktopItem extends SelectHaexDesktopItems {
label?: string
icon?: string
}
export const useDesktopStore = defineStore('desktopStore', () => {
const { currentVault } = storeToRefs(useVaultStore())
const desktopItems = ref<IDesktopItem[]>([])
const loadDesktopItemsAsync = async () => {
if (!currentVault.value?.drizzle) {
console.error('Kein Vault geöffnet')
return
}
try {
const items = await currentVault.value.drizzle
.select()
.from(haexDesktopItems)
desktopItems.value = items
} catch (error) {
console.error('Fehler beim Laden der Desktop-Items:', error)
throw error
}
}
const addDesktopItemAsync = async (
itemType: DesktopItemType,
referenceId: string,
positionX: number = 0,
positionY: number = 0,
) => {
if (!currentVault.value?.drizzle) {
throw new Error('Kein Vault geöffnet')
}
try {
const newItem: InsertHaexDesktopItems = {
itemType: itemType,
referenceId: referenceId,
positionX: positionX,
positionY: positionY,
}
const result = await currentVault.value.drizzle
.insert(haexDesktopItems)
.values(newItem)
.returning()
if (result.length > 0 && result[0]) {
desktopItems.value.push(result[0])
return result[0]
}
} catch (error) {
console.error('Fehler beim Hinzufügen des Desktop-Items:', error)
throw error
}
}
const updateDesktopItemPositionAsync = async (
id: string,
positionX: number,
positionY: number,
) => {
if (!currentVault.value?.drizzle) {
throw new Error('Kein Vault geöffnet')
}
try {
const result = await currentVault.value.drizzle
.update(haexDesktopItems)
.set({
positionX: positionX,
positionY: positionY,
})
.where(eq(haexDesktopItems.id, id))
.returning()
if (result.length > 0 && result[0]) {
const index = desktopItems.value.findIndex((item) => item.id === id)
if (index !== -1) {
desktopItems.value[index] = result[0]
}
}
} catch (error) {
console.error('Fehler beim Aktualisieren der Position:', error)
throw error
}
}
const removeDesktopItemAsync = async (id: string) => {
if (!currentVault.value?.drizzle) {
throw new Error('Kein Vault geöffnet')
}
try {
// Soft delete using haexTombstone
await currentVault.value.drizzle
.delete(haexDesktopItems)
.where(eq(haexDesktopItems.id, id))
desktopItems.value = desktopItems.value.filter((item) => item.id !== id)
} catch (error) {
console.error('Fehler beim Entfernen des Desktop-Items:', error)
throw error
}
}
const getDesktopItemByReference = (
itemType: DesktopItemType,
referenceId: string,
) => {
return desktopItems.value.find(
(item) => item.itemType === itemType && item.referenceId === referenceId,
)
}
const getContextMenuItems = (
id: string,
itemType: DesktopItemType,
referenceId: string,
onOpen: () => void,
onUninstall: () => void,
) => {
return [
[
{
label: 'Öffnen',
icon: 'i-heroicons-arrow-top-right-on-square',
click: onOpen,
},
],
[
{
label: 'Von Desktop entfernen',
icon: 'i-heroicons-x-mark',
click: async () => {
await removeDesktopItemAsync(id)
},
},
{
label: 'Deinstallieren',
icon: 'i-heroicons-trash',
click: onUninstall,
},
],
]
}
return {
desktopItems,
loadDesktopItemsAsync,
addDesktopItemAsync,
updateDesktopItemPositionAsync,
removeDesktopItemAsync,
getDesktopItemByReference,
getContextMenuItems,
}
})

View File

@ -51,11 +51,12 @@ export const useDeviceStore = defineStore('vaultInstanceStore', () => {
const readDeviceNameAsync = async (id?: string) => {
const { readDeviceNameAsync } = useVaultSettingsStore()
const _id = id ?? deviceId.value
const _id = id || deviceId.value
console.log('readDeviceNameAsync id', _id)
if (!_id) return
deviceName.value = (await readDeviceNameAsync(_id))?.value ?? ''
return deviceName.value
}

View File

@ -3,7 +3,10 @@
import { drizzle } from 'drizzle-orm/sqlite-proxy'
import { invoke } from '@tauri-apps/api/core'
import { schema } from '@/../src-tauri/database/index'
import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'
import type {
AsyncRemoteCallback,
SqliteRemoteDatabase,
} from 'drizzle-orm/sqlite-proxy'
interface IVault {
name: string
@ -55,40 +58,10 @@ export const useVaultStore = defineStore('vaultStore', () => {
...openVaults.value,
[vaultId]: {
name: fileName,
drizzle: drizzle<typeof schema>(
async (sql, params: unknown[], method) => {
let rows: any[] = []
let results: any[] = []
// If the query is a SELECT, use the select method
if (isSelectQuery(sql)) {
console.log('sql_select', sql, params, method)
rows = await invoke<unknown[]>('sql_select', {
sql,
params,
}).catch((e) => {
console.error('SQL select Error:', e, sql, params)
return []
})
} else {
console.log('sql_execute', sql, params, method)
// Otherwise, use the execute method
rows = await invoke<unknown[]>('sql_execute', {
sql,
params,
}).catch((e) => {
console.error('SQL execute Error:', e, sql, params)
return []
})
return { rows: [] }
}
results = method === 'all' ? rows : rows[0]
return { rows: results }
},
{ schema: schema, logger: true },
),
drizzle: drizzle<typeof schema>(drizzleCallback, {
schema: schema,
logger: false,
}),
},
}
@ -141,7 +114,34 @@ const getVaultIdAsync = async (path: string) => {
}
const isSelectQuery = (sql: string) => {
console.log('check isSelectQuery', sql)
const selectRegex = /^\s*SELECT\b/i
return selectRegex.test(sql)
}
const drizzleCallback = (async (
sql: string,
params: unknown[],
method: 'get' | 'run' | 'all' | 'values',
) => {
let rows: unknown[] = []
if (isSelectQuery(sql)) {
rows = await invoke<unknown[]>('sql_select', { sql, params }).catch((e) => {
console.error('SQL select Error:', e, sql, params)
return []
})
} else {
rows = await invoke<unknown[]>('sql_execute', { sql, params }).catch(
(e) => {
console.error('SQL execute Error:', e, sql, params)
return []
},
)
}
if (method === 'get') {
return { rows: rows.length > 0 ? [rows[0]] : [] }
} else {
return { rows }
}
}) satisfies AsyncRemoteCallback

View File

@ -128,7 +128,7 @@ export const useVaultSettingsStore = defineStore('vaultSettingsStore', () => {
eq(schema.haexSettings.key, id),
),
})
console.log('readDeviceNameAsync', deviceName)
console.log('store: readDeviceNameAsync', deviceName)
return deviceName
}