Refactor file handling in Explorer component

- Updated file type checks from `file.kind.type` to `file.kind` for consistency across various components in the Explorer views.
- Enhanced the `Thumb` component to conditionally hide the icon based on thumbnail loading status.
- Adjusted the `HeroStats` component for improved readability and structure.
- Added a new `iconScale` prop to the `FileInspector` component's thumbnail for better visual scaling.
This commit is contained in:
Jamie Pine 2025-11-21 08:01:37 -08:00
parent 553fadd2d4
commit 008d05414a
23 changed files with 154 additions and 134 deletions

View File

@ -20,3 +20,4 @@

View File

@ -70,3 +70,4 @@ enum VideoMediaData {

View File

@ -10,3 +10,4 @@ pub use output::LibraryOpenOutput;

View File

@ -124,3 +124,4 @@ mod tests {

View File

@ -16,3 +16,4 @@ pub struct VolumeTrackInput {

View File

@ -13,3 +13,4 @@ pub struct VolumeUntrackInput {

View File

@ -7,3 +7,4 @@ analysis.md

View File

@ -41,3 +41,4 @@ required-features = ["cli"]

View File

@ -52,3 +52,4 @@ fn main() -> Result<()> {

View File

@ -19,3 +19,4 @@ pub fn export_json(templates: &[Template], groups: &[LogGroup]) -> Result<String

View File

@ -241,3 +241,4 @@ mod tests {

View File

@ -106,3 +106,4 @@ mod tests {

View File

@ -147,7 +147,8 @@ export const Thumb = memo(function Thumb({
alt=""
className={clsx(
"object-contain transition-opacity",
thumbLoaded && "opacity-0",
// Only hide icon if we actually have a thumbnail that loaded
thumbLoaded && thumbnailSrc && "opacity-0",
)}
style={{
width: iconSize,
@ -191,16 +192,21 @@ export function Icon({
size?: number;
className?: string;
}) {
const kindCapitalized = file.content_identity?.kind
? file.content_identity.kind.charAt(0).toUpperCase() +
file.content_identity.kind.slice(1)
: "Document";
// This is jank and has to be done in several places. Ideally a util function.
const fileKind =
file?.content_identity?.kind && file.content_identity.kind !== "unknown"
? file.content_identity.kind
: file.kind === "File"
? file.extension || "File"
: file.kind;
// this too
const kindCapitalized = fileKind.charAt(0).toUpperCase() + fileKind.slice(1);
const icon = getIcon(
kindCapitalized,
true, // Dark theme
file.kind.type === "File" ? file.kind.data?.extension : undefined,
file.kind.type === "Directory",
file.extension,
file.kind === "Directory",
);
return (

View File

@ -67,7 +67,7 @@ export function Column({ path, isActive, onNavigate }: ColumnProps) {
icon: FolderOpen,
label: "Open",
onClick: (file: File) => {
if (file.kind.type === "Directory") {
if (file.kind === "Directory") {
onNavigate(file.sd_path);
}
},

View File

@ -40,7 +40,7 @@ export function ColumnItem({
>
<FileComponent.Thumb file={file} size={20} />
<span className="text-sm truncate flex-1">{file.name}</span>
{file.kind.type === "Directory" && (
{file.kind === "Directory" && (
<svg
className="size-3 text-ink-dull"
fill="none"

View File

@ -70,7 +70,7 @@ export const FileCard = memo(function FileCard({ file, fileIndex, allFiles, sele
icon: FolderOpen,
label: "Open",
onClick: () => {
if (file.kind.type === "Directory") {
if (file.kind === "Directory") {
setCurrentPath(file.sd_path);
} else {
console.log("Open file:", file.name);
@ -78,7 +78,7 @@ export const FileCard = memo(function FileCard({ file, fileIndex, allFiles, sele
}
},
keybind: "⌘O",
condition: () => file.kind.type === "Directory" || file.kind.type === "File",
condition: () => file.kind === "Directory" || file.kind === "File",
},
{
icon: MagnifyingGlass,
@ -308,7 +308,7 @@ export const FileCard = memo(function FileCard({ file, fileIndex, allFiles, sele
type: "submenu",
icon: FileText,
label: "Document Processing",
condition: () => ["pdf", "doc", "docx"].includes(file.kind.type === "File" ? file.kind.data?.extension || "" : ""),
condition: () => file.kind === "File" && ["pdf", "doc", "docx"].includes(file.extension || ""),
submenu: [
{
icon: TextAa,
@ -425,7 +425,7 @@ export const FileCard = memo(function FileCard({ file, fileIndex, allFiles, sele
};
const handleDoubleClick = () => {
if (file.kind.type === "Directory") {
if (file.kind === "Directory") {
setCurrentPath(file.sd_path);
}
};

View File

@ -24,7 +24,7 @@ export function FileRow({ file, fileIndex, allFiles }: FileRowProps) {
};
const handleDoubleClick = () => {
if (file.kind.type === "Directory") {
if (file.kind === "Directory") {
setCurrentPath(file.sd_path);
}
};
@ -60,7 +60,7 @@ export function FileRow({ file, fileIndex, allFiles }: FileRowProps) {
{formatRelativeTime(file.modified_at)}
</div>
<div className="w-24 text-sm text-ink-dull">
{file.kind.type === "File" ? file.kind.data?.extension || "—" : "Folder"}
{file.kind === "File" ? file.extension || "—" : "Folder"}
</div>
</div>
);

View File

@ -11,7 +11,7 @@ interface SizeCircleProps {
// Get file extension or type
function getFileType(file: File): string {
if (file.kind.type === "Directory") return "Folder";
if (file.kind === "Directory") return "Folder";
const name = file.name;
const lastDot = name.lastIndexOf(".");
@ -22,7 +22,7 @@ function getFileType(file: File): string {
// Get color based on file type
function getFileColor(file: File): string {
if (file.kind.type === "Directory") return "bg-blue-500";
if (file.kind === "Directory") return "bg-blue-500";
const ext = file.name.split(".").pop()?.toLowerCase() || "";

View File

@ -29,7 +29,7 @@ function getTailwindColor(className: string): string {
}
function getFileColorClass(file: File): string {
if (file.kind.type === "Directory") return "bg-accent";
if (file.kind === "Directory") return "bg-accent";
const ext = file.name.split(".").pop()?.toLowerCase() || "";
@ -71,7 +71,7 @@ function getFileColor(file: File): string {
}
function getFileType(file: File): string {
if (file.kind.type === "Directory") return "Folder";
if (file.kind === "Directory") return "Folder";
const name = file.name;
const lastDot = name.lastIndexOf(".");
@ -348,7 +348,7 @@ export function SizeView() {
}
// Navigate if directory
if (d.data.file.kind.type === "Directory") {
if (d.data.file.kind === "Directory") {
setCurrentPathRef.current(d.data.file.sd_path);
}
})

View File

@ -59,3 +59,4 @@ export function useJobDispatch() {

View File

@ -137,6 +137,7 @@ function OverviewTab({ file }: { file: File }) {
<FileComponent.Thumb
file={file}
size={200}
iconScale={0.6}
className="w-full max-w-full"
/>
</div>

View File

@ -3,138 +3,137 @@ import clsx from "clsx";
import { CloudArrowUp, HardDrives, Files, Cpu } from "@phosphor-icons/react";
interface HeroStatsProps {
totalStorage: number; // bytes
usedStorage: number; // bytes
totalFiles: number;
locationCount: number;
tagCount: number;
deviceCount: number;
uniqueContentCount: number;
totalStorage: number; // bytes
usedStorage: number; // bytes
totalFiles: number;
locationCount: number;
tagCount: number;
deviceCount: number;
uniqueContentCount: number;
}
function formatBytes(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
}
export function HeroStats({
totalStorage,
usedStorage,
totalFiles,
locationCount,
deviceCount,
uniqueContentCount,
totalStorage,
usedStorage,
totalFiles,
locationCount,
deviceCount,
uniqueContentCount,
}: HeroStatsProps) {
const usagePercent = totalStorage > 0 ? (usedStorage / totalStorage) * 100 : 0;
const usagePercent =
totalStorage > 0 ? (usedStorage / totalStorage) * 100 : 0;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-app-box border border-app-line rounded-2xl p-8"
>
return (
<div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-app-box border border-app-line rounded-2xl p-8"
>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
{/* Total Storage */}
<StatCard
icon={HardDrives}
label="Total Storage"
value={formatBytes(totalStorage)}
subtitle={
<>
<span className="text-accent">{formatBytes(usedStorage)}</span>{" "}
used
</>
}
progress={usagePercent}
color="from-blue-500 to-cyan-500"
/>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8">
{/* Total Storage */}
<StatCard
icon={HardDrives}
label="Total Storage"
value={formatBytes(totalStorage)}
subtitle={
<>
<span className="text-accent">{formatBytes(usedStorage)}</span> used
</>
}
progress={usagePercent}
color="from-blue-500 to-cyan-500"
/>
{/* Files */}
<StatCard
icon={Files}
label="Files Indexed"
value={totalFiles.toLocaleString()}
subtitle={`${uniqueContentCount.toLocaleString()} unique files`}
color="from-purple-500 to-pink-500"
/>
{/* Files */}
<StatCard
icon={Files}
label="Files Indexed"
value={totalFiles.toLocaleString()}
subtitle={`${uniqueContentCount.toLocaleString()} unique files`}
color="from-purple-500 to-pink-500"
/>
{/* Devices */}
<StatCard
icon={CloudArrowUp}
label="Connected Devices"
value={deviceCount}
subtitle={`registered in library`}
color="from-green-500 to-emerald-500"
/>
{/* Devices */}
<StatCard
icon={CloudArrowUp}
label="Connected Devices"
value={deviceCount}
subtitle={`registered in library`}
color="from-green-500 to-emerald-500"
/>
{/* Storage Health - Future feature */}
<StatCard
icon={Cpu}
label="Storage Health"
value="Good"
subtitle="all volumes healthy"
color="from-orange-500 to-red-500"
badge="PREVIEW"
/>
</div>
</motion.div>
);
{/* Storage Health - Future feature */}
<StatCard
icon={Cpu}
label="Storage Health"
value="Good"
subtitle="all volumes healthy"
color="from-orange-500 to-red-500"
badge="PREVIEW"
/>
</div>
</div>
);
}
interface StatCardProps {
icon: React.ElementType;
label: string;
value: string | number;
subtitle: React.ReactNode;
progress?: number;
pulse?: boolean;
color: string;
badge?: string;
icon: React.ElementType;
label: string;
value: string | number;
subtitle: React.ReactNode;
progress?: number;
pulse?: boolean;
color: string;
badge?: string;
}
function StatCard({
icon: Icon,
label,
value,
subtitle,
progress,
pulse,
color,
badge,
icon: Icon,
label,
value,
subtitle,
progress,
pulse,
color,
badge,
}: StatCardProps) {
return (
<div className="relative">
{badge && (
<div className="absolute -top-2 -right-2 px-2 py-0.5 bg-sidebar-box text-sidebar-ink text-xs font-medium rounded-full border border-sidebar-line">
{badge}
</div>
)}
return (
<div className="relative">
{badge && (
<div className="absolute -top-2 -right-2 px-2 py-0.5 bg-sidebar-box text-sidebar-ink text-xs font-medium rounded-full border border-sidebar-line">
{badge}
</div>
)}
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<div className="p-2 rounded-lg bg-sidebar-box">
<Icon className="size-5 text-sidebar-ink" weight="duotone" />
</div>
{pulse && (
<motion.div
animate={{ scale: [1, 1.2, 1], opacity: [1, 0.5, 1] }}
transition={{ duration: 2, repeat: Infinity }}
className="size-2 rounded-full bg-accent"
/>
)}
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<div className="p-2 rounded-lg bg-sidebar-box">
<Icon className="size-5 text-sidebar-ink" weight="duotone" />
</div>
{pulse && (
<motion.div
animate={{ scale: [1, 1.2, 1], opacity: [1, 0.5, 1] }}
transition={{ duration: 2, repeat: Infinity }}
className="size-2 rounded-full bg-accent"
/>
)}
</div>
<div>
<div className="text-3xl font-bold text-ink mb-1">{value}</div>
<div className="text-xs text-ink-dull mb-1">{label}</div>
<div className="text-xs text-ink-faint">{subtitle}</div>
</div>
</div>
</div>
);
<div>
<div className="text-3xl font-bold text-ink mb-1">{value}</div>
<div className="text-xs text-ink-dull mb-1">{label}</div>
<div className="text-xs text-ink-faint">{subtitle}</div>
</div>
</div>
</div>
);
}