@@ -36,7 +36,11 @@ import {
3636import { TagSelectorButton } from "../components/Tags" ;
3737import clsx from "clsx" ;
3838import type { File } from "@sd/ts-client" ;
39- import { useNormalizedQuery , useLibraryMutation } from "../context" ;
39+ import {
40+ useNormalizedQuery ,
41+ useLibraryMutation ,
42+ getDeviceIcon ,
43+ } from "../context" ;
4044import { formatBytes } from "../components/Explorer/utils" ;
4145import { File as FileComponent } from "../components/Explorer/File" ;
4246import { useContextMenu } from "../hooks/useContextMenu" ;
@@ -948,6 +952,7 @@ function SidecarItem({
948952}
949953
950954function InstancesTab ( { file } : { file : File } ) {
955+
951956 // Query for alternate instances with full File data
952957 const instancesQuery = useNormalizedQuery <
953958 { entry_uuid : string } ,
@@ -960,33 +965,45 @@ function InstancesTab({ file }: { file: File }) {
960965
961966 const instances = instancesQuery . data ?. instances || [ ] ;
962967
963- const getPathDisplay = ( sdPath : typeof file . sd_path ) => {
964- if ( "Physical" in sdPath ) {
965- return sdPath . Physical . path ;
966- } else if ( "Cloud" in sdPath ) {
967- return sdPath . Cloud . path ;
968- } else {
969- return "Content" ;
970- }
971- } ;
968+ // Query devices to get proper names and icons
969+ const devicesQuery = useNormalizedQuery < any , any [ ] > ( {
970+ wireMethod : "query:devices.list" ,
971+ input : {
972+ include_offline : true ,
973+ include_details : false ,
974+ show_paired : true ,
975+ } ,
976+ resourceType : "device" ,
977+ } ) ;
972978
973- const getDeviceDisplay = ( sdPath : typeof file . sd_path ) => {
974- if ( "Physical" in sdPath ) {
975- return sdPath . Physical . device_slug || "Local Device" ;
976- } else if ( "Cloud" in sdPath ) {
977- return "Cloud Storage" ;
978- } else {
979- return "Content Addressed" ;
980- }
979+ const devices = devicesQuery . data || [ ] ;
980+
981+ // Group instances by device_slug
982+ const instancesByDevice = instances . reduce (
983+ ( acc , instance ) => {
984+ let deviceSlug = "unknown" ;
985+ if ( "Physical" in instance . sd_path ) {
986+ deviceSlug = instance . sd_path . Physical . device_slug ;
987+ } else if ( "Cloud" in instance . sd_path ) {
988+ deviceSlug = "cloud" ;
989+ }
990+
991+ if ( ! acc [ deviceSlug ] ) {
992+ acc [ deviceSlug ] = [ ] ;
993+ }
994+ acc [ deviceSlug ] . push ( instance ) ;
995+ return acc ;
996+ } ,
997+ { } as Record < string , File [ ] > ,
998+ ) ;
999+
1000+ const getDeviceName = ( deviceSlug : string ) => {
1001+ const device = devices . find ( ( d ) => d . slug === deviceSlug ) ;
1002+ return device ?. name || deviceSlug ;
9811003 } ;
9821004
983- const formatDate = ( dateStr : string ) => {
984- const date = new Date ( dateStr ) ;
985- return date . toLocaleDateString ( "en-US" , {
986- month : "short" ,
987- day : "numeric" ,
988- year : "numeric" ,
989- } ) ;
1005+ const getDeviceInfo = ( deviceSlug : string ) => {
1006+ return devices . find ( ( d ) => d . slug === deviceSlug ) ;
9901007 } ;
9911008
9921009 if ( instancesQuery . isLoading ) {
@@ -1009,7 +1026,7 @@ function InstancesTab({ file }: { file: File }) {
10091026 }
10101027
10111028 return (
1012- < 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" >
1029+ < 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" >
10131030 < p className = "text-xs text-sidebar-inkDull" >
10141031 All copies of this file across your devices and locations
10151032 </ p >
@@ -1019,78 +1036,140 @@ function InstancesTab({ file }: { file: File }) {
10191036 No alternate instances found
10201037 </ div >
10211038 ) : (
1022- < div className = "space-y-2.5" >
1023- { instances . map ( ( instance , i ) => (
1024- < div
1025- key = { i }
1026- className = "p-2.5 bg-app-box/40 rounded-lg border border-app-line/50 hover:bg-app-box/60 transition-colors"
1027- >
1028- < div className = "flex items-start gap-3" >
1029- { /* Thumbnail */ }
1030- < div className = "shrink-0" >
1031- < FileComponent . Thumb
1032- file = { instance }
1033- size = { 64 }
1034- iconScale = { 0.5 }
1035- className = "rounded overflow-hidden"
1036- />
1037- </ div >
1038-
1039- { /* Info */ }
1040- < div className = "flex-1 min-w-0 space-y-1.5" >
1041- < div className = "flex items-start justify-between gap-2" >
1042- < div className = "flex-1 min-w-0" >
1043- < div className = "text-xs font-medium text-sidebar-ink truncate" >
1044- { instance . name }
1045- { instance . extension &&
1046- `.${ instance . extension } ` }
1047- </ div >
1048- < div className = "text-[11px] text-sidebar-inkDull mt-0.5" >
1049- { formatBytes ( instance . size ) }
1050- </ div >
1051- </ div >
1052- < div
1053- className = { clsx (
1054- "size-2 rounded-full shrink-0 mt-1" ,
1055- instance . is_local
1056- ? "bg-accent"
1057- : "bg-sidebar-inkDull/40" ,
1058- ) }
1059- title = {
1060- instance . is_local
1061- ? "Available locally"
1062- : "Remote"
1063- }
1039+ < div className = "space-y-4" >
1040+ { Object . entries ( instancesByDevice ) . map (
1041+ ( [ deviceSlug , deviceInstances ] ) => {
1042+ const deviceInfo = getDeviceInfo ( deviceSlug ) ;
1043+ const deviceName = getDeviceName ( deviceSlug ) ;
1044+
1045+ return (
1046+ < div key = { deviceSlug } className = "space-y-1" >
1047+ { /* Device Header */ }
1048+ < div className = "flex items-center gap-2 px-2" >
1049+ < img
1050+ src = { getDeviceIcon ( deviceInfo ) }
1051+ className = "size-4 shrink-0"
1052+ alt = ""
10641053 />
1065- </ div >
1066-
1067- < div className = "flex items-center gap-1.5 text-[11px] text-sidebar-inkDull" >
1068- < MapPin size = { 12 } weight = "bold" />
1069- < span className = "truncate" >
1070- { getDeviceDisplay (
1071- instance . sd_path ,
1072- ) }
1054+ < span className = "text-xs font-semibold text-sidebar-ink" >
1055+ { deviceName }
10731056 </ span >
1057+ < div className = "flex-1" />
1058+ < 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" >
1059+ { deviceInstances . length }
1060+ </ div >
10741061 </ div >
10751062
1076- < div className = "text-[10px] text-sidebar-inkDull/70 font-mono truncate" >
1077- { getPathDisplay ( instance . sd_path ) }
1078- </ div >
1079-
1080- < div className = "text-[10px] text-sidebar-inkDull/70" >
1081- Modified{ " " }
1082- { formatDate ( instance . modified_at ) }
1063+ { /* List of instances */ }
1064+ < div className = "space-y-0.5" >
1065+ { deviceInstances . map (
1066+ ( instance , i ) => (
1067+ < InstanceRow
1068+ key = { i }
1069+ instance = { instance }
1070+ />
1071+ ) ,
1072+ ) }
10831073 </ div >
10841074 </ div >
1085- </ div >
1086- </ div >
1087- ) ) }
1075+ ) ;
1076+ } ,
1077+ ) }
10881078 </ div >
10891079 ) }
10901080 </ div >
10911081 ) ;
10921082}
10931083
1084+ function InstanceRow ( { instance } : { instance : File } ) {
1085+ const getPathDisplay = ( sdPath : typeof instance . sd_path ) => {
1086+ if ( "Physical" in sdPath ) {
1087+ return sdPath . Physical . path ;
1088+ } else if ( "Cloud" in sdPath ) {
1089+ return sdPath . Cloud . path ;
1090+ } else {
1091+ return "Content" ;
1092+ }
1093+ } ;
1094+
1095+ const formatDate = ( dateStr : string ) => {
1096+ const date = new Date ( dateStr ) ;
1097+ return date . toLocaleDateString ( "en-US" , {
1098+ month : "short" ,
1099+ day : "numeric" ,
1100+ } ) ;
1101+ } ;
1102+
1103+ return (
1104+ < div
1105+ className = "flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-app-box/40 transition-colors cursor-default"
1106+ title = { getPathDisplay ( instance . sd_path ) }
1107+ >
1108+ { /* Thumbnail */ }
1109+ < div className = "flex-shrink-0 [&_*]:!rounded-[3px]" >
1110+ < FileComponent . Thumb file = { instance } size = { 20 } />
1111+ </ div >
1112+
1113+ { /* File info */ }
1114+ < div className = "flex-1 min-w-0 flex items-center gap-2" >
1115+ < span className = "text-xs text-sidebar-ink truncate" >
1116+ { instance . name }
1117+ { instance . extension && `.${ instance . extension } ` }
1118+ </ span >
1119+ </ div >
1120+
1121+ { /* Metadata */ }
1122+ < div className = "flex items-center gap-2 shrink-0" >
1123+ { /* Tags */ }
1124+ { instance . tags && instance . tags . length > 0 && (
1125+ < div
1126+ className = "flex items-center gap-0.5"
1127+ title = { instance . tags
1128+ . map ( ( t ) => t . canonical_name )
1129+ . join ( ", " ) }
1130+ >
1131+ { instance . tags . slice ( 0 , 3 ) . map ( ( tag ) => (
1132+ < div
1133+ key = { tag . id }
1134+ className = "size-1.5 rounded-full"
1135+ style = { {
1136+ backgroundColor : tag . color || "#3B82F6" ,
1137+ } }
1138+ />
1139+ ) ) }
1140+ { instance . tags . length > 3 && (
1141+ < span className = "text-[9px] text-ink-faint font-medium" >
1142+ +{ instance . tags . length - 3 }
1143+ </ span >
1144+ ) }
1145+ </ div >
1146+ ) }
1147+
1148+ { /* Modified date */ }
1149+ < span className = "text-[10px] text-sidebar-inkDull" >
1150+ { formatDate ( instance . modified_at ) }
1151+ </ span >
1152+
1153+ { /* Size */ }
1154+ < span className = "text-[10px] text-sidebar-inkDull min-w-[50px] text-right" >
1155+ { formatBytes ( instance . size ) }
1156+ </ span >
1157+
1158+ { /* Local indicator */ }
1159+ < div
1160+ className = { clsx (
1161+ "size-1.5 rounded-full" ,
1162+ instance . is_local
1163+ ? "bg-accent"
1164+ : "bg-sidebar-inkDull/40" ,
1165+ ) }
1166+ title = { instance . is_local ? "Available locally" : "Remote" }
1167+ />
1168+ </ div >
1169+ </ div >
1170+ ) ;
1171+ }
1172+
10941173function ChatTab ( ) {
10951174 const [ message , setMessage ] = useState ( "" ) ;
10961175
0 commit comments