This commit is contained in:
eric
2025-08-28 13:06:53 +08:00
parent 620ba326a9
commit 1b98ac6e72
350 changed files with 154034 additions and 0 deletions

View File

@@ -0,0 +1,448 @@
<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://192.168.31.245: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>