376 lines
8.5 KiB
Vue
376 lines
8.5 KiB
Vue
<template>
|
|
<div
|
|
class="item"
|
|
role="button"
|
|
tabindex="0"
|
|
:draggable="isDraggable"
|
|
@dragstart="dragStart"
|
|
@dragover="dragOver"
|
|
@drop="drop"
|
|
@click="itemClick"
|
|
@mousedown="handleMouseDown"
|
|
@mouseup="handleMouseUp"
|
|
@mouseleave="handleMouseLeave"
|
|
@touchstart="handleTouchStart"
|
|
@touchend="handleTouchEnd"
|
|
@touchcancel="handleTouchCancel"
|
|
@touchmove="handleTouchMove"
|
|
:data-dir="isDir"
|
|
:data-type="type"
|
|
:aria-label="name"
|
|
:aria-selected="isSelected"
|
|
:data-ext="getExtension(name).toLowerCase()"
|
|
>
|
|
<div>
|
|
<img
|
|
v-if="!readOnly && type === 'image' && isThumbsEnabled"
|
|
v-lazy="thumbnailUrl"
|
|
/>
|
|
<i v-else class="material-icons"></i>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="name">{{ name }}</p>
|
|
|
|
<p v-if="isDir" class="size" data-order="-1">—</p>
|
|
<p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
|
|
|
|
<p class="modified">
|
|
<time :datetime="modified">{{ humanTime() }}</time>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useAuthStore } from "@/stores/auth";
|
|
import { useFileStore } from "@/stores/file";
|
|
import { useLayoutStore } from "@/stores/layout";
|
|
|
|
import { enableThumbs } from "@/utils/constants";
|
|
import { filesize } from "@/utils";
|
|
import dayjs from "dayjs";
|
|
import { files as api } from "@/api";
|
|
import * as upload from "@/utils/upload";
|
|
import { computed, inject, ref } from "vue";
|
|
import { useRouter } from "vue-router";
|
|
|
|
const touches = ref<number>(0);
|
|
|
|
const longPressTimer = ref<number | null>(null);
|
|
const longPressTriggered = ref<boolean>(false);
|
|
const longPressDelay = ref<number>(500);
|
|
const startPosition = ref<{ x: number; y: number } | null>(null);
|
|
const moveThreshold = ref<number>(10);
|
|
|
|
const $showError = inject<IToastError>("$showError")!;
|
|
const router = useRouter();
|
|
|
|
const props = defineProps<{
|
|
name: string;
|
|
isDir: boolean;
|
|
url: string;
|
|
type: string;
|
|
size: number;
|
|
modified: string;
|
|
index: number;
|
|
readOnly?: boolean;
|
|
path?: string;
|
|
}>();
|
|
|
|
const authStore = useAuthStore();
|
|
const fileStore = useFileStore();
|
|
const layoutStore = useLayoutStore();
|
|
|
|
const singleClick = computed(
|
|
() => !props.readOnly && authStore.user?.singleClick
|
|
);
|
|
const isSelected = computed(
|
|
() => fileStore.selected.indexOf(props.index) !== -1
|
|
);
|
|
const isDraggable = computed(
|
|
() => !props.readOnly && authStore.user?.perm.rename
|
|
);
|
|
|
|
const canDrop = computed(() => {
|
|
if (!props.isDir || props.readOnly) return false;
|
|
|
|
for (const i of fileStore.selected) {
|
|
if (fileStore.req?.items[i].url === props.url) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
const thumbnailUrl = computed(() => {
|
|
const file = {
|
|
path: props.path,
|
|
modified: props.modified,
|
|
};
|
|
|
|
return api.getPreviewURL(file as Resource, "thumb");
|
|
});
|
|
|
|
const isThumbsEnabled = computed(() => {
|
|
return enableThumbs;
|
|
});
|
|
|
|
const humanSize = () => {
|
|
return props.type == "invalid_link" ? "invalid link" : filesize(props.size);
|
|
};
|
|
|
|
const humanTime = () => {
|
|
if (!props.readOnly && authStore.user?.dateFormat) {
|
|
return dayjs(props.modified).format("L LT");
|
|
}
|
|
return dayjs(props.modified).fromNow();
|
|
};
|
|
|
|
const dragStart = () => {
|
|
if (fileStore.selectedCount === 0) {
|
|
fileStore.selected.push(props.index);
|
|
return;
|
|
}
|
|
|
|
if (!isSelected.value) {
|
|
fileStore.selected = [];
|
|
fileStore.selected.push(props.index);
|
|
}
|
|
};
|
|
|
|
const dragOver = (event: Event) => {
|
|
if (!canDrop.value) return;
|
|
|
|
event.preventDefault();
|
|
let el = event.target as HTMLElement | null;
|
|
if (el !== null) {
|
|
for (let i = 0; i < 5; i++) {
|
|
if (!el?.classList.contains("item")) {
|
|
el = el?.parentElement ?? null;
|
|
}
|
|
}
|
|
|
|
if (el !== null) el.style.opacity = "1";
|
|
}
|
|
};
|
|
|
|
const drop = async (event: Event) => {
|
|
if (!canDrop.value) return;
|
|
event.preventDefault();
|
|
|
|
if (fileStore.selectedCount === 0) return;
|
|
|
|
let el = event.target as HTMLElement | null;
|
|
for (let i = 0; i < 5; i++) {
|
|
if (el !== null && !el.classList.contains("item")) {
|
|
el = el.parentElement;
|
|
}
|
|
}
|
|
|
|
const items: any[] = [];
|
|
|
|
for (const i of fileStore.selected) {
|
|
if (fileStore.req) {
|
|
items.push({
|
|
from: fileStore.req?.items[i].url,
|
|
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
|
|
name: fileStore.req?.items[i].name,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Get url from ListingItem instance
|
|
if (el === null) {
|
|
return;
|
|
}
|
|
const path = el.__vue__.url;
|
|
const baseItems = (await api.fetch(path)).items;
|
|
|
|
const action = (overwrite: boolean, rename: boolean) => {
|
|
api
|
|
.move(items, overwrite, rename)
|
|
.then(() => {
|
|
fileStore.reload = true;
|
|
})
|
|
.catch($showError);
|
|
};
|
|
|
|
const conflict = upload.checkConflict(items, baseItems);
|
|
|
|
let overwrite = false;
|
|
let rename = false;
|
|
|
|
if (conflict) {
|
|
layoutStore.showHover({
|
|
prompt: "replace-rename",
|
|
confirm: (event: Event, option: any) => {
|
|
overwrite = option == "overwrite";
|
|
rename = option == "rename";
|
|
|
|
event.preventDefault();
|
|
layoutStore.closeHovers();
|
|
action(overwrite, rename);
|
|
},
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
action(overwrite, rename);
|
|
};
|
|
|
|
const itemClick = (event: Event | KeyboardEvent) => {
|
|
// If long press was triggered, prevent normal click behavior
|
|
if (longPressTriggered.value) {
|
|
longPressTriggered.value = false;
|
|
return;
|
|
}
|
|
|
|
if (
|
|
singleClick.value &&
|
|
!(event as KeyboardEvent).ctrlKey &&
|
|
!(event as KeyboardEvent).metaKey &&
|
|
!(event as KeyboardEvent).shiftKey &&
|
|
!fileStore.multiple
|
|
)
|
|
open();
|
|
else click(event);
|
|
};
|
|
|
|
const click = (event: Event | KeyboardEvent) => {
|
|
if (!singleClick.value && fileStore.selectedCount !== 0)
|
|
event.preventDefault();
|
|
|
|
setTimeout(() => {
|
|
touches.value = 0;
|
|
}, 300);
|
|
|
|
touches.value++;
|
|
if (touches.value > 1) {
|
|
open();
|
|
}
|
|
|
|
if (fileStore.selected.indexOf(props.index) !== -1) {
|
|
fileStore.removeSelected(props.index);
|
|
return;
|
|
}
|
|
|
|
if ((event as KeyboardEvent).shiftKey && fileStore.selected.length > 0) {
|
|
let fi = 0;
|
|
let la = 0;
|
|
|
|
if (props.index > fileStore.selected[0]) {
|
|
fi = fileStore.selected[0] + 1;
|
|
la = props.index;
|
|
} else {
|
|
fi = props.index;
|
|
la = fileStore.selected[0] - 1;
|
|
}
|
|
|
|
for (; fi <= la; fi++) {
|
|
if (fileStore.selected.indexOf(fi) == -1) {
|
|
fileStore.selected.push(fi);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!singleClick.value &&
|
|
!(event as KeyboardEvent).ctrlKey &&
|
|
!(event as KeyboardEvent).metaKey &&
|
|
!fileStore.multiple
|
|
) {
|
|
fileStore.selected = [];
|
|
}
|
|
fileStore.selected.push(props.index);
|
|
};
|
|
|
|
const open = () => {
|
|
router.push({ path: props.url });
|
|
};
|
|
|
|
const getExtension = (fileName: string): string => {
|
|
const lastDotIndex = fileName.lastIndexOf(".");
|
|
if (lastDotIndex === -1) {
|
|
return fileName;
|
|
}
|
|
return fileName.substring(lastDotIndex);
|
|
};
|
|
|
|
// Long-press helper functions
|
|
const startLongPress = (clientX: number, clientY: number) => {
|
|
startPosition.value = { x: clientX, y: clientY };
|
|
longPressTimer.value = window.setTimeout(() => {
|
|
handleLongPress();
|
|
}, longPressDelay.value);
|
|
};
|
|
|
|
const cancelLongPress = () => {
|
|
if (longPressTimer.value !== null) {
|
|
window.clearTimeout(longPressTimer.value);
|
|
longPressTimer.value = null;
|
|
}
|
|
startPosition.value = null;
|
|
};
|
|
|
|
const handleLongPress = () => {
|
|
if (singleClick.value) {
|
|
longPressTriggered.value = true;
|
|
click(new Event("longpress"));
|
|
}
|
|
cancelLongPress();
|
|
};
|
|
|
|
const checkMovement = (clientX: number, clientY: number): boolean => {
|
|
if (!startPosition.value) return false;
|
|
|
|
const deltaX = Math.abs(clientX - startPosition.value.x);
|
|
const deltaY = Math.abs(clientY - startPosition.value.y);
|
|
|
|
return deltaX > moveThreshold.value || deltaY > moveThreshold.value;
|
|
};
|
|
|
|
// Event handlers
|
|
const handleMouseDown = (event: MouseEvent) => {
|
|
if (event.button === 0) {
|
|
startLongPress(event.clientX, event.clientY);
|
|
}
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
cancelLongPress();
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
cancelLongPress();
|
|
};
|
|
|
|
const handleTouchStart = (event: TouchEvent) => {
|
|
if (event.touches.length === 1) {
|
|
const touch = event.touches[0];
|
|
startLongPress(touch.clientX, touch.clientY);
|
|
}
|
|
};
|
|
|
|
const handleTouchEnd = () => {
|
|
cancelLongPress();
|
|
};
|
|
|
|
const handleTouchCancel = () => {
|
|
cancelLongPress();
|
|
};
|
|
|
|
const handleTouchMove = (event: TouchEvent) => {
|
|
if (event.touches.length === 1 && startPosition.value) {
|
|
const touch = event.touches[0];
|
|
if (checkMovement(touch.clientX, touch.clientY)) {
|
|
cancelLongPress();
|
|
}
|
|
}
|
|
};
|
|
</script>
|