From 60803ab7ec2311e210fb5f45b00a9a01ef93b0a9 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Thu, 22 Jan 2026 11:22:18 -0800 Subject: [PATCH] refactor(mobile): remove unused navigation components and enhance search functionality - Deleted obsolete navigation components including DrawerNavigator, RootNavigator, and TabNavigator to streamline the mobile app structure. - Introduced the GlassSearchBar component for improved search capabilities across screens. - Updated BrowseScreen to integrate the new search bar and enhance user experience with animated scrolling and dynamic content display. - Adjusted layout and styling for better responsiveness and visual appeal. - Added phosphor-react-native dependency for icon support in the GlassSearchBar. --- .../EXPLORER-TABS-IMPLEMENTATION-SUMMARY.md | 309 ------------------ apps/mobile/package.json | 1 + apps/mobile/src/App.tsx | 35 -- .../src/app/(drawer)/(tabs)/_layout.tsx | 9 - apps/mobile/src/components/GlassSearchBar.tsx | 14 +- .../mobile/src/navigation/DrawerNavigator.tsx | 32 -- apps/mobile/src/navigation/RootNavigator.tsx | 38 --- apps/mobile/src/navigation/TabNavigator.tsx | 105 ------ apps/mobile/src/navigation/index.ts | 2 - .../src/navigation/stacks/BrowseStack.tsx | 16 - .../src/navigation/stacks/NetworkStack.tsx | 14 - .../src/navigation/stacks/OverviewStack.tsx | 14 - .../src/navigation/stacks/SettingsStack.tsx | 14 - apps/mobile/src/navigation/types.ts | 58 ---- .../src/screens/browse/BrowseScreen.tsx | 120 +++++-- .../screens/browse/components/SpaceGroup.tsx | 71 ++++ .../screens/browse/components/SpaceItem.tsx | 224 +++++++++++++ .../src/screens/browse/components/index.ts | 2 + .../src/screens/overview/OverviewScreen.tsx | 4 +- apps/tauri/DAEMON_SETUP.md | 227 ------------- apps/tauri/DRAG_DROP_README.md | 237 -------------- apps/tauri/V1_TAURI_CONFIG_REFERENCE.md | 299 ----------------- bun.lockb | Bin 1047914 -> 1048402 bytes 23 files changed, 403 insertions(+), 1442 deletions(-) delete mode 100644 .tasks/EXPLORER-TABS-IMPLEMENTATION-SUMMARY.md delete mode 100644 apps/mobile/src/App.tsx delete mode 100644 apps/mobile/src/navigation/DrawerNavigator.tsx delete mode 100644 apps/mobile/src/navigation/RootNavigator.tsx delete mode 100644 apps/mobile/src/navigation/TabNavigator.tsx delete mode 100644 apps/mobile/src/navigation/index.ts delete mode 100644 apps/mobile/src/navigation/stacks/BrowseStack.tsx delete mode 100644 apps/mobile/src/navigation/stacks/NetworkStack.tsx delete mode 100644 apps/mobile/src/navigation/stacks/OverviewStack.tsx delete mode 100644 apps/mobile/src/navigation/stacks/SettingsStack.tsx delete mode 100644 apps/mobile/src/navigation/types.ts create mode 100644 apps/mobile/src/screens/browse/components/SpaceGroup.tsx create mode 100644 apps/mobile/src/screens/browse/components/SpaceItem.tsx delete mode 100644 apps/tauri/DAEMON_SETUP.md delete mode 100644 apps/tauri/DRAG_DROP_README.md delete mode 100644 apps/tauri/V1_TAURI_CONFIG_REFERENCE.md diff --git a/.tasks/EXPLORER-TABS-IMPLEMENTATION-SUMMARY.md b/.tasks/EXPLORER-TABS-IMPLEMENTATION-SUMMARY.md deleted file mode 100644 index dc5f894cb..000000000 --- a/.tasks/EXPLORER-TABS-IMPLEMENTATION-SUMMARY.md +++ /dev/null @@ -1,309 +0,0 @@ -# Explorer Tabs Implementation Summary - -**Status:** Phase 1 Complete (MVP) -**Date:** December 24, 2025 -**Branch:** `cursor/explorer-tab-interface-implementation-dec8` - ---- - -## Overview - -Successfully implemented browser-like tabs for the Spacedrive Explorer, allowing users to browse multiple locations simultaneously. The implementation follows the design document's Phase 1 (MVP) approach with simplified router management. - ---- - -## What Was Implemented - -### 1. Core Tab Infrastructure - -**Created Files:** - -- `packages/interface/src/components/TabManager/TabManagerContext.tsx` - - Core tab state management - - Tab creation, deletion, and switching logic - - Scroll state persistence per tab - - Single shared router for all tabs (simplified approach) - -- `packages/interface/src/components/TabManager/TabBar.tsx` - - Visual tab bar component - - Tab titles and close buttons - - Active tab indicator with framer-motion animations - - New tab button (+) - -- `packages/interface/src/components/TabManager/TabView.tsx` - - Tab content rendering component (prepared for future multi-router approach) - -- `packages/interface/src/components/TabManager/useTabManager.ts` - - Type-safe hook for accessing tab manager context - -- `packages/interface/src/components/TabManager/TabNavigationSync.tsx` - - Syncs router location with active tab's saved path - - Saves current location when navigating within a tab - - Restores saved location when switching to a different tab - -- `packages/interface/src/components/TabManager/TabKeyboardHandler.tsx` - - Keyboard shortcut handlers for tab operations - - Uses existing keybind system infrastructure - -- `packages/interface/src/components/TabManager/index.ts` - - Public API exports - -### 2. Modified Files - -**Router Configuration:** - -- `packages/interface/src/router.tsx` - - Extracted route configuration as `explorerRoutes` array - - Kept `createExplorerRouter()` for backward compatibility - -**Main Explorer:** - -- `packages/interface/src/Explorer.tsx` - - Wrapped app in `TabManagerProvider` - - Added `TabKeyboardHandler` for global shortcuts - - Added `TabBar` component below TopBar - - Adjusted layout to flex-column for proper tab bar positioning - - Added `TabNavigationSync` inside router context - -**Context Providers:** - -- `packages/interface/src/components/Explorer/context.tsx` - - Added optional `isActiveTab` prop (for future multi-tab isolation) - -- `packages/interface/src/components/Explorer/SelectionContext.tsx` - - Added optional `isActiveTab` prop - - Platform sync only active for active tab (prevents conflicts) - - Menu updates only for active tab - -**Keybind Registry:** - -- `packages/interface/src/util/keybinds/registry.ts` - - **Removed:** `explorer.openInNewTab` (conflicted with global.newTab) - - **Added:** Tab-related keybinds: - - `global.newTab` (Cmd+T) - Create new tab - - `global.closeTab` (Cmd+W) - Close active tab - - `global.nextTab` (Cmd+Shift+]) - Switch to next tab - - `global.previousTab` (Cmd+Shift+[) - Switch to previous tab - - `global.selectTab1-9` (Cmd+1-9) - Jump to specific tab - ---- - -## Key Features - -### ✅ Implemented - -1. **Tab Creation** - - New tabs start at Overview (/) - - Keyboard shortcut: Cmd+T - - Click + button in tab bar - -2. **Tab Closing** - - Close via × button on tab - - Keyboard shortcut: Cmd+W - - Last tab cannot be closed (prevents empty state) - -3. **Tab Switching** - - Click tab to switch - - Keyboard: Cmd+Shift+[ / ] for prev/next - - Keyboard: Cmd+1-9 to jump to specific tab - -4. **Navigation Persistence** - - Each tab remembers its last location - - Switching tabs restores saved location - - Independent navigation history per tab (via shared router) - -5. **Visual Design** - - Tab bar positioned below TopBar - - Active tab indicator with smooth animation - - Semantic colors (bg-sidebar, text-sidebar-ink) - - Close button shows on hover - -6. **Selection Isolation** - - Each tab maintains independent file selection - - Only active tab syncs to platform API - - Menu items update based on active tab's selection - ---- - -## Architecture Decisions - -### Simplified Router Approach - -**Design Doc:** Each tab has its own router (browser router for active, memory router for inactive) - -**Implementation:** Single shared browser router with path synchronization - -**Rationale:** -- React Router v6's RouterProvider doesn't support dynamic router swapping -- Simpler state management for MVP -- Navigation still works independently per tab via saved paths -- Can be enhanced to multi-router in future if needed - -### State Management - -**Tab State:** -```typescript -interface Tab { - id: string; // Unique identifier - title: string; // Display name - savedPath: string; // Last location (e.g., "/explorer?path=...") - icon: string | null; // Future: location icon - isPinned: boolean; // Future: pinned tabs - lastActive: number; // Timestamp for LRU -} -``` - -**Scroll State:** Prepared but not yet implemented (Phase 4 feature) - -### Context Isolation - -Prepared for full isolation with `isActiveTab` prop on contexts: -- `ExplorerProvider({ isActiveTab })` -- `SelectionProvider({ isActiveTab })` - -Currently all tabs use the same context instances (shared state), but platform sync is filtered by active tab to prevent conflicts. - ---- - -## Testing Status - -**Linting:** ✅ All files pass with no errors - -**Manual Testing Needed:** -- [ ] Create multiple tabs -- [ ] Switch between tabs -- [ ] Navigate within tabs -- [ ] Close tabs -- [ ] Keyboard shortcuts (Cmd+T, Cmd+W, Cmd+Shift+[/]) -- [ ] Tab switching remembers location -- [ ] File selection isolation -- [ ] Last tab cannot close - ---- - -## Known Limitations (To Be Addressed in Future Phases) - -1. **Scroll Position:** Not yet preserved when switching tabs -2. **View Mode:** Shared across tabs (not per-tab yet) -3. **Router Isolation:** Shared router (not per-tab router instances) -4. **Tab Titles:** Static "Overview" (should update based on location) -5. **Drag-Drop:** No drag-to-reorder tabs yet -6. **Persistence:** Tab state not saved on app restart -7. **Performance:** No lazy unmounting for inactive tabs - ---- - -## Next Steps (Future Phases) - -### Phase 2: Enhanced State Isolation -- Implement per-tab view mode and sort preferences -- Add dynamic tab titles based on location -- Per-tab scroll position preservation - -### Phase 3: Performance Optimization -- Lazy mounting for inactive tabs -- Query client GC for inactive tabs -- Memory budget management - -### Phase 4: Persistence -- Save/restore tab state on app restart -- Handle stale tabs (deleted locations) - -### Phase 5: Polish -- Tab drag-to-reorder -- Tab context menu -- Cross-tab file drag-drop -- "Reopen Closed Tab" (Cmd+Shift+T) -- Tab close animations - ---- - -## Code Quality - -- ✅ No linter errors -- ✅ Follows CLAUDE.md guidelines (semantic colors, no React.FC, function components) -- ✅ Type-safe (full TypeScript) -- ✅ Documented with inline comments -- ✅ Follows existing patterns (TabBar similar to Inspector/Tabs.tsx) -- ✅ Uses existing infrastructure (useKeybind hook, framer-motion) - ---- - -## Files Changed Summary - -**New Files (7):** -- TabManager/TabManagerContext.tsx -- TabManager/TabBar.tsx -- TabManager/TabView.tsx -- TabManager/useTabManager.ts -- TabManager/TabNavigationSync.tsx -- TabManager/TabKeyboardHandler.tsx -- TabManager/index.ts - -**Modified Files (5):** -- Explorer.tsx -- router.tsx -- components/Explorer/context.tsx -- components/Explorer/SelectionContext.tsx -- util/keybinds/registry.ts - -**Total Lines Added:** ~600 lines - ---- - -## Success Criteria (Phase 1) - -✅ User can open multiple tabs (Cmd+T) -✅ User can close tabs (Cmd+W) -✅ User can switch tabs (Cmd+Shift+[/]) -✅ Each tab maintains independent navigation -✅ Tab switching updates URL correctly -✅ No visual glitches during switching -✅ Last tab cannot be closed -✅ Keybinds work like browser tabs -✅ No memory leaks or crashes -✅ Code passes linting - ---- - -## Risk Assessment - -**Low Risk:** -- Well-isolated component (doesn't affect core Explorer logic) -- Uses existing infrastructure (keybinds, framer-motion) -- Can be disabled by removing TabManagerProvider wrapper - -**Rollback Plan:** -If issues arise, simply remove: -1. TabManagerProvider wrapper from Explorer.tsx -2. TabBar import and usage -3. Restore original router.tsx structure - -All other changes are backward-compatible. - ---- - -## Performance Notes - -**Current Implementation:** -- Single router (no per-tab overhead) -- All tabs loaded in memory (no lazy unmounting yet) -- Estimated memory per tab: ~5-10KB (just state, no rendered DOM) - -**Future Optimization Targets:** -- Phase 3: Add lazy unmounting for 10+ tabs -- Phase 3: QueryClient GC for inactive tabs - ---- - -## Documentation - -See design document at `/workspace/.tasks/EXPLORER-TABS-DESIGN.md` for full architectural details and future roadmap. - ---- - -## Conclusion - -Phase 1 (MVP) successfully implements core tab functionality with a simplified architecture suitable for immediate use. The foundation is in place for future enhancements including full state isolation, performance optimization, and session persistence. - -The implementation is production-ready for testing with the caveat that scroll position and view preferences are shared across tabs (to be addressed in Phase 2). diff --git a/apps/mobile/package.json b/apps/mobile/package.json index c19193a10..1ffe3e9b8 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -50,6 +50,7 @@ "expo-splash-screen": "~31.0.11", "expo-status-bar": "~3.0.8", "nativewind": "^4.1.23", + "phosphor-react-native": "^2.1.0", "react": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx deleted file mode 100644 index a22a60824..000000000 --- a/apps/mobile/src/App.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useState } from "react"; -import { View, ViewProps } from "react-native"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; -import { SafeAreaProvider } from "react-native-safe-area-context"; -import { StatusBar } from "expo-status-bar"; -import { SpacedriveProvider } from "./client"; -import { RootNavigator } from "./navigation"; -import { AppResetContext } from "./contexts"; -import "./global.css"; - -// Type workaround for GestureHandlerRootView children prop -const GestureRoot = GestureHandlerRootView as React.ComponentType< - ViewProps & { children?: React.ReactNode } ->; - -export default function App() { - const [resetKey, setResetKey] = useState(0); - - const resetApp = () => { - setResetKey((prev) => prev + 1); - }; - - return ( - - - - - - - - - - - ); -} diff --git a/apps/mobile/src/app/(drawer)/(tabs)/_layout.tsx b/apps/mobile/src/app/(drawer)/(tabs)/_layout.tsx index 9b37ac6d2..09a6b5db9 100644 --- a/apps/mobile/src/app/(drawer)/(tabs)/_layout.tsx +++ b/apps/mobile/src/app/(drawer)/(tabs)/_layout.tsx @@ -30,15 +30,6 @@ export default function TabLayout() { )} - - - {Platform.OS === 'ios' ? ( - - ) : ( - - )} - - {Platform.OS === 'ios' ? ( diff --git a/apps/mobile/src/components/GlassSearchBar.tsx b/apps/mobile/src/components/GlassSearchBar.tsx index 7693d2dc8..6d5db2cf0 100644 --- a/apps/mobile/src/components/GlassSearchBar.tsx +++ b/apps/mobile/src/components/GlassSearchBar.tsx @@ -1,11 +1,12 @@ import React, { forwardRef } from "react"; -import { TextInput, View, Pressable, Platform, type TextInputProps } from "react-native"; +import { TextInput, View, Pressable, type TextInputProps } from "react-native"; import { BlurView } from "expo-blur"; import { LiquidGlassView, isLiquidGlassSupported, } from "@callstack/liquid-glass"; import { useRouter } from "expo-router"; +import { MagnifyingGlass } from "phosphor-react-native"; interface GlassSearchBarProps extends Omit { onPress?: () => void; @@ -26,17 +27,14 @@ export const GlassSearchBar = forwardRef( }; const content = ( - - - - - + + diff --git a/apps/mobile/src/navigation/DrawerNavigator.tsx b/apps/mobile/src/navigation/DrawerNavigator.tsx deleted file mode 100644 index 80219e8af..000000000 --- a/apps/mobile/src/navigation/DrawerNavigator.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import { - createDrawerNavigator, - DrawerContentComponentProps, -} from "@react-navigation/drawer"; -import { TabNavigator } from "./TabNavigator"; -import { SidebarContent } from "../components/sidebar/SidebarContent"; -import type { DrawerParamList } from "./types"; - -const Drawer = createDrawerNavigator(); - -export function DrawerNavigator() { - return ( - ( - - )} - screenOptions={{ - headerShown: false, - drawerType: "slide", - drawerStyle: { - width: 280, - backgroundColor: "hsl(235, 15%, 16%)", - }, - overlayColor: "rgba(0, 0, 0, 0.5)", - swipeEdgeWidth: 50, - }} - > - - - ); -} diff --git a/apps/mobile/src/navigation/RootNavigator.tsx b/apps/mobile/src/navigation/RootNavigator.tsx deleted file mode 100644 index a250bcc15..000000000 --- a/apps/mobile/src/navigation/RootNavigator.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import { NavigationContainer, DefaultTheme } from "@react-navigation/native"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import { DrawerNavigator } from "./DrawerNavigator"; -import type { RootStackParamList } from "./types"; - -const Stack = createNativeStackNavigator(); - -// Dark theme for navigation -const SpacedriveTheme = { - ...DefaultTheme, - dark: true, - colors: { - ...DefaultTheme.colors, - primary: "hsl(208, 100%, 57%)", - background: "hsl(235, 15%, 13%)", - card: "hsl(235, 10%, 6%)", - text: "hsl(235, 0%, 100%)", - border: "hsl(235, 15%, 23%)", - notification: "hsl(208, 100%, 57%)", - }, -}; - -export function RootNavigator() { - return ( - - - - {/* Add Onboarding and Search screens later */} - - - ); -} diff --git a/apps/mobile/src/navigation/TabNavigator.tsx b/apps/mobile/src/navigation/TabNavigator.tsx deleted file mode 100644 index 4045f53f4..000000000 --- a/apps/mobile/src/navigation/TabNavigator.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from "react"; -import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; -import { View, Text, Platform } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { OverviewStack } from "./stacks/OverviewStack"; -import { BrowseStack } from "./stacks/BrowseStack"; -import { NetworkStack } from "./stacks/NetworkStack"; -import { SettingsStack } from "./stacks/SettingsStack"; -import type { TabParamList } from "./types"; -import { useJobs } from "../hooks/useJobs"; - -const Tab = createBottomTabNavigator(); - -// Simple icon components (replace with phosphor-react-native later) -const TabIcon = ({ name, focused, badge }: { name: string; focused: boolean; badge?: number }) => ( - - - - {badge !== undefined && badge > 0 && ( - - - {badge > 99 ? '99+' : badge} - - - )} - - - {name} - - -); - -function OverviewTabIcon({ focused }: { focused: boolean }) { - const { activeJobCount } = useJobs(); - return ; -} - -export function TabNavigator() { - const insets = useSafeAreaInsets(); - const tabBarHeight = Platform.OS === "ios" ? 80 : 60; - - return ( - - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - - ); -} diff --git a/apps/mobile/src/navigation/index.ts b/apps/mobile/src/navigation/index.ts deleted file mode 100644 index 240af835a..000000000 --- a/apps/mobile/src/navigation/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { RootNavigator } from "./RootNavigator"; -export type * from "./types"; diff --git a/apps/mobile/src/navigation/stacks/BrowseStack.tsx b/apps/mobile/src/navigation/stacks/BrowseStack.tsx deleted file mode 100644 index 2a4d0ebe4..000000000 --- a/apps/mobile/src/navigation/stacks/BrowseStack.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import { BrowseScreen } from "../../screens/browse/BrowseScreen"; -import { ExplorerScreen } from "../../screens/explorer/ExplorerScreen"; -import type { BrowseStackParamList } from "../types"; - -const Stack = createNativeStackNavigator(); - -export function BrowseStack() { - return ( - - - - - ); -} diff --git a/apps/mobile/src/navigation/stacks/NetworkStack.tsx b/apps/mobile/src/navigation/stacks/NetworkStack.tsx deleted file mode 100644 index 9db37374e..000000000 --- a/apps/mobile/src/navigation/stacks/NetworkStack.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import { NetworkScreen } from "../../screens/network/NetworkScreen"; -import type { NetworkStackParamList } from "../types"; - -const Stack = createNativeStackNavigator(); - -export function NetworkStack() { - return ( - - - - ); -} diff --git a/apps/mobile/src/navigation/stacks/OverviewStack.tsx b/apps/mobile/src/navigation/stacks/OverviewStack.tsx deleted file mode 100644 index 5737ed02f..000000000 --- a/apps/mobile/src/navigation/stacks/OverviewStack.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import { OverviewScreen } from "../../screens/overview/OverviewScreen"; -import type { OverviewStackParamList } from "../types"; - -const Stack = createNativeStackNavigator(); - -export function OverviewStack() { - return ( - - - - ); -} diff --git a/apps/mobile/src/navigation/stacks/SettingsStack.tsx b/apps/mobile/src/navigation/stacks/SettingsStack.tsx deleted file mode 100644 index d8c964503..000000000 --- a/apps/mobile/src/navigation/stacks/SettingsStack.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import { SettingsScreen } from "../../screens/settings/SettingsScreen"; -import type { SettingsStackParamList } from "../types"; - -const Stack = createNativeStackNavigator(); - -export function SettingsStack() { - return ( - - - - ); -} diff --git a/apps/mobile/src/navigation/types.ts b/apps/mobile/src/navigation/types.ts deleted file mode 100644 index 249a6c9a5..000000000 --- a/apps/mobile/src/navigation/types.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { NavigatorScreenParams } from "@react-navigation/native"; - -// Root stack contains main app and onboarding -export type RootStackParamList = { - Main: NavigatorScreenParams; - Onboarding: undefined; - Search: undefined; -}; - -// Drawer contains the sidebar and tab navigator -export type DrawerParamList = { - Tabs: NavigatorScreenParams; -}; - -// Bottom tabs -export type TabParamList = { - OverviewTab: NavigatorScreenParams; - BrowseTab: NavigatorScreenParams; - NetworkTab: NavigatorScreenParams; - SettingsTab: NavigatorScreenParams; -}; - -// Overview stack -export type OverviewStackParamList = { - Overview: undefined; -}; - -// Browse stack -export type BrowseStackParamList = { - BrowseHome: undefined; - Explorer: - | { type: "path"; path: string } // JSON.stringify(SdPath) - | { type: "view"; view: string; id?: string }; // Virtual views -}; - -// Network stack -export type NetworkStackParamList = { - Network: undefined; - Peers: undefined; - Pairing: undefined; -}; - -// Settings stack -export type SettingsStackParamList = { - SettingsHome: undefined; - GeneralSettings: undefined; - LibrarySettings: undefined; - AppearanceSettings: undefined; - PrivacySettings: undefined; - About: undefined; -}; - -// Utility types for typed navigation -declare global { - namespace ReactNavigation { - interface RootParamList extends RootStackParamList {} - } -} diff --git a/apps/mobile/src/screens/browse/BrowseScreen.tsx b/apps/mobile/src/screens/browse/BrowseScreen.tsx index 73965d7e3..2d736a2af 100644 --- a/apps/mobile/src/screens/browse/BrowseScreen.tsx +++ b/apps/mobile/src/screens/browse/BrowseScreen.tsx @@ -1,17 +1,27 @@ -import React, { useState, useRef, useCallback } from "react"; +import { useState, useRef, useCallback } from "react"; import { View, Text, ScrollView, Dimensions, - NativeScrollEvent, - NativeSyntheticEvent, + type NativeScrollEvent, + type NativeSyntheticEvent, } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useSafeAreaInsets, type EdgeInsets } from "react-native-safe-area-context"; +import Animated, { + useSharedValue, + useAnimatedScrollHandler, + useAnimatedStyle, + interpolate, + Extrapolation, +} from "react-native-reanimated"; import { useNormalizedQuery } from "../../client"; -import { DevicesGroup, LocationsGroup, VolumesGroup } from "./components"; import { PageIndicator } from "../../components/PageIndicator"; +import { GlassSearchBar } from "../../components/GlassSearchBar"; import sharedColors from "@sd/ui/style/colors"; +import type { SpaceItem, SpaceGroup } from "@sd/ts-client"; +import { SpaceItem as SpaceItemComponent, SpaceGroupComponent } from "./components"; +import { SettingsGroup } from "../../components/primitive"; interface Space { id: string; @@ -21,40 +31,100 @@ interface Space { const SCREEN_WIDTH = Dimensions.get("window").width; -function SpaceContent({ space, insets }: { space: Space; insets: any }) { +function SpaceContent({ + space, + insets +}: { + space: Space; + insets: EdgeInsets; +}) { + const scrollY = useSharedValue(0); + + const scrollHandler = useAnimatedScrollHandler({ + onScroll: (event) => { + scrollY.value = event.contentOffset.y; + }, + }); + + // Fetch space layout + const { data: layout } = useNormalizedQuery({ + query: "spaces.get_layout", + input: { space_id: space.id }, + resourceType: "space_layout", + resourceId: space.id, + enabled: !!space.id, + }); + + // Space name scale on overscroll (anchored left) + const spaceNameScale = useAnimatedStyle(() => { + const scale = interpolate( + scrollY.value, + [-200, 0], + [1.3, 1], + Extrapolation.CLAMP + ); + + return { + transform: [{ scale }], + transformOrigin: 'left center', + }; + }); + + // Filter out Overview items (mobile doesn't show Overview in browse tab) + const spaceItems = (layout?.space_items || []).filter( + (item) => item.item_type !== "Overview" + ); + const groups = layout?.groups || []; + return ( - {/* Header */} - + - {space.name} + + {space.name} + - - Your libraries and spaces - - {/* Locations */} - + {/* Search Bar */} + + + - {/* Devices */} - + {/* Space Items (pinned shortcuts) */} + {spaceItems.length > 0 && ( + + + {spaceItems.map((item) => ( + + ))} + + + )} - {/* Volumes */} - - + {/* Groups */} + {groups.map(({ group, items }) => ( + + ))} + ); } @@ -117,7 +187,11 @@ export function BrowseScreen() { decelerationRate="fast" > {spacesList.map((space) => ( - + ))} diff --git a/apps/mobile/src/screens/browse/components/SpaceGroup.tsx b/apps/mobile/src/screens/browse/components/SpaceGroup.tsx new file mode 100644 index 000000000..cc9371308 --- /dev/null +++ b/apps/mobile/src/screens/browse/components/SpaceGroup.tsx @@ -0,0 +1,71 @@ +import React, { useState } from "react"; +import { View, Text, Pressable } from "react-native"; +import type { SpaceGroup, SpaceItem } from "@sd/ts-client"; +import { SettingsGroup } from "../../../components/primitive"; +import { SpaceItem as SpaceItemComponent } from "./SpaceItem"; +import { DevicesGroup } from "./DevicesGroup"; +import { LocationsGroup } from "./LocationsGroup"; +import { VolumesGroup } from "./VolumesGroup"; +import { CaretDown, CaretRight } from "phosphor-react-native"; + +interface SpaceGroupProps { + group: SpaceGroup; + items: SpaceItem[]; +} + +export function SpaceGroupComponent({ group, items }: SpaceGroupProps) { + const [isCollapsed, setIsCollapsed] = useState(group.is_collapsed ?? false); + + const handleToggle = () => { + setIsCollapsed(!isCollapsed); + }; + + // System groups - use existing components + if (group.group_type === "Devices") { + return ; + } + + if (group.group_type === "Locations") { + return ; + } + + if (group.group_type === "Volumes") { + return ; + } + + if (group.group_type === "Tags") { + // Tags group not implemented yet + return null; + } + + // Custom/QuickAccess groups - render items + return ( + + {/* Group Header */} + + + {group.name} + + {isCollapsed ? ( + + ) : ( + + )} + + + {/* Group Items */} + {!isCollapsed && items.length > 0 && ( + + {items + .filter((item) => item.item_type !== "Overview") + .map((item) => ( + + ))} + + )} + + ); +} diff --git a/apps/mobile/src/screens/browse/components/SpaceItem.tsx b/apps/mobile/src/screens/browse/components/SpaceItem.tsx new file mode 100644 index 000000000..c4bca77a2 --- /dev/null +++ b/apps/mobile/src/screens/browse/components/SpaceItem.tsx @@ -0,0 +1,224 @@ +import React from "react"; +import { Image } from "react-native"; +import { useRouter } from "expo-router"; +import type { SpaceItem as SpaceItemType, ItemType, SdPath } from "@sd/ts-client"; +import { SettingsLink } from "../../../components/primitive"; +import FolderIcon from "@sd/assets/icons/Folder.png"; +import { MagnifyingGlass, Clock, Heart, Folders, HardDrive, Tag } from "phosphor-react-native"; + +// Type guards +function isOverviewItem(t: ItemType): t is "Overview" { + return t === "Overview"; +} + +function isRecentsItem(t: ItemType): t is "Recents" { + return t === "Recents"; +} + +function isFavoritesItem(t: ItemType): t is "Favorites" { + return t === "Favorites"; +} + +function isFileKindsItem(t: ItemType): t is "FileKinds" { + return t === "FileKinds"; +} + +function isLocationItem(t: ItemType): t is { Location: { location_id: string } } { + return typeof t === "object" && "Location" in t; +} + +function isVolumeItem(t: ItemType): t is { Volume: { volume_id: string } } { + return typeof t === "object" && "Volume" in t; +} + +function isTagItem(t: ItemType): t is { Tag: { tag_id: string } } { + return typeof t === "object" && "Tag" in t; +} + +function isPathItem(t: ItemType): t is { Path: { sd_path: SdPath } } { + return typeof t === "object" && "Path" in t; +} + +function isRawLocation(item: SpaceItemType | Record): boolean { + return "name" in item && "sd_path" in item && !("item_type" in item); +} + +// Get icon for item type +function getItemIcon(itemType: ItemType): React.ReactNode { + if (isOverviewItem(itemType)) { + return ; + } + if (isRecentsItem(itemType)) { + return ; + } + if (isFavoritesItem(itemType)) { + return ; + } + if (isFileKindsItem(itemType)) { + return ; + } + if (isLocationItem(itemType) || isPathItem(itemType)) { + return ( + + ); + } + if (isVolumeItem(itemType)) { + return ; + } + if (isTagItem(itemType)) { + return ; + } + return ( + + ); +} + +// Get label for item type +function getItemLabel(itemType: ItemType, resolvedFile?: any): string { + if (isOverviewItem(itemType)) return "Overview"; + if (isRecentsItem(itemType)) return "Recents"; + if (isFavoritesItem(itemType)) return "Favorites"; + if (isFileKindsItem(itemType)) return "File Kinds"; + if (isLocationItem(itemType)) return itemType.Location.name || "Unnamed Location"; + if (isVolumeItem(itemType)) return itemType.Volume.name || "Unnamed Volume"; + if (isTagItem(itemType)) return itemType.Tag.name || "Unnamed Tag"; + if (isPathItem(itemType)) { + if (resolvedFile?.name) return resolvedFile.name; + const sdPath = itemType.Path.sd_path; + if (typeof sdPath === "object" && "Physical" in sdPath) { + const parts = (sdPath as { Physical: { path: string } }).Physical.path.split("/"); + return parts[parts.length - 1] || "Path"; + } + return "Path"; + } + return "Unknown"; +} + +// Get navigation params for item (mobile uses different format) +function getItemNavigation(itemType: ItemType, itemSdPath?: SdPath): { pathname: string; params?: any } | null { + if (isOverviewItem(itemType)) { + return { pathname: "/" }; + } + if (isRecentsItem(itemType)) { + return { pathname: "/recents" }; + } + if (isFavoritesItem(itemType)) { + return { pathname: "/favorites" }; + } + if (isFileKindsItem(itemType)) { + return { pathname: "/file-kinds" }; + } + + if (isLocationItem(itemType)) { + if (itemSdPath) { + return { + pathname: "/explorer", + params: { + type: "path", + path: JSON.stringify(itemSdPath), + }, + }; + } + return null; + } + + if (isVolumeItem(itemType)) { + // Volume navigation handled separately + return null; + } + + if (isTagItem(itemType)) { + return { + pathname: "/explorer", + params: { + type: "view", + view: "tag", + id: itemType.Tag.tag_id, + }, + }; + } + + if (isPathItem(itemType)) { + return { + pathname: "/explorer", + params: { + type: "path", + path: JSON.stringify(itemType.Path.sd_path), + }, + }; + } + + return null; +} + +interface SpaceItemProps { + item: SpaceItemType; +} + +export function SpaceItem({ item }: SpaceItemProps) { + const router = useRouter(); + + // Handle raw location (legacy format) + if (isRawLocation(item)) { + const rawItem = item as { name?: string; sd_path?: SdPath }; + const label = rawItem.name || "Unnamed Location"; + + return ( + + } + label={label} + onPress={() => { + if (rawItem.sd_path) { + router.push({ + pathname: "/explorer", + params: { + type: "path", + path: JSON.stringify(rawItem.sd_path), + }, + }); + } + }} + /> + ); + } + + // Normal space item + const itemType = item.item_type; + const icon = getItemIcon(itemType); + const label = getItemLabel(itemType, item.resolved_file); + const navigation = getItemNavigation(itemType, item.sd_path); + + // Handle volume items specially + if (isVolumeItem(itemType)) { + // Volumes are handled by VolumesGroup component + return null; + } + + if (!navigation) { + return null; + } + + return ( + { + router.push(navigation); + }} + /> + ); +} diff --git a/apps/mobile/src/screens/browse/components/index.ts b/apps/mobile/src/screens/browse/components/index.ts index 1c092089f..f0749a3ed 100644 --- a/apps/mobile/src/screens/browse/components/index.ts +++ b/apps/mobile/src/screens/browse/components/index.ts @@ -1,3 +1,5 @@ export { DevicesGroup } from "./DevicesGroup"; export { LocationsGroup } from "./LocationsGroup"; export { VolumesGroup } from "./VolumesGroup"; +export { SpaceItem } from "./SpaceItem"; +export { SpaceGroupComponent } from "./SpaceGroup"; diff --git a/apps/mobile/src/screens/overview/OverviewScreen.tsx b/apps/mobile/src/screens/overview/OverviewScreen.tsx index d6591ae84..4df620f34 100644 --- a/apps/mobile/src/screens/overview/OverviewScreen.tsx +++ b/apps/mobile/src/screens/overview/OverviewScreen.tsx @@ -306,7 +306,7 @@ export function OverviewScreen() { heroAnimatedStyle, ]} > - + {/* Search Bar */} - + diff --git a/apps/tauri/DAEMON_SETUP.md b/apps/tauri/DAEMON_SETUP.md deleted file mode 100644 index 18044bf33..000000000 --- a/apps/tauri/DAEMON_SETUP.md +++ /dev/null @@ -1,227 +0,0 @@ -# Spacedrive Daemon Setup Guide - -## Overview - -The Spacedrive daemon is a background process that handles file indexing, networking, and core operations. On macOS, running background processes requires special permissions and configuration. - -## Production Build Configuration - -### 1. macOS Entitlements - -The app requires entitlements to spawn daemon processes. These are configured in `src-tauri/Entitlements.plist`. - -**To enable in `tauri.conf.json`:** - -```json -{ - "bundle": { - "macOS": { - "minimumSystemVersion": "10.15", - "entitlements": "Entitlements.plist" - } - } -} -``` - -### 2. Background Item Permission - -macOS 13+ (Ventura) requires explicit user permission for background items. - -**User Action Required:** -1. System Settings → General → Login Items & Extensions -2. Find "Spacedrive" in the list -3. Enable the background item permission - -The DaemonManager screen includes a button to open these settings directly. - -### 3. Daemon Binary Location - -The daemon binary (`sd-daemon`) must be bundled with the app: - -**Development:** -- Located at: `workspace/target/debug/sd-daemon` -- Built with: `cargo build --bin sd-daemon` - -**Production:** -- Must be in app bundle's Resources directory -- Configure in `tauri.conf.json`: - -```json -{ - "bundle": { - "resources": [ - "../../../target/release/sd-daemon" - ], - "externalBin": [ - "sd-daemon" - ] - } -} -``` - -## Daemon Operation Modes - -### Mode 1: Background Process (Preferred) - -Daemon runs as a separate system process: - -**Advantages:** -- Survives app restarts -- Lower memory footprint -- Can run headless - -**Requirements:** -- macOS background item permission -- `sd-daemon` binary bundled with app - -### Mode 2: In-Process (Fallback) - -Daemon runs within the Tauri app process: - -**Advantages:** -- No permission required -- Always works - -**Disadvantages:** -- Daemon dies when app closes -- Higher memory usage - -**Implementation Note:** In-process mode is not yet implemented. When permission is denied, the app should fall back to this mode automatically. - -## DaemonManager UI - -Access via `/daemon` route in the app. - -**Features:** -- Real-time daemon status -- Start/Stop controls -- Socket and server URL display -- Settings toggles (auto-start, run in-process) -- Quick link to macOS system settings - -## Development - -### Testing Daemon Spawn - -```bash -# Build daemon -cargo build --bin sd-daemon - -# Build and run Tauri app -cd apps/tauri -bun run tauri dev -``` - -### Check Daemon Status - -```bash -# Check if socket exists -ls -la ~/.local/share/spacedrive/daemon/daemon.sock - -# Try connecting -echo '{"Query":{"type":"ping"}}' | nc -U ~/.local/share/spacedrive/daemon/daemon.sock -``` - -### Common Issues - -**"Daemon binary not found"** -- Run `cargo build --bin sd-daemon` first -- Check `target/debug/sd-daemon` exists - -**"Permission denied" on spawn** -- macOS blocked the background process -- Check System Settings → Login Items -- Enable Spacedrive background permission - -**"Socket not created after 3 seconds"** -- Daemon crashed on startup -- Check daemon logs: `~/.local/share/spacedrive/daemon.log` -- Verify data directory permissions - -**"Failed to connect to daemon"** -- Socket file exists but daemon not running (stale socket) -- App will auto-clean stale sockets -- Try manual cleanup: `rm ~/.local/share/spacedrive/daemon/daemon.sock` - -## Architecture - -### Tauri Commands - -- `get_daemon_status()` - Get current daemon state -- `start_daemon_process()` - Spawn daemon as background process -- `stop_daemon_process()` - Kill daemon (only if we started it) -- `open_macos_settings()` - Open system settings for permissions -- `get_daemon_socket()` - Get socket path for client connection -- `get_server_url()` - Get HTTP server URL for sidecars - -### State Management - -```rust -struct DaemonState { - started_by_us: bool, // Did we spawn it? - socket_path: PathBuf, // Unix socket location - data_dir: PathBuf, // Spacedrive data directory - server_url: Option, // HTTP server for files - daemon_process: Option, // Process handle (if we spawned it) -} -``` - -### Startup Flow - -1. App launches -2. Check if daemon already running (try connect to socket) -3. If running: connect and use it -4. If not running: spawn new daemon process -5. Wait for socket to be created (max 3 seconds) -6. Connect and initialize - -### Shutdown Flow - -1. App closing -2. Check `started_by_us` flag -3. If true: kill daemon process -4. If false: leave it running (another app instance may be using it) - -## Future Enhancements - -### TODO: In-Process Mode - -Implement fallback when background permission denied: - -```rust -#[tauri::command] -async fn start_daemon_in_process( - data_dir: PathBuf, - state: State, -) -> Result<(), String> { - // Spawn daemon as tokio task instead of separate process - tauri::async_runtime::spawn(async move { - sd_daemon::run(data_dir).await - }); - Ok(()) -} -``` - -### TODO: Settings Persistence - -Save user preferences: -- Auto-start daemon on launch -- Prefer in-process mode -- Daemon log level - -Use Tauri's store plugin or save to daemon config file. - -### TODO: ServiceManagement Framework - -For better macOS integration, use Apple's ServiceManagement framework to register the daemon as a login item programmatically. - -```rust -// Use cocoa/objc to call SMAppService APIs -// This would show the system permission dialog automatically -``` - -## Resources - -- [macOS Background Task API](https://developer.apple.com/documentation/servicemanagement) -- [Tauri Bundling Guide](https://tauri.app/v1/guides/building/macos) -- [Unix Domain Sockets](https://man7.org/linux/man-pages/man7/unix.7.html) diff --git a/apps/tauri/DRAG_DROP_README.md b/apps/tauri/DRAG_DROP_README.md deleted file mode 100644 index fe97788a1..000000000 --- a/apps/tauri/DRAG_DROP_README.md +++ /dev/null @@ -1,237 +0,0 @@ -# Native Drag & Drop System - -A production-ready native drag-and-drop implementation for Spacedrive using AppKit on macOS. - -## Features - -- **Native OS Integration**: Real `NSDraggingSession` - files can be dropped into Finder, other apps -- **Custom Overlay**: User-controlled React component follows cursor during drag -- **Multi-Window Support**: Drag state synchronized across all Spacedrive windows via Tauri events -- **Live Updates**: Overlay can react to drag events in real-time -- **Type-Safe**: Full TypeScript definitions matching Rust types -- **File Promises**: Support for virtual files generated on-drop - -## Quick Start - -### 1. Start a Drag Operation - -```typescript -import { useDragOperation } from './hooks/useDragOperation'; - -function MyComponent() { - const { startDrag, isDragging } = useDragOperation({ - onDragStart: (sessionId) => console.log('Started:', sessionId), - onDragEnd: (result) => console.log('Result:', result), - }); - - const handleDrag = async () => { - await startDrag({ - items: [ - { - id: 'file-1', - kind: { type: 'File', path: '/path/to/file.pdf' } - } - ], - allowedOperations: ['copy', 'move'], - }); - }; - - return ; -} -``` - -### 2. Create a Drop Zone - -```typescript -import { useDropZone } from './hooks/useDropZone'; - -function DropTarget() { - const { isHovered, dropZoneProps } = useDropZone({ - onDrop: (items) => console.log('Dropped:', items), - }); - - return ( -
- Drop files here -
- ); -} -``` - -### 3. Customize the Drag Overlay - -The overlay component at `/drag-overlay` renders during drag operations: - -```typescript -// apps/tauri/src/routes/DragOverlay.tsx -export function DragOverlay() { - const [session, setSession] = useState(null); - - useEffect(() => { - getDragSession().then(setSession); - }, []); - - return ( -
- {session?.config.items.length} files -
- ); -} -``` - -## Architecture - -``` -React App Rust/Tauri macOS (Swift) -┌─────────────┐ ┌──────────────┐ ┌───────────────┐ -│ │ │ │ │ │ -│ startDrag() ├─invoke────► │ begin_drag ├─FFI─────► │ beginNative │ -│ │ │ │ │ Drag │ -│ │ │ │ │ │ -│ │ │ DragCoord │ │ NSDragging │ -│ │ │ inator │ │ Source │ -│ │ ◄──────────┤ ◄───NSNotif──┤ │ -│ onDragMoved │ emit event │ emit to all │ │ draggingMoved │ -│ │ │ windows │ │ │ -└─────────────┘ └──────────────┘ └───────────────┘ -``` - -## API Reference - -### `useDragOperation(options)` - -Hook for initiating drag operations. - -**Options:** -- `onDragStart?: (sessionId: string) => void` -- `onDragMove?: (x: number, y: number) => void` -- `onDragEnd?: (result: DragResult) => void` - -**Returns:** -- `isDragging: boolean` -- `currentSession: DragSession | null` -- `cursorPosition: { x: number; y: number } | null` -- `startDrag: (config) => Promise` -- `cancelDrag: (sessionId) => Promise` - -### `useDropZone(options)` - -Hook for creating drop targets. - -**Options:** -- `onDrop?: (items: DragItem[]) => void` -- `onDragEnter?: () => void` -- `onDragLeave?: () => void` - -**Returns:** -- `isHovered: boolean` -- `dragItems: DragItem[]` -- `dropZoneProps: object` - spread onto your drop zone element - -### Types - -```typescript -type DragItemKind = - | { type: 'File'; path: string } - | { type: 'FilePromise'; name: string; mimeType: string } - | { type: 'Text'; content: string }; - -interface DragItem { - kind: DragItemKind; - id: string; -} - -interface DragConfig { - items: DragItem[]; - overlayUrl: string; - overlaySize: [number, number]; - allowedOperations: DragOperation[]; -} -``` - -## Demo - -Run the demo window: - -```bash -# From apps/tauri -bun run dev -``` - -Then open the drag demo by changing the main window label to `drag-demo` or by programmatically showing it: - -```typescript -import { invoke } from '@tauri-apps/api/core'; - -await invoke('show_window', { - window: { type: 'Main' } // Opens the demo -}); -``` - -## Implementation Details - -### Rust Layer (`src-tauri/src/drag/`) - -- **`mod.rs`**: `DragCoordinator` - global state manager -- **`session.rs`**: Session tracking with UUIDs -- **`events.rs`**: Type-safe event definitions -- **`commands.rs`**: Tauri command handlers - -### Swift Layer (`crates/macos/src-swift/drag.swift`) - -- **`NativeDragSource`**: Implements `NSDraggingSource` protocol -- **File Promises**: Implements `NSFilePromiseProviderDelegate` -- **Notification Bridge**: Sends events back to Rust via `NotificationCenter` - -### TypeScript Layer (`src/`) - -- **`lib/drag.ts`**: Low-level API wrappers -- **`hooks/useDragOperation.ts`**: Drag initiation hook -- **`hooks/useDropZone.ts`**: Drop target hook -- **`routes/DragOverlay.tsx`**: Cursor-following overlay component - -## Known Limitations - -- **macOS only** - Windows/Linux support not yet implemented -- **Overlay mouse events**: Currently the overlay ignores all mouse events (by Tauri config, not objc2 calls) -- **File promise callbacks**: Requires implementing file generation logic via NSNotification - -## Future Enhancements - -1. **Windows Support**: Implement OLE drag-drop -2. **Linux Support**: X11/Wayland integration -3. **Bidirectional Drop**: Handle drops FROM external apps INTO Spacedrive -4. **Drag Modifiers**: Support Cmd/Ctrl for copy vs. move -5. **Multi-Monitor**: Better positioning for multi-display setups - -## Troubleshooting - -**Drag doesn't start:** -- Check console for Rust errors -- Ensure `begin_drag` command is registered in `main.rs` -- Verify Swift build succeeded - -**Overlay doesn't appear:** -- Check `DragOverlay` window is created -- Verify `/drag-overlay` route exists -- Check for TypeScript errors in overlay component - -**Drop doesn't work:** -- Ensure `useDropZone` hook is mounted before drag starts -- Check window labels match between drag source and drop target -- Verify event listeners are properly cleaned up - -## Contributing - -This is an experimental feature. Contributions welcome, especially for: -- Windows/Linux platform support -- Better error handling -- Performance optimizations -- Additional drag item types - -## License - -Same as Spacedrive project. diff --git a/apps/tauri/V1_TAURI_CONFIG_REFERENCE.md b/apps/tauri/V1_TAURI_CONFIG_REFERENCE.md deleted file mode 100644 index 109373dd2..000000000 --- a/apps/tauri/V1_TAURI_CONFIG_REFERENCE.md +++ /dev/null @@ -1,299 +0,0 @@ -# V1 Tauri Configuration Reference - -This document captures the sophisticated Tauri configuration from V1 that should be incrementally ported to V2. - -## Current V2 Status - -**Already Ported:** -- Basic window configuration (1400x750, hidden title, transparent) -- `app_ready` command for controlled window showing -- Core plugins: dialog, fs, shell, clipboard-manager, os -- macOS private API enabled -- Basic CSP configuration -- Bundle settings for Linux/macOS/Windows - -**TODO - High Priority:** -- [ ] Custom Tauri commands (file operations, menu management, reveal items) -- [ ] Platform-specific code (macOS Swift bridge, Linux/Windows file ops) -- [ ] Menu system (macOS menu bar with keybinds) -- [ ] Drag and drop tracking system -- [ ] Window effects (blur, vibrancy) -- [ ] Deep-link plugin for `spacedrive://` URL scheme - -**TODO - Medium Priority:** -- [ ] Updater plugin and custom update flow -- [ ] Custom server plugin (for serving thumbnails) -- [ ] Error plugin (for pre-rspc error display) -- [ ] TypeScript bindings with tauri-specta -- [ ] Full screen detection and titlebar style switching - -**TODO - Lower Priority:** -- [ ] CORS fetch plugin (for Supertokens auth) -- [ ] Linux environment normalization (XDG, GStreamer, Flatpak/Snap detection) -- [ ] Windows file association APIs -- [ ] AI models bundling (onnxruntime) - ---- - -## V1 Custom Tauri Commands - -These should be ported as needed: - -### App Lifecycle -```rust -app_ready // Already ported -reset_spacedrive // TODO: Wipes data directory -reload_webview // TODO: Platform-specific webview reload -``` - -### Menu & UI -```rust -set_menu_bar_item_state // TODO: Enable/disable menu items -refresh_menu_bar // TODO: Update menu based on library -lock_app_theme // TODO: Force light/dark mode (macOS) -``` - -### File Operations -```rust -open_file_paths // TODO: Open files by library ID -open_ephemeral_files // TODO: Open files by path -get_file_path_open_with_apps // TODO: Get "Open With" apps -get_ephemeral_files_open_with_apps // TODO: Same for paths -open_file_path_with // TODO: Open with specific app -open_ephemeral_file_with // TODO: Open path with specific app -reveal_items // TODO: Reveal in file manager -open_logs_dir // TODO: Open logs directory -open_trash_in_os_explorer // TODO: Open OS trash -``` - -### Drag & Drop -```rust -start_drag // TODO: Cursor-tracked drag (macOS/Windows) -stop_drag // TODO: Stop drag tracking -``` - -### macOS Specific -```rust -request_fda_macos // TODO: Request Full Disk Access -set_titlebar_style // TODO: Custom titlebar -disable_app_nap // TODO: Prevent sleep during indexing -enable_app_nap // TODO: Re-enable sleep -``` - -### Updater -```rust -check_for_update // TODO: Check for updates -install_update // TODO: Download and install -``` - ---- - -## V1 Window Configuration - -```json -{ - "width": 1400, - "height": 750, - "minWidth": 768, - "minHeight": 500, - "hiddenTitle": true, - "transparent": true, - "center": true, - "visible": false, // Shown via app_ready - "dragDropEnabled": true, - "windowEffects": { - "effects": ["sidebar"], - "state": "followsWindowActiveState", - "radius": 9 - } -} -``` - -**Current V2:** Basic config without windowEffects (add when needed). - ---- - -## V1 Content Security Policy - -```json -{ - "default-src": "'self' webkit-pdfjs-viewer: asset: http://asset.localhost blob: data: filesystem: http: https: tauri:", - "connect-src": "'self' ipc: http://ipc.localhost ws: wss: http: https: tauri:", - "img-src": "'self' asset: http://asset.localhost blob: data: filesystem: http: https: tauri:", - "style-src": "'self' 'unsafe-inline' http: https: tauri:" -} -``` - -**Current V2:** Simplified CSP (expand as we add features like custom server). - ---- - -## V1 Bundle Configuration - -### Linux -```json -{ - "deb": { - "depends": ["libc6", "libxdo3", "dbus", "libwebkit2gtk-4.1-0", "libgtk-3-0"], - "files": { - "/usr/share/spacedrive/models/yolov8s.onnx": "../../../models/yolov8s.onnx" - } - } -} -``` - -**Critical:** dbus must NOT be vendored (breaks on X11/Nvidia). - -### macOS -```json -{ - "minimumSystemVersion": "10.15", - "frameworks": [".deps/Spacedrive.framework"] -} -``` - -**V1 Pattern:** Custom Swift framework bundled (may need for V2 Swift bridge). - -### Windows -```json -{ - "webviewInstallMode": { - "type": "embedBootstrapper", - "silent": true - } -} -``` - ---- - -## V1 Plugins - -### Official Tauri Plugins -```toml -tauri-plugin-clipboard-manager = "2.0" # Ported -tauri-plugin-deep-link = "2.0" # TODO -tauri-plugin-dialog = "2.0" # Ported -tauri-plugin-drag = "2.0" # TODO (custom fork with move operation) -tauri-plugin-http = "2.0" # TODO (if needed) -tauri-plugin-os = "2.0" # Ported -tauri-plugin-shell = "2.0" # Ported -tauri-plugin-updater = "2.0" # TODO -``` - -### Custom Plugins -- **tauri-plugin-cors-fetch:** Modified for Supertokens auth (TODO) -- **sd_server_plugin:** Axum server for custom URI protocol (TODO) -- **sd_error_plugin:** Injects `window.__SD_ERROR__` (TODO) -- **Updater injection:** Injects `window.__SD_UPDATER__` flag (TODO) - ---- - -## V1 Platform-Specific Code - -### macOS (Swift via swift-rs) - -**Window Management:** -- `set_titlebar_style` - Invisible toolbar trick -- `disable_app_nap` / `enable_app_nap` - System sleep control -- `lock_app_theme` - Force light/dark mode -- `reload_webview` - Proper reload without artifacts - -**File Operations:** -- `get_open_with_applications` - Apps that can open file (with icons) -- `open_file_path_with` - Open with specific application -- Fallback compatibility for macOS 10.15+ - -### Linux - -**Environment Normalization (critical!):** -- XDG directory setup (HOME, DATA_HOME, CONFIG_HOME, etc.) -- GStreamer plugin paths -- PATH normalization -- Flatpak/Snap detection -- NVIDIA GPU workaround: `WEBKIT_DISABLE_DMABUF_RENDERER=1` - -**File Operations:** -- GTK-based "Open With" detection -- Content type detection -- AppLaunchContext for opening files -- Fallback `getpwuid_r` for $HOME - -### Windows - -**File Operations:** -- `list_apps_associated_with_ext` - Uses `SHAssocEnumHandlers` -- `open_file_path_with` - Uses `IAssocHandler` and `IShellItem` -- COM initialization (thread-local with atexit) - ---- - -## V1 Menu System (macOS) - -Full macOS menu bar: - -``` -Spacedrive - ├─ About Spacedrive - ├─ New Library [Cmd+N] - ├─ Hide [Cmd+H] - └─ Quit [Cmd+Q] - -Edit - ├─ Undo [Cmd+Z] - ├─ Redo [Cmd+Shift+Z] - └─ Select All [Cmd+A] - -View - ├─ Overview [Cmd+Shift+O] - ├─ Search [Cmd+F] - ├─ Settings [Cmd+,] - ├─ Layouts - └─ Dev Tools - -Window - ├─ Minimize [Cmd+M] - ├─ Fullscreen [Cmd+Ctrl+F] - └─ Reload Webview -``` - -**Features:** -- Library-locked menu items (disabled until library loaded) -- Keybind forwarding to frontend -- Fullscreen detection with titlebar switching - ---- - -## V1 Drag & Drop Implementation - -**Advanced Features:** -- Cursor position tracking at 8ms intervals -- Detects when cursor leaves window bounds -- Creates drag session with base64 image preview -- Callback for drop result and position -- Linux: Disabled (not implemented) - -**Custom `drag` crate:** Modified fork with "move-operation" branch from spacedriveapp. - ---- - -## V1 Key Gotchas - -1. **Linux dbus:** MUST NOT be vendored (breaks on X11/Nvidia) -2. **GTK version:** Must match Tauri's (0.18 with v3_24 feature) -3. **NVIDIA workaround:** `WEBKIT_DISABLE_DMABUF_RENDERER=1` -4. **Swift NULL delimiter hack:** For file path arrays -5. **Updater:** Disabled on Linux -6. **Tauri version:** Pinned to v2.0.6 exactly -7. **AI models:** Special linker config for onnxruntime TLS - ---- - -## Next Steps for V2 - -1. **Immediate:** Keep current basic setup, focus on core integration -2. **Phase 2:** Add file operation commands as UI needs them -3. **Phase 3:** Port platform-specific code (Swift bridge, Linux normalization) -4. **Phase 4:** Add menu system and keyboard shortcuts -5. **Phase 5:** Advanced features (updater, drag tracking, window effects) - -**Principle:** Port incrementally as features are needed, don't front-load everything. diff --git a/bun.lockb b/bun.lockb index dd295e60cd133d324906368bd84215f5301929dd..8f32541328f9741217b60556b956a50384b80267 100755 GIT binary patch delta 47566 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 td_!XiM-mr1w8` zKzcuf-WAaVC8DNCuabyCdiO*jz8Mp2kfFENEJAv!s;8(=qa{dh)OiF=LX(l+xTCl6 z=r?(KtW?kh2J80f-B`3{FR)I9|s47)zD*?r#O20_5YuQBN?M0(n|*( zMU&AKq<6o(Wo-`!tCbo{bTk@+Mk2lN-~pR_IJhn24uYN0U8oDX8|hUPucNK#4YUpE zxycUnCVC6KjowAO&~CH`eSr3%{pbMt5PgI`MhDR$bQm2$pQ6u7^Q`D|0!Pso=s3zn zU!t$kx99}=0i8x?(2wT-G+3hLF9a{3i|8_H#VEB#ZBSd(4Bd+AGvp1Bp7At7{n7pC zW>m-8e;Vw{_n)5sG}x}oSQ4e$vx)=3Cy-;tIg8HDos2}Q&?nh-=|6)-& z)Qor%y2V<47Oa->B6I0w^a@&y)}Xa$J=%aaqE}H4+JrWv*U%ei8>+@s)03meke(Jz zL;9_075Fu%D!LZwsgWKMrJx>24}y5`bEmH`f%9be1sy;iql4%ZbO;?rdQaNRXcc+| ztww9mTGR~HMfFg9w9ndq9_(EB8wT-PbON2s$^JZO{&3bD-|yDwSn!@wdb{KXl!G>+ zSJC#ImB)D7aN+sPGrg~32AXP1bAz4R>IXVKkly>z9i8EUKs63wD@tsQ+Mu@RHq;K? zj@qNaBn6H9m z3P%u(L_rjlbKO_L^8RqWk8?{-%deT8DSDA(5;swOxQOoMuCOxgOhm0vCv-FVgmxT4 zhtUzV7u}5X?moS2P4C`&5RFDt&{Q-X%|MUmT>d8blRxej>gh`zdb!pu=q?nFB9UG> zw#;Hq1~dHKY|hDG@3#62z5al%KbWsa>9qJIq(2kS<$m@t?g(a~+2|cg&<``yIjEm- z)ABXFnG4q+2G>}lQ^E4x+i8_sUo<^t8w{hdvJ8~YRK5B7ef zKY@*)sQJv7j@%aN4^-E%@3Y*21o;+S@KB0=PkI{tg3h4x=oI=D{fNFnXVG`)0{RV| zLl@D%(6{J&^b_M!{@@FF!7t*MYxK}_=y{}P zwt6>T5u|6ZVW*$5CGtk5s=tQQwA|p%SRL?fo(M zRSi}8ITe12K0{xj<0uy$MaR&W=nHhjj-CyEpYaKe`2rn6$Iyr9I692<4zVNXBb1B2 zLZ6~9(PwBM`WStV_M@ZdAUc3_TdE$+=5Y3+S^AhWkCtYkxu_xHV@?rYQEoETr4Hy^ zPM3cLll;LHGAlz_%l;|2tJ4yQ1vGuHbbsU5Iz5aQ(GKAAA<%EciM2Rm-@C z`LM^oqY!$g_z>Z7Xe=6qMxqgx-8ibU|3TZ@II2v&G{^&JIO@*n*&pwRx)Sb-_d%(I z2cQ(f_o2=f-Xv;?o{0GF=UIY2an2;Hhjn+^swPooDs=%(A)1$EBzvn($*|rLcC1O% z^%mJQ>Za7eWK2Z^(ExNmIzX9u8bUlTk63_$2Dv+45(m21Bt zmfbX}LE+IPtI1>VbjxiT)x3QM!Kcs!G#=&k=uyJobGAQ$D^D23O~GG>DJ>ICMEc

Yn3D3pnxo}>6 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

*05Fh_sh=T4(xwFCoh&*+MJ7nw_>_=(IQ(! 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 `VnQLNLi{(c_VST8mWCE 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