fix: 修复镜像列表滚动条问题 #238

This commit is contained in:
mofeng-git
2026-04-11 13:13:24 +08:00
parent c3a3f41a2c
commit 3e35181583
2 changed files with 282 additions and 228 deletions

View File

@@ -28,7 +28,6 @@ import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { ScrollArea } from '@/components/ui/scroll-area'
import { import {
HardDrive, HardDrive,
Upload, Upload,
@@ -512,7 +511,7 @@ onUnmounted(() => {
<TooltipProvider> <TooltipProvider>
<Dialog :open="open" @update:open="emit('update:open', $event)"> <Dialog :open="open" @update:open="emit('update:open', $event)">
<DialogContent class="sm:max-w-[600px] max-h-[90vh] overflow-hidden flex flex-col p-0"> <DialogContent class="sm:max-w-[600px] max-h-[90vh] overflow-hidden flex flex-col p-0">
<DialogHeader class="px-6 pt-6"> <DialogHeader class="px-6 pt-6 shrink-0">
<DialogTitle class="flex items-center gap-2"> <DialogTitle class="flex items-center gap-2">
<HardDrive class="h-5 w-5" /> <HardDrive class="h-5 w-5" />
{{ t('msd.title') }} {{ t('msd.title') }}
@@ -551,10 +550,11 @@ onUnmounted(() => {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Separator /> <Separator class="shrink-0" />
<Tabs v-model="activeTab" class="flex-1 flex flex-col overflow-hidden px-6 pb-6 pt-4"> <div class="flex-1 min-h-0 flex flex-col px-6 pb-6 pt-4">
<TabsList class="w-full grid grid-cols-2"> <Tabs v-model="activeTab" class="flex-1 flex flex-col min-h-0">
<TabsList class="w-full grid grid-cols-2 shrink-0">
<TabsTrigger value="images"> <TabsTrigger value="images">
<Disc class="h-4 w-4 mr-1.5" /> <Disc class="h-4 w-4 mr-1.5" />
{{ t('msd.images') }} {{ t('msd.images') }}
@@ -566,15 +566,13 @@ onUnmounted(() => {
</TabsList> </TabsList>
<!-- Tab Description --> <!-- Tab Description -->
<p class="text-xs text-muted-foreground mt-2 mb-1"> <p class="text-xs text-muted-foreground mt-2 mb-1 shrink-0">
{{ activeTab === 'images' ? t('msd.imagesDesc') : t('msd.driveDesc') }} {{ activeTab === 'images' ? t('msd.imagesDesc') : t('msd.driveDesc') }}
</p> </p>
<ScrollArea class="flex-1 mt-2"> <TabsContent value="images" class="flex-1 min-h-0 m-0 flex flex-col space-y-3">
<!-- Images Tab -->
<TabsContent value="images" class="m-0 space-y-3 pr-4">
<!-- Compact Upload Toolbar --> <!-- Compact Upload Toolbar -->
<div class="flex items-center gap-2 min-w-0"> <div class="shrink-0 flex items-center gap-2 min-w-0">
<label class="flex-1"> <label class="flex-1">
<input <input
type="file" type="file"
@@ -598,10 +596,10 @@ onUnmounted(() => {
{{ t('msd.downloadFromUrl') }} {{ t('msd.downloadFromUrl') }}
</Button> </Button>
</div> </div>
<Progress v-if="uploading" :model-value="uploadProgress" class="h-1" /> <Progress v-if="uploading" :model-value="uploadProgress" class="h-1 shrink-0" />
<!-- Options - Vertical compact layout --> <!-- Options - Vertical compact layout -->
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 p-2 rounded-lg bg-muted/50 text-xs min-w-0"> <div class="shrink-0 flex flex-wrap items-center gap-x-4 gap-y-2 p-2 rounded-lg bg-muted/50 text-xs min-w-0">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span class="text-muted-foreground whitespace-nowrap">{{ t('msd.storageMode') }}:</span> <span class="text-muted-foreground whitespace-nowrap">{{ t('msd.storageMode') }}:</span>
<HelpTooltip :content="mountMode === 'flash' ? t('help.flashMode') : t('help.cdromMode')" icon-size="sm" /> <HelpTooltip :content="mountMode === 'flash' ? t('help.flashMode') : t('help.cdromMode')" icon-size="sm" />
@@ -629,99 +627,101 @@ onUnmounted(() => {
</div> </div>
<!-- Image List --> <!-- Image List -->
<div class="space-y-2 min-w-0"> <div class="flex-1 min-h-0 flex flex-col space-y-2 min-w-0">
<div class="flex items-center justify-between"> <div class="shrink-0 flex items-center justify-between">
<h4 class="text-sm font-medium">{{ t('msd.imageList') }}</h4> <h4 class="text-sm font-medium">{{ t('msd.imageList') }}</h4>
<Button variant="ghost" size="icon" class="h-7 w-7" @click="loadImages"> <Button variant="ghost" size="icon" class="h-7 w-7" @click="loadImages">
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loadingImages }" /> <RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loadingImages }" />
</Button> </Button>
</div> </div>
<div v-if="images.length === 0" class="text-center py-6 text-muted-foreground text-sm"> <div v-if="images.length === 0" class="shrink-0 text-center py-6 text-muted-foreground text-sm">
{{ t('msd.noImages') }} {{ t('msd.noImages') }}
</div> </div>
<div v-else class="space-y-2"> <div v-else class="flex-1 min-h-0 overflow-y-auto pr-2 custom-scrollbar">
<div <div class="space-y-2">
v-for="image in images" <div
:key="image.id" v-for="image in images"
class="p-3 rounded-lg border transition-colors" :key="image.id"
:class="[ class="p-3 rounded-lg border transition-colors"
msdConnected && systemStore.msd?.imageId === image.id :class="[
? 'border-primary bg-primary/5' msdConnected && systemStore.msd?.imageId === image.id
: 'hover:bg-accent/50' ? 'border-primary bg-primary/5'
]" : 'hover:bg-accent/50'
> ]"
<div class="flex items-start justify-between gap-2"> >
<div class="flex items-start gap-2 w-0 flex-1"> <div class="flex items-start justify-between gap-2">
<Disc class="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" /> <div class="flex items-start gap-2 w-0 flex-1">
<div class="w-0 flex-1"> <Disc class="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
<Tooltip> <div class="w-0 flex-1">
<TooltipTrigger as-child> <Tooltip>
<p class="text-sm font-medium cursor-help overflow-hidden text-ellipsis whitespace-nowrap">{{ image.name }}</p>
</TooltipTrigger>
<TooltipContent>
<p class="max-w-sm break-all">{{ image.name }}</p>
</TooltipContent>
</Tooltip>
<div class="flex items-center gap-2 mt-0.5 flex-wrap">
<span class="text-xs text-muted-foreground">{{ formatBytes(image.size) }}</span>
<Tooltip v-if="isLargeFile(image)">
<TooltipTrigger as-child> <TooltipTrigger as-child>
<Badge <p class="text-sm font-medium cursor-help overflow-hidden text-ellipsis whitespace-nowrap">{{ image.name }}</p>
variant="outline"
class="text-[10px] h-4 px-1.5 border-amber-500/50 text-amber-600 dark:text-amber-400 cursor-help"
>
<AlertCircle class="h-2.5 w-2.5 mr-0.5" />
{{ t('msd.largeFileWarning') }}
</Badge>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{{ t('msd.largeFileTooltip') }}</p> <p class="max-w-sm break-all">{{ image.name }}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<div class="flex items-center gap-2 mt-0.5 flex-wrap">
<span class="text-xs text-muted-foreground">{{ formatBytes(image.size) }}</span>
<Tooltip v-if="isLargeFile(image)">
<TooltipTrigger as-child>
<Badge
variant="outline"
class="text-[10px] h-4 px-1.5 border-amber-500/50 text-amber-600 dark:text-amber-400 cursor-help"
>
<AlertCircle class="h-2.5 w-2.5 mr-0.5" />
{{ t('msd.largeFileWarning') }}
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('msd.largeFileTooltip') }}</p>
</TooltipContent>
</Tooltip>
</div>
</div> </div>
</div> </div>
</div> <div class="flex items-center gap-1.5 shrink-0">
<div class="flex items-center gap-1.5 shrink-0"> <template v-if="msdConnected && systemStore.msd?.imageId === image.id">
<template v-if="msdConnected && systemStore.msd?.imageId === image.id"> <Badge variant="default" class="text-xs h-7 px-2">
<Badge variant="default" class="text-xs h-7 px-2"> <span class="relative flex h-1.5 w-1.5 mr-1.5">
<span class="relative flex h-1.5 w-1.5 mr-1.5"> <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span> <span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-white"></span>
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-white"></span> </span>
</span> {{ t('common.connected') }}
{{ t('common.connected') }} </Badge>
</Badge> </template>
</template> <template v-else>
<template v-else> <Button
variant="default"
size="sm"
class="h-7 text-xs"
:disabled="operationInProgress"
@click="connectImage(image)"
>
<Link v-if="!connecting" class="h-3.5 w-3.5 mr-1" />
<span v-if="connecting">{{ t('common.connecting') }}...</span>
<span v-else>{{ t('msd.connect') }}</span>
</Button>
</template>
<Button <Button
variant="default" variant="ghost"
size="sm" size="icon"
class="h-7 text-xs" class="h-7 w-7 text-destructive hover:text-destructive"
:disabled="operationInProgress" :disabled="operationInProgress || (msdConnected && systemStore.msd?.imageId === image.id)"
@click="connectImage(image)" @click="confirmDelete('image', image.id, image.name)"
> >
<Link v-if="!connecting" class="h-3.5 w-3.5 mr-1" /> <Trash2 class="h-3.5 w-3.5" />
<span v-if="connecting">{{ t('common.connecting') }}...</span>
<span v-else>{{ t('msd.connect') }}</span>
</Button> </Button>
</template> </div>
<Button
variant="ghost"
size="icon"
class="h-7 w-7 text-destructive hover:text-destructive"
:disabled="operationInProgress || (msdConnected && systemStore.msd?.imageId === image.id)"
@click="confirmDelete('image', image.id, image.name)"
>
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- System Storage Footer --> <!-- System Storage Footer -->
<div v-if="systemStore.diskSpace" class="pt-2 border-t mt-2"> <div v-if="systemStore.diskSpace" class="shrink-0 pt-2 border-t mt-2">
<p class="text-[11px] text-muted-foreground text-center"> <p class="text-[11px] text-muted-foreground text-center">
{{ t('msd.systemAvailable') }}: {{ formatBytes(systemStore.diskSpace.available) }} {{ t('msd.systemAvailable') }}: {{ formatBytes(systemStore.diskSpace.available) }}
</p> </p>
@@ -729,10 +729,9 @@ onUnmounted(() => {
</div> </div>
</TabsContent> </TabsContent>
<!-- Drive Tab --> <TabsContent value="drive" class="flex-1 min-h-0 m-0 flex flex-col space-y-4">
<TabsContent value="drive" class="m-0 space-y-4 pr-4">
<template v-if="!driveInitialized"> <template v-if="!driveInitialized">
<div class="text-center py-8 space-y-4"> <div class="shrink-0 text-center py-8 space-y-4">
<HardDrive class="h-10 w-10 mx-auto text-muted-foreground" /> <HardDrive class="h-10 w-10 mx-auto text-muted-foreground" />
<p class="text-sm text-muted-foreground">{{ t('msd.driveNotInitialized') }}</p> <p class="text-sm text-muted-foreground">{{ t('msd.driveNotInitialized') }}</p>
<Button size="sm" @click="initializeDrive"> <Button size="sm" @click="initializeDrive">
@@ -743,7 +742,7 @@ onUnmounted(() => {
<template v-else> <template v-else>
<!-- Drive Info Card --> <!-- Drive Info Card -->
<div class="p-3 rounded-lg border space-y-3" :class="msdConnected && msdMode === 'drive' ? 'border-primary bg-primary/5' : 'bg-muted/50'"> <div class="shrink-0 p-3 rounded-lg border space-y-3" :class="msdConnected && msdMode === 'drive' ? 'border-primary bg-primary/5' : 'bg-muted/50'">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<HardDrive class="h-4 w-4 text-muted-foreground" /> <HardDrive class="h-4 w-4 text-muted-foreground" />
@@ -801,9 +800,9 @@ onUnmounted(() => {
</div> </div>
<!-- File Browser --> <!-- File Browser -->
<div class="space-y-2"> <div class="flex-1 min-h-0 flex flex-col space-y-2">
<!-- Toolbar --> <!-- Toolbar -->
<div class="flex items-center justify-between gap-2"> <div class="shrink-0 flex items-center justify-between gap-2">
<div class="flex items-center gap-1 min-w-0 flex-1"> <div class="flex items-center gap-1 min-w-0 flex-1">
<Button <Button
v-if="currentPath !== '/'" v-if="currentPath !== '/'"
@@ -827,7 +826,7 @@ onUnmounted(() => {
</template> </template>
</nav> </nav>
</div> </div>
<div class="flex items-center gap-1 shrink-0"> <div class="shrink-0 flex items-center gap-1 shrink-0">
<label> <label>
<input type="file" class="hidden" :disabled="uploadingFile" @change="handleFileUpload" /> <input type="file" class="hidden" :disabled="uploadingFile" @change="handleFileUpload" />
<Button variant="ghost" size="icon" as="span" class="h-7 w-7 cursor-pointer"> <Button variant="ghost" size="icon" as="span" class="h-7 w-7 cursor-pointer">
@@ -843,67 +842,69 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<Progress v-if="uploadingFile" :model-value="fileUploadProgress" class="h-1" /> <Progress v-if="uploadingFile" :model-value="fileUploadProgress" class="h-1 shrink-0" />
<!-- File List --> <!-- File List -->
<div v-if="driveFiles.length === 0" class="text-center py-6 text-muted-foreground text-sm"> <div v-if="driveFiles.length === 0" class="shrink-0 text-center py-6 text-muted-foreground text-sm">
{{ t('msd.emptyFolder') }} {{ t('msd.emptyFolder') }}
</div> </div>
<div v-else class="space-y-1"> <div v-else class="flex-1 min-h-0 overflow-y-auto pr-2 custom-scrollbar">
<div <div class="space-y-1">
v-for="file in driveFiles"
:key="file.path"
class="flex items-center justify-between p-2 rounded-lg hover:bg-accent/50 transition-colors"
>
<div <div
class="flex items-center gap-2 cursor-pointer flex-1 min-w-0" v-for="file in driveFiles"
@click="file.is_dir && navigateTo(file.path)" :key="file.path"
class="flex items-center justify-between p-2 rounded-lg hover:bg-accent/50 transition-colors"
> >
<Folder v-if="file.is_dir" class="h-4 w-4 text-blue-500 shrink-0" /> <div
<File v-else class="h-4 w-4 text-muted-foreground shrink-0" /> class="flex items-center gap-2 cursor-pointer flex-1 min-w-0"
<div class="min-w-0"> @click="file.is_dir && navigateTo(file.path)"
<Tooltip> >
<TooltipTrigger as-child> <Folder v-if="file.is_dir" class="h-4 w-4 text-blue-500 shrink-0" />
<p class="text-sm font-medium truncate cursor-help">{{ file.name }}</p> <File v-else class="h-4 w-4 text-muted-foreground shrink-0" />
</TooltipTrigger> <div class="min-w-0">
<TooltipContent> <Tooltip>
<p class="max-w-sm break-all">{{ file.name }}</p> <TooltipTrigger as-child>
</TooltipContent> <p class="text-sm font-medium truncate cursor-help">{{ file.name }}</p>
</Tooltip> </TooltipTrigger>
<p v-if="!file.is_dir" class="text-xs text-muted-foreground"> <TooltipContent>
{{ formatBytes(file.size) }} <p class="max-w-sm break-all">{{ file.name }}</p>
</p> </TooltipContent>
</Tooltip>
<p v-if="!file.is_dir" class="text-xs text-muted-foreground">
{{ formatBytes(file.size) }}
</p>
</div>
</div>
<div class="flex items-center gap-0.5 shrink-0">
<Button
v-if="!file.is_dir"
variant="ghost"
size="icon"
class="h-7 w-7"
as="a"
:href="msdApi.downloadDriveFile(file.path)"
download
>
<Download class="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-7 w-7 text-destructive"
@click="confirmDelete('file', file.path, file.name)"
>
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div> </div>
</div>
<div class="flex items-center gap-0.5 shrink-0">
<Button
v-if="!file.is_dir"
variant="ghost"
size="icon"
class="h-7 w-7"
as="a"
:href="msdApi.downloadDriveFile(file.path)"
download
>
<Download class="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-7 w-7 text-destructive"
@click="confirmDelete('file', file.path, file.name)"
>
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</TabsContent> </TabsContent>
</ScrollArea> </Tabs>
</Tabs> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</TooltipProvider> </TooltipProvider>
@@ -1102,3 +1103,28 @@ onUnmounted(() => {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</template> </template>
<style scoped>
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.3);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground) / 0.5);
}
/* For Firefox */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
}
</style>

View File

@@ -18,7 +18,6 @@ import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { ScrollArea } from '@/components/ui/scroll-area'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -294,8 +293,8 @@ onMounted(async () => {
<template> <template>
<Sheet :open="open" @update:open="emit('update:open', $event)"> <Sheet :open="open" @update:open="emit('update:open', $event)">
<SheetContent side="right" class="w-full sm:max-w-lg overflow-hidden flex flex-col"> <SheetContent side="right" class="w-full sm:max-w-lg overflow-hidden flex flex-col h-[dvh]">
<SheetHeader> <SheetHeader class="shrink-0">
<div class="flex items-center justify-between pr-8"> <div class="flex items-center justify-between pr-8">
<div> <div>
<SheetTitle class="flex items-center gap-2"> <SheetTitle class="flex items-center gap-2">
@@ -314,10 +313,10 @@ onMounted(async () => {
</div> </div>
</SheetHeader> </SheetHeader>
<Separator class="my-4" /> <Separator class="my-4 shrink-0" />
<Tabs v-model="activeTab" class="flex-1 flex flex-col overflow-hidden"> <Tabs v-model="activeTab" class="flex-1 flex flex-col min-h-0 overflow-hidden">
<TabsList class="w-full grid grid-cols-2"> <TabsList class="w-full grid grid-cols-2 shrink-0">
<TabsTrigger value="images"> <TabsTrigger value="images">
<Disc class="h-4 w-4 mr-1.5" /> <Disc class="h-4 w-4 mr-1.5" />
{{ t('msd.images') }} {{ t('msd.images') }}
@@ -328,11 +327,11 @@ onMounted(async () => {
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<ScrollArea class="flex-1 mt-4"> <div class="flex-1 min-h-0 mt-4 flex flex-col">
<!-- Images Tab --> <!-- Images Tab -->
<TabsContent value="images" class="m-0 space-y-4"> <TabsContent value="images" class="flex-1 min-h-0 m-0 flex flex-col space-y-4">
<!-- Upload Area --> <!-- Upload Area -->
<div class="space-y-3"> <div class="shrink-0 space-y-3">
<label class="block"> <label class="block">
<input <input
type="file" type="file"
@@ -352,7 +351,7 @@ onMounted(async () => {
</div> </div>
<!-- Options --> <!-- Options -->
<div class="flex items-center gap-4 p-3 rounded-lg bg-muted/50"> <div class="shrink-0 flex items-center gap-4 p-3 rounded-lg bg-muted/50">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Switch id="cdrom" v-model:checked="cdromMode" /> <Switch id="cdrom" v-model:checked="cdromMode" />
<Label for="cdrom" class="text-xs">{{ t('msd.cdromMode') }}</Label> <Label for="cdrom" class="text-xs">{{ t('msd.cdromMode') }}</Label>
@@ -364,63 +363,65 @@ onMounted(async () => {
</div> </div>
<!-- Image List --> <!-- Image List -->
<div class="space-y-2"> <div class="flex-1 min-h-0 flex flex-col space-y-2">
<div class="flex items-center justify-between"> <div class="shrink-0 flex items-center justify-between">
<h4 class="text-sm font-medium">{{ t('msd.imageList') }}</h4> <h4 class="text-sm font-medium">{{ t('msd.imageList') }}</h4>
<Button variant="ghost" size="icon" class="h-7 w-7" @click="loadImages"> <Button variant="ghost" size="icon" class="h-7 w-7" @click="loadImages">
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loadingImages }" /> <RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loadingImages }" />
</Button> </Button>
</div> </div>
<div v-if="images.length === 0" class="text-center py-6 text-muted-foreground text-sm"> <div v-if="images.length === 0" class="shrink-0 text-center py-6 text-muted-foreground text-sm">
{{ t('msd.noImages') }} {{ t('msd.noImages') }}
</div> </div>
<div v-else class="space-y-1.5"> <ScrollArea v-else class="flex-1 min-h-0 pr-4">
<div <div class="space-y-1.5">
v-for="image in images" <div
:key="image.id" v-for="image in images"
class="flex items-center justify-between p-2.5 rounded-lg border hover:bg-accent/50 transition-colors" :key="image.id"
> class="flex items-center justify-between p-2.5 rounded-lg border hover:bg-accent/50 transition-colors"
<div class="flex items-center gap-2.5 min-w-0 flex-1"> >
<Disc class="h-4 w-4 text-muted-foreground shrink-0" /> <div class="flex items-center gap-2.5 min-w-0 flex-1">
<div class="min-w-0"> <Disc class="h-4 w-4 text-muted-foreground shrink-0" />
<p class="text-sm font-medium truncate">{{ image.name }}</p> <div class="min-w-0">
<p class="text-xs text-muted-foreground"> <p class="text-sm font-medium truncate">{{ image.name }}</p>
{{ formatBytes(image.size) }} <p class="text-xs text-muted-foreground">
</p> {{ formatBytes(image.size) }}
</p>
</div>
</div>
<div class="flex items-center gap-1 shrink-0">
<Button
v-if="!msdConnected || systemStore.msd?.imageId !== image.id"
variant="outline"
size="sm"
class="h-7 text-xs"
@click="connectImage(image)"
>
<Link class="h-3.5 w-3.5 mr-1" />
{{ t('msd.connect') }}
</Button>
<Badge v-else variant="default" class="text-xs">{{ t('common.connected') }}</Badge>
<Button
variant="ghost"
size="icon"
class="h-7 w-7 text-destructive"
@click="confirmDelete('image', image.id, image.name)"
>
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div> </div>
</div> </div>
<div class="flex items-center gap-1 shrink-0">
<Button
v-if="!msdConnected || systemStore.msd?.imageId !== image.id"
variant="outline"
size="sm"
class="h-7 text-xs"
@click="connectImage(image)"
>
<Link class="h-3.5 w-3.5 mr-1" />
{{ t('msd.connect') }}
</Button>
<Badge v-else variant="default" class="text-xs">{{ t('common.connected') }}</Badge>
<Button
variant="ghost"
size="icon"
class="h-7 w-7 text-destructive"
@click="confirmDelete('image', image.id, image.name)"
>
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div>
</div> </div>
</div> </ScrollArea>
</div> </div>
</TabsContent> </TabsContent>
<!-- Drive Tab --> <!-- Drive Tab -->
<TabsContent value="drive" class="m-0 space-y-4"> <TabsContent value="drive" class="flex-1 min-h-0 m-0 flex flex-col space-y-4">
<template v-if="!driveInitialized"> <template v-if="!driveInitialized">
<div class="text-center py-8 space-y-4"> <div class="shrink-0 text-center py-8 space-y-4">
<HardDrive class="h-10 w-10 mx-auto text-muted-foreground" /> <HardDrive class="h-10 w-10 mx-auto text-muted-foreground" />
<p class="text-sm text-muted-foreground">{{ t('msd.driveNotInitialized') }}</p> <p class="text-sm text-muted-foreground">{{ t('msd.driveNotInitialized') }}</p>
<Button size="sm" @click="initializeDrive"> <Button size="sm" @click="initializeDrive">
@@ -431,7 +432,7 @@ onMounted(async () => {
<template v-else> <template v-else>
<!-- Drive Info --> <!-- Drive Info -->
<div class="p-3 rounded-lg bg-muted/50 space-y-2"> <div class="shrink-0 p-3 rounded-lg bg-muted/50 space-y-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="space-y-0.5"> <div class="space-y-0.5">
<p class="text-xs text-muted-foreground">{{ t('msd.driveSize') }}: {{ (driveInfo?.size || 0) / 1024 / 1024 }}MB</p> <p class="text-xs text-muted-foreground">{{ t('msd.driveSize') }}: {{ (driveInfo?.size || 0) / 1024 / 1024 }}MB</p>
@@ -459,9 +460,9 @@ onMounted(async () => {
</div> </div>
<!-- File Browser --> <!-- File Browser -->
<div class="space-y-2"> <div class="flex-1 min-h-0 flex flex-col space-y-2">
<!-- Toolbar --> <!-- Toolbar -->
<div class="flex items-center justify-between gap-2"> <div class="shrink-0 flex items-center justify-between gap-2">
<div class="flex items-center gap-1 min-w-0 flex-1"> <div class="flex items-center gap-1 min-w-0 flex-1">
<Button <Button
v-if="currentPath !== '/'" v-if="currentPath !== '/'"
@@ -485,7 +486,7 @@ onMounted(async () => {
</template> </template>
</nav> </nav>
</div> </div>
<div class="flex items-center gap-1 shrink-0"> <div class="shrink-0 flex items-center gap-1 shrink-0">
<label> <label>
<input type="file" class="hidden" :disabled="uploadingFile" @change="handleFileUpload" /> <input type="file" class="hidden" :disabled="uploadingFile" @change="handleFileUpload" />
<Button variant="ghost" size="icon" as="span" class="h-7 w-7 cursor-pointer"> <Button variant="ghost" size="icon" as="span" class="h-7 w-7 cursor-pointer">
@@ -501,59 +502,61 @@ onMounted(async () => {
</div> </div>
</div> </div>
<Progress v-if="uploadingFile" :model-value="fileUploadProgress" class="h-1" /> <Progress v-if="uploadingFile" :model-value="fileUploadProgress" class="h-1 shrink-0" />
<!-- File List --> <!-- File List -->
<div v-if="driveFiles.length === 0" class="text-center py-6 text-muted-foreground text-sm"> <div v-if="driveFiles.length === 0" class="shrink-0 text-center py-6 text-muted-foreground text-sm">
{{ t('msd.emptyFolder') }} {{ t('msd.emptyFolder') }}
</div> </div>
<div v-else class="space-y-1"> <ScrollArea v-else class="flex-1 min-h-0 pr-4">
<div <div class="space-y-1">
v-for="file in driveFiles"
:key="file.path"
class="flex items-center justify-between p-2 rounded-lg hover:bg-accent/50 transition-colors"
>
<div <div
class="flex items-center gap-2 cursor-pointer flex-1 min-w-0" v-for="file in driveFiles"
@click="file.is_dir && navigateTo(file.path)" :key="file.path"
class="flex items-center justify-between p-2 rounded-lg hover:bg-accent/50 transition-colors"
> >
<Folder v-if="file.is_dir" class="h-4 w-4 text-blue-500 shrink-0" /> <div
<File v-else class="h-4 w-4 text-muted-foreground shrink-0" /> class="flex items-center gap-2 cursor-pointer flex-1 min-w-0"
<div class="min-w-0"> @click="file.is_dir && navigateTo(file.path)"
<p class="text-sm font-medium truncate">{{ file.name }}</p> >
<p v-if="!file.is_dir" class="text-xs text-muted-foreground"> <Folder v-if="file.is_dir" class="h-4 w-4 text-blue-500 shrink-0" />
{{ formatBytes(file.size) }} <File v-else class="h-4 w-4 text-muted-foreground shrink-0" />
</p> <div class="min-w-0">
<p class="text-sm font-medium truncate">{{ file.name }}</p>
<p v-if="!file.is_dir" class="text-xs text-muted-foreground">
{{ formatBytes(file.size) }}
</p>
</div>
</div>
<div class="flex items-center gap-0.5 shrink-0">
<Button
v-if="!file.is_dir"
variant="ghost"
size="icon"
class="h-7 w-7"
as="a"
:href="msdApi.downloadDriveFile(file.path)"
download
>
<Download class="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-7 w-7 text-destructive"
@click="confirmDelete('file', file.path, file.name)"
>
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div> </div>
</div> </div>
<div class="flex items-center gap-0.5 shrink-0">
<Button
v-if="!file.is_dir"
variant="ghost"
size="icon"
class="h-7 w-7"
as="a"
:href="msdApi.downloadDriveFile(file.path)"
download
>
<Download class="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-7 w-7 text-destructive"
@click="confirmDelete('file', file.path, file.name)"
>
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div>
</div> </div>
</div> </ScrollArea>
</div> </div>
</template> </template>
</TabsContent> </TabsContent>
</ScrollArea> </div>
</Tabs> </Tabs>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
@@ -588,3 +591,28 @@ onMounted(async () => {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</template> </template>
<style scoped>
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.3);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground) / 0.5);
}
/* For Firefox */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
}
</style>