From 2ef179d4df908f64713521bf53b60a29ed056e01 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Tue, 11 Nov 2025 15:44:00 -0800 Subject: [PATCH] chore: update Zustand dependency to version 5.0.8, modify Cargo.toml for sd-core features, and enhance sidecar manager initialization in library management --- apps/cli/Cargo.toml | 2 +- apps/tauri/src-tauri/Cargo.toml | 2 +- bun.lock | 3 +- core/Cargo.toml | 2 +- core/src/library/manager.rs | 17 +- core/src/ops/libraries/create/action.rs | 15 + core/src/service/sidecar_manager.rs | 21 +- packages/interface | 2 +- packages/ts-client/package.json | 3 +- packages/ts-client/src/generated/types.ts | 468 ++++++++++++++++++---- packages/ts-client/src/index.ts | 3 + packages/ts-client/src/stores/sidebar.ts | 61 +++ scripts/bundle-libheif.sh | 27 ++ xtask/src/config.rs | 10 +- xtask/src/main.rs | 1 - xtask/src/native_deps.rs | 106 ++++- 16 files changed, 632 insertions(+), 111 deletions(-) create mode 100644 packages/ts-client/src/stores/sidebar.ts create mode 100755 scripts/bundle-libheif.sh diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml index 8e0abf076..84bae3091 100644 --- a/apps/cli/Cargo.toml +++ b/apps/cli/Cargo.toml @@ -16,7 +16,7 @@ inquire = "0.7" qr2term = "0.3" ratatui = "0.26" reqwest = { version = "0.12", features = ["json"] } -sd-core = { path = "../../core", features = ["cli"] } +sd-core = { path = "../../core", features = ["cli", "heif", "ffmpeg"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 6c929d25e..239e2231a 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -20,7 +20,7 @@ tauri-plugin-os = "2.0" # Core bridge sd-tauri-core = { path = "../sd-tauri-core" } -sd-core = { path = "../../../core" } +sd-core = { path = "../../../core", features = ["heif", "ffmpeg"] } # Async runtime tokio = { version = "1.40", features = ["full"] } diff --git a/bun.lock b/bun.lock index 6bb262187..ebd09ddd1 100644 --- a/bun.lock +++ b/bun.lock @@ -182,7 +182,7 @@ "sonner": "^1.0.3", "tailwind-merge": "^1.14.0", "zod": "^3.23", - "zustand": "^5.0.2", + "zustand": "^5.0.8", }, "devDependencies": { "@types/react": "npm:types-react@rc", @@ -196,6 +196,7 @@ "dependencies": { "@types/ws": "^8.0.0", "ws": "^8.0.0", + "zustand": "^5.0.8", }, "devDependencies": { "@tanstack/react-query": "^5.62.0", diff --git a/core/Cargo.toml b/core/Cargo.toml index 49c01d61a..142624c05 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,7 +10,7 @@ ffmpeg = ["dep:sd-ffmpeg"] # AI models support ai = [] # HEIF image support -heif = [] +heif = ["sd-images/heif"] # Mobile platform support mobile = [] # CLI support (clap is now always available) diff --git a/core/src/library/manager.rs b/core/src/library/manager.rs index 78e3e98cd..a3f50dd85 100644 --- a/core/src/library/manager.rs +++ b/core/src/library/manager.rs @@ -461,7 +461,19 @@ impl LibraryManager { libraries.insert(config.id, library.clone()); } - // Now that the library is registered in the context, resume interrupted jobs + // Initialize sidecar manager before resuming jobs + if let Some(sidecar_manager) = context.get_sidecar_manager().await { + if let Err(e) = sidecar_manager.init_library(&library).await { + error!( + "Failed to initialize sidecar manager for library {}: {}", + config.id, e + ); + } + } else { + warn!("Sidecar manager not available during library open"); + } + + // Now that the library is registered and sidecar manager is initialized, resume interrupted jobs if let Err(e) = library.jobs.resume_interrupted_jobs_after_load().await { warn!( "Failed to resume interrupted jobs for library {}: {}", @@ -469,9 +481,6 @@ impl LibraryManager { ); } - // Note: Sidecar manager initialization should be done by the Core when libraries are loaded - // This allows Core to pass its services reference - // Initialize sync service if networking is available // If networking isn't ready, sync simply won't be initialized until caller does it explicitly // TODO: maybe consider checking if networking is enabled rather than just checking if it's available diff --git a/core/src/ops/libraries/create/action.rs b/core/src/ops/libraries/create/action.rs index 3bcd806a0..5a45e189b 100644 --- a/core/src/ops/libraries/create/action.rs +++ b/core/src/ops/libraries/create/action.rs @@ -40,6 +40,21 @@ impl CoreAction for LibraryCreateAction { ) .await?; + // Initialize sidecar manager for the new library + if let Err(e) = context + .get_sidecar_manager() + .await + .ok_or_else(|| ActionError::Internal("Sidecar manager not available".to_string()))? + .init_library(&library) + .await + { + tracing::error!( + "Failed to initialize sidecar manager for library {}: {}", + library.id(), + e + ); + } + // Get the library details let library_id = library.id(); let name = library.name().await; diff --git a/core/src/service/sidecar_manager.rs b/core/src/service/sidecar_manager.rs index c36de26c3..bedd2ce32 100644 --- a/core/src/service/sidecar_manager.rs +++ b/core/src/service/sidecar_manager.rs @@ -47,8 +47,21 @@ impl SidecarManager { let library_path = library.path(); let sidecars_dir = library_path.join("sidecars"); + info!( + "Initializing sidecar manager for library {} at path: {}", + library.id(), + library_path.display() + ); + // Ensure sidecars directory exists - tokio::fs::create_dir_all(&sidecars_dir).await?; + tokio::fs::create_dir_all(&sidecars_dir).await.map_err(|e| { + error!( + "Failed to create sidecars directory {}: {}", + sidecars_dir.display(), + e + ); + e + })?; // Create path builder let builder = Arc::new(SidecarPathBuilder::new(&library_path)); @@ -56,7 +69,11 @@ impl SidecarManager { let mut builders = self.path_builders.write().await; builders.insert(library.id(), builder); - info!("Initialized sidecar manager for library {}", library.id()); + info!( + "Successfully initialized sidecar manager for library {} (path builders count: {})", + library.id(), + builders.len() + ); Ok(()) } diff --git a/packages/interface b/packages/interface index d23adbb23..d5d318f9b 160000 --- a/packages/interface +++ b/packages/interface @@ -1 +1 @@ -Subproject commit d23adbb237cc40008f3744a3b799b1ef1a838a44 +Subproject commit d5d318f9b98c0ba33c77be4d07fd0e15e13c97f1 diff --git a/packages/ts-client/package.json b/packages/ts-client/package.json index 6fe25b9f1..a1232051f 100644 --- a/packages/ts-client/package.json +++ b/packages/ts-client/package.json @@ -40,8 +40,9 @@ "typescript": "^5.0.0" }, "dependencies": { + "@types/ws": "^8.0.0", "ws": "^8.0.0", - "@types/ws": "^8.0.0" + "zustand": "^5.0.8" }, "files": [ "dist/**/*", diff --git a/packages/ts-client/src/generated/types.ts b/packages/ts-client/src/generated/types.ts index bf3294300..788860d41 100644 --- a/packages/ts-client/src/generated/types.ts +++ b/packages/ts-client/src/generated/types.ts @@ -5,6 +5,14 @@ export type ActionContextInfo = { action_type: string; initiated_at: string; initiated_by: string | null; action_input: JsonValue; context: JsonValue }; +export type AddGroupInput = { space_id: string; name: string; group_type: GroupType }; + +export type AddGroupOutput = { group: SpaceGroup }; + +export type AddItemInput = { space_id: string; group_id: string | null; item_type: ItemType }; + +export type AddItemOutput = { item: SpaceItem }; + /** * Represents an APFS container (physical storage with multiple volumes) */ @@ -197,6 +205,14 @@ export type DateField = "CreatedAt" | "ModifiedAt" | "AccessedAt"; */ export type DateRangeFilter = { field: DateField; start: string | null; end: string | null }; +export type DeleteGroupInput = { group_id: string }; + +export type DeleteGroupOutput = { success: boolean }; + +export type DeleteItemInput = { item_id: string }; + +export type DeleteItemOutput = { success: boolean }; + export type DeviceInfo = { id: string; name: string; os: string; hardware_model: string | null; created_at: string }; /** @@ -707,6 +723,35 @@ export type GetSyncMetricsOutput = { */ metrics: SyncMetricsSnapshot }; +/** + * Types of groups that can appear in a space + */ +export type GroupType = +/** + * Fixed quick navigation (Overview, Recents, Favorites) + */ +"QuickAccess" | +/** + * Device with its volumes and locations as children + */ +{ Device: { device_id: string } } | +/** + * All locations across all devices + */ +"Locations" | +/** + * Tag collection + */ +"Tags" | +/** + * Cloud storage providers + */ +"Cloud" | +/** + * User-defined custom group + */ +"Custom"; + /** * Canonical input for indexing requests from any interface (CLI, API, etc.) */ @@ -909,6 +954,39 @@ summary: string }; export type IssueType = { type: "MissingFromIndex" } | { type: "StaleInIndex" } | { type: "SizeMismatch" } | { type: "ModifiedTimeMismatch" } | { type: "InodeMismatch" } | { type: "ExtensionMismatch" } | { type: "ParentMismatch" } | { type: "KindMismatch" }; +/** + * Types of items that can appear in a group + */ +export type ItemType = +/** + * Overview screen (fixed) + */ +"Overview" | +/** + * Recent files (fixed) + */ +"Recents" | +/** + * Favorited files (fixed) + */ +"Favorites" | +/** + * Indexed location + */ +{ Location: { location_id: string } } | +/** + * Storage volume (with locations as children) + */ +{ Volume: { volume_id: string } } | +/** + * Tag filter + */ +{ Tag: { tag_id: string } } | +/** + * Any arbitrary path (dragged from explorer) + */ +{ Path: { sd_path: SdPath } }; + export type JobCancelInput = { job_id: string }; export type JobCancelOutput = { job_id: string; success: boolean }; @@ -1420,6 +1498,69 @@ export type LocationsListOutput = { locations: LocationInfo[] }; export type LocationsListQueryInput = null; +/** + * Input for media listing + */ +export type MediaListingInput = { +/** + * The directory path to list media for + */ +path: SdPath; +/** + * Whether to include media from descendant directories (default: false) + */ +include_descendants: boolean | null; +/** + * Which media types to include (default: both Image and Video) + */ +media_types: ContentKind[] | null; +/** + * Optional limit on number of results (default: 1000) + */ +limit: number | null; +/** + * Sort order for results + */ +sort_by: MediaSortBy }; + +/** + * Output containing media files + */ +export type MediaListingOutput = { +/** + * Media files (images/videos) + */ +files: File[]; +/** + * Total count of media files found + */ +total_count: number; +/** + * Whether there are more results than returned + */ +has_more: boolean }; + +/** + * Sort options for media listing + */ +export type MediaSortBy = +/** + * Sort by modification date (newest first) + */ +"modified" | +/** + * Sort by creation date (newest first) + */ +"created" | +/** + * Sort by name (alphabetical) + */ +"name" | +/** + * Sort by size (largest first) + */ +"size"; + /** * Mount type classification */ @@ -1625,6 +1766,12 @@ createdAt: string; */ statistics: LibraryStatistics }; +export type ReorderGroupsInput = { space_id: string; group_ids: string[] }; + +export type ReorderItemsInput = { group_id: string; item_ids: string[] }; + +export type ReorderOutput = { success: boolean }; + /** * Detailed breakdown of how the score was calculated */ @@ -1855,10 +2002,155 @@ export type SortField = "Relevance" | "Name" | "Size" | "ModifiedAt" | "CreatedA */ export type SortOptions = { field: SortField; direction: SortDirection }; +/** + * A Space defines a sidebar layout and filtering context + */ +export type Space = { +/** + * Unique identifier + */ +id: string; +/** + * Human-friendly name (e.g., "All Devices", "Work Files") + */ +name: string; +/** + * Icon identifier (Phosphor icon name or emoji) + */ +icon: string; +/** + * Color for visual identification (hex format: #RRGGBB) + */ +color: string; +/** + * Sort order in space switcher + */ +order: number; +/** + * Timestamps + */ +created_at: string; updated_at: string }; + +export type SpaceCreateInput = { name: string; icon: string; color: string }; + +export type SpaceCreateOutput = { space: Space }; + +export type SpaceDeleteInput = { space_id: string }; + +export type SpaceDeleteOutput = { success: boolean }; + +export type SpaceGetOutput = { space: Space }; + +export type SpaceGetQueryInput = { space_id: string }; + +/** + * A SpaceGroup is a collapsible section in the sidebar + */ +export type SpaceGroup = { +/** + * Unique identifier + */ +id: string; +/** + * Space this group belongs to + */ +space_id: string; +/** + * Group name (e.g., "Quick Access", "MacBook Pro") + */ +name: string; +/** + * Type of group (determines content and behavior) + */ +group_type: GroupType; +/** + * Whether group is collapsed + */ +is_collapsed: boolean; +/** + * Sort order within space + */ +order: number; +/** + * Timestamp + */ +created_at: string }; + +/** + * A group with its items + */ +export type SpaceGroupWithItems = { +/** + * The group + */ +group: SpaceGroup; +/** + * Items in this group (sorted by order) + */ +items: SpaceItem[] }; + +/** + * An item within a space (can be space-level or within a group) + */ +export type SpaceItem = { +/** + * Unique identifier + */ +id: string; +/** + * Space this item belongs to + */ +space_id: string; +/** + * Group this item belongs to (None = space-level item) + */ +group_id: string | null; +/** + * Type and data of this item + */ +item_type: ItemType; +/** + * Sort order within space or group + */ +order: number; +/** + * Timestamp + */ +created_at: string }; + +/** + * Complete sidebar layout for a space + */ +export type SpaceLayout = { +/** + * The space + */ +space: Space; +/** + * Space-level items (pinned shortcuts, no group) + */ +space_items: SpaceItem[]; +/** + * Groups with their items + */ +groups: SpaceGroupWithItems[] }; + +export type SpaceLayoutOutput = { layout: SpaceLayout }; + +export type SpaceLayoutQueryInput = { space_id: string }; + +export type SpaceUpdateInput = { space_id: string; name: string | null; icon: string | null; color: string | null }; + +export type SpaceUpdateOutput = { space: Space }; + export type SpacedropSendInput = { device_id: string; paths: SdPath[]; sender: string | null }; export type SpacedropSendOutput = { job_id: string | null; session_id: string | null }; +export type SpacesListOutput = { spaces: Space[] }; + +export type SpacesListQueryInput = null; + /** * State transition event */ @@ -2055,6 +2347,10 @@ total_count: number; */ total_size: number }; +export type UpdateGroupInput = { group_id: string; name: string | null; is_collapsed: boolean | null }; + +export type UpdateGroupOutput = { group: SpaceGroup }; + /** * A volume in Spacedrive - unified model for runtime and database */ @@ -2286,133 +2582,161 @@ fingerprint: VolumeFingerprint }; // ===== API Type Unions ===== export type CoreAction = - { type: 'libraries.delete'; input: LibraryDeleteInput; output: LibraryDeleteOutput } - | { type: 'network.stop'; input: NetworkStopInput; output: NetworkStopOutput } + { type: 'network.sync_setup'; input: LibrarySyncSetupInput; output: LibrarySyncSetupOutput } | { type: 'network.device.revoke'; input: DeviceRevokeInput; output: DeviceRevokeOutput } + | { type: 'libraries.delete'; input: LibraryDeleteInput; output: LibraryDeleteOutput } + | { type: 'network.stop'; input: NetworkStopInput; output: NetworkStopOutput } | { type: 'network.pair.cancel'; input: PairCancelInput; output: PairCancelOutput } - | { type: 'network.pair.generate'; input: PairGenerateInput; output: PairGenerateOutput } - | { type: 'network.pair.join'; input: PairJoinInput; output: PairJoinOutput } - | { type: 'network.sync_setup'; input: LibrarySyncSetupInput; output: LibrarySyncSetupOutput } | { type: 'libraries.create'; input: LibraryCreateInput; output: LibraryCreateOutput } - | { type: 'network.spacedrop.send'; input: SpacedropSendInput; output: SpacedropSendOutput } + | { type: 'network.pair.generate'; input: PairGenerateInput; output: PairGenerateOutput } | { type: 'network.start'; input: NetworkStartInput; output: NetworkStartOutput } + | { type: 'network.pair.join'; input: PairJoinInput; output: PairJoinOutput } + | { type: 'network.spacedrop.send'; input: SpacedropSendInput; output: SpacedropSendOutput } ; export type LibraryAction = - { type: 'indexing.verify'; input: IndexVerifyInput; output: IndexVerifyOutput } - | { type: 'volumes.add_cloud'; input: VolumeAddCloudInput; output: VolumeAddCloudOutput } - | { type: 'tags.apply'; input: ApplyTagsInput; output: ApplyTagsOutput } - | { type: 'locations.rescan'; input: LocationRescanInput; output: LocationRescanOutput } - | { type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput } - | { type: 'locations.remove'; input: LocationRemoveInput; output: LocationRemoveOutput } - | { type: 'volumes.untrack'; input: VolumeUntrackInput; output: VolumeUntrackOutput } - | { type: 'volumes.speed_test'; input: VolumeSpeedTestInput; output: VolumeSpeedTestOutput } - | { type: 'indexing.start'; input: IndexInput; output: JobReceipt } + { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt } + | { type: 'spaces.create'; input: SpaceCreateInput; output: SpaceCreateOutput } | { type: 'volumes.track'; input: VolumeTrackInput; output: VolumeTrackOutput } - | { type: 'libraries.rename'; input: LibraryRenameInput; output: LibraryRenameOutput } - | { type: 'volumes.remove_cloud'; input: VolumeRemoveCloudInput; output: VolumeRemoveCloudOutput } + | { type: 'volumes.add_cloud'; input: VolumeAddCloudInput; output: VolumeAddCloudOutput } + | { type: 'spaces.delete_group'; input: DeleteGroupInput; output: DeleteGroupOutput } + | { type: 'volumes.speed_test'; input: VolumeSpeedTestInput; output: VolumeSpeedTestOutput } + | { type: 'spaces.update_group'; input: UpdateGroupInput; output: UpdateGroupOutput } | { type: 'libraries.export'; input: LibraryExportInput; output: LibraryExportOutput } + | { type: 'spaces.update'; input: SpaceUpdateInput; output: SpaceUpdateOutput } | { type: 'jobs.resume'; input: JobResumeInput; output: JobResumeOutput } - | { type: 'locations.add'; input: LocationAddInput; output: LocationAddOutput } - | { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt } | { type: 'jobs.pause'; input: JobPauseInput; output: JobPauseOutput } - | { type: 'tags.create'; input: CreateTagInput; output: CreateTagOutput } + | { type: 'indexing.start'; input: IndexInput; output: JobReceipt } + | { type: 'locations.add'; input: LocationAddInput; output: LocationAddOutput } + | { type: 'locations.remove'; input: LocationRemoveInput; output: LocationRemoveOutput } + | { type: 'spaces.add_group'; input: AddGroupInput; output: AddGroupOutput } | { type: 'files.copy'; input: FileCopyInput; output: JobReceipt } + | { type: 'spaces.delete_item'; input: DeleteItemInput; output: DeleteItemOutput } + | { type: 'libraries.rename'; input: LibraryRenameInput; output: LibraryRenameOutput } + | { type: 'spaces.add_item'; input: AddItemInput; output: AddItemOutput } + | { type: 'tags.apply'; input: ApplyTagsInput; output: ApplyTagsOutput } + | { type: 'volumes.remove_cloud'; input: VolumeRemoveCloudInput; output: VolumeRemoveCloudOutput } + | { type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput } + | { type: 'volumes.untrack'; input: VolumeUntrackInput; output: VolumeUntrackOutput } + | { type: 'spaces.delete'; input: SpaceDeleteInput; output: SpaceDeleteOutput } + | { type: 'locations.rescan'; input: LocationRescanInput; output: LocationRescanOutput } + | { type: 'spaces.reorder_items'; input: ReorderItemsInput; output: ReorderOutput } + | { type: 'spaces.reorder_groups'; input: ReorderGroupsInput; output: ReorderOutput } | { type: 'media.thumbnail'; input: ThumbnailInput; output: JobReceipt } + | { type: 'indexing.verify'; input: IndexVerifyInput; output: IndexVerifyOutput } + | { type: 'tags.create'; input: CreateTagInput; output: CreateTagOutput } ; export type CoreQuery = - { type: 'network.status'; input: NetworkStatusQueryInput; output: NetworkStatus } - | { type: 'core.status'; input: Empty; output: CoreStatus } - | { type: 'network.sync_setup.discover'; input: DiscoverRemoteLibrariesInput; output: DiscoverRemoteLibrariesOutput } - | { type: 'libraries.list'; input: ListLibrariesInput; output: [LibraryInfo] } + { type: 'core.status'; input: Empty; output: CoreStatus } + | { type: 'network.status'; input: NetworkStatusQueryInput; output: NetworkStatus } | { type: 'network.pair.status'; input: PairStatusQueryInput; output: PairStatusOutput } - | { type: 'network.devices.list'; input: ListPairedDevicesInput; output: ListPairedDevicesOutput } | { type: 'core.events.list'; input: ListEventsInput; output: ListEventsOutput } + | { type: 'libraries.list'; input: ListLibrariesInput; output: [LibraryInfo] } + | { type: 'network.sync_setup.discover'; input: DiscoverRemoteLibrariesInput; output: DiscoverRemoteLibrariesOutput } + | { type: 'network.devices.list'; input: ListPairedDevicesInput; output: ListPairedDevicesOutput } ; export type LibraryQuery = - { type: 'files.directory_listing'; input: DirectoryListingInput; output: DirectoryListingOutput } + { type: 'files.unique_to_location'; input: UniqueToLocationInput; output: UniqueToLocationOutput } + | { type: 'spaces.list'; input: SpacesListQueryInput; output: SpacesListOutput } + | { type: 'spaces.get_layout'; input: SpaceLayoutQueryInput; output: SpaceLayoutOutput } + | { type: 'files.media_listing'; input: MediaListingInput; output: MediaListingOutput } + | { type: 'spaces.get'; input: SpaceGetQueryInput; output: SpaceGetOutput } + | { type: 'jobs.list'; input: JobListInput; output: JobListOutput } | { type: 'files.by_path'; input: FileByPathQuery; output: File } - | { type: 'files.by_id'; input: FileByIdQuery; output: File } - | { type: 'files.unique_to_location'; input: UniqueToLocationInput; output: UniqueToLocationOutput } - | { type: 'volumes.list'; input: VolumeListQueryInput; output: VolumeListOutput } - | { type: 'locations.list'; input: LocationsListQueryInput; output: LocationsListOutput } - | { type: 'tags.search'; input: SearchTagsInput; output: SearchTagsOutput } + | { type: 'files.directory_listing'; input: DirectoryListingInput; output: DirectoryListingOutput } | { type: 'jobs.info'; input: JobInfoQueryInput; output: JobInfoOutput } + | { type: 'volumes.list'; input: VolumeListQueryInput; output: VolumeListOutput } + | { type: 'devices.list'; input: ListLibraryDevicesInput; output: [LibraryDeviceInfo] } + | { type: 'files.by_id'; input: FileByIdQuery; output: File } + | { type: 'tags.search'; input: SearchTagsInput; output: SearchTagsOutput } + | { type: 'libraries.info'; input: LibraryInfoQueryInput; output: LibraryInfoOutput } + | { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput } + | { type: 'sync.metrics'; input: GetSyncMetricsInput; output: GetSyncMetricsOutput } + | { type: 'locations.list'; input: LocationsListQueryInput; output: LocationsListOutput } | { type: 'test.ping'; input: PingInput; output: PingOutput } | { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput } - | { type: 'devices.list'; input: ListLibraryDevicesInput; output: [LibraryDeviceInfo] } - | { type: 'sync.metrics'; input: GetSyncMetricsInput; output: GetSyncMetricsOutput } - | { type: 'libraries.info'; input: LibraryInfoQueryInput; output: LibraryInfoOutput } - | { type: 'jobs.list'; input: JobListInput; output: JobListOutput } - | { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput } ; // ===== Wire Method Mappings ===== export const WIRE_METHODS = { coreActions: { + 'network.sync_setup': 'action:network.sync_setup.input', + 'network.device.revoke': 'action:network.device.revoke.input', 'libraries.delete': 'action:libraries.delete.input', 'network.stop': 'action:network.stop.input', - 'network.device.revoke': 'action:network.device.revoke.input', 'network.pair.cancel': 'action:network.pair.cancel.input', - 'network.pair.generate': 'action:network.pair.generate.input', - 'network.pair.join': 'action:network.pair.join.input', - 'network.sync_setup': 'action:network.sync_setup.input', 'libraries.create': 'action:libraries.create.input', - 'network.spacedrop.send': 'action:network.spacedrop.send.input', + 'network.pair.generate': 'action:network.pair.generate.input', 'network.start': 'action:network.start.input', + 'network.pair.join': 'action:network.pair.join.input', + 'network.spacedrop.send': 'action:network.spacedrop.send.input', }, libraryActions: { - 'indexing.verify': 'action:indexing.verify.input', - 'volumes.add_cloud': 'action:volumes.add_cloud.input', - 'tags.apply': 'action:tags.apply.input', - 'locations.rescan': 'action:locations.rescan.input', - 'jobs.cancel': 'action:jobs.cancel.input', - 'locations.remove': 'action:locations.remove.input', - 'volumes.untrack': 'action:volumes.untrack.input', - 'volumes.speed_test': 'action:volumes.speed_test.input', - 'indexing.start': 'action:indexing.start.input', - 'volumes.track': 'action:volumes.track.input', - 'libraries.rename': 'action:libraries.rename.input', - 'volumes.remove_cloud': 'action:volumes.remove_cloud.input', - 'libraries.export': 'action:libraries.export.input', - 'jobs.resume': 'action:jobs.resume.input', - 'locations.add': 'action:locations.add.input', 'files.delete': 'action:files.delete.input', + 'spaces.create': 'action:spaces.create.input', + 'volumes.track': 'action:volumes.track.input', + 'volumes.add_cloud': 'action:volumes.add_cloud.input', + 'spaces.delete_group': 'action:spaces.delete_group.input', + 'volumes.speed_test': 'action:volumes.speed_test.input', + 'spaces.update_group': 'action:spaces.update_group.input', + 'libraries.export': 'action:libraries.export.input', + 'spaces.update': 'action:spaces.update.input', + 'jobs.resume': 'action:jobs.resume.input', 'jobs.pause': 'action:jobs.pause.input', - 'tags.create': 'action:tags.create.input', + 'indexing.start': 'action:indexing.start.input', + 'locations.add': 'action:locations.add.input', + 'locations.remove': 'action:locations.remove.input', + 'spaces.add_group': 'action:spaces.add_group.input', 'files.copy': 'action:files.copy.input', + 'spaces.delete_item': 'action:spaces.delete_item.input', + 'libraries.rename': 'action:libraries.rename.input', + 'spaces.add_item': 'action:spaces.add_item.input', + 'tags.apply': 'action:tags.apply.input', + 'volumes.remove_cloud': 'action:volumes.remove_cloud.input', + 'jobs.cancel': 'action:jobs.cancel.input', + 'volumes.untrack': 'action:volumes.untrack.input', + 'spaces.delete': 'action:spaces.delete.input', + 'locations.rescan': 'action:locations.rescan.input', + 'spaces.reorder_items': 'action:spaces.reorder_items.input', + 'spaces.reorder_groups': 'action:spaces.reorder_groups.input', 'media.thumbnail': 'action:media.thumbnail.input', + 'indexing.verify': 'action:indexing.verify.input', + 'tags.create': 'action:tags.create.input', }, coreQueries: { - 'network.status': 'query:network.status', 'core.status': 'query:core.status', - 'network.sync_setup.discover': 'query:network.sync_setup.discover', - 'libraries.list': 'query:libraries.list', + 'network.status': 'query:network.status', 'network.pair.status': 'query:network.pair.status', - 'network.devices.list': 'query:network.devices.list', 'core.events.list': 'query:core.events.list', + 'libraries.list': 'query:libraries.list', + 'network.sync_setup.discover': 'query:network.sync_setup.discover', + 'network.devices.list': 'query:network.devices.list', }, libraryQueries: { - 'files.directory_listing': 'query:files.directory_listing', - 'files.by_path': 'query:files.by_path', - 'files.by_id': 'query:files.by_id', 'files.unique_to_location': 'query:files.unique_to_location', - 'volumes.list': 'query:volumes.list', - 'locations.list': 'query:locations.list', - 'tags.search': 'query:tags.search', + 'spaces.list': 'query:spaces.list', + 'spaces.get_layout': 'query:spaces.get_layout', + 'files.media_listing': 'query:files.media_listing', + 'spaces.get': 'query:spaces.get', + 'jobs.list': 'query:jobs.list', + 'files.by_path': 'query:files.by_path', + 'files.directory_listing': 'query:files.directory_listing', 'jobs.info': 'query:jobs.info', + 'volumes.list': 'query:volumes.list', + 'devices.list': 'query:devices.list', + 'files.by_id': 'query:files.by_id', + 'tags.search': 'query:tags.search', + 'libraries.info': 'query:libraries.info', + 'search.files': 'query:search.files', + 'sync.metrics': 'query:sync.metrics', + 'locations.list': 'query:locations.list', 'test.ping': 'query:test.ping', 'locations.suggested': 'query:locations.suggested', - 'devices.list': 'query:devices.list', - 'sync.metrics': 'query:sync.metrics', - 'libraries.info': 'query:libraries.info', - 'jobs.list': 'query:jobs.list', - 'search.files': 'query:search.files', }, } as const; diff --git a/packages/ts-client/src/index.ts b/packages/ts-client/src/index.ts index e5486ede2..0118be50a 100644 --- a/packages/ts-client/src/index.ts +++ b/packages/ts-client/src/index.ts @@ -55,5 +55,8 @@ export { // React hooks (requires @tanstack/react-query peer dependency) export * from "./hooks"; +// Zustand stores +export * from "./stores/sidebar"; + // All auto-generated types export * from "./generated/types"; diff --git a/packages/ts-client/src/stores/sidebar.ts b/packages/ts-client/src/stores/sidebar.ts new file mode 100644 index 000000000..597c0ffea --- /dev/null +++ b/packages/ts-client/src/stores/sidebar.ts @@ -0,0 +1,61 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface DraggedItem { + type: 'file' | 'space-item' | 'space-group'; + data: any; +} + +interface SidebarStore { + // Persisted state + currentSpaceId: string | null; + setCurrentSpace: (id: string | null) => void; + + // Ephemeral state + collapsedGroups: Set; + toggleGroup: (groupId: string) => void; + collapseAll: (groupIds: string[]) => void; + expandAll: () => void; + + // Drag state + draggedItem: DraggedItem | null; + setDraggedItem: (item: DraggedItem | null) => void; +} + +export const useSidebarStore = create()( + persist( + (set) => ({ + // Persisted + currentSpaceId: null, + setCurrentSpace: (id) => set({ currentSpaceId: id }), + + // Ephemeral + collapsedGroups: new Set(), + toggleGroup: (groupId) => + set((state) => { + const newSet = new Set(state.collapsedGroups); + if (newSet.has(groupId)) { + newSet.delete(groupId); + } else { + newSet.add(groupId); + } + return { collapsedGroups: newSet }; + }), + collapseAll: (groupIds) => + set({ + collapsedGroups: new Set(groupIds), + }), + expandAll: () => set({ collapsedGroups: new Set() }), + + // Drag + draggedItem: null, + setDraggedItem: (item) => set({ draggedItem: item }), + }), + { + name: 'spacedrive-sidebar', + partialize: (state) => ({ + currentSpaceId: state.currentSpaceId, + }), + } + ) +); diff --git a/scripts/bundle-libheif.sh b/scripts/bundle-libheif.sh new file mode 100755 index 000000000..0b268c99c --- /dev/null +++ b/scripts/bundle-libheif.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +# Bundle libheif for local development +# This script copies libheif.dylib from Homebrew to the target directory + +TARGET_DIR="${1:-target/debug}" +LIBHEIF_SRC="/opt/homebrew/lib/libheif.1.dylib" + +if [ ! -f "$LIBHEIF_SRC" ]; then + echo "Error: libheif not found at $LIBHEIF_SRC" + echo "Install with: brew install libheif" + exit 1 +fi + +# Create target directory if it doesn't exist +mkdir -p "$TARGET_DIR" + +# Copy libheif and its dependencies +echo "Copying libheif to $TARGET_DIR..." +cp -f "$LIBHEIF_SRC" "$TARGET_DIR/libheif.1.dylib" + +# Create symlink for compatibility +ln -sf libheif.1.dylib "$TARGET_DIR/libheif.dylib" + +echo "✓ libheif bundled successfully" +echo " Location: $TARGET_DIR/libheif.1.dylib" diff --git a/xtask/src/config.rs b/xtask/src/config.rs index 71c6166e6..d5e2b706d 100644 --- a/xtask/src/config.rs +++ b/xtask/src/config.rs @@ -105,18 +105,12 @@ pub fn generate_cargo_config( .collect::>() .join("\n"); - // Add cargo aliases at the end - let with_aliases = format!( - "{}\n\n# Cargo aliases for xtask commands\n[alias]\n# Run xtask commands\nxtask = \"run --package xtask --\"\n# Shortcut for building iOS framework\nios = \"run --package xtask -- build-ios\"\n", - rendered.trim() - ); - // Validate TOML before writing - toml::from_str::(&with_aliases).context("Generated config is not valid TOML")?; + toml::from_str::(&rendered).context("Generated config is not valid TOML")?; // Write output let output_path = root.join(".cargo").join("config.toml"); - fs::write(&output_path, with_aliases).context("Failed to write config.toml")?; + fs::write(&output_path, rendered).context("Failed to write config.toml")?; println!(" ✓ Generated {}", output_path.display()); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index ca42c0406..23cc746ae 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -109,7 +109,6 @@ fn setup() -> Result<()> { println!(); println!("Creating symlinks for shared libraries..."); native_deps::symlink_libs_macos(&project_root, &native_deps_dir)?; - println!(" ✓ Symlinks created"); } #[cfg(target_os = "linux")] diff --git a/xtask/src/native_deps.rs b/xtask/src/native_deps.rs index d526888de..2abad27e8 100644 --- a/xtask/src/native_deps.rs +++ b/xtask/src/native_deps.rs @@ -87,30 +87,71 @@ pub fn symlink_libs_macos(root: &Path, native_deps: &Path) -> Result<()> { { use std::os::unix::fs as unix_fs; - let lib_dir = native_deps.join("lib"); - if !lib_dir.exists() { - return Ok(()); // No libs to symlink + // Create Spacedrive.framework symlink for dylibs (matches v1 behavior) + let framework = native_deps.join("Spacedrive.framework"); + if framework.exists() { + // Sign all dylibs in the framework (required for macOS 13+) + let libs_dir = framework.join("Libraries"); + if libs_dir.exists() { + println!(" Signing framework libraries..."); + for entry in fs::read_dir(&libs_dir)? { + let entry = entry?; + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) == Some("dylib") { + // Remove signature first + let _ = std::process::Command::new("codesign") + .args(&["--remove-signature", path.to_str().unwrap()]) + .output(); + + // Sign with ad-hoc signature (- means ad-hoc) + let status = std::process::Command::new("codesign") + .args(&["-s", "-", "-f", path.to_str().unwrap()]) + .status() + .context("Failed to run codesign")?; + + if !status.success() { + println!(" Warning: Failed to sign {}", path.display()); + } + } + } + println!(" ✓ Signed framework libraries"); + } + + let target_frameworks = root.join("target").join("Frameworks"); + fs::create_dir_all(&target_frameworks)?; + + let framework_link = target_frameworks.join("Spacedrive.framework"); + + // Remove existing symlink if present + let _ = fs::remove_file(&framework_link); + + unix_fs::symlink(&framework, &framework_link) + .context("Failed to symlink Spacedrive.framework")?; + + println!(" ✓ Linked Spacedrive.framework (includes libheif)"); } - // Create symlink in root for FFmpeg - let target = root.join("target"); - fs::create_dir_all(&target)?; + // Also symlink individual dylibs from lib/ to target/ for easier access + let lib_dir = native_deps.join("lib"); + if lib_dir.exists() { + let target = root.join("target"); - for entry in fs::read_dir(&lib_dir)? { - let entry = entry?; - let filename = entry.file_name(); - let filename_str = filename.to_string_lossy(); + for entry in fs::read_dir(&lib_dir)? { + let entry = entry?; + let filename = entry.file_name(); + let filename_str = filename.to_string_lossy(); - // Only symlink dylibs - if filename_str.ends_with(".dylib") { - let src = entry.path(); - let dst = target.join(&filename); + // Only symlink dylibs + if filename_str.ends_with(".dylib") { + let src = entry.path(); + let dst = target.join(&filename); - // Remove existing symlink if present - let _ = fs::remove_file(&dst); + // Remove existing symlink if present + let _ = fs::remove_file(&dst); - unix_fs::symlink(&src, &dst) - .with_context(|| format!("Failed to symlink {}", filename_str))?; + unix_fs::symlink(&src, &dst) + .with_context(|| format!("Failed to symlink {}", filename_str))?; + } } } } @@ -118,6 +159,35 @@ pub fn symlink_libs_macos(root: &Path, native_deps: &Path) -> Result<()> { Ok(()) } +/// Bundle libheif from Homebrew (macOS temporary solution) +/// +/// This copies libheif from Homebrew to the target directory until it's included +/// in the native-deps package. On macOS, libheif is available via Homebrew and +/// we bundle it for development builds. +pub fn bundle_libheif_from_homebrew(root: &Path) -> Result<()> { + #[cfg(target_os = "macos")] + { + let homebrew_libheif = Path::new("/opt/homebrew/lib/libheif.1.dylib"); + + if !homebrew_libheif.exists() { + println!(" libheif not found in Homebrew. Install with: brew install libheif"); + println!(" HEIC support will not be available."); + return Ok(()); + } + + let target_dir = root.join("target"); + fs::create_dir_all(&target_dir)?; + + let dest = target_dir.join("libheif.1.dylib"); + fs::copy(homebrew_libheif, &dest) + .context("Failed to copy libheif from Homebrew")?; + + println!(" ✓ Bundled libheif from Homebrew"); + } + + Ok(()) +} + /// Create symlinks for shared libraries on Linux pub fn symlink_libs_linux(_root: &Path, _native_deps: &Path) -> Result<()> { #[cfg(target_os = "linux")]