diff --git a/core/src/bin/generate_typescript_types.rs b/core/src/bin/generate_typescript_types.rs index 9f38357fe..8d18c6049 100644 --- a/core/src/bin/generate_typescript_types.rs +++ b/core/src/bin/generate_typescript_types.rs @@ -57,6 +57,10 @@ fn main() -> Result<(), Box> { "// This file is auto-generated. See core/src/bin/generate_typescript_types.rs\n\n", ); + // Add Empty type for unit type (()) + typescript_code.push_str("// Empty type for operations with no input\n"); + typescript_code.push_str("export type Empty = Record;\n\n"); + // Generate all types using Specta TypeScript // Note: TypeScript Specta doesn't have built-in duplicate handling like Swift // The error about duplicate types is expected - multiple operations reference the same types diff --git a/core/src/infra/sync/registry.rs b/core/src/infra/sync/registry.rs index 456e91cbb..45bb571ce 100644 --- a/core/src/infra/sync/registry.rs +++ b/core/src/infra/sync/registry.rs @@ -535,7 +535,7 @@ pub fn get_fk_mappings(model_type: &str) -> Option> { FKMapping::new("entry_id", "entries"), ]), "user_metadata_tag" => Some(vec![ - FKMapping::new("metadata_id", "user_metadata"), + FKMapping::new("user_metadata_id", "user_metadata"), FKMapping::new("tag_id", "tag"), ]), "tag_relationship" => Some(vec![ diff --git a/package.json b/package.json index aa6c0e048..2f404b652 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { "name": "spacedrive", "private": true, - "scripts": {}, + "scripts": { + "preinstall": "npx only-allow bun" + }, "devDependencies": { "@babel/plugin-syntax-import-assertions": "^7.24.0", "@cspell/dict-rust": "^4.0.2", diff --git a/packages/interface/src/components/SpacesSidebar/DevicesGroup.tsx b/packages/interface/src/components/SpacesSidebar/DevicesGroup.tsx index 9ee6e5d3a..f72625f98 100644 --- a/packages/interface/src/components/SpacesSidebar/DevicesGroup.tsx +++ b/packages/interface/src/components/SpacesSidebar/DevicesGroup.tsx @@ -1,32 +1,13 @@ import { CaretRight, Desktop, WifiHigh } from "@phosphor-icons/react"; import clsx from "clsx"; import { useNavigate } from "react-router-dom"; -import { useLibraryQuery } from "../../context"; -import NodeIcon from "@sd/assets/icons/Node.png"; -import LaptopIcon from "@sd/assets/icons/Laptop.png"; -import MobileIcon from "@sd/assets/icons/Mobile.png"; -import PCIcon from "@sd/assets/icons/PC.png"; +import { useLibraryQuery, getDeviceIcon } from "../../context"; interface DevicesGroupProps { isCollapsed: boolean; onToggle: () => void; } -// Helper to get icon based on OS -function getDeviceIcon(os: string): string { - const osLower = os.toLowerCase(); - if (osLower.includes("mac") || osLower.includes("darwin")) { - return LaptopIcon; - } - if (osLower.includes("ios") || osLower.includes("android")) { - return MobileIcon; - } - if (osLower.includes("windows") || osLower.includes("linux")) { - return PCIcon; - } - return NodeIcon; -} - export function DevicesGroup({ isCollapsed, onToggle }: DevicesGroupProps) { const navigate = useNavigate(); @@ -77,7 +58,7 @@ export function DevicesGroup({ isCollapsed, onToggle }: DevicesGroupProps) { )} > {/* Device Icon */} - + {/* Device Name */} {device.name} diff --git a/packages/interface/src/components/SpacesSidebar/VolumesGroup.tsx b/packages/interface/src/components/SpacesSidebar/VolumesGroup.tsx index cf2eb15bf..29e183325 100644 --- a/packages/interface/src/components/SpacesSidebar/VolumesGroup.tsx +++ b/packages/interface/src/components/SpacesSidebar/VolumesGroup.tsx @@ -1,20 +1,9 @@ import { CaretRight } from "@phosphor-icons/react"; import clsx from "clsx"; import { useNavigate } from "react-router-dom"; -import { useNormalizedCache } from "@sd/ts-client"; +import { useNormalizedCache, getVolumeIcon } from "@sd/ts-client"; import { SpaceItem } from "./SpaceItem"; -import type { VolumeItem, CloudServiceType } from "@sd/ts-client"; - -// Import cloud provider icons -import DriveAmazonS3 from "@sd/assets/icons/Drive-AmazonS3.png"; -import DriveGoogleDrive from "@sd/assets/icons/Drive-GoogleDrive.png"; -import DriveDropbox from "@sd/assets/icons/Drive-Dropbox.png"; -import DriveOneDrive from "@sd/assets/icons/Drive-OneDrive.png"; -import DriveBackBlaze from "@sd/assets/icons/Drive-BackBlaze.png"; -import DrivePCloud from "@sd/assets/icons/Drive-PCloud.png"; -import DriveBox from "@sd/assets/icons/Drive-Box.png"; -import HDDIcon from "@sd/assets/icons/HDD.png"; -import DriveIcon from "@sd/assets/icons/Drive.png"; +import type { VolumeItem } from "@sd/ts-client"; interface VolumesGroupProps { isCollapsed: boolean; @@ -23,60 +12,6 @@ interface VolumesGroupProps { filter?: "TrackedOnly" | "UntrackedOnly" | "All"; } -// Map cloud service types to icons -const cloudProviderIcons: Record = { - s3: DriveAmazonS3, - gdrive: DriveGoogleDrive, - dropbox: DriveDropbox, - onedrive: DriveOneDrive, - gcs: DriveGoogleDrive, - azblob: DriveBox, - b2: DriveBackBlaze, - wasabi: DriveAmazonS3, - spaces: DriveAmazonS3, - cloud: DrivePCloud, -}; - -// Helper to parse cloud service from volume fingerprint -function parseCloudService(volume: VolumeItem): CloudServiceType | null { - // Check if this is a cloud volume by looking at mount_point pattern - const mountPoint = volume.mount_point; - if (!mountPoint) return null; - - // Parse mount_point for cloud service (format: "s3://bucket-name") - const match = mountPoint.match(/^(\w+):\/\//); - if (!match) return null; - - const scheme = match[1]; - - // Verify it's a cloud scheme (not file:// or other local schemes) - if (scheme === "s3" || scheme === "gdrive" || scheme === "dropbox" || - scheme === "onedrive" || scheme === "gcs" || scheme === "azblob" || - scheme === "b2" || scheme === "wasabi" || scheme === "spaces" || - scheme === "cloud") { - return scheme as CloudServiceType; - } - - return null; -} - -// Get icon for a volume based on its type -function getVolumeIcon(volume: VolumeItem): string { - // Check if it's a cloud volume (by mount_point pattern or filesystem type) - const cloudService = parseCloudService(volume); - if (cloudService) { - return cloudProviderIcons[cloudService] || DriveIcon; - } - - // For external drives, use HDD icon - if (volume.volume_type === "External") { - return HDDIcon; - } - - // Default to generic drive icon - return DriveIcon; -} - export function VolumesGroup({ isCollapsed, onToggle, diff --git a/packages/interface/src/components/SyncSetupModal.tsx b/packages/interface/src/components/SyncSetupModal.tsx index d7d995f48..6e2967deb 100644 --- a/packages/interface/src/components/SyncSetupModal.tsx +++ b/packages/interface/src/components/SyncSetupModal.tsx @@ -1,470 +1,515 @@ -import { useMemo, useState } from 'react'; -import { useForm } from 'react-hook-form'; +import { useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; import { - ArrowsClockwise, - CheckCircle, - CircleNotch, - DeviceMobile, - Share, - SignIn -} from '@phosphor-icons/react'; -import type { LibrarySyncAction, PairedDeviceInfo, RemoteLibraryInfo } from '@sd/ts-client'; -import { Button, Dialog, dialogManager, useDialog } from '@sd/ui'; -import { useCoreQuery, useCoreMutation, useSpacedriveClient } from '../context'; + ArrowsClockwise, + CheckCircle, + CircleNotch, + DeviceMobile, + Share, + SignIn, +} from "@phosphor-icons/react"; +import type { + LibrarySyncAction, + PairedDeviceInfo, + RemoteLibraryInfo, +} from "@sd/ts-client"; +import { Button, Dialog, dialogManager, useDialog } from "@sd/ui"; +import { useCoreQuery, useCoreMutation, useSpacedriveClient } from "../context"; interface SyncSetupDialogProps { - id: number; + id: number; } -type SyncStep = 'select-device' | 'choose-action' | 'confirm' | 'executing'; +type SyncStep = "select-device" | "choose-action" | "confirm" | "executing"; export function useSyncSetupDialog() { - return dialogManager.create((props: SyncSetupDialogProps) => ( - - )); + return dialogManager.create((props: SyncSetupDialogProps) => ( + + )); } function SyncSetupDialog(props: SyncSetupDialogProps) { - const dialog = useDialog(props); - const client = useSpacedriveClient(); - const [step, setStep] = useState('select-device'); - const [selectedDevice, setSelectedDevice] = useState(null); - const [selectedAction, setSelectedAction] = useState<'share' | 'join' | null>(null); - const [selectedRemoteLibrary, setSelectedRemoteLibrary] = - useState(null); + const dialog = useDialog(props); + const client = useSpacedriveClient(); + const [step, setStep] = useState("select-device"); + const [selectedDevice, setSelectedDevice] = useState( + null, + ); + const [selectedAction, setSelectedAction] = useState<"share" | "join" | null>( + null, + ); + const [selectedRemoteLibrary, setSelectedRemoteLibrary] = + useState(null); - // Get current device info and library - const { data: coreStatus } = useCoreQuery({ - type: 'core.status', - input: {} - }); + // Get current device info and library + const { data: coreStatus, isLoading, error, isFetching } = useCoreQuery({ + type: "core.status", + input: null as any, // Unit type () in Rust = null in JSON + }); - const currentLibraryId = client.getCurrentLibraryId(); - const currentDeviceId = coreStatus?.device_info.id; + console.log({ + coreStatus, + isLoading, + error: error?.message || error, + isFetching, + hasData: !!coreStatus + }); - // Query paired devices - const pairedDevicesQuery = useCoreQuery({ - type: 'network.devices.list', - input: { connectedOnly: false } - }); + const currentLibraryId = client.getCurrentLibraryId(); + const currentDeviceId = coreStatus?.device_info.id; - // Query remote libraries when device is selected - const discoveryQuery = useCoreQuery( - { - type: 'network.sync_setup.discover', - input: { deviceId: selectedDevice?.id || '' } - }, - { - enabled: selectedDevice !== null - } - ); + // Query paired devices + const pairedDevicesQuery = useCoreQuery({ + type: "network.devices.list", + input: { connectedOnly: false }, + }); - // Sync setup mutation - const syncSetupMutation = useCoreMutation('network.sync_setup', { - onSuccess: () => { - // Close dialog on success - dialog.state.open = false; - } - }); + // Query remote libraries when device is selected + const discoveryQuery = useCoreQuery( + { + type: "network.sync_setup.discover", + input: { deviceId: selectedDevice?.id || "" }, + }, + { + enabled: selectedDevice !== null, + }, + ); - const form = useForm(); + // Sync setup mutation + const syncSetupMutation = useCoreMutation("network.sync_setup", { + onSuccess: () => { + // Close dialog on success + dialog.state.open = false; + }, + }); - const handleDeviceSelect = (device: PairedDeviceInfo) => { - setSelectedDevice(device); - setStep('choose-action'); - }; + const form = useForm(); - const handleActionSelect = (action: 'share' | 'join', library?: RemoteLibraryInfo) => { - setSelectedAction(action); - if (action === 'join' && library) { - setSelectedRemoteLibrary(library); - } - setStep('confirm'); - }; + const handleDeviceSelect = (device: PairedDeviceInfo) => { + setSelectedDevice(device); + setStep("choose-action"); + }; - const handleConfirm = async () => { - if (!selectedDevice || !selectedAction || !currentLibraryId || !currentDeviceId) return; + const handleActionSelect = ( + action: "share" | "join", + library?: RemoteLibraryInfo, + ) => { + setSelectedAction(action); + if (action === "join" && library) { + setSelectedRemoteLibrary(library); + } + setStep("confirm"); + }; - setStep('executing'); + const handleConfirm = async () => { + console.log("Confirming sync setup", { + selectedDevice, + selectedAction, + currentLibraryId, + currentDeviceId, + }); + if ( + !selectedDevice || + !selectedAction || + !currentLibraryId || + !currentDeviceId + ) + return; - // Build the LibrarySyncAction - let action: LibrarySyncAction; - let remoteLibraryId: string | undefined; + setStep("executing"); - // Get current library info - const currentLibrary = coreStatus?.libraries.find((lib) => lib.id === currentLibraryId); - const libraryName = currentLibrary?.name || 'My Library'; + // Build the LibrarySyncAction + let action: LibrarySyncAction; + let remoteLibraryId: string | undefined; - if (selectedAction === 'share') { - action = { - type: 'shareLocalLibrary', - libraryName - }; - } else if (selectedAction === 'join' && selectedRemoteLibrary) { - action = { - type: 'joinRemoteLibrary', - remoteLibraryId: selectedRemoteLibrary.id, - remoteLibraryName: selectedRemoteLibrary.name - }; - remoteLibraryId = selectedRemoteLibrary.id; - } else { - return; - } + // Get current library info + const currentLibrary = coreStatus?.libraries.find( + (lib) => lib.id === currentLibraryId, + ); + const libraryName = currentLibrary?.name || "My Library"; - // Execute sync setup - syncSetupMutation.mutate({ - localDeviceId: currentDeviceId, - remoteDeviceId: selectedDevice.id, - localLibraryId: currentLibraryId, - remoteLibraryId, - action, - leaderDeviceId: currentDeviceId // Deprecated but required - }); - }; + if (selectedAction === "share") { + action = { + type: "shareLocalLibrary", + libraryName, + }; + } else if (selectedAction === "join" && selectedRemoteLibrary) { + action = { + type: "joinRemoteLibrary", + remoteLibraryId: selectedRemoteLibrary.id, + remoteLibraryName: selectedRemoteLibrary.name, + }; + remoteLibraryId = selectedRemoteLibrary.id; + } else { + return; + } - const renderContent = () => { - switch (step) { - case 'select-device': - return ( - - ); + const data = { + localDeviceId: currentDeviceId, + remoteDeviceId: selectedDevice.id, + localLibraryId: currentLibraryId, + remoteLibraryId, + action, + leaderDeviceId: currentDeviceId, // Deprecated but required + }; - case 'choose-action': - return ( - setStep('select-device')} - /> - ); + console.log({ data }); - case 'confirm': - return ( - setStep('choose-action')} - onConfirm={handleConfirm} - /> - ); + // Execute sync setup + syncSetupMutation.mutate(data); + }; - case 'executing': - return ( - - ); - } - }; + const renderContent = () => { + switch (step) { + case "select-device": + return ( + + ); - return ( - } - closeBtn - hideButtons - > -
{renderContent()}
-
- ); + case "choose-action": + return ( + setStep("select-device")} + /> + ); + + case "confirm": + return ( + setStep("choose-action")} + onConfirm={handleConfirm} + /> + ); + + case "executing": + return ( + + ); + } + }; + + return ( + } + closeBtn + hideButtons + > +
{renderContent()}
+
+ ); } // Step 1: Select Device interface SelectDeviceStepProps { - devices: PairedDeviceInfo[]; - isLoading: boolean; - onSelect: (device: PairedDeviceInfo) => void; + devices: PairedDeviceInfo[]; + isLoading: boolean; + onSelect: (device: PairedDeviceInfo) => void; } -function SelectDeviceStep({ devices, isLoading, onSelect }: SelectDeviceStepProps) { - if (isLoading) { - return ( -
- -
- ); - } +function SelectDeviceStep({ + devices, + isLoading, + onSelect, +}: SelectDeviceStepProps) { + if (isLoading) { + return ( +
+ +
+ ); + } - if (devices.length === 0) { - return ( -
- -
-

No paired devices found

-

- Pair a device first using the "Pair Device" button -

-
-
- ); - } + if (devices.length === 0) { + return ( +
+ +
+

No paired devices found

+

+ Pair a device first using the "Pair Device" button +

+
+
+ ); + } - return ( -
-

- Select a paired device to sync your library with: -

-
- {devices.map((device) => ( - - ))} -
-
- ); + return ( +
+

+ Select a paired device to sync your library with: +

+
+ {devices.map((device) => ( + + ))} +
+
+ ); } // Step 2: Choose Action interface ChooseActionStepProps { - device: PairedDeviceInfo; - remoteLibraries: RemoteLibraryInfo[]; - isOnline: boolean; - isLoading: boolean; - onSelectAction: (action: 'share' | 'join', library?: RemoteLibraryInfo) => void; - onBack: () => void; + device: PairedDeviceInfo; + remoteLibraries: RemoteLibraryInfo[]; + isOnline: boolean; + isLoading: boolean; + onSelectAction: ( + action: "share" | "join", + library?: RemoteLibraryInfo, + ) => void; + onBack: () => void; } function ChooseActionStep({ - device, - remoteLibraries, - isOnline, - isLoading, - onSelectAction, - onBack + device, + remoteLibraries, + isOnline, + isLoading, + onSelectAction, + onBack, }: ChooseActionStepProps) { - if (isLoading) { - return ( -
- -

Discovering libraries...

-
- ); - } + if (isLoading) { + return ( +
+ +

Discovering libraries...

+
+ ); + } - if (!isOnline) { - return ( -
- -
-

Device is offline

-

- {device.name} must be online to set up sync -

-
- -
- ); - } + if (!isOnline) { + return ( +
+ +
+

Device is offline

+

+ {device.name} must be online to set up sync +

+
+ +
+ ); + } - return ( -
-
-

- Syncing with: {device.name} -

-
+ return ( +
+
+

+ Syncing with:{" "} + {device.name} +

+
-
-

Choose an action:

+
+

Choose an action:

- {/* Share Local Library */} - + {/* Share Local Library */} + - {/* Join Remote Library */} - {remoteLibraries.length > 0 ? ( -
-

- Or join a library from {device.name}: -

- {remoteLibraries.map((library) => ( - - ))} -
- ) : ( -
-

- No libraries found on {device.name} -

-
- )} -
+ {/* Join Remote Library */} + {remoteLibraries.length > 0 ? ( +
+

+ Or join a library from {device.name}: +

+ {remoteLibraries.map((library) => ( + + ))} +
+ ) : ( +
+

+ No libraries found on {device.name} +

+
+ )} +
-
- -
-
- ); +
+ +
+
+ ); } // Step 3: Confirm interface ConfirmStepProps { - device: PairedDeviceInfo; - action: 'share' | 'join'; - remoteLibrary: RemoteLibraryInfo | null; - onBack: () => void; - onConfirm: () => void; + device: PairedDeviceInfo; + action: "share" | "join"; + remoteLibrary: RemoteLibraryInfo | null; + onBack: () => void; + onConfirm: () => void; } -function ConfirmStep({ device, action, remoteLibrary, onBack, onConfirm }: ConfirmStepProps) { - return ( -
-
-

Sync Configuration

-
-
- Remote Device: - {device.name} -
-
- Action: - - {action === 'share' ? 'Share My Library' : 'Join Remote Library'} - -
- {action === 'join' && remoteLibrary && ( -
- Remote Library: - - {remoteLibrary.name} - -
- )} -
-
+function ConfirmStep({ + device, + action, + remoteLibrary, + onBack, + onConfirm, +}: ConfirmStepProps) { + return ( +
+
+

Sync Configuration

+
+
+ Remote Device: + {device.name} +
+
+ Action: + + {action === "share" ? "Share My Library" : "Join Remote Library"} + +
+ {action === "join" && remoteLibrary && ( +
+ Remote Library: + {remoteLibrary.name} +
+ )} +
+
-
-

- {action === 'share' - ? 'This will create a synchronized copy of your library on the remote device. Both devices will stay in sync.' - : 'This will download the remote library to your device. Your local library will sync with theirs.'} -

-
+
+

+ {action === "share" + ? "This will create a synchronized copy of your library on the remote device. Both devices will stay in sync." + : "This will download the remote library to your device. Your local library will sync with theirs."} +

+
-
- - -
-
- ); +
+ + +
+
+ ); } // Step 4: Executing interface ExecutingStepProps { - isLoading: boolean; - error?: string; + isLoading: boolean; + error?: string; } function ExecutingStep({ isLoading, error }: ExecutingStepProps) { - if (isLoading) { - return ( -
- -

Setting up sync...

-
- ); - } + if (isLoading) { + return ( +
+ +

Setting up sync...

+
+ ); + } - if (error) { - return ( -
-
-

Sync setup failed

-

{error}

-
-
- ); - } + if (error) { + return ( +
+
+

Sync setup failed

+

{error}

+
+
+ ); + } - return ( -
- -
-

Sync setup complete!

-

Your library is now syncing

-
-
- ); + return ( +
+ +
+

Sync setup complete!

+

Your library is now syncing

+
+
+ ); } diff --git a/packages/interface/src/context.tsx b/packages/interface/src/context.tsx index 6e8793eda..31528f78c 100644 --- a/packages/interface/src/context.tsx +++ b/packages/interface/src/context.tsx @@ -28,4 +28,7 @@ export type { LocationInfo, LocationsListOutput, LibraryInfo, -} from "@sd/ts-client"; \ No newline at end of file +} from "@sd/ts-client"; + +// Export icon utilities +export { getDeviceIcon, getVolumeIcon } from "@sd/ts-client"; \ No newline at end of file diff --git a/packages/ts-client/src/deviceIcons.ts b/packages/ts-client/src/deviceIcons.ts index ac147a452..d48714f82 100644 --- a/packages/ts-client/src/deviceIcons.ts +++ b/packages/ts-client/src/deviceIcons.ts @@ -24,13 +24,14 @@ export function getDeviceIcon(device: LibraryDeviceInfo): DeviceIcon { if (device.hardware_model) { const model = device.hardware_model.toLowerCase(); - // Mac Studio - if (model.includes("mac studio") || model.includes("macstudio")) { + // Mac Studio: Mac13,1 Mac13,2 (M1 Max/Ultra 2022), Mac14,13 Mac14,14 (M2 Max/Ultra 2023) + // Mac Pro: Mac14,8 (M2 Ultra 2023) + if (model.match(/mac1[34],(1|2|8|13|14)/)) { return SilverBox; } - // Mac Mini - if (model.includes("mac mini") || model.includes("macmini")) { + // Mac Mini: Mac14,3 Mac14,12 (M2/Pro 2023), Mac15,12 Mac15,13 (M4 2024) + if (model.match(/mac1[45],(3|12|13)/)) { return MiniSilverBox; } diff --git a/packages/ts-client/src/generated/types.ts b/packages/ts-client/src/generated/types.ts index 36e54956a..2f1f303ab 100644 --- a/packages/ts-client/src/generated/types.ts +++ b/packages/ts-client/src/generated/types.ts @@ -1,6 +1,9 @@ // Generated by Spacedrive using Specta + rspc-inspired type extraction - DO NOT EDIT // This file is auto-generated. See core/src/bin/generate_typescript_types.rs +// Empty type for operations with no input +export type Empty = Record; + // This file has been generated by Specta. DO NOT EDIT. export type ActionContextInfo = { action_type: string; initiated_at: string; initiated_by: string | null; action_input: JsonValue; context: JsonValue }; diff --git a/packages/ts-client/src/index.ts b/packages/ts-client/src/index.ts index 27c47bb0b..d3cecbd88 100644 --- a/packages/ts-client/src/index.ts +++ b/packages/ts-client/src/index.ts @@ -59,8 +59,9 @@ export * from "./hooks"; // Zustand stores export * from "./stores/sidebar"; -// Device utilities +// Device and volume utilities export * from "./deviceIcons"; +export * from "./volumeIcons"; // All auto-generated types export * from "./generated/types"; diff --git a/packages/ts-client/src/volumeIcons.ts b/packages/ts-client/src/volumeIcons.ts new file mode 100644 index 000000000..b54223d13 --- /dev/null +++ b/packages/ts-client/src/volumeIcons.ts @@ -0,0 +1,88 @@ +// @ts-nocheck +import type { CloudServiceType } from "./generated/types"; +import DriveAmazonS3 from "@sd/assets/icons/Drive-AmazonS3.png"; +import DriveGoogleDrive from "@sd/assets/icons/Drive-GoogleDrive.png"; +import DriveDropbox from "@sd/assets/icons/Drive-Dropbox.png"; +import DriveOneDrive from "@sd/assets/icons/Drive-OneDrive.png"; +import DriveBackBlaze from "@sd/assets/icons/Drive-BackBlaze.png"; +import DrivePCloud from "@sd/assets/icons/Drive-PCloud.png"; +import DriveBox from "@sd/assets/icons/Drive-Box.png"; +import HDDIcon from "@sd/assets/icons/HDD.png"; +import DriveIcon from "@sd/assets/icons/Drive.png"; + +export type VolumeIcon = string; + +// Map cloud service types to icons +const cloudProviderIcons: Record = { + s3: DriveAmazonS3, + gdrive: DriveGoogleDrive, + dropbox: DriveDropbox, + onedrive: DriveOneDrive, + gcs: DriveGoogleDrive, + azblob: DriveBox, + b2: DriveBackBlaze, + wasabi: DriveAmazonS3, + spaces: DriveAmazonS3, + cloud: DrivePCloud, +}; + +/** + * Parse cloud service type from volume mount point. + * Cloud volumes typically have mount points like "s3://bucket-name" + */ +function parseCloudService(mountPoint: string | null): CloudServiceType | null { + if (!mountPoint) return null; + + // Parse mount_point for cloud service (format: "s3://bucket-name") + const match = mountPoint.match(/^(\w+):\/\//); + if (!match) return null; + + const scheme = match[1]; + + // Verify it's a cloud scheme (not file:// or other local schemes) + const cloudSchemes: CloudServiceType[] = [ + "s3", + "gdrive", + "dropbox", + "onedrive", + "gcs", + "azblob", + "b2", + "wasabi", + "spaces", + "cloud", + ]; + + if (cloudSchemes.includes(scheme as CloudServiceType)) { + return scheme as CloudServiceType; + } + + return null; +} + +/** + * Determines the appropriate volume icon based on volume information. + * + * Priority order: + * 1. Cloud service type (parsed from mount point) + * 2. Volume type (External vs Internal) + * 3. Default to generic drive icon + */ +export function getVolumeIcon(volume: { + mount_point: string | null; + volume_type?: "Internal" | "External" | "Removable"; +}): VolumeIcon { + // Check if it's a cloud volume + const cloudService = parseCloudService(volume.mount_point); + if (cloudService) { + return cloudProviderIcons[cloudService] || DriveIcon; + } + + // For external/removable drives, use HDD icon + if (volume.volume_type === "External" || volume.volume_type === "Removable") { + return HDDIcon; + } + + // Default to generic drive icon + return DriveIcon; +}