mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-19 13:55:40 -04:00
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:
@@ -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("");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user