Files
gitlab-instance-0a899031_fi…/frontend/src/components/files/ListingItem.vue
2025-08-28 19:49:01 +08:00

449 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">&mdash;</p>
<p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
<p class="modified">
<time :datetime="modified">{{ humanTime() }}</time>
<!-- 直播按钮 -->
<button v-if="!isDir && isVideoFile(name)" class="live-button" @click="startLive">
开始直播
</button>
<button v-if="!isDir && isVideoFile(name)" class="live-button" @click="stopLive">
停止直播
</button>
</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();
}
}
};
const videoExtensions = ["mp4", "mov", "mkv", "flv", "avi", "webm"];
const getExtension2 = (fileName: string): string => {
const idx = fileName.lastIndexOf(".");
return idx === -1 ? "" : fileName.slice(idx + 1).toLowerCase();
};
const isVideoFile = (fileName: string) => {
return videoExtensions.includes(getExtension2(fileName));
};
// 假设 props.url 是文件路径,例如: "file/xxx.mp4"
const stopLive = async () => {
try {
// 生成 thread_id需要和 start_live 保持一致
const thread_id = props.url.replace(/\//g, "_");
// 调用 FastAPI 停止接口
const res = await fetch("http://live.local:8700/stop_live", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ thread_id }) // 如果想停止全部可以 body: {}
});
const data = await res.json();
if (data.status === "ok") {
alert(`直播已停止: ${props.url}`);
} else {
alert(`停止失败: ${data.message}`);
}
} catch (err) {
console.error(err);
alert("停止直播接口调用失败,请检查后端服务是否启动");
}
};
const startLive = async () => {
try {
// props.url 是 FileBrowser 内部路径,例如 /videos/test.mp4
const file_path = props.url;
// const file_path = encodeURIComponent(props.url);
console.log('file_path',file_path)
// 调用 FastAPI 后端接口
const res = await fetch("http://live.local:8700/start_live", {
// const res = await fetch("http://192.168.31.245:8700/start_live", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: file_path })
});
const data = await res.json();
if (data.status === "ok") {
alert(`直播已开始: ${file_path}`);
} else {
alert(`直播失败: ${data.msg}`);
}
} catch (err) {
console.error(err);
alert("直播接口调用失败,请检查后端服务是否启动");
}
};
</script>
<style scoped>
.live-button {
margin-left: 10px;
display: inline-block;
padding: 4px 12px;
font-size: 14px;
line-height: 1.5;
border-radius: 4px;
background-color: green;
color: #fff;
border: none;
cursor: pointer;
white-space: nowrap;
/* 防止换行 */
}
</style>