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