add MusicPlayer

This commit is contained in:
Timi
2026-04-09 20:53:46 +08:00
parent 788db69bc8
commit d9e32c4dbe
12 changed files with 1059 additions and 14 deletions

View File

@@ -9,7 +9,7 @@
<div aria-hidden="true" class="spacer top"></div>
</template>
<template #default="{ item }">
<t-swipe-cell>
<t-swipe-cell :disabled="!isSwipeActionVisible(item)" :left="resolveLeftActions(item)">
<t-cell
class="cell"
:title="item.name"
@@ -35,17 +35,52 @@
</template>
<script setup lang="ts">
import { h, resolveComponent } from "vue";
import { RecycleScroller } from "vue-virtual-scroller";
import { type ExplorerItem } from "./fileExplorer.shared";
import { isBrowserSupportedAudio } from "./fileAudio.shared";
const props = defineProps<{
items: ExplorerItem[];
pendingPath?: string;
}>();
const emit = defineEmits<{
open: [item: ExplorerItem];
}>();
const emit = defineEmits(["open", "queue", "play"]);
function isSwipeActionVisible(item: ExplorerItem): boolean {
return isBrowserSupportedAudio(item);
}
function resolveLeftActions(item: ExplorerItem) {
if (!isSwipeActionVisible(item)) {
return undefined;
}
const buttonComponent = resolveComponent("t-button");
const iconComponent = resolveComponent("t-icon");
return () => h("div", { class: "actions" }, [
h(buttonComponent, {
class: "action-btn queue-action",
size: "small",
shape: "square",
theme: "primary",
variant: "outline",
onClick: () => handleQueue(item)
}, {
icon: () => h(iconComponent, { name: "queue" })
}),
h(buttonComponent, {
class: "action-btn play-action",
size: "small",
shape: "square",
theme: "primary",
onClick: () => handlePlay(item)
}, {
icon: () => h(iconComponent, { name: "play" })
})
]);
}
function handleOpen(item: ExplorerItem): void {
if (props.pendingPath) {
@@ -54,6 +89,22 @@ function handleOpen(item: ExplorerItem): void {
emit("open", item);
}
function handleQueue(item: ExplorerItem): void {
if (props.pendingPath) {
return;
}
emit("queue", item);
}
function handlePlay(item: ExplorerItem): void {
console.log("play", item);
if (props.pendingPath) {
return;
}
emit("play", item);
}
</script>
<style scoped lang="less">
@@ -113,6 +164,28 @@ function handleOpen(item: ExplorerItem): void {
color: var(--app-sub);
}
&:deep(.actions) {
gap: .5rem;
height: 100%;
display: flex;
padding: 0 .75rem;
align-items: center;
}
&:deep(.queue-action) {
background: rgba(255, 255, 255, .92);
}
&:deep(.play-action) {
background: var(--td-brand-color);
}
&:deep(.action-btn) {
width: 2.25rem;
height: 2.25rem;
border-radius: .75rem;
}
.cell {
&:deep(.t-cell__content) {

View File

@@ -6,6 +6,8 @@
:items="currentItems"
:pending-path="pendingFolderPath"
@open="openItem"
@queue="queueItem"
@play="playItem"
/>
<file-explorer-grid
v-else
@@ -44,12 +46,14 @@ const props = withDefaults(defineProps<{
useRoutePath: true
});
const emit = defineEmits<{
"update:path": [value: string[]];
"open-folder": [value: string[]];
"open-file": [value: ExplorerItem];
"update:mode": [value: DisplayMode];
}>();
const emit = defineEmits([
"update:path",
"open-folder",
"open-file",
"queue-file",
"play-file",
"update:mode"
]);
const route = useRoute();
const router = useRouter();
@@ -220,6 +224,15 @@ async function openItem(item: ExplorerItem): Promise<void> {
emit("open-file", item);
}
function queueItem(item: ExplorerItem): void {
emit("queue-file", item);
}
function playItem(item: ExplorerItem): void {
console.log("play-item", item);
emit("play-file", item);
}
async function syncPath(nextSegments: string[]): Promise<void> {
if (shouldUseRoutePath.value) {
if (!nextSegments.length) {

View File

@@ -0,0 +1,42 @@
import type { ExplorerItem } from "./fileExplorer.shared";
const audioMimeMap: Record<string, string[]> = {
mp3: ["audio/mpeg"],
mpeg: ["audio/mpeg"],
m4a: ["audio/mp4", "audio/x-m4a"],
mp4: ["audio/mp4"],
aac: ["audio/aac", "audio/mp4"],
ogg: ["audio/ogg", "audio/vorbis"],
oga: ["audio/ogg", "audio/vorbis"],
wav: ["audio/wav", "audio/wave", "audio/x-wav"],
webm: ["audio/webm"],
flac: ["audio/flac", "audio/x-flac"]
};
function resolveAudioExtension(item: ExplorerItem): string {
const rawExtension = item.extension || item.name.split(".").pop() || "";
return rawExtension.trim().toLowerCase();
}
export function isAudioExplorerItem(item: ExplorerItem): boolean {
return item.type === "file" && item.rawType === "AUDIO";
}
export function isBrowserSupportedAudio(item: ExplorerItem): boolean {
if (!isAudioExplorerItem(item)) {
return false;
}
const extension = resolveAudioExtension(item);
const mimeList = audioMimeMap[extension];
if (!mimeList?.length) {
return false;
}
if (typeof window === "undefined") {
return true;
}
const audio = document.createElement("audio");
return mimeList.some((mime) => !!audio.canPlayType(mime));
}

View File

@@ -17,6 +17,7 @@ export interface ServerFile {
export interface ExplorerItem {
name: string;
type: FileItemType;
extension?: string;
size?: string;
updatedAt: string;
icon: string;
@@ -55,6 +56,7 @@ export function mapServerFileToExplorerItem(file: ServerFile): ExplorerItem {
return {
name: file.name || "未命名",
type: isDirectory ? "dir" : "file",
extension: file.extension,
size: isDirectory ? undefined : formatFileSize(file.size),
updatedAt: formatModifiedAt(file.modifiedAt),
icon: resolveFileIcon(file, isDirectory),