Enhance InstancesTab component for improved device instance display

- Refactored the `InstancesTab` component to group file instances by device, providing a clearer overview of file availability across devices.
- Introduced a new `InstanceRow` component for better organization and readability of individual file instances.
- Added device querying to retrieve device names and icons, enhancing the user interface with relevant device information.
- Updated the layout to improve visual clarity and user experience when viewing alternate file instances.
This commit is contained in:
Jamie Pine
2025-12-26 12:05:19 -08:00
parent 996df041a4
commit 2b6f2a14d7

View File

@@ -36,7 +36,11 @@ import {
import { TagSelectorButton } from "../components/Tags";
import clsx from "clsx";
import type { File } from "@sd/ts-client";
import { useNormalizedQuery, useLibraryMutation } from "../context";
import {
useNormalizedQuery,
useLibraryMutation,
getDeviceIcon,
} from "../context";
import { formatBytes } from "../components/Explorer/utils";
import { File as FileComponent } from "../components/Explorer/File";
import { useContextMenu } from "../hooks/useContextMenu";
@@ -948,6 +952,7 @@ function SidecarItem({
}
function InstancesTab({ file }: { file: File }) {
// Query for alternate instances with full File data
const instancesQuery = useNormalizedQuery<
{ entry_uuid: string },
@@ -960,33 +965,45 @@ function InstancesTab({ file }: { file: File }) {
const instances = instancesQuery.data?.instances || [];
const getPathDisplay = (sdPath: typeof file.sd_path) => {
if ("Physical" in sdPath) {
return sdPath.Physical.path;
} else if ("Cloud" in sdPath) {
return sdPath.Cloud.path;
} else {
return "Content";
}
// Query devices to get proper names and icons
const devicesQuery = useNormalizedQuery<any, any[]>({
wireMethod: "query:devices.list",
input: {
include_offline: true,
include_details: false,
show_paired: true,
},
resourceType: "device",
});
const devices = devicesQuery.data || [];
// Group instances by device_slug
const instancesByDevice = instances.reduce(
(acc, instance) => {
let deviceSlug = "unknown";
if ("Physical" in instance.sd_path) {
deviceSlug = instance.sd_path.Physical.device_slug;
} else if ("Cloud" in instance.sd_path) {
deviceSlug = "cloud";
}
if (!acc[deviceSlug]) {
acc[deviceSlug] = [];
}
acc[deviceSlug].push(instance);
return acc;
},
{} as Record<string, File[]>,
);
const getDeviceName = (deviceSlug: string) => {
const device = devices.find((d) => d.slug === deviceSlug);
return device?.name || deviceSlug;
};
const getDeviceDisplay = (sdPath: typeof file.sd_path) => {
if ("Physical" in sdPath) {
return sdPath.Physical.device_slug || "Local Device";
} else if ("Cloud" in sdPath) {
return "Cloud Storage";
} else {
return "Content Addressed";
}
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
const getDeviceInfo = (deviceSlug: string) => {
return devices.find((d) => d.slug === deviceSlug);
};
if (instancesQuery.isLoading) {
@@ -1009,7 +1026,7 @@ function InstancesTab({ file }: { file: File }) {
}
return (
<div className="no-scrollbar mask-fade-out flex flex-col space-y-4 overflow-x-hidden overflow-y-scroll pb-10 px-2 pt-2">
<div className="no-scrollbar mask-fade-out flex flex-col space-y-5 overflow-x-hidden overflow-y-scroll pb-10 px-2 pt-2">
<p className="text-xs text-sidebar-inkDull">
All copies of this file across your devices and locations
</p>
@@ -1019,78 +1036,140 @@ function InstancesTab({ file }: { file: File }) {
No alternate instances found
</div>
) : (
<div className="space-y-2.5">
{instances.map((instance, i) => (
<div
key={i}
className="p-2.5 bg-app-box/40 rounded-lg border border-app-line/50 hover:bg-app-box/60 transition-colors"
>
<div className="flex items-start gap-3">
{/* Thumbnail */}
<div className="shrink-0">
<FileComponent.Thumb
file={instance}
size={64}
iconScale={0.5}
className="rounded overflow-hidden"
/>
</div>
<div className="space-y-4">
{Object.entries(instancesByDevice).map(
([deviceSlug, deviceInstances]) => {
const deviceInfo = getDeviceInfo(deviceSlug);
const deviceName = getDeviceName(deviceSlug);
{/* Info */}
<div className="flex-1 min-w-0 space-y-1.5">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="text-xs font-medium text-sidebar-ink truncate">
{instance.name}
{instance.extension &&
`.${instance.extension}`}
</div>
<div className="text-[11px] text-sidebar-inkDull mt-0.5">
{formatBytes(instance.size)}
</div>
</div>
<div
className={clsx(
"size-2 rounded-full shrink-0 mt-1",
instance.is_local
? "bg-accent"
: "bg-sidebar-inkDull/40",
)}
title={
instance.is_local
? "Available locally"
: "Remote"
}
return (
<div key={deviceSlug} className="space-y-1">
{/* Device Header */}
<div className="flex items-center gap-2 px-2">
<img
src={getDeviceIcon(deviceInfo)}
className="size-4 shrink-0"
alt=""
/>
</div>
<div className="flex items-center gap-1.5 text-[11px] text-sidebar-inkDull">
<MapPin size={12} weight="bold" />
<span className="truncate">
{getDeviceDisplay(
instance.sd_path,
)}
<span className="text-xs font-semibold text-sidebar-ink">
{deviceName}
</span>
<div className="flex-1" />
<div className="flex items-center justify-center size-5 rounded-full bg-app-box border border-app-line text-[10px] font-semibold text-sidebar-inkDull">
{deviceInstances.length}
</div>
</div>
<div className="text-[10px] text-sidebar-inkDull/70 font-mono truncate">
{getPathDisplay(instance.sd_path)}
</div>
<div className="text-[10px] text-sidebar-inkDull/70">
Modified{" "}
{formatDate(instance.modified_at)}
{/* List of instances */}
<div className="space-y-0.5">
{deviceInstances.map(
(instance, i) => (
<InstanceRow
key={i}
instance={instance}
/>
),
)}
</div>
</div>
</div>
</div>
))}
);
},
)}
</div>
)}
</div>
);
}
function InstanceRow({ instance }: { instance: File }) {
const getPathDisplay = (sdPath: typeof instance.sd_path) => {
if ("Physical" in sdPath) {
return sdPath.Physical.path;
} else if ("Cloud" in sdPath) {
return sdPath.Cloud.path;
} else {
return "Content";
}
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
};
return (
<div
className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-app-box/40 transition-colors cursor-default"
title={getPathDisplay(instance.sd_path)}
>
{/* Thumbnail */}
<div className="flex-shrink-0 [&_*]:!rounded-[3px]">
<FileComponent.Thumb file={instance} size={20} />
</div>
{/* File info */}
<div className="flex-1 min-w-0 flex items-center gap-2">
<span className="text-xs text-sidebar-ink truncate">
{instance.name}
{instance.extension && `.${instance.extension}`}
</span>
</div>
{/* Metadata */}
<div className="flex items-center gap-2 shrink-0">
{/* Tags */}
{instance.tags && instance.tags.length > 0 && (
<div
className="flex items-center gap-0.5"
title={instance.tags
.map((t) => t.canonical_name)
.join(", ")}
>
{instance.tags.slice(0, 3).map((tag) => (
<div
key={tag.id}
className="size-1.5 rounded-full"
style={{
backgroundColor: tag.color || "#3B82F6",
}}
/>
))}
{instance.tags.length > 3 && (
<span className="text-[9px] text-ink-faint font-medium">
+{instance.tags.length - 3}
</span>
)}
</div>
)}
{/* Modified date */}
<span className="text-[10px] text-sidebar-inkDull">
{formatDate(instance.modified_at)}
</span>
{/* Size */}
<span className="text-[10px] text-sidebar-inkDull min-w-[50px] text-right">
{formatBytes(instance.size)}
</span>
{/* Local indicator */}
<div
className={clsx(
"size-1.5 rounded-full",
instance.is_local
? "bg-accent"
: "bg-sidebar-inkDull/40",
)}
title={instance.is_local ? "Available locally" : "Remote"}
/>
</div>
</div>
);
}
function ChatTab() {
const [message, setMessage] = useState("");