mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-09 07:43:21 -04:00
Add Empty type and centralized icon utilities
This commit is contained in:
@@ -57,6 +57,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"// 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<string, never>;\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
|
||||
|
||||
@@ -535,7 +535,7 @@ pub fn get_fk_mappings(model_type: &str) -> Option<Vec<super::FKMapping>> {
|
||||
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![
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 */}
|
||||
<img src={getDeviceIcon(device.os)} alt="" className="size-4" />
|
||||
<img src={getDeviceIcon(device)} alt="" className="size-4" />
|
||||
|
||||
{/* Device Name */}
|
||||
<span className="flex-1 truncate text-left">{device.name}</span>
|
||||
|
||||
@@ -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<CloudServiceType, string> = {
|
||||
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,
|
||||
|
||||
@@ -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) => (
|
||||
<SyncSetupDialog {...props} />
|
||||
));
|
||||
return dialogManager.create((props: SyncSetupDialogProps) => (
|
||||
<SyncSetupDialog {...props} />
|
||||
));
|
||||
}
|
||||
|
||||
function SyncSetupDialog(props: SyncSetupDialogProps) {
|
||||
const dialog = useDialog(props);
|
||||
const client = useSpacedriveClient();
|
||||
const [step, setStep] = useState<SyncStep>('select-device');
|
||||
const [selectedDevice, setSelectedDevice] = useState<PairedDeviceInfo | null>(null);
|
||||
const [selectedAction, setSelectedAction] = useState<'share' | 'join' | null>(null);
|
||||
const [selectedRemoteLibrary, setSelectedRemoteLibrary] =
|
||||
useState<RemoteLibraryInfo | null>(null);
|
||||
const dialog = useDialog(props);
|
||||
const client = useSpacedriveClient();
|
||||
const [step, setStep] = useState<SyncStep>("select-device");
|
||||
const [selectedDevice, setSelectedDevice] = useState<PairedDeviceInfo | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedAction, setSelectedAction] = useState<"share" | "join" | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedRemoteLibrary, setSelectedRemoteLibrary] =
|
||||
useState<RemoteLibraryInfo | null>(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 (
|
||||
<SelectDeviceStep
|
||||
devices={pairedDevicesQuery.data?.devices || []}
|
||||
isLoading={pairedDevicesQuery.isLoading}
|
||||
onSelect={handleDeviceSelect}
|
||||
/>
|
||||
);
|
||||
const data = {
|
||||
localDeviceId: currentDeviceId,
|
||||
remoteDeviceId: selectedDevice.id,
|
||||
localLibraryId: currentLibraryId,
|
||||
remoteLibraryId,
|
||||
action,
|
||||
leaderDeviceId: currentDeviceId, // Deprecated but required
|
||||
};
|
||||
|
||||
case 'choose-action':
|
||||
return (
|
||||
<ChooseActionStep
|
||||
device={selectedDevice!}
|
||||
remoteLibraries={discoveryQuery.data?.libraries || []}
|
||||
isOnline={discoveryQuery.data?.isOnline || false}
|
||||
isLoading={discoveryQuery.isLoading}
|
||||
onSelectAction={handleActionSelect}
|
||||
onBack={() => setStep('select-device')}
|
||||
/>
|
||||
);
|
||||
console.log({ data });
|
||||
|
||||
case 'confirm':
|
||||
return (
|
||||
<ConfirmStep
|
||||
device={selectedDevice!}
|
||||
action={selectedAction!}
|
||||
remoteLibrary={selectedRemoteLibrary}
|
||||
onBack={() => setStep('choose-action')}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
);
|
||||
// Execute sync setup
|
||||
syncSetupMutation.mutate(data);
|
||||
};
|
||||
|
||||
case 'executing':
|
||||
return (
|
||||
<ExecutingStep
|
||||
isLoading={syncSetupMutation.isPending}
|
||||
error={syncSetupMutation.error?.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
const renderContent = () => {
|
||||
switch (step) {
|
||||
case "select-device":
|
||||
return (
|
||||
<SelectDeviceStep
|
||||
devices={pairedDevicesQuery.data?.devices || []}
|
||||
isLoading={pairedDevicesQuery.isLoading}
|
||||
onSelect={handleDeviceSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
dialog={dialog}
|
||||
form={form}
|
||||
title="Setup Library Sync"
|
||||
description="Sync your library with another device"
|
||||
icon={<ArrowsClockwise />}
|
||||
closeBtn
|
||||
hideButtons
|
||||
>
|
||||
<div className="min-h-[400px]">{renderContent()}</div>
|
||||
</Dialog>
|
||||
);
|
||||
case "choose-action":
|
||||
return (
|
||||
<ChooseActionStep
|
||||
device={selectedDevice!}
|
||||
remoteLibraries={discoveryQuery.data?.libraries || []}
|
||||
isOnline={discoveryQuery.data?.isOnline || false}
|
||||
isLoading={discoveryQuery.isLoading}
|
||||
onSelectAction={handleActionSelect}
|
||||
onBack={() => setStep("select-device")}
|
||||
/>
|
||||
);
|
||||
|
||||
case "confirm":
|
||||
return (
|
||||
<ConfirmStep
|
||||
device={selectedDevice!}
|
||||
action={selectedAction!}
|
||||
remoteLibrary={selectedRemoteLibrary}
|
||||
onBack={() => setStep("choose-action")}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
);
|
||||
|
||||
case "executing":
|
||||
return (
|
||||
<ExecutingStep
|
||||
isLoading={syncSetupMutation.isPending}
|
||||
error={syncSetupMutation.error?.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
dialog={dialog}
|
||||
form={form}
|
||||
title="Setup Library Sync"
|
||||
description="Sync your library with another device"
|
||||
icon={<ArrowsClockwise />}
|
||||
closeBtn
|
||||
hideButtons
|
||||
>
|
||||
<div className="min-h-[400px]">{renderContent()}</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<CircleNotch className="animate-spin" size={32} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function SelectDeviceStep({
|
||||
devices,
|
||||
isLoading,
|
||||
onSelect,
|
||||
}: SelectDeviceStepProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<CircleNotch className="animate-spin" size={32} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (devices.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||
<DeviceMobile size={48} className="text-ink-faint" />
|
||||
<div className="text-center">
|
||||
<p className="text-ink-dull">No paired devices found</p>
|
||||
<p className="text-sm text-ink-faint">
|
||||
Pair a device first using the "Pair Device" button
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (devices.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||
<DeviceMobile size={48} className="text-ink-faint" />
|
||||
<div className="text-center">
|
||||
<p className="text-ink-dull">No paired devices found</p>
|
||||
<p className="text-sm text-ink-faint">
|
||||
Pair a device first using the "Pair Device" button
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-ink-dull">
|
||||
Select a paired device to sync your library with:
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{devices.map((device) => (
|
||||
<button
|
||||
key={device.id}
|
||||
onClick={() => onSelect(device)}
|
||||
className="w-full rounded-lg border border-app-line bg-app-box p-4 text-left transition-colors hover:border-accent hover:bg-app-darkBox"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<DeviceMobile size={20} />
|
||||
<h3 className="font-medium text-ink">{device.name}</h3>
|
||||
{device.isConnected && (
|
||||
<span className="rounded-full bg-green-500 px-2 py-0.5 text-xs text-white">
|
||||
Connected
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-ink-dull">
|
||||
{device.deviceType} • {device.osVersion}
|
||||
</p>
|
||||
<p className="text-xs text-ink-faint">
|
||||
Last seen:{' '}
|
||||
{new Date(device.lastSeen).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-ink-dull">
|
||||
Select a paired device to sync your library with:
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{devices.map((device) => (
|
||||
<button
|
||||
key={device.id}
|
||||
onClick={() => onSelect(device)}
|
||||
className="w-full rounded-lg border border-app-line bg-app-box p-4 text-left transition-colors hover:border-accent hover:bg-app-darkBox"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<DeviceMobile size={20} />
|
||||
<h3 className="font-medium text-ink">{device.name}</h3>
|
||||
{device.isConnected && (
|
||||
<span className="rounded-full bg-green-500 px-2 py-0.5 text-xs text-white">
|
||||
Connected
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-ink-dull">
|
||||
{device.deviceType} • {device.osVersion}
|
||||
</p>
|
||||
<p className="text-xs text-ink-faint">
|
||||
Last seen: {new Date(device.lastSeen).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<CircleNotch className="animate-spin" size={32} />
|
||||
<p className="ml-3 text-ink-dull">Discovering libraries...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<CircleNotch className="animate-spin" size={32} />
|
||||
<p className="ml-3 text-ink-dull">Discovering libraries...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isOnline) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||
<DeviceMobile size={48} className="text-ink-faint" />
|
||||
<div className="text-center">
|
||||
<p className="text-ink-dull">Device is offline</p>
|
||||
<p className="text-sm text-ink-faint">
|
||||
{device.name} must be online to set up sync
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!isOnline) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||
<DeviceMobile size={48} className="text-ink-faint" />
|
||||
<div className="text-center">
|
||||
<p className="text-ink-dull">Device is offline</p>
|
||||
<p className="text-sm text-ink-faint">
|
||||
{device.name} must be online to set up sync
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-ink-dull">
|
||||
Syncing with: <span className="font-medium text-ink">{device.name}</span>
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-ink-dull">
|
||||
Syncing with:{" "}
|
||||
<span className="font-medium text-ink">{device.name}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-medium text-ink">Choose an action:</h3>
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-medium text-ink">Choose an action:</h3>
|
||||
|
||||
{/* Share Local Library */}
|
||||
<button
|
||||
onClick={() => onSelectAction('share')}
|
||||
className="w-full rounded-lg border border-app-line bg-app-box p-4 text-left transition-colors hover:border-accent hover:bg-app-darkBox"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Share size={24} className="mt-1 text-accent" />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-ink">
|
||||
Share my library to this device
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-ink-dull">
|
||||
Create a shared library from your local library. The other
|
||||
device will receive a copy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/* Share Local Library */}
|
||||
<button
|
||||
onClick={() => onSelectAction("share")}
|
||||
className="w-full rounded-lg border border-app-line bg-app-box p-4 text-left transition-colors hover:border-accent hover:bg-app-darkBox"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Share size={24} className="mt-1 text-accent" />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-ink">
|
||||
Share my library to this device
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-ink-dull">
|
||||
Create a shared library from your local library. The other
|
||||
device will receive a copy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Join Remote Library */}
|
||||
{remoteLibraries.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-ink">
|
||||
Or join a library from {device.name}:
|
||||
</h4>
|
||||
{remoteLibraries.map((library) => (
|
||||
<button
|
||||
key={library.id}
|
||||
onClick={() => onSelectAction('join', library)}
|
||||
className="w-full rounded-lg border border-app-line bg-app-box p-4 text-left transition-colors hover:border-accent hover:bg-app-darkBox"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<SignIn size={24} className="mt-1 text-accent" />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-ink">
|
||||
{library.name}
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-ink-dull">
|
||||
{library.statistics.total_files.toLocaleString()}{' '}
|
||||
files •{' '}
|
||||
{library.statistics.location_count.toLocaleString()}{' '}
|
||||
locations
|
||||
</p>
|
||||
<p className="text-xs text-ink-faint">
|
||||
Created:{' '}
|
||||
{new Date(library.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-app-line bg-app-box p-4">
|
||||
<p className="text-sm text-ink-faint">
|
||||
No libraries found on {device.name}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Join Remote Library */}
|
||||
{remoteLibraries.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-ink">
|
||||
Or join a library from {device.name}:
|
||||
</h4>
|
||||
{remoteLibraries.map((library) => (
|
||||
<button
|
||||
key={library.id}
|
||||
onClick={() => onSelectAction("join", library)}
|
||||
className="w-full rounded-lg border border-app-line bg-app-box p-4 text-left transition-colors hover:border-accent hover:bg-app-darkBox"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<SignIn size={24} className="mt-1 text-accent" />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-ink">{library.name}</h4>
|
||||
<p className="mt-1 text-sm text-ink-dull">
|
||||
{library.statistics.total_files.toLocaleString()} files •{" "}
|
||||
{library.statistics.location_count.toLocaleString()}{" "}
|
||||
locations
|
||||
</p>
|
||||
<p className="text-xs text-ink-faint">
|
||||
Created:{" "}
|
||||
{new Date(library.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-app-line bg-app-box p-4">
|
||||
<p className="text-sm text-ink-faint">
|
||||
No libraries found on {device.name}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-start">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="flex justify-start">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg bg-app-darkBox p-4">
|
||||
<h3 className="mb-3 font-medium text-ink">Sync Configuration</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink-dull">Remote Device:</span>
|
||||
<span className="font-medium text-ink">{device.name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink-dull">Action:</span>
|
||||
<span className="font-medium text-ink">
|
||||
{action === 'share' ? 'Share My Library' : 'Join Remote Library'}
|
||||
</span>
|
||||
</div>
|
||||
{action === 'join' && remoteLibrary && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink-dull">Remote Library:</span>
|
||||
<span className="font-medium text-ink">
|
||||
{remoteLibrary.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
function ConfirmStep({
|
||||
device,
|
||||
action,
|
||||
remoteLibrary,
|
||||
onBack,
|
||||
onConfirm,
|
||||
}: ConfirmStepProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg bg-app-darkBox p-4">
|
||||
<h3 className="mb-3 font-medium text-ink">Sync Configuration</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink-dull">Remote Device:</span>
|
||||
<span className="font-medium text-ink">{device.name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink-dull">Action:</span>
|
||||
<span className="font-medium text-ink">
|
||||
{action === "share" ? "Share My Library" : "Join Remote Library"}
|
||||
</span>
|
||||
</div>
|
||||
{action === "join" && remoteLibrary && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink-dull">Remote Library:</span>
|
||||
<span className="font-medium text-ink">{remoteLibrary.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-yellow-500/20 bg-yellow-500/10 p-4">
|
||||
<p className="text-sm text-yellow-200">
|
||||
{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.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-yellow-500/20 bg-yellow-500/10 p-4">
|
||||
<p className="text-sm text-yellow-200">
|
||||
{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."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={onBack} className="flex-1">
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="accent" onClick={onConfirm} className="flex-1">
|
||||
Confirm & Setup Sync
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={onBack} className="flex-1">
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="accent" onClick={onConfirm} className="flex-1">
|
||||
Confirm & Setup Sync
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Step 4: Executing
|
||||
interface ExecutingStepProps {
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
function ExecutingStep({ isLoading, error }: ExecutingStepProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||
<CircleNotch className="animate-spin" size={48} />
|
||||
<p className="text-ink-dull">Setting up sync...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||
<CircleNotch className="animate-spin" size={48} />
|
||||
<p className="text-ink-dull">Setting up sync...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||
<div className="text-red-500">
|
||||
<p className="font-medium">Sync setup failed</p>
|
||||
<p className="text-sm">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||
<div className="text-red-500">
|
||||
<p className="font-medium">Sync setup failed</p>
|
||||
<p className="text-sm">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||
<CheckCircle size={48} className="text-green-500" />
|
||||
<div className="text-center">
|
||||
<p className="font-medium text-ink">Sync setup complete!</p>
|
||||
<p className="text-sm text-ink-dull">Your library is now syncing</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||
<CheckCircle size={48} className="text-green-500" />
|
||||
<div className="text-center">
|
||||
<p className="font-medium text-ink">Sync setup complete!</p>
|
||||
<p className="text-sm text-ink-dull">Your library is now syncing</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,4 +28,7 @@ export type {
|
||||
LocationInfo,
|
||||
LocationsListOutput,
|
||||
LibraryInfo,
|
||||
} from "@sd/ts-client";
|
||||
} from "@sd/ts-client";
|
||||
|
||||
// Export icon utilities
|
||||
export { getDeviceIcon, getVolumeIcon } from "@sd/ts-client";
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, never>;
|
||||
|
||||
// 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 };
|
||||
|
||||
@@ -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";
|
||||
|
||||
88
packages/ts-client/src/volumeIcons.ts
Normal file
88
packages/ts-client/src/volumeIcons.ts
Normal file
@@ -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<CloudServiceType, string> = {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user