diff --git a/.gitignore b/.gitignore
index 3b54cf9ad..8c61345ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -284,6 +284,98 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,rust,node,react,turbo,vercel,nextjs,storybookjs
+### Xcode ###
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+build/
+DerivedData/
+*.moved-aside
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+
+## Obj-C/Swift specific
+*.hmap
+
+## App packaging
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+# *.xcodeproj
+#
+# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
+# hence it is not needed unless you have added a package configuration file to your project
+# .swiftpm
+
+.build/
+
+## CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+## Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build/
+
+## Accio dependency management
+Dependencies/
+.accio/
+
+## fastlane
+#
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+## Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
+
### Project ###
!apps/desktop/dist
!apps/web/dist
diff --git a/apps/macos2/Spacedrive/Spacedrive.entitlements b/apps/macos2/Spacedrive/Spacedrive.entitlements
new file mode 100644
index 000000000..32a6338b9
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.get-task-allow
+
+
+
diff --git a/apps/macos2/Spacedrive/Spacedrive.xcodeproj/project.pbxproj b/apps/macos2/Spacedrive/Spacedrive.xcodeproj/project.pbxproj
index 3ed8af8a5..f7b03e128 100644
--- a/apps/macos2/Spacedrive/Spacedrive.xcodeproj/project.pbxproj
+++ b/apps/macos2/Spacedrive/Spacedrive.xcodeproj/project.pbxproj
@@ -6,6 +6,10 @@
objectVersion = 77;
objects = {
+/* Begin PBXBuildFile section */
+ 6ADA37DD2E8684310017BA3A /* SpacedriveClient in Frameworks */ = {isa = PBXBuildFile; productRef = 6ADA37DC2E8684310017BA3A /* SpacedriveClient */; };
+/* End PBXBuildFile section */
+
/* Begin PBXContainerItemProxy section */
6A0F02F22E86800300B2DEBA /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
@@ -24,7 +28,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
- 6A0F02E42E86800200B2DEBA /* Spacedrive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Spacedrive.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 6A0F02E42E86800200B2DEBA /* AppIcon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppIcon.app; sourceTree = BUILT_PRODUCTS_DIR; };
6A0F02F12E86800300B2DEBA /* SpacedriveTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SpacedriveTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
6A0F02FB2E86800300B2DEBA /* SpacedriveUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SpacedriveUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
@@ -52,6 +56,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 6ADA37DD2E8684310017BA3A /* SpacedriveClient in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -78,6 +83,7 @@
6A0F02E62E86800200B2DEBA /* Spacedrive */,
6A0F02F42E86800300B2DEBA /* SpacedriveTests */,
6A0F02FE2E86800300B2DEBA /* SpacedriveUITests */,
+ 6ADA37DB2E8684310017BA3A /* Frameworks */,
6A0F02E52E86800200B2DEBA /* Products */,
);
sourceTree = "";
@@ -85,19 +91,26 @@
6A0F02E52E86800200B2DEBA /* Products */ = {
isa = PBXGroup;
children = (
- 6A0F02E42E86800200B2DEBA /* Spacedrive.app */,
+ 6A0F02E42E86800200B2DEBA /* AppIcon.app */,
6A0F02F12E86800300B2DEBA /* SpacedriveTests.xctest */,
6A0F02FB2E86800300B2DEBA /* SpacedriveUITests.xctest */,
);
name = Products;
sourceTree = "";
};
+ 6ADA37DB2E8684310017BA3A /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
- 6A0F02E32E86800200B2DEBA /* Spacedrive */ = {
+ 6A0F02E32E86800200B2DEBA /* AppIcon */ = {
isa = PBXNativeTarget;
- buildConfigurationList = 6A0F03052E86800300B2DEBA /* Build configuration list for PBXNativeTarget "Spacedrive" */;
+ buildConfigurationList = 6A0F03052E86800300B2DEBA /* Build configuration list for PBXNativeTarget "AppIcon" */;
buildPhases = (
6A0F02E02E86800200B2DEBA /* Sources */,
6A0F02E12E86800200B2DEBA /* Frameworks */,
@@ -110,11 +123,12 @@
fileSystemSynchronizedGroups = (
6A0F02E62E86800200B2DEBA /* Spacedrive */,
);
- name = Spacedrive;
+ name = AppIcon;
packageProductDependencies = (
+ 6ADA37DC2E8684310017BA3A /* SpacedriveClient */,
);
productName = Spacedrive;
- productReference = 6A0F02E42E86800200B2DEBA /* Spacedrive.app */;
+ productReference = 6A0F02E42E86800200B2DEBA /* AppIcon.app */;
productType = "com.apple.product-type.application";
};
6A0F02F02E86800300B2DEBA /* SpacedriveTests */ = {
@@ -195,12 +209,15 @@
);
mainGroup = 6A0F02DB2E86800200B2DEBA;
minimizedProjectReferenceProxies = 1;
+ packageReferences = (
+ 6A0F036F2E86817A00B2DEBA /* XCLocalSwiftPackageReference "../../../packages/swift-client" */,
+ );
preferredProjectObjectVersion = 77;
productRefGroup = 6A0F02E52E86800200B2DEBA /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
- 6A0F02E32E86800200B2DEBA /* Spacedrive */,
+ 6A0F02E32E86800200B2DEBA /* AppIcon */,
6A0F02F02E86800300B2DEBA /* SpacedriveTests */,
6A0F02FA2E86800300B2DEBA /* SpacedriveUITests */,
);
@@ -258,12 +275,12 @@
/* Begin PBXTargetDependency section */
6A0F02F32E86800300B2DEBA /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
- target = 6A0F02E32E86800200B2DEBA /* Spacedrive */;
+ target = 6A0F02E32E86800200B2DEBA /* AppIcon */;
targetProxy = 6A0F02F22E86800300B2DEBA /* PBXContainerItemProxy */;
};
6A0F02FD2E86800300B2DEBA /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
- target = 6A0F02E32E86800200B2DEBA /* Spacedrive */;
+ target = 6A0F02E32E86800200B2DEBA /* AppIcon */;
targetProxy = 6A0F02FC2E86800300B2DEBA /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
@@ -390,10 +407,12 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
+ CODE_SIGN_ENTITLEMENTS = Spacedrive.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZYS9K9Q88U;
- ENABLE_APP_SANDBOX = YES;
+ ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
@@ -434,10 +453,12 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
+ CODE_SIGN_ENTITLEMENTS = Spacedrive.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZYS9K9Q88U;
- ENABLE_APP_SANDBOX = YES;
+ ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
@@ -477,6 +498,7 @@
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_ENTITLEMENTS = Spacedrive.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZYS9K9Q88U;
@@ -503,6 +525,7 @@
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_ENTITLEMENTS = Spacedrive.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZYS9K9Q88U;
@@ -528,6 +551,7 @@
6A0F030C2E86800300B2DEBA /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = Spacedrive.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZYS9K9Q88U;
@@ -553,6 +577,7 @@
6A0F030D2E86800300B2DEBA /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = Spacedrive.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZYS9K9Q88U;
@@ -587,7 +612,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
- 6A0F03052E86800300B2DEBA /* Build configuration list for PBXNativeTarget "Spacedrive" */ = {
+ 6A0F03052E86800300B2DEBA /* Build configuration list for PBXNativeTarget "AppIcon" */ = {
isa = XCConfigurationList;
buildConfigurations = (
6A0F03062E86800300B2DEBA /* Debug */,
@@ -615,6 +640,21 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+ 6A0F036F2E86817A00B2DEBA /* XCLocalSwiftPackageReference "../../../packages/swift-client" */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = "../../../packages/swift-client";
+ };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 6ADA37DC2E8684310017BA3A /* SpacedriveClient */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 6A0F036F2E86817A00B2DEBA /* XCLocalSwiftPackageReference "../../../packages/swift-client" */;
+ productName = SpacedriveClient;
+ };
+/* End XCSwiftPackageProductDependency section */
};
rootObject = 6A0F02DC2E86800200B2DEBA /* Project object */;
}
diff --git a/apps/macos2/Spacedrive/Spacedrive.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/macos2/Spacedrive/Spacedrive.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..919434a62
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/apps/macos2/Spacedrive/Spacedrive/Components/ConnectivityCard.swift b/apps/macos2/Spacedrive/Spacedrive/Components/ConnectivityCard.swift
new file mode 100644
index 000000000..4ea860193
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Components/ConnectivityCard.swift
@@ -0,0 +1,170 @@
+import SpacedriveClient
+import SwiftUI
+
+/// Clean macOS-style card showing daemon connectivity and service status
+struct ConnectivityCard: View {
+ @EnvironmentObject var appState: SharedAppState
+
+ private var coreStatus: CoreStatus? {
+ appState.coreStatus
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ // Clean header
+ HStack {
+ Text("Services")
+ .font(.system(size: 16, weight: .semibold))
+ .foregroundColor(SpacedriveColors.Text.primary)
+
+ Spacer()
+
+ // Simple status indicator
+ HStack(spacing: 6) {
+ Circle()
+ .fill(connectionStatusColor)
+ .frame(width: 6, height: 6)
+
+ Text(appState.connectionStatus.displayName)
+ .font(.system(size: 12))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ }
+ }
+
+ // Services in clean rows
+ VStack(spacing: 8) {
+ ServiceRow(
+ name: "Location Watcher",
+ status: coreStatus?.services.locationWatcher.running == true ? .online : .offline,
+ icon: "doc.text.magnifyingglass"
+ )
+
+ ServiceRow(
+ name: "Networking",
+ status: coreStatus?.services.networking.running == true ? .online : .offline,
+ icon: "network"
+ )
+
+ ServiceRow(
+ name: "Volume Monitor",
+ status: coreStatus?.services.volumeMonitor.running == true ? .online : .offline,
+ icon: "externaldrive"
+ )
+
+ ServiceRow(
+ name: "File Sharing",
+ status: coreStatus?.services.fileSharing.running == true ? .online : .offline,
+ icon: "square.and.arrow.up"
+ )
+ }
+
+ // Clean info section
+ if let status = coreStatus {
+ Divider()
+ .background(SpacedriveColors.Border.secondary)
+ .padding(.vertical, 4)
+
+ HStack(spacing: 24) {
+ InfoRow(label: "Libraries", value: "\(status.libraryCount)")
+ InfoRow(label: "Devices", value: "\(status.network.pairedDevices)")
+ InfoRow(label: "Version", value: status.version)
+ }
+ }
+ }
+ .padding(16)
+ .background(SpacedriveColors.Background.tertiary)
+ .cornerRadius(8)
+ }
+
+ private var connectionStatusColor: Color {
+ switch appState.connectionStatus {
+ case .connected:
+ return SpacedriveColors.Accent.success
+ case .connecting:
+ return SpacedriveColors.Accent.warning
+ case .disconnected, .error:
+ return SpacedriveColors.Accent.error
+ }
+ }
+}
+
+/// Clean macOS-style service row
+struct ServiceRow: View {
+ let name: String
+ let status: ServiceBadgeStatus
+ let icon: String
+
+ var body: some View {
+ HStack(spacing: 8) {
+ Image(systemName: icon)
+ .font(.system(size: 13))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ .frame(width: 16)
+
+ Text(name)
+ .font(.system(size: 13))
+ .foregroundColor(SpacedriveColors.Text.primary)
+
+ Spacer()
+
+ Circle()
+ .fill(statusColor)
+ .frame(width: 6, height: 6)
+ }
+ }
+
+ private var statusColor: Color {
+ switch status {
+ case .online:
+ return SpacedriveColors.Accent.success
+ case .offline:
+ return SpacedriveColors.Accent.error
+ case .degraded:
+ return SpacedriveColors.Accent.warning
+ }
+ }
+}
+
+/// Clean macOS-style info row
+struct InfoRow: View {
+ let label: String
+ let value: String
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(label)
+ .font(.system(size: 11))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+
+ Text(value)
+ .font(.system(size: 12, weight: .medium))
+ .foregroundColor(SpacedriveColors.Text.primary)
+ }
+ }
+}
+
+/// Service status enum
+enum ServiceBadgeStatus {
+ case online
+ case offline
+ case degraded
+
+ var displayName: String {
+ switch self {
+ case .online:
+ return "Online"
+ case .offline:
+ return "Offline"
+ case .degraded:
+ return "Degraded"
+ }
+ }
+}
+
+#Preview {
+ ConnectivityCard()
+ .environmentObject(SharedAppState.shared)
+ .frame(width: 280)
+ .padding()
+ .background(SpacedriveColors.Background.primary)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Components/IconDebugView.swift b/apps/macos2/Spacedrive/Spacedrive/Components/IconDebugView.swift
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/macos2/Spacedrive/Spacedrive/Components/LibrarySelector.swift b/apps/macos2/Spacedrive/Spacedrive/Components/LibrarySelector.swift
new file mode 100644
index 000000000..bd49df4de
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Components/LibrarySelector.swift
@@ -0,0 +1,243 @@
+import SwiftUI
+import SpacedriveClient
+
+struct LibrarySelector: View {
+ @EnvironmentObject var appState: SharedAppState
+ @State private var showingLibraryPicker = false
+
+ var body: some View {
+ VStack(spacing: 12) {
+ // Current library card
+ if let currentLibrary = appState.currentLibrary {
+ LibraryCard(library: currentLibrary, isSelected: true)
+ } else if let currentLibraryId = appState.currentLibraryId,
+ let library = appState.availableLibraries.first(where: { $0.id == currentLibraryId })
+ {
+ LibraryCard(library: library, isSelected: true)
+ } else {
+ EmptyLibraryCard()
+ }
+
+ // Switch library button
+ if appState.availableLibraries.count > 1 {
+ Button(action: {
+ showingLibraryPicker = true
+ }) {
+ HStack(spacing: 8) {
+ Image(systemName: "arrow.triangle.2.circlepath")
+ .font(.system(size: 13, weight: .medium))
+
+ Text("Switch Library")
+ .font(.system(size: 13, weight: .medium))
+ }
+ .foregroundColor(SpacedriveColors.Accent.primary)
+ .padding(.horizontal, 16)
+ .padding(.vertical, 8)
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .fill(SpacedriveColors.Accent.primary.opacity(0.08))
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+ }
+ .sheet(isPresented: $showingLibraryPicker) {
+ LibraryPickerView()
+ }
+ }
+}
+
+/// Individual library card
+struct LibraryCard: View {
+ let library: LibraryInfo
+ let isSelected: Bool
+
+ var body: some View {
+ VStack(spacing: 16) {
+ // Top row: Icon, name, and path
+ HStack(spacing: 12) {
+ SpacedriveIconView(.database, size: 28)
+ .foregroundColor(SpacedriveColors.Accent.primary)
+
+ VStack(alignment: .leading, spacing: 4) {
+ Text(library.name)
+ .font(.system(size: 18, weight: .semibold))
+ .foregroundColor(SpacedriveColors.Text.primary)
+
+ Text(library.path)
+ .font(.system(size: 12))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ .lineLimit(1)
+ }
+
+ Spacer()
+ }
+
+ // Bottom row: Statistics
+ if let stats = library.stats {
+ LibraryStatsView(stats: stats)
+ }
+ }
+ .padding(.horizontal, 20)
+ .padding(.vertical, 18)
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(SpacedriveColors.Background.tertiary)
+ .overlay(
+ RoundedRectangle(cornerRadius: 12)
+ .stroke(
+ isSelected ? SpacedriveColors.Border.primary : SpacedriveColors.Border.secondary,
+ lineWidth: isSelected ? 1 : 0.5
+ )
+ )
+ )
+ }
+}
+
+/// Empty state card when no library is selected
+struct EmptyLibraryCard: View {
+ var body: some View {
+ HStack(spacing: 12) {
+ SpacedriveIconView(.database, size: 28)
+ .foregroundColor(SpacedriveColors.Text.tertiary)
+
+ VStack(alignment: .leading, spacing: 4) {
+ Text("No Library Selected")
+ .font(.system(size: 18, weight: .medium))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+
+ Text("Select a library to view jobs")
+ .font(.system(size: 12))
+ .foregroundColor(SpacedriveColors.Text.tertiary)
+ }
+
+ Spacer()
+ }
+ .padding(.horizontal, 20)
+ .padding(.vertical, 18)
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(SpacedriveColors.Background.tertiary)
+ .overlay(
+ RoundedRectangle(cornerRadius: 12)
+ .stroke(SpacedriveColors.Border.secondary, lineWidth: 0.5)
+ )
+ )
+ }
+}
+
+/// Library picker sheet
+struct LibraryPickerView: View {
+ @EnvironmentObject var appState: SharedAppState
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // Header
+ HStack {
+ Text("Select Library")
+ .font(.title2)
+ .fontWeight(.semibold)
+ .foregroundColor(SpacedriveColors.Text.primary)
+
+ Spacer()
+
+ Button("Done") {
+ dismiss()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ .padding(.horizontal, 20)
+ .padding(.vertical, 16)
+ .background(SpacedriveColors.Background.secondary)
+
+ Divider()
+ .background(SpacedriveColors.Border.primary)
+
+ // Library list
+ ScrollView {
+ LazyVStack(spacing: 16) {
+ ForEach(appState.availableLibraries) { library in
+ LibraryCard(
+ library: library,
+ isSelected: library.id == appState.currentLibraryId
+ )
+ .onTapGesture {
+ appState.dispatch(.switchToLibrary(library))
+ dismiss()
+ }
+ }
+ }
+ .padding(.horizontal, 20)
+ .padding(.vertical, 20)
+ }
+ .background(SpacedriveColors.Background.primary)
+ }
+ .frame(width: 400, height: 500)
+ .background(SpacedriveColors.Background.primary)
+ }
+}
+
+/// Library statistics view with proper hierarchy
+struct LibraryStatsView: View {
+ let stats: LibraryStatistics
+
+ var body: some View {
+ HStack(spacing: 24) {
+ // Hero: Total size (most prominent)
+ VStack(alignment: .leading, spacing: 6) {
+ Text(formatBytes(stats.totalSize))
+ .font(.system(size: 28, weight: .bold, design: .rounded))
+ .foregroundColor(SpacedriveColors.Text.primary)
+
+ Text("Total Size")
+ .font(.system(size: 13, weight: .medium))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ }
+
+ Spacer()
+
+ // Secondary stats in a clean row
+ HStack(spacing: 20) {
+ StatItem(value: "\(stats.totalFiles)", label: "Files")
+ StatItem(value: "\(stats.locationCount)", label: "Locations")
+ StatItem(value: "\(stats.tagCount)", label: "Tags")
+ }
+ }
+ }
+}
+
+/// Individual stat item with proper sizing
+struct StatItem: View {
+ let value: String
+ let label: String
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(value)
+ .font(.system(size: 16, weight: .semibold))
+ .foregroundColor(SpacedriveColors.Text.primary)
+
+ Text(label)
+ .font(.system(size: 12))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ }
+ }
+}
+
+
+/// Format bytes into human readable format
+private func formatBytes(_ bytes: UInt64) -> String {
+ let formatter = ByteCountFormatter()
+ formatter.allowedUnits = [.useKB, .useMB, .useGB, .useTB]
+ formatter.countStyle = .file
+ return formatter.string(fromByteCount: Int64(bytes))
+}
+
+struct LibrarySelector_Previews: PreviewProvider {
+ static var previews: some View {
+ LibrarySelector()
+ .environmentObject(SharedAppState.shared)
+ .frame(width: 300)
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Components/SDButton.swift b/apps/macos2/Spacedrive/Spacedrive/Components/SDButton.swift
new file mode 100644
index 000000000..7b8f3b989
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Components/SDButton.swift
@@ -0,0 +1,236 @@
+import SwiftUI
+
+/// Spacedrive Button Component - Reusable button with consistent styling
+struct SDButton: View {
+ enum Style {
+ case primary
+ case secondary
+ case tertiary
+ case destructive
+ case ghost
+
+ var backgroundColor: Color {
+ switch self {
+ case .primary:
+ return SpacedriveColors.Accent.primary
+ case .secondary:
+ return SpacedriveColors.Background.surface
+ case .tertiary:
+ return SpacedriveColors.Background.tertiary
+ case .destructive:
+ return SpacedriveColors.Accent.error
+ case .ghost:
+ return Color.clear
+ }
+ }
+
+ var textColor: Color {
+ switch self {
+ case .primary:
+ return .white
+ case .secondary, .tertiary:
+ return SpacedriveColors.Text.primary
+ case .destructive:
+ return .white
+ case .ghost:
+ return SpacedriveColors.Text.primary
+ }
+ }
+
+ var borderColor: Color? {
+ switch self {
+ case .primary, .destructive:
+ return nil
+ case .secondary, .tertiary:
+ return SpacedriveColors.Border.primary
+ case .ghost:
+ return SpacedriveColors.Border.secondary
+ }
+ }
+ }
+
+ enum Size {
+ case small
+ case medium
+ case large
+
+ var padding: EdgeInsets {
+ switch self {
+ case .small:
+ return EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12)
+ case .medium:
+ return EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
+ case .large:
+ return EdgeInsets(top: 12, leading: 20, bottom: 12, trailing: 20)
+ }
+ }
+
+ var font: Font {
+ switch self {
+ case .small:
+ return SpacedriveTypography.Scale.labelSmall(.medium)
+ case .medium:
+ return SpacedriveTypography.Scale.label(.medium)
+ case .large:
+ return SpacedriveTypography.Scale.labelLarge(.medium)
+ }
+ }
+
+ var cornerRadius: CGFloat {
+ switch self {
+ case .small:
+ return 6
+ case .medium:
+ return 8
+ case .large:
+ return 10
+ }
+ }
+ }
+
+ let title: String
+ let style: Style
+ let size: Size
+ let isDisabled: Bool
+ let isLoading: Bool
+ let icon: String?
+ let action: () -> Void
+
+ @State private var isHovered = false
+ @State private var isPressed = false
+
+ init(
+ _ title: String,
+ style: Style = .primary,
+ size: Size = .medium,
+ isDisabled: Bool = false,
+ isLoading: Bool = false,
+ icon: String? = nil,
+ action: @escaping () -> Void
+ ) {
+ self.title = title
+ self.style = style
+ self.size = size
+ self.isDisabled = isDisabled
+ self.isLoading = isLoading
+ self.icon = icon
+ self.action = action
+ }
+
+ var body: some View {
+ Button(action: {
+ if !isDisabled, !isLoading {
+ action()
+ }
+ }) {
+ HStack(spacing: 6) {
+ if isLoading {
+ ProgressView()
+ .progressViewStyle(CircularProgressViewStyle())
+ .scaleEffect(0.8)
+ .frame(width: 12, height: 12)
+ } else if let icon = icon {
+ Image(systemName: icon)
+ .font(size.font)
+ }
+
+ if !title.isEmpty {
+ Text(title)
+ .font(size.font)
+ .foregroundColor(effectiveTextColor)
+ }
+ }
+ .padding(size.padding)
+ .background(effectiveBackgroundColor)
+ .cornerRadius(size.cornerRadius)
+ .overlay(
+ RoundedRectangle(cornerRadius: size.cornerRadius)
+ .stroke(style.borderColor ?? Color.clear, lineWidth: 1)
+ )
+ .scaleEffect(isPressed ? 0.98 : 1.0)
+ .animation(.easeInOut(duration: 0.1), value: isPressed)
+ .animation(.easeInOut(duration: 0.2), value: isHovered)
+ }
+ .buttonStyle(PlainButtonStyle())
+ .disabled(isDisabled || isLoading)
+ .onHover { hovering in
+ isHovered = hovering
+ }
+ .pressEvents {
+ isPressed = true
+ } onRelease: {
+ isPressed = false
+ }
+ }
+
+ private var effectiveBackgroundColor: Color {
+ if isDisabled {
+ return style.backgroundColor.opacity(0.3)
+ } else if isPressed {
+ return style.backgroundColor.opacity(0.8)
+ } else if isHovered {
+ return style.backgroundColor.opacity(0.9)
+ } else {
+ return style.backgroundColor
+ }
+ }
+
+ private var effectiveTextColor: Color {
+ if isDisabled {
+ return style.textColor.opacity(0.5)
+ } else {
+ return style.textColor
+ }
+ }
+}
+
+// MARK: - Press Events Modifier
+
+struct PressEvents: ViewModifier {
+ let onPress: () -> Void
+ let onRelease: () -> Void
+
+ func body(content: Content) -> some View {
+ content
+ .simultaneousGesture(
+ DragGesture(minimumDistance: 0)
+ .onChanged { _ in
+ onPress()
+ }
+ .onEnded { _ in
+ onRelease()
+ }
+ )
+ }
+}
+
+extension View {
+ func pressEvents(onPress: @escaping () -> Void, onRelease: @escaping () -> Void) -> some View {
+ modifier(PressEvents(onPress: onPress, onRelease: onRelease))
+ }
+}
+
+// MARK: - Preview
+
+#Preview {
+ VStack(spacing: 16) {
+ HStack(spacing: 12) {
+ SDButton("Primary", style: .primary, size: .small) {}
+ SDButton("Secondary", style: .secondary, size: .small) {}
+ SDButton("Tertiary", style: .tertiary, size: .small) {}
+ }
+
+ HStack(spacing: 12) {
+ SDButton("Medium Primary", style: .primary, size: .medium, icon: "plus") {}
+ SDButton("Loading", style: .primary, size: .medium, isLoading: true) {}
+ SDButton("Disabled", style: .primary, size: .medium, isDisabled: true) {}
+ }
+
+ HStack(spacing: 12) {
+ SDButton("Large Ghost", style: .ghost, size: .large, icon: "gear") {}
+ SDButton("Destructive", style: .destructive, size: .large) {}
+ }
+ }
+ .padding(20)
+ .background(SpacedriveColors.Background.primary)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Components/SDCard.swift b/apps/macos2/Spacedrive/Spacedrive/Components/SDCard.swift
new file mode 100644
index 000000000..d0fdcba92
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Components/SDCard.swift
@@ -0,0 +1,182 @@
+import SwiftUI
+
+/// Spacedrive Card Component - Reusable container with consistent styling
+struct SDCard: View {
+ enum Style {
+ case elevated
+ case bordered
+ case flat
+
+ var backgroundColor: Color {
+ switch self {
+ case .elevated:
+ return SpacedriveColors.Background.surface
+ case .bordered:
+ return SpacedriveColors.Background.tertiary
+ case .flat:
+ return SpacedriveColors.Background.secondary
+ }
+ }
+
+ var borderColor: Color? {
+ switch self {
+ case .elevated, .flat:
+ return nil
+ case .bordered:
+ return SpacedriveColors.Border.primary
+ }
+ }
+
+ var shadowRadius: CGFloat {
+ switch self {
+ case .elevated:
+ return 8
+ case .bordered, .flat:
+ return 0
+ }
+ }
+ }
+
+ let style: Style
+ let padding: EdgeInsets
+ let cornerRadius: CGFloat
+ let content: Content
+
+ @State private var isHovered = false
+
+ init(
+ style: Style = .elevated,
+ padding: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16),
+ cornerRadius: CGFloat = 12,
+ @ViewBuilder content: () -> Content
+ ) {
+ self.style = style
+ self.padding = padding
+ self.cornerRadius = cornerRadius
+ self.content = content()
+ }
+
+ var body: some View {
+ content
+ .padding(padding)
+ .background(effectiveBackgroundColor)
+ .cornerRadius(cornerRadius)
+ .overlay(
+ RoundedRectangle(cornerRadius: cornerRadius)
+ .stroke(style.borderColor ?? Color.clear, lineWidth: 1)
+ )
+ .shadow(
+ color: Color.black.opacity(0.2),
+ radius: style.shadowRadius,
+ x: 0,
+ y: style.shadowRadius / 2
+ )
+ .scaleEffect(isHovered ? 1.02 : 1.0)
+ .animation(.easeInOut(duration: 0.2), value: isHovered)
+ .onHover { hovering in
+ if style == .elevated {
+ isHovered = hovering
+ }
+ }
+ }
+
+ private var effectiveBackgroundColor: Color {
+ if isHovered && style == .elevated {
+ return style.backgroundColor.opacity(0.9)
+ } else {
+ return style.backgroundColor
+ }
+ }
+}
+
+// MARK: - Specialized Card Types
+
+struct SDJobCard: View {
+ let content: Content
+
+ init(@ViewBuilder content: () -> Content) {
+ self.content = content()
+ }
+
+ var body: some View {
+ SDCard(
+ style: .bordered,
+ padding: EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12),
+ cornerRadius: 8
+ ) {
+ content
+ }
+ }
+}
+
+struct SDStatusCard: View {
+ let content: Content
+
+ init(@ViewBuilder content: () -> Content) {
+ self.content = content()
+ }
+
+ var body: some View {
+ SDCard(
+ style: .flat,
+ padding: EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12),
+ cornerRadius: 6
+ ) {
+ content
+ }
+ }
+}
+
+// MARK: - Preview
+
+#Preview {
+ VStack(spacing: 16) {
+ SDCard(style: .elevated) {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Elevated Card")
+ .h5()
+ Text("This is an elevated card with shadow and hover effects.")
+ .bodySmall(color: SpacedriveColors.Text.secondary)
+
+ HStack {
+ SDButton("Action", style: .primary, size: .small) {}
+ SDButton("Cancel", style: .secondary, size: .small) {}
+ Spacer()
+ }
+ }
+ }
+
+ SDCard(style: .bordered) {
+ HStack {
+ VStack(alignment: .leading) {
+ Text("Bordered Card")
+ .h6()
+ Text("With border styling")
+ .caption()
+ }
+ Spacer()
+ Image(systemName: "folder")
+ .foregroundColor(SpacedriveColors.Accent.primary)
+ }
+ }
+
+ SDJobCard {
+ HStack {
+ Circle()
+ .fill(SpacedriveColors.Accent.success)
+ .frame(width: 8, height: 8)
+
+ VStack(alignment: .leading) {
+ Text("Processing Files")
+ .label()
+ Text("45% complete")
+ .caption()
+ }
+
+ Spacer()
+ }
+ }
+ }
+ .padding(20)
+ .background(SpacedriveColors.Background.primary)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Components/SpacedriveIcon.swift b/apps/macos2/Spacedrive/Spacedrive/Components/SpacedriveIcon.swift
new file mode 100644
index 000000000..6ee509c95
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Components/SpacedriveIcon.swift
@@ -0,0 +1,339 @@
+import SwiftUI
+
+/// Enum representing all available Spacedrive icons
+/// This enum provides type-safe access to all icons from the Spacedrive v1 assets
+enum SpacedriveIcon: String, CaseIterable {
+ // File Type Icons
+ case album = "Album"
+ case albumLight = "Album_Light"
+ case album20 = "Album-20"
+ case alias = "Alias"
+ case aliasLight = "Alias_Light"
+ case alias20 = "Alias-20"
+ case application = "Application"
+ case applicationLight = "Application_Light"
+ case archive = "Archive"
+ case archiveLight = "Archive_Light"
+ case archive20 = "Archive-20"
+ case audio = "Audio"
+ case audioLight = "Audio_Light"
+ case audio20 = "Audio-20"
+ case book = "Book"
+ case bookLight = "Book_Light"
+ case book20 = "Book-20"
+ case bookBlue = "BookBlue"
+ case code20 = "Code-20"
+ case collection = "Collection"
+ case collectionLight = "Collection_Light"
+ case collection20 = "Collection-20"
+ case collectionSparkle = "CollectionSparkle"
+ case collectionSparkleLight = "CollectionSparkle_Light"
+ case config20 = "Config-20"
+ case database = "Database"
+ case databaseLight = "Database_Light"
+ case database20 = "Database-20"
+ case document = "Document"
+ case documentLight = "Document_Light"
+ case document20 = "Document-20"
+ case documentDoc = "Document_doc"
+ case documentDocLight = "Document_doc_Light"
+ case documentPdf = "Document_pdf"
+ case documentPdfLight = "Document_pdf_Light"
+ case documentSrt = "Document_srt"
+ case documentXls = "Document_xls"
+ case documentXlsLight = "Document_xls_Light"
+ case documentXmp = "Document_xmp"
+ case dotfile20 = "Dotfile-20"
+ case encrypted = "Encrypted"
+ case encryptedLight = "Encrypted_Light"
+ case encrypted20 = "Encrypted-20"
+ case entity = "Entity"
+ case entityLight = "Entity_Light"
+ case executable = "Executable"
+ case executableLight = "Executable_Light"
+ case executableLightOld = "Executable_Light_old"
+ case executableOld = "Executable_old"
+ case executable20 = "Executable-20"
+ case faceLight = "Face_Light"
+ case folder = "Folder"
+ case folderLight = "Folder_Light"
+ case folder20 = "Folder-20"
+ case folderTagXmp = "Folder-tag-xmp"
+ case folderGrey = "FolderGrey"
+ case folderGreyLight = "FolderGrey_Light"
+ case folderNoSpace = "FolderNoSpace"
+ case folderNoSpaceLight = "FolderNoSpace_Light"
+ case font20 = "Font-20"
+ case game = "Game"
+ case gameLight = "Game_Light"
+ case image = "Image"
+ case imageLight = "Image_Light"
+ case image20 = "Image-20"
+ case key = "Key"
+ case keyLight = "Key_Light"
+ case key20 = "Key-20"
+ case keys = "Keys"
+ case keysLight = "Keys_Light"
+ case link = "Link"
+ case linkLight = "Link_Light"
+ case link20 = "Link-20"
+ case lock = "Lock"
+ case lockLight = "Lock_Light"
+ case mesh = "Mesh"
+ case meshLight = "Mesh_Light"
+ case mesh20 = "Mesh-20"
+ case movie = "Movie"
+ case movieLight = "Movie_Light"
+ case package = "Package"
+ case packageLight = "Package_Light"
+ case package20 = "Package-20"
+ case screenshot = "Screenshot"
+ case screenshotLight = "Screenshot_Light"
+ case screenshot20 = "Screenshot-20"
+ case screenshotAlt = "ScreenshotAlt"
+ case text = "Text"
+ case textLight = "Text_Light"
+ case text20 = "Text-20"
+ case textAlt = "TextAlt"
+ case textAltLight = "TextAlt_Light"
+ case textTxt = "Text_txt"
+ case texturedMesh = "TexturedMesh"
+ case texturedMeshLight = "TexturedMesh_Light"
+ case trash = "Trash"
+ case trashLight = "Trash_Light"
+ case undefined = "Undefined"
+ case undefinedLight = "Undefined_Light"
+ case unknown20 = "Unknown-20"
+ case video = "Video"
+ case videoLight = "Video_Light"
+ case video20 = "Video-20"
+ case webPageArchive20 = "WebPageArchive-20"
+ case widget = "Widget"
+ case widgetLight = "Widget_Light"
+ case widget20 = "Widget-20"
+
+ // Drive/Cloud Service Icons
+ case amazonS3 = "AmazonS3"
+ case androidPhotos = "AndroidPhotos"
+ case appleFiles = "AppleFiles"
+ case applePhotos = "ApplePhotos"
+ case backBlaze = "BackBlaze"
+ case box = "Box"
+ case cloudSync = "CloudSync"
+ case cloudSyncLight = "CloudSync_Light"
+ case dav = "DAV"
+ case deleteLocation = "DeleteLocation"
+ case drive = "Drive"
+ case driveLight = "Drive_Light"
+ case driveAmazonS3 = "Drive-AmazonS3"
+ case driveAmazonS3Light = "Drive-AmazonS3_Light"
+ case driveBackBlaze = "Drive-BackBlaze"
+ case driveBackBlazeLight = "Drive-BackBlaze_Light"
+ case driveBox = "Drive-Box"
+ case driveBoxLight = "Drive-box_Light"
+ case driveDarker = "Drive-Darker"
+ case driveDav = "Drive-DAV"
+ case driveDavLight = "Drive-DAV_Light"
+ case driveDropbox = "Drive-Dropbox"
+ case driveDropboxLight = "Drive-Dropbox_Light"
+ case driveGoogleDrive = "Drive-GoogleDrive"
+ case driveGoogleDriveLight = "Drive-GoogleDrive_Light"
+ case driveMega = "Drive-Mega"
+ case driveMegaLight = "Drive-Mega_Light"
+ case driveOneDrive = "Drive-OneDrive"
+ case driveOneDriveLight = "Drive-OneDrive_Light"
+ case driveOpenStack = "Drive-OpenStack"
+ case driveOpenStackLight = "Drive-OpenStack_Light"
+ case drivePCloud = "Drive-PCloud"
+ case drivePCloudLight = "Drive-PCloud_Light"
+ case dropbox = "Dropbox"
+ case googleDrive = "GoogleDrive"
+ case location = "Location"
+ case locationManaged = "LocationManaged"
+ case locationReplica = "LocationReplica"
+ case mega = "Mega"
+ case moveLocation = "MoveLocation"
+ case moveLocationLight = "MoveLocation_Light"
+ case newLocation = "NewLocation"
+ case node = "Node"
+ case nodeLight = "Node_Light"
+ case oneDrive = "OneDrive"
+ case openStack = "OpenStack"
+ case pCloud = "PCloud"
+ case spacedrop = "Spacedrop"
+ case spacedropLight = "Spacedrop_Light"
+ case spacedrop1 = "Spacedrop-1"
+ case sync = "Sync"
+ case syncLight = "Sync_Light"
+
+ // Device Icons
+ case ball = "Ball"
+ case globe = "Globe"
+ case globeLight = "Globe_Light"
+ case globeAlt = "GlobeAlt"
+ case hdd = "HDD"
+ case hddLight = "HDD_Light"
+ case heart = "Heart"
+ case heartLight = "Heart_Light"
+ case home = "Home"
+ case homeLight = "Home_Light"
+ case laptop = "Laptop"
+ case laptopLight = "Laptop_Light"
+ case mobile = "Mobile"
+ case mobileLight = "Mobile_Light"
+ case mobileAndroid = "Mobile-Android"
+ case miniSilverBox = "MiniSilverBox"
+ case pc = "PC"
+ case scrapbook = "Scrapbook"
+ case scrapbookLight = "Scrapbook_Light"
+ case sd = "SD"
+ case sdLight = "SD_Light"
+ case search = "Search"
+ case searchLight = "Search_Light"
+ case searchAlt = "SearchAlt"
+ case server = "Server"
+ case serverLight = "Server_Light"
+ case silverBox = "SilverBox"
+ case tablet = "Tablet"
+ case tabletLight = "Tablet_Light"
+ case tags = "Tags"
+ case tagsLight = "Tags_Light"
+ case terminal = "Terminal"
+ case terminalLight = "Terminal_Light"
+
+ /// Returns the filename for the icon (without extension)
+ var filename: String {
+ return rawValue
+ }
+
+ /// Returns the full filename with .png extension
+ var fullFilename: String {
+ return "\(rawValue).png"
+ }
+
+ /// Returns a user-friendly display name for the icon
+ var displayName: String {
+ return rawValue.replacingOccurrences(of: "_", with: " ")
+ .replacingOccurrences(of: "-", with: " ")
+ .capitalized
+ }
+
+ /// Returns true if this is a light theme variant
+ var isLightVariant: Bool {
+ return rawValue.hasSuffix("_Light")
+ }
+
+ /// Returns true if this is a 20px variant
+ var is20pxVariant: Bool {
+ return rawValue.hasSuffix("-20")
+ }
+
+ /// Returns the base name without variant suffixes
+ var baseName: String {
+ var name = rawValue
+ if name.hasSuffix("_Light") {
+ name = String(name.dropLast(6)) // Remove "_Light"
+ }
+ if name.hasSuffix("-20") {
+ name = String(name.dropLast(3)) // Remove "-20"
+ }
+ return name
+ }
+}
+
+/// SwiftUI view component for displaying Spacedrive icons
+struct SpacedriveIconView: View {
+ let icon: SpacedriveIcon
+ let size: CGFloat
+ let color: Color?
+
+ init(_ icon: SpacedriveIcon, size: CGFloat = 16, color: Color? = nil) {
+ self.icon = icon
+ self.size = size
+ self.color = color
+ }
+
+ var body: some View {
+ if let bundlePath = Bundle.main.path(forResource: "Spacedrive_Spacedrive", ofType: "bundle"),
+ let bundle = Bundle(path: bundlePath),
+ let imagePath = bundle.path(forResource: icon.filename, ofType: "png"),
+ let image = NSImage(contentsOfFile: imagePath)
+ {
+ Image(nsImage: image)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: size, height: size)
+ .foregroundColor(color)
+ } else {
+ // Fallback for missing images
+ Rectangle()
+ .fill(Color.gray.opacity(0.3))
+ .frame(width: size, height: size)
+ .overlay(
+ Text("?")
+ .font(.caption)
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ )
+ }
+ }
+}
+
+/// Convenience extensions for common icon usage patterns
+extension SpacedriveIcon {
+ /// Get the appropriate icon variant based on theme and size preferences
+ static func preferredVariant(
+ baseName: String,
+ isLightTheme: Bool = false,
+ prefer20px: Bool = false
+ ) -> SpacedriveIcon? {
+ // Try to find the preferred variant
+ let variants = [
+ // 20px variants first if preferred
+ prefer20px ? "\(baseName)-20" : nil,
+ // Light variants if light theme
+ isLightTheme ? "\(baseName)_Light" : nil,
+ // Base variant
+ baseName,
+ ].compactMap { $0 }
+
+ for variant in variants {
+ if let icon = SpacedriveIcon(rawValue: variant) {
+ return icon
+ }
+ }
+
+ return nil
+ }
+
+ /// Get all variants of a base icon name
+ static func allVariants(for baseName: String) -> [SpacedriveIcon] {
+ return SpacedriveIcon.allCases.filter { $0.baseName == baseName }
+ }
+}
+
+/// Preview for testing the icon component
+struct SpacedriveIconView_Previews: PreviewProvider {
+ static var previews: some View {
+ VStack(spacing: 16) {
+ Text("Spacedrive Icons")
+ .font(.title)
+ .padding()
+
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: 12) {
+ ForEach(Array(SpacedriveIcon.allCases.prefix(24)), id: \.self) { icon in
+ VStack(spacing: 4) {
+ SpacedriveIconView(icon, size: 24)
+ Text(icon.displayName)
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ }
+ .padding(8)
+ .background(Color.gray.opacity(0.1))
+ .cornerRadius(8)
+ }
+ }
+ .padding()
+ }
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Components/SpacedriveIconExample.swift b/apps/macos2/Spacedrive/Spacedrive/Components/SpacedriveIconExample.swift
new file mode 100644
index 000000000..5d7aebfb4
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Components/SpacedriveIconExample.swift
@@ -0,0 +1,113 @@
+import SwiftUI
+
+/// Example usage of the SpacedriveIcon component
+struct SpacedriveIconExample: View {
+ var body: some View {
+ VStack(spacing: 20) {
+ Text("Spacedrive Icon Examples")
+ .font(.title)
+ .padding()
+
+ // Basic usage examples
+ VStack(alignment: .leading, spacing: 12) {
+ Text("Basic Usage")
+ .font(.headline)
+
+ HStack(spacing: 16) {
+ SpacedriveIconView(.folder, size: 24)
+ SpacedriveIconView(.document, size: 24)
+ SpacedriveIconView(.image, size: 24)
+ SpacedriveIconView(.video, size: 24)
+ SpacedriveIconView(.audio, size: 24)
+ }
+
+ Text("Different Sizes")
+ .font(.headline)
+ .padding(.top)
+
+ HStack(spacing: 16) {
+ SpacedriveIconView(.folder, size: 16)
+ SpacedriveIconView(.folder, size: 24)
+ SpacedriveIconView(.folder, size: 32)
+ SpacedriveIconView(.folder, size: 48)
+ }
+
+ Text("Light Theme Variants")
+ .font(.headline)
+ .padding(.top)
+
+ HStack(spacing: 16) {
+ SpacedriveIconView(.folder, size: 24)
+ SpacedriveIconView(.folderLight, size: 24)
+ SpacedriveIconView(.document, size: 24)
+ SpacedriveIconView(.documentLight, size: 24)
+ }
+
+ Text("20px Variants")
+ .font(.headline)
+ .padding(.top)
+
+ HStack(spacing: 16) {
+ SpacedriveIconView(.folder, size: 24)
+ SpacedriveIconView(.folder20, size: 24)
+ SpacedriveIconView(.image, size: 24)
+ SpacedriveIconView(.image20, size: 24)
+ }
+
+ Text("Drive/Cloud Services")
+ .font(.headline)
+ .padding(.top)
+
+ HStack(spacing: 16) {
+ SpacedriveIconView(.drive, size: 24)
+ SpacedriveIconView(.driveDropbox, size: 24)
+ SpacedriveIconView(.driveGoogleDrive, size: 24)
+ SpacedriveIconView(.driveAmazonS3, size: 24)
+ SpacedriveIconView(.driveOneDrive, size: 24)
+ }
+
+ Text("Device Icons")
+ .font(.headline)
+ .padding(.top)
+
+ HStack(spacing: 16) {
+ SpacedriveIconView(.laptop, size: 24)
+ SpacedriveIconView(.mobile, size: 24)
+ SpacedriveIconView(.tablet, size: 24)
+ SpacedriveIconView(.server, size: 24)
+ SpacedriveIconView(.hdd, size: 24)
+ }
+ }
+ .padding()
+
+ // Icon picker example
+ VStack(alignment: .leading, spacing: 12) {
+ Text("Icon Information")
+ .font(.headline)
+
+ let sampleIcon = SpacedriveIcon.folder
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Icon: \(sampleIcon.displayName)")
+ Text("Filename: \(sampleIcon.fullFilename)")
+ Text("Base Name: \(sampleIcon.baseName)")
+ Text("Is Light Variant: \(sampleIcon.isLightVariant ? "Yes" : "No")")
+ Text("Is 20px Variant: \(sampleIcon.is20pxVariant ? "Yes" : "No")")
+ }
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding()
+ .background(Color.gray.opacity(0.1))
+ .cornerRadius(8)
+ .padding()
+ }
+ }
+}
+
+/// Preview for the example
+struct SpacedriveIconExample_Previews: PreviewProvider {
+ static var previews: some View {
+ SpacedriveIconExample()
+ .frame(width: 600, height: 800)
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Components/Window/CustomTitleBar.swift b/apps/macos2/Spacedrive/Spacedrive/Components/Window/CustomTitleBar.swift
new file mode 100644
index 000000000..ce8ec82e1
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Components/Window/CustomTitleBar.swift
@@ -0,0 +1,23 @@
+import AppKit
+import SwiftUI
+
+// Environment key for window reference (kept for compatibility)
+private struct WindowEnvironmentKey: EnvironmentKey {
+ static let defaultValue: NSWindow? = nil
+}
+
+extension EnvironmentValues {
+ var window: NSWindow? {
+ get { self[WindowEnvironmentKey.self] }
+ set { self[WindowEnvironmentKey.self] = newValue }
+ }
+}
+
+struct CustomTitleBar: View {
+ var body: some View {
+ // Provide spacing for native traffic lights - they will appear in the transparent title bar area
+ Spacer()
+ .frame(height: 28) // Standard macOS title bar height
+ }
+}
+
diff --git a/apps/macos2/Spacedrive/Spacedrive/Components/Window/NativeTrafficLights.swift b/apps/macos2/Spacedrive/Spacedrive/Components/Window/NativeTrafficLights.swift
new file mode 100644
index 000000000..dd49ec20a
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Components/Window/NativeTrafficLights.swift
@@ -0,0 +1,158 @@
+import SwiftUI
+import AppKit
+
+/// Modifier to configure native macOS traffic lights for SwiftUI windows
+struct NativeTrafficLightsModifier: ViewModifier {
+ func body(content: Content) -> some View {
+ content
+ .background(NativeTrafficLightsView())
+ }
+}
+
+/// Background view that configures the window for native traffic lights
+struct NativeTrafficLightsView: NSViewRepresentable {
+ func makeNSView(context: Context) -> NSView {
+ let view = NSView()
+
+ // Configure the window when it becomes available
+ DispatchQueue.main.async {
+ if let window = view.window {
+ configureWindow(window)
+ }
+ }
+
+ return view
+ }
+
+ func updateNSView(_ nsView: NSView, context: Context) {
+ // Update if needed
+ }
+
+ private func configureWindow(_ window: NSWindow) {
+ // Configure for seamless native traffic light integration
+ window.titlebarAppearsTransparent = true
+ window.titleVisibility = .hidden
+ window.isMovableByWindowBackground = true
+
+ // Customize title bar appearance
+ customizeTitleBarAppearance(window)
+
+ // Ensure proper window behavior
+ window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
+ }
+
+ private func customizeTitleBarAppearance(_ window: NSWindow) {
+ // Set title bar background color to match your app
+ DispatchQueue.main.async {
+ // Find the title bar view and customize it
+ if let titlebarView = window.standardWindowButton(.closeButton)?.superview {
+ titlebarView.wantsLayer = true
+ titlebarView.layer?.backgroundColor = SpacedriveColors.NSColors.backgroundPrimary.cgColor
+ }
+
+ // You can also set the window's background color
+ window.backgroundColor = SpacedriveColors.NSColors.backgroundPrimary
+ }
+ }
+}
+
+/// Advanced title bar modifier that allows custom components
+struct CustomTitleBarModifier: ViewModifier {
+ let centerContent: AnyView?
+ let rightContent: AnyView?
+
+ init(
+ center: CenterContent? = nil,
+ right: RightContent? = nil
+ ) {
+ self.centerContent = center.map { AnyView($0) }
+ self.rightContent = right.map { AnyView($0) }
+ }
+
+ func body(content: Content) -> some View {
+ content
+ .background(CustomTitleBarView(centerContent: centerContent, rightContent: rightContent))
+ }
+}
+
+struct CustomTitleBarView: NSViewRepresentable {
+ let centerContent: AnyView?
+ let rightContent: AnyView?
+
+ func makeNSView(context: Context) -> NSView {
+ let view = NSView()
+
+ DispatchQueue.main.async {
+ if let window = view.window {
+ configureWindow(window)
+ addCustomComponents(to: window)
+ }
+ }
+
+ return view
+ }
+
+ func updateNSView(_ nsView: NSView, context: Context) {
+ // Update if needed
+ }
+
+ private func configureWindow(_ window: NSWindow) {
+ window.titlebarAppearsTransparent = true
+ window.titleVisibility = .hidden
+ window.isMovableByWindowBackground = true
+
+ // Customize appearance
+ if let titlebarView = window.standardWindowButton(.closeButton)?.superview {
+ titlebarView.wantsLayer = true
+ titlebarView.layer?.backgroundColor = SpacedriveColors.NSColors.backgroundPrimary.cgColor
+ }
+
+ window.backgroundColor = SpacedriveColors.NSColors.backgroundPrimary
+ window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
+ }
+
+ private func addCustomComponents(to window: NSWindow) {
+ guard let titlebarView = window.standardWindowButton(.closeButton)?.superview else { return }
+
+ // Add center content
+ if let centerContent = centerContent {
+ let hostingView = NSHostingView(rootView: centerContent)
+ titlebarView.addSubview(hostingView)
+
+ // Center the content
+ hostingView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ hostingView.centerXAnchor.constraint(equalTo: titlebarView.centerXAnchor),
+ hostingView.centerYAnchor.constraint(equalTo: titlebarView.centerYAnchor)
+ ])
+ }
+
+ // Add right content
+ if let rightContent = rightContent {
+ let hostingView = NSHostingView(rootView: rightContent)
+ titlebarView.addSubview(hostingView)
+
+ // Position on the right
+ hostingView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ hostingView.trailingAnchor.constraint(equalTo: titlebarView.trailingAnchor, constant: -12),
+ hostingView.centerYAnchor.constraint(equalTo: titlebarView.centerYAnchor)
+ ])
+ }
+ }
+}
+
+extension View {
+ /// Apply native traffic lights configuration to a SwiftUI view
+ func nativeTrafficLights() -> some View {
+ self.modifier(NativeTrafficLightsModifier())
+ }
+
+ /// Apply custom title bar with optional center and right components
+ func customTitleBar(
+ center: CenterContent? = nil,
+ right: RightContent? = nil
+ ) -> some View {
+ self.modifier(CustomTitleBarModifier(center: center, right: right))
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Components/Window/WindowContainer.swift b/apps/macos2/Spacedrive/Spacedrive/Components/Window/WindowContainer.swift
new file mode 100644
index 000000000..f598fe4a8
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Components/Window/WindowContainer.swift
@@ -0,0 +1,47 @@
+import AppKit
+import SwiftUI
+
+/// Shared Window Container - Provides consistent window chrome with native traffic lights for all windows
+struct WindowContainer: View {
+ let content: Content
+
+ init(@ViewBuilder content: () -> Content) {
+ self.content = content()
+ }
+
+ var body: some View {
+ ZStack {
+ // Rounded background using design system colors with Tahoe-style corners
+ UnevenRoundedRectangle(
+ topLeadingRadius: 13, // 8% increase from 12
+ bottomLeadingRadius: 13,
+ bottomTrailingRadius: 13,
+ topTrailingRadius: 13
+ )
+ .fill(Color(SpacedriveColors.NSColors.backgroundPrimary))
+
+ VStack(spacing: 0) {
+ // Spacer for native title bar area (where traffic lights will be)
+ Spacer()
+ .frame(height: 28)
+
+ // Main content area
+ content
+ }
+ }
+ }
+}
+
+#Preview {
+ WindowContainer {
+ VStack {
+ Text("Sample Window Content")
+ .h3()
+ Text("This is how content appears inside the window container")
+ .bodySmall(color: SpacedriveColors.Text.secondary)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .padding(20)
+ }
+ .frame(width: 400, height: 300)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/ContentView.swift b/apps/macos2/Spacedrive/Spacedrive/ContentView.swift
deleted file mode 100644
index 22cce699d..000000000
--- a/apps/macos2/Spacedrive/Spacedrive/ContentView.swift
+++ /dev/null
@@ -1,24 +0,0 @@
-//
-// ContentView.swift
-// Spacedrive
-//
-// Created by jamie on 2025-09-26.
-//
-
-import SwiftUI
-
-struct ContentView: View {
- var body: some View {
- VStack {
- Image(systemName: "globe")
- .imageScale(.large)
- .foregroundStyle(.tint)
- Text("Hello, world!")
- }
- .padding()
- }
-}
-
-#Preview {
- ContentView()
-}
diff --git a/apps/macos2/Spacedrive/Spacedrive/DesignSystem/SpacedriveColors.swift b/apps/macos2/Spacedrive/Spacedrive/DesignSystem/SpacedriveColors.swift
new file mode 100644
index 000000000..af1084275
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/DesignSystem/SpacedriveColors.swift
@@ -0,0 +1,83 @@
+import AppKit
+import SwiftUI
+
+/// Spacedrive Design System - Color Palette
+/// Centralized color definitions for consistent theming across all windows
+enum SpacedriveColors {
+ // MARK: - Background Colors
+
+ enum Background {
+ static let primary = Color(red: 0.07, green: 0.07, blue: 0.09) // Darker main background
+ static let secondary = Color(red: 0.12, green: 0.12, blue: 0.14) // Darker box/card backgrounds
+ static let tertiary = Color(red: 0.11, green: 0.11, blue: 0.14) // Darker card backgrounds
+ static let surface = Color(red: 0.08, green: 0.08, blue: 0.12) // Darker elevated surfaces
+ }
+
+ // MARK: - Text Colors
+
+ enum Text {
+ static let primary = Color.white
+ static let secondary = Color.white.opacity(0.7)
+ static let tertiary = Color.white.opacity(0.5)
+ static let disabled = Color.white.opacity(0.3)
+ }
+
+ // MARK: - Accent Colors
+
+ enum Accent {
+ static let primary = Color(red: 0.0, green: 0.48, blue: 1.0) // Spacedrive blue
+ static let secondary = Color(red: 0.34, green: 0.34, blue: 0.34) // Neutral
+ static let success = Color.green
+ static let warning = Color.orange
+ static let error = Color.red
+ static let info = Color.blue
+ }
+
+ // MARK: - Interactive Colors
+
+ enum Interactive {
+ static let hover = Color(red: 0.20, green: 0.20, blue: 0.25).opacity(0.5) // Hover effect for #1C1D26 backgrounds
+ static let pressed = Color(red: 0.25, green: 0.25, blue: 0.30).opacity(0.6) // Pressed effect
+ static let selected = Accent.primary.opacity(0.2)
+ static let focus = Accent.primary
+ }
+
+ // MARK: - Border Colors
+
+ enum Border {
+ static let primary = Color(red: 0.25, green: 0.25, blue: 0.30).opacity(0.3) // Border for #1C1D26 backgrounds
+ static let secondary = Color(red: 0.20, green: 0.20, blue: 0.25).opacity(0.2) // Lighter border
+ static let focus = Accent.primary
+ }
+
+ // MARK: - Traffic Light Colors (Using Native macOS)
+ // Native traffic lights are handled by the system - no custom colors needed
+}
+
+// MARK: - NSColor Extensions for AppKit Integration
+
+extension SpacedriveColors {
+ enum NSColors {
+ static let backgroundPrimary = NSColor(red: 0.07, green: 0.07, blue: 0.09, alpha: 1.0) // Darker main background
+ static let backgroundSecondary = NSColor(red: 0.08, green: 0.08, blue: 0.12, alpha: 1.0) // Darker box/card backgrounds
+ static let accentPrimary = NSColor(red: 0.0, green: 0.48, blue: 1.0, alpha: 1.0)
+ }
+}
+
+// MARK: - Environment Key for Theme
+
+private struct ThemeEnvironmentKey: EnvironmentKey {
+ static let defaultValue: SpacedriveTheme = .dark
+}
+
+extension EnvironmentValues {
+ var spacedriveTheme: SpacedriveTheme {
+ get { self[ThemeEnvironmentKey.self] }
+ set { self[ThemeEnvironmentKey.self] = newValue }
+ }
+}
+
+enum SpacedriveTheme: Codable {
+ case dark
+ case light // Future support
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/DesignSystem/SpacedriveTypography.swift b/apps/macos2/Spacedrive/Spacedrive/DesignSystem/SpacedriveTypography.swift
new file mode 100644
index 000000000..7e69297db
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/DesignSystem/SpacedriveTypography.swift
@@ -0,0 +1,183 @@
+import SwiftUI
+
+/// Spacedrive Design System - Typography
+/// Centralized font definitions for consistent text styling
+enum SpacedriveTypography {
+ // MARK: - Font Weights
+
+ enum Weight {
+ case thin
+ case light
+ case regular
+ case medium
+ case semibold
+ case bold
+ case heavy
+ case black
+
+ var systemWeight: Font.Weight {
+ switch self {
+ case .thin: return .thin
+ case .light: return .light
+ case .regular: return .regular
+ case .medium: return .medium
+ case .semibold: return .semibold
+ case .bold: return .bold
+ case .heavy: return .heavy
+ case .black: return .black
+ }
+ }
+ }
+
+ // MARK: - Typography Scale
+
+ enum Scale {
+ // Headlines
+ static func h1(_ weight: Weight = .bold) -> Font {
+ return .system(size: 32, weight: weight.systemWeight, design: .default)
+ }
+
+ static func h2(_ weight: Weight = .bold) -> Font {
+ return .system(size: 24, weight: weight.systemWeight, design: .default)
+ }
+
+ static func h3(_ weight: Weight = .semibold) -> Font {
+ return .system(size: 20, weight: weight.systemWeight, design: .default)
+ }
+
+ static func h4(_ weight: Weight = .semibold) -> Font {
+ return .system(size: 18, weight: weight.systemWeight, design: .default)
+ }
+
+ static func h5(_ weight: Weight = .medium) -> Font {
+ return .system(size: 16, weight: weight.systemWeight, design: .default)
+ }
+
+ static func h6(_ weight: Weight = .medium) -> Font {
+ return .system(size: 14, weight: weight.systemWeight, design: .default)
+ }
+
+ // Body Text
+ static func body(_ weight: Weight = .regular) -> Font {
+ return .system(size: 14, weight: weight.systemWeight, design: .default)
+ }
+
+ static func bodyLarge(_ weight: Weight = .regular) -> Font {
+ return .system(size: 16, weight: weight.systemWeight, design: .default)
+ }
+
+ static func bodySmall(_ weight: Weight = .regular) -> Font {
+ return .system(size: 12, weight: weight.systemWeight, design: .default)
+ }
+
+ // Labels
+ static func label(_ weight: Weight = .medium) -> Font {
+ return .system(size: 12, weight: weight.systemWeight, design: .default)
+ }
+
+ static func labelLarge(_ weight: Weight = .medium) -> Font {
+ return .system(size: 14, weight: weight.systemWeight, design: .default)
+ }
+
+ static func labelSmall(_ weight: Weight = .medium) -> Font {
+ return .system(size: 10, weight: weight.systemWeight, design: .default)
+ }
+
+ // Caption
+ static func caption(_ weight: Weight = .regular) -> Font {
+ return .system(size: 11, weight: weight.systemWeight, design: .default)
+ }
+
+ static func captionSmall(_ weight: Weight = .regular) -> Font {
+ return .system(size: 9, weight: weight.systemWeight, design: .default)
+ }
+
+ // Code/Monospace
+ static func code(_ weight: Weight = .regular) -> Font {
+ return .system(size: 12, weight: weight.systemWeight, design: .monospaced)
+ }
+
+ static func codeSmall(_ weight: Weight = .regular) -> Font {
+ return .system(size: 10, weight: weight.systemWeight, design: .monospaced)
+ }
+ }
+}
+
+// MARK: - Text Style Modifiers
+
+extension Text {
+ // Headlines
+ func h1(_ weight: SpacedriveTypography.Weight = .bold, color: Color = SpacedriveColors.Text.primary) -> some View {
+ font(SpacedriveTypography.Scale.h1(weight))
+ .foregroundColor(color)
+ }
+
+ func h2(_ weight: SpacedriveTypography.Weight = .bold, color: Color = SpacedriveColors.Text.primary) -> some View {
+ font(SpacedriveTypography.Scale.h2(weight))
+ .foregroundColor(color)
+ }
+
+ func h3(_ weight: SpacedriveTypography.Weight = .semibold, color: Color = SpacedriveColors.Text.primary) -> some View {
+ font(SpacedriveTypography.Scale.h3(weight))
+ .foregroundColor(color)
+ }
+
+ func h4(_ weight: SpacedriveTypography.Weight = .semibold, color: Color = SpacedriveColors.Text.primary) -> some View {
+ font(SpacedriveTypography.Scale.h4(weight))
+ .foregroundColor(color)
+ }
+
+ func h5(_ weight: SpacedriveTypography.Weight = .medium, color: Color = SpacedriveColors.Text.primary) -> some View {
+ font(SpacedriveTypography.Scale.h5(weight))
+ .foregroundColor(color)
+ }
+
+ func h6(_ weight: SpacedriveTypography.Weight = .medium, color: Color = SpacedriveColors.Text.primary) -> some View {
+ font(SpacedriveTypography.Scale.h6(weight))
+ .foregroundColor(color)
+ }
+
+ // Body
+ func body(_ weight: SpacedriveTypography.Weight = .regular, color: Color = SpacedriveColors.Text.primary) -> some View {
+ font(SpacedriveTypography.Scale.body(weight))
+ .foregroundColor(color)
+ }
+
+ func bodyLarge(_ weight: SpacedriveTypography.Weight = .regular, color: Color = SpacedriveColors.Text.primary) -> some View {
+ font(SpacedriveTypography.Scale.bodyLarge(weight))
+ .foregroundColor(color)
+ }
+
+ func bodySmall(_ weight: SpacedriveTypography.Weight = .regular, color: Color = SpacedriveColors.Text.secondary) -> some View {
+ font(SpacedriveTypography.Scale.bodySmall(weight))
+ .foregroundColor(color)
+ }
+
+ // Labels
+ func label(_ weight: SpacedriveTypography.Weight = .medium, color: Color = SpacedriveColors.Text.secondary) -> some View {
+ font(SpacedriveTypography.Scale.label(weight))
+ .foregroundColor(color)
+ }
+
+ func labelLarge(_ weight: SpacedriveTypography.Weight = .medium, color: Color = SpacedriveColors.Text.secondary) -> some View {
+ font(SpacedriveTypography.Scale.labelLarge(weight))
+ .foregroundColor(color)
+ }
+
+ func labelSmall(_ weight: SpacedriveTypography.Weight = .medium, color: Color = SpacedriveColors.Text.tertiary) -> some View {
+ font(SpacedriveTypography.Scale.labelSmall(weight))
+ .foregroundColor(color)
+ }
+
+ // Caption
+ func caption(_ weight: SpacedriveTypography.Weight = .regular, color: Color = SpacedriveColors.Text.tertiary) -> some View {
+ font(SpacedriveTypography.Scale.caption(weight))
+ .foregroundColor(color)
+ }
+
+ // Code
+ func code(_ weight: SpacedriveTypography.Weight = .regular, color: Color = SpacedriveColors.Text.primary) -> some View {
+ font(SpacedriveTypography.Scale.code(weight))
+ .foregroundColor(color)
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Development/DevControlsView.swift b/apps/macos2/Spacedrive/Spacedrive/Development/DevControlsView.swift
new file mode 100644
index 000000000..2f72cf12d
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Development/DevControlsView.swift
@@ -0,0 +1,164 @@
+import SwiftUI
+
+/// Development Controls View
+/// A compact view that can be added to any window for quick development window switching
+struct DevControlsView: View {
+ @StateObject private var devManager = DevWindowManager.shared
+ @State private var showingQuickActions = false
+
+ var body: some View {
+ VStack(spacing: 8) {
+ // Compact toggle button
+ Button(action: { showingQuickActions.toggle() }) {
+ HStack(spacing: 6) {
+ Image(systemName: "rectangle.3.group")
+ .font(.caption)
+ Text("Dev")
+ .font(.caption)
+ .fontWeight(.medium)
+ }
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(Color.blue.opacity(0.1))
+ .foregroundColor(.blue)
+ .cornerRadius(6)
+ }
+ .buttonStyle(PlainButtonStyle())
+
+ // Quick actions popup
+ if showingQuickActions {
+ VStack(spacing: 4) {
+ ForEach(DevWindowConfiguration.allCases, id: \.self) { config in
+ Button(action: {
+ devManager.switchTo(config)
+ showingQuickActions = false
+ }) {
+ HStack {
+ Text(config.displayName)
+ .font(.caption)
+ .foregroundColor(.primary)
+ Spacer()
+ if devManager.currentConfiguration == config {
+ Image(systemName: "checkmark")
+ .font(.caption2)
+ .foregroundColor(.blue)
+ }
+ }
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(
+ RoundedRectangle(cornerRadius: 4)
+ .fill(devManager.currentConfiguration == config ?
+ Color.blue.opacity(0.1) : Color.clear)
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+ }
+ .padding(8)
+ .background(Color(.windowBackgroundColor))
+ .cornerRadius(8)
+ .shadow(radius: 4)
+ }
+ }
+ .onTapGesture {
+ // Close popup when tapping outside
+ showingQuickActions = false
+ }
+ }
+}
+
+/// Development Status Bar
+/// Shows current configuration and allows quick switching
+struct DevStatusBar: View {
+ @StateObject private var devManager = DevWindowManager.shared
+
+ var body: some View {
+ HStack {
+ Image(systemName: "hammer.fill")
+ .foregroundColor(.orange)
+ .font(.caption)
+
+ Text("Dev:")
+ .font(.caption)
+ .foregroundColor(.secondary)
+
+ Text(devManager.currentConfiguration.displayName)
+ .font(.caption)
+ .fontWeight(.medium)
+ .foregroundColor(.primary)
+
+ Spacer()
+
+ Button("Switch") {
+ // Open the full configuration selector
+ NSApp.sendAction(Selector(("showDevConfiguration:")), to: nil, from: nil)
+ }
+ .font(.caption)
+ .buttonStyle(.borderless)
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(Color.orange.opacity(0.1))
+ .cornerRadius(4)
+ }
+}
+
+/// Development Window Overlay
+/// Can be overlaid on any window to show development controls
+struct DevWindowOverlay: View {
+ @State private var isVisible = true
+
+ var body: some View {
+ if isVisible {
+ VStack {
+ HStack {
+ Spacer()
+ DevControlsView()
+ .padding(.trailing, 12)
+ .padding(.top, 8)
+ }
+ Spacer()
+ }
+ .allowsHitTesting(true)
+ }
+ }
+}
+
+// MARK: - View Extensions for Easy Integration
+
+extension View {
+ /// Adds development controls to any view
+ func withDevControls() -> some View {
+ ZStack {
+ self
+ DevWindowOverlay()
+ }
+ }
+
+ /// Adds a development status bar at the top
+ func withDevStatusBar() -> some View {
+ VStack(spacing: 0) {
+ DevStatusBar()
+ self
+ }
+ }
+}
+
+// MARK: - Keyboard Shortcuts for Quick Switching
+
+#if DEBUG
+struct DevKeyboardShortcuts: View {
+ var body: some View {
+ EmptyView()
+ .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("devShortcut"))) { notification in
+ if let config = notification.object as? DevWindowConfiguration {
+ DevWindowManager.shared.switchTo(config)
+ }
+ }
+ }
+}
+
+// Add this to your main view for keyboard shortcuts
+// DevKeyboardShortcuts()
+#endif
diff --git a/apps/macos2/Spacedrive/Spacedrive/Development/DevWindowConfiguration.swift b/apps/macos2/Spacedrive/Spacedrive/Development/DevWindowConfiguration.swift
new file mode 100644
index 000000000..ef12b9383
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Development/DevWindowConfiguration.swift
@@ -0,0 +1,204 @@
+import SwiftUI
+import Combine
+
+/// Development Window Configuration Helper
+/// This file provides easy-to-use functions for switching between different window configurations during development.
+///
+/// Usage:
+/// - Change the `currentDevConfiguration` variable below to switch between different setups
+/// - Rebuild and run to see the new configuration
+/// - Use the DevWindowToggleView for runtime switching
+
+// MARK: - Quick Toggle Configuration
+// 🔧 CHANGE THIS VALUE TO SWITCH WINDOW CONFIGURATIONS 🔧
+// Available options: .default, .browserOnly, .companionOnly, .allWindows, .compact, .development
+let currentDevConfiguration: DevWindowConfiguration = .development
+
+// MARK: - Development Window Manager
+
+@MainActor
+class DevWindowManager: ObservableObject {
+ static let shared = DevWindowManager()
+
+ @Published var currentConfiguration: DevWindowConfiguration = currentDevConfiguration
+
+ private init() {
+ // Set the initial configuration in SharedAppState
+ SharedAppState.shared.setDevWindowConfiguration(currentDevConfiguration)
+ }
+
+ func switchTo(_ config: DevWindowConfiguration) {
+ currentConfiguration = config
+ SharedAppState.shared.setDevWindowConfiguration(config)
+
+ // Close all existing windows
+ SharedAppState.shared.dispatch(.closeAllWindows)
+
+ // Open new windows after a brief delay
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+ SharedAppState.shared.dispatch(.openDevWindows)
+ }
+
+ print("🔧 Switched to development configuration: \(config.displayName)")
+ }
+
+ func getConfigurationDescription() -> String {
+ return currentConfiguration.description
+ }
+}
+
+// MARK: - Development Window Toggle View
+
+struct DevWindowToggleView: View {
+ @StateObject private var devManager = DevWindowManager.shared
+ @State private var showingConfiguration = false
+
+ var body: some View {
+ VStack(spacing: 12) {
+ // Current Configuration Display
+ HStack {
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Dev Config")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ Text(devManager.currentConfiguration.displayName)
+ .font(.headline)
+ .foregroundColor(.primary)
+ }
+
+ Spacer()
+
+ Button("Switch") {
+ showingConfiguration.toggle()
+ }
+ .buttonStyle(.bordered)
+ .controlSize(.small)
+ }
+
+ // Configuration Description
+ Text(devManager.getConfigurationDescription())
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.leading)
+ }
+ .padding()
+ .background(Color.gray.opacity(0.1))
+ .cornerRadius(8)
+ .sheet(isPresented: $showingConfiguration) {
+ DevConfigurationSelector()
+ }
+ }
+}
+
+struct DevConfigurationSelector: View {
+ @Environment(\.dismiss) private var dismiss
+ @StateObject private var devManager = DevWindowManager.shared
+
+ var body: some View {
+ NavigationView {
+ VStack(spacing: 20) {
+ Text("Development Window Configuration")
+ .font(.title2)
+ .fontWeight(.semibold)
+ .padding(.top)
+
+ Text("Choose a window configuration for development. This will close all current windows and open the selected configuration.")
+ .font(.body)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal)
+
+ LazyVGrid(columns: [
+ GridItem(.flexible()),
+ GridItem(.flexible())
+ ], spacing: 16) {
+ ForEach(DevWindowConfiguration.allCases, id: \.self) { config in
+ DevConfigCard(
+ configuration: config,
+ isSelected: devManager.currentConfiguration == config
+ ) {
+ devManager.switchTo(config)
+ dismiss()
+ }
+ }
+ }
+ .padding(.horizontal)
+
+ Spacer()
+ }
+ .toolbar {
+ ToolbarItem(placement: .primaryAction) {
+ Button("Done") {
+ dismiss()
+ }
+ }
+ }
+ }
+ .frame(width: 600, height: 500)
+ }
+}
+
+struct DevConfigCard: View {
+ let configuration: DevWindowConfiguration
+ let isSelected: Bool
+ let onTap: () -> Void
+
+ var body: some View {
+ Button(action: onTap) {
+ VStack(alignment: .leading, spacing: 8) {
+ Text(configuration.displayName)
+ .font(.headline)
+ .foregroundColor(isSelected ? .white : .primary)
+ .multilineTextAlignment(.leading)
+
+ Text(configuration.description)
+ .font(.caption)
+ .foregroundColor(isSelected ? .white.opacity(0.8) : .secondary)
+ .multilineTextAlignment(.leading)
+
+ Spacer()
+ }
+ .padding()
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .frame(height: 120)
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .fill(isSelected ? Color.blue : Color.gray.opacity(0.1))
+ .overlay(
+ RoundedRectangle(cornerRadius: 8)
+ .stroke(isSelected ? Color.blue : Color.clear, lineWidth: 2)
+ )
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+
+// MARK: - Quick Access Functions
+
+/// Quick function to switch to browser-only mode
+@MainActor
+func switchToBrowserOnly() {
+ DevWindowManager.shared.switchTo(.browserOnly)
+}
+
+/// Quick function to switch to development mode (browser + inspector)
+@MainActor
+func switchToDevelopmentMode() {
+ DevWindowManager.shared.switchTo(.development)
+}
+
+/// Quick function to switch to companion-only mode
+@MainActor
+func switchToCompanionOnly() {
+ DevWindowManager.shared.switchTo(.companionOnly)
+}
+
+/// Quick function to open all windows
+@MainActor
+func openAllWindows() {
+ DevWindowManager.shared.switchTo(.allWindows)
+}
+
+// MARK: - Window Management Extensions
+// (Extensions removed to avoid redeclaration conflicts)
diff --git a/apps/macos2/Spacedrive/Spacedrive/MenuBar/MenuBarManager.swift b/apps/macos2/Spacedrive/Spacedrive/MenuBar/MenuBarManager.swift
new file mode 100644
index 000000000..a285c8881
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/MenuBar/MenuBarManager.swift
@@ -0,0 +1,508 @@
+import AppKit
+import SwiftUI
+import Combine
+
+/// Manages the macOS menu bar for Spacedrive
+@MainActor
+class MenuBarManager: ObservableObject {
+ static let shared = MenuBarManager()
+
+ private var cancellables = Set()
+
+ private init() {
+ setupObservers()
+ }
+
+ private func setupObservers() {
+ // Observe changes to available libraries and current library
+ SharedAppState.shared.$availableLibraries
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ self?.updateLibrariesMenu()
+ }
+ .store(in: &cancellables)
+
+ SharedAppState.shared.$currentLibraryId
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ self?.updateLibrariesMenu()
+ }
+ .store(in: &cancellables)
+ }
+
+ private var mainMenu: NSMenu?
+
+ func setupMenuBar() {
+ let mainMenu = NSMenu()
+ self.mainMenu = mainMenu
+ NSApp.mainMenu = mainMenu
+
+ // App Menu (Spacedrive)
+ setupAppMenu(mainMenu)
+
+ // Daemon Menu
+ setupDaemonMenu(mainMenu)
+
+ // File Menu
+ setupFileMenu(mainMenu)
+
+ // Edit Menu
+ setupEditMenu(mainMenu)
+
+ // View Menu
+ setupViewMenu(mainMenu)
+
+ // Window Menu
+ setupWindowMenu(mainMenu)
+
+ // Help Menu
+ setupHelpMenu(mainMenu)
+ }
+
+ func getMainMenu() -> NSMenu? {
+ return mainMenu
+ }
+
+ func ensureMenuBarVisible() {
+ // Force menu bar to be visible and properly set
+ if let menu = mainMenu {
+ NSApp.mainMenu = menu
+ } else {
+ // Recreate menu bar if it's missing
+ setupMenuBar()
+ }
+ }
+
+ // MARK: - App Menu (Spacedrive)
+
+ private func setupAppMenu(_ mainMenu: NSMenu) {
+ let appMenuItem = NSMenuItem()
+ mainMenu.addItem(appMenuItem)
+
+ let appMenu = NSMenu()
+ appMenuItem.submenu = appMenu
+
+ // About Spacedrive
+ let aboutItem = NSMenuItem(
+ title: "About Spacedrive",
+ action: #selector(showAbout),
+ keyEquivalent: ""
+ )
+ aboutItem.target = self
+ appMenu.addItem(aboutItem)
+
+ appMenu.addItem(NSMenuItem.separator())
+
+ // Preferences
+ let preferencesItem = NSMenuItem(
+ title: "Preferences...",
+ action: #selector(showPreferences),
+ keyEquivalent: ","
+ )
+ preferencesItem.target = self
+ appMenu.addItem(preferencesItem)
+
+ appMenu.addItem(NSMenuItem.separator())
+
+ // Services
+ let servicesMenuItem = NSMenuItem(title: "Services", action: nil, keyEquivalent: "")
+ let servicesMenu = NSMenu()
+ servicesMenuItem.submenu = servicesMenu
+ appMenu.addItem(servicesMenuItem)
+ NSApp.servicesMenu = servicesMenu
+
+ appMenu.addItem(NSMenuItem.separator())
+
+ // Hide Spacedrive
+ let hideItem = NSMenuItem(
+ title: "Hide Spacedrive",
+ action: #selector(NSApp.hide(_:)),
+ keyEquivalent: "h"
+ )
+ appMenu.addItem(hideItem)
+
+ // Hide Others
+ let hideOthersItem = NSMenuItem(
+ title: "Hide Others",
+ action: #selector(NSApp.hideOtherApplications(_:)),
+ keyEquivalent: "h"
+ )
+ hideOthersItem.keyEquivalentModifierMask = [.command, .option]
+ appMenu.addItem(hideOthersItem)
+
+ // Show All
+ let showAllItem = NSMenuItem(
+ title: "Show All",
+ action: #selector(NSApp.unhideAllApplications(_:)),
+ keyEquivalent: ""
+ )
+ appMenu.addItem(showAllItem)
+
+ appMenu.addItem(NSMenuItem.separator())
+
+ // Quit Spacedrive
+ let quitItem = NSMenuItem(
+ title: "Quit Spacedrive",
+ action: #selector(NSApp.terminate(_:)),
+ keyEquivalent: "q"
+ )
+ appMenu.addItem(quitItem)
+ }
+
+ // MARK: - Daemon Menu
+
+ private func setupDaemonMenu(_ mainMenu: NSMenu) {
+ let daemonMenuItem = NSMenuItem(title: "Daemon", action: nil, keyEquivalent: "")
+ mainMenu.addItem(daemonMenuItem)
+
+ let daemonMenu = NSMenu(title: "Daemon")
+ daemonMenuItem.submenu = daemonMenu
+
+ // Connection Status (disabled item showing current status)
+ let statusItem = NSMenuItem(
+ title: "Status: Disconnected",
+ action: nil,
+ keyEquivalent: ""
+ )
+ statusItem.isEnabled = false
+ daemonMenu.addItem(statusItem)
+
+ daemonMenu.addItem(NSMenuItem.separator())
+
+ // Connect to Daemon
+ let connectItem = NSMenuItem(
+ title: "Connect to Daemon",
+ action: #selector(connectToDaemon),
+ keyEquivalent: ""
+ )
+ connectItem.target = self
+ daemonMenu.addItem(connectItem)
+
+ // Disconnect from Daemon
+ let disconnectItem = NSMenuItem(
+ title: "Disconnect from Daemon",
+ action: #selector(disconnectFromDaemon),
+ keyEquivalent: ""
+ )
+ disconnectItem.target = self
+ disconnectItem.isEnabled = false
+ daemonMenu.addItem(disconnectItem)
+
+ daemonMenu.addItem(NSMenuItem.separator())
+
+ // Refresh Jobs
+ let refreshItem = NSMenuItem(
+ title: "Refresh Jobs",
+ action: #selector(refreshJobs),
+ keyEquivalent: "r"
+ )
+ refreshItem.target = self
+ daemonMenu.addItem(refreshItem)
+
+ // Store references for dynamic updates
+ for item in daemonMenu.items {
+ switch item.title {
+ case "Status: Disconnected", "Status: Connected", "Status: Connecting":
+ item.representedObject = "status"
+ case "Connect to Daemon":
+ item.representedObject = "connect"
+ case "Disconnect from Daemon":
+ item.representedObject = "disconnect"
+ default:
+ break
+ }
+ }
+
+ // Store menu reference for updates
+ self.daemonMenu = daemonMenu
+ }
+
+ private var daemonMenu: NSMenu?
+
+ // MARK: - File Menu
+
+ private func setupFileMenu(_ mainMenu: NSMenu) {
+ let fileMenuItem = NSMenuItem(title: "File", action: nil, keyEquivalent: "")
+ mainMenu.addItem(fileMenuItem)
+
+ let fileMenu = NSMenu(title: "File")
+ fileMenuItem.submenu = fileMenu
+
+ // New Window
+ let newWindowItem = NSMenuItem(
+ title: "New Browser Window",
+ action: #selector(newBrowserWindow),
+ keyEquivalent: "n"
+ )
+ newWindowItem.target = self
+ fileMenu.addItem(newWindowItem)
+
+ fileMenu.addItem(NSMenuItem.separator())
+
+ // Libraries subsection
+ setupLibrariesSubmenu(fileMenu)
+
+ fileMenu.addItem(NSMenuItem.separator())
+
+ // Close Window
+ let closeItem = NSMenuItem(
+ title: "Close Window",
+ action: #selector(performClose),
+ keyEquivalent: "w"
+ )
+ closeItem.target = self
+ fileMenu.addItem(closeItem)
+ }
+
+ // MARK: - Libraries Submenu
+
+ private func setupLibrariesSubmenu(_ fileMenu: NSMenu) {
+ let librariesMenuItem = NSMenuItem(title: "Libraries", action: nil, keyEquivalent: "")
+ fileMenu.addItem(librariesMenuItem)
+
+ let librariesMenu = NSMenu(title: "Libraries")
+ librariesMenuItem.submenu = librariesMenu
+
+ // Store reference for dynamic updates
+ self.librariesMenu = librariesMenu
+
+ // Initial population
+ updateLibrariesMenu()
+ }
+
+ private var librariesMenu: NSMenu?
+
+ // MARK: - Edit Menu
+
+ private func setupEditMenu(_ mainMenu: NSMenu) {
+ let editMenuItem = NSMenuItem(title: "Edit", action: nil, keyEquivalent: "")
+ mainMenu.addItem(editMenuItem)
+
+ let editMenu = NSMenu(title: "Edit")
+ editMenuItem.submenu = editMenu
+
+ // Standard edit items
+ editMenu.addItem(NSMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z"))
+ editMenu.addItem(NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "Z"))
+ editMenu.addItem(NSMenuItem.separator())
+ editMenu.addItem(NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"))
+ editMenu.addItem(NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"))
+ editMenu.addItem(NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"))
+ editMenu.addItem(NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"))
+ }
+
+ // MARK: - View Menu
+
+ private func setupViewMenu(_ mainMenu: NSMenu) {
+ let viewMenuItem = NSMenuItem(title: "View", action: nil, keyEquivalent: "")
+ mainMenu.addItem(viewMenuItem)
+
+ let viewMenu = NSMenu(title: "View")
+ viewMenuItem.submenu = viewMenu
+
+ // Show Job Monitor
+ let jobMonitorItem = NSMenuItem(
+ title: "Show Job Monitor",
+ action: #selector(showJobMonitor),
+ keyEquivalent: "j"
+ )
+ jobMonitorItem.target = self
+ viewMenu.addItem(jobMonitorItem)
+
+ // Show Icon Showcase
+ let iconShowcaseItem = NSMenuItem(
+ title: "Show Icon Showcase",
+ action: #selector(showIconShowcase),
+ keyEquivalent: "i"
+ )
+ iconShowcaseItem.target = self
+ viewMenu.addItem(iconShowcaseItem)
+
+ // Show Inspector
+ let inspectorItem = NSMenuItem(
+ title: "Show Inspector",
+ action: #selector(showInspector),
+ keyEquivalent: "f"
+ )
+ inspectorItem.target = self
+ viewMenu.addItem(inspectorItem)
+
+ viewMenu.addItem(NSMenuItem.separator())
+
+ // Enter Full Screen
+ let fullScreenItem = NSMenuItem(
+ title: "Enter Full Screen",
+ action: #selector(toggleFullScreen),
+ keyEquivalent: "f"
+ )
+ fullScreenItem.keyEquivalentModifierMask = [.command, .control]
+ fullScreenItem.target = self
+ viewMenu.addItem(fullScreenItem)
+ }
+
+ // MARK: - Window Menu
+
+ private func setupWindowMenu(_ mainMenu: NSMenu) {
+ let windowMenuItem = NSMenuItem(title: "Window", action: nil, keyEquivalent: "")
+ mainMenu.addItem(windowMenuItem)
+
+ let windowMenu = NSMenu(title: "Window")
+ windowMenuItem.submenu = windowMenu
+ NSApp.windowsMenu = windowMenu
+
+ // Minimize
+ windowMenu.addItem(NSMenuItem(title: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m"))
+
+ // Zoom
+ windowMenu.addItem(NSMenuItem(title: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: ""))
+
+ windowMenu.addItem(NSMenuItem.separator())
+
+ // Bring All to Front
+ windowMenu.addItem(NSMenuItem(title: "Bring All to Front", action: #selector(NSApp.arrangeInFront(_:)), keyEquivalent: ""))
+ }
+
+ // MARK: - Help Menu
+
+ private func setupHelpMenu(_ mainMenu: NSMenu) {
+ let helpMenuItem = NSMenuItem(title: "Help", action: nil, keyEquivalent: "")
+ mainMenu.addItem(helpMenuItem)
+
+ let helpMenu = NSMenu(title: "Help")
+ helpMenuItem.submenu = helpMenu
+ NSApp.helpMenu = helpMenu
+
+ let helpItem = NSMenuItem(
+ title: "Spacedrive Help",
+ action: #selector(showHelp),
+ keyEquivalent: "?"
+ )
+ helpItem.target = self
+ helpMenu.addItem(helpItem)
+ }
+
+ // MARK: - Menu Actions
+
+ @objc private func showAbout() {
+ NSApp.orderFrontStandardAboutPanel(nil)
+ }
+
+ @objc private func showPreferences() {
+ // SwiftUI handles settings window automatically
+ NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
+ }
+
+ @objc private func connectToDaemon() {
+ SharedAppState.shared.dispatch(.connectToDaemon)
+ }
+
+ @objc private func disconnectFromDaemon() {
+ SharedAppState.shared.dispatch(.disconnectFromDaemon)
+ }
+
+ @objc private func refreshJobs() {
+ SharedAppState.shared.dispatch(.refreshJobs)
+ }
+
+ @objc private func newBrowserWindow() {
+ // SwiftUI handles window opening automatically
+ print("Browser window request - handled by SwiftUI WindowGroup")
+ }
+
+ @objc private func showJobMonitor() {
+ // SwiftUI handles window opening automatically
+ print("Job monitor request - handled by SwiftUI WindowGroup")
+ }
+
+ @objc private func showIconShowcase() {
+ // SwiftUI handles window opening automatically
+ print("Icon showcase request - handled by SwiftUI WindowGroup")
+ }
+
+ @objc private func showInspector() {
+ // SwiftUI handles window opening automatically
+ print("Inspector request - handled by SwiftUI WindowGroup")
+ }
+
+ @objc private func performClose() {
+ NSApp.keyWindow?.performClose(nil)
+ }
+
+ @objc private func toggleFullScreen() {
+ NSApp.keyWindow?.toggleFullScreen(nil)
+ }
+
+ @objc private func showHelp() {
+ if let url = URL(string: "https://spacedrive.com/docs") {
+ NSWorkspace.shared.open(url)
+ }
+ }
+
+ @objc private func switchToLibrary(_ sender: NSMenuItem) {
+ guard let library = sender.representedObject as? LibraryInfo else { return }
+ SharedAppState.shared.dispatch(.switchToLibrary(library))
+ }
+
+ // MARK: - Dynamic Menu Updates
+
+ func updateDaemonMenuStatus(_ status: ConnectionStatus) {
+ guard let daemonMenu = daemonMenu else { return }
+
+ for item in daemonMenu.items {
+ guard let representedObject = item.representedObject as? String else { continue }
+
+ switch representedObject {
+ case "status":
+ item.title = "Status: \(status.displayName)"
+ case "connect":
+ item.isEnabled = status != .connected && status != .connecting
+ case "disconnect":
+ item.isEnabled = status == .connected
+ default:
+ break
+ }
+ }
+ }
+
+ func updateLibrariesMenu() {
+ guard let librariesMenu = librariesMenu else { return }
+
+ // Clear existing items
+ librariesMenu.removeAllItems()
+
+ // Get current libraries from SharedAppState
+ let availableLibraries = SharedAppState.shared.availableLibraries
+ let currentLibraryId = SharedAppState.shared.currentLibraryId
+
+ if availableLibraries.isEmpty {
+ // No libraries available
+ let noLibrariesItem = NSMenuItem(
+ title: "No Libraries Available",
+ action: nil,
+ keyEquivalent: ""
+ )
+ noLibrariesItem.isEnabled = false
+ librariesMenu.addItem(noLibrariesItem)
+ } else {
+ // Add each library as a menu item
+ for library in availableLibraries {
+ let libraryItem = NSMenuItem(
+ title: library.name,
+ action: #selector(switchToLibrary),
+ keyEquivalent: ""
+ )
+ libraryItem.target = self
+ libraryItem.representedObject = library
+
+ // Add checkmark for selected library
+ if library.id == currentLibraryId {
+ libraryItem.state = .on
+ }
+
+ librariesMenu.addItem(libraryItem)
+ }
+ }
+ }
+}
+
+// ConnectionStatus displayName is already defined in JobModels.swift
diff --git a/apps/macos2/Spacedrive/Spacedrive/Services/DaemonConnector.swift b/apps/macos2/Spacedrive/Spacedrive/Services/DaemonConnector.swift
new file mode 100644
index 000000000..bd371f44a
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Services/DaemonConnector.swift
@@ -0,0 +1,567 @@
+import Foundation
+import os.log
+import Combine
+@preconcurrency import SpacedriveClient
+
+// Now using generated types from SpacedriveClient - no manual definitions needed!
+
+@MainActor
+class DaemonConnector: ObservableObject {
+ @Published var connectionStatus: ConnectionStatus = .disconnected
+ @Published var jobs: [JobInfo] = []
+ @Published var currentLibraryId: String?
+ @Published var availableLibraries: [LibraryInfo] = []
+ @Published var coreStatus: CoreStatus?
+
+ private let socketPath = "/Users/jamespine/Library/Application Support/spacedrive/daemon/daemon.sock"
+ let client: SpacedriveClient
+ private var eventTask: Task?
+ private let logger = Logger(subsystem: "com.spacedrive.daemon", category: "DaemonConnector")
+
+ init() {
+ logger.info("DaemonConnector initializing with socket path: \(self.socketPath)")
+ client = SpacedriveClient(socketPath: self.socketPath)
+ connect()
+ }
+
+ nonisolated deinit {
+ logger.info("DaemonConnector deinitializing")
+ eventTask?.cancel()
+ eventTask = nil
+ }
+
+ func connect() {
+ guard connectionStatus != .connecting, connectionStatus != .connected else {
+ return
+ }
+
+ connectionStatus = .connecting
+
+ // Check if socket file exists
+ guard FileManager.default.fileExists(atPath: socketPath) else {
+ print("❌ Daemon socket not found at: \(socketPath)")
+ connectionStatus = .error("Daemon socket not found. Is Spacedrive daemon running?")
+ return
+ }
+
+ // Start connection and event subscription
+ Task {
+ await startConnection()
+ }
+ }
+
+ func disconnect() {
+ eventTask?.cancel()
+ eventTask = nil
+ connectionStatus = .disconnected
+ }
+
+ private func startConnection() async {
+ do {
+ // Test connection with ping (with timeout)
+ try await withTimeout(seconds: 5) { [self] in
+ try await client.ping()
+ }
+
+ connectionStatus = .connected
+
+ await fetchCoreStatus()
+
+ // Start event subscription
+ await subscribeToEvents()
+
+ } catch {
+ print("❌ Connection failed: \(error)")
+ connectionStatus = .error("Failed to connect: \(error.localizedDescription)")
+ }
+ }
+
+ private func withTimeout(seconds: TimeInterval, operation: @escaping @Sendable () async throws -> T) async throws -> T {
+ try await withThrowingTaskGroup(of: T.self) { group in
+ group.addTask {
+ try await operation()
+ }
+
+ group.addTask {
+ try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
+ throw SpacedriveError.connectionFailed("Connection timeout after \(seconds) seconds")
+ }
+
+ let result = try await group.next()!
+ group.cancelAll()
+ return result
+ }
+ }
+
+ private func subscribeToEvents() async {
+ let eventTypes = [
+ "JobStarted",
+ "JobProgress",
+ "JobCompleted",
+ "JobFailed",
+ "JobPaused",
+ "JobResumed",
+ "LibraryCreated",
+ "LibraryOpened",
+ "LibraryClosed",
+ "LibraryDeleted",
+ "EntryCreated",
+ "EntryModified",
+ "EntryDeleted",
+ "EntryMoved",
+ ]
+
+ // First, fetch the current job list to establish baseline state
+ await fetchJobList()
+
+ // Also fetch core status
+ await fetchCoreStatus()
+
+ // Then subscribe to events for real-time updates
+ eventTask = Task {
+ do {
+ for try await event in client.subscribe(to: eventTypes) {
+ await handleEvent(event)
+ }
+ } catch {
+ connectionStatus = .error("Event subscription failed: \(error.localizedDescription)")
+ }
+ }
+ }
+
+ private func handleEvent(_ event: SpacedriveEvent) async {
+ // Handle events using the new type-safe Event enum
+ switch event {
+ // Core lifecycle events
+ case .coreStarted:
+ break
+
+ case .coreShutdown:
+ break
+
+ // Job events - update existing jobs in real-time
+ case let .jobStarted(data):
+ self.handleJobStarted(jobId: data.jobId, jobType: data.jobType)
+
+ case let .jobProgress(data):
+ self.handleJobProgress(
+ jobId: data.jobId,
+ jobType: data.jobType,
+ progress: data.progress,
+ message: data.message,
+ genericProgress: data.genericProgress
+ )
+
+ case let .jobCompleted(data):
+ self.handleJobCompleted(jobId: data.jobId, jobType: data.jobType, output: data.output)
+
+ case let .jobFailed(data):
+ self.handleJobFailed(jobId: data.jobId, jobType: data.jobType, error: data.error)
+
+ case let .jobPaused(data):
+ self.handleJobPaused(jobId: data.jobId)
+
+ case let .jobResumed(data):
+ self.handleJobResumed(jobId: data.jobId)
+
+ // Library events
+ case .libraryCreated:
+ Task { await self.refreshLibraryStats() }
+
+ case .libraryOpened:
+ Task { await self.refreshLibraryStats() }
+
+ case .libraryClosed:
+ Task { await self.refreshLibraryStats() }
+
+ case .libraryDeleted:
+ Task { await self.refreshLibraryStats() }
+
+ // Entry events that affect library statistics
+ case .entryCreated:
+ Task { await self.refreshLibraryStats() }
+
+ case .entryModified:
+ Task { await self.refreshLibraryStats() }
+
+ case .entryDeleted:
+ Task { await self.refreshLibraryStats() }
+
+ case .entryMoved:
+ Task { await self.refreshLibraryStats() }
+
+ // Other events can be handled as needed
+ default:
+ break
+ }
+ }
+
+ // MARK: - Job Event Handlers
+
+ private func handleJobStarted(jobId: String, jobType: String) {
+ // Only create a new job if it doesn't exist
+ if job(withId: jobId) == nil {
+ let newJob = JobInfo(
+ id: jobId,
+ name: jobType.capitalized,
+ status: .running,
+ progress: 0.0,
+ startedAt: Date(),
+ completedAt: nil,
+ errorMessage: nil
+ )
+ updateOrAddJob(newJob)
+ }
+ }
+
+ private func handleJobProgress(jobId: String, jobType: String, progress: Double, message _: String?, genericProgress: GenericProgress?) {
+ if var existingJob = job(withId: jobId) {
+ existingJob.progress = progress
+
+ // Keep the actual job type as the title
+ existingJob.name = formatJobTitle(jobType)
+ existingJob.jobType = jobType
+
+ // Use enhanced progress information for better UX
+ if let generic = genericProgress {
+ // Store additional progress details
+ existingJob.currentPhase = generic.phase
+ existingJob.completionInfo = "\(generic.completion.completed)/\(generic.completion.total)"
+
+ // Extract current path from genericProgress, avoiding fake status messages
+ existingJob.currentPath = extractCurrentPath(from: generic)
+
+ // Check for errors/warnings
+ if generic.performance.errorCount > 0 || generic.performance.warningCount > 0 {
+ existingJob.hasIssues = true
+ existingJob.issuesInfo = "⚠️ \(generic.performance.errorCount) errors, \(generic.performance.warningCount) warnings"
+ }
+ }
+
+ existingJob.status = .running
+ updateOrAddJob(existingJob)
+
+ // Enhanced logging with more details
+ if genericProgress != nil {
+ } else {}
+ } else {
+ // If we receive progress for a job we don't know about, create it
+ let newJob = JobInfo(
+ id: jobId,
+ name: formatJobTitle(jobType),
+ status: .running,
+ progress: progress,
+ startedAt: Date(),
+ completedAt: nil,
+ errorMessage: nil,
+ currentPhase: genericProgress?.phase,
+ completionInfo: genericProgress != nil ? "\(genericProgress!.completion.completed)/\(genericProgress!.completion.total)" : nil,
+ hasIssues: false,
+ issuesInfo: nil,
+ currentPath: genericProgress != nil ? extractCurrentPath(from: genericProgress!) : nil,
+ jobType: jobType
+ )
+ updateOrAddJob(newJob)
+ }
+ }
+
+ // Helper to format job type into a user-friendly title
+ private func formatJobTitle(_ jobType: String) -> String {
+ switch jobType.lowercased() {
+ case "indexer":
+ return "File Indexer"
+ case "file_copy":
+ return "File Copy"
+ case "thumbnail_generator":
+ return "Thumbnail Generator"
+ case "file_move":
+ return "File Move"
+ case "file_delete":
+ return "File Delete"
+ case "media_processor":
+ return "Media Processor"
+ default:
+ return jobType.replacingOccurrences(of: "_", with: " ").capitalized
+ }
+ }
+
+ // Helper to extract real current path from genericProgress, avoiding fake status messages
+ private func extractCurrentPath(from genericProgress: GenericProgress) -> String? {
+ guard let currentPath = genericProgress.currentPath else { return nil }
+
+ switch currentPath {
+ case let .physical(pathData):
+ let path = pathData.path
+
+ // Check if this looks like a real file path vs a status message
+ // Status messages often contain patterns like "(X/Y)" or "- XX.X%"
+ let isStatusMessage = path.contains(#"\(\d+/\d+\)"#) ||
+ path.contains(#" - \d+\.?\d*%"#) ||
+ path.hasPrefix("Generating content identities") ||
+ path.hasPrefix("Aggregating directory")
+
+ // Only return if it looks like a real file path
+ return isStatusMessage ? nil : path
+
+ default:
+ return nil
+ }
+ }
+
+ private func handleJobCompleted(jobId: String, jobType _: String, output _: JobOutput) {
+ if var existingJob = job(withId: jobId) {
+ existingJob.status = .completed
+ existingJob.progress = 1.0
+ existingJob.completedAt = Date()
+ updateOrAddJob(existingJob)
+ }
+ }
+
+ private func handleJobFailed(jobId: String, jobType _: String, error: String) {
+ if var existingJob = job(withId: jobId) {
+ existingJob.status = .failed
+ existingJob.errorMessage = error
+ updateOrAddJob(existingJob)
+ }
+ }
+
+ private func handleJobPaused(jobId: String) {
+ if var existingJob = job(withId: jobId) {
+ existingJob.status = .paused
+ updateOrAddJob(existingJob)
+ }
+ }
+
+ private func handleJobResumed(jobId: String) {
+ if var existingJob = job(withId: jobId) {
+ existingJob.status = .running
+ updateOrAddJob(existingJob)
+ }
+ }
+
+ private func updateOrAddJob(_ jobInfo: JobInfo) {
+ if let index = jobs.firstIndex(where: { $0.id == jobInfo.id }) {
+ jobs[index] = jobInfo
+ } else {
+ jobs.append(jobInfo)
+ }
+
+ // Sort jobs by start date (newest first)
+ jobs.sort { $0.startedAt > $1.startedAt }
+ }
+
+ private func job(withId id: String) -> JobInfo? {
+ return jobs.first { $0.id == id }
+ }
+
+ func reconnect() {
+ disconnect()
+ Task { @MainActor in
+ try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
+ connect()
+ }
+ }
+
+ // MARK: - Job Control
+
+ func pauseJob(_ jobId: String) {
+ guard connectionStatus == .connected else {
+ return
+ }
+
+ Task {
+ do {
+ // Call the jobs.pause action using the new type-safe method
+ _ = try await client.jobs.pause(JobPauseInput(jobId: jobId))
+
+ } catch {}
+ }
+ }
+
+ func resumeJob(_ jobId: String) {
+ guard connectionStatus == .connected else {
+ return
+ }
+
+ Task {
+ do {
+ // Call the jobs.resume action using the new type-safe method
+ _ = try await client.jobs.resume(JobResumeInput(jobId: jobId))
+
+ } catch {}
+ }
+ }
+
+ // MARK: - Library Management
+
+
+ func switchToLibrary(_ library: LibraryInfo) async {
+ do {
+ // Switch to the library using the Swift client's async method
+ try await client.switchToLibrary(library.id)
+
+ // Set the current library ID synchronously
+ currentLibraryId = library.id
+
+ // Refresh job list for the new library
+ await fetchJobList()
+
+ } catch {}
+ }
+
+ func getCurrentLibraryInfo() -> LibraryInfo? {
+ return availableLibraries.first { $0.id == currentLibraryId }
+ }
+
+ // MARK: - Library Statistics Refresh
+
+ private func refreshLibraryStats() async {
+ do {
+ let libraries = try await client.libraries.list(ListLibrariesInput(includeStats: true))
+
+ availableLibraries = libraries.map { library in
+ LibraryInfo(
+ id: library.id,
+ name: library.name,
+ path: library.path,
+ isDefault: false, // TODO: Determine if this is the default library
+ stats: library.stats
+ )
+ }
+ } catch {
+ logger.error("Failed to refresh library statistics: \(error.localizedDescription)")
+ }
+ }
+
+ // MARK: - Job List Management
+
+ private func fetchJobList() async {
+ do {
+ // Check if we have a current library
+ guard let libraryId = currentLibraryId else {
+ logger.info("No current library ID, skipping job fetch")
+ return
+ }
+
+ logger.info("Fetching jobs for library: \(libraryId)")
+
+ // Get jobs using the new type-safe method
+ let jobsResponse = try await client.jobs.list(JobListInput(status: nil))
+
+ logger.info("Received \(jobsResponse.jobs.count) jobs from daemon")
+
+ // Convert JobListItem to JobInfo for the UI using convenience initializer
+ let convertedJobs = jobsResponse.jobs.map { jobItem in
+ JobInfo(from: jobItem)
+ }
+
+ // Update the UI with the converted jobs
+ jobs = convertedJobs
+ logger.info("Updated UI with \(convertedJobs.count) jobs")
+
+ } catch {
+ logger.error("Failed to fetch jobs: \(error.localizedDescription)")
+ connectionStatus = .error("Failed to fetch jobs: \(error.localizedDescription)")
+ }
+ }
+
+ private func fetchCoreStatus() async {
+ do {
+ // Create empty input for core status query
+ let emptyData = Data("{}".utf8)
+ let emptyInput = try JSONDecoder().decode(Empty.self, from: emptyData)
+
+ // Get libraries with stats
+ let libraries = try await client.getLibraries(includeStats: true)
+
+ // Update available libraries
+ availableLibraries = libraries.map { library in
+ LibraryInfo(
+ id: library.id,
+ name: library.name,
+ path: library.path,
+ isDefault: false, // TODO: Determine if this is the default library
+ stats: library.stats
+ )
+ }
+
+ // Auto-select first library if none is selected
+ if currentLibraryId == nil, !libraries.isEmpty {
+ do {
+ try await client.switchToLibrary(libraries[0].id)
+ // Set the library ID synchronously
+ currentLibraryId = libraries[0].id
+ // Fetch jobs after setting the library ID
+ await fetchJobList()
+ } catch {
+ logger.error("Failed to auto-select first library: \(error.localizedDescription)")
+ }
+ } else if currentLibraryId != nil {
+ // Fetch jobs for the existing library
+ await fetchJobList()
+ }
+
+ // Now try core status
+ let status = try await client.core.status(emptyInput)
+
+ // Update the UI with the core status
+ coreStatus = status
+
+ } catch {
+ logger.error("Failed to fetch core status: \(error.localizedDescription)")
+ // Don't update connection status for core status failures
+ }
+ }
+
+ // MARK: - Query Execution
+
+ /// Execute a library query
+ nonisolated func executeLibraryQuery(_ query: T) async throws -> T {
+ logger.info("Executing library query: \(String(describing: type(of: query)))")
+ do {
+ let result = try await client.query(query)
+ logger.info("Library query executed successfully")
+ return result
+ } catch {
+ logger.error("Library query failed: \(error.localizedDescription)")
+ logger.error("Query error details: \(String(describing: error))")
+ throw error
+ }
+ }
+
+ /// Execute a query using the internal client
+ nonisolated func query(_ query: T) async throws -> T {
+ logger.info("Executing generic query: \(String(describing: type(of: query)))")
+ do {
+ let result = try await client.query(query)
+ logger.info("Generic query executed successfully")
+ return result
+ } catch {
+ logger.error("Generic query failed: \(error.localizedDescription)")
+ logger.error("Query error details: \(String(describing: error))")
+ throw error
+ }
+ }
+
+ /// Execute a FileByPathQuery and return the File result
+ nonisolated func queryFileByPath(_ query: FileByPathQuery) async throws -> File? {
+ logger.info("Executing FileByPathQuery for path: \(query.path)")
+ logger.info("Query details - path: \(query.path)")
+
+ do {
+ let result = try await client.queryFileByPath(query)
+ if let file = result {
+ logger.info("FileByPathQuery successful - found file: \(file.name)")
+ logger.info("File details - size: \(file.size), extension: \(file.extension ?? "none")")
+ } else {
+ logger.warning("FileByPathQuery successful but no file found for path: \(query.path)")
+ }
+ return result
+ } catch {
+ logger.error("FileByPathQuery failed for path: \(query.path)")
+ logger.error("Error: \(error.localizedDescription)")
+ logger.error("Error details: \(String(describing: error))")
+ throw error
+ }
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/SpacedriveApp.swift b/apps/macos2/Spacedrive/Spacedrive/SpacedriveApp.swift
index 8ea80875b..469de9635 100644
--- a/apps/macos2/Spacedrive/Spacedrive/SpacedriveApp.swift
+++ b/apps/macos2/Spacedrive/Spacedrive/SpacedriveApp.swift
@@ -6,12 +6,146 @@
//
import SwiftUI
+import AppKit
@main
struct SpacedriveApp: App {
+ @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+
var body: some Scene {
- WindowGroup {
- ContentView()
+ // Main companion window
+ WindowGroup("Spacedrive") {
+ JobCompanionView()
+ .withSharedState()
+ .customTitleBar(
+ center: Text("Jamie's Library")
+ .font(.headline)
+ .foregroundColor(.white),
+ right: HStack(spacing: 8) {
+ Button(action: {}) {
+ Image(systemName: "gear")
+ .foregroundColor(.white)
+ }
+ .buttonStyle(PlainButtonStyle())
+
+ Button(action: {}) {
+ Image(systemName: "person.circle")
+ .foregroundColor(.white)
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+ )
+ }
+ .windowStyle(.titleBar)
+ .windowResizability(.contentSize)
+ .defaultSize(width: 400, height: 600)
+
+ // Browser window
+ WindowGroup("Browser") {
+ BrowserView()
+ .withSharedState()
+ .customTitleBar(
+ center: HStack(spacing: 12) {
+ Image(systemName: "folder")
+ .foregroundColor(.white)
+ Text("File Browser")
+ .font(.headline)
+ .foregroundColor(.white)
+ },
+ right: HStack(spacing: 8) {
+ Button(action: {}) {
+ Image(systemName: "magnifyingglass")
+ .foregroundColor(.white)
+ }
+ .buttonStyle(PlainButtonStyle())
+
+ Button(action: {}) {
+ Image(systemName: "list.bullet")
+ .foregroundColor(.white)
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+ )
+ }
+ .windowStyle(.titleBar)
+ .windowResizability(.contentSize)
+ .defaultSize(width: 1200, height: 800)
+
+ // Icon Showcase window
+ WindowGroup("Icon Showcase") {
+ IconShowcaseView()
+ .withSharedState()
+ .nativeTrafficLights()
+ }
+ .windowStyle(.titleBar)
+ .windowResizability(.contentSize)
+ .defaultSize(width: 1000, height: 800)
+
+ // Inspector window
+ WindowGroup("Inspector") {
+ InspectorView()
+ .withSharedState()
+ .nativeTrafficLights()
+ }
+ .windowStyle(.titleBar)
+ .windowResizability(.contentSize)
+ .defaultSize(width: 500, height: 700)
+
+ // Settings window
+ Settings {
+ SettingsView()
+ .withSharedState()
}
}
}
+
+class AppDelegate: NSObject, NSApplicationDelegate {
+ func applicationDidFinishLaunching(_: Notification) {
+ print("Spacedrive for macOS")
+
+ // Configure app as foreground application
+ NSApp.setActivationPolicy(.regular)
+
+ setupMenuBar()
+ initializeAppState()
+ }
+
+ func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
+ return true
+ }
+
+ func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool {
+ // When clicking on dock icon or app, activate and show window
+ NSApp.activate(ignoringOtherApps: true)
+ return true
+ }
+
+ @MainActor
+ private func setupMenuBar() {
+ MenuBarManager.shared.setupMenuBar()
+ }
+
+ @MainActor
+ private func initializeAppState() {
+ // Initialize shared app state
+ SharedAppState.shared.initializeDaemonConnection()
+
+ // Apply development window configuration
+ applyDevWindowConfiguration()
+ }
+
+ @MainActor
+ private func applyDevWindowConfiguration() {
+ // Set the development window configuration
+ SharedAppState.shared.setDevWindowConfiguration(currentDevConfiguration)
+
+ // Open windows based on the configuration
+ if currentDevConfiguration.shouldAutoOpen {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
+ SharedAppState.shared.openWindowsForCurrentDevConfiguration()
+ }
+ }
+
+ print("🔧 Development window configuration set to: \(currentDevConfiguration.displayName)")
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/State/SharedAppState.swift b/apps/macos2/Spacedrive/Spacedrive/State/SharedAppState.swift
new file mode 100644
index 000000000..90ec4c8d5
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/State/SharedAppState.swift
@@ -0,0 +1,426 @@
+import Combine
+import SwiftUI
+import SpacedriveClient
+
+/// Shared Application State - Global state management across all windows
+/// Similar to Redux store or React Context, but with Combine/ObservableObject
+@MainActor
+class SharedAppState: ObservableObject {
+ static let shared = SharedAppState()
+
+ // MARK: - Connection State
+
+ @Published var connectionStatus: ConnectionStatus = .disconnected
+ @Published var daemonConnector: DaemonConnector?
+
+ // MARK: - User Preferences
+
+ @Published var userPreferences = UserPreferences()
+
+ // MARK: - Jobs State
+
+ @Published var globalJobs: [JobInfo] = []
+ @Published var jobsLastUpdated: Date = .init()
+
+ // MARK: - Core Status
+
+ @Published var coreStatus: CoreStatus?
+
+ // MARK: - Library State
+
+ @Published var currentLibrary: LibraryInfo?
+ @Published var availableLibraries: [LibraryInfo] = []
+ @Published var currentLibraryId: String?
+
+ // MARK: - UI State
+
+ @Published var theme: SpacedriveTheme = .dark
+ @Published var sidebarCollapsed: Bool = false
+
+ private var cancellables = Set()
+
+ private init() {
+ setupConnections()
+ loadUserPreferences()
+ }
+
+ // MARK: - Connection Management
+
+ func initializeDaemonConnection() {
+ if daemonConnector == nil {
+ daemonConnector = DaemonConnector()
+
+ // Subscribe to daemon connector state
+ daemonConnector?.$connectionStatus
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] status in
+ self?.connectionStatus = status
+ // Update menu bar status
+ MenuBarManager.shared.updateDaemonMenuStatus(status)
+ }
+ .store(in: &cancellables)
+
+ daemonConnector?.$jobs
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] jobs in
+ self?.globalJobs = jobs
+ self?.jobsLastUpdated = Date()
+ }
+ .store(in: &cancellables)
+
+ daemonConnector?.$availableLibraries
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] libraries in
+ self?.availableLibraries = libraries
+ // Update currentLibrary if we have a currentLibraryId but currentLibrary is nil
+ if let currentLibraryId = self?.currentLibraryId,
+ self?.currentLibrary == nil,
+ let library = libraries.first(where: { $0.id == currentLibraryId })
+ {
+ self?.currentLibrary = library
+ }
+ }
+ .store(in: &cancellables)
+
+ daemonConnector?.$currentLibraryId
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] libraryId in
+ self?.currentLibraryId = libraryId
+ if let libraryId = libraryId {
+ self?.currentLibrary = self?.availableLibraries.first { $0.id == libraryId }
+ } else {
+ self?.currentLibrary = nil
+ }
+ }
+ .store(in: &cancellables)
+
+ daemonConnector?.$coreStatus
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] coreStatus in
+ print("🔍 SharedAppState received coreStatus update: \(coreStatus != nil ? "loaded" : "nil")")
+ if let status = coreStatus {
+ print("🔍 Core status services: location_watcher=\(status.services.locationWatcher.running), networking=\(status.services.networking.running), volume_monitor=\(status.services.volumeMonitor.running), file_sharing=\(status.services.fileSharing.running)")
+ }
+ self?.coreStatus = coreStatus
+ }
+ .store(in: &cancellables)
+
+ daemonConnector?.connect()
+ }
+ }
+
+ func disconnectDaemon() {
+ daemonConnector?.disconnect()
+ connectionStatus = .disconnected
+ }
+
+ // MARK: - Library Management
+
+ func selectLibrary(_ library: LibraryInfo) {
+ currentLibrary = library
+ currentLibraryId = library.id
+ userPreferences.lastSelectedLibraryId = library.id
+ saveUserPreferences()
+
+ // Switch library in the daemon connector
+ Task {
+ await daemonConnector?.switchToLibrary(library)
+ }
+ }
+
+ // MARK: - Preferences Management
+
+ private func loadUserPreferences() {
+ // Load from UserDefaults
+ if let data = UserDefaults.standard.data(forKey: "SpacedriveUserPreferences"),
+ let preferences = try? JSONDecoder().decode(UserPreferences.self, from: data)
+ {
+ userPreferences = preferences
+ theme = preferences.theme
+ sidebarCollapsed = preferences.sidebarCollapsed
+ }
+ }
+
+ func saveUserPreferences() {
+ if let data = try? JSONEncoder().encode(userPreferences) {
+ UserDefaults.standard.set(data, forKey: "SpacedriveUserPreferences")
+ }
+ }
+
+ func updatePreference(_ keyPath: WritableKeyPath, to value: T) {
+ userPreferences[keyPath: keyPath] = value
+
+ // Update published properties if needed
+ if keyPath == \.theme {
+ theme = userPreferences.theme
+ } else if keyPath == \.sidebarCollapsed {
+ sidebarCollapsed = userPreferences.sidebarCollapsed
+ }
+
+ saveUserPreferences()
+ }
+
+ // MARK: - Development Window Configuration
+
+ func setDevWindowConfiguration(_ config: DevWindowConfiguration) {
+ updatePreference(\.devWindowConfiguration, to: config)
+ }
+
+ func openWindowsForCurrentDevConfiguration() {
+ let config = userPreferences.devWindowConfiguration
+ print("🔧 Dev configuration set to: \(config.displayName)")
+
+ // Open windows using SwiftUI's window management
+ DispatchQueue.main.async {
+ switch config {
+ case .browserOnly, .development:
+ // Open browser window
+ if let browserWindow = NSApp.windows.first(where: { $0.title == "Browser" }) {
+ browserWindow.makeKeyAndOrderFront(nil)
+ } else {
+ // Create new browser window
+ let window = NSWindow(
+ contentRect: NSRect(x: 0, y: 0, width: 1200, height: 800),
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
+ backing: .buffered,
+ defer: false
+ )
+ window.title = "Browser"
+ window.contentView = NSHostingView(rootView: BrowserView().withSharedState())
+ window.center()
+ window.makeKeyAndOrderFront(nil)
+ }
+
+ if config == .development {
+ // Also open inspector for development mode
+ if let inspectorWindow = NSApp.windows.first(where: { $0.title == "Inspector" }) {
+ inspectorWindow.makeKeyAndOrderFront(nil)
+ } else {
+ let window = NSWindow(
+ contentRect: NSRect(x: 0, y: 0, width: 500, height: 700),
+ styleMask: [.titled, .closable, .miniaturizable, .resizable],
+ backing: .buffered,
+ defer: false
+ )
+ window.title = "Inspector"
+ window.contentView = NSHostingView(rootView: InspectorView().withSharedState())
+ window.center()
+ window.makeKeyAndOrderFront(nil)
+ }
+ }
+
+ case .companionOnly, .default:
+ // Main companion window is already open
+ if let companionWindow = NSApp.windows.first(where: { $0.title == "Spacedrive" }) {
+ companionWindow.makeKeyAndOrderFront(nil)
+ }
+
+ case .allWindows:
+ // Open all windows
+ self.openWindowsForCurrentDevConfiguration() // Recursive call for browser + inspector
+ // Add other windows as needed
+
+ case .compact:
+ // Compact mode - just companion
+ if let companionWindow = NSApp.windows.first(where: { $0.title == "Spacedrive" }) {
+ companionWindow.makeKeyAndOrderFront(nil)
+ }
+ }
+ }
+ }
+
+ func getDevWindowConfiguration() -> DevWindowConfiguration {
+ return userPreferences.devWindowConfiguration
+ }
+
+ func closeAllWindows() {
+ NSApp.windows.forEach { $0.close() }
+ }
+
+ // MARK: - Action Dispatchers (Redux-like)
+
+ func dispatch(_ action: AppAction) {
+ switch action {
+ case .connectToDaemon:
+ initializeDaemonConnection()
+
+ case .disconnectFromDaemon:
+ disconnectDaemon()
+
+ case let .selectLibrary(library):
+ selectLibrary(library)
+
+ case let .switchToLibrary(library):
+ selectLibrary(library)
+
+ case let .updateTheme(newTheme):
+ updatePreference(\.theme, to: newTheme)
+
+ case .toggleSidebar:
+ updatePreference(\.sidebarCollapsed, to: !sidebarCollapsed)
+
+ case .refreshJobs:
+ daemonConnector?.reconnect()
+
+ case let .pauseJob(jobId):
+ daemonConnector?.pauseJob(jobId)
+
+ case let .resumeJob(jobId):
+ daemonConnector?.resumeJob(jobId)
+
+ case let .setDevWindowConfiguration(config):
+ setDevWindowConfiguration(config)
+
+ case .openDevWindows:
+ openWindowsForCurrentDevConfiguration()
+
+ case .closeAllWindows:
+ closeAllWindows()
+ }
+ }
+
+ private func setupConnections() {
+ // Auto-connect on app launch
+ initializeDaemonConnection()
+ }
+
+ // MARK: - SwiftUI Window Management
+ // Note: SwiftUI handles window management automatically via WindowGroup
+}
+
+// MARK: - Actions (Redux-like action system)
+
+enum AppAction {
+ case connectToDaemon
+ case disconnectFromDaemon
+ case selectLibrary(LibraryInfo)
+ case switchToLibrary(LibraryInfo)
+ case updateTheme(SpacedriveTheme)
+ case toggleSidebar
+ case refreshJobs
+ case pauseJob(String)
+ case resumeJob(String)
+ case setDevWindowConfiguration(DevWindowConfiguration)
+ case openDevWindows
+ case closeAllWindows
+}
+
+// MARK: - User Preferences
+
+struct UserPreferences: Codable {
+ var theme: SpacedriveTheme = .dark
+ var lastSelectedLibraryId: String?
+ var sidebarCollapsed: Bool = false
+ var windowPositions: [String: WindowPosition] = [:]
+ var autoConnectToDaemon: Bool = true
+ var showJobNotifications: Bool = true
+ var compactMode: Bool = false
+
+ // Development preferences
+ var devWindowConfiguration: DevWindowConfiguration = .default
+ var devShowAllWindows: Bool = false
+ var devAutoOpenBrowser: Bool = false
+}
+
+struct WindowPosition: Codable {
+ let x: Double
+ let y: Double
+ let width: Double
+ let height: Double
+}
+
+// MARK: - Development Window Configuration
+
+enum DevWindowConfiguration: String, CaseIterable, Codable {
+ case `default` = "default"
+ case browserOnly = "browser_only"
+ case companionOnly = "companion_only"
+ case allWindows = "all_windows"
+ case compact = "compact"
+ case development = "development"
+
+ var displayName: String {
+ switch self {
+ case .default:
+ return "Default (Companion + Browser)"
+ case .browserOnly:
+ return "Browser Only"
+ case .companionOnly:
+ return "Companion Only"
+ case .allWindows:
+ return "All Windows"
+ case .compact:
+ return "Compact Mode"
+ case .development:
+ return "Development Mode"
+ }
+ }
+
+ var description: String {
+ switch self {
+ case .default:
+ return "Opens companion window and browser window"
+ case .browserOnly:
+ return "Opens only the browser window"
+ case .companionOnly:
+ return "Opens only the companion window"
+ case .allWindows:
+ return "Opens all available windows"
+ case .compact:
+ return "Opens companion with compact settings"
+ case .development:
+ return "Opens browser + inspector for development"
+ }
+ }
+
+ var shouldAutoOpen: Bool {
+ switch self {
+ case .browserOnly, .development:
+ return true
+ default:
+ return false
+ }
+ }
+}
+
+// MARK: - Library Info
+
+struct LibraryInfo: Codable, Identifiable {
+ let id: String
+ let name: String
+ let path: String
+ let isDefault: Bool
+ let stats: LibraryStatistics?
+
+ init(id: String, name: String, path: String, isDefault: Bool = false, stats: LibraryStatistics? = nil) {
+ self.id = id
+ self.name = name
+ self.path = path
+ self.isDefault = isDefault
+ self.stats = stats
+ }
+}
+
+// SpacedriveTheme is already Codable in its declaration
+
+// MARK: - View Modifiers for Shared State
+
+struct WithSharedState: View {
+ let content: Content
+
+ init(@ViewBuilder content: () -> Content) {
+ self.content = content()
+ }
+
+ var body: some View {
+ content
+ .environmentObject(SharedAppState.shared)
+ .environment(\.spacedriveTheme, SharedAppState.shared.theme)
+ }
+}
+
+extension View {
+ func withSharedState() -> some View {
+ WithSharedState { self }
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/ContentArea.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/ContentArea.swift
new file mode 100644
index 000000000..e8b24028a
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/ContentArea.swift
@@ -0,0 +1,159 @@
+import SwiftUI
+import UniformTypeIdentifiers
+
+struct ContentArea: View {
+ @ObservedObject var browserState: BrowserState
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // Content header
+ HStack {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(browserState.selectedLocation?.name ?? "Select a location")
+ .font(.system(size: 16, weight: .semibold))
+ .foregroundColor(SpacedriveColors.Text.primary)
+
+ Text(browserState.currentPath)
+ .font(.system(size: 12))
+ .foregroundColor(SpacedriveColors.Text.tertiary)
+ }
+
+ Spacer()
+
+ // Content actions
+ HStack(spacing: 8) {
+ Button(action: {}) {
+ Image(systemName: "list.bullet")
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ }
+ .buttonStyle(PlainButtonStyle())
+
+ Button(action: {}) {
+ Image(systemName: "grid")
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ }
+ .buttonStyle(PlainButtonStyle())
+
+ Button(action: {}) {
+ Image(systemName: "slider.horizontal.3")
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ }
+ .buttonStyle(PlainButtonStyle())
+
+ // Liquid Glass Button
+ LiquidGlassButton(
+ action: {
+ print("Liquid Glass button tapped!")
+ },
+ icon: "sparkles",
+ title: "Glass"
+ )
+ }
+ }
+ .padding(.horizontal, 20)
+ .padding(.vertical, 16)
+
+ Divider()
+ .background(SpacedriveColors.Border.primary)
+
+ // Main content area
+ if browserState.selectedLocation != nil {
+ ScrollView {
+ LazyVGrid(columns: Array(repeating: GridItem(.adaptive(minimum: 120), spacing: 16), count: 4), spacing: 16) {
+ ForEach(0..<20, id: \.self) { index in
+ ContentItemView(
+ name: "Item \(index + 1)",
+ isDirectory: index % 3 == 0
+ )
+ }
+ }
+ .padding(20)
+ }
+ } else {
+ // Empty state
+ VStack(spacing: 16) {
+ Image(systemName: "folder")
+ .font(.system(size: 48))
+ .foregroundColor(SpacedriveColors.Text.tertiary)
+
+ Text("Select a location to browse")
+ .font(.system(size: 16, weight: .medium))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+
+ Text("Choose a location from the sidebar to view its contents")
+ .font(.system(size: 14))
+ .foregroundColor(SpacedriveColors.Text.tertiary)
+ .multilineTextAlignment(.center)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+
+ Spacer()
+ }
+ .background(SpacedriveColors.Background.primary)
+ .clipShape(UnevenRoundedRectangle(
+ topLeadingRadius: 13, // 8% increase from 12
+ bottomLeadingRadius: 13,
+ bottomTrailingRadius: 0,
+ topTrailingRadius: 0
+ ))
+ .onDrop(of: [.fileURL], isTargeted: nil) { providers in
+ handleFileDrop(providers)
+ }
+ }
+
+ private func handleFileDrop(_ providers: [NSItemProvider]) -> Bool {
+ guard let provider = providers.first else { return false }
+
+ provider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { item, error in
+ guard let data = item as? Data,
+ let url = URL(dataRepresentation: data, relativeTo: nil) else {
+ return
+ }
+
+ let path = url.path
+ let fileExtension = url.pathExtension.lowercased()
+
+ // Check if it's an image file
+ let imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "heic", "webp"]
+ if imageExtensions.contains(fileExtension) {
+ DispatchQueue.main.async {
+ browserState.showImagePreview(path: path)
+ }
+ }
+ }
+
+ return true
+ }
+}
+
+struct ContentItemView: View {
+ let name: String
+ let isDirectory: Bool
+
+ var body: some View {
+ VStack(spacing: 8) {
+ Image(systemName: isDirectory ? "folder.fill" : "doc.fill")
+ .font(.system(size: 32))
+ .foregroundColor(isDirectory ? SpacedriveColors.Accent.primary : SpacedriveColors.Text.secondary)
+
+ Text(name)
+ .font(.system(size: 12, weight: .medium))
+ .foregroundColor(SpacedriveColors.Text.primary)
+ .lineLimit(2)
+ .multilineTextAlignment(.center)
+ }
+ .frame(width: 80, height: 80)
+ .background(SpacedriveColors.Background.secondary)
+ .clipShape(UnevenRoundedRectangle(
+ topLeadingRadius: 9, // 8% increase from 8
+ bottomLeadingRadius: 9,
+ bottomTrailingRadius: 9,
+ topTrailingRadius: 9
+ ))
+ .onTapGesture {
+ print("Tapped \(name)")
+ }
+ }
+}
+
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/ImagePreviewView.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/ImagePreviewView.swift
new file mode 100644
index 000000000..433675413
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/ImagePreviewView.swift
@@ -0,0 +1,143 @@
+import SwiftUI
+import AppKit
+
+/// Image preview view with zoom and pan capabilities
+struct ImagePreviewView: View {
+ let imagePath: String
+ let onClose: () -> Void
+
+ @State private var scale: CGFloat = 1.0
+ @State private var lastScale: CGFloat = 1.0
+ @State private var offset: CGSize = .zero
+ @State private var lastOffset: CGSize = .zero
+ @State private var image: NSImage?
+ @State private var imageSize: CGSize = .zero
+
+ // Sidebar width for initial positioning
+ private let sidebarWidth: CGFloat = 200
+
+ var body: some View {
+ ZStack {
+ // Background
+ Color.black
+ .ignoresSafeArea()
+
+ if let image = image {
+ // Image with zoom and pan
+ Image(nsImage: image)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .scaleEffect(scale)
+ .offset(offset)
+ .gesture(
+ SimultaneousGesture(
+ // Pinch to zoom
+ MagnificationGesture()
+ .onChanged { value in
+ let delta = value / lastScale
+ lastScale = value
+ scale *= delta
+ }
+ .onEnded { _ in
+ lastScale = 1.0
+ // Constrain scale
+ scale = max(0.5, min(scale, 5.0))
+ },
+
+ // Drag to pan
+ DragGesture()
+ .onChanged { value in
+ offset = CGSize(
+ width: lastOffset.width + value.translation.width,
+ height: lastOffset.height + value.translation.height
+ )
+ }
+ .onEnded { _ in
+ lastOffset = offset
+ }
+ )
+ )
+ .onTapGesture(count: 2) {
+ // Double tap to reset zoom
+ withAnimation(.easeInOut(duration: 0.3)) {
+ scale = 1.0
+ offset = .zero
+ lastOffset = .zero
+ }
+ }
+ } else {
+ // Loading state
+ VStack(spacing: 16) {
+ ProgressView()
+ .progressViewStyle(CircularProgressViewStyle(tint: .white))
+ .scaleEffect(1.5)
+
+ Text("Loading image...")
+ .foregroundColor(.white)
+ .font(.system(size: 16))
+ }
+ }
+
+ // Close button
+ VStack {
+ HStack {
+ Spacer()
+ Button(action: onClose) {
+ Image(systemName: "xmark.circle.fill")
+ .font(.system(size: 24))
+ .foregroundColor(.white.opacity(0.8))
+ .background(Color.black.opacity(0.3))
+ .clipShape(Circle())
+ }
+ .buttonStyle(PlainButtonStyle())
+ .padding(.top, 20)
+ .padding(.trailing, 20)
+ }
+ Spacer()
+ }
+ }
+ .onAppear {
+ loadImage()
+ // Initial positioning to compensate for sidebar
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ adjustInitialPosition()
+ }
+ }
+ }
+
+ private func loadImage() {
+ guard let nsImage = NSImage(contentsOfFile: imagePath) else {
+ print("Failed to load image from: \(imagePath)")
+ return
+ }
+
+ self.image = nsImage
+ self.imageSize = nsImage.size
+ }
+
+ private func adjustInitialPosition() {
+ // Center the image in the available space (accounting for sidebar)
+ // This ensures the image appears centered in the content area initially
+ let availableWidth = NSScreen.main?.frame.width ?? 1200
+
+ // Calculate the offset needed to center the image in the content area
+ // (not the full screen, but the area to the right of the sidebar)
+ let contentAreaWidth = availableWidth - sidebarWidth
+ let contentAreaCenterX = (contentAreaWidth / 2) + sidebarWidth
+
+ // Center horizontally in the content area
+ offset = CGSize(
+ width: contentAreaCenterX - (availableWidth / 2),
+ height: 0
+ )
+ lastOffset = offset
+ }
+}
+
+#Preview {
+ ImagePreviewView(
+ imagePath: "/System/Library/Desktop Pictures/Monterey.heic",
+ onClose: {}
+ )
+ .frame(width: 1200, height: 800)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/LiquidGlassButton.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/LiquidGlassButton.swift
new file mode 100644
index 000000000..edfce73e8
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/LiquidGlassButton.swift
@@ -0,0 +1,67 @@
+import SwiftUI
+
+/// A button with liquid glass effect styling
+struct LiquidGlassButton: View {
+ let action: () -> Void
+ let icon: String
+ let title: String?
+
+ @State private var isPressed = false
+
+ init(action: @escaping () -> Void, icon: String, title: String? = nil) {
+ self.action = action
+ self.icon = icon
+ self.title = title
+ }
+
+ var body: some View {
+ Button(action: action) {
+ HStack(spacing: 6) {
+ Image(systemName: icon)
+ .font(.system(size: 14, weight: .medium))
+
+ if let title = title {
+ Text(title)
+ .font(.system(size: 12, weight: .medium))
+ }
+ }
+ .foregroundColor(.white)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+ .background(
+ UnevenRoundedRectangle(
+ topLeadingRadius: 9, // 8% increase from 8
+ bottomLeadingRadius: 9,
+ bottomTrailingRadius: 9,
+ topTrailingRadius: 9
+ )
+ .fill(.ultraThinMaterial)
+ .overlay(
+ UnevenRoundedRectangle(
+ topLeadingRadius: 9,
+ bottomLeadingRadius: 9,
+ bottomTrailingRadius: 9,
+ topTrailingRadius: 9
+ )
+ .stroke(.white.opacity(0.2), lineWidth: 1)
+ )
+ )
+ .scaleEffect(isPressed ? 0.95 : 1.0)
+ .animation(.easeInOut(duration: 0.1), value: isPressed)
+ }
+ .buttonStyle(PlainButtonStyle())
+ .onLongPressGesture(minimumDuration: 0, maximumDistance: .infinity, pressing: { pressing in
+ isPressed = pressing
+ }, perform: {})
+ }
+}
+
+#Preview {
+ HStack(spacing: 12) {
+ LiquidGlassButton(action: {}, icon: "sparkles", title: "Liquid Glass")
+ LiquidGlassButton(action: {}, icon: "wand.and.stars")
+ LiquidGlassButton(action: {}, icon: "crystal.ball", title: "Magic")
+ }
+ .padding()
+ .background(Color.black)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/LiquidGlassSidebar.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/LiquidGlassSidebar.swift
new file mode 100644
index 000000000..22c8c4512
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/LiquidGlassSidebar.swift
@@ -0,0 +1,134 @@
+import SwiftUI
+
+/// Liquid glass version of the Tahoe sidebar
+struct LiquidGlassSidebar: View {
+ @ObservedObject var browserState: BrowserState
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // Sidebar header
+ HStack {
+ Text("Locations")
+ .font(.system(size: 12, weight: .semibold))
+ .foregroundColor(.white.opacity(0.9))
+ .textCase(.uppercase)
+ .tracking(0.5)
+
+ Spacer()
+
+ Button(action: {
+ withAnimation(.easeInOut(duration: 0.2)) {
+ browserState.sidebarCollapsed.toggle()
+ }
+ }) {
+ Image(systemName: browserState.sidebarCollapsed ? "chevron.right" : "chevron.left")
+ .font(.system(size: 10, weight: .medium))
+ .foregroundColor(.white.opacity(0.8))
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+ .padding(.horizontal, 16)
+ .padding(.vertical, 12)
+
+ if !browserState.sidebarCollapsed {
+ // Location buttons
+ VStack(spacing: 2) {
+ ForEach(browserState.locations) { location in
+ LiquidGlassSidebarLocationButton(
+ location: location,
+ isSelected: browserState.selectedLocation?.id == location.id
+ ) {
+ browserState.selectLocation(location)
+ }
+ }
+ }
+ .padding(.horizontal, 12)
+ .padding(.bottom, 12)
+ }
+
+ Spacer()
+ }
+ .frame(width: browserState.sidebarCollapsed ? 60 : 200)
+ .background(.ultraThinMaterial)
+ .overlay(
+ UnevenRoundedRectangle(
+ topLeadingRadius: 0,
+ bottomLeadingRadius: 0,
+ bottomTrailingRadius: 13, // 8% increase from 12
+ topTrailingRadius: 13
+ )
+ .stroke(.white.opacity(0.2), lineWidth: 1)
+ )
+ .clipShape(UnevenRoundedRectangle(
+ topLeadingRadius: 0,
+ bottomLeadingRadius: 0,
+ bottomTrailingRadius: 13,
+ topTrailingRadius: 13
+ ))
+ .shadow(color: Color.black.opacity(0.1), radius: 8, x: 2, y: 0)
+ }
+}
+
+/// Liquid glass version of the sidebar location button
+struct LiquidGlassSidebarLocationButton: View {
+ let location: BrowserLocation
+ let isSelected: Bool
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ HStack(spacing: 8) {
+ Image(systemName: location.iconName)
+ .font(.system(size: 14, weight: .medium))
+ .foregroundColor(.white.opacity(0.9))
+ .frame(width: 16)
+
+ if !isSelected {
+ Text(location.name)
+ .font(.system(size: 13, weight: .medium))
+ .foregroundColor(.white.opacity(0.8))
+ .lineLimit(1)
+ }
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+ .background(
+ UnevenRoundedRectangle(
+ topLeadingRadius: 9, // 8% increase from 8
+ bottomLeadingRadius: 9,
+ bottomTrailingRadius: 9,
+ topTrailingRadius: 9
+ )
+ .fill(isSelected ? .white.opacity(0.2) : .clear)
+ .overlay(
+ UnevenRoundedRectangle(
+ topLeadingRadius: 9,
+ bottomLeadingRadius: 9,
+ bottomTrailingRadius: 9,
+ topTrailingRadius: 9
+ )
+ .stroke(.white.opacity(0.1), lineWidth: 1)
+ )
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+
+#Preview {
+ ZStack {
+ // Background to show the glass effect
+ LinearGradient(
+ colors: [.blue, .purple, .pink],
+ startPoint: .topLeading,
+ endPoint: .bottomTrailing
+ )
+ .ignoresSafeArea()
+
+ HStack {
+ LiquidGlassSidebar(browserState: BrowserState())
+ Spacer()
+ }
+ }
+ .frame(width: 1200, height: 800)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/SidebarLocationButton.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/SidebarLocationButton.swift
new file mode 100644
index 000000000..5e3305cc4
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/SidebarLocationButton.swift
@@ -0,0 +1,54 @@
+import SwiftUI
+
+struct SidebarLocationButton: View {
+ let location: BrowserLocation
+ let isSelected: Bool
+ let onTap: () -> Void
+
+ var body: some View {
+ Button(action: onTap) {
+ HStack(spacing: 8) {
+ Image(systemName: location.iconName)
+ .foregroundColor(iconColor)
+ .frame(width: 16, height: 16)
+
+ Text(location.name)
+ .foregroundColor(textColor)
+ .font(.system(size: 13, weight: .medium))
+
+ Spacer()
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 6)
+ .background(backgroundColor)
+ .cornerRadius(6)
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+
+ private var backgroundColor: Color {
+ if isSelected {
+ return SpacedriveColors.Interactive.selected.opacity(0.3)
+ } else {
+ return Color.clear
+ }
+ }
+
+ private var textColor: Color {
+ if isSelected {
+ return SpacedriveColors.Text.primary
+ } else {
+ return SpacedriveColors.Text.secondary
+ }
+ }
+
+ private var iconColor: Color {
+ if isSelected {
+ return SpacedriveColors.Accent.primary
+ } else {
+ return SpacedriveColors.Text.tertiary
+ }
+ }
+}
+
+
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/TahoeSidebar.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/TahoeSidebar.swift
new file mode 100644
index 000000000..01a341143
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Components/TahoeSidebar.swift
@@ -0,0 +1,65 @@
+import SwiftUI
+
+struct TahoeSidebar: View {
+ @ObservedObject var browserState: BrowserState
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // Sidebar header
+ HStack {
+ Text("Locations")
+ .font(.system(size: 12, weight: .semibold))
+ .foregroundColor(SpacedriveColors.Text.tertiary)
+ .textCase(.uppercase)
+ .tracking(0.5)
+
+ Spacer()
+
+ Button(action: {
+ withAnimation(.easeInOut(duration: 0.2)) {
+ browserState.sidebarCollapsed.toggle()
+ }
+ }) {
+ Image(systemName: browserState.sidebarCollapsed ? "chevron.right" : "chevron.left")
+ .font(.system(size: 10, weight: .medium))
+ .foregroundColor(SpacedriveColors.Text.tertiary)
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+ .padding(.horizontal, 16)
+ .padding(.vertical, 12)
+
+ if !browserState.sidebarCollapsed {
+ // Location buttons
+ VStack(spacing: 2) {
+ ForEach(browserState.locations) { location in
+ SidebarLocationButton(
+ location: location,
+ isSelected: browserState.selectedLocation?.id == location.id
+ ) {
+ browserState.selectLocation(location)
+ }
+ }
+ }
+ .padding(.horizontal, 12)
+ .padding(.bottom, 12)
+ }
+
+ Spacer()
+ }
+ .frame(width: browserState.sidebarCollapsed ? 60 : 200)
+ .background(SpacedriveColors.Background.secondary)
+ .clipShape(UnevenRoundedRectangle(
+ topLeadingRadius: 0,
+ bottomLeadingRadius: 0,
+ bottomTrailingRadius: 13, // 8% increase from 12
+ topTrailingRadius: 13
+ ))
+ .shadow(color: Color.black.opacity(0.1), radius: 8, x: 2, y: 0)
+ }
+}
+
+#Preview {
+ TahoeSidebar(browserState: BrowserState())
+ .frame(width: 200, height: 600)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Models/BrowserModels.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Models/BrowserModels.swift
new file mode 100644
index 000000000..ab92d7967
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Models/BrowserModels.swift
@@ -0,0 +1,143 @@
+import Foundation
+import SwiftUI
+import Combine
+
+/// Represents a location/folder in the browser
+struct BrowserLocation: Identifiable, Hashable {
+ let id = UUID()
+ let name: String
+ let path: String
+ let iconName: String
+ let isDirectory: Bool
+
+ init(name: String, path: String, iconName: String, isDirectory: Bool = true) {
+ self.name = name
+ self.path = path
+ self.iconName = iconName
+ self.isDirectory = isDirectory
+ }
+
+ // Computed property for SwiftUI compatibility
+ var icon: String { iconName }
+}
+
+/// Represents a file or folder item in the browser
+struct BrowserItem: Identifiable, Hashable {
+ let id = UUID()
+ let name: String
+ let path: String
+ let icon: String
+ let type: String
+ let size: String
+ let modifiedDate: String
+ let isDirectory: Bool
+}
+
+/// Browser state management
+@MainActor
+class BrowserState: ObservableObject {
+ @Published var selectedLocation: BrowserLocation?
+ @Published var currentPath: String = "/"
+ @Published var locations: [BrowserLocation] = []
+ @Published var sidebarCollapsed: Bool = false
+ @Published var hasNewNotifications: Bool = false
+
+ // Image preview state
+ @Published var previewImagePath: String?
+ @Published var isShowingImagePreview: Bool = false
+
+ // Computed properties for the new UI
+ @Published var currentItems: [BrowserItem] = []
+ @Published var recentItems: [BrowserItem] = []
+
+ init() {
+ setupDefaultLocations()
+ setupSampleData()
+ }
+
+ private func setupDefaultLocations() {
+ locations = [
+ BrowserLocation(name: "Home", path: "/Users/jamespine", iconName: "house.fill"),
+ BrowserLocation(name: "Desktop", path: "/Users/jamespine/Desktop", iconName: "desktopcomputer"),
+ BrowserLocation(name: "Documents", path: "/Users/jamespine/Documents", iconName: "doc.fill"),
+ BrowserLocation(name: "Downloads", path: "/Users/jamespine/Downloads", iconName: "arrow.down.circle.fill"),
+ BrowserLocation(name: "Pictures", path: "/Users/jamespine/Pictures", iconName: "photo.fill"),
+ BrowserLocation(name: "Music", path: "/Users/jamespine/Music", iconName: "music.note"),
+ BrowserLocation(name: "Movies", path: "/Users/jamespine/Movies", iconName: "video.fill"),
+ BrowserLocation(name: "Applications", path: "/Applications", iconName: "app.fill"),
+ BrowserLocation(name: "Library", path: "/Users/jamespine/Library", iconName: "folder.fill"),
+ BrowserLocation(name: "System", path: "/System", iconName: "gear.circle.fill"),
+ BrowserLocation(name: "Volumes", path: "/Volumes", iconName: "externaldrive.fill")
+ ]
+
+ selectedLocation = locations.first
+ }
+
+ private func setupSampleData() {
+ // Sample current items
+ currentItems = [
+ BrowserItem(name: "Project Files", path: "/Users/jamespine/Desktop/Project Files", icon: "folder", type: "Folder", size: "2.3 GB", modifiedDate: "Today", isDirectory: true),
+ BrowserItem(name: "Screenshot.png", path: "/Users/jamespine/Desktop/Screenshot.png", icon: "photo", type: "PNG Image", size: "1.2 MB", modifiedDate: "Yesterday", isDirectory: false),
+ BrowserItem(name: "Document.pdf", path: "/Users/jamespine/Desktop/Document.pdf", icon: "doc.text", type: "PDF Document", size: "856 KB", modifiedDate: "2 days ago", isDirectory: false),
+ BrowserItem(name: "Video.mp4", path: "/Users/jamespine/Desktop/Video.mp4", icon: "video", type: "MP4 Video", size: "45.2 MB", modifiedDate: "3 days ago", isDirectory: false),
+ BrowserItem(name: "Archive.zip", path: "/Users/jamespine/Desktop/Archive.zip", icon: "archivebox", type: "ZIP Archive", size: "12.8 MB", modifiedDate: "1 week ago", isDirectory: false),
+ BrowserItem(name: "Code Project", path: "/Users/jamespine/Desktop/Code Project", icon: "folder.fill", type: "Folder", size: "156 MB", modifiedDate: "1 week ago", isDirectory: true)
+ ]
+
+ // Sample recent items
+ recentItems = [
+ BrowserItem(name: "Recent Document.docx", path: "/Users/jamespine/Documents/Recent Document.docx", icon: "doc.text.fill", type: "Word Document", size: "234 KB", modifiedDate: "1 hour ago", isDirectory: false),
+ BrowserItem(name: "Presentation.key", path: "/Users/jamespine/Documents/Presentation.key", icon: "presentation", type: "Keynote Presentation", size: "8.7 MB", modifiedDate: "3 hours ago", isDirectory: false),
+ BrowserItem(name: "Spreadsheet.xlsx", path: "/Users/jamespine/Documents/Spreadsheet.xlsx", icon: "tablecells", type: "Excel Spreadsheet", size: "1.1 MB", modifiedDate: "5 hours ago", isDirectory: false)
+ ]
+
+ // Simulate notifications
+ Task {
+ try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
+ hasNewNotifications = true
+ }
+ }
+
+ func selectLocation(_ location: BrowserLocation) {
+ selectedLocation = location
+ currentPath = location.path
+ // In a real app, this would load the actual files from the location
+ loadItemsForLocation(location)
+ }
+
+ private func loadItemsForLocation(_ location: BrowserLocation) {
+ // Simulate loading different items for different locations
+ switch location.name {
+ case "Pictures":
+ currentItems = [
+ BrowserItem(name: "Vacation Photos", path: "\(location.path)/Vacation Photos", icon: "photo.on.rectangle", type: "Folder", size: "2.1 GB", modifiedDate: "Today", isDirectory: true),
+ BrowserItem(name: "Screenshot 2024.png", path: "\(location.path)/Screenshot 2024.png", icon: "photo", type: "PNG Image", size: "2.3 MB", modifiedDate: "Yesterday", isDirectory: false),
+ BrowserItem(name: "Family Portrait.jpg", path: "\(location.path)/Family Portrait.jpg", icon: "photo", type: "JPEG Image", size: "4.7 MB", modifiedDate: "2 days ago", isDirectory: false)
+ ]
+ case "Documents":
+ currentItems = [
+ BrowserItem(name: "Work Projects", path: "\(location.path)/Work Projects", icon: "folder", type: "Folder", size: "1.8 GB", modifiedDate: "Today", isDirectory: true),
+ BrowserItem(name: "Meeting Notes.txt", path: "\(location.path)/Meeting Notes.txt", icon: "doc.text", type: "Text Document", size: "12 KB", modifiedDate: "Yesterday", isDirectory: false),
+ BrowserItem(name: "Budget.xlsx", path: "\(location.path)/Budget.xlsx", icon: "tablecells", type: "Excel Spreadsheet", size: "456 KB", modifiedDate: "3 days ago", isDirectory: false)
+ ]
+ default:
+ // Use default sample data
+ break
+ }
+ }
+
+ func toggleSidebar() {
+ sidebarCollapsed.toggle()
+ }
+
+ func showImagePreview(path: String) {
+ previewImagePath = path
+ isShowingImagePreview = true
+ }
+
+ func hideImagePreview() {
+ previewImagePath = nil
+ isShowingImagePreview = false
+ }
+}
+
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Views/BrowserView.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Views/BrowserView.swift
new file mode 100644
index 000000000..abaa37c3e
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Browser/Views/BrowserView.swift
@@ -0,0 +1,314 @@
+import SwiftUI
+
+struct BrowserView: View {
+ @StateObject private var browserState = BrowserState()
+ @State private var searchText = ""
+ @State private var showingInspector = true
+ @State private var selectedItem: BrowserItem?
+
+ var body: some View {
+ NavigationSplitView {
+ // Sidebar
+ SidebarView(browserState: browserState)
+ .navigationSplitViewColumnWidth(min: 200, ideal: 250)
+ } content: {
+ // Main content
+ ContentView(browserState: browserState, selectedItem: $selectedItem)
+ } detail: {
+ // Inspector
+ if showingInspector {
+ InspectorDetailView(selectedItem: selectedItem)
+ .navigationSplitViewColumnWidth(min: 200, ideal: 220)
+ }
+ }
+ .searchable(text: $searchText, prompt: "Search files...")
+ .toolbar {
+ // View controls
+ ToolbarItemGroup(placement: .primaryAction) {
+ Button(action: { browserState.toggleSidebar() }) {
+ Image(systemName: "sidebar.left")
+ }
+ .buttonStyle(.glass)
+
+ Button(action: { showingInspector.toggle() }) {
+ Image(systemName: "sidebar.right")
+ }
+ .buttonStyle(.glass)
+ }
+
+ // Action buttons
+ ToolbarItemGroup(placement: .secondaryAction) {
+ Button("New Folder") {
+ // TODO: Implement new folder
+ }
+ .buttonStyle(.glass)
+
+ Button("Import") {
+ // TODO: Implement import
+ }
+ .buttonStyle(.glass)
+ }
+ }
+ .backgroundExtensionEffect() // Enable content to extend behind translucent elements
+ .onChange(of: selectedItem) { _, newValue in
+ // Handle selection changes
+ }
+ }
+}
+
+// MARK: - Sidebar View
+struct SidebarView: View {
+ @ObservedObject var browserState: BrowserState
+
+ var body: some View {
+ List(selection: $browserState.selectedLocation) {
+ locationsSection
+ recentSection
+ }
+ .listStyle(.sidebar)
+ .navigationTitle("Spacedrive")
+ }
+
+ private var locationsSection: some View {
+ Section("Locations") {
+ ForEach(browserState.locations) { location in
+ FinderSidebarLocationRow(
+ location: location,
+ isSelected: browserState.selectedLocation?.id == location.id
+ ) {
+ browserState.selectLocation(location)
+ }
+ }
+ }
+ }
+
+ private var recentSection: some View {
+ Section("Recent") {
+ ForEach(browserState.recentItems.prefix(5)) { item in
+ Button(action: {
+ // TODO: Handle recent item selection
+ }) {
+ HStack(spacing: 8) {
+ Image(systemName: item.icon)
+ .font(.system(size: 16))
+ .foregroundColor(.secondary)
+ .frame(width: 20)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(item.name)
+ .font(.system(size: 13))
+ .foregroundColor(.primary)
+ .lineLimit(1)
+
+ Text(item.modifiedDate)
+ .font(.system(size: 11))
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+ }
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+ }
+ }
+}
+
+// MARK: - Content View
+struct ContentView: View {
+ @ObservedObject var browserState: BrowserState
+ @Binding var selectedItem: BrowserItem?
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // Breadcrumb navigation
+ HStack {
+ Button(action: {}) {
+ HStack {
+ Image(systemName: "chevron.left")
+ Text("Back")
+ }
+ }
+ .buttonStyle(.glass)
+
+ Spacer()
+
+ Text(browserState.currentPath)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding()
+
+ Divider()
+
+ // Content grid
+ ScrollView {
+ LazyVGrid(columns: Array(repeating: GridItem(.adaptive(minimum: 120)), count: 1)) {
+ ForEach(browserState.currentItems) { item in
+ ContentItemCard(item: item) {
+ selectedItem = item
+ }
+ }
+ }
+ .padding()
+ }
+ }
+ .navigationTitle(browserState.selectedLocation?.name ?? "Browser")
+ }
+}
+
+// MARK: - Content Item Card
+struct ContentItemCard: View {
+ let item: BrowserItem
+ let onTap: () -> Void
+
+ var body: some View {
+ VStack(spacing: 8) {
+ Image(systemName: item.icon)
+ .font(.system(size: 32))
+ .foregroundColor(.primary)
+
+ Text(item.name)
+ .font(.caption)
+ .lineLimit(2)
+ .multilineTextAlignment(.center)
+
+ Text(item.size)
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ .frame(width: 100, height: 100)
+ .background(.ultraThinMaterial)
+ .clipShape(UnevenRoundedRectangle(
+ topLeadingRadius: 9,
+ bottomLeadingRadius: 9,
+ bottomTrailingRadius: 9,
+ topTrailingRadius: 9
+ ))
+ .onTapGesture {
+ onTap()
+ }
+ }
+}
+
+// MARK: - Inspector Detail View
+struct InspectorDetailView: View {
+ let selectedItem: BrowserItem?
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ if let item = selectedItem {
+ // File preview
+ VStack {
+ Image(systemName: item.icon)
+ .font(.system(size: 48))
+ .foregroundColor(.primary)
+
+ Text(item.name)
+ .font(.subheadline)
+ .multilineTextAlignment(.center)
+ .lineLimit(2)
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+ .background(.ultraThinMaterial)
+ .clipShape(UnevenRoundedRectangle(
+ topLeadingRadius: 9,
+ bottomLeadingRadius: 9,
+ bottomTrailingRadius: 9,
+ topTrailingRadius: 9
+ ))
+
+ // File details
+ VStack(alignment: .leading, spacing: 6) {
+ Text("Details")
+ .font(.subheadline)
+ .fontWeight(.semibold)
+
+ BrowserPropertyRow(label: "Type", value: item.type)
+ BrowserPropertyRow(label: "Size", value: item.size)
+ BrowserPropertyRow(label: "Modified", value: item.modifiedDate)
+ BrowserPropertyRow(label: "Path", value: item.path)
+ }
+
+ Spacer()
+ } else {
+ VStack {
+ Image(systemName: "doc")
+ .font(.system(size: 32))
+ .foregroundColor(.secondary)
+
+ Text("No Selection")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+
+ Text("Select a file to view its details")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+ .navigationTitle("Inspector")
+ }
+}
+
+// MARK: - Property Row
+struct BrowserPropertyRow: View {
+ let label: String
+ let value: String
+
+ var body: some View {
+ HStack {
+ Text(label)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ Spacer()
+ Text(value)
+ .font(.caption)
+ .textSelection(.enabled)
+ }
+ }
+}
+
+// MARK: - Finder-Style Sidebar Location Row
+struct FinderSidebarLocationRow: View {
+ let location: BrowserLocation
+ let isSelected: Bool
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ HStack(spacing: 8) {
+ Image(systemName: location.iconName)
+ .font(.system(size: 16))
+ .foregroundColor(.primary)
+ .frame(width: 20)
+
+ Text(location.name)
+ .font(.system(size: 13))
+ .foregroundColor(.primary)
+ .lineLimit(1)
+
+ Spacer()
+ }
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(
+ RoundedRectangle(cornerRadius: 6)
+ .fill(isSelected ? Color.accentColor.opacity(0.15) : Color.clear)
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ }
+}
+
+#Preview {
+ BrowserView()
+ .frame(width: 1200, height: 800)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/IconShowcase/Views/IconShowcaseView.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/IconShowcase/Views/IconShowcaseView.swift
new file mode 100644
index 000000000..64c61826e
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/IconShowcase/Views/IconShowcaseView.swift
@@ -0,0 +1,292 @@
+import SwiftUI
+
+/// Comprehensive icon showcase window for Spacedrive icons
+struct IconShowcaseView: View {
+ @State private var searchText = ""
+ @State private var selectedCategory: IconCategory = .all
+ @State private var selectedSize: IconSize = .default
+ @State private var showLightVariants = false
+ @State private var show20pxVariants = false
+ @EnvironmentObject var appState: SharedAppState
+
+ enum IconCategory: String, CaseIterable {
+ case all = "All Icons"
+ case fileTypes = "File Types"
+ case drives = "Drives & Cloud"
+ case devices = "Devices"
+ case ui = "UI Elements"
+
+ var icons: [SpacedriveIcon] {
+ switch self {
+ case .all:
+ return SpacedriveIcon.allCases
+ case .fileTypes:
+ return SpacedriveIcon.allCases.filter { icon in
+ ["Album", "Alias", "Application", "Archive", "Audio", "Book", "Collection",
+ "Database", "Document", "Encrypted", "Entity", "Executable", "Folder",
+ "Game", "Image", "Key", "Link", "Lock", "Mesh", "Movie", "Package",
+ "Screenshot", "Text", "TexturedMesh", "Trash", "Undefined", "Video", "Widget"]
+ .contains(icon.baseName)
+ }
+ case .drives:
+ return SpacedriveIcon.allCases.filter { icon in
+ icon.rawValue.contains("Drive") ||
+ ["AmazonS3", "BackBlaze", "Box", "DAV", "Dropbox", "GoogleDrive", "Mega",
+ "OneDrive", "OpenStack", "PCloud", "Location", "Sync", "Spacedrop"]
+ .contains(icon.baseName)
+ }
+ case .devices:
+ return SpacedriveIcon.allCases.filter { icon in
+ ["Ball", "Globe", "HDD", "Heart", "Home", "Laptop", "Mobile", "PC",
+ "Server", "Tablet", "Terminal"]
+ .contains(icon.baseName)
+ }
+ case .ui:
+ return SpacedriveIcon.allCases.filter { icon in
+ ["Search", "Tags", "Scrapbook", "Screenshot", "Face", "Entity"]
+ .contains(icon.baseName)
+ }
+ }
+ }
+ }
+
+ enum IconSize: String, CaseIterable {
+ case small = "Small (16px)"
+ case medium = "Medium (24px)"
+ case large = "Large (32px)"
+ case xlarge = "Extra Large (48px)"
+
+ var size: CGFloat {
+ switch self {
+ case .small: return 16
+ case .medium: return 24
+ case .large: return 32
+ case .xlarge: return 48
+ }
+ }
+
+ static let `default` = IconSize.xlarge
+ }
+
+ private var filteredIcons: [SpacedriveIcon] {
+ let categoryIcons = selectedCategory.icons
+
+ let filtered = categoryIcons.filter { icon in
+ // Search filter
+ let matchesSearch = searchText.isEmpty ||
+ icon.displayName.localizedCaseInsensitiveContains(searchText) ||
+ icon.rawValue.localizedCaseInsensitiveContains(searchText)
+
+ // Variant filters - hide variants by default unless explicitly shown
+ let matchesLightFilter = showLightVariants || !icon.isLightVariant
+ let matches20pxFilter = show20pxVariants || !icon.is20pxVariant
+
+ return matchesSearch && matchesLightFilter && matches20pxFilter
+ }
+
+ return filtered.sorted { $0.displayName < $1.displayName }
+ }
+
+ var body: some View {
+ WindowContainer {
+ NavigationView {
+ // Sidebar
+ VStack(alignment: .leading, spacing: 16) {
+ Text("Icon Showcase")
+ .font(.title2)
+ .fontWeight(.bold)
+ .foregroundColor(SpacedriveColors.Text.primary)
+ .padding(.horizontal)
+ .padding(.top, 16)
+
+ // Search
+ HStack {
+ Image(systemName: "magnifyingglass")
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ TextField("Search icons...", text: $searchText)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ }
+ .padding(.horizontal)
+
+ // Category Selection
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Categories")
+ .font(.headline)
+ .foregroundColor(SpacedriveColors.Text.primary)
+ .padding(.horizontal)
+
+ ForEach(IconCategory.allCases, id: \.self) { category in
+ Button(action: {
+ selectedCategory = category
+ }) {
+ HStack {
+ Text(category.rawValue)
+ Spacer()
+ if selectedCategory == category {
+ Image(systemName: "checkmark")
+ .foregroundColor(.accentColor)
+ }
+ }
+ .padding(.horizontal)
+ .padding(.vertical, 4)
+ }
+ .buttonStyle(PlainButtonStyle())
+ .background(selectedCategory == category ? Color.accentColor.opacity(0.1) : Color.clear)
+ .cornerRadius(6)
+ .padding(.horizontal)
+ }
+ }
+
+ Divider()
+
+ // Size Selection
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Icon Size")
+ .font(.headline)
+ .foregroundColor(SpacedriveColors.Text.primary)
+ .padding(.horizontal)
+
+ ForEach(IconSize.allCases, id: \.self) { size in
+ Button(action: {
+ selectedSize = size
+ }) {
+ HStack {
+ Text(size.rawValue)
+ Spacer()
+ if selectedSize == size {
+ Image(systemName: "checkmark")
+ .foregroundColor(.accentColor)
+ }
+ }
+ .padding(.horizontal)
+ .padding(.vertical, 4)
+ }
+ .buttonStyle(PlainButtonStyle())
+ .background(selectedSize == size ? Color.accentColor.opacity(0.1) : Color.clear)
+ .cornerRadius(6)
+ .padding(.horizontal)
+ }
+ }
+
+ Divider()
+
+ // Variant Filters
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Show Variants")
+ .font(.headline)
+ .foregroundColor(SpacedriveColors.Text.primary)
+ .padding(.horizontal)
+
+ Toggle("Light Theme", isOn: $showLightVariants)
+ .padding(.horizontal)
+
+ Toggle("20px Variants", isOn: $show20pxVariants)
+ .padding(.horizontal)
+ }
+
+ Spacer()
+
+ // Stats
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Showing \(filteredIcons.count) icons")
+ .font(.caption)
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ .padding(.horizontal)
+ }
+ }
+ .frame(width: 280)
+ .background(SpacedriveColors.Background.secondary)
+
+ // Main Content
+ ScrollView {
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: 5), spacing: 20) {
+ ForEach(filteredIcons, id: \.self) { icon in
+ IconShowcaseCard(
+ icon: icon,
+ size: selectedSize.size,
+ showLightVariants: showLightVariants,
+ show20pxVariants: show20pxVariants
+ )
+ }
+ }
+ .padding(24)
+ }
+ .background(SpacedriveColors.Background.primary)
+ }
+ .navigationTitle("Spacedrive Icons")
+ .frame(minWidth: 800, minHeight: 600)
+ }
+ }
+}
+
+/// Individual icon card in the showcase
+struct IconShowcaseCard: View {
+ let icon: SpacedriveIcon
+ let size: CGFloat
+ let showLightVariants: Bool
+ let show20pxVariants: Bool
+ @State private var isHovered = false
+
+ var body: some View {
+ VStack(spacing: 8) {
+ // Icon display
+ SpacedriveIconView(icon, size: size)
+ .frame(width: size + 8, height: size + 8)
+ .background(
+ RoundedRectangle(cornerRadius: 8)
+ .fill(isHovered ? Color.accentColor.opacity(0.1) : Color.clear)
+ )
+
+ // Variant indicators
+ HStack(spacing: 4) {
+ if icon.isLightVariant {
+ Text("L")
+ .font(.caption2)
+ .foregroundColor(.white)
+ .padding(.horizontal, 4)
+ .padding(.vertical, 2)
+ .background(Color.blue)
+ .cornerRadius(4)
+ }
+
+ if icon.is20pxVariant {
+ Text("20")
+ .font(.caption2)
+ .foregroundColor(.white)
+ .padding(.horizontal, 4)
+ .padding(.vertical, 2)
+ .background(Color.green)
+ .cornerRadius(4)
+ }
+ }
+
+ // Filename
+ Text(icon.filename)
+ .font(.caption2)
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ }
+ .padding(12)
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(SpacedriveColors.Background.tertiary)
+ .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1)
+ )
+ .scaleEffect(isHovered ? 1.02 : 1.0)
+ .animation(.easeInOut(duration: 0.2), value: isHovered)
+ .onHover { hovering in
+ isHovered = hovering
+ }
+ .help("\(icon.displayName)\n\(icon.filename)")
+ }
+}
+
+/// Preview for the icon showcase
+struct IconShowcaseView_Previews: PreviewProvider {
+ static var previews: some View {
+ IconShowcaseView()
+ .environmentObject(SharedAppState.shared)
+ .frame(width: 1000, height: 800)
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Inspector/ViewModels/InspectorViewModel.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Inspector/ViewModels/InspectorViewModel.swift
new file mode 100644
index 000000000..6e6f8aa98
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Inspector/ViewModels/InspectorViewModel.swift
@@ -0,0 +1,135 @@
+import Foundation
+import os.log
+import SpacedriveClient
+import SwiftUI
+import Combine
+
+/// Custom errors for the Inspector
+enum InspectorError: LocalizedError {
+ case daemonConnectorNotAvailable
+ case invalidFileURL
+ case fileNotFound
+
+ var errorDescription: String? {
+ switch self {
+ case .daemonConnectorNotAvailable:
+ return "Daemon connector is not available"
+ case .invalidFileURL:
+ return "Invalid file URL provided"
+ case .fileNotFound:
+ return "File not found"
+ }
+ }
+}
+
+/// ViewModel for the Inspector window
+@MainActor
+class InspectorViewModel: ObservableObject {
+ @Published var file: File?
+ @Published var isLoading = false
+ @Published var errorMessage: String?
+ @Published var draggedFileURL: URL?
+
+ private var daemonConnector: DaemonConnector?
+ private let logger = Logger(subsystem: "com.spacedrive.inspector", category: "InspectorViewModel")
+
+ init() {
+ logger.info("InspectorViewModel initialized")
+ setupDaemonConnector()
+ }
+
+ deinit {
+ logger.info("InspectorViewModel deinitialized")
+ }
+
+ private func setupDaemonConnector() {
+ logger.info("Setting up DaemonConnector")
+ daemonConnector = DaemonConnector()
+ logger.info("DaemonConnector setup complete")
+ }
+
+ /// Load file information by path
+ func loadFileByPath(_ url: URL) {
+ logger.info("Starting to load file: \(url.path)")
+
+ isLoading = true
+ errorMessage = nil
+ draggedFileURL = url
+
+ Task {
+ do {
+ logger.info("Creating FileByPathQuery for path: \(url.path)")
+
+ // Create the query with the local path directly
+ let query = FileByPathQuery(path: url.path)
+
+ logger.info("Query created successfully, executing...")
+
+ // Execute the query
+ guard let daemonConnector = daemonConnector else {
+ logger.error("DaemonConnector is nil")
+ throw InspectorError.daemonConnectorNotAvailable
+ }
+
+ let result = try await daemonConnector.queryFileByPath(query)
+
+ logger.info("Query executed successfully, result: \(result != nil ? "file found" : "file not found")")
+
+ await MainActor.run {
+ self.file = result
+ self.isLoading = false
+ if result != nil {
+ self.logger.info("File loaded successfully: \(url.lastPathComponent)")
+ } else {
+ self.logger.warning("File not found in database: \(url.path)")
+ self.errorMessage = "File not found in Spacedrive database"
+ }
+ }
+ } catch {
+ logger.error("Failed to load file: \(error.localizedDescription)")
+ logger.error("Error details: \(String(describing: error))")
+
+ await MainActor.run {
+ self.errorMessage = "Failed to load file: \(error.localizedDescription)"
+ self.isLoading = false
+ }
+ }
+ }
+ }
+
+ /// Clear the current file
+ func clearFile() {
+ logger.info("Clearing current file")
+ file = nil
+ errorMessage = nil
+ draggedFileURL = nil
+ logger.info("File cleared successfully")
+ }
+
+ /// Check if a file URL is valid for inspection
+ func isValidFileURL(_ url: URL) -> Bool {
+ logger.info("Validating file URL: \(url.path)")
+
+ // Check if it's a file URL and the file exists
+ guard url.isFileURL else {
+ logger.warning("Invalid file URL (not a file URL): \(url)")
+ return false
+ }
+
+ var isDirectory: ObjCBool = false
+ let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
+
+ // Only allow files, not directories
+ let isValid = exists && !isDirectory.boolValue
+
+ if !exists {
+ logger.warning("File does not exist: \(url.path)")
+ } else if isDirectory.boolValue {
+ logger.warning("Path is a directory, not a file: \(url.path)")
+ } else {
+ logger.info("File URL is valid: \(url.lastPathComponent)")
+ }
+
+ return isValid
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Inspector/Views/InspectorView.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Inspector/Views/InspectorView.swift
new file mode 100644
index 000000000..1bd0ca56e
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Inspector/Views/InspectorView.swift
@@ -0,0 +1,422 @@
+import AppKit
+import SpacedriveClient
+import SwiftUI
+import UniformTypeIdentifiers
+
+/// Inspector window view for displaying file information
+struct InspectorView: View {
+ @StateObject private var viewModel = InspectorViewModel()
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // Header
+ headerView
+
+ Divider()
+
+ // Content
+ contentView
+ }
+ .background(Color(NSColor.windowBackgroundColor))
+ .onDrop(of: [.fileURL], isTargeted: nil) { providers in
+ handleFileDrop(providers)
+ }
+ }
+
+ // MARK: - Header View
+
+ private var headerView: some View {
+ HStack {
+ Image(systemName: "doc.text.magnifyingglass")
+ .foregroundColor(.accentColor)
+ .font(.title2)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text("File Inspector")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ Text("Drag a file here to inspect")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+
+ if viewModel.file != nil {
+ Button("Clear") {
+ viewModel.clearFile()
+ }
+ .buttonStyle(.borderless)
+ }
+ }
+ .padding()
+ }
+
+ // MARK: - Content View
+
+ private var contentView: some View {
+ Group {
+ if viewModel.isLoading {
+ loadingView
+ } else if let errorMessage = viewModel.errorMessage {
+ errorView(errorMessage)
+ } else if let file = viewModel.file {
+ fileInfoView(file)
+ } else {
+ emptyStateView
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+
+ // MARK: - Loading View
+
+ private var loadingView: some View {
+ VStack(spacing: 16) {
+ ProgressView()
+ .scaleEffect(1.2)
+
+ Text("Loading file information...")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ }
+ }
+
+ // MARK: - Error View
+
+ private func errorView(_ message: String) -> some View {
+ VStack(spacing: 16) {
+ Image(systemName: "exclamationmark.triangle")
+ .font(.largeTitle)
+ .foregroundColor(.red)
+
+ Text("Error")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ Text(message)
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal)
+ }
+ }
+
+ // MARK: - Empty State View
+
+ private var emptyStateView: some View {
+ VStack(spacing: 20) {
+ Image(systemName: "doc.badge.plus")
+ .font(.system(size: 60))
+ .foregroundColor(.secondary)
+
+ VStack(spacing: 8) {
+ Text("No File Selected")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ Text("Drag and drop a file from your system to inspect its properties")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal)
+ }
+ }
+ }
+
+ // MARK: - File Info View
+
+ private func fileInfoView(_ file: File) -> some View {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 16) {
+ // File Header
+ fileHeaderView(file)
+
+ Divider()
+
+ // Basic Properties
+ basicPropertiesView(file)
+
+ Divider()
+
+ // Content Identity
+ if let contentIdentity = file.contentIdentity {
+ contentIdentityView(contentIdentity)
+
+ Divider()
+ }
+
+ // Tags
+ if !file.tags.isEmpty {
+ tagsView(file.tags)
+
+ Divider()
+ }
+
+ // Sidecars
+ if !file.sidecars.isEmpty {
+ sidecarsView(file.sidecars)
+
+ Divider()
+ }
+
+ // Alternate Paths
+ if !file.alternatePaths.isEmpty {
+ alternatePathsView(file.alternatePaths)
+ }
+ }
+ .padding()
+ }
+ }
+
+ // MARK: - File Header
+
+ private func fileHeaderView(_ file: File) -> some View {
+ HStack(spacing: 12) {
+ // File Icon
+ Image(systemName: "doc")
+ .font(.system(size: 40))
+ .foregroundColor(.accentColor)
+
+ VStack(alignment: .leading, spacing: 4) {
+ Text(file.name)
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ Text("File Path")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .lineLimit(2)
+ }
+
+ Spacer()
+ }
+ }
+
+ // MARK: - Basic Properties
+
+ private func basicPropertiesView(_ file: File) -> some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Properties")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ LazyVGrid(columns: [
+ GridItem(.flexible()),
+ GridItem(.flexible()),
+ ], spacing: 8) {
+ PropertyRow(label: "Size", value: ByteCountFormatter.string(fromByteCount: Int64(file.size), countStyle: .file))
+ PropertyRow(label: "Created", value: file.createdAt)
+ PropertyRow(label: "Modified", value: file.modifiedAt)
+ PropertyRow(label: "Content Kind", value: file.contentKind.rawValue)
+ PropertyRow(label: "Extension", value: file.extension ?? "None")
+ PropertyRow(label: "Is Local", value: file.isLocal ? "Yes" : "No")
+ }
+ }
+ }
+
+ // MARK: - Content Identity
+
+ private func contentIdentityView(_ contentIdentity: ContentIdentity) -> some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Content Identity")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ VStack(alignment: .leading, spacing: 4) {
+ PropertyRow(label: "UUID", value: contentIdentity.uuid)
+ PropertyRow(label: "Kind", value: contentIdentity.kind.rawValue)
+ PropertyRow(label: "Hash", value: String(contentIdentity.hash.prefix(16)) + "...")
+ PropertyRow(label: "Created", value: contentIdentity.createdAt)
+ }
+ }
+ }
+
+ // MARK: - Tags
+
+ private func tagsView(_ tags: [Tag]) -> some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Tags")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ LazyVGrid(columns: [
+ GridItem(.flexible()),
+ GridItem(.flexible()),
+ ], spacing: 8) {
+ ForEach(tags, id: \.id) { tag in
+ TagChip(tag: tag)
+ }
+ }
+ }
+ }
+
+ // MARK: - Sidecars
+
+ private func sidecarsView(_ sidecars: [Sidecar]) -> some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Sidecars")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ ForEach(sidecars, id: \.id) { sidecar in
+ SidecarRow(sidecar: sidecar)
+ }
+ }
+ }
+
+ // MARK: - Alternate Paths
+
+ private func alternatePathsView(_ paths: [SdPath]) -> some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Alternate Paths")
+ .font(.headline)
+ .foregroundColor(.primary)
+
+ ForEach(paths.indices, id: \.self) { _ in
+ HStack {
+ Image(systemName: "doc.on.doc")
+ .foregroundColor(.secondary)
+
+ Text("Alternate Path")
+ .font(.caption)
+ .foregroundColor(.secondary)
+
+ Spacer()
+ }
+ .padding(.vertical, 2)
+ }
+ }
+ }
+
+ // MARK: - File Drop Handler
+
+ private func handleFileDrop(_ providers: [NSItemProvider]) -> Bool {
+ guard let provider = providers.first else { return false }
+
+ provider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { item, _ in
+ guard let data = item as? Data,
+ let url = URL(dataRepresentation: data, relativeTo: nil)
+ else {
+ return
+ }
+
+ DispatchQueue.main.async {
+ if viewModel.isValidFileURL(url) {
+ viewModel.loadFileByPath(url)
+ }
+ }
+ }
+
+ return true
+ }
+}
+
+// MARK: - Helper Views
+
+struct PropertyRow: View {
+ let label: String
+ let value: String
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(label)
+ .font(.caption)
+ .foregroundColor(.secondary)
+
+ Text(value)
+ .font(.subheadline)
+ .foregroundColor(.primary)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+}
+
+struct TagChip: View {
+ let tag: Tag
+
+ var body: some View {
+ HStack {
+ Circle()
+ .fill(Color(hex: tag.color ?? "#666666"))
+ .frame(width: 8, height: 8)
+
+ Text(tag.displayName ?? tag.canonicalName)
+ .font(.caption)
+ .foregroundColor(.primary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(Color(NSColor.controlBackgroundColor))
+ .cornerRadius(4)
+ }
+}
+
+struct SidecarRow: View {
+ let sidecar: Sidecar
+
+ var body: some View {
+ HStack {
+ Image(systemName: "doc.badge.gearshape")
+ .foregroundColor(.secondary)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(sidecar.kind)
+ .font(.subheadline)
+ .foregroundColor(.primary)
+
+ Text("\(ByteCountFormatter.string(fromByteCount: sidecar.size, countStyle: .file)) • \(sidecar.status)")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ Spacer()
+
+ Text(sidecar.format)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding(.vertical, 4)
+ }
+}
+
+// MARK: - Extensions
+
+extension DateFormatter {
+ static let shortDateTime: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+ return formatter
+ }()
+}
+
+extension Color {
+ init(hex: String) {
+ let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
+ var int: UInt64 = 0
+ Scanner(string: hex).scanHexInt64(&int)
+ let a, r, g, b: UInt64
+ switch hex.count {
+ case 3: // RGB (12-bit)
+ (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
+ case 6: // RGB (24-bit)
+ (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
+ case 8: // ARGB (32-bit)
+ (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
+ default:
+ (a, r, g, b) = (1, 1, 1, 0)
+ }
+
+ self.init(
+ .sRGB,
+ red: Double(r) / 255,
+ green: Double(g) / 255,
+ blue: Double(b) / 255,
+ opacity: Double(a) / 255
+ )
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Models/JobModels.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Models/JobModels.swift
new file mode 100644
index 000000000..01655924c
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Models/JobModels.swift
@@ -0,0 +1,311 @@
+import Foundation
+import SpacedriveClient
+
+extension JsonValue {
+ var stringValue: String? {
+ if case let .string(string) = self {
+ return string
+ }
+ return nil
+ }
+
+ var dictionaryValue: [String: JsonValue]? {
+ if case let .object(dict) = self {
+ return dict
+ }
+ return nil
+ }
+}
+
+// MARK: - Job Models
+
+struct JobInfo: Codable, Identifiable {
+ let id: String
+ var name: String
+ var status: JobStatus
+ var progress: Double
+ let startedAt: Date
+ var completedAt: Date?
+ var errorMessage: String?
+
+ // Enhanced fields from improved job progress events
+ var currentPhase: String?
+ var completionInfo: String?
+ var hasIssues: Bool = false
+ var issuesInfo: String?
+ var currentPath: String? // Current file/directory being processed
+ var jobType: String? // Store the original job type for title display
+
+ // Action context fields for richer job information
+ var actionType: String?
+ var actionContext: ActionContextInfo?
+
+ enum CodingKeys: String, CodingKey {
+ case id
+ case name
+ case status
+ case progress
+ case startedAt = "started_at"
+ case completedAt = "completed_at"
+ case errorMessage = "error_message"
+ case currentPhase = "current_phase"
+ case completionInfo = "completion_info"
+ case hasIssues = "has_issues"
+ case issuesInfo = "issues_info"
+ case currentPath = "current_path"
+ case jobType = "job_type"
+ case actionType = "action_type"
+ case actionContext = "action_context"
+ }
+
+ // Convenience initializer from generated JobListItem
+ init(from jobListItem: JobListItem) {
+ id = jobListItem.id
+ name = jobListItem.name
+ status = JobStatus(rawValue: jobListItem.status.rawValue) ?? .failed
+ progress = Double(jobListItem.progress)
+ startedAt = Date() // TODO: Get actual start time from daemon
+ completedAt = nil // TODO: Get actual completion time from daemon
+ errorMessage = nil // TODO: Get error message from daemon if status is failed
+ currentPhase = nil
+ completionInfo = nil
+ hasIssues = false
+ issuesInfo = nil
+ currentPath = nil
+ jobType = nil
+ actionType = jobListItem.actionType
+ actionContext = jobListItem.actionContext
+ }
+
+ // Convenience initializer for creating JobInfo with individual parameters
+ init(id: String, name: String, status: JobStatus, progress: Double, startedAt: Date, completedAt: Date? = nil, errorMessage: String? = nil, currentPhase: String? = nil, completionInfo: String? = nil, hasIssues: Bool = false, issuesInfo: String? = nil, currentPath: String? = nil, jobType: String? = nil, actionType: String? = nil, actionContext: ActionContextInfo? = nil) {
+ self.id = id
+ self.name = name
+ self.status = status
+ self.progress = progress
+ self.startedAt = startedAt
+ self.completedAt = completedAt
+ self.errorMessage = errorMessage
+ self.currentPhase = currentPhase
+ self.completionInfo = completionInfo
+ self.hasIssues = hasIssues
+ self.issuesInfo = issuesInfo
+ self.currentPath = currentPath
+ self.jobType = jobType
+ self.actionType = actionType
+ self.actionContext = actionContext
+ }
+
+ // Computed property for richer job name display using raw data
+ var displayName: String {
+ // If we have action context, extract meaningful info from the raw data
+ if let actionContext = actionContext {
+ switch actionContext.actionType {
+ case "locations.add":
+ let inputObj = actionContext.actionInput.dictionaryValue
+ if let pathValue = inputObj?["path"],
+ let path = pathValue.stringValue
+ {
+ // Extract directory name from path
+ let directoryName = URL(fileURLWithPath: path).lastPathComponent
+ return "Adding '\(directoryName)'"
+ }
+ return "Adding Location"
+ case "files.copy":
+ let inputObj = actionContext.actionInput.dictionaryValue
+ if let sourceValue = inputObj?["source"],
+ let source = sourceValue.stringValue
+ {
+ let sourceName = URL(fileURLWithPath: source).lastPathComponent
+ return "Copying '\(sourceName)'"
+ }
+ return "Copying Files"
+ case "files.move":
+ let inputObj = actionContext.actionInput.dictionaryValue
+ if let sourceValue = inputObj?["source"],
+ let source = sourceValue.stringValue
+ {
+ let sourceName = URL(fileURLWithPath: source).lastPathComponent
+ return "Moving '\(sourceName)'"
+ }
+ return "Moving Files"
+ case "files.delete":
+ let inputObj = actionContext.actionInput.dictionaryValue
+ if let targetValue = inputObj?["target"],
+ let target = targetValue.stringValue
+ {
+ let targetName = URL(fileURLWithPath: target).lastPathComponent
+ return "Deleting '\(targetName)'"
+ }
+ return "Deleting Files"
+ case "media.thumbnail":
+ return "Generating Thumbnails"
+ case "media.extract":
+ return "Extracting Media"
+ default:
+ // For unknown action types, try to format them nicely
+ return actionContext.actionType.replacingOccurrences(of: ".", with: " ").capitalized
+ }
+ }
+
+ // Fallback to the original name or job type
+ return jobType?.capitalized ?? name.capitalized
+ }
+
+ // Computed property for additional context information
+ var contextInfo: String? {
+ guard let actionContext = actionContext else { return nil }
+
+ // Extract meaningful info from raw action input
+ switch actionContext.actionType {
+ case "locations.add":
+ let inputObj = actionContext.actionInput.dictionaryValue
+ if let pathValue = inputObj?["path"],
+ let path = pathValue.stringValue
+ {
+ // Extract directory name from path
+ let directoryName = URL(fileURLWithPath: path).lastPathComponent
+ return "Adding '\(directoryName)' at \(path)"
+ }
+ case "files.copy":
+ let inputObj = actionContext.actionInput.dictionaryValue
+ if let sourceValue = inputObj?["source"],
+ let source = sourceValue.stringValue
+ {
+ if let destValue = inputObj?["destination"],
+ let destination = destValue.stringValue
+ {
+ return "Copying from \(source) to \(destination)"
+ } else {
+ return "Copying from \(source)"
+ }
+ }
+ case "files.move":
+ let inputObj = actionContext.actionInput.dictionaryValue
+ if let sourceValue = inputObj?["source"],
+ let source = sourceValue.stringValue
+ {
+ if let destValue = inputObj?["destination"],
+ let destination = destValue.stringValue
+ {
+ return "Moving from \(source) to \(destination)"
+ } else {
+ return "Moving from \(source)"
+ }
+ }
+ case "files.delete":
+ let inputObj = actionContext.actionInput.dictionaryValue
+ if let targetValue = inputObj?["target"],
+ let target = targetValue.stringValue
+ {
+ return "Deleting \(target)"
+ }
+ case "media.thumbnail":
+ return "Generating thumbnails for media files"
+ case "media.extract":
+ return "Extracting metadata from media files"
+ default:
+ break
+ }
+
+ return nil
+ }
+}
+
+enum JobStatus: String, Codable, CaseIterable {
+ case running
+ case completed
+ case failed
+ case paused
+ case queued
+ case cancelled
+
+ var displayName: String {
+ switch self {
+ case .running:
+ return "Running"
+ case .completed:
+ return "Completed"
+ case .failed:
+ return "Failed"
+ case .paused:
+ return "Paused"
+ case .queued:
+ return "Queued"
+ case .cancelled:
+ return "Cancelled"
+ }
+ }
+
+ init(from generatedStatus: JobStatus) {
+ switch generatedStatus {
+ case .running:
+ self = .running
+ case .completed:
+ self = .completed
+ case .failed:
+ self = .failed
+ case .paused:
+ self = .paused
+ case .queued:
+ self = .queued
+ case .cancelled:
+ self = .cancelled
+ }
+ }
+
+ var icon: String {
+ switch self {
+ case .running:
+ return "circle.fill"
+ case .completed:
+ return "checkmark.circle.fill"
+ case .failed:
+ return "xmark.circle.fill"
+ case .paused:
+ return "pause.circle.fill"
+ case .queued:
+ return "clock.fill"
+ case .cancelled:
+ return "xmark.circle.fill"
+ }
+ }
+}
+
+// MARK: - Legacy Models (to be replaced with generated types)
+
+// TODO: Replace these with generated types once Event has JsonSchema derive
+// For now, keeping minimal models for the companion app
+
+// MARK: - Connection Status
+
+enum ConnectionStatus: Equatable {
+ case disconnected
+ case connecting
+ case connected
+ case error(String)
+
+ var displayName: String {
+ switch self {
+ case .disconnected:
+ return "Disconnected"
+ case .connecting:
+ return "Connecting..."
+ case .connected:
+ return "Connected"
+ case let .error(message):
+ return "Error: \(message)"
+ }
+ }
+
+ var color: String {
+ switch self {
+ case .disconnected, .error:
+ return "red"
+ case .connecting:
+ return "yellow"
+ case .connected:
+ return "green"
+ }
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/ViewModels/JobListViewModel.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/ViewModels/JobListViewModel.swift
new file mode 100644
index 000000000..924f7db93
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/ViewModels/JobListViewModel.swift
@@ -0,0 +1,88 @@
+import Combine
+import Foundation
+
+@MainActor
+class JobListViewModel: ObservableObject {
+ @Published var jobs: [JobInfo] = []
+ @Published var connectionStatus: ConnectionStatus = .disconnected
+
+ private var daemonConnector: DaemonConnector?
+ private var cancellables = Set()
+
+ init() {
+ setupDaemonConnector()
+ }
+
+ deinit {
+ daemonConnector = nil
+ }
+
+ private func setupDaemonConnector() {
+ daemonConnector = DaemonConnector()
+
+ // Bind daemon connector's published properties to our own
+ daemonConnector?.$jobs
+ .receive(on: DispatchQueue.main)
+ .assign(to: \.jobs, on: self)
+ .store(in: &cancellables)
+
+ daemonConnector?.$connectionStatus
+ .receive(on: DispatchQueue.main)
+ .assign(to: \.connectionStatus, on: self)
+ .store(in: &cancellables)
+ }
+
+ func reconnect() {
+ daemonConnector?.reconnect()
+ }
+
+ func disconnect() {
+ daemonConnector?.disconnect()
+ }
+
+ // MARK: - Computed Properties
+
+ var activeJobs: [JobInfo] {
+ jobs.filter { job in
+ job.status == .running || job.status == .queued
+ }
+ }
+
+ var completedJobs: [JobInfo] {
+ jobs.filter { job in
+ job.status == .completed
+ }
+ }
+
+ var failedJobs: [JobInfo] {
+ jobs.filter { job in
+ job.status == .failed
+ }
+ }
+
+ var jobCounts: (active: Int, completed: Int, failed: Int) {
+ return (
+ active: activeJobs.count,
+ completed: completedJobs.count,
+ failed: failedJobs.count
+ )
+ }
+
+ // MARK: - Helper Methods
+
+ func job(withId id: String) -> JobInfo? {
+ return jobs.first { $0.id == id }
+ }
+
+ func removeCompletedJobs() {
+ jobs.removeAll { job in
+ job.status == .completed &&
+ job.completedAt != nil &&
+ Date().timeIntervalSince(job.completedAt!) > 3600 // Remove completed jobs older than 1 hour
+ }
+ }
+
+ func clearAllJobs() {
+ jobs.removeAll()
+ }
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Views/JobCompanionView.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Views/JobCompanionView.swift
new file mode 100644
index 000000000..a6a7aa7dd
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Views/JobCompanionView.swift
@@ -0,0 +1,45 @@
+import AppKit
+import SwiftUI
+
+/// Job Companion Window - The floating job monitor window
+struct JobCompanionView: View {
+ @EnvironmentObject var appState: SharedAppState
+
+ var body: some View {
+ WindowContainer {
+ JobMonitorView()
+ .frame(minWidth: 300, minHeight: 400)
+ }
+ }
+}
+
+#Preview {
+ let appState = SharedAppState.shared
+
+ // Add some sample jobs for preview
+ appState.globalJobs = [
+ JobInfo(
+ id: "1",
+ name: "file_indexer",
+ status: .running,
+ progress: 0.65,
+ startedAt: Date().addingTimeInterval(-300),
+ completedAt: nil,
+ errorMessage: nil
+ ),
+ JobInfo(
+ id: "2",
+ name: "thumbnail_generator",
+ status: .completed,
+ progress: 1.0,
+ startedAt: Date().addingTimeInterval(-600),
+ completedAt: Date().addingTimeInterval(-60),
+ errorMessage: nil
+ ),
+ ]
+
+ return JobCompanionView()
+ .environmentObject(appState)
+ .frame(width: 400, height: 600)
+ .background(SpacedriveColors.Background.primary)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Views/JobMonitorView.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Views/JobMonitorView.swift
new file mode 100644
index 000000000..0eef8e669
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Views/JobMonitorView.swift
@@ -0,0 +1,103 @@
+import SwiftUI
+
+struct JobMonitorView: View {
+ @EnvironmentObject var appState: SharedAppState
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // Library selector at the top
+ LibrarySelector()
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+
+ Rectangle()
+ .fill(SpacedriveColors.Border.secondary)
+ .frame(height: 1)
+
+ // Job list
+ if appState.globalJobs.isEmpty {
+ emptyStateView
+ } else {
+ jobListView
+ }
+
+ Spacer()
+ }
+ // .background(SpacedriveColors.Background.primary)
+ }
+
+ private var emptyStateView: some View {
+ VStack(spacing: 16) {
+ SpacedriveIconView(.folderNoSpace, size: 100)
+ .foregroundColor(SpacedriveColors.Accent.success.opacity(0.6))
+
+ VStack(spacing: 8) {
+ Text("No Active Jobs")
+ .h4()
+
+ Text("All jobs are completed or no jobs are currently running.")
+ .bodySmall(color: SpacedriveColors.Text.secondary)
+ .multilineTextAlignment(.center)
+ }
+ }
+ .frame(maxWidth: .infinity)
+ .padding(32)
+ }
+
+ private var jobListView: some View {
+ ScrollView {
+ LazyVStack(spacing: 6) {
+ ForEach(appState.globalJobs) { job in
+ JobRowView(job: job)
+ .transition(.asymmetric(
+ insertion: .scale.combined(with: .opacity),
+ removal: .opacity
+ ))
+ }
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+ }
+ .animation(.easeInOut(duration: 0.2), value: appState.globalJobs.count)
+ }
+}
+
+#Preview {
+ let appState = SharedAppState.shared
+
+ // Add some sample jobs for preview
+ appState.globalJobs = [
+ JobInfo(
+ id: "1",
+ name: "file_indexer",
+ status: .running,
+ progress: 0.65,
+ startedAt: Date().addingTimeInterval(-300),
+ completedAt: nil,
+ errorMessage: nil
+ ),
+ JobInfo(
+ id: "2",
+ name: "thumbnail_generator",
+ status: .completed,
+ progress: 1.0,
+ startedAt: Date().addingTimeInterval(-600),
+ completedAt: Date().addingTimeInterval(-60),
+ errorMessage: nil
+ ),
+ JobInfo(
+ id: "3",
+ name: "file_copy",
+ status: .failed,
+ progress: 0.3,
+ startedAt: Date().addingTimeInterval(-120),
+ completedAt: Date().addingTimeInterval(-30),
+ errorMessage: "Permission denied"
+ ),
+ ]
+
+ return JobMonitorView()
+ .environmentObject(appState)
+ .frame(width: 400, height: 600)
+ .background(SpacedriveColors.Background.primary)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Views/JobRowView.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Views/JobRowView.swift
new file mode 100644
index 000000000..15aaff400
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Jobs/Views/JobRowView.swift
@@ -0,0 +1,368 @@
+import SpacedriveClient
+import SwiftUI
+
+struct JobRowView: View {
+ let job: JobInfo
+ @Environment(\.window) private var window
+ @State private var isWindowActive = true
+
+ private var progressPercentage: String {
+ return String(format: "%.0f%%", job.progress * 100)
+ }
+
+ private var timeAgo: String {
+ let formatter = RelativeDateTimeFormatter()
+ formatter.unitsStyle = .abbreviated
+ return formatter.localizedString(for: job.startedAt, relativeTo: Date())
+ }
+
+ private var duration: String? {
+ guard let completedAt = job.completedAt else { return nil }
+ let duration = completedAt.timeIntervalSince(job.startedAt)
+
+ if duration < 60 {
+ return String(format: "%.0fs", duration)
+ } else if duration < 3600 {
+ return String(format: "%.0fm", duration / 60)
+ } else {
+ return String(format: "%.1fh", duration / 3600)
+ }
+ }
+
+ private var subtextForJob: String {
+ // Show current path when available and job is running
+ if job.status == .running, let currentPath = job.currentPath, !currentPath.isEmpty {
+ return currentPath
+ }
+
+ // Show context info if available (for queued jobs especially)
+ if let contextInfo = job.contextInfo, !contextInfo.isEmpty {
+ return contextInfo
+ }
+
+ // Default subtext based on job status and type
+ switch job.status {
+ case .running:
+ if let phase = job.currentPhase {
+ return phase
+ } else {
+ return "Processing..."
+ }
+ case .completed:
+ if let duration = duration {
+ return "Completed in \(duration)"
+ } else {
+ return "Completed successfully"
+ }
+ case .failed:
+ return job.errorMessage ?? "Job failed"
+ case .paused:
+ return "Paused"
+ case .queued:
+ return "Waiting to start"
+ case .cancelled:
+ return "Cancelled"
+ }
+ }
+
+ var body: some View {
+ SDJobCard {
+ HStack(spacing: 10) {
+ // Status indicator - simple colored circle
+ Circle()
+ .fill(statusColor)
+ .frame(width: 8, height: 8)
+ .opacity(job.status == .completed ? 1.0 : 0.8)
+
+ // Main content area with equal spacing between rows
+ VStack(alignment: .leading, spacing: 6) {
+ // Row 1: Job title and status
+ HStack {
+ Text(job.displayName)
+ .font(.system(size: 13, weight: .medium))
+ .foregroundColor(SpacedriveColors.Text.primary)
+ .lineLimit(1)
+ .padding(.bottom, -2)
+ .truncationMode(.tail)
+
+ Spacer()
+ // Status and progress info
+ HStack(spacing: 6) {
+ // Pause/Resume button for active jobs
+ if job.status == .running || job.status == .paused {
+ JobActionButton(job: job)
+ }
+ if job.status == .running {
+ // Show phase if available, otherwise percentage
+ if let phase = job.currentPhase, !phase.isEmpty {
+ Text(phase)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ } else {
+ Text(progressPercentage)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ }
+ } else {
+ Text(job.status.displayName)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundColor(job.status == .failed ? SpacedriveColors.Accent.error : SpacedriveColors.Text.secondary)
+ }
+
+ // Show completion info or time info
+ if let completionInfo = job.completionInfo, !completionInfo.isEmpty, job.status == .running {
+ Text("• \(completionInfo)")
+ .font(.system(size: 11))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ .opacity(0.7)
+ } else if let duration = duration {
+ Text("• \(duration)")
+ .font(.system(size: 11))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ .opacity(0.7)
+ } else if job.status == .running {
+ Text("• \(timeAgo)")
+ .font(.system(size: 11))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ .opacity(0.7)
+ }
+
+ // Show issues indicator if present
+ if job.hasIssues, let issuesInfo = job.issuesInfo {
+ Text("⚠️")
+ .font(.system(size: 10))
+ .foregroundColor(SpacedriveColors.Accent.warning)
+ .help(issuesInfo)
+ }
+ }
+ }
+
+ // Row 2: Subtext
+ Text(subtextForJob)
+ .font(.system(size: 10))
+ .foregroundColor(SpacedriveColors.Text.secondary)
+ .opacity(0.7)
+ .padding(.bottom, 2)
+ .lineLimit(1)
+ .truncationMode(.tail)
+
+ // Row 3: Progress bar
+ HStack {
+ if job.status == .running || job.status == .paused {
+ ProgressView(value: job.progress, total: 1.0)
+ .progressViewStyle(LinearProgressViewStyle(tint: statusColor))
+ .frame(height: 4)
+ } else if job.status == .completed {
+ // Completed indicator line
+ ProgressView(value: job.progress, total: 1.0)
+ .progressViewStyle(LinearProgressViewStyle(tint: SpacedriveColors.Accent.success.opacity(0.6)))
+ .frame(height: 4)
+ } else {
+ // Empty space for other statuses to maintain consistent height
+ Rectangle()
+ .fill(Color.clear)
+ .frame(height: 4)
+ }
+
+ Spacer()
+ }
+
+ // Error message (if present)
+ if let errorMessage = job.errorMessage, !errorMessage.isEmpty {
+ Text(errorMessage)
+ .font(.system(size: 11))
+ .foregroundColor(SpacedriveColors.Accent.error)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ }
+ }
+ .frame(height: 52) // Reduced total height with better spacing
+ }
+ }
+ .onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)) { _ in
+ updateWindowActiveState()
+ }
+ .onReceive(NotificationCenter.default.publisher(for: NSWindow.didResignKeyNotification)) { _ in
+ updateWindowActiveState()
+ }
+ .onAppear {
+ updateWindowActiveState()
+ }
+ }
+
+ private func updateWindowActiveState() {
+ isWindowActive = window?.isKeyWindow ?? false
+ }
+
+ private var statusColor: Color {
+ switch job.status {
+ case .running:
+ return SpacedriveColors.Accent.info
+ case .completed:
+ return SpacedriveColors.Accent.success
+ case .failed:
+ return SpacedriveColors.Accent.error
+ case .paused:
+ return SpacedriveColors.Accent.warning
+ case .queued:
+ return SpacedriveColors.Text.tertiary
+ case .cancelled:
+ return SpacedriveColors.Accent.error
+ }
+ }
+}
+
+/// Small action button for pause/resume job functionality
+struct JobActionButton: View {
+ let job: JobInfo
+ @EnvironmentObject var appState: SharedAppState
+ @State private var isHovered = false
+
+ var body: some View {
+ Button(action: {
+ performAction()
+ }) {
+ Image(systemName: iconName)
+ .font(.system(size: 10, weight: .medium))
+ .foregroundColor(iconColor)
+ .frame(width: 16, height: 16)
+ .background(
+ Circle()
+ .fill(backgroundColor)
+ .frame(width: 16, height: 16)
+ )
+ }
+ .buttonStyle(PlainButtonStyle())
+ .help(helpText)
+ .onHover { hovering in
+ isHovered = hovering
+ }
+ }
+
+ private var iconName: String {
+ switch job.status {
+ case .running:
+ return "pause.fill"
+ case .paused:
+ return "play.fill"
+ default:
+ return "pause.fill"
+ }
+ }
+
+ private var iconColor: Color {
+ if isHovered {
+ return SpacedriveColors.Text.primary
+ } else {
+ return SpacedriveColors.Text.secondary
+ }
+ }
+
+ private var backgroundColor: Color {
+ if isHovered {
+ return SpacedriveColors.Interactive.hover
+ } else {
+ return Color.clear
+ }
+ }
+
+ private var helpText: String {
+ switch job.status {
+ case .running:
+ return "Pause job"
+ case .paused:
+ return "Resume job"
+ default:
+ return "Job action"
+ }
+ }
+
+ private func performAction() {
+ switch job.status {
+ case .running:
+ appState.dispatch(.pauseJob(job.id))
+ case .paused:
+ appState.dispatch(.resumeJob(job.id))
+ default:
+ break
+ }
+ }
+}
+
+#Preview {
+ VStack(spacing: 12) {
+ JobRowView(job: JobInfo(
+ id: "12345678-1234-1234-1234-123456789012",
+ name: "indexer",
+ status: .running,
+ progress: 0.65,
+ startedAt: Date().addingTimeInterval(-300),
+ completedAt: nil,
+ errorMessage: nil,
+ actionType: "locations.add",
+ actionContext: ActionContextInfo(
+ actionType: "locations.add",
+ initiatedAt: "2025-01-24T20:24:00.820790Z",
+ initiatedBy: "user",
+ actionInput: JsonValue.objectValue([
+ "path": JsonValue.stringValue("/Users/jamespine/Downloads"),
+ "name": JsonValue.stringValue("Downloads"),
+ ]),
+ context: JsonValue.objectValue([
+ "operation": JsonValue.stringValue("add_location"),
+ "trigger": JsonValue.stringValue("user_action"),
+ ])
+ )
+ ))
+
+ JobRowView(job: JobInfo(
+ id: "87654321-4321-4321-4321-210987654321",
+ name: "indexer",
+ status: .completed,
+ progress: 1.0,
+ startedAt: Date().addingTimeInterval(-600),
+ completedAt: Date().addingTimeInterval(-60),
+ errorMessage: nil,
+ actionType: "files.copy",
+ actionContext: ActionContextInfo(
+ actionType: "files.copy",
+ initiatedAt: "2025-01-24T19:30:00.820790Z",
+ initiatedBy: "user",
+ actionInput: JsonValue.objectValue([
+ "source": JsonValue.stringValue("/Users/jamespine/Documents"),
+ "destination": JsonValue.stringValue("/Users/jamespine/Backup"),
+ ]),
+ context: JsonValue.objectValue([
+ "operation": JsonValue.stringValue("copy_files"),
+ "trigger": JsonValue.stringValue("user_action"),
+ ])
+ )
+ ))
+
+ JobRowView(job: JobInfo(
+ id: "11111111-2222-3333-4444-555555555555",
+ name: "thumbnail_generator",
+ status: .failed,
+ progress: 0.3,
+ startedAt: Date().addingTimeInterval(-120),
+ completedAt: Date().addingTimeInterval(-30),
+ errorMessage: "Failed to process image: unsupported format",
+ actionType: "media.thumbnail",
+ actionContext: ActionContextInfo(
+ actionType: "media.thumbnail",
+ initiatedAt: "2025-01-24T20:00:00.820790Z",
+ initiatedBy: "system",
+ actionInput: JsonValue.objectValue([
+ "file_count": JsonValue.stringValue("25"),
+ "file_types": JsonValue.arrayValue([JsonValue.stringValue("jpg"), JsonValue.stringValue("png")]),
+ ]),
+ context: JsonValue.objectValue([
+ "operation": JsonValue.stringValue("generate_thumbnails"),
+ "trigger": JsonValue.stringValue("system"),
+ ])
+ )
+ ))
+ }
+ .padding()
+ .background(Color.black.opacity(0.1))
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Settings/Components/SettingsNavigationView.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Settings/Components/SettingsNavigationView.swift
new file mode 100644
index 000000000..6d9a80025
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Settings/Components/SettingsNavigationView.swift
@@ -0,0 +1,193 @@
+import SwiftUI
+
+/// Settings Navigation Sidebar - Left navigation for settings sections
+struct SettingsNavigationView: View {
+ @Binding var selectedSection: SettingsSection
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 0) {
+ // Header
+ VStack(alignment: .leading, spacing: 6) {
+ Text("Settings")
+ .h3(.semibold)
+ .padding(.horizontal, 20)
+ .padding(.top, 20)
+
+ Text("Configure Spacedrive")
+ .bodySmall(color: SpacedriveColors.Text.secondary)
+ .padding(.horizontal, 20)
+ .padding(.bottom, 20)
+ }
+
+ Rectangle()
+ .fill(SpacedriveColors.Border.secondary)
+ .frame(height: 1)
+
+ // Navigation List
+ ScrollView {
+ LazyVStack(spacing: 2) {
+ ForEach(SettingsSection.allCases) { section in
+ SettingsNavigationItem(
+ section: section,
+ isSelected: selectedSection == section
+ ) {
+ selectedSection = section
+ }
+ }
+ }
+ .padding(.vertical, 12)
+ }
+
+ Spacer()
+
+ // Footer
+ Rectangle()
+ .fill(SpacedriveColors.Border.secondary)
+ .frame(height: 1)
+
+ VStack(alignment: .leading, spacing: 4) {
+ HStack {
+ Circle()
+ .fill(connectionStatusColor)
+ .frame(width: 8, height: 8)
+
+ Text("Daemon: \(connectionStatusText)")
+ .caption(color: SpacedriveColors.Text.secondary)
+
+ Spacer()
+ }
+ .padding(.horizontal, 20)
+ .padding(.top, 16)
+ .padding(.bottom, 20)
+ }
+ }
+ .background(SpacedriveColors.Background.secondary)
+ }
+
+ @EnvironmentObject var appState: SharedAppState
+
+ private var connectionStatusColor: Color {
+ switch appState.connectionStatus {
+ case .connected:
+ return SpacedriveColors.Accent.success
+ case .connecting:
+ return SpacedriveColors.Accent.warning
+ case .disconnected, .error:
+ return SpacedriveColors.Accent.error
+ }
+ }
+
+ private var connectionStatusText: String {
+ switch appState.connectionStatus {
+ case .connected:
+ return "Connected"
+ case .connecting:
+ return "Connecting..."
+ case .disconnected:
+ return "Disconnected"
+ case .error:
+ return "Error"
+ }
+ }
+}
+
+/// Individual navigation item in the settings sidebar
+struct SettingsNavigationItem: View {
+ let section: SettingsSection
+ let isSelected: Bool
+ let action: () -> Void
+
+ @State private var isHovered = false
+
+ var body: some View {
+ Button(action: action) {
+ HStack(spacing: 12) {
+ // Icon with consistent sizing
+ Image(systemName: section.icon)
+ .font(.system(size: 16, weight: .medium))
+ .frame(width: 20, height: 20)
+ .foregroundColor(iconColor)
+
+ // Title text
+ Text(section.rawValue)
+ .label(.medium, color: textColor)
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ // Selection indicator
+ if isSelected {
+ Circle()
+ .fill(SpacedriveColors.Accent.primary)
+ .frame(width: 6, height: 6)
+ }
+ }
+ .padding(.horizontal, 16)
+ .padding(.vertical, 12)
+ .background(
+ RoundedRectangle(cornerRadius: 10)
+ .fill(backgroundColor)
+ .overlay(
+ RoundedRectangle(cornerRadius: 10)
+ .stroke(borderColor, lineWidth: borderWidth)
+ )
+ )
+ .scaleEffect(isHovered ? 1.02 : 1.0)
+ .animation(.easeInOut(duration: 0.2), value: isHovered)
+ .animation(.easeInOut(duration: 0.2), value: isSelected)
+ }
+ .buttonStyle(PlainButtonStyle())
+ .padding(.horizontal, 12)
+ .onHover { hovering in
+ isHovered = hovering
+ }
+ }
+
+ private var backgroundColor: Color {
+ if isSelected {
+ return SpacedriveColors.Accent.primary.opacity(0.12)
+ } else if isHovered {
+ return SpacedriveColors.Interactive.hover.opacity(0.8)
+ } else {
+ return Color.clear
+ }
+ }
+
+ private var borderColor: Color {
+ if isSelected {
+ return SpacedriveColors.Accent.primary.opacity(0.3)
+ } else if isHovered {
+ return SpacedriveColors.Border.primary.opacity(0.5)
+ } else {
+ return Color.clear
+ }
+ }
+
+ private var borderWidth: CGFloat {
+ if isSelected || isHovered {
+ return 1.0
+ } else {
+ return 0.0
+ }
+ }
+
+ private var textColor: Color {
+ if isSelected {
+ return SpacedriveColors.Accent.primary
+ } else {
+ return SpacedriveColors.Text.primary
+ }
+ }
+
+ private var iconColor: Color {
+ if isSelected {
+ return SpacedriveColors.Accent.primary
+ } else {
+ return SpacedriveColors.Text.secondary
+ }
+ }
+}
+
+#Preview {
+ SettingsNavigationView(selectedSection: .constant(.general))
+ .environmentObject(SharedAppState.shared)
+ .frame(width: 250, height: 500)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Settings/Views/SettingsContentView.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Settings/Views/SettingsContentView.swift
new file mode 100644
index 000000000..c0e84619b
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Settings/Views/SettingsContentView.swift
@@ -0,0 +1,306 @@
+import SwiftUI
+
+/// Settings Content Area - Main content that changes based on selected section
+struct SettingsContentView: View {
+ let selectedSection: SettingsSection
+
+ var body: some View {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 0) {
+ // Section Header
+ VStack(alignment: .leading, spacing: 6) {
+ HStack {
+ Image(systemName: selectedSection.icon)
+ .foregroundColor(SpacedriveColors.Accent.primary)
+ .font(.system(size: 22, weight: .medium))
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(selectedSection.rawValue)
+ .h2(.semibold)
+
+ Text(selectedSection.description)
+ .bodySmall(color: SpacedriveColors.Text.secondary)
+ }
+
+ Spacer()
+ }
+ .padding(.horizontal, 28)
+ .padding(.top, 28)
+ .padding(.bottom, 20)
+ }
+
+ Rectangle()
+ .fill(SpacedriveColors.Border.secondary)
+ .frame(height: 1)
+ .padding(.horizontal, 28)
+
+ // Content based on selected section
+ contentForSection(selectedSection)
+ .padding(28)
+ }
+ }
+ .background(SpacedriveColors.Background.primary)
+ }
+
+ @ViewBuilder
+ private func contentForSection(_ section: SettingsSection) -> some View {
+ switch section {
+ case .general:
+ GeneralSettingsView()
+ case .daemon:
+ DaemonSettingsView()
+ case .appearance:
+ AppearanceSettingsView()
+ case .privacy:
+ PrivacySettingsView()
+ case .advanced:
+ AdvancedSettingsView()
+ case .about:
+ AboutSettingsView()
+ }
+ }
+}
+
+// MARK: - Individual Settings Views
+
+struct GeneralSettingsView: View {
+ @EnvironmentObject var appState: SharedAppState
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 20) {
+ // Services status section
+ SettingsGroup("Services") {
+ ConnectivityCard()
+ .environmentObject(appState)
+ }
+
+ // Auto-launch section
+ SettingsGroup("Startup") {
+ SettingsToggle(
+ "Launch at login",
+ description: "Automatically start Spacedrive when you log in",
+ isOn: .constant(false)
+ )
+
+ SettingsToggle(
+ "Auto-connect to daemon",
+ description: "Automatically connect to the daemon on startup",
+ isOn: .constant(appState.userPreferences.autoConnectToDaemon)
+ )
+ }
+
+ // Notifications section
+ SettingsGroup("Notifications") {
+ SettingsToggle(
+ "Show job notifications",
+ description: "Display notifications when jobs complete",
+ isOn: .constant(appState.userPreferences.showJobNotifications)
+ )
+ }
+ }
+ }
+}
+
+struct DaemonSettingsView: View {
+ @EnvironmentObject var appState: SharedAppState
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 20) {
+ SettingsGroup("Connection") {
+ HStack {
+ VStack(alignment: .leading) {
+ Text("Status: \(appState.connectionStatus.displayName)")
+ .body(.medium)
+ Text("Manage daemon connection and settings")
+ .bodySmall(color: SpacedriveColors.Text.secondary)
+ }
+
+ Spacer()
+
+ SDButton("Connect", style: .primary, size: .medium) {
+ appState.dispatch(.connectToDaemon)
+ }
+
+ SDButton("Disconnect", style: .secondary, size: .medium) {
+ appState.dispatch(.disconnectFromDaemon)
+ }
+ }
+
+ SettingsToggle(
+ "Auto-reconnect",
+ description: "Automatically reconnect if connection is lost",
+ isOn: .constant(true)
+ )
+ }
+ }
+ }
+}
+
+struct AppearanceSettingsView: View {
+ @EnvironmentObject var appState: SharedAppState
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 20) {
+ SettingsGroup("Theme") {
+ HStack {
+ Text("Theme:")
+ .body(.medium)
+
+ Spacer()
+
+ Picker("Theme", selection: .constant(SpacedriveTheme.dark)) {
+ Text("Dark").tag(SpacedriveTheme.dark)
+ Text("Light").tag(SpacedriveTheme.light)
+ }
+ .pickerStyle(SegmentedPickerStyle())
+ .frame(width: 200)
+ }
+
+ SettingsToggle(
+ "Compact mode",
+ description: "Use smaller interface elements",
+ isOn: .constant(appState.userPreferences.compactMode)
+ )
+ }
+ }
+ }
+}
+
+struct PrivacySettingsView: View {
+ var body: some View {
+ VStack(alignment: .leading, spacing: 20) {
+ SettingsGroup("Data Collection") {
+ SettingsToggle(
+ "Analytics",
+ description: "Help improve Spacedrive by sharing anonymous usage data",
+ isOn: .constant(false)
+ )
+
+ SettingsToggle(
+ "Crash reports",
+ description: "Automatically send crash reports to help fix bugs",
+ isOn: .constant(true)
+ )
+ }
+ }
+ }
+}
+
+struct AdvancedSettingsView: View {
+ var body: some View {
+ VStack(alignment: .leading, spacing: 20) {
+ SettingsGroup("Debug") {
+ HStack {
+ VStack(alignment: .leading) {
+ Text("Developer mode")
+ .body(.medium)
+ Text("Enable advanced debugging features")
+ .bodySmall(color: SpacedriveColors.Text.secondary)
+ }
+
+ Spacer()
+
+ SDButton("Enable", style: .secondary, size: .medium) {
+ // Enable developer mode
+ }
+ }
+
+ SDButton("Reset all settings", style: .destructive, size: .medium) {
+ // Reset settings
+ }
+ }
+ }
+ }
+}
+
+struct AboutSettingsView: View {
+ var body: some View {
+ VStack(alignment: .leading, spacing: 20) {
+ SettingsGroup("Version") {
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Spacedrive")
+ .h4(.semibold)
+ Text("Version 0.1.0")
+ .body()
+ Text("Build 1")
+ .bodySmall(color: SpacedriveColors.Text.secondary)
+ }
+ }
+
+ SettingsGroup("Links") {
+ VStack(spacing: 12) {
+ SDButton("Visit Website", style: .secondary, size: .medium, icon: "safari") {
+ // Open website
+ }
+
+ SDButton("View on GitHub", style: .secondary, size: .medium, icon: "chevron.left.forwardslash.chevron.right") {
+ // Open GitHub
+ }
+
+ SDButton("Report Issue", style: .secondary, size: .medium, icon: "exclamationmark.bubble") {
+ // Report issue
+ }
+ }
+ }
+ }
+ }
+}
+
+// MARK: - Settings Components
+
+struct SettingsGroup: View {
+ let title: String
+ let content: Content
+
+ init(_ title: String, @ViewBuilder content: () -> Content) {
+ self.title = title
+ self.content = content()
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ Text(title)
+ .h5(.semibold)
+ .foregroundColor(SpacedriveColors.Text.primary)
+
+ SDCard(style: .bordered, padding: EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)) {
+ content
+ }
+ }
+ }
+}
+
+struct SettingsToggle: View {
+ let title: String
+ let description: String
+ @Binding var isOn: Bool
+
+ init(_ title: String, description: String, isOn: Binding) {
+ self.title = title
+ self.description = description
+ _isOn = isOn
+ }
+
+ var body: some View {
+ HStack {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(title)
+ .body(.medium)
+
+ Text(description)
+ .bodySmall(color: SpacedriveColors.Text.secondary)
+ }
+
+ Spacer()
+
+ Toggle("", isOn: $isOn)
+ .toggleStyle(SwitchToggleStyle())
+ }
+ }
+}
+
+#Preview {
+ SettingsContentView(selectedSection: .general)
+ .environmentObject(SharedAppState.shared)
+ .frame(width: 500, height: 600)
+}
diff --git a/apps/macos2/Spacedrive/Spacedrive/Windows/Settings/Views/SettingsView.swift b/apps/macos2/Spacedrive/Spacedrive/Windows/Settings/Views/SettingsView.swift
new file mode 100644
index 000000000..47be154cc
--- /dev/null
+++ b/apps/macos2/Spacedrive/Spacedrive/Windows/Settings/Views/SettingsView.swift
@@ -0,0 +1,73 @@
+import SwiftUI
+
+/// Settings Window - Main settings interface with left navigation
+struct SettingsView: View {
+ @EnvironmentObject var appState: SharedAppState
+ @State private var selectedSection: SettingsSection = .general
+
+ var body: some View {
+ WindowContainer {
+ HSplitView {
+ // Left Navigation Sidebar
+ SettingsNavigationView(selectedSection: $selectedSection)
+ .frame(minWidth: 220, maxWidth: 280)
+
+ // Main Content Area
+ SettingsContentView(selectedSection: selectedSection)
+ .frame(minWidth: 450, maxWidth: .infinity)
+ }
+ }
+ }
+}
+
+/// Settings sections for navigation
+enum SettingsSection: String, CaseIterable, Identifiable {
+ case general = "General"
+ case daemon = "Daemon"
+ case appearance = "Appearance"
+ case privacy = "Privacy"
+ case advanced = "Advanced"
+ case about = "About"
+
+ var id: String { rawValue }
+
+ var icon: String {
+ switch self {
+ case .general:
+ return "gear"
+ case .daemon:
+ return "server.rack"
+ case .appearance:
+ return "paintbrush"
+ case .privacy:
+ return "hand.raised"
+ case .advanced:
+ return "wrench.and.screwdriver"
+ case .about:
+ return "info.circle"
+ }
+ }
+
+ var description: String {
+ switch self {
+ case .general:
+ return "General application settings"
+ case .daemon:
+ return "Daemon connection and configuration"
+ case .appearance:
+ return "Theme and visual preferences"
+ case .privacy:
+ return "Privacy and security settings"
+ case .advanced:
+ return "Advanced configuration options"
+ case .about:
+ return "About Spacedrive"
+ }
+ }
+}
+
+#Preview {
+ SettingsView()
+ .environmentObject(SharedAppState.shared)
+ .frame(width: 800, height: 600)
+}