chore: update Zustand dependency to version 5.0.8, modify Cargo.toml for sd-core features, and enhance sidecar manager initialization in library management

This commit is contained in:
Jamie Pine 2025-11-11 15:44:00 -08:00
parent fa5df2d51d
commit 2ef179d4df
16 changed files with 632 additions and 111 deletions

View File

@ -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"] }

View File

@ -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"] }

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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;

View File

@ -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(())
}

@ -1 +1 @@
Subproject commit d23adbb237cc40008f3744a3b799b1ef1a838a44
Subproject commit d5d318f9b98c0ba33c77be4d07fd0e15e13c97f1

View File

@ -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/**/*",

View File

@ -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;

View File

@ -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";

View File

@ -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<string>;
toggleGroup: (groupId: string) => void;
collapseAll: (groupIds: string[]) => void;
expandAll: () => void;
// Drag state
draggedItem: DraggedItem | null;
setDraggedItem: (item: DraggedItem | null) => void;
}
export const useSidebarStore = create<SidebarStore>()(
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,
}),
}
)
);

27
scripts/bundle-libheif.sh Executable file
View File

@ -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"

View File

@ -105,18 +105,12 @@ pub fn generate_cargo_config(
.collect::<Vec<_>>()
.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::<toml::Value>(&with_aliases).context("Generated config is not valid TOML")?;
toml::from_str::<toml::Value>(&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());

View File

@ -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")]

View File

@ -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")]