mirror of
https://github.com/haexhub/haex-hub.git
synced 2025-12-17 06:30:50 +01:00
add desktop
This commit is contained in:
119
src/components/haex/desktop/icon.vue
Normal file
119
src/components/haex/desktop/icon.vue
Normal 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>
|
||||
250
src/components/haex/desktop/index.vue
Normal file
250
src/components/haex/desktop/index.vue
Normal 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>
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user