Initial Commit
This commit is contained in:
203
src/components/FileContextMenu.vue
Normal file
203
src/components/FileContextMenu.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useApp } from '@/composables/useApp'
|
||||
import {
|
||||
FolderOpen,
|
||||
Download,
|
||||
FileArchive,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Eye,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
itemPath: string
|
||||
itemName: string
|
||||
itemType: 'file' | 'directory'
|
||||
mimeType: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'rename'): void
|
||||
(e: 'preview'): void
|
||||
(e: 'download'): void
|
||||
(e: 'downloadZip'): void
|
||||
(e: 'delete'): void
|
||||
(e: 'open'): void
|
||||
}>()
|
||||
|
||||
const app = useApp()
|
||||
const router = useRouter()
|
||||
|
||||
const menuOpen = ref(false)
|
||||
const menuX = ref(0)
|
||||
const menuY = ref(0)
|
||||
|
||||
const longPressTimer = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
const didLongPress = ref(false)
|
||||
const touchMoved = ref(false)
|
||||
|
||||
function canPreview() {
|
||||
return props.mimeType.startsWith('image/') || props.mimeType.startsWith('video/') || props.mimeType.startsWith('audio/') || props.mimeType === 'application/pdf'
|
||||
}
|
||||
|
||||
function showMenu(event: MouseEvent) {
|
||||
const wrapper = menuWrapper.value
|
||||
if (wrapper) {
|
||||
const rect = wrapper.getBoundingClientRect()
|
||||
menuX.value = event.clientX || rect.left
|
||||
menuY.value = event.clientY || rect.bottom
|
||||
} else {
|
||||
menuX.value = event.clientX || 0
|
||||
menuY.value = event.clientY || 0
|
||||
}
|
||||
menuOpen.value = true
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', closeMenu, { once: true })
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menuOpen.value = false
|
||||
document.removeEventListener('click', closeMenu)
|
||||
}
|
||||
|
||||
function doAction(fn: () => void) {
|
||||
closeMenu()
|
||||
fn()
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (didLongPress.value) {
|
||||
didLongPress.value = false
|
||||
return
|
||||
}
|
||||
emit('open')
|
||||
}
|
||||
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
showMenu(e)
|
||||
}
|
||||
|
||||
function handleTouchStart(e: TouchEvent) {
|
||||
touchMoved.value = false
|
||||
didLongPress.value = false
|
||||
longPressTimer.value = setTimeout(() => {
|
||||
didLongPress.value = true
|
||||
const touch = e.touches[0] || e.changedTouches[0]
|
||||
showMenu(new MouseEvent('contextmenu', { clientX: touch.clientX, clientY: touch.clientY }))
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function handleTouchMove() {
|
||||
touchMoved.value = true
|
||||
if (longPressTimer.value) {
|
||||
clearTimeout(longPressTimer.value)
|
||||
longPressTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchEnd(e: TouchEvent) {
|
||||
if (longPressTimer.value) {
|
||||
clearTimeout(longPressTimer.value)
|
||||
longPressTimer.value = null
|
||||
}
|
||||
if (!touchMoved.value && !didLongPress.value) {
|
||||
e.preventDefault()
|
||||
emit('open')
|
||||
}
|
||||
}
|
||||
|
||||
function clampX(x: number) {
|
||||
return Math.min(x, typeof window !== 'undefined' ? window.innerWidth - 190 : 200)
|
||||
}
|
||||
function clampY(y: number) {
|
||||
return Math.min(y, typeof window !== 'undefined' ? window.innerHeight - 250 : 200)
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (longPressTimer.value) clearTimeout(longPressTimer.value)
|
||||
})
|
||||
|
||||
const menuWrapper = ref<HTMLElement>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="menuWrapper"
|
||||
@click="handleClick"
|
||||
@contextmenu="handleContextMenu"
|
||||
@touchstart.passive="handleTouchStart"
|
||||
@touchmove.passive="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="menuOpen"
|
||||
class="fixed z-[100] min-w-[180px] bg-card border border-border rounded-lg shadow-xl py-1"
|
||||
:style="{ left: clampX(menuX) + 'px', top: clampY(menuY) + 'px' }"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
v-if="itemType === 'directory'"
|
||||
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm hover:bg-muted/70 transition-colors text-left"
|
||||
@click="doAction(() => router.push({ query: { path: itemPath } }))"
|
||||
>
|
||||
<FolderOpen class="size-3.5 shrink-0" />
|
||||
Open
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="itemType === 'directory'"
|
||||
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm hover:bg-muted/70 transition-colors text-left"
|
||||
@click="doAction(() => emit('downloadZip'))"
|
||||
>
|
||||
<FileArchive class="size-3.5 shrink-0" />
|
||||
Download as ZIP
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-else
|
||||
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm hover:bg-muted/70 transition-colors text-left"
|
||||
@click="doAction(() => emit('download'))"
|
||||
>
|
||||
<Download class="size-3.5 shrink-0" />
|
||||
Download
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="itemType === 'file' && canPreview()"
|
||||
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm hover:bg-muted/70 transition-colors text-left"
|
||||
@click="doAction(() => emit('preview'))"
|
||||
>
|
||||
<Eye class="size-3.5 shrink-0" />
|
||||
Preview
|
||||
</button>
|
||||
|
||||
<div class="h-px bg-border my-0.5 mx-2" />
|
||||
|
||||
<button
|
||||
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm hover:bg-muted/70 transition-colors text-left"
|
||||
@click="doAction(() => emit('rename'))"
|
||||
>
|
||||
<Pencil class="size-3.5 shrink-0" />
|
||||
Rename
|
||||
</button>
|
||||
|
||||
<div class="h-px bg-border my-0.5 mx-2" />
|
||||
|
||||
<button
|
||||
class="w-full flex items-center gap-2.5 px-3 py-2 text-sm hover:bg-destructive/10 text-destructive transition-colors text-left"
|
||||
@click="doAction(() => emit('delete'))"
|
||||
>
|
||||
<Trash2 class="size-3.5 shrink-0" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
Reference in New Issue
Block a user