mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-19 22:19:49 -04:00
Add archive sources UI with full Rust integration
- Create core/src/data/ module with SourceManager wrapping sd-archive Engine - Add Sources to GroupType and Source to ItemType enums - Add default Sources group to new library creation - Register source operations: create, list, get, delete, sync, list_items - Register adapter operations: list, config, update - Add bundled adapter sync from workspace adapters/ directory - Add adapter update system with BLAKE3 change detection and backup/rollback - Frontend: Sources home, source detail with virtualized list, adapters screen - Frontend: SourcesGroup sidebar, SpaceGroup dispatch, spaceItemUtils - Frontend: TopBar integration (path bar, search, sync, actions menu) - Frontend: Tab title sync, adapter icon lookup hook - Regenerate TypeScript types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -261,8 +261,8 @@ PODS:
|
||||
- hermes-engine (0.81.5):
|
||||
- hermes-engine/Pre-built (= 0.81.5)
|
||||
- hermes-engine/Pre-built (0.81.5)
|
||||
- libavif/core (0.11.1)
|
||||
- libavif/libdav1d (0.11.1):
|
||||
- libavif/core (1.0.0)
|
||||
- libavif/libdav1d (1.0.0):
|
||||
- libavif/core
|
||||
- libdav1d (>= 0.6.0)
|
||||
- libdav1d (1.2.0)
|
||||
@@ -2348,9 +2348,9 @@ PODS:
|
||||
- Yoga
|
||||
- SDMobileCore (1.0.0):
|
||||
- ExpoModulesCore
|
||||
- SDWebImage (5.21.5):
|
||||
- SDWebImage/Core (= 5.21.5)
|
||||
- SDWebImage/Core (5.21.5)
|
||||
- SDWebImage (5.21.7):
|
||||
- SDWebImage/Core (= 5.21.7)
|
||||
- SDWebImage/Core (5.21.7)
|
||||
- SDWebImageAVIFCoder (0.11.1):
|
||||
- libavif/core (>= 0.11.0)
|
||||
- SDWebImage (~> 5.10)
|
||||
@@ -2367,107 +2367,107 @@ PODS:
|
||||
- ZXingObjC/Core
|
||||
|
||||
DEPENDENCIES:
|
||||
- "EXConstants (from `../../../node_modules/.bun/expo-constants@18.0.11+9bddab83625b4ca7/node_modules/expo-constants/ios`)"
|
||||
- "EXConstants (from `../../../node_modules/.bun/expo-constants@18.0.11+3ee4ad13fefe912b/node_modules/expo-constants/ios`)"
|
||||
- "EXJSONUtils (from `../../../node_modules/.bun/expo-json-utils@0.15.0/node_modules/expo-json-utils/ios`)"
|
||||
- "EXManifests (from `../../../node_modules/.bun/expo-manifests@1.0.10+9bddab83625b4ca7/node_modules/expo-manifests/ios`)"
|
||||
- "Expo (from `../../../node_modules/.bun/expo@54.0.27+9bddab83625b4ca7/node_modules/expo`)"
|
||||
- "expo-dev-client (from `../../../node_modules/.bun/expo-dev-client@6.0.20+9bddab83625b4ca7/node_modules/expo-dev-client/ios`)"
|
||||
- "expo-dev-launcher (from `../../../node_modules/.bun/expo-dev-launcher@6.0.20+9bddab83625b4ca7/node_modules/expo-dev-launcher`)"
|
||||
- "expo-dev-menu (from `../../../node_modules/.bun/expo-dev-menu@7.0.18+9bddab83625b4ca7/node_modules/expo-dev-menu`)"
|
||||
- "expo-dev-menu-interface (from `../../../node_modules/.bun/expo-dev-menu-interface@2.0.0+9bddab83625b4ca7/node_modules/expo-dev-menu-interface/ios`)"
|
||||
- "ExpoAsset (from `../../../node_modules/.bun/expo-asset@12.0.11+9bddab83625b4ca7/node_modules/expo-asset/ios`)"
|
||||
- "ExpoBlur (from `../../../node_modules/.bun/expo-blur@15.0.8+9bddab83625b4ca7/node_modules/expo-blur/ios`)"
|
||||
- "ExpoCamera (from `../../../node_modules/.bun/expo-camera@17.0.10+9bddab83625b4ca7/node_modules/expo-camera/ios`)"
|
||||
- "ExpoDocumentPicker (from `../../../node_modules/.bun/expo-document-picker@14.0.8+9bddab83625b4ca7/node_modules/expo-document-picker/ios`)"
|
||||
- "ExpoFileSystem (from `../../../node_modules/.bun/expo-file-system@19.0.20+9bddab83625b4ca7/node_modules/expo-file-system/ios`)"
|
||||
- "ExpoFont (from `../../../node_modules/.bun/expo-font@14.0.10+9915544d233ac918/node_modules/expo-font/ios`)"
|
||||
- "ExpoHaptics (from `../../../node_modules/.bun/expo-haptics@15.0.8+9bddab83625b4ca7/node_modules/expo-haptics/ios`)"
|
||||
- "ExpoHead (from `../../../node_modules/.bun/expo-router@6.0.17+74a3cf8ce2a4bc65/node_modules/expo-router/ios`)"
|
||||
- "ExpoImage (from `../../../node_modules/.bun/expo-image@3.0.11+9bddab83625b4ca7/node_modules/expo-image/ios`)"
|
||||
- "EXManifests (from `../../../node_modules/.bun/expo-manifests@1.0.10+3ee4ad13fefe912b/node_modules/expo-manifests/ios`)"
|
||||
- "Expo (from `../../../node_modules/.bun/expo@54.0.27+3ee4ad13fefe912b/node_modules/expo`)"
|
||||
- "expo-dev-client (from `../../../node_modules/.bun/expo-dev-client@6.0.20+3ee4ad13fefe912b/node_modules/expo-dev-client/ios`)"
|
||||
- "expo-dev-launcher (from `../../../node_modules/.bun/expo-dev-launcher@6.0.20+3ee4ad13fefe912b/node_modules/expo-dev-launcher`)"
|
||||
- "expo-dev-menu (from `../../../node_modules/.bun/expo-dev-menu@7.0.18+3ee4ad13fefe912b/node_modules/expo-dev-menu`)"
|
||||
- "expo-dev-menu-interface (from `../../../node_modules/.bun/expo-dev-menu-interface@2.0.0+3ee4ad13fefe912b/node_modules/expo-dev-menu-interface/ios`)"
|
||||
- "ExpoAsset (from `../../../node_modules/.bun/expo-asset@12.0.11+3ee4ad13fefe912b/node_modules/expo-asset/ios`)"
|
||||
- "ExpoBlur (from `../../../node_modules/.bun/expo-blur@15.0.8+3ee4ad13fefe912b/node_modules/expo-blur/ios`)"
|
||||
- "ExpoCamera (from `../../../node_modules/.bun/expo-camera@17.0.10+3ee4ad13fefe912b/node_modules/expo-camera/ios`)"
|
||||
- "ExpoDocumentPicker (from `../../../node_modules/.bun/expo-document-picker@14.0.8+3ee4ad13fefe912b/node_modules/expo-document-picker/ios`)"
|
||||
- "ExpoFileSystem (from `../../../node_modules/.bun/expo-file-system@19.0.20+3ee4ad13fefe912b/node_modules/expo-file-system/ios`)"
|
||||
- "ExpoFont (from `../../../node_modules/.bun/expo-font@14.0.10+c262bee79918334c/node_modules/expo-font/ios`)"
|
||||
- "ExpoHaptics (from `../../../node_modules/.bun/expo-haptics@15.0.8+3ee4ad13fefe912b/node_modules/expo-haptics/ios`)"
|
||||
- "ExpoHead (from `../../../node_modules/.bun/expo-router@6.0.17+7dc14032edcce378/node_modules/expo-router/ios`)"
|
||||
- "ExpoImage (from `../../../node_modules/.bun/expo-image@3.0.11+3ee4ad13fefe912b/node_modules/expo-image/ios`)"
|
||||
- "ExpoKeepAwake (from `../../../node_modules/.bun/expo-keep-awake@15.0.8+ddb0696906414ead/node_modules/expo-keep-awake/ios`)"
|
||||
- "ExpoLinking (from `../../../node_modules/.bun/expo-linking@8.0.10+9bddab83625b4ca7/node_modules/expo-linking/ios`)"
|
||||
- "ExpoModulesCore (from `../../../node_modules/.bun/expo-modules-core@3.0.28+c1b4cdd6e4cb2f11/node_modules/expo-modules-core`)"
|
||||
- "ExpoSplashScreen (from `../../../node_modules/.bun/expo-splash-screen@31.0.12+9bddab83625b4ca7/node_modules/expo-splash-screen/ios`)"
|
||||
- "EXUpdatesInterface (from `../../../node_modules/.bun/expo-updates-interface@2.0.0+9bddab83625b4ca7/node_modules/expo-updates-interface/ios`)"
|
||||
- "FBLazyVector (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/FBLazyVector`)"
|
||||
- "hermes-engine (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)"
|
||||
- "LiquidGlass (from `../../../node_modules/.bun/@callstack+liquid-glass@0.7.0+c1b4cdd6e4cb2f11/node_modules/@callstack/liquid-glass`)"
|
||||
- "RCTDeprecation (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)"
|
||||
- "RCTRequired (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Required`)"
|
||||
- "RCTTypeSafety (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/TypeSafety`)"
|
||||
- "React (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/`)"
|
||||
- "React-callinvoker (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/callinvoker`)"
|
||||
- "React-Core (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/`)"
|
||||
- "React-Core-prebuilt (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/React-Core-prebuilt.podspec`)"
|
||||
- "React-Core/RCTWebSocket (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/`)"
|
||||
- "React-CoreModules (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/React/CoreModules`)"
|
||||
- "React-cxxreact (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/cxxreact`)"
|
||||
- "React-debug (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/debug`)"
|
||||
- "React-defaultsnativemodule (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/defaults`)"
|
||||
- "React-domnativemodule (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/dom`)"
|
||||
- "React-Fabric (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon`)"
|
||||
- "React-FabricComponents (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon`)"
|
||||
- "React-FabricImage (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon`)"
|
||||
- "React-featureflags (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/featureflags`)"
|
||||
- "React-featureflagsnativemodule (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)"
|
||||
- "React-graphics (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/graphics`)"
|
||||
- "React-hermes (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/hermes`)"
|
||||
- "React-idlecallbacksnativemodule (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)"
|
||||
- "React-ImageManager (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)"
|
||||
- "React-jserrorhandler (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jserrorhandler`)"
|
||||
- "React-jsi (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsi`)"
|
||||
- "React-jsiexecutor (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsiexecutor`)"
|
||||
- "React-jsinspector (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsinspector-modern`)"
|
||||
- "React-jsinspectorcdp (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsinspector-modern/cdp`)"
|
||||
- "React-jsinspectornetwork (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsinspector-modern/network`)"
|
||||
- "React-jsinspectortracing (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)"
|
||||
- "React-jsitooling (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsitooling`)"
|
||||
- "React-jsitracing (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/hermes/executor/`)"
|
||||
- "React-logger (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/logger`)"
|
||||
- "React-Mapbuffer (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon`)"
|
||||
- "React-microtasksnativemodule (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)"
|
||||
- "react-native-safe-area-context (from `../../../node_modules/.bun/react-native-safe-area-context@5.6.2+c1b4cdd6e4cb2f11/node_modules/react-native-safe-area-context`)"
|
||||
- "ExpoLinking (from `../../../node_modules/.bun/expo-linking@8.0.10+3ee4ad13fefe912b/node_modules/expo-linking/ios`)"
|
||||
- "ExpoModulesCore (from `../../../node_modules/.bun/expo-modules-core@3.0.28+87dd5a4c738f4c73/node_modules/expo-modules-core`)"
|
||||
- "ExpoSplashScreen (from `../../../node_modules/.bun/expo-splash-screen@31.0.12+3ee4ad13fefe912b/node_modules/expo-splash-screen/ios`)"
|
||||
- "EXUpdatesInterface (from `../../../node_modules/.bun/expo-updates-interface@2.0.0+3ee4ad13fefe912b/node_modules/expo-updates-interface/ios`)"
|
||||
- "FBLazyVector (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/FBLazyVector`)"
|
||||
- "hermes-engine (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)"
|
||||
- "LiquidGlass (from `../../../node_modules/.bun/@callstack+liquid-glass@0.7.0+87dd5a4c738f4c73/node_modules/@callstack/liquid-glass`)"
|
||||
- "RCTDeprecation (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)"
|
||||
- "RCTRequired (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Required`)"
|
||||
- "RCTTypeSafety (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/TypeSafety`)"
|
||||
- "React (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/`)"
|
||||
- "React-callinvoker (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/callinvoker`)"
|
||||
- "React-Core (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/`)"
|
||||
- "React-Core-prebuilt (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/React-Core-prebuilt.podspec`)"
|
||||
- "React-Core/RCTWebSocket (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/`)"
|
||||
- "React-CoreModules (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/React/CoreModules`)"
|
||||
- "React-cxxreact (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/cxxreact`)"
|
||||
- "React-debug (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/debug`)"
|
||||
- "React-defaultsnativemodule (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/defaults`)"
|
||||
- "React-domnativemodule (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/dom`)"
|
||||
- "React-Fabric (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon`)"
|
||||
- "React-FabricComponents (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon`)"
|
||||
- "React-FabricImage (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon`)"
|
||||
- "React-featureflags (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/featureflags`)"
|
||||
- "React-featureflagsnativemodule (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)"
|
||||
- "React-graphics (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/graphics`)"
|
||||
- "React-hermes (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/hermes`)"
|
||||
- "React-idlecallbacksnativemodule (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)"
|
||||
- "React-ImageManager (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)"
|
||||
- "React-jserrorhandler (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jserrorhandler`)"
|
||||
- "React-jsi (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsi`)"
|
||||
- "React-jsiexecutor (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsiexecutor`)"
|
||||
- "React-jsinspector (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsinspector-modern`)"
|
||||
- "React-jsinspectorcdp (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsinspector-modern/cdp`)"
|
||||
- "React-jsinspectornetwork (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsinspector-modern/network`)"
|
||||
- "React-jsinspectortracing (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)"
|
||||
- "React-jsitooling (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsitooling`)"
|
||||
- "React-jsitracing (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/hermes/executor/`)"
|
||||
- "React-logger (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/logger`)"
|
||||
- "React-Mapbuffer (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon`)"
|
||||
- "React-microtasksnativemodule (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)"
|
||||
- "react-native-safe-area-context (from `../../../node_modules/.bun/react-native-safe-area-context@5.6.2+87dd5a4c738f4c73/node_modules/react-native-safe-area-context`)"
|
||||
- "react-native-slider (from `../../../node_modules/.bun/@react-native-community+slider@5.1.1/node_modules/@react-native-community/slider`)"
|
||||
- "React-NativeModulesApple (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)"
|
||||
- "React-oscompat (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/oscompat`)"
|
||||
- "React-perflogger (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/reactperflogger`)"
|
||||
- "React-performancetimeline (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/performance/timeline`)"
|
||||
- "React-RCTActionSheet (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/ActionSheetIOS`)"
|
||||
- "React-RCTAnimation (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/NativeAnimation`)"
|
||||
- "React-RCTAppDelegate (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/AppDelegate`)"
|
||||
- "React-RCTBlob (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Blob`)"
|
||||
- "React-RCTFabric (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/React`)"
|
||||
- "React-RCTFBReactNativeSpec (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/React`)"
|
||||
- "React-RCTImage (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Image`)"
|
||||
- "React-RCTLinking (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/LinkingIOS`)"
|
||||
- "React-RCTNetwork (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Network`)"
|
||||
- "React-RCTRuntime (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/React/Runtime`)"
|
||||
- "React-RCTSettings (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Settings`)"
|
||||
- "React-RCTText (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Text`)"
|
||||
- "React-RCTVibration (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Vibration`)"
|
||||
- "React-rendererconsistency (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/consistency`)"
|
||||
- "React-renderercss (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/css`)"
|
||||
- "React-rendererdebug (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/debug`)"
|
||||
- "React-RuntimeApple (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/runtime/platform/ios`)"
|
||||
- "React-RuntimeCore (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/runtime`)"
|
||||
- "React-runtimeexecutor (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/runtimeexecutor`)"
|
||||
- "React-RuntimeHermes (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/runtime`)"
|
||||
- "React-runtimescheduler (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)"
|
||||
- "React-timing (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/timing`)"
|
||||
- "React-utils (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/utils`)"
|
||||
- "React-NativeModulesApple (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)"
|
||||
- "React-oscompat (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/oscompat`)"
|
||||
- "React-perflogger (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/reactperflogger`)"
|
||||
- "React-performancetimeline (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/performance/timeline`)"
|
||||
- "React-RCTActionSheet (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/ActionSheetIOS`)"
|
||||
- "React-RCTAnimation (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/NativeAnimation`)"
|
||||
- "React-RCTAppDelegate (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/AppDelegate`)"
|
||||
- "React-RCTBlob (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Blob`)"
|
||||
- "React-RCTFabric (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/React`)"
|
||||
- "React-RCTFBReactNativeSpec (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/React`)"
|
||||
- "React-RCTImage (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Image`)"
|
||||
- "React-RCTLinking (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/LinkingIOS`)"
|
||||
- "React-RCTNetwork (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Network`)"
|
||||
- "React-RCTRuntime (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/React/Runtime`)"
|
||||
- "React-RCTSettings (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Settings`)"
|
||||
- "React-RCTText (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Text`)"
|
||||
- "React-RCTVibration (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Vibration`)"
|
||||
- "React-rendererconsistency (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/consistency`)"
|
||||
- "React-renderercss (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/css`)"
|
||||
- "React-rendererdebug (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/debug`)"
|
||||
- "React-RuntimeApple (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/runtime/platform/ios`)"
|
||||
- "React-RuntimeCore (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/runtime`)"
|
||||
- "React-runtimeexecutor (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/runtimeexecutor`)"
|
||||
- "React-RuntimeHermes (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/runtime`)"
|
||||
- "React-runtimescheduler (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)"
|
||||
- "React-timing (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/timing`)"
|
||||
- "React-utils (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/utils`)"
|
||||
- ReactAppDependencyProvider (from `build/generated/ios`)
|
||||
- ReactCodegen (from `build/generated/ios`)
|
||||
- "ReactCommon/turbomodule/core (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon`)"
|
||||
- "ReactNativeDependencies (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`)"
|
||||
- "RNCAsyncStorage (from `../../../node_modules/.bun/@react-native-async-storage+async-storage@2.2.0+c1b4cdd6e4cb2f11/node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNCClipboard (from `../../../node_modules/.bun/@react-native-clipboard+clipboard@1.16.3+c1b4cdd6e4cb2f11/node_modules/@react-native-clipboard/clipboard`)"
|
||||
- "RNGestureHandler (from `../../../node_modules/.bun/react-native-gesture-handler@2.28.0+c1b4cdd6e4cb2f11/node_modules/react-native-gesture-handler`)"
|
||||
- "RNReanimated (from `../../../node_modules/.bun/react-native-reanimated@4.1.6+58856117def9c0ae/node_modules/react-native-reanimated`)"
|
||||
- "RNScreens (from `../../../node_modules/.bun/react-native-screens@4.16.0+c1b4cdd6e4cb2f11/node_modules/react-native-screens`)"
|
||||
- "RNSVG (from `../../../node_modules/.bun/react-native-svg@15.12.1+c1b4cdd6e4cb2f11/node_modules/react-native-svg`)"
|
||||
- "RNWorklets (from `../../../node_modules/.bun/react-native-worklets@0.7.1+c1b4cdd6e4cb2f11/node_modules/react-native-worklets`)"
|
||||
- "ReactCommon/turbomodule/core (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon`)"
|
||||
- "ReactNativeDependencies (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`)"
|
||||
- "RNCAsyncStorage (from `../../../node_modules/.bun/@react-native-async-storage+async-storage@2.2.0+87dd5a4c738f4c73/node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNCClipboard (from `../../../node_modules/.bun/@react-native-clipboard+clipboard@1.16.3+87dd5a4c738f4c73/node_modules/@react-native-clipboard/clipboard`)"
|
||||
- "RNGestureHandler (from `../../../node_modules/.bun/react-native-gesture-handler@2.28.0+87dd5a4c738f4c73/node_modules/react-native-gesture-handler`)"
|
||||
- "RNReanimated (from `../../../node_modules/.bun/react-native-reanimated@4.1.6+d983531a34c8e10a/node_modules/react-native-reanimated`)"
|
||||
- "RNScreens (from `../../../node_modules/.bun/react-native-screens@4.16.0+87dd5a4c738f4c73/node_modules/react-native-screens`)"
|
||||
- "RNSVG (from `../../../node_modules/.bun/react-native-svg@15.12.1+87dd5a4c738f4c73/node_modules/react-native-svg`)"
|
||||
- "RNWorklets (from `../../../node_modules/.bun/react-native-worklets@0.7.1+87dd5a4c738f4c73/node_modules/react-native-worklets`)"
|
||||
- SDMobileCore (from `../modules/sd-mobile-core/ios`)
|
||||
- "Yoga (from `../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/yoga`)"
|
||||
- "Yoga (from `../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/yoga`)"
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -2482,206 +2482,206 @@ SPEC REPOS:
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
EXConstants:
|
||||
:path: "../../../node_modules/.bun/expo-constants@18.0.11+9bddab83625b4ca7/node_modules/expo-constants/ios"
|
||||
:path: "../../../node_modules/.bun/expo-constants@18.0.11+3ee4ad13fefe912b/node_modules/expo-constants/ios"
|
||||
EXJSONUtils:
|
||||
:path: "../../../node_modules/.bun/expo-json-utils@0.15.0/node_modules/expo-json-utils/ios"
|
||||
EXManifests:
|
||||
:path: "../../../node_modules/.bun/expo-manifests@1.0.10+9bddab83625b4ca7/node_modules/expo-manifests/ios"
|
||||
:path: "../../../node_modules/.bun/expo-manifests@1.0.10+3ee4ad13fefe912b/node_modules/expo-manifests/ios"
|
||||
Expo:
|
||||
:path: "../../../node_modules/.bun/expo@54.0.27+9bddab83625b4ca7/node_modules/expo"
|
||||
:path: "../../../node_modules/.bun/expo@54.0.27+3ee4ad13fefe912b/node_modules/expo"
|
||||
expo-dev-client:
|
||||
:path: "../../../node_modules/.bun/expo-dev-client@6.0.20+9bddab83625b4ca7/node_modules/expo-dev-client/ios"
|
||||
:path: "../../../node_modules/.bun/expo-dev-client@6.0.20+3ee4ad13fefe912b/node_modules/expo-dev-client/ios"
|
||||
expo-dev-launcher:
|
||||
:path: "../../../node_modules/.bun/expo-dev-launcher@6.0.20+9bddab83625b4ca7/node_modules/expo-dev-launcher"
|
||||
:path: "../../../node_modules/.bun/expo-dev-launcher@6.0.20+3ee4ad13fefe912b/node_modules/expo-dev-launcher"
|
||||
expo-dev-menu:
|
||||
:path: "../../../node_modules/.bun/expo-dev-menu@7.0.18+9bddab83625b4ca7/node_modules/expo-dev-menu"
|
||||
:path: "../../../node_modules/.bun/expo-dev-menu@7.0.18+3ee4ad13fefe912b/node_modules/expo-dev-menu"
|
||||
expo-dev-menu-interface:
|
||||
:path: "../../../node_modules/.bun/expo-dev-menu-interface@2.0.0+9bddab83625b4ca7/node_modules/expo-dev-menu-interface/ios"
|
||||
:path: "../../../node_modules/.bun/expo-dev-menu-interface@2.0.0+3ee4ad13fefe912b/node_modules/expo-dev-menu-interface/ios"
|
||||
ExpoAsset:
|
||||
:path: "../../../node_modules/.bun/expo-asset@12.0.11+9bddab83625b4ca7/node_modules/expo-asset/ios"
|
||||
:path: "../../../node_modules/.bun/expo-asset@12.0.11+3ee4ad13fefe912b/node_modules/expo-asset/ios"
|
||||
ExpoBlur:
|
||||
:path: "../../../node_modules/.bun/expo-blur@15.0.8+9bddab83625b4ca7/node_modules/expo-blur/ios"
|
||||
:path: "../../../node_modules/.bun/expo-blur@15.0.8+3ee4ad13fefe912b/node_modules/expo-blur/ios"
|
||||
ExpoCamera:
|
||||
:path: "../../../node_modules/.bun/expo-camera@17.0.10+9bddab83625b4ca7/node_modules/expo-camera/ios"
|
||||
:path: "../../../node_modules/.bun/expo-camera@17.0.10+3ee4ad13fefe912b/node_modules/expo-camera/ios"
|
||||
ExpoDocumentPicker:
|
||||
:path: "../../../node_modules/.bun/expo-document-picker@14.0.8+9bddab83625b4ca7/node_modules/expo-document-picker/ios"
|
||||
:path: "../../../node_modules/.bun/expo-document-picker@14.0.8+3ee4ad13fefe912b/node_modules/expo-document-picker/ios"
|
||||
ExpoFileSystem:
|
||||
:path: "../../../node_modules/.bun/expo-file-system@19.0.20+9bddab83625b4ca7/node_modules/expo-file-system/ios"
|
||||
:path: "../../../node_modules/.bun/expo-file-system@19.0.20+3ee4ad13fefe912b/node_modules/expo-file-system/ios"
|
||||
ExpoFont:
|
||||
:path: "../../../node_modules/.bun/expo-font@14.0.10+9915544d233ac918/node_modules/expo-font/ios"
|
||||
:path: "../../../node_modules/.bun/expo-font@14.0.10+c262bee79918334c/node_modules/expo-font/ios"
|
||||
ExpoHaptics:
|
||||
:path: "../../../node_modules/.bun/expo-haptics@15.0.8+9bddab83625b4ca7/node_modules/expo-haptics/ios"
|
||||
:path: "../../../node_modules/.bun/expo-haptics@15.0.8+3ee4ad13fefe912b/node_modules/expo-haptics/ios"
|
||||
ExpoHead:
|
||||
:path: "../../../node_modules/.bun/expo-router@6.0.17+74a3cf8ce2a4bc65/node_modules/expo-router/ios"
|
||||
:path: "../../../node_modules/.bun/expo-router@6.0.17+7dc14032edcce378/node_modules/expo-router/ios"
|
||||
ExpoImage:
|
||||
:path: "../../../node_modules/.bun/expo-image@3.0.11+9bddab83625b4ca7/node_modules/expo-image/ios"
|
||||
:path: "../../../node_modules/.bun/expo-image@3.0.11+3ee4ad13fefe912b/node_modules/expo-image/ios"
|
||||
ExpoKeepAwake:
|
||||
:path: "../../../node_modules/.bun/expo-keep-awake@15.0.8+ddb0696906414ead/node_modules/expo-keep-awake/ios"
|
||||
ExpoLinking:
|
||||
:path: "../../../node_modules/.bun/expo-linking@8.0.10+9bddab83625b4ca7/node_modules/expo-linking/ios"
|
||||
:path: "../../../node_modules/.bun/expo-linking@8.0.10+3ee4ad13fefe912b/node_modules/expo-linking/ios"
|
||||
ExpoModulesCore:
|
||||
:path: "../../../node_modules/.bun/expo-modules-core@3.0.28+c1b4cdd6e4cb2f11/node_modules/expo-modules-core"
|
||||
:path: "../../../node_modules/.bun/expo-modules-core@3.0.28+87dd5a4c738f4c73/node_modules/expo-modules-core"
|
||||
ExpoSplashScreen:
|
||||
:path: "../../../node_modules/.bun/expo-splash-screen@31.0.12+9bddab83625b4ca7/node_modules/expo-splash-screen/ios"
|
||||
:path: "../../../node_modules/.bun/expo-splash-screen@31.0.12+3ee4ad13fefe912b/node_modules/expo-splash-screen/ios"
|
||||
EXUpdatesInterface:
|
||||
:path: "../../../node_modules/.bun/expo-updates-interface@2.0.0+9bddab83625b4ca7/node_modules/expo-updates-interface/ios"
|
||||
:path: "../../../node_modules/.bun/expo-updates-interface@2.0.0+3ee4ad13fefe912b/node_modules/expo-updates-interface/ios"
|
||||
FBLazyVector:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/FBLazyVector"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/FBLazyVector"
|
||||
hermes-engine:
|
||||
:podspec: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
||||
:podspec: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
||||
:tag: hermes-2025-07-07-RNv0.81.0-e0fc67142ec0763c6b6153ca2bf96df815539782
|
||||
LiquidGlass:
|
||||
:path: "../../../node_modules/.bun/@callstack+liquid-glass@0.7.0+c1b4cdd6e4cb2f11/node_modules/@callstack/liquid-glass"
|
||||
:path: "../../../node_modules/.bun/@callstack+liquid-glass@0.7.0+87dd5a4c738f4c73/node_modules/@callstack/liquid-glass"
|
||||
RCTDeprecation:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
|
||||
RCTRequired:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Required"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Required"
|
||||
RCTTypeSafety:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/TypeSafety"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/TypeSafety"
|
||||
React:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/"
|
||||
React-callinvoker:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/callinvoker"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/callinvoker"
|
||||
React-Core:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/"
|
||||
React-Core-prebuilt:
|
||||
:podspec: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/React-Core-prebuilt.podspec"
|
||||
:podspec: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/React-Core-prebuilt.podspec"
|
||||
React-CoreModules:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/React/CoreModules"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/React/CoreModules"
|
||||
React-cxxreact:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/cxxreact"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/cxxreact"
|
||||
React-debug:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/debug"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/debug"
|
||||
React-defaultsnativemodule:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/defaults"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/defaults"
|
||||
React-domnativemodule:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/dom"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/dom"
|
||||
React-Fabric:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon"
|
||||
React-FabricComponents:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon"
|
||||
React-FabricImage:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon"
|
||||
React-featureflags:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/featureflags"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/featureflags"
|
||||
React-featureflagsnativemodule:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
|
||||
React-graphics:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/graphics"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/graphics"
|
||||
React-hermes:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/hermes"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/hermes"
|
||||
React-idlecallbacksnativemodule:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
|
||||
React-ImageManager:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
|
||||
React-jserrorhandler:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jserrorhandler"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jserrorhandler"
|
||||
React-jsi:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsi"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsi"
|
||||
React-jsiexecutor:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsiexecutor"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsiexecutor"
|
||||
React-jsinspector:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsinspector-modern"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsinspector-modern"
|
||||
React-jsinspectorcdp:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsinspector-modern/cdp"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsinspector-modern/cdp"
|
||||
React-jsinspectornetwork:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsinspector-modern/network"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsinspector-modern/network"
|
||||
React-jsinspectortracing:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
|
||||
React-jsitooling:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/jsitooling"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/jsitooling"
|
||||
React-jsitracing:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/hermes/executor/"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/hermes/executor/"
|
||||
React-logger:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/logger"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/logger"
|
||||
React-Mapbuffer:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon"
|
||||
React-microtasksnativemodule:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
|
||||
react-native-safe-area-context:
|
||||
:path: "../../../node_modules/.bun/react-native-safe-area-context@5.6.2+c1b4cdd6e4cb2f11/node_modules/react-native-safe-area-context"
|
||||
:path: "../../../node_modules/.bun/react-native-safe-area-context@5.6.2+87dd5a4c738f4c73/node_modules/react-native-safe-area-context"
|
||||
react-native-slider:
|
||||
:path: "../../../node_modules/.bun/@react-native-community+slider@5.1.1/node_modules/@react-native-community/slider"
|
||||
React-NativeModulesApple:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
||||
React-oscompat:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/oscompat"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/oscompat"
|
||||
React-perflogger:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/reactperflogger"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/reactperflogger"
|
||||
React-performancetimeline:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/performance/timeline"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/performance/timeline"
|
||||
React-RCTActionSheet:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/ActionSheetIOS"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/ActionSheetIOS"
|
||||
React-RCTAnimation:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/NativeAnimation"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/NativeAnimation"
|
||||
React-RCTAppDelegate:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/AppDelegate"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/AppDelegate"
|
||||
React-RCTBlob:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Blob"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Blob"
|
||||
React-RCTFabric:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/React"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/React"
|
||||
React-RCTFBReactNativeSpec:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/React"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/React"
|
||||
React-RCTImage:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Image"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Image"
|
||||
React-RCTLinking:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/LinkingIOS"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/LinkingIOS"
|
||||
React-RCTNetwork:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Network"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Network"
|
||||
React-RCTRuntime:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/React/Runtime"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/React/Runtime"
|
||||
React-RCTSettings:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Settings"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Settings"
|
||||
React-RCTText:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Text"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Text"
|
||||
React-RCTVibration:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/Libraries/Vibration"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/Libraries/Vibration"
|
||||
React-rendererconsistency:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/consistency"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/consistency"
|
||||
React-renderercss:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/css"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/css"
|
||||
React-rendererdebug:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/debug"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/debug"
|
||||
React-RuntimeApple:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/runtime/platform/ios"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/runtime/platform/ios"
|
||||
React-RuntimeCore:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/runtime"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/runtime"
|
||||
React-runtimeexecutor:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/runtimeexecutor"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/runtimeexecutor"
|
||||
React-RuntimeHermes:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/runtime"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/runtime"
|
||||
React-runtimescheduler:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
|
||||
React-timing:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/timing"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/timing"
|
||||
React-utils:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/react/utils"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/react/utils"
|
||||
ReactAppDependencyProvider:
|
||||
:path: build/generated/ios
|
||||
ReactCodegen:
|
||||
:path: build/generated/ios
|
||||
ReactCommon:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon"
|
||||
ReactNativeDependencies:
|
||||
:podspec: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec"
|
||||
:podspec: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec"
|
||||
RNCAsyncStorage:
|
||||
:path: "../../../node_modules/.bun/@react-native-async-storage+async-storage@2.2.0+c1b4cdd6e4cb2f11/node_modules/@react-native-async-storage/async-storage"
|
||||
:path: "../../../node_modules/.bun/@react-native-async-storage+async-storage@2.2.0+87dd5a4c738f4c73/node_modules/@react-native-async-storage/async-storage"
|
||||
RNCClipboard:
|
||||
:path: "../../../node_modules/.bun/@react-native-clipboard+clipboard@1.16.3+c1b4cdd6e4cb2f11/node_modules/@react-native-clipboard/clipboard"
|
||||
:path: "../../../node_modules/.bun/@react-native-clipboard+clipboard@1.16.3+87dd5a4c738f4c73/node_modules/@react-native-clipboard/clipboard"
|
||||
RNGestureHandler:
|
||||
:path: "../../../node_modules/.bun/react-native-gesture-handler@2.28.0+c1b4cdd6e4cb2f11/node_modules/react-native-gesture-handler"
|
||||
:path: "../../../node_modules/.bun/react-native-gesture-handler@2.28.0+87dd5a4c738f4c73/node_modules/react-native-gesture-handler"
|
||||
RNReanimated:
|
||||
:path: "../../../node_modules/.bun/react-native-reanimated@4.1.6+58856117def9c0ae/node_modules/react-native-reanimated"
|
||||
:path: "../../../node_modules/.bun/react-native-reanimated@4.1.6+d983531a34c8e10a/node_modules/react-native-reanimated"
|
||||
RNScreens:
|
||||
:path: "../../../node_modules/.bun/react-native-screens@4.16.0+c1b4cdd6e4cb2f11/node_modules/react-native-screens"
|
||||
:path: "../../../node_modules/.bun/react-native-screens@4.16.0+87dd5a4c738f4c73/node_modules/react-native-screens"
|
||||
RNSVG:
|
||||
:path: "../../../node_modules/.bun/react-native-svg@15.12.1+c1b4cdd6e4cb2f11/node_modules/react-native-svg"
|
||||
:path: "../../../node_modules/.bun/react-native-svg@15.12.1+87dd5a4c738f4c73/node_modules/react-native-svg"
|
||||
RNWorklets:
|
||||
:path: "../../../node_modules/.bun/react-native-worklets@0.7.1+c1b4cdd6e4cb2f11/node_modules/react-native-worklets"
|
||||
:path: "../../../node_modules/.bun/react-native-worklets@0.7.1+87dd5a4c738f4c73/node_modules/react-native-worklets"
|
||||
SDMobileCore:
|
||||
:path: "../modules/sd-mobile-core/ios"
|
||||
Yoga:
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native/ReactCommon/yoga"
|
||||
:path: "../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
EXConstants: c378c1b344ff1ecfbad27b90f0e48d1d0ead8cbb
|
||||
@@ -2708,7 +2708,7 @@ SPEC CHECKSUMS:
|
||||
EXUpdatesInterface: 5adf50cb41e079c861da6d9b4b954c3db9a50734
|
||||
FBLazyVector: e95a291ad2dadb88e42b06e0c5fb8262de53ec12
|
||||
hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172
|
||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||
libavif: 5f8e715bea24debec477006f21ef9e95432e254d
|
||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
LiquidGlass: 2bbbcea458d5a2b018c2dd024040c4023d73eec8
|
||||
@@ -2775,18 +2775,18 @@ SPEC CHECKSUMS:
|
||||
React-timing: 6fa9883de2e41791e5dc4ec404e5e37f3f50e801
|
||||
React-utils: 6e2035b53d087927768649a11a26c4e092448e34
|
||||
ReactAppDependencyProvider: 1bcd3527ac0390a1c898c114f81ff954be35ed79
|
||||
ReactCodegen: f7a907c67cee1b036d34e217698f27696f14f403
|
||||
ReactCodegen: 9c2af94dc4ee5c1b98fb465418036615be3793b3
|
||||
ReactCommon: 08810150b1206cc44aecf5f6ae19af32f29151a8
|
||||
ReactNativeDependencies: 71ce9c28beb282aa720ea7b46980fff9669f428a
|
||||
RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4
|
||||
RNCClipboard: 88d7eeb555d1183915f0885bdbc5c97eb6f7f3ba
|
||||
RNGestureHandler: 2914750df066d89bf9d8f48a10ad5f0051108ac3
|
||||
RNReanimated: a242d405fcd1b7206cfa42dfc24a747a6d2cac23
|
||||
RNReanimated: 83246804817326398f1506dd916bf6fe47fa6242
|
||||
RNScreens: d8d6f1792f6e7ac12b0190d33d8d390efc0c1845
|
||||
RNSVG: 31d6639663c249b7d5abc9728dde2041eb2a3c34
|
||||
RNWorklets: 64422f411fb58e8e20725853a2d7858b599af7e2
|
||||
SDMobileCore: ced74b2c27bd0e0d87f2281a6ddb7a209176ae50
|
||||
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
|
||||
RNWorklets: bdca513296f69bf7fe8418208da31447c65b23ed
|
||||
SDMobileCore: 1dd1a6e1e9d5e9702dcca9cc51a87bc678b0a6c4
|
||||
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
||||
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
|
||||
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
@@ -517,7 +517,7 @@
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/.bun/react-native@0.81.5+c1b4cdd6e4cb2f11/node_modules/react-native";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/.bun/react-native@0.81.5+87dd5a4c738f4c73/node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
USE_HERMES = true;
|
||||
|
||||
219
core/src/data/manager.rs
Normal file
219
core/src/data/manager.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
//! SourceManager: library-scoped wrapper around sd-archive Engine.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sd_archive::{Engine, EngineConfig};
|
||||
use tracing::info;
|
||||
|
||||
/// Manages archive data sources for a single library.
|
||||
pub struct SourceManager {
|
||||
engine: Engine,
|
||||
}
|
||||
|
||||
impl SourceManager {
|
||||
/// Create a new source manager rooted at the library's archive directory.
|
||||
pub async fn new(library_path: PathBuf) -> Result<Self, String> {
|
||||
let data_dir = library_path.join("archive");
|
||||
|
||||
let config = EngineConfig {
|
||||
data_dir: data_dir.clone(),
|
||||
};
|
||||
let engine = Engine::new(config)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to initialize archive engine: {e}"))?;
|
||||
|
||||
// Sync bundled adapters from the source tree into the installed adapters
|
||||
// directory. Uses CARGO_MANIFEST_DIR at compile time to find the workspace
|
||||
// root, matching the pattern from the spacedrive-data prototype.
|
||||
let installed_dir = data_dir.join("adapters");
|
||||
Self::sync_bundled_adapters(&installed_dir);
|
||||
|
||||
// Reload adapters after sync (picks up any newly copied adapters)
|
||||
Engine::load_script_adapters(&installed_dir, engine.adapters())
|
||||
.map_err(|e| format!("Failed to reload adapters: {e}"))?;
|
||||
|
||||
info!("Source manager initialized at {}", library_path.display());
|
||||
|
||||
Ok(Self { engine })
|
||||
}
|
||||
|
||||
/// Sync bundled adapters from the compile-time workspace into the installed
|
||||
/// adapters directory. New adapters are copied; existing ones are updated if
|
||||
/// the adapter.toml has changed.
|
||||
fn sync_bundled_adapters(installed_dir: &std::path::Path) {
|
||||
let source_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.map(|p| p.join("adapters"));
|
||||
|
||||
let source_dir = match source_dir {
|
||||
Some(d) if d.is_dir() => d,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
info!(
|
||||
"Syncing bundled adapters from {} to {}",
|
||||
source_dir.display(),
|
||||
installed_dir.display()
|
||||
);
|
||||
|
||||
let entries = match std::fs::read_dir(&source_dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let src_path = entry.path();
|
||||
if !src_path.is_dir() || !src_path.join("adapter.toml").exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let adapter_name = match src_path.file_name() {
|
||||
Some(n) => n.to_owned(),
|
||||
None => continue,
|
||||
};
|
||||
let dest_path = installed_dir.join(&adapter_name);
|
||||
|
||||
if !dest_path.exists() {
|
||||
// New adapter — copy entire directory
|
||||
if let Err(e) = copy_dir_recursive(&src_path, &dest_path) {
|
||||
tracing::warn!(
|
||||
adapter = ?adapter_name,
|
||||
error = %e,
|
||||
"failed to install bundled adapter"
|
||||
);
|
||||
} else {
|
||||
info!(adapter = ?adapter_name, "installed bundled adapter");
|
||||
}
|
||||
} else {
|
||||
// Existing adapter — update if adapter.toml changed
|
||||
let src_manifest = std::fs::read_to_string(src_path.join("adapter.toml"));
|
||||
let dest_manifest = std::fs::read_to_string(dest_path.join("adapter.toml"));
|
||||
|
||||
if let (Ok(src), Ok(dest)) = (src_manifest, dest_manifest) {
|
||||
if src != dest {
|
||||
if let Err(e) = copy_dir_recursive(&src_path, &dest_path) {
|
||||
tracing::warn!(
|
||||
adapter = ?adapter_name,
|
||||
error = %e,
|
||||
"failed to update bundled adapter"
|
||||
);
|
||||
} else {
|
||||
info!(adapter = ?adapter_name, "updated bundled adapter");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List all sources.
|
||||
pub async fn list_sources(&self) -> Result<Vec<sd_archive::SourceInfo>, String> {
|
||||
self.engine
|
||||
.list_sources()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list sources: {e}"))
|
||||
}
|
||||
|
||||
/// Create a new source.
|
||||
pub async fn create_source(
|
||||
&self,
|
||||
name: &str,
|
||||
adapter_id: &str,
|
||||
config: serde_json::Value,
|
||||
) -> Result<sd_archive::SourceInfo, String> {
|
||||
self.engine
|
||||
.create_source(name, adapter_id, config)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create source: {e}"))
|
||||
}
|
||||
|
||||
/// Delete a source.
|
||||
pub async fn delete_source(&self, source_id: &str) -> Result<(), String> {
|
||||
self.engine
|
||||
.delete_source(source_id)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete source: {e}"))
|
||||
}
|
||||
|
||||
/// Sync a source.
|
||||
pub async fn sync_source(
|
||||
&self,
|
||||
source_id: &str,
|
||||
) -> Result<sd_archive::SyncReport, String> {
|
||||
self.engine
|
||||
.sync(source_id)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to sync source: {e}"))
|
||||
}
|
||||
|
||||
/// List items from a source.
|
||||
pub async fn list_items(
|
||||
&self,
|
||||
source_id: &str,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<Vec<sd_archive::db::ItemRow>, String> {
|
||||
self.engine
|
||||
.list_items(source_id, limit, offset)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list items: {e}"))
|
||||
}
|
||||
|
||||
/// List available adapters with update status.
|
||||
pub fn list_adapters(&self) -> Vec<sd_archive::AdapterInfo> {
|
||||
let source_dir = self.engine.source_adapters_dir();
|
||||
self.engine
|
||||
.list_adapters_with_updates(source_dir.as_deref())
|
||||
}
|
||||
|
||||
/// Update an installed adapter from its source directory.
|
||||
pub fn update_adapter(
|
||||
&self,
|
||||
adapter_id: &str,
|
||||
) -> Result<sd_archive::AdapterUpdateResult, String> {
|
||||
let source_dir = self
|
||||
.engine
|
||||
.source_adapters_dir()
|
||||
.ok_or_else(|| "Cannot find source adapters directory".to_string())?
|
||||
.join(adapter_id);
|
||||
|
||||
if !source_dir.join("adapter.toml").exists() {
|
||||
return Err(format!("No source adapter found for '{adapter_id}'"));
|
||||
}
|
||||
|
||||
self.engine
|
||||
.update_adapter(adapter_id, &source_dir)
|
||||
.map_err(|e| format!("Failed to update adapter: {e}"))
|
||||
}
|
||||
|
||||
/// Get config fields for an adapter.
|
||||
pub fn adapter_config_fields(
|
||||
&self,
|
||||
adapter_id: &str,
|
||||
) -> Result<Vec<sd_archive::adapter::script::ConfigField>, String> {
|
||||
self.engine
|
||||
.adapter_config_fields(adapter_id)
|
||||
.map_err(|e| format!("Failed to get adapter config: {e}"))
|
||||
}
|
||||
|
||||
/// Get the underlying engine.
|
||||
pub fn engine(&self) -> &Engine {
|
||||
&self.engine
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively copy a directory.
|
||||
fn copy_dir_recursive(src: &std::path::Path, dest: &std::path::Path) -> Result<(), String> {
|
||||
std::fs::create_dir_all(dest).map_err(|e| e.to_string())?;
|
||||
for entry in std::fs::read_dir(src).map_err(|e| e.to_string())? {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
let src_path = entry.path();
|
||||
let dest_path = dest.join(entry.file_name());
|
||||
if src_path.is_dir() {
|
||||
copy_dir_recursive(&src_path, &dest_path)?;
|
||||
} else {
|
||||
std::fs::copy(&src_path, &dest_path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
5
core/src/data/mod.rs
Normal file
5
core/src/data/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! Data module: manages archive data sources within a library.
|
||||
//!
|
||||
//! Wraps `sd-archive` engine to provide library-scoped source management.
|
||||
|
||||
pub mod manager;
|
||||
@@ -231,6 +231,9 @@ pub enum GroupType {
|
||||
/// Tag collection
|
||||
Tags,
|
||||
|
||||
/// Archive data sources (email, notes, bookmarks, etc.)
|
||||
Sources,
|
||||
|
||||
/// Cloud storage providers
|
||||
Cloud,
|
||||
|
||||
@@ -436,6 +439,12 @@ pub enum ItemType {
|
||||
|
||||
/// Any arbitrary path (dragged from explorer)
|
||||
Path { sd_path: SdPath },
|
||||
|
||||
/// All archive data sources screen
|
||||
Sources,
|
||||
|
||||
/// Specific archive data source
|
||||
Source { source_id: String },
|
||||
}
|
||||
/// Complete sidebar layout for a space
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
|
||||
@@ -1438,6 +1438,45 @@ impl LibraryManager {
|
||||
|
||||
info!("Created default Tags group for library {}", library.id());
|
||||
|
||||
// Create Sources group
|
||||
let sources_group_id =
|
||||
deterministic_library_default_uuid(library_id, "space_group", "Sources");
|
||||
let sources_type_json = serde_json::to_string(&GroupType::Sources)
|
||||
.map_err(|e| LibraryError::Other(format!("Failed to serialize group_type: {}", e)))?;
|
||||
|
||||
let sources_group_model = crate::infra::db::entities::space_group::ActiveModel {
|
||||
id: NotSet,
|
||||
uuid: Set(sources_group_id),
|
||||
space_id: Set(space_result.id),
|
||||
name: Set("Sources".to_string()),
|
||||
group_type: Set(sources_type_json),
|
||||
is_collapsed: Set(true),
|
||||
order: Set(4),
|
||||
created_at: Set(now.into()),
|
||||
};
|
||||
|
||||
// Use atomic upsert to handle race conditions with sync
|
||||
GroupEntity::insert(sources_group_model)
|
||||
.on_conflict(
|
||||
sea_orm::sea_query::OnConflict::column(GroupColumn::Uuid)
|
||||
.update_columns([
|
||||
GroupColumn::SpaceId,
|
||||
GroupColumn::Name,
|
||||
GroupColumn::GroupType,
|
||||
GroupColumn::IsCollapsed,
|
||||
GroupColumn::Order,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(db)
|
||||
.await
|
||||
.map_err(LibraryError::DatabaseError)?;
|
||||
|
||||
info!(
|
||||
"Created default Sources group for library {}",
|
||||
library.id()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
5
core/src/ops/adapters/config/mod.rs
Normal file
5
core/src/ops/adapters/config/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! Get adapter config fields
|
||||
|
||||
pub mod query;
|
||||
|
||||
pub use query::*;
|
||||
91
core/src/ops/adapters/config/query.rs
Normal file
91
core/src/ops/adapters/config/query.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! Get adapter config fields query
|
||||
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::query::{LibraryQuery, QueryError, QueryResult},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct GetAdapterConfigInput {
|
||||
pub adapter_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct AdapterConfigField {
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub field_type: String,
|
||||
pub required: bool,
|
||||
pub secret: bool,
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GetAdapterConfigQuery {
|
||||
pub input: GetAdapterConfigInput,
|
||||
}
|
||||
|
||||
impl LibraryQuery for GetAdapterConfigQuery {
|
||||
type Input = GetAdapterConfigInput;
|
||||
type Output = Vec<AdapterConfigField>;
|
||||
|
||||
fn from_input(input: Self::Input) -> QueryResult<Self> {
|
||||
if input.adapter_id.trim().is_empty() {
|
||||
return Err(QueryError::Validation {
|
||||
field: "adapter_id".to_string(),
|
||||
message: "adapter_id cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
context: Arc<CoreContext>,
|
||||
session: crate::infra::api::SessionContext,
|
||||
) -> QueryResult<Self::Output> {
|
||||
let library_id = session
|
||||
.current_library_id
|
||||
.ok_or_else(|| QueryError::Internal("No library in session".to_string()))?;
|
||||
let library = context
|
||||
.libraries()
|
||||
.await
|
||||
.get_library(library_id)
|
||||
.await
|
||||
.ok_or_else(|| QueryError::Internal("Library not found".to_string()))?;
|
||||
|
||||
if library.source_manager().is_none() {
|
||||
library
|
||||
.init_source_manager()
|
||||
.await
|
||||
.map_err(|e| QueryError::Internal(format!("Failed to init source manager: {e}")))?;
|
||||
}
|
||||
|
||||
let source_manager = library
|
||||
.source_manager()
|
||||
.ok_or_else(|| QueryError::Internal("Source manager not available".to_string()))?;
|
||||
|
||||
let fields = source_manager
|
||||
.adapter_config_fields(&self.input.adapter_id)
|
||||
.map_err(|e| QueryError::Internal(e))?;
|
||||
|
||||
Ok(fields
|
||||
.into_iter()
|
||||
.map(|f| AdapterConfigField {
|
||||
key: f.key,
|
||||
name: f.name,
|
||||
description: f.description,
|
||||
field_type: f.field_type,
|
||||
required: f.required,
|
||||
secret: f.secret,
|
||||
default: f.default.map(|d| d.to_string()),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_query!(GetAdapterConfigQuery, "adapters.config");
|
||||
5
core/src/ops/adapters/list/mod.rs
Normal file
5
core/src/ops/adapters/list/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! List available adapters
|
||||
|
||||
pub mod query;
|
||||
|
||||
pub use query::*;
|
||||
83
core/src/ops/adapters/list/query.rs
Normal file
83
core/src/ops/adapters/list/query.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! List available adapters query
|
||||
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::query::{LibraryQuery, QueryError, QueryResult},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct ListAdaptersInput {}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct AdapterInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub version: String,
|
||||
pub author: String,
|
||||
pub data_type: String,
|
||||
pub icon_svg: Option<String>,
|
||||
pub update_available: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListAdaptersQuery {
|
||||
pub input: ListAdaptersInput,
|
||||
}
|
||||
|
||||
impl LibraryQuery for ListAdaptersQuery {
|
||||
type Input = ListAdaptersInput;
|
||||
type Output = Vec<AdapterInfo>;
|
||||
|
||||
fn from_input(input: Self::Input) -> QueryResult<Self> {
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
context: Arc<CoreContext>,
|
||||
session: crate::infra::api::SessionContext,
|
||||
) -> QueryResult<Self::Output> {
|
||||
let library_id = session
|
||||
.current_library_id
|
||||
.ok_or_else(|| QueryError::Internal("No library in session".to_string()))?;
|
||||
let library = context
|
||||
.libraries()
|
||||
.await
|
||||
.get_library(library_id)
|
||||
.await
|
||||
.ok_or_else(|| QueryError::Internal("Library not found".to_string()))?;
|
||||
|
||||
if library.source_manager().is_none() {
|
||||
library
|
||||
.init_source_manager()
|
||||
.await
|
||||
.map_err(|e| QueryError::Internal(format!("Failed to init source manager: {e}")))?;
|
||||
}
|
||||
|
||||
let source_manager = library
|
||||
.source_manager()
|
||||
.ok_or_else(|| QueryError::Internal("Source manager not available".to_string()))?;
|
||||
|
||||
let adapters = source_manager.list_adapters();
|
||||
|
||||
Ok(adapters
|
||||
.into_iter()
|
||||
.map(|a| AdapterInfo {
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
description: a.description,
|
||||
version: a.version,
|
||||
author: a.author,
|
||||
data_type: a.data_type,
|
||||
icon_svg: a.icon_svg,
|
||||
update_available: a.update_available,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_query!(ListAdaptersQuery, "adapters.list");
|
||||
9
core/src/ops/adapters/mod.rs
Normal file
9
core/src/ops/adapters/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! Adapter operations for archive data sources.
|
||||
|
||||
pub mod config;
|
||||
pub mod list;
|
||||
pub mod update;
|
||||
|
||||
pub use config::*;
|
||||
pub use list::*;
|
||||
pub use update::*;
|
||||
73
core/src/ops/adapters/update/action.rs
Normal file
73
core/src/ops/adapters/update/action.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
//! Adapter update action handler
|
||||
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::action::{error::ActionError, LibraryAction},
|
||||
library::Library,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct UpdateAdapterInput {
|
||||
pub adapter_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct UpdateAdapterOutput {
|
||||
pub adapter_id: String,
|
||||
pub old_version: String,
|
||||
pub new_version: String,
|
||||
pub schema_changed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateAdapterAction {
|
||||
input: UpdateAdapterInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for UpdateAdapterAction {
|
||||
type Input = UpdateAdapterInput;
|
||||
type Output = UpdateAdapterOutput;
|
||||
|
||||
fn from_input(input: UpdateAdapterInput) -> Result<Self, String> {
|
||||
if input.adapter_id.trim().is_empty() {
|
||||
return Err("Adapter ID cannot be empty".to_string());
|
||||
}
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: Arc<Library>,
|
||||
_context: Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
if library.source_manager().is_none() {
|
||||
library.init_source_manager().await.map_err(|e| {
|
||||
ActionError::Internal(format!("Failed to init source manager: {e}"))
|
||||
})?;
|
||||
}
|
||||
|
||||
let source_manager = library
|
||||
.source_manager()
|
||||
.ok_or_else(|| ActionError::Internal("Source manager not available".to_string()))?;
|
||||
|
||||
let result = source_manager
|
||||
.update_adapter(&self.input.adapter_id)
|
||||
.map_err(|e| ActionError::Internal(e))?;
|
||||
|
||||
Ok(UpdateAdapterOutput {
|
||||
adapter_id: result.adapter_id,
|
||||
old_version: result.old_version,
|
||||
new_version: result.new_version,
|
||||
schema_changed: result.schema_changed,
|
||||
})
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"adapters.update"
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(UpdateAdapterAction, "adapters.update");
|
||||
5
core/src/ops/adapters/update/mod.rs
Normal file
5
core/src/ops/adapters/update/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! Update an adapter
|
||||
|
||||
pub mod action;
|
||||
|
||||
pub use action::*;
|
||||
@@ -25,6 +25,7 @@ pub mod models;
|
||||
pub mod network;
|
||||
pub mod search;
|
||||
pub mod sidecar;
|
||||
pub mod adapters;
|
||||
pub mod sources;
|
||||
pub mod spaces;
|
||||
pub mod sync;
|
||||
|
||||
66
core/src/ops/sources/delete/action.rs
Normal file
66
core/src/ops/sources/delete/action.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! Source deletion action handler
|
||||
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::action::{error::ActionError, LibraryAction},
|
||||
library::Library,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct DeleteSourceInput {
|
||||
pub source_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct DeleteSourceOutput {
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeleteSourceAction {
|
||||
input: DeleteSourceInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for DeleteSourceAction {
|
||||
type Input = DeleteSourceInput;
|
||||
type Output = DeleteSourceOutput;
|
||||
|
||||
fn from_input(input: DeleteSourceInput) -> Result<Self, String> {
|
||||
if input.source_id.trim().is_empty() {
|
||||
return Err("Source ID cannot be empty".to_string());
|
||||
}
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: Arc<Library>,
|
||||
_context: Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
if library.source_manager().is_none() {
|
||||
library.init_source_manager().await.map_err(|e| {
|
||||
ActionError::Internal(format!("Failed to init source manager: {e}"))
|
||||
})?;
|
||||
}
|
||||
|
||||
let source_manager = library
|
||||
.source_manager()
|
||||
.ok_or_else(|| ActionError::Internal("Source manager not available".to_string()))?;
|
||||
|
||||
source_manager
|
||||
.delete_source(&self.input.source_id)
|
||||
.await
|
||||
.map_err(|e| ActionError::Internal(e))?;
|
||||
|
||||
Ok(DeleteSourceOutput { deleted: true })
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"sources.delete"
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(DeleteSourceAction, "sources.delete");
|
||||
5
core/src/ops/sources/delete/mod.rs
Normal file
5
core/src/ops/sources/delete/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! Source deletion action
|
||||
|
||||
pub mod action;
|
||||
|
||||
pub use action::*;
|
||||
5
core/src/ops/sources/get/mod.rs
Normal file
5
core/src/ops/sources/get/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! Get a single source by ID
|
||||
|
||||
pub mod query;
|
||||
|
||||
pub use query::*;
|
||||
90
core/src/ops/sources/get/query.rs
Normal file
90
core/src/ops/sources/get/query.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
//! Single source query implementation
|
||||
|
||||
use crate::ops::sources::list::SourceInfo;
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::query::{LibraryQuery, QueryError, QueryResult},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct GetSourceInput {
|
||||
pub source_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GetSourceQuery {
|
||||
pub input: GetSourceInput,
|
||||
}
|
||||
|
||||
impl LibraryQuery for GetSourceQuery {
|
||||
type Input = GetSourceInput;
|
||||
type Output = SourceInfo;
|
||||
|
||||
fn from_input(input: Self::Input) -> QueryResult<Self> {
|
||||
if input.source_id.trim().is_empty() {
|
||||
return Err(QueryError::Validation {
|
||||
field: "source_id".to_string(),
|
||||
message: "source_id cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
context: Arc<CoreContext>,
|
||||
session: crate::infra::api::SessionContext,
|
||||
) -> QueryResult<Self::Output> {
|
||||
let library_id = session
|
||||
.current_library_id
|
||||
.ok_or_else(|| QueryError::Internal("No library in session".to_string()))?;
|
||||
let library = context
|
||||
.libraries()
|
||||
.await
|
||||
.get_library(library_id)
|
||||
.await
|
||||
.ok_or_else(|| QueryError::Internal("Library not found".to_string()))?;
|
||||
|
||||
if library.source_manager().is_none() {
|
||||
library
|
||||
.init_source_manager()
|
||||
.await
|
||||
.map_err(|e| QueryError::Internal(format!("Failed to init source manager: {e}")))?;
|
||||
}
|
||||
|
||||
let source_manager = library
|
||||
.source_manager()
|
||||
.ok_or_else(|| QueryError::Internal("Source manager not available".to_string()))?;
|
||||
|
||||
let sources = source_manager
|
||||
.list_sources()
|
||||
.await
|
||||
.map_err(|e| QueryError::Internal(e))?;
|
||||
|
||||
let source = sources
|
||||
.into_iter()
|
||||
.find(|s| s.id == self.input.source_id)
|
||||
.ok_or_else(|| {
|
||||
QueryError::Internal(format!("Source not found: {}", self.input.source_id))
|
||||
})?;
|
||||
|
||||
let id = Uuid::parse_str(&source.id)
|
||||
.map_err(|e| QueryError::Internal(format!("Invalid source ID: {e}")))?;
|
||||
|
||||
Ok(SourceInfo::new(
|
||||
id,
|
||||
source.name,
|
||||
source.data_type,
|
||||
source.adapter_id,
|
||||
source.item_count,
|
||||
source.last_synced,
|
||||
source.status,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_query!(GetSourceQuery, "sources.get");
|
||||
5
core/src/ops/sources/list_items/mod.rs
Normal file
5
core/src/ops/sources/list_items/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! Source items listing query
|
||||
|
||||
pub mod query;
|
||||
|
||||
pub use query::*;
|
||||
94
core/src/ops/sources/list_items/query.rs
Normal file
94
core/src/ops/sources/list_items/query.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
//! Source items listing query implementation
|
||||
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::query::{LibraryQuery, QueryError, QueryResult},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct ListSourceItemsInput {
|
||||
pub source_id: String,
|
||||
pub limit: u32,
|
||||
pub offset: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SourceItem {
|
||||
pub id: String,
|
||||
pub external_id: String,
|
||||
pub title: String,
|
||||
pub preview: Option<String>,
|
||||
pub subtitle: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListSourceItemsQuery {
|
||||
pub input: ListSourceItemsInput,
|
||||
}
|
||||
|
||||
impl LibraryQuery for ListSourceItemsQuery {
|
||||
type Input = ListSourceItemsInput;
|
||||
type Output = Vec<SourceItem>;
|
||||
|
||||
fn from_input(input: Self::Input) -> QueryResult<Self> {
|
||||
if input.source_id.trim().is_empty() {
|
||||
return Err(QueryError::Validation {
|
||||
field: "source_id".to_string(),
|
||||
message: "source_id cannot be empty".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
context: Arc<CoreContext>,
|
||||
session: crate::infra::api::SessionContext,
|
||||
) -> QueryResult<Self::Output> {
|
||||
let library_id = session
|
||||
.current_library_id
|
||||
.ok_or_else(|| QueryError::Internal("No library in session".to_string()))?;
|
||||
let library = context
|
||||
.libraries()
|
||||
.await
|
||||
.get_library(library_id)
|
||||
.await
|
||||
.ok_or_else(|| QueryError::Internal("Library not found".to_string()))?;
|
||||
|
||||
if library.source_manager().is_none() {
|
||||
library
|
||||
.init_source_manager()
|
||||
.await
|
||||
.map_err(|e| QueryError::Internal(format!("Failed to init source manager: {e}")))?;
|
||||
}
|
||||
|
||||
let source_manager = library
|
||||
.source_manager()
|
||||
.ok_or_else(|| QueryError::Internal("Source manager not available".to_string()))?;
|
||||
|
||||
let items = source_manager
|
||||
.list_items(
|
||||
&self.input.source_id,
|
||||
self.input.limit as usize,
|
||||
self.input.offset as usize,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| QueryError::Internal(e))?;
|
||||
|
||||
Ok(items
|
||||
.into_iter()
|
||||
.map(|item| SourceItem {
|
||||
id: item.id,
|
||||
external_id: item.external_id,
|
||||
title: item.title,
|
||||
preview: item.preview,
|
||||
subtitle: item.subtitle,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_query!(ListSourceItemsQuery, "sources.list_items");
|
||||
@@ -4,7 +4,15 @@
|
||||
//! like emails, notes, bookmarks, etc. from various adapters.
|
||||
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod get;
|
||||
pub mod list;
|
||||
pub mod list_items;
|
||||
pub mod sync;
|
||||
|
||||
pub use create::*;
|
||||
pub use delete::*;
|
||||
pub use get::*;
|
||||
pub use list::*;
|
||||
pub use list_items::*;
|
||||
pub use sync::*;
|
||||
|
||||
74
core/src/ops/sources/sync/action.rs
Normal file
74
core/src/ops/sources/sync/action.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! Source sync action handler
|
||||
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infra::action::{error::ActionError, LibraryAction},
|
||||
library::Library,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SyncSourceInput {
|
||||
pub source_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
pub struct SyncSourceOutput {
|
||||
pub records_upserted: u64,
|
||||
pub records_deleted: u64,
|
||||
pub duration_ms: u64,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SyncSourceAction {
|
||||
input: SyncSourceInput,
|
||||
}
|
||||
|
||||
impl LibraryAction for SyncSourceAction {
|
||||
type Input = SyncSourceInput;
|
||||
type Output = SyncSourceOutput;
|
||||
|
||||
fn from_input(input: SyncSourceInput) -> Result<Self, String> {
|
||||
if input.source_id.trim().is_empty() {
|
||||
return Err("Source ID cannot be empty".to_string());
|
||||
}
|
||||
Ok(Self { input })
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
self,
|
||||
library: Arc<Library>,
|
||||
_context: Arc<CoreContext>,
|
||||
) -> Result<Self::Output, ActionError> {
|
||||
if library.source_manager().is_none() {
|
||||
library.init_source_manager().await.map_err(|e| {
|
||||
ActionError::Internal(format!("Failed to init source manager: {e}"))
|
||||
})?;
|
||||
}
|
||||
|
||||
let source_manager = library
|
||||
.source_manager()
|
||||
.ok_or_else(|| ActionError::Internal("Source manager not available".to_string()))?;
|
||||
|
||||
let report = source_manager
|
||||
.sync_source(&self.input.source_id)
|
||||
.await
|
||||
.map_err(|e| ActionError::Internal(e))?;
|
||||
|
||||
Ok(SyncSourceOutput {
|
||||
records_upserted: report.records_upserted,
|
||||
records_deleted: report.records_deleted,
|
||||
duration_ms: report.duration_ms,
|
||||
error: report.error,
|
||||
})
|
||||
}
|
||||
|
||||
fn action_kind(&self) -> &'static str {
|
||||
"sources.sync"
|
||||
}
|
||||
}
|
||||
|
||||
crate::register_library_action!(SyncSourceAction, "sources.sync");
|
||||
5
core/src/ops/sources/sync/mod.rs
Normal file
5
core/src/ops/sources/sync/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! Source sync action
|
||||
|
||||
pub mod action;
|
||||
|
||||
pub use action::*;
|
||||
@@ -92,7 +92,7 @@ impl Engine {
|
||||
}
|
||||
|
||||
/// Load all script adapters from the adapters directory.
|
||||
fn load_script_adapters(
|
||||
pub fn load_script_adapters(
|
||||
adapters_dir: &std::path::Path,
|
||||
registry: &AdapterRegistry,
|
||||
) -> Result<()> {
|
||||
@@ -529,6 +529,142 @@ impl Engine {
|
||||
Ok(manifest.adapter.config)
|
||||
}
|
||||
|
||||
/// Check whether a source adapter directory has changed compared to the installed version.
|
||||
pub fn check_adapter_update(
|
||||
&self,
|
||||
adapter_id: &str,
|
||||
source_dir: &std::path::Path,
|
||||
) -> Option<bool> {
|
||||
let installed_toml = self
|
||||
.config
|
||||
.data_dir
|
||||
.join("adapters")
|
||||
.join(adapter_id)
|
||||
.join("adapter.toml");
|
||||
let source_toml = source_dir.join("adapter.toml");
|
||||
|
||||
if !installed_toml.exists() || !source_toml.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let installed_content = std::fs::read(&installed_toml).ok()?;
|
||||
let source_content = std::fs::read(&source_toml).ok()?;
|
||||
|
||||
let installed_hash = blake3::hash(&installed_content);
|
||||
let source_hash = blake3::hash(&source_content);
|
||||
|
||||
Some(installed_hash != source_hash)
|
||||
}
|
||||
|
||||
/// List adapters with update-available status.
|
||||
pub fn list_adapters_with_updates(
|
||||
&self,
|
||||
source_adapters_dir: Option<&std::path::Path>,
|
||||
) -> Vec<crate::adapter::AdapterInfo> {
|
||||
let mut infos = self.adapters.list();
|
||||
|
||||
if let Some(source_dir) = source_adapters_dir {
|
||||
for info in &mut infos {
|
||||
let adapter_source = source_dir.join(&info.id);
|
||||
if let Some(has_update) = self.check_adapter_update(&info.id, &adapter_source) {
|
||||
info.update_available = has_update;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
infos
|
||||
}
|
||||
|
||||
/// The path to the bundled adapters directory (workspace root's adapters/).
|
||||
pub fn source_adapters_dir(&self) -> Option<std::path::PathBuf> {
|
||||
let candidates = [
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.map(|p| p.join("adapters")),
|
||||
Some(self.config.data_dir.join("bundled_adapters")),
|
||||
];
|
||||
|
||||
for candidate in candidates.into_iter().flatten() {
|
||||
if candidate.is_dir() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Update an installed adapter from a source directory.
|
||||
///
|
||||
/// Backs up the installed adapter, copies new files, and re-registers.
|
||||
/// Schema migrations happen automatically on next sync.
|
||||
pub fn update_adapter(
|
||||
&self,
|
||||
adapter_id: &str,
|
||||
source_dir: &std::path::Path,
|
||||
) -> Result<crate::adapter::AdapterUpdateResult> {
|
||||
let new_adapter = ScriptAdapter::from_dir(source_dir)?;
|
||||
if new_adapter.id() != adapter_id {
|
||||
return Err(Error::Other(format!(
|
||||
"adapter ID mismatch: expected '{}', got '{}'",
|
||||
adapter_id,
|
||||
new_adapter.id()
|
||||
)));
|
||||
}
|
||||
|
||||
let installed_dir = self.config.data_dir.join("adapters").join(adapter_id);
|
||||
if !installed_dir.exists() {
|
||||
return Err(Error::AdapterNotFound(adapter_id.to_string()));
|
||||
}
|
||||
|
||||
// Read old version
|
||||
let old_manifest = crate::adapter::script::AdapterManifest::from_file(
|
||||
&installed_dir.join("adapter.toml"),
|
||||
)?;
|
||||
let old_version = old_manifest.adapter.version.clone();
|
||||
let new_version = new_adapter.manifest().adapter.version.clone();
|
||||
|
||||
// Schema diff
|
||||
let old_schema = ScriptAdapter::from_dir(&installed_dir)?.schema().clone();
|
||||
let new_schema = new_adapter.schema().clone();
|
||||
let schema_changed = crate::schema::migration::schema_hash(&old_schema)
|
||||
!= crate::schema::migration::schema_hash(&new_schema);
|
||||
|
||||
// Backup
|
||||
let backup_name = format!(
|
||||
"{}.bak.{}",
|
||||
adapter_id,
|
||||
chrono::Utc::now().format("%Y%m%d_%H%M%S")
|
||||
);
|
||||
let backup_dir = self.config.data_dir.join("adapters").join(&backup_name);
|
||||
std::fs::rename(&installed_dir, &backup_dir)?;
|
||||
|
||||
tracing::info!(adapter_id, backup = %backup_dir.display(), "backed up adapter before update");
|
||||
|
||||
// Copy new files (restore backup on failure)
|
||||
if let Err(e) = copy_dir_recursive(source_dir, &installed_dir) {
|
||||
tracing::error!(adapter_id, error = %e, "update failed, restoring backup");
|
||||
if installed_dir.exists() {
|
||||
let _ = std::fs::remove_dir_all(&installed_dir);
|
||||
}
|
||||
std::fs::rename(&backup_dir, &installed_dir)?;
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Re-register
|
||||
let adapter = ScriptAdapter::from_dir(&installed_dir)?;
|
||||
self.adapters.register(Arc::new(adapter));
|
||||
|
||||
tracing::info!(adapter_id, %old_version, %new_version, schema_changed, "adapter updated");
|
||||
|
||||
Ok(crate::adapter::AdapterUpdateResult {
|
||||
adapter_id: adapter_id.to_string(),
|
||||
old_version,
|
||||
new_version,
|
||||
schema_changed,
|
||||
backup_path: backup_dir.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Install a script adapter from a directory path (sideloading).
|
||||
pub fn install_adapter(&self, source_dir: &std::path::Path) -> Result<String> {
|
||||
let adapter = ScriptAdapter::from_dir(source_dir)?;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import {
|
||||
Atom,
|
||||
Brain,
|
||||
CalendarDots,
|
||||
CaretDown,
|
||||
ChatCircleDots,
|
||||
Checks,
|
||||
ClockCounterClockwise,
|
||||
DotsThree
|
||||
DotsThree,
|
||||
MoonStars
|
||||
} from '@phosphor-icons/react';
|
||||
import {Ball, BallBlue} from '@sd/assets/images';
|
||||
import {
|
||||
@@ -39,7 +37,7 @@ export const primaryItems = [
|
||||
{icon: ChatCircleDots, label: 'Chat', path: '/spacebot/chat'},
|
||||
{icon: Checks, label: 'Tasks', path: '/spacebot/tasks'},
|
||||
{icon: Brain, label: 'Memories', path: '/spacebot/memories'},
|
||||
{icon: Atom, label: 'Autonomy', path: '/spacebot/autonomy'},
|
||||
{icon: MoonStars, label: 'Dream', path: '/spacebot/autonomy'},
|
||||
{icon: CalendarDots, label: 'Schedule', path: '/spacebot/schedule'}
|
||||
];
|
||||
|
||||
|
||||
@@ -42,11 +42,11 @@ function SidebarHistoryItem({
|
||||
isActive ? 'bg-sidebar-selected/40' : 'hover:bg-sidebar-box'
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div className="text-sidebar-ink text-sm font-medium">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sidebar-ink truncate text-sm font-medium">
|
||||
{conversation.title}
|
||||
</div>
|
||||
<div className="text-sidebar-inkDull line-clamp-2 text-xs">
|
||||
<div className="text-ink-dull truncate text-xs">
|
||||
{conversation.last_message_preview ?? 'No messages yet'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function TasksRoute() {
|
||||
|
||||
const tasksQuery = useQuery({
|
||||
queryKey,
|
||||
queryFn: () => apiClient.listTasks({agent_id: selectedAgent, limit: 200}),
|
||||
queryFn: () => apiClient.listTasks(selectedAgent, 200),
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
|
||||
66
packages/interface/src/components/Sources/SourceCard.tsx
Normal file
66
packages/interface/src/components/Sources/SourceCard.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAdapterIcons } from "../../hooks/useAdapterIcons";
|
||||
import { SourceTypeIcon } from "./SourceTypeIcon";
|
||||
import { SourceStatusBadge } from "./SourceStatusBadge";
|
||||
|
||||
interface SourceCardProps {
|
||||
source: {
|
||||
id: string;
|
||||
name: string;
|
||||
data_type: string;
|
||||
adapter_id: string;
|
||||
status: string;
|
||||
item_count: number;
|
||||
last_synced: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
function formatRelative(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHr / 24);
|
||||
|
||||
if (diffMin < 1) return "just now";
|
||||
if (diffMin < 60) return `${diffMin}m ago`;
|
||||
if (diffHr < 24) return `${diffHr}h ago`;
|
||||
if (diffDay < 30) return `${diffDay}d ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
export function SourceCard({ source }: SourceCardProps) {
|
||||
const navigate = useNavigate();
|
||||
const { getIcon } = useAdapterIcons();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => navigate(`/sources/${source.id}`)}
|
||||
className="border-app-line bg-app-box hover:border-app-line/80 hover:bg-app-hover group relative rounded-lg border p-4 text-left transition-all"
|
||||
>
|
||||
<div className="mb-3 flex items-center gap-3">
|
||||
<SourceTypeIcon type={source.data_type} svg={getIcon(source.adapter_id)} size="md" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-ink truncate text-sm font-medium">
|
||||
{source.name}
|
||||
</h3>
|
||||
<p className="text-ink-faint text-xs">{source.adapter_id}</p>
|
||||
</div>
|
||||
<SourceStatusBadge status={source.status} />
|
||||
</div>
|
||||
|
||||
<div className="text-ink-faint flex items-center justify-between text-xs">
|
||||
<span>
|
||||
{source.item_count.toLocaleString()} item
|
||||
{source.item_count !== 1 ? "s" : ""}
|
||||
</span>
|
||||
<span>
|
||||
{source.last_synced
|
||||
? formatRelative(source.last_synced)
|
||||
: "Never synced"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
55
packages/interface/src/components/Sources/SourceDataRow.tsx
Normal file
55
packages/interface/src/components/Sources/SourceDataRow.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
interface SourceDataRowProps {
|
||||
title: string;
|
||||
preview?: string | null;
|
||||
subtitle?: string | null;
|
||||
date?: string | null;
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanPreview(text: string): string {
|
||||
return text.replace(/[\r\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
|
||||
}
|
||||
|
||||
export function SourceDataRow({
|
||||
title,
|
||||
preview,
|
||||
subtitle,
|
||||
date,
|
||||
}: SourceDataRowProps) {
|
||||
return (
|
||||
<div className="hover:bg-app-hover flex min-w-0 flex-col gap-0.5 overflow-hidden rounded-lg px-3 py-2.5 transition-colors">
|
||||
{/* Title row */}
|
||||
<div className="flex min-w-0 items-baseline gap-2">
|
||||
<span className="text-ink min-w-0 flex-1 truncate text-sm">
|
||||
{title}
|
||||
</span>
|
||||
{date && (
|
||||
<span className="text-ink-faint shrink-0 text-[11px]">
|
||||
{formatDate(date)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<p className="text-ink-faint truncate text-xs">{subtitle}</p>
|
||||
)}
|
||||
{preview && (
|
||||
<p className="text-ink-faint/70 truncate text-xs">
|
||||
{cleanPreview(preview)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
packages/interface/src/components/Sources/SourcePathBar.tsx
Normal file
39
packages/interface/src/components/Sources/SourcePathBar.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { CaretRight, Database } from "@phosphor-icons/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface SourcePathBarProps {
|
||||
sourceName: string;
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
export function SourcePathBar({
|
||||
sourceName,
|
||||
itemCount,
|
||||
}: SourcePathBarProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border-app-line/50 bg-app-overlay/80 flex h-8 items-center gap-1.5 rounded-full border px-3 backdrop-blur-xl"
|
||||
>
|
||||
<Database size={14} className="text-ink-faint shrink-0" />
|
||||
|
||||
<button
|
||||
onClick={() => navigate("/sources")}
|
||||
className="text-sidebar-inkDull hover:text-sidebar-ink whitespace-nowrap text-xs font-medium transition-colors"
|
||||
>
|
||||
Sources
|
||||
</button>
|
||||
|
||||
<CaretRight size={12} className="text-ink-faint shrink-0 opacity-50" />
|
||||
|
||||
<span className="text-sidebar-ink whitespace-nowrap text-xs font-medium">
|
||||
{sourceName}
|
||||
</span>
|
||||
|
||||
<span className="text-ink-faint ml-1 whitespace-nowrap text-[11px]">
|
||||
{itemCount.toLocaleString()} items
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
interface SourceStatusBadgeProps {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function SourceStatusBadge({ status }: SourceStatusBadgeProps) {
|
||||
return (
|
||||
<span className="text-ink-faint inline-flex items-center gap-1.5 text-[11px] font-medium">
|
||||
<span
|
||||
className={`h-1.5 w-1.5 rounded-full ${
|
||||
status === "syncing"
|
||||
? "bg-accent animate-pulse"
|
||||
: status === "error"
|
||||
? "bg-red-400"
|
||||
: "bg-ink-faint"
|
||||
}`}
|
||||
/>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
46
packages/interface/src/components/Sources/SourceTypeIcon.tsx
Normal file
46
packages/interface/src/components/Sources/SourceTypeIcon.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
interface SourceTypeIconProps {
|
||||
type: string;
|
||||
svg?: string | null;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const SIZE_CLASSES = {
|
||||
sm: "h-5 w-5 p-0.5",
|
||||
md: "h-8 w-8 p-1.5",
|
||||
lg: "h-10 w-10 p-2",
|
||||
};
|
||||
|
||||
const FALLBACK_LABELS: Record<string, string> = {
|
||||
email: "mail",
|
||||
file: "doc",
|
||||
note: "note",
|
||||
bookmark: "link",
|
||||
history: "time",
|
||||
markdown: "md",
|
||||
session: "term",
|
||||
};
|
||||
|
||||
export function SourceTypeIcon({
|
||||
type: typeName,
|
||||
svg,
|
||||
size = "md",
|
||||
}: SourceTypeIconProps) {
|
||||
if (svg) {
|
||||
return (
|
||||
<div
|
||||
className={`shrink-0 rounded-lg [&>svg]:h-full [&>svg]:w-full ${SIZE_CLASSES[size]}`}
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const label = FALLBACK_LABELS[typeName] ?? typeName.slice(0, 4);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-accent/10 text-accent flex shrink-0 items-center justify-center rounded-lg font-mono text-[10px] font-medium uppercase ${SIZE_CLASSES[size]}`}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { Database } from "@phosphor-icons/react";
|
||||
import { useLibraryQuery } from "../../contexts/SpacedriveContext";
|
||||
import { useAdapterIcons } from "../../hooks/useAdapterIcons";
|
||||
import { GroupHeader } from "./GroupHeader";
|
||||
|
||||
interface SourcesGroupProps {
|
||||
isCollapsed: boolean;
|
||||
onToggle: () => void;
|
||||
sortableAttributes?: any;
|
||||
sortableListeners?: any;
|
||||
}
|
||||
|
||||
export function SourcesGroup({
|
||||
isCollapsed,
|
||||
onToggle,
|
||||
sortableAttributes,
|
||||
sortableListeners,
|
||||
}: SourcesGroupProps) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { getIcon } = useAdapterIcons();
|
||||
|
||||
const { data: sources } = useLibraryQuery({
|
||||
type: "sources.list",
|
||||
input: { data_type: null },
|
||||
});
|
||||
|
||||
const sourcesList = sources ?? [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GroupHeader
|
||||
label="Sources"
|
||||
isCollapsed={isCollapsed}
|
||||
onToggle={onToggle}
|
||||
sortableAttributes={sortableAttributes}
|
||||
sortableListeners={sortableListeners}
|
||||
/>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="space-y-0.5">
|
||||
{sourcesList.map((source) => {
|
||||
const isActive = location.pathname === `/sources/${source.id}`;
|
||||
return (
|
||||
<button
|
||||
key={source.id}
|
||||
onClick={() => navigate(`/sources/${source.id}`)}
|
||||
className={`flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-sm font-medium ${
|
||||
isActive
|
||||
? "bg-sidebar-selected text-sidebar-ink"
|
||||
: "text-sidebar-inkDull hover:text-sidebar-ink"
|
||||
}`}
|
||||
>
|
||||
{getIcon(source.adapter_id) ? (
|
||||
<div
|
||||
className={`size-4 shrink-0 [&>svg]:h-full [&>svg]:w-full ${
|
||||
isActive
|
||||
? "opacity-100"
|
||||
: "opacity-60 grayscale"
|
||||
}`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getIcon(source.adapter_id)!,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Database
|
||||
className="size-4 shrink-0"
|
||||
weight={isActive ? "fill" : "bold"}
|
||||
/>
|
||||
)}
|
||||
<span className="truncate">{source.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,10 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
type: "FileKinds",
|
||||
label: "File Kinds",
|
||||
},
|
||||
{
|
||||
type: "Sources",
|
||||
label: "Sources",
|
||||
},
|
||||
];
|
||||
|
||||
function DraggablePaletteItem({ item }: { item: PaletteItem }) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DevicesGroup } from "./DevicesGroup";
|
||||
import { LocationsGroup } from "./LocationsGroup";
|
||||
import { VolumesGroup } from "./VolumesGroup";
|
||||
import { TagsGroup } from "./TagsGroup";
|
||||
import { SourcesGroup } from "./SourcesGroup";
|
||||
import { GroupHeader } from "./GroupHeader";
|
||||
import { useDroppable, useDndContext } from "@dnd-kit/core";
|
||||
|
||||
@@ -115,6 +116,20 @@ export function SpaceGroup({
|
||||
);
|
||||
}
|
||||
|
||||
// Sources group - fetches archive data sources
|
||||
if (group.group_type === "Sources") {
|
||||
return (
|
||||
<div data-group-id={group.id}>
|
||||
<SourcesGroup
|
||||
isCollapsed={isCollapsed}
|
||||
onToggle={handleToggle}
|
||||
sortableAttributes={sortableAttributes}
|
||||
sortableListeners={sortableListeners}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty drop zone for groups with no items
|
||||
const { setNodeRef: setEmptyRef, isOver: isOverEmpty } = useDroppable({
|
||||
id: `group-${group.id}-empty`,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
HardDrive,
|
||||
Tag as TagIcon,
|
||||
Folders,
|
||||
Database,
|
||||
} from "@phosphor-icons/react";
|
||||
import { Location } from "@sd/assets/icons";
|
||||
import type {
|
||||
@@ -65,6 +66,16 @@ export function isPathItem(t: ItemType): t is { Path: { sd_path: SdPath } } {
|
||||
return typeof t === "object" && "Path" in t;
|
||||
}
|
||||
|
||||
export function isSourcesItem(t: ItemType): t is "Sources" {
|
||||
return t === "Sources";
|
||||
}
|
||||
|
||||
export function isSourceItem(
|
||||
t: ItemType,
|
||||
): t is { Source: { source_id: string } } {
|
||||
return typeof t === "object" && "Source" in t;
|
||||
}
|
||||
|
||||
// Check if item is a "raw" location (legacy format with name/sd_path but no item_type)
|
||||
export function isRawLocation(
|
||||
item: SpaceItemType | Record<string, unknown>,
|
||||
@@ -78,10 +89,12 @@ function getItemIcon(itemType: ItemType): IconData {
|
||||
if (isRecentsItem(itemType)) return { type: "component", icon: Clock };
|
||||
if (isFavoritesItem(itemType)) return { type: "component", icon: Heart };
|
||||
if (isFileKindsItem(itemType)) return { type: "component", icon: Folders };
|
||||
if (isSourcesItem(itemType)) return { type: "component", icon: Database };
|
||||
if (isLocationItem(itemType)) return { type: "image", icon: Location };
|
||||
if (isVolumeItem(itemType)) return { type: "component", icon: HardDrive };
|
||||
if (isTagItem(itemType)) return { type: "component", icon: TagIcon };
|
||||
if (isPathItem(itemType)) return { type: "image", icon: Location };
|
||||
if (isSourceItem(itemType)) return { type: "component", icon: Database };
|
||||
return { type: "image", icon: Location };
|
||||
}
|
||||
|
||||
@@ -91,6 +104,7 @@ function getItemLabel(itemType: ItemType, resolvedFile?: File | null): string {
|
||||
if (isRecentsItem(itemType)) return "Recents";
|
||||
if (isFavoritesItem(itemType)) return "Favorites";
|
||||
if (isFileKindsItem(itemType)) return "File Kinds";
|
||||
if (isSourcesItem(itemType)) return "Sources";
|
||||
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";
|
||||
@@ -106,6 +120,7 @@ function getItemLabel(itemType: ItemType, resolvedFile?: File | null): string {
|
||||
}
|
||||
return "Path";
|
||||
}
|
||||
if (isSourceItem(itemType)) return "Source";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
@@ -119,6 +134,7 @@ function getItemPath(
|
||||
if (isRecentsItem(itemType)) return "/recents";
|
||||
if (isFavoritesItem(itemType)) return "/favorites";
|
||||
if (isFileKindsItem(itemType)) return "/file-kinds";
|
||||
if (isSourcesItem(itemType)) return "/sources";
|
||||
|
||||
if (isLocationItem(itemType)) {
|
||||
// Use explorer route with location's SD path (passed from item.sd_path)
|
||||
@@ -151,6 +167,10 @@ function getItemPath(
|
||||
return `/explorer?path=${encodeURIComponent(JSON.stringify(itemType.Path.sd_path))}`;
|
||||
}
|
||||
|
||||
if (isSourceItem(itemType)) {
|
||||
return `/sources/${itemType.Source.source_id}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@ import { useTabManager } from "./useTabManager";
|
||||
/**
|
||||
* Derives a tab title from the current route pathname and search params
|
||||
*/
|
||||
function deriveTitleFromPath(pathname: string, search: string): string {
|
||||
function deriveTitleFromPath(pathname: string, search: string): string | null {
|
||||
// Static route mappings
|
||||
const routeTitles: Record<string, string> = {
|
||||
"/": "Overview",
|
||||
"/favorites": "Favorites",
|
||||
"/recents": "Recents",
|
||||
"/file-kinds": "File Kinds",
|
||||
"/sources": "Sources",
|
||||
"/sources/adapters": "Adapters",
|
||||
"/search": "Search",
|
||||
"/jobs": "Jobs",
|
||||
"/daemon": "Daemon",
|
||||
@@ -28,6 +30,12 @@ function deriveTitleFromPath(pathname: string, search: string): string {
|
||||
return tagId ? `Tag: ${tagId.slice(0, 8)}...` : "Tag";
|
||||
}
|
||||
|
||||
// Handle source detail routes: /sources/:sourceId
|
||||
// Title is set dynamically by SourceDetail component, return null to skip overwrite
|
||||
if (pathname.startsWith("/sources/") && pathname !== "/sources/adapters") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle explorer routes
|
||||
if (pathname === "/explorer" && search) {
|
||||
const params = new URLSearchParams(search);
|
||||
@@ -91,9 +99,9 @@ export function TabNavigationSync() {
|
||||
updateTabPath(activeTabId, currentPath);
|
||||
}
|
||||
|
||||
// Always update title based on current location
|
||||
// Update title based on current location (null = managed by the route component)
|
||||
const newTitle = deriveTitleFromPath(location.pathname, location.search);
|
||||
if (activeTab && newTitle !== activeTab.title) {
|
||||
if (activeTab && newTitle !== null && newTitle !== activeTab.title) {
|
||||
updateTabTitle(activeTabId, newTitle);
|
||||
}
|
||||
}, [currentPath, activeTab, activeTabId, updateTabPath, updateTabTitle, location.pathname, location.search]);
|
||||
|
||||
23
packages/interface/src/hooks/useAdapterIcons.ts
Normal file
23
packages/interface/src/hooks/useAdapterIcons.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useLibraryQuery } from "../contexts/SpacedriveContext";
|
||||
import { useCallback } from "react";
|
||||
|
||||
/**
|
||||
* Hook that provides adapter icon lookup by adapter ID.
|
||||
* Fetches the adapters list once and caches via React Query.
|
||||
*/
|
||||
export function useAdapterIcons() {
|
||||
const { data: adapters } = useLibraryQuery({
|
||||
type: "adapters.list",
|
||||
input: {},
|
||||
});
|
||||
|
||||
const getIcon = useCallback(
|
||||
(adapterId: string): string | null => {
|
||||
if (!adapters) return null;
|
||||
return adapters.find((a) => a.id === adapterId)?.icon_svg ?? null;
|
||||
},
|
||||
[adapters],
|
||||
);
|
||||
|
||||
return { getIcon };
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import { DaemonManager } from "./routes/daemon";
|
||||
import { TagView } from "./routes/tag";
|
||||
import { FileKindsView } from "./routes/file-kinds";
|
||||
import { RecentsView } from "./routes/explorer/views/RecentsView";
|
||||
import { SourcesHome } from "./routes/sources";
|
||||
import { SourceDetail } from "./routes/sources/SourceDetail";
|
||||
import { AdaptersScreen } from "./routes/sources/Adapters";
|
||||
import { SpacebotProvider } from "./Spacebot/SpacebotContext";
|
||||
import { SpacebotLayout } from "./Spacebot/SpacebotLayout";
|
||||
import { ChatRoute } from "./Spacebot/routes/ChatRoute";
|
||||
@@ -63,6 +66,18 @@ export const explorerRoutes = [
|
||||
path: "tag/:tagId",
|
||||
element: <TagView />,
|
||||
},
|
||||
{
|
||||
path: "sources",
|
||||
element: <SourcesHome />,
|
||||
},
|
||||
{
|
||||
path: "sources/adapters",
|
||||
element: <AdaptersScreen />,
|
||||
},
|
||||
{
|
||||
path: "sources/:sourceId",
|
||||
element: <SourceDetail />,
|
||||
},
|
||||
{
|
||||
path: "search",
|
||||
element: (
|
||||
|
||||
383
packages/interface/src/routes/sources/Adapters.tsx
Normal file
383
packages/interface/src/routes/sources/Adapters.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
FolderOpen,
|
||||
} from "@phosphor-icons/react";
|
||||
import {
|
||||
useLibraryQuery,
|
||||
useLibraryMutation,
|
||||
} from "../../contexts/SpacedriveContext";
|
||||
import { TopBarPortal, TopBarItem } from "../../TopBar";
|
||||
import { CircleButton } from "@spaceui/primitives";
|
||||
import { ExpandableSearchButton } from "../explorer/components/ExpandableSearchButton";
|
||||
import { SourceTypeIcon } from "../../components/Sources/SourceTypeIcon";
|
||||
|
||||
export function AdaptersScreen() {
|
||||
const navigate = useNavigate();
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
|
||||
const { data: adapters, isLoading } = useLibraryQuery({
|
||||
type: "adapters.list",
|
||||
input: {},
|
||||
});
|
||||
|
||||
const [selectedAdapter, setSelectedAdapter] = useState<string | null>(null);
|
||||
const selected = adapters?.find((a) => a.id === selectedAdapter);
|
||||
|
||||
const filteredAdapters = useMemo(() => {
|
||||
if (!adapters || !searchValue.trim()) return adapters;
|
||||
const q = searchValue.toLowerCase();
|
||||
return adapters.filter(
|
||||
(a) =>
|
||||
a.name.toLowerCase().includes(q) ||
|
||||
a.description.toLowerCase().includes(q) ||
|
||||
a.data_type.toLowerCase().includes(q),
|
||||
);
|
||||
}, [adapters, searchValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopBarPortal
|
||||
left={
|
||||
<>
|
||||
<TopBarItem
|
||||
id="navigation"
|
||||
label="Navigation"
|
||||
priority="high"
|
||||
>
|
||||
<CircleButton
|
||||
icon={ArrowLeft}
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
</TopBarItem>
|
||||
<TopBarItem
|
||||
id="title"
|
||||
label="Title"
|
||||
priority="high"
|
||||
>
|
||||
<span className="text-ink whitespace-nowrap text-xs font-medium">
|
||||
Data Adapters
|
||||
</span>
|
||||
</TopBarItem>
|
||||
</>
|
||||
}
|
||||
right={
|
||||
<>
|
||||
<TopBarItem
|
||||
id="search"
|
||||
label="Search"
|
||||
priority="high"
|
||||
>
|
||||
<ExpandableSearchButton
|
||||
placeholder="Search adapters..."
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onClear={() => setSearchValue("")}
|
||||
/>
|
||||
</TopBarItem>
|
||||
<TopBarItem
|
||||
id="install"
|
||||
label="Install"
|
||||
priority="high"
|
||||
>
|
||||
<CircleButton
|
||||
icon={FolderOpen}
|
||||
onClick={() => {
|
||||
/* TODO: Install from directory */
|
||||
}}
|
||||
title="Install adapter from directory"
|
||||
/>
|
||||
</TopBarItem>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="p-6">
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-ink-faint text-sm">Loading...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{adapters && adapters.length === 0 && !searchValue && (
|
||||
<div className="flex flex-col items-center justify-center py-20">
|
||||
<p className="text-ink-dull text-sm">No adapters installed</p>
|
||||
<p className="text-ink-faint mt-1 text-xs">
|
||||
Adapters are loaded from the adapters directory on startup
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredAdapters && filteredAdapters.length === 0 && searchValue && (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<p className="text-ink-faint text-sm">No matching adapters</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredAdapters && filteredAdapters.length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3">
|
||||
{filteredAdapters.map((adapter) => (
|
||||
<button
|
||||
key={adapter.id}
|
||||
onClick={() => setSelectedAdapter(adapter.id)}
|
||||
className="border-app-line bg-app-box hover:border-app-line/80 hover:bg-app-hover flex items-center gap-3 rounded-lg border p-3 text-left transition-all"
|
||||
>
|
||||
<SourceTypeIcon
|
||||
type={adapter.data_type}
|
||||
svg={adapter.icon_svg}
|
||||
size="md"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-ink truncate text-sm font-medium">
|
||||
{adapter.name}
|
||||
</span>
|
||||
<span className="bg-app-line/60 text-ink-faint shrink-0 rounded px-1.5 py-0.5 text-[10px]">
|
||||
v{adapter.version}
|
||||
</span>
|
||||
{adapter.update_available && (
|
||||
<span className="shrink-0 rounded-full bg-blue-500/15 px-2 py-0.5 text-[10px] font-medium text-blue-400 ring-1 ring-blue-500/30">
|
||||
Update
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-ink-faint mt-0.5 truncate text-xs">
|
||||
{adapter.description || adapter.data_type}
|
||||
</div>
|
||||
{adapter.author && (
|
||||
<div className="text-ink-faint/70 mt-0.5 text-[11px]">
|
||||
by {adapter.author}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Configure dialog */}
|
||||
{selected && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="border-app-line bg-app-box w-full max-w-md rounded-xl border p-6 shadow-2xl">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<SourceTypeIcon
|
||||
type={selected.data_type}
|
||||
svg={selected.icon_svg}
|
||||
size="sm"
|
||||
/>
|
||||
<h3 className="text-ink text-sm font-semibold">
|
||||
{selected.name}
|
||||
</h3>
|
||||
</div>
|
||||
<ConfigureForm
|
||||
adapterId={selected.id}
|
||||
adapterName={selected.name}
|
||||
onDone={() => setSelectedAdapter(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigureForm({
|
||||
adapterId,
|
||||
adapterName,
|
||||
onDone,
|
||||
}: {
|
||||
adapterId: string;
|
||||
adapterName: string;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
data: fields,
|
||||
isLoading,
|
||||
error: queryError,
|
||||
} = useLibraryQuery({
|
||||
type: "adapters.config",
|
||||
input: { adapter_id: adapterId },
|
||||
});
|
||||
|
||||
const createSource = useLibraryMutation("sources.create");
|
||||
const syncSource = useLibraryMutation("sources.sync");
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [result, setResult] = useState<string | null>(null);
|
||||
|
||||
const handleFieldChange = useCallback((key: string, value: string) => {
|
||||
setValues((prev) => ({ ...prev, [key]: value }));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!name.trim()) {
|
||||
setError("Source name is required");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const config: Record<string, unknown> = {};
|
||||
for (const field of fields ?? []) {
|
||||
const val = values[field.key]?.trim() ?? "";
|
||||
if (field.required && !val) {
|
||||
setError(`${field.name} is required`);
|
||||
return;
|
||||
}
|
||||
if (!val && !field.required) continue;
|
||||
|
||||
if (field.field_type === "integer" && val) {
|
||||
config[field.key] = parseInt(val, 10);
|
||||
} else if (field.field_type === "boolean") {
|
||||
config[field.key] = val === "true";
|
||||
} else {
|
||||
config[field.key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
const source = await createSource.mutateAsync({
|
||||
name: name.trim(),
|
||||
adapter_id: adapterId,
|
||||
config,
|
||||
});
|
||||
|
||||
setSyncing(true);
|
||||
const report = await syncSource.mutateAsync({
|
||||
source_id: source.id,
|
||||
});
|
||||
|
||||
if (report.error) {
|
||||
setResult(`Synced with warning: ${report.error}`);
|
||||
} else {
|
||||
setResult(
|
||||
`Synced ${report.records_upserted} records in ${(report.duration_ms / 1000).toFixed(1)}s`,
|
||||
);
|
||||
}
|
||||
setSyncing(false);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="text-ink-faint py-8 text-center text-sm">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (queryError) {
|
||||
return (
|
||||
<div className="rounded-md border border-red-400/20 p-3 text-xs text-red-400">
|
||||
Failed to load config: {String(queryError)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const isError = result.startsWith("Sync failed");
|
||||
return (
|
||||
<div className="flex flex-col items-center py-6">
|
||||
<p
|
||||
className={`text-sm ${isError ? "text-red-400" : "text-ink"}`}
|
||||
>
|
||||
{isError ? "Something went wrong" : "Source added"}
|
||||
</p>
|
||||
<p className="text-ink-faint mt-1 text-xs">{result}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
onDone();
|
||||
navigate("/sources");
|
||||
}}
|
||||
className="bg-accent hover:bg-accent-deep mt-4 rounded-lg px-3.5 py-1.5 text-sm font-medium text-white transition-colors"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (syncing) {
|
||||
return (
|
||||
<div className="flex flex-col items-center py-6">
|
||||
<div className="border-accent mb-3 h-5 w-5 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
<p className="text-ink-dull text-sm">Syncing...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<label className="text-ink-dull mb-1 block text-xs font-medium">
|
||||
Source Name
|
||||
</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={`e.g., My ${adapterName}`}
|
||||
className="border-app-line bg-app-input text-ink w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-accent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{fields?.map((field) => (
|
||||
<div key={field.key}>
|
||||
<label className="text-ink-dull mb-1 block text-xs font-medium">
|
||||
{field.name}
|
||||
{field.required && (
|
||||
<span className="ml-1 text-red-400">*</span>
|
||||
)}
|
||||
</label>
|
||||
{field.description && (
|
||||
<p className="text-ink-faint mb-1 text-[11px] leading-relaxed">
|
||||
{field.description}
|
||||
</p>
|
||||
)}
|
||||
<input
|
||||
type={field.secret ? "password" : "text"}
|
||||
value={values[field.key] ?? ""}
|
||||
onChange={(e) =>
|
||||
handleFieldChange(field.key, e.target.value)
|
||||
}
|
||||
placeholder={
|
||||
field.default
|
||||
? `Default: ${field.default}`
|
||||
: undefined
|
||||
}
|
||||
className="border-app-line bg-app-input text-ink w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-accent"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md border border-red-400/20 p-2 text-xs text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-1 flex gap-2">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={createSource.isPending}
|
||||
className="bg-accent hover:bg-accent-deep flex-1 rounded-lg px-3.5 py-1.5 text-sm font-medium text-white transition-colors disabled:opacity-40"
|
||||
>
|
||||
Add & Sync
|
||||
</button>
|
||||
<button
|
||||
onClick={onDone}
|
||||
className="text-ink-faint hover:text-ink rounded-lg px-3.5 py-1.5 text-sm font-medium transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
484
packages/interface/src/routes/sources/SourceDetail.tsx
Normal file
484
packages/interface/src/routes/sources/SourceDetail.tsx
Normal file
@@ -0,0 +1,484 @@
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowsClockwise,
|
||||
DotsThree,
|
||||
Trash,
|
||||
ArrowSquareUp,
|
||||
} from "@phosphor-icons/react";
|
||||
import {
|
||||
useLibraryQuery,
|
||||
useLibraryMutation,
|
||||
} from "../../contexts/SpacedriveContext";
|
||||
import { useTabManager } from "../../components/TabManager/useTabManager";
|
||||
import { TopBarPortal, TopBarItem } from "../../TopBar";
|
||||
import { CircleButton, Popover, usePopover } from "@spaceui/primitives";
|
||||
import { ExpandableSearchButton } from "../explorer/components/ExpandableSearchButton";
|
||||
import { SourcePathBar } from "../../components/Sources/SourcePathBar";
|
||||
import { SourceDataRow } from "../../components/Sources/SourceDataRow";
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
export function SourceDetail() {
|
||||
const { sourceId } = useParams<{ sourceId: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Infinite scroll state
|
||||
const [allItems, setAllItems] = useState<
|
||||
Array<{
|
||||
id: string;
|
||||
external_id: string;
|
||||
title: string;
|
||||
preview: string | null;
|
||||
subtitle: string | null;
|
||||
}>
|
||||
>([]);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
|
||||
const {
|
||||
data: source,
|
||||
isLoading,
|
||||
error,
|
||||
} = useLibraryQuery({
|
||||
type: "sources.get",
|
||||
input: { source_id: sourceId ?? "" },
|
||||
});
|
||||
|
||||
// Initial page
|
||||
const { data: initialItems, isLoading: itemsLoading } = useLibraryQuery({
|
||||
type: "sources.list_items",
|
||||
input: { source_id: sourceId ?? "", limit: PAGE_SIZE, offset: 0 },
|
||||
});
|
||||
|
||||
// Seed allItems from initial fetch
|
||||
useEffect(() => {
|
||||
if (initialItems) {
|
||||
setAllItems(initialItems);
|
||||
setOffset(initialItems.length);
|
||||
setHasMore(initialItems.length >= PAGE_SIZE);
|
||||
}
|
||||
}, [initialItems]);
|
||||
|
||||
// Reset when sourceId changes
|
||||
useEffect(() => {
|
||||
setAllItems([]);
|
||||
setOffset(0);
|
||||
setHasMore(true);
|
||||
}, [sourceId]);
|
||||
|
||||
const { data: adapters } = useLibraryQuery({
|
||||
type: "adapters.list",
|
||||
input: {},
|
||||
});
|
||||
|
||||
const syncMutation = useLibraryMutation("sources.sync");
|
||||
const deleteMutation = useLibraryMutation("sources.delete");
|
||||
const updateMutation = useLibraryMutation("adapters.update");
|
||||
|
||||
const adapterHasUpdate =
|
||||
adapters?.find((a) => a.id === source?.adapter_id)?.update_available ??
|
||||
false;
|
||||
|
||||
// Sync tab title with source name
|
||||
const { activeTabId, updateTabTitle } = useTabManager();
|
||||
useEffect(() => {
|
||||
if (source?.name) {
|
||||
updateTabTitle(activeTabId, source.name);
|
||||
}
|
||||
}, [source?.name, activeTabId, updateTabTitle]);
|
||||
|
||||
// Filter items by search
|
||||
const filteredItems = useMemo(() => {
|
||||
if (!searchValue.trim()) return allItems;
|
||||
const q = searchValue.toLowerCase();
|
||||
return allItems.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(q) ||
|
||||
item.subtitle?.toLowerCase().includes(q) ||
|
||||
item.preview?.toLowerCase().includes(q),
|
||||
);
|
||||
}, [allItems, searchValue]);
|
||||
|
||||
// Virtualizer
|
||||
const virtualizer = useVirtualizer({
|
||||
count: filteredItems.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 64,
|
||||
overscan: 20,
|
||||
// Avoid flushSync during render (React 19 compat)
|
||||
scrollToFn: (offset, { adjustments, behavior }, instance) => {
|
||||
const el = instance.scrollElement;
|
||||
if (!el) return;
|
||||
const top = offset + (adjustments ?? 0);
|
||||
el.scrollTo({ top, behavior });
|
||||
},
|
||||
});
|
||||
|
||||
// Load more when scrolling near bottom
|
||||
const loadMore = useCallback(async () => {
|
||||
if (loadingMore || !hasMore || !sourceId) return;
|
||||
setLoadingMore(true);
|
||||
// We can't use useLibraryQuery for imperative fetches,
|
||||
// so we'll bump the offset and let a new query handle it
|
||||
}, [loadingMore, hasMore, sourceId]);
|
||||
|
||||
// Fetch next page
|
||||
const { data: nextPage } = useLibraryQuery(
|
||||
{
|
||||
type: "sources.list_items",
|
||||
input: {
|
||||
source_id: sourceId ?? "",
|
||||
limit: PAGE_SIZE,
|
||||
offset,
|
||||
},
|
||||
},
|
||||
{ enabled: loadingMore && offset > 0 },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (nextPage && loadingMore) {
|
||||
setAllItems((prev) => [...prev, ...nextPage]);
|
||||
setOffset((prev) => prev + nextPage.length);
|
||||
setHasMore(nextPage.length >= PAGE_SIZE);
|
||||
setLoadingMore(false);
|
||||
}
|
||||
}, [nextPage, loadingMore]);
|
||||
|
||||
// Trigger load more when virtualizer reaches near the end
|
||||
const lastVirtualItem =
|
||||
virtualizer.getVirtualItems()[
|
||||
virtualizer.getVirtualItems().length - 1
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
lastVirtualItem &&
|
||||
lastVirtualItem.index >= filteredItems.length - 10 &&
|
||||
hasMore &&
|
||||
!loadingMore &&
|
||||
!searchValue
|
||||
) {
|
||||
setLoadingMore(true);
|
||||
}
|
||||
}, [
|
||||
lastVirtualItem?.index,
|
||||
filteredItems.length,
|
||||
hasMore,
|
||||
loadingMore,
|
||||
searchValue,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-ink-faint text-sm">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !source) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-lg border border-red-400/20 p-4">
|
||||
<p className="text-sm text-red-400">
|
||||
Failed to load source:{" "}
|
||||
{error ? String(error) : "Not found"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<TopBarPortal
|
||||
left={
|
||||
<>
|
||||
<TopBarItem
|
||||
id="back"
|
||||
label="Back"
|
||||
priority="high"
|
||||
>
|
||||
<CircleButton
|
||||
icon={ArrowLeft}
|
||||
onClick={() => navigate("/sources")}
|
||||
/>
|
||||
</TopBarItem>
|
||||
<TopBarItem
|
||||
id="source-path"
|
||||
label="Path"
|
||||
priority="high"
|
||||
>
|
||||
<SourcePathBar
|
||||
sourceName={source.name}
|
||||
itemCount={source.item_count}
|
||||
/>
|
||||
</TopBarItem>
|
||||
</>
|
||||
}
|
||||
right={
|
||||
<>
|
||||
<TopBarItem
|
||||
id="search"
|
||||
label="Search"
|
||||
priority="high"
|
||||
>
|
||||
<ExpandableSearchButton
|
||||
placeholder="Search items..."
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onClear={() => setSearchValue("")}
|
||||
/>
|
||||
</TopBarItem>
|
||||
<TopBarItem
|
||||
id="sync"
|
||||
label="Sync"
|
||||
priority="high"
|
||||
>
|
||||
<CircleButton
|
||||
icon={ArrowsClockwise}
|
||||
onClick={() =>
|
||||
syncMutation.mutate({
|
||||
source_id: source.id,
|
||||
})
|
||||
}
|
||||
title="Sync"
|
||||
active={
|
||||
syncMutation.isPending ||
|
||||
source.status === "syncing"
|
||||
}
|
||||
/>
|
||||
</TopBarItem>
|
||||
<TopBarItem
|
||||
id="more-actions"
|
||||
label="More"
|
||||
priority="normal"
|
||||
>
|
||||
<MoreActionsMenu
|
||||
adapterHasUpdate={adapterHasUpdate}
|
||||
onUpdate={() =>
|
||||
updateMutation.mutate({
|
||||
adapter_id: source.adapter_id,
|
||||
})
|
||||
}
|
||||
isUpdating={updateMutation.isPending}
|
||||
onDelete={() => setShowDeleteConfirm(true)}
|
||||
/>
|
||||
</TopBarItem>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Banners */}
|
||||
{(updateMutation.data ||
|
||||
updateMutation.error ||
|
||||
syncMutation.error ||
|
||||
(syncMutation.data && !syncMutation.data.error)) && (
|
||||
<div className="border-app-line/30 space-y-2 border-b px-6 py-3">
|
||||
{updateMutation.data && (
|
||||
<div className="border-accent/20 rounded-lg border p-3">
|
||||
<p className="text-accent text-xs">
|
||||
Updated {updateMutation.data.adapter_id}: v
|
||||
{updateMutation.data.old_version} → v
|
||||
{updateMutation.data.new_version}
|
||||
{updateMutation.data.schema_changed
|
||||
? " (schema changed — will migrate on next sync)"
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{updateMutation.error && (
|
||||
<div className="rounded-lg border border-red-400/20 p-3">
|
||||
<p className="text-xs text-red-400">
|
||||
Update failed: {String(updateMutation.error)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{syncMutation.error && (
|
||||
<div className="rounded-lg border border-red-400/20 p-3">
|
||||
<p className="text-xs text-red-400">
|
||||
Sync failed: {String(syncMutation.error)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{syncMutation.data && !syncMutation.data.error && (
|
||||
<div className="border-accent/20 rounded-lg border p-3">
|
||||
<p className="text-accent text-xs">
|
||||
Synced{" "}
|
||||
{syncMutation.data.records_upserted} records in{" "}
|
||||
{(
|
||||
syncMutation.data.duration_ms / 1000
|
||||
).toFixed(1)}
|
||||
s
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Virtualized items list */}
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto">
|
||||
{itemsLoading && (
|
||||
<div className="text-ink-faint py-12 text-center text-sm">
|
||||
Loading items...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!itemsLoading && filteredItems.length === 0 && (
|
||||
<div className="text-ink-faint py-12 text-center text-sm">
|
||||
{searchValue
|
||||
? "No matching items"
|
||||
: "No items yet. Run a sync to populate."}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredItems.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
height: virtualizer.getTotalSize(),
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const item = filteredItems[virtualRow.index];
|
||||
if (!item) return null;
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: virtualRow.size,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<SourceDataRow
|
||||
title={item.title}
|
||||
preview={item.preview}
|
||||
subtitle={item.subtitle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingMore && (
|
||||
<div className="text-ink-faint py-4 text-center text-xs">
|
||||
Loading more...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Delete confirmation */}
|
||||
{showDeleteConfirm && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="border-app-line bg-app-box w-full max-w-sm rounded-xl border p-6 shadow-2xl">
|
||||
<h3 className="text-ink text-base font-semibold">
|
||||
Delete source
|
||||
</h3>
|
||||
<p className="text-ink-faint mt-2 text-sm">
|
||||
This will permanently delete{" "}
|
||||
<strong className="text-ink-dull">
|
||||
{source.name}
|
||||
</strong>{" "}
|
||||
and all its data.
|
||||
</p>
|
||||
{deleteMutation.error && (
|
||||
<p className="mt-2 text-xs text-red-400">
|
||||
{String(deleteMutation.error)}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="border-app-line text-ink-faint hover:text-ink rounded-lg border px-3.5 py-1.5 text-sm font-medium transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
deleteMutation.mutate(
|
||||
{ source_id: source.id },
|
||||
{
|
||||
onSuccess: () =>
|
||||
navigate("/sources"),
|
||||
},
|
||||
)
|
||||
}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="rounded-lg bg-red-500 px-3.5 py-1.5 text-sm font-medium text-white transition-colors hover:bg-red-500/80 disabled:opacity-50"
|
||||
>
|
||||
{deleteMutation.isPending
|
||||
? "Deleting..."
|
||||
: "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MoreActionsMenu({
|
||||
adapterHasUpdate,
|
||||
onUpdate,
|
||||
isUpdating,
|
||||
onDelete,
|
||||
}: {
|
||||
adapterHasUpdate: boolean;
|
||||
onUpdate: () => void;
|
||||
isUpdating: boolean;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const popover = usePopover();
|
||||
|
||||
return (
|
||||
<Popover.Root open={popover.open} onOpenChange={popover.setOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<CircleButton icon={DotsThree} title="More actions" />
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={8}
|
||||
className="!bg-app-box z-50 w-[200px] !rounded-lg !p-1"
|
||||
>
|
||||
{adapterHasUpdate && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onUpdate();
|
||||
popover.setOpen(false);
|
||||
}}
|
||||
disabled={isUpdating}
|
||||
className="hover:bg-app-hover text-ink flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm disabled:opacity-40"
|
||||
>
|
||||
<ArrowSquareUp size={16} className="text-blue-400" />
|
||||
{isUpdating ? "Updating..." : "Update adapter"}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
popover.setOpen(false);
|
||||
}}
|
||||
className="hover:bg-app-hover flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm text-red-400"
|
||||
>
|
||||
<Trash size={16} />
|
||||
Delete source
|
||||
</button>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
||||
98
packages/interface/src/routes/sources/index.tsx
Normal file
98
packages/interface/src/routes/sources/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Plus, ArrowLeft } from "@phosphor-icons/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLibraryQuery } from "../../contexts/SpacedriveContext";
|
||||
import { useTabManager } from "../../components/TabManager/useTabManager";
|
||||
import { SourceCard } from "../../components/Sources/SourceCard";
|
||||
import { TopBarPortal, TopBarItem } from "../../TopBar";
|
||||
import { CircleButton } from "@spaceui/primitives";
|
||||
import { SearchBar } from "@spaceui/primitives";
|
||||
|
||||
export function SourcesHome() {
|
||||
const navigate = useNavigate();
|
||||
const { createTab } = useTabManager();
|
||||
const { data: sources, isLoading, error } = useLibraryQuery({
|
||||
type: "sources.list",
|
||||
input: { data_type: null },
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopBarPortal
|
||||
left={
|
||||
<>
|
||||
<TopBarItem id="back" label="Back" priority="high">
|
||||
<CircleButton
|
||||
icon={ArrowLeft}
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
</TopBarItem>
|
||||
<TopBarItem id="title" label="Title" priority="high">
|
||||
<h1 className="text-ink text-xl font-bold">
|
||||
Sources
|
||||
</h1>
|
||||
</TopBarItem>
|
||||
</>
|
||||
}
|
||||
right={
|
||||
<>
|
||||
<TopBarItem id="search" label="Search" priority="high">
|
||||
<SearchBar
|
||||
placeholder="Search sources..."
|
||||
value=""
|
||||
onChange={() => {}}
|
||||
onClear={() => {}}
|
||||
className="w-64"
|
||||
/>
|
||||
</TopBarItem>
|
||||
<TopBarItem id="add-source" label="Add Source" priority="high">
|
||||
<CircleButton
|
||||
icon={Plus}
|
||||
onClick={() => createTab("Adapters", "/sources/adapters")}
|
||||
title="Add Source"
|
||||
/>
|
||||
</TopBarItem>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="p-6">
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-ink-faint text-sm">Loading...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="border-red-400/20 rounded-lg border p-4">
|
||||
<p className="text-sm text-red-400">
|
||||
Failed to load sources: {String(error)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sources && sources.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-20">
|
||||
<p className="text-ink-dull text-sm">No sources yet</p>
|
||||
<p className="text-ink-faint mt-1 text-xs">
|
||||
Add a data source to get started
|
||||
</p>
|
||||
<button
|
||||
onClick={() => createTab("Adapters", "/sources/adapters")}
|
||||
className="bg-accent hover:bg-accent-deep mt-4 rounded-lg px-3.5 py-1.5 text-sm font-medium text-white transition-colors"
|
||||
>
|
||||
Add Source
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sources && sources.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{sources.map((source) => (
|
||||
<SourceCard key={source.id} source={source} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,10 @@ export type ActiveJobsInput = Record<string, never>;
|
||||
|
||||
export type ActiveJobsOutput = { jobs: ActiveJobItem[]; running_count: number; paused_count: number };
|
||||
|
||||
export type AdapterConfigField = { key: string; name: string; description: string; field_type: string; required: boolean; secret: boolean; default: string | null };
|
||||
|
||||
export type AdapterInfo = { id: string; name: string; description: string; version: string; author: string; data_type: string; icon_svg: string | null; update_available: boolean };
|
||||
|
||||
export type AddGroupInput = { space_id: string; name: string; group_type: GroupType };
|
||||
|
||||
export type AddGroupOutput = { group: SpaceGroup };
|
||||
@@ -476,6 +480,44 @@ folder_path: SdPath;
|
||||
*/
|
||||
job_receipt?: JobReceipt | null };
|
||||
|
||||
/**
|
||||
* Input for creating a new archive source
|
||||
*/
|
||||
export type CreateSourceInput = {
|
||||
/**
|
||||
* Display name for the source
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Adapter ID (e.g., "gmail", "obsidian", "chrome-bookmarks")
|
||||
*/
|
||||
adapter_id: string;
|
||||
/**
|
||||
* Adapter-specific configuration
|
||||
*/
|
||||
config: JsonValue };
|
||||
|
||||
/**
|
||||
* Output from creating a new archive source
|
||||
*/
|
||||
export type CreateSourceOutput = {
|
||||
/**
|
||||
* The ID of the newly created source
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The display name of the source
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The adapter ID used
|
||||
*/
|
||||
adapter_id: string;
|
||||
/**
|
||||
* Current status (usually "idle" initially)
|
||||
*/
|
||||
status: string };
|
||||
|
||||
export type CreateTagInput = {
|
||||
/**
|
||||
* The canonical name for this tag
|
||||
@@ -551,6 +593,10 @@ export type DeleteItemInput = { item_id: string };
|
||||
|
||||
export type DeleteItemOutput = { success: boolean };
|
||||
|
||||
export type DeleteSourceInput = { source_id: string };
|
||||
|
||||
export type DeleteSourceOutput = { deleted: boolean };
|
||||
|
||||
export type DeleteWhisperModelInput = { model: string };
|
||||
|
||||
export type DeleteWhisperModelOutput = { deleted: boolean };
|
||||
@@ -1528,6 +1574,8 @@ completion: ProgressCompletion;
|
||||
*/
|
||||
performance: PerformanceMetrics };
|
||||
|
||||
export type GetAdapterConfigInput = { adapter_id: string };
|
||||
|
||||
/**
|
||||
* Input for getting app configuration
|
||||
*/
|
||||
@@ -1538,6 +1586,8 @@ export type GetAppConfigQueryInput = null;
|
||||
*/
|
||||
export type GetLibraryConfigQueryInput = null;
|
||||
|
||||
export type GetSourceInput = { source_id: string };
|
||||
|
||||
/**
|
||||
* Input for getting sync activity summary
|
||||
*/
|
||||
@@ -1660,6 +1710,10 @@ export type GroupType =
|
||||
* Tag collection
|
||||
*/
|
||||
"Tags" |
|
||||
/**
|
||||
* Archive data sources (email, notes, bookmarks, etc.)
|
||||
*/
|
||||
"Sources" |
|
||||
/**
|
||||
* Cloud storage providers
|
||||
*/
|
||||
@@ -2029,7 +2083,15 @@ export type ItemType =
|
||||
/**
|
||||
* Any arbitrary path (dragged from explorer)
|
||||
*/
|
||||
{ Path: { sd_path: SdPath } };
|
||||
{ Path: { sd_path: SdPath } } |
|
||||
/**
|
||||
* All archive data sources screen
|
||||
*/
|
||||
"Sources" |
|
||||
/**
|
||||
* Specific archive data source
|
||||
*/
|
||||
{ Source: { source_id: string } };
|
||||
|
||||
export type JobCancelInput = { job_id: string };
|
||||
|
||||
@@ -2583,6 +2645,8 @@ devicesRegistered: boolean;
|
||||
*/
|
||||
message: string };
|
||||
|
||||
export type ListAdaptersInput = Record<string, never>;
|
||||
|
||||
export type ListEventsInput = Record<string, never>;
|
||||
|
||||
export type ListEventsOutput = {
|
||||
@@ -2645,6 +2709,14 @@ total: number;
|
||||
*/
|
||||
connected: number };
|
||||
|
||||
export type ListSourceItemsInput = { source_id: string; limit: number; offset: number };
|
||||
|
||||
export type ListSourcesInput = {
|
||||
/**
|
||||
* Filter by data type
|
||||
*/
|
||||
data_type: string | null };
|
||||
|
||||
export type ListWhisperModelsInput = Record<string, never>;
|
||||
|
||||
export type ListWhisperModelsOutput = { models: ModelInfo[]; total_downloaded_size: number };
|
||||
@@ -3635,6 +3707,41 @@ export type SortField = "Relevance" | "Name" | "Size" | "ModifiedAt" | "CreatedA
|
||||
*/
|
||||
export type SortOptions = { field: SortField; direction: SortDirection };
|
||||
|
||||
/**
|
||||
* Information about a source
|
||||
*/
|
||||
export type SourceInfo = {
|
||||
/**
|
||||
* Source ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Display name
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Data type (e.g., "email", "bookmark", "note")
|
||||
*/
|
||||
data_type: string;
|
||||
/**
|
||||
* Adapter ID
|
||||
*/
|
||||
adapter_id: string;
|
||||
/**
|
||||
* Number of items
|
||||
*/
|
||||
item_count: number;
|
||||
/**
|
||||
* Last sync timestamp
|
||||
*/
|
||||
last_synced: string | null;
|
||||
/**
|
||||
* Current status
|
||||
*/
|
||||
status: string };
|
||||
|
||||
export type SourceItem = { id: string; external_id: string; title: string; preview: string | null; subtitle: string | null };
|
||||
|
||||
/**
|
||||
* A Space defines a sidebar layout and filtering context
|
||||
*/
|
||||
@@ -3927,6 +4034,10 @@ export type SyncPartnerInfo = { device_uuid: string; device_name: string; is_pai
|
||||
|
||||
export type SyncPartnersDebugInfo = { total_devices: number; sync_enabled_devices: number; paired_devices: number; final_sync_partners: number; device_details: DeviceDebugInfo[] };
|
||||
|
||||
export type SyncSourceInput = { source_id: string };
|
||||
|
||||
export type SyncSourceOutput = { records_upserted: number; records_deleted: number; duration_ms: number; error: string | null };
|
||||
|
||||
/**
|
||||
* State metrics snapshot
|
||||
*/
|
||||
@@ -4189,6 +4300,10 @@ total_count: number;
|
||||
*/
|
||||
total_size: number };
|
||||
|
||||
export type UpdateAdapterInput = { adapter_id: string };
|
||||
|
||||
export type UpdateAdapterOutput = { adapter_id: string; old_version: string; new_version: string; schema_changed: boolean };
|
||||
|
||||
/**
|
||||
* Input for updating app configuration
|
||||
* All fields are optional for partial updates
|
||||
@@ -4793,7 +4908,8 @@ export type CoreAction =
|
||||
;
|
||||
|
||||
export type LibraryAction =
|
||||
{ type: 'config.library.update'; input: UpdateLibraryConfigInput; output: UpdateLibraryConfigOutput }
|
||||
{ type: 'adapters.update'; input: UpdateAdapterInput; output: UpdateAdapterOutput }
|
||||
| { type: 'config.library.update'; input: UpdateLibraryConfigInput; output: UpdateLibraryConfigOutput }
|
||||
| { type: 'files.copy'; input: FileCopyInput; output: JobReceipt }
|
||||
| { type: 'files.createFolder'; input: CreateFolderInput; output: CreateFolderOutput }
|
||||
| { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt }
|
||||
@@ -4820,6 +4936,9 @@ export type LibraryAction =
|
||||
| { type: 'media.thumbnail'; input: ThumbnailInput; output: JobReceipt }
|
||||
| { type: 'media.thumbnail.regenerate'; input: RegenerateThumbnailInput; output: RegenerateThumbnailOutput }
|
||||
| { type: 'media.thumbstrip.generate'; input: GenerateThumbstripInput; output: GenerateThumbstripOutput }
|
||||
| { type: 'sources.create'; input: CreateSourceInput; output: CreateSourceOutput }
|
||||
| { type: 'sources.delete'; input: DeleteSourceInput; output: DeleteSourceOutput }
|
||||
| { type: 'sources.sync'; input: SyncSourceInput; output: SyncSourceOutput }
|
||||
| { type: 'spaces.add_group'; input: AddGroupInput; output: AddGroupOutput }
|
||||
| { type: 'spaces.add_item'; input: AddItemInput; output: AddItemOutput }
|
||||
| { type: 'spaces.create'; input: SpaceCreateInput; output: SpaceCreateOutput }
|
||||
@@ -4859,7 +4978,9 @@ export type CoreQuery =
|
||||
;
|
||||
|
||||
export type LibraryQuery =
|
||||
{ type: 'config.library.get'; input: GetLibraryConfigQueryInput; output: LibrarySettingsOutput }
|
||||
{ type: 'adapters.config'; input: GetAdapterConfigInput; output: [AdapterConfigField] }
|
||||
| { type: 'adapters.list'; input: ListAdaptersInput; output: [AdapterInfo] }
|
||||
| { type: 'config.library.get'; input: GetLibraryConfigQueryInput; output: LibrarySettingsOutput }
|
||||
| { type: 'devices.list'; input: ListLibraryDevicesInput; output: [Device] }
|
||||
| { type: 'files.alternate_instances'; input: AlternateInstancesInput; output: AlternateInstancesOutput }
|
||||
| { type: 'files.by_id'; input: FileByIdQuery; output: File }
|
||||
@@ -4877,6 +4998,9 @@ export type LibraryQuery =
|
||||
| { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput }
|
||||
| { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput }
|
||||
| { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput }
|
||||
| { type: 'sources.get'; input: GetSourceInput; output: SourceInfo }
|
||||
| { type: 'sources.list'; input: ListSourcesInput; output: [SourceInfo] }
|
||||
| { type: 'sources.list_items'; input: ListSourceItemsInput; output: [SourceItem] }
|
||||
| { type: 'spaces.get'; input: SpaceGetQueryInput; output: SpaceGetOutput }
|
||||
| { type: 'spaces.get_layout'; input: SpaceLayoutQueryInput; output: SpaceLayout }
|
||||
| { type: 'spaces.list'; input: SpacesListQueryInput; output: SpacesListOutput }
|
||||
@@ -4915,6 +5039,7 @@ export const WIRE_METHODS = {
|
||||
},
|
||||
|
||||
libraryActions: {
|
||||
'adapters.update': 'action:adapters.update.input',
|
||||
'config.library.update': 'action:config.library.update.input',
|
||||
'files.copy': 'action:files.copy.input',
|
||||
'files.createFolder': 'action:files.createFolder.input',
|
||||
@@ -4942,6 +5067,9 @@ export const WIRE_METHODS = {
|
||||
'media.thumbnail': 'action:media.thumbnail.input',
|
||||
'media.thumbnail.regenerate': 'action:media.thumbnail.regenerate.input',
|
||||
'media.thumbstrip.generate': 'action:media.thumbstrip.generate.input',
|
||||
'sources.create': 'action:sources.create.input',
|
||||
'sources.delete': 'action:sources.delete.input',
|
||||
'sources.sync': 'action:sources.sync.input',
|
||||
'spaces.add_group': 'action:spaces.add_group.input',
|
||||
'spaces.add_item': 'action:spaces.add_item.input',
|
||||
'spaces.create': 'action:spaces.create.input',
|
||||
@@ -4981,6 +5109,8 @@ export const WIRE_METHODS = {
|
||||
},
|
||||
|
||||
libraryQueries: {
|
||||
'adapters.config': 'query:adapters.config',
|
||||
'adapters.list': 'query:adapters.list',
|
||||
'config.library.get': 'query:config.library.get',
|
||||
'devices.list': 'query:devices.list',
|
||||
'files.alternate_instances': 'query:files.alternate_instances',
|
||||
@@ -4999,6 +5129,9 @@ export const WIRE_METHODS = {
|
||||
'locations.suggested': 'query:locations.suggested',
|
||||
'locations.validate_path': 'query:locations.validate_path',
|
||||
'search.files': 'query:search.files',
|
||||
'sources.get': 'query:sources.get',
|
||||
'sources.list': 'query:sources.list',
|
||||
'sources.list_items': 'query:sources.list_items',
|
||||
'spaces.get': 'query:spaces.get',
|
||||
'spaces.get_layout': 'query:spaces.get_layout',
|
||||
'spaces.list': 'query:spaces.list',
|
||||
|
||||
Reference in New Issue
Block a user