From 46e25ccd48db879ff88c4b379ef465844b2bcbf8 Mon Sep 17 00:00:00 2001 From: Babayaga Date: Thu, 26 Mar 2026 23:01:41 +0100 Subject: [PATCH] v1 cleanup --- packages/ui/src/App.tsx | 29 +- .../src/apps/filebrowser/FileBrowserPanel.tsx | 1738 +++--- .../src/components/AddToCollectionModal.tsx | 700 +-- .../ui/src/components/CollapsibleSection.tsx | 2 +- .../ui/src/components/CollectionButton.tsx | 98 +- packages/ui/src/components/Comments.tsx | 1284 ++--- packages/ui/src/components/EditImageModal.tsx | 1128 ++-- packages/ui/src/components/GalleryLarge.tsx | 314 +- packages/ui/src/components/HashtagText.tsx | 104 +- packages/ui/src/components/ImageGallery.tsx | 1112 ++-- packages/ui/src/components/ImageWizard.tsx | 3968 +++++++------- .../ImageWizard/components/index.ts | 28 +- .../ImageWizard/handlers/dataHandlers.ts | 282 +- .../ui/src/components/MagicWizardButton.tsx | 234 +- packages/ui/src/components/MarkdownEditor.tsx | 256 +- .../ui/src/components/MarkdownEditorEx.tsx | 632 +-- .../ui/src/components/MarkdownRenderer.tsx | 866 +-- .../ui/src/components/OrganizationsList.tsx | 146 +- packages/ui/src/components/PhotoCard.tsx | 1236 ++--- packages/ui/src/components/PhotoGrid.tsx | 1284 ++--- packages/ui/src/components/PromptForm.tsx | 1066 ++-- packages/ui/src/components/PublishDialog.tsx | 564 +- .../ui/src/components/TemplateManager.tsx | 186 +- packages/ui/src/components/ThemeProvider.tsx | 152 +- packages/ui/src/components/ThemeToggle.tsx | 78 +- packages/ui/src/components/TopNavigation.tsx | 678 +-- .../ui/src/components/VersionSelector.tsx | 674 +-- .../ui/src/components/logging/LogsPage.tsx | 2 +- .../src/components/tree/components/cursor.tsx | 30 +- .../tree/components/default-container.tsx | 478 +- .../tree/components/default-cursor.tsx | 84 +- .../tree/components/default-drag-preview.tsx | 184 +- .../tree/components/default-node.tsx | 100 +- .../tree/components/default-row.tsx | 42 +- .../components/drag-preview-container.tsx | 52 +- .../tree/components/list-inner-element.tsx | 44 +- .../tree/components/list-outer-element.tsx | 84 +- .../components/tree/components/outer-drop.ts | 14 +- .../components/tree/components/provider.tsx | 196 +- .../tree/components/row-container.tsx | 166 +- .../tree/components/tree-container.tsx | 26 +- .../src/components/tree/components/tree.tsx | 56 +- packages/ui/src/components/tree/context.ts | 72 +- .../src/components/tree/data/create-index.ts | 18 +- .../src/components/tree/data/create-list.ts | 134 +- .../src/components/tree/data/create-root.ts | 104 +- .../ui/src/components/tree/data/make-tree.ts | 74 +- .../src/components/tree/data/simple-tree.ts | 206 +- .../src/components/tree/dnd/compute-drop.ts | 370 +- .../ui/src/components/tree/dnd/drag-hook.ts | 72 +- .../ui/src/components/tree/dnd/drop-hook.ts | 122 +- .../src/components/tree/dnd/measure-hover.ts | 52 +- .../components/tree/dnd/outer-drop-hook.ts | 88 +- .../components/tree/hooks/use-fresh-node.ts | 32 +- .../components/tree/hooks/use-simple-tree.ts | 120 +- .../tree/hooks/use-validated-props.ts | 66 +- packages/ui/src/components/tree/index.ts | 18 +- .../components/tree/interfaces/node-api.ts | 418 +- .../tree/interfaces/tree-api.test.ts | 30 +- .../components/tree/interfaces/tree-api.ts | 1326 ++--- .../ui/src/components/tree/state/dnd-slice.ts | 94 +- .../src/components/tree/state/drag-slice.ts | 94 +- .../src/components/tree/state/edit-slice.ts | 38 +- .../src/components/tree/state/focus-slice.ts | 56 +- .../ui/src/components/tree/state/initial.ts | 50 +- .../src/components/tree/state/open-slice.ts | 106 +- .../src/components/tree/state/root-reducer.ts | 42 +- .../components/tree/state/selection-slice.ts | 168 +- packages/ui/src/components/tree/types/dnd.ts | 18 +- .../ui/src/components/tree/types/handlers.ts | 64 +- .../ui/src/components/tree/types/renderers.ts | 68 +- .../ui/src/components/tree/types/state.ts | 6 +- .../src/components/tree/types/tree-props.ts | 160 +- .../ui/src/components/tree/types/utils.ts | 36 +- packages/ui/src/components/tree/utils.ts | 364 +- packages/ui/src/components/ui/accordion.tsx | 104 +- .../ui/src/components/ui/alert-dialog.tsx | 208 +- packages/ui/src/components/ui/alert.tsx | 86 +- .../ui/src/components/ui/aspect-ratio.tsx | 10 +- packages/ui/src/components/ui/avatar.tsx | 76 +- packages/ui/src/components/ui/badge.tsx | 68 +- packages/ui/src/components/ui/breadcrumb.tsx | 180 +- packages/ui/src/components/ui/button.tsx | 94 +- packages/ui/src/components/ui/calendar.tsx | 108 +- packages/ui/src/components/ui/card.tsx | 86 +- packages/ui/src/components/ui/carousel.tsx | 448 +- packages/ui/src/components/ui/chart.tsx | 606 +-- packages/ui/src/components/ui/checkbox.tsx | 52 +- .../src/components/ui/collapsible-section.tsx | 328 +- packages/ui/src/components/ui/collapsible.tsx | 18 +- packages/ui/src/components/ui/command.tsx | 264 +- .../ui/src/components/ui/context-menu.tsx | 356 +- packages/ui/src/components/ui/dialog.tsx | 190 +- packages/ui/src/components/ui/drawer.tsx | 174 +- .../ui/src/components/ui/dropdown-menu.tsx | 358 +- packages/ui/src/components/ui/form.tsx | 258 +- packages/ui/src/components/ui/hover-card.tsx | 54 +- packages/ui/src/components/ui/input-otp.tsx | 122 +- packages/ui/src/components/ui/input.tsx | 44 +- packages/ui/src/components/ui/label.tsx | 34 +- packages/ui/src/components/ui/menubar.tsx | 414 +- .../ui/src/components/ui/navigation-menu.tsx | 240 +- packages/ui/src/components/ui/pagination.tsx | 162 +- packages/ui/src/components/ui/popover.tsx | 58 +- packages/ui/src/components/ui/progress.tsx | 46 +- packages/ui/src/components/ui/radio-group.tsx | 72 +- packages/ui/src/components/ui/resizable.tsx | 74 +- packages/ui/src/components/ui/scroll-area.tsx | 76 +- packages/ui/src/components/ui/select.tsx | 286 +- packages/ui/src/components/ui/separator.tsx | 40 +- packages/ui/src/components/ui/sheet.tsx | 214 +- packages/ui/src/components/ui/sidebar.tsx | 1274 ++--- packages/ui/src/components/ui/skeleton.tsx | 14 +- packages/ui/src/components/ui/slider.tsx | 46 +- packages/ui/src/components/ui/sonner.tsx | 54 +- packages/ui/src/components/ui/switch.tsx | 54 +- packages/ui/src/components/ui/table.tsx | 144 +- packages/ui/src/components/ui/tabs.tsx | 106 +- packages/ui/src/components/ui/textarea.tsx | 42 +- packages/ui/src/components/ui/toast.tsx | 222 +- packages/ui/src/components/ui/toaster.tsx | 48 +- .../ui/src/components/ui/toggle-group.tsx | 98 +- packages/ui/src/components/ui/toggle.tsx | 74 +- packages/ui/src/components/ui/tooltip.tsx | 56 +- packages/ui/src/components/ui/use-toast.ts | 6 +- .../src/components/widgets/GalleryWidget.tsx | 42 +- .../components/widgets/GalleryWidgetEdit.tsx | 1150 ++-- .../components/widgets/GalleryWidgetTypes.ts | 20 +- .../components/widgets/GalleryWidgetView.tsx | 626 +-- .../components/widgets/MarkdownTextWidget.tsx | 2 +- .../ui/src/components/widgets/MenuWidget.tsx | 138 +- .../src/components/widgets/MenuWidgetEdit.tsx | 822 +-- .../src/components/widgets/MenuWidgetView.tsx | 950 ++-- .../ui/src/contexts/OrganizationContext.tsx | 72 +- packages/ui/src/contexts/WS_Socket.tsx | 460 +- packages/ui/src/hooks/use-mobile.tsx | 42 +- packages/ui/src/hooks/use-toast.ts | 372 +- packages/ui/src/hooks/useAuth.tsx | 620 +-- packages/ui/src/hooks/useDropZone.ts | 172 +- packages/ui/src/hooks/useFeedData.ts | 648 +-- packages/ui/src/hooks/useNativeVoiceInput.ts | 138 +- packages/ui/src/hooks/useScrollRestoration.ts | 238 +- .../ui/src/integrations/supabase/client.ts | 34 +- .../ui/src/integrations/supabase/types.ts | 4732 ++++++++--------- packages/ui/src/lib/audioUtils.ts | 136 +- packages/ui/src/lib/log.ts | 158 +- packages/ui/src/lib/openai.ts | 2906 +++++----- packages/ui/src/lib/registerWidgets.ts | 2574 ++++----- packages/ui/src/lib/replicate.ts | 358 +- packages/ui/src/lib/utils.ts | 12 +- .../modules/ai/components/ToolSections.tsx | 466 +- .../ui/src/modules/layout/GenericCanvas.tsx | 88 +- .../ui/src/modules/layout/LayoutContainer.tsx | 42 +- .../modules/layout/LayoutContainerEdit.tsx | 1618 +++--- .../modules/layout/LayoutContainerView.tsx | 466 +- .../ui/src/modules/layout/LayoutContext.tsx | 912 ++-- .../ui/src/modules/layout/LayoutManager.ts | 2212 ++++---- packages/ui/src/modules/pages/PageCard.tsx | 420 +- .../ui/src/modules/pages/PagePickerDialog.tsx | 448 +- .../ui/src/modules/pages/VersionManager.tsx | 346 +- .../modules/pages/editor/UserPageDetails.tsx | 76 +- .../pages/editor/UserPageDetailsView.tsx | 386 +- .../src/modules/places/CompetitorSidebar.tsx | 71 - .../modules/places/CompetitorsGridView.tsx | 92 +- .../src/modules/places/CompetitorsMapView.tsx | 2 +- packages/ui/src/modules/places/EmailCell.tsx | 90 +- .../src/modules/places/EmailFinderToolbar.tsx | 116 - .../ui/src/modules/places/GADMPlayground.tsx | 434 -- .../ui/src/modules/places/GridProgress.tsx | 29 - .../places/GridSearchPlayground.old.tsx | 582 -- .../modules/places/GridSearchPlayground.tsx | 185 - .../ui/src/modules/places/RJSFTemplates.tsx | 269 - .../ui/src/modules/places/SearchSidebar.tsx | 206 - .../src/modules/places/client-gridsearch.ts | 594 ++- .../ui/src/modules/places/client-searches.ts | 310 -- .../modules/places/components/AreaScanner.tsx | 203 - .../places/components/GenerateGridForm.tsx | 183 - .../places/components/GridSearchMap.tsx | 374 -- .../places/components/GridSearchSelector.tsx | 81 - .../modules/places/components/MapFooter.tsx | 227 +- .../places/components/MapLayerToggles.tsx | 92 +- .../places/components/MapOverlayToolbars.tsx | 172 +- .../places/components/MapPosterOverlay.tsx | 398 +- .../places/components/RegionSelector.tsx | 13 +- .../modules/places/components/RulerButton.tsx | 72 +- .../map-layers/LiveSearchLayers.tsx | 342 +- .../components/map-layers/RegionLayers.tsx | 478 +- .../components/map-layers/SimulatorLayers.tsx | 306 +- .../modules/places/components/map-styles.ts | 80 +- packages/ui/src/modules/places/emailFinder.ts | 219 - .../places/gadm-picker/GadmPickedRegions.tsx | 194 +- .../modules/places/gadm-picker/GadmPicker.tsx | 1799 +++---- .../places/gadm-picker/GadmPickerContext.tsx | 84 +- .../gadm-picker/GadmRegionCollector.tsx | 574 +- .../places/gadm-picker/GadmTreePicker.tsx | 1280 ++--- .../components/GadmSearchControls.tsx | 244 +- .../src/modules/places/gadm-picker/index.ts | 2 +- .../gridsearch/CompetitorsReportView.tsx | 188 +- .../modules/places/gridsearch/GridSearch.tsx | 318 +- .../places/gridsearch/GridSearchResults.tsx | 369 +- .../gridsearch/GridSearchStreamContext.tsx | 388 +- .../places/gridsearch/GridSearchWizard.tsx | 546 +- .../modules/places/gridsearch/JobViewer.tsx | 574 +- .../places/gridsearch/OngoingSearches.tsx | 170 +- .../gridsearch/RestoredSearchContext.tsx | 156 +- .../simulator/GridSearchSimulator.tsx | 210 +- .../simulator/components/DeferredInputs.tsx | 96 +- .../components/SimulatorControls.tsx | 188 +- .../components/SimulatorSettingsPanel.tsx | 730 +-- .../simulator/components/SimulatorStats.tsx | 112 +- .../simulator/hooks/useGridSimulatorState.ts | 782 +-- .../places/gridsearch/simulator/types.ts | 50 +- .../src/modules/places/hooks/useEnrichment.ts | 79 - .../places/hooks/useGridSearchState.ts | 436 +- .../modules/places/hooks/useMapControls.ts | 225 +- .../src/modules/places/hooks/useRulerTool.ts | 396 +- packages/ui/src/modules/places/index.tsx | 859 --- .../modules/places/useCompetitorActions.ts | 404 -- .../ui/src/modules/places/useGridColumns.tsx | 42 +- .../src/modules/places/utils/poster-themes.ts | 546 +- .../modules/places/v1/client-gridsearch.ts | 249 + .../modules/places/{ => v1}/client-search.ts | 0 packages/ui/src/modules/posts/NewPost.tsx | 454 +- packages/ui/src/modules/posts/PostPage.tsx | 2202 ++++---- .../views/renderers/components/Gallery.tsx | 42 +- .../renderers/components/GalleryEdit.tsx | 294 +- .../renderers/components/GalleryTypes.ts | 174 +- .../renderers/components/GalleryView.tsx | 256 +- .../storage/hooks/useDefaultActions.ts | 492 +- .../hooks/useDefaultKeyboardHandler.ts | 412 +- .../hooks/useDefaultSelectionHandler.ts | 320 +- .../modules/storage/views/ThreeDViewer.tsx | 12 +- 232 files changed, 38422 insertions(+), 42977 deletions(-) delete mode 100644 packages/ui/src/modules/places/CompetitorSidebar.tsx delete mode 100644 packages/ui/src/modules/places/EmailFinderToolbar.tsx delete mode 100644 packages/ui/src/modules/places/GADMPlayground.tsx delete mode 100644 packages/ui/src/modules/places/GridProgress.tsx delete mode 100644 packages/ui/src/modules/places/GridSearchPlayground.old.tsx delete mode 100644 packages/ui/src/modules/places/GridSearchPlayground.tsx delete mode 100644 packages/ui/src/modules/places/RJSFTemplates.tsx delete mode 100644 packages/ui/src/modules/places/SearchSidebar.tsx delete mode 100644 packages/ui/src/modules/places/client-searches.ts delete mode 100644 packages/ui/src/modules/places/components/AreaScanner.tsx delete mode 100644 packages/ui/src/modules/places/components/GenerateGridForm.tsx delete mode 100644 packages/ui/src/modules/places/components/GridSearchMap.tsx delete mode 100644 packages/ui/src/modules/places/components/GridSearchSelector.tsx delete mode 100644 packages/ui/src/modules/places/emailFinder.ts delete mode 100644 packages/ui/src/modules/places/hooks/useEnrichment.ts delete mode 100644 packages/ui/src/modules/places/index.tsx delete mode 100644 packages/ui/src/modules/places/useCompetitorActions.ts create mode 100644 packages/ui/src/modules/places/v1/client-gridsearch.ts rename packages/ui/src/modules/places/{ => v1}/client-search.ts (100%) diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index fb5b87be..5757fd09 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -16,16 +16,17 @@ import { WebSocketProvider } from "@/contexts/WS_Socket"; import { registerAllWidgets } from "@/lib/registerWidgets"; import TopNavigation from "@/components/TopNavigation"; import Footer from "@/components/Footer"; -const GlobalDragDrop = React.lazy(() => import("@/components/GlobalDragDrop")); import { DragDropProvider } from "@/contexts/DragDropContext"; import { useAppStore } from "@/store/appStore"; +const GlobalDragDrop = React.lazy(() => import("@/components/GlobalDragDrop")); // Register all widgets on app boot registerAllWidgets(); import Index from "./pages/Index"; import Auth from "./pages/Auth"; + const UpdatePassword = React.lazy(() => import("./pages/UpdatePassword")); import Profile from "./pages/Profile"; @@ -34,8 +35,8 @@ const Post = React.lazy(() => import("./modules/posts/PostPage")); import UserProfile from "./pages/UserProfile"; import TagPage from "./pages/TagPage"; import SearchResults from "./pages/SearchResults"; -const LogsPage = React.lazy(() => import("./components/logging/LogsPage")); +const LogsPage = React.lazy(() => import("./components/logging/LogsPage")); const Wizard = React.lazy(() => import("./pages/Wizard")); const ProviderSettings = React.lazy(() => import("./pages/ProviderSettings")); const NotFound = React.lazy(() => import("./pages/NotFound")); @@ -51,7 +52,6 @@ let VideoPlayerPlaygroundIntern: any; let PlaygroundImages: any; let PlaygroundImageEditor: any; let VideoGenPlayground: any; -let GridSearchPlayground: any; let PlaygroundCanvas: any; let TypesPlayground: any; let VariablePlayground: any; @@ -76,13 +76,11 @@ if (enablePlaygrounds) { PlaygroundImages = React.lazy(() => import("./pages/PlaygroundImages")); PlaygroundImageEditor = React.lazy(() => import("./pages/PlaygroundImageEditor")); VideoGenPlayground = React.lazy(() => import("./pages/VideoGenPlayground")); - GridSearchPlayground = React.lazy(() => import("./modules/places/GridSearchPlayground")); PlaygroundCanvas = React.lazy(() => import("./modules/layout/PlaygroundCanvas")); TypesPlayground = React.lazy(() => import("@/modules/types/TypesPlayground")); VariablePlayground = React.lazy(() => import("./components/variables/VariablesEditor").then(module => ({ default: module.VariablesEditor }))); I18nPlayground = React.lazy(() => import("./components/playground/I18nPlayground")); PlaygroundChat = React.lazy(() => import("./pages/PlaygroundChat")); - PlacesModule = React.lazy(() => import("./modules/places/index")); LocationDetail = React.lazy(() => import("./modules/places/LocationDetail")); Tetris = React.lazy(() => import("./apps/tetris/Tetris")); FileBrowser = React.lazy(() => import("./apps/filebrowser/FileBrowser")); @@ -101,7 +99,6 @@ const EditPost = React.lazy(() => import("./modules/posts/EditPost")); // import { EcommerceBundleWrapper } from "./bundles/ecommerce"; - // Create a single tracker instance outside the component to avoid re-creation on re-renders /* const tracker = new Tracker({ @@ -114,25 +111,6 @@ tracker.use(trackerAssist()); const AppWrapper = () => { const location = useLocation(); - // Start tracker once on mount - /* - React.useEffect(() => { - - tracker.start().catch(() => { - // Silently ignore — DoNotTrack is active or browser doesn't support required APIs - console.log('OpenReplay tracker failed to start'); - }); - - }, []); - - // Update user identity when auth state changes - React.useEffect(() => { - if (user?.email) { - tracker.setUserID(user.email); - tracker.setMetadata('roles', roles.join(',')); - } - }, [user?.email, roles]); -*/ const searchParams = new URLSearchParams(location.search); const isPageEditor = location.pathname.includes('/pages/') && searchParams.get('edit') === 'true'; const isFullScreenPage = location.pathname.startsWith('/video-feed') || isPageEditor; @@ -206,7 +184,6 @@ const AppWrapper = () => { {/* Playground Routes */} {enablePlaygrounds && ( <> - Loading...}>} /> Loading...}>} /> Loading...}>} /> Loading...}>} /> diff --git a/packages/ui/src/apps/filebrowser/FileBrowserPanel.tsx b/packages/ui/src/apps/filebrowser/FileBrowserPanel.tsx index db31d29c..1b821ea4 100644 --- a/packages/ui/src/apps/filebrowser/FileBrowserPanel.tsx +++ b/packages/ui/src/apps/filebrowser/FileBrowserPanel.tsx @@ -1,869 +1,869 @@ -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { Loader2 } from 'lucide-react'; -import { useAuth } from '@/hooks/useAuth'; -import ImageLightbox from '@/components/ImageLightbox'; -import LightboxText from '@/modules/storage/views/LightboxText'; -import LightboxIframe from '@/modules/storage/views/LightboxIframe'; -import { renderFileViewer } from '@/modules/storage/FileViewerRegistry'; -import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; - -import type { INode, SortKey } from '@/modules/storage/types'; -import { getMimeCategory, vfsUrl, formatSize } from '@/modules/storage/helpers'; -import FileBrowserToolbar from '@/modules/storage/FileBrowserToolbar'; -import FileListView from '@/modules/storage/FileListView'; -import FileGridView from '@/modules/storage/FileGridView'; -import FileDetailPanel from '@/modules/storage/FileDetailPanel'; -import MarkdownRenderer from '@/components/MarkdownRenderer'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Switch } from '@/components/ui/switch'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { IMAGE_EXTS, VIDEO_EXTS, CODE_EXTS } from '@/modules/storage/helpers'; -import { T } from '@/i18n'; - -import { useVfsAdapter } from '@/modules/storage/hooks/useVfsAdapter'; -import { useSelection } from '@/modules/storage/hooks/useSelection'; -import { useFilePreview } from '@/modules/storage/hooks/useFilePreview'; -import { useDefaultKeyboardHandler } from '@/modules/storage/hooks/useDefaultKeyboardHandler'; -import { useDefaultSelectionHandler } from '@/modules/storage/hooks/useDefaultSelectionHandler'; -import { useDefaultActions } from '@/modules/storage/hooks/useDefaultActions'; -import { FileTree } from './FileTree'; -import SearchDialog from './SearchDialog'; - -// ── Props ──────────────────────────────────────────────────────── - -export interface FileBrowserPanelProps { - mount?: string; - path?: string; - glob?: string; - mode?: 'simple' | 'advanced'; - viewMode?: 'list' | 'thumbs' | 'tree'; - sortBy?: SortKey; - showToolbar?: boolean; - canChangeMount?: boolean; - allowFileViewer?: boolean; - allowLightbox?: boolean; - allowPreview?: boolean; - allowDownload?: boolean; - jail?: boolean; - onPathChange?: (path: string) => void; - onMountChange?: (mount: string) => void; - /** If set, auto-open this file in lightbox after directory loads */ - initialFile?: string; - /** If true, automatically loads and renders a readme.md (case-insensitive) in the current directory */ - index?: boolean; - /** If true, allows the fallback FileBrowserPanel to render when no readme is found. */ - allowFallback?: boolean; - /** ID for saving user preferences like viewMode locally (e.g. 'pm-filebrowser-left-panel') */ - autoSaveId?: string; - showFolders?: boolean; - showExplorer?: boolean; - showPreview?: boolean; - showTree?: boolean; - onToggleExplorer?: () => void; - onTogglePreview?: () => void; - onFilterChange?: (glob: string, showFolders: boolean) => void; - onSelect?: (nodes: INode[] | INode | null) => void; - searchQuery?: string; - onSearchQueryChange?: (q: string) => void; - autoFocus?: boolean; - includeSize?: boolean; - splitSizeHorizontal?: number[]; - splitSizeVertical?: number[]; - onLayoutChange?: (sizes: number[], direction: 'horizontal' | 'vertical') => void; - showStatusBar?: boolean; -} - -// ── Main Component ─────────────────────────────────────────────── - -const FileBrowserPanel: React.FC = ({ - mount: mountProp = 'machines', - path: pathProp = '/', - glob = '*.*', - mode = 'simple', - viewMode: initialViewMode = 'list', - sortBy: initialSort = 'name', - showToolbar = true, - canChangeMount = false, - allowFileViewer = true, - allowLightbox = true, - allowPreview = true, - allowDownload = true, - jail = false, - initialFile, - allowFallback = true, - autoFocus = true, - includeSize = false, - index = true, - autoSaveId, - showFolders: showFoldersProp, - showExplorer = true, - showPreview = true, - showTree = true, - onToggleExplorer, - onTogglePreview, - onPathChange, - onMountChange, - onSelect, - onFilterChange, - searchQuery, - onSearchQueryChange, - splitSizeHorizontal, - splitSizeVertical, - onLayoutChange, - showStatusBar = true -}) => { - - const { session } = useAuth(); - const accessToken = session?.access_token; - - const [readmeContent, setReadmeContent] = useState(null); - const [selectedReadmeContent, setSelectedReadmeContent] = useState(null); - - // ── Controlled / uncontrolled mode ──────────────────────────── - - const [internalMount, setInternalMount] = useState(mountProp); - const mount = onMountChange ? mountProp : internalMount; - - const [internalGlob, setInternalGlob] = useState(glob); - const [internalShowFolders, setInternalShowFolders] = useState(true); - const actualCurrentGlob = onFilterChange ? glob : internalGlob; - const showFolders = onFilterChange ? (showFoldersProp ?? true) : internalShowFolders; - - const updateFilter = useCallback((newGlob: string, newShowFolders: boolean) => { - if (onFilterChange) onFilterChange(newGlob, newShowFolders); - else { - setInternalGlob(newGlob); - setInternalShowFolders(newShowFolders); - } - }, [onFilterChange]); - - // ── Available mounts ───────────────────────────────────────── - - const [availableMounts, setAvailableMounts] = useState([]); - useEffect(() => { - const headers: Record = {}; - if (accessToken) headers['Authorization'] = `Bearer ${accessToken}`; - fetch('/api/vfs/mounts', { headers }) - .then(r => r.ok ? r.json() : []) - .then((mounts: { name: string }[]) => setAvailableMounts(mounts.map(m => m.name))) - .catch(() => { }); - }, [accessToken]); - - // ── VFS Adapter ─────────────────────────────────────────────── - - const { - nodes, - sorted, - loading, - error, - currentPath, - currentGlob, - updatePath, - updateMount, - fetchDir, - canGoUp, - goUp: rawGoUp, - breadcrumbs, - jailRoot, - isSearchMode - } = useVfsAdapter({ - mount, - pathProp, - glob: actualCurrentGlob, - showFolders, - accessToken, - index, - jail, - jailPath: pathProp, - sortBy: initialSort, - sortAsc: true, - includeSize, - searchQuery, - onPathChange, - onMountChange: (m) => { - setInternalMount(m); - if (onMountChange) onMountChange(m); - }, - onFetched: async (fetchedNodes, isSearch) => { - setReadmeContent(null); - if (index && !isSearch) { - const readmeNode = fetchedNodes.find(n => n.name.toLowerCase() === 'readme.md'); - if (readmeNode) { - const tokenParam = accessToken ? `token=${encodeURIComponent(accessToken)}` : ''; - const base = vfsUrl('get', mount, readmeNode.path); - const fileUrl = tokenParam ? `${base}?${tokenParam}` : base; - const fileRes = await fetch(fileUrl, { cache: 'no-cache' }); - if (fileRes.ok) { - const content = await fileRes.text(); - setReadmeContent(content); - } - } - } - } - }); - - // ── View Mode & Zoom ───────────────────────────────────────── - - const [internalViewMode, setInternalViewMode] = useState<'list' | 'thumbs' | 'tree'>(() => { - if (autoSaveId) { - const saved = localStorage.getItem(`${autoSaveId}-viewMode`); - if (saved === 'list' || saved === 'thumbs' || saved === 'tree') return saved; - } - return initialViewMode; - }); - - const [internalMode, setInternalMode] = useState<'simple' | 'advanced'>(() => { - if (autoSaveId) { - const saved = localStorage.getItem(`${autoSaveId}-mode`); - if (saved === 'simple' || saved === 'advanced') return saved; - } - return mode; - }); - - const setViewMode = useCallback((m: 'list' | 'thumbs' | 'tree') => { - setInternalViewMode(m); - if (autoSaveId) localStorage.setItem(`${autoSaveId}-viewMode`, m); - }, [autoSaveId]); - - const setDisplayMode = useCallback((m: 'simple' | 'advanced') => { - setInternalMode(m); - if (autoSaveId) localStorage.setItem(`${autoSaveId}-mode`, m); - }, [autoSaveId]); - - const [splitDirection, setSplitDirectionState] = useState<'horizontal' | 'vertical'>(() => { - if (autoSaveId) { - const saved = localStorage.getItem(`${autoSaveId}-splitDir`); - if (saved === 'horizontal' || saved === 'vertical') return saved; - } - return typeof window !== 'undefined' && window.innerWidth < 768 ? 'vertical' : 'horizontal'; - }); - - const setSplitDirection = useCallback((m: 'horizontal' | 'vertical') => { - setSplitDirectionState(m); - if (autoSaveId) localStorage.setItem(`${autoSaveId}-splitDir`, m); - }, [autoSaveId]); - - const viewMode = internalViewMode; - const currentMode = internalMode; - const activeSplitSize = splitDirection === 'horizontal' ? splitSizeHorizontal : splitSizeVertical; - - const [thumbSize, setThumbSize] = useState(() => { - const v = localStorage.getItem('fb-thumb-size'); - return v ? Math.max(60, Math.min(200, Number(v))) : 80; - }); - const [fontSize, setFontSize] = useState(() => { - const v = localStorage.getItem('fb-font-size'); - return v ? Math.max(10, Math.min(18, Number(v))) : 14; - }); - - const zoomIn = () => { - if (viewMode === 'thumbs') setThumbSize(s => { const n = Math.min(200, s + 20); localStorage.setItem('fb-thumb-size', String(n)); return n; }); - else setFontSize(s => { const n = Math.min(18, s + 1); localStorage.setItem('fb-font-size', String(n)); return n; }); - }; - const zoomOut = () => { - if (viewMode === 'thumbs') setThumbSize(s => { const n = Math.max(60, s - 20); localStorage.setItem('fb-thumb-size', String(n)); return n; }); - else setFontSize(s => { const n = Math.max(10, s - 1); localStorage.setItem('fb-font-size', String(n)); return n; }); - }; - - // ── Selection & Refs ───────────────────────────────────────── - - const listRef = useRef(null); - const containerRef = useRef(null); - - const { - focusIdx, - setFocusIdx, - selected, - setSelected, - itemCount, - getNode, - handleItemClick, - clearSelection - } = useSelection({ - sorted, - canGoUp, - onSelect - }); - - // Dummy Sort controls for now since useVfsAdapter uses static sortBy - const [sortBy, setSortBy] = useState(initialSort); - const [sortAsc, setSortAsc] = useState(true); - const cycleSort = () => { - const keys: SortKey[] = ['name', 'ext', 'date', 'type']; - const i = keys.indexOf(sortBy); - if (sortAsc) { setSortAsc(false); } else { setSortBy(keys[(i + 1) % keys.length]); setSortAsc(true); } - }; - - // ── Previews ───────────────────────────────────────────────── - - const { - lightboxNode, - setLightboxNode, - textLightboxNode, - setTextLightboxNode, - iframeLightboxNode, - setIframeLightboxNode, - openPreview, - closeAllPreviews - } = useFilePreview({ allowLightbox, allowFileViewer }); - - // ── Filter Dialog State ──────────────────────────────────────── - - const [filterDialogOpen, setFilterDialogOpen] = useState(false); - const [tempGlob, setTempGlob] = useState(currentGlob); - const [tempShowFolders, setTempShowFolders] = useState(showFolders); - - const applyTempFilter = () => { - updateFilter(tempGlob, tempShowFolders); - setFilterDialogOpen(false); - setTimeout(() => containerRef.current?.focus(), 0); - }; - - const mediaGlob = Array.from(new Set([...IMAGE_EXTS, ...VIDEO_EXTS])).map(ext => `*.${ext}`).join(','); - const codeGlob = Array.from(CODE_EXTS).map(ext => `*.${ext}`).join(','); - - const tokenParam = accessToken ? `token=${encodeURIComponent(accessToken)}` : ''; - - // ── Standalone scroll & grid helpers (shared by keyboard + selection hooks) ── - - const scrollItemIntoView = useCallback((idx: number) => { - if (!listRef.current) return; - const items = listRef.current.querySelectorAll('[data-fb-idx]'); - const el = items[idx] as HTMLElement | undefined; - el?.scrollIntoView({ block: 'nearest' }); - }, []); - - const getGridCols = useCallback((): number => { - if (viewMode !== 'thumbs' || !listRef.current) return 1; - const style = getComputedStyle(listRef.current); - const cols = style.gridTemplateColumns.split(' ').length; - return Math.max(1, cols); - }, [viewMode]); - - // ── Default Selection Handler (first so we get wrapped goUp) ── - - const [pendingFileSelect, setPendingFileSelect] = useState(null); - - const { goUp } = useDefaultSelectionHandler({ - sorted, - canGoUp, - rawGoUp, - currentPath, - loading, - viewMode, - autoFocus, - index, - isSearchMode, - initialFile, - allowFallback, - setFocusIdx, - setSelected, - onSelect, - pendingFileSelect, - setPendingFileSelect, - scrollItemIntoView, - containerRef, - listRef, - }); - - // ── Default Keyboard Handler (uses wrapped goUp) ───────────── - - const { - searchOpen, - setSearchOpen, - searchDisplay, - searchBufferRef, - pendingSearchSelection, - setPendingSearchSelection, - handleKeyDown - } = useDefaultKeyboardHandler({ - focusIdx, - setFocusIdx, - selected, - setSelected, - itemCount, - getNode, - clearSelection, - canGoUp, - goUp, - updatePath, - openPreview, - viewMode, - setViewMode, - setDisplayMode, - currentGlob, - showFolders, - cycleSort, - setTempGlob, - setTempShowFolders, - setFilterDialogOpen, - containerRef, - scrollItemIntoView, - getGridCols, - autoFocus, - allowFallback, - currentPath, - onSearchQueryChange, - searchQuery, - isSearchMode, - onSelect, - sorted, - }); - - // ── Default Actions ────────────────────────────────────────── - - const { - selectedFile, - getFileUrl, - handleView, - handleDownload, - handleDownloadDir, - mediaNodes, - lightboxIdx, - lightboxPrev, - lightboxNext, - closeLightbox, - closeTextLightbox, - closeIframeLightbox, - handleDoubleClick, - handleLinkClick - } = useDefaultActions({ - mount, - mountProp, - pathProp, - accessToken, - selected, - sorted, - canGoUp, - setFocusIdx, - setSelected, - lightboxNode, - setLightboxNode, - textLightboxNode, - setTextLightboxNode, - iframeLightboxNode, - setIframeLightboxNode, - openPreview, - updatePath, - setPendingFileSelect, - containerRef, - getNode, - goUp, - }); - - return ( -
- - - {/* ═══ Toolbar ═══════════════════════════════════ */} - {showToolbar && ( - 0} - handleDownloadDir={handleDownloadDir} - allowDownloadDir={allowDownload} - sortBy={sortBy} - sortAsc={sortAsc} - cycleSort={cycleSort} - zoomIn={zoomIn} - zoomOut={zoomOut} - viewMode={viewMode} - setViewMode={setViewMode} - displayMode={currentMode} - setDisplayMode={setDisplayMode} - splitDirection={splitDirection} - setSplitDirection={setSplitDirection} - showExplorer={showExplorer} - onToggleExplorer={onToggleExplorer} - showPreview={showPreview} - onTogglePreview={onTogglePreview} - onFilterOpen={() => { - setTempGlob(currentGlob); - setTempShowFolders(showFolders); - setFilterDialogOpen(true); - }} - onSearchOpen={() => setSearchOpen(true)} - fontSize={fontSize} - isSearchMode={isSearchMode} - onClearSearch={() => onSearchQueryChange && onSearchQueryChange('')} - /> - )} - - {/* ═══ Content ═══════════════════════════════════ */} - {loading ? ( -
- - Loading… -
- ) : error ? ( -
- {error} -
- ) : itemCount === 0 ? ( -
- Empty directory -
- ) : ( -
- - { - if (onLayoutChange) onLayoutChange(sizes, splitDirection); - }} - {...(activeSplitSize && activeSplitSize.length > 0 ? {} : { autoSaveId: autoSaveId ? `${autoSaveId}-split-${splitDirection}` : `pm-filebrowser-panel-layout-${splitDirection}` })} - className={`flex-1 flex overflow-hidden ${splitDirection === 'vertical' ? 'flex-col min-h-0' : 'flex-row min-w-0'}`} - > - {showExplorer && ( - -
- {viewMode === 'tree' ? ( -
- { - const clean = node.path.replace(/^\/+/, ''); - const base = vfsUrl('ls', mount, clean); - const url = `${base}?includeSize=true`; - const headers: Record = {}; - if (accessToken) headers['Authorization'] = `Bearer ${accessToken}`; - const res = await fetch(url, { headers }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return res.json(); - }} - onSelectionChange={(nodes) => { - setSelected(nodes); - }} - onSelect={(n) => { - setSelected([n]); - }} - onActivate={(n) => { - if (getMimeCategory(n) === 'dir') { - updatePath(n.path || n.name); - } else { - openPreview(n); - } - }} - /> -
- ) : viewMode === 'list' ? ( -
- -
- ) : ( -
- -
- )} -
-
- )} - - {/* Right Pane conditionally renders if preview or fallback exists */} - {showPreview && ((!showExplorer && selected.length === 1) || (allowPreview && selected.length === 1 && getMimeCategory(selected[0]) !== 'dir') || (selected.length === 0 && readmeContent) || (allowPreview && selectedReadmeContent) || (allowPreview && allowFallback && selected.length === 1 && getMimeCategory(selected[0]) === 'dir')) && ( - <> - {showExplorer && } - 1 ? activeSplitSize[1] : (showExplorer ? 40 : 100)} minSize={15} className="relative min-w-0 bg-card/30"> -
- {((!showExplorer && selected.length === 1 && getMimeCategory(selected[0]) !== 'dir') || (allowPreview && selected.length === 1 && getMimeCategory(selected[0]) !== 'dir')) ? ( -
- {renderFileViewer({ - selected: selected[0], - url: getFileUrl(selected[0]), - fileName: selected[0].name, - inline: true, - isOpen: true, - onClose: () => { }, - onLinkClick: (href, e) => handleLinkClick(href, e, selected[0].parent || '/') - })} -
- ) : ((selected.length === 0 && readmeContent) || (allowPreview && selectedReadmeContent)) ? ( -
- { - const basePath = (selectedReadmeContent && selected.length === 1) - ? (selected[0].parent || '/') - : currentPath; - handleLinkClick(href, e, basePath); - }} - /> -
- ) : (allowPreview && allowFallback && selected.length === 1 && getMimeCategory(selected[0]) === 'dir') ? ( -
- -
- ) : null} -
-
- - )} -
- - {/* Detail panel (advanced, desktop only) */} - - - {/* Detail panel (advanced, desktop only) */} - {mode === 'advanced' && ( - - )} -
- )} - - {showStatusBar &&
- sum + (n.size || 0), 0))}${selectedFile ? ` · ${selectedFile.name} (${formatSize(selectedFile.size)})` : ''}`} - style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} - > - {sorted.length} {sorted.length !== 1 ? 'items' : 'item'} - {' · '}{formatSize(sorted.reduce((sum, n) => sum + (n.size || 0), 0))} - {selectedFile ? ` · ${selectedFile.name} (${formatSize(selectedFile.size)})` : ''} - - - {mount}:{currentPath || '/'} - -
} - - {/* ═══ Lightboxes ════════════════════════════════ */} - dir === 'prev' ? lightboxPrev() : lightboxNext()} - showPrompt={false} - /> - - - - {/* ── Dialogs ───────────────────────────────────────────── */} - {filterDialogOpen && ( - { - if (!open) { - setFilterDialogOpen(false); - setTimeout(() => containerRef.current?.focus(), 0); - } - }}> - - - Filter Current View - - Enter a list of comma-separated wildcard matcher expressions (e.g., *.jpg, *.png) or use a preset below. - - -
-
- - -
-
- - setTempGlob(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - applyTempFilter(); - } - }} - autoFocus - placeholder="*.*" - /> -
-
- setTempGlob('*.*')}> - All Files (*.*) - - setTempGlob(mediaGlob)}> - Media - - setTempGlob(codeGlob)}> - Code - -
-
-
- - -
-
-
- )} - - {/* Search dialog */} - {searchOpen && ( - { - const isDir = getMimeCategory(node) === 'dir'; - - if (isDir) { - updatePath(node.path.startsWith('/') ? node.path : `/${node.path}`); - } else { - const parentPath = node.parent || '/'; - - const currentTarget = parentPath.startsWith('/') ? parentPath : `/${parentPath}`; - const normalizedCurrent = currentPath.replace(/\/+$/, '') || '/'; - const normalizedTarget = currentTarget.replace(/\/+$/, '') || '/'; - - if (normalizedTarget !== normalizedCurrent) { - setPendingSearchSelection(node.name); - updatePath(currentTarget); - } else { - const idx = sorted.findIndex(n => n.name === node.name); - if (idx >= 0) { - const focusIndex = canGoUp ? idx + 1 : idx; - setFocusIdx(focusIndex); - const itemNode = sorted[idx]; - if (itemNode) { - const newlySelected = [itemNode]; - setSelected(newlySelected); - if (onSelect) { - onSelect(newlySelected); - } - requestAnimationFrame(() => scrollItemIntoView(focusIndex)); - } - } - } - } - }} - onClose={() => { - setSearchOpen(false); - setTimeout(() => { - if (viewMode === 'tree') { - listRef.current?.focus(); - } else { - containerRef.current?.focus(); - } - }, 0); - }} - /> - )} -
- ); -}; - -export { FileBrowserPanel }; -export default FileBrowserPanel; +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { Loader2 } from 'lucide-react'; +import { useAuth } from '@/hooks/useAuth'; +import ImageLightbox from '@/components/ImageLightbox'; +import LightboxText from '@/modules/storage/views/LightboxText'; +import LightboxIframe from '@/modules/storage/views/LightboxIframe'; +import { renderFileViewer } from '@/modules/storage/FileViewerRegistry'; +import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; + +import type { INode, SortKey } from '@/modules/storage/types'; +import { getMimeCategory, vfsUrl, formatSize } from '@/modules/storage/helpers'; +import FileBrowserToolbar from '@/modules/storage/FileBrowserToolbar'; +import FileListView from '@/modules/storage/FileListView'; +import FileGridView from '@/modules/storage/FileGridView'; +import FileDetailPanel from '@/modules/storage/FileDetailPanel'; +import MarkdownRenderer from '@/components/MarkdownRenderer'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { IMAGE_EXTS, VIDEO_EXTS, CODE_EXTS } from '@/modules/storage/helpers'; +import { T } from '@/i18n'; + +import { useVfsAdapter } from '@/modules/storage/hooks/useVfsAdapter'; +import { useSelection } from '@/modules/storage/hooks/useSelection'; +import { useFilePreview } from '@/modules/storage/hooks/useFilePreview'; +import { useDefaultKeyboardHandler } from '@/modules/storage/hooks/useDefaultKeyboardHandler'; +import { useDefaultSelectionHandler } from '@/modules/storage/hooks/useDefaultSelectionHandler'; +import { useDefaultActions } from '@/modules/storage/hooks/useDefaultActions'; +import { FileTree } from './FileTree'; +import SearchDialog from './SearchDialog'; + +// ── Props ──────────────────────────────────────────────────────── + +export interface FileBrowserPanelProps { + mount?: string; + path?: string; + glob?: string; + mode?: 'simple' | 'advanced'; + viewMode?: 'list' | 'thumbs' | 'tree'; + sortBy?: SortKey; + showToolbar?: boolean; + canChangeMount?: boolean; + allowFileViewer?: boolean; + allowLightbox?: boolean; + allowPreview?: boolean; + allowDownload?: boolean; + jail?: boolean; + onPathChange?: (path: string) => void; + onMountChange?: (mount: string) => void; + /** If set, auto-open this file in lightbox after directory loads */ + initialFile?: string; + /** If true, automatically loads and renders a readme.md (case-insensitive) in the current directory */ + index?: boolean; + /** If true, allows the fallback FileBrowserPanel to render when no readme is found. */ + allowFallback?: boolean; + /** ID for saving user preferences like viewMode locally (e.g. 'pm-filebrowser-left-panel') */ + autoSaveId?: string; + showFolders?: boolean; + showExplorer?: boolean; + showPreview?: boolean; + showTree?: boolean; + onToggleExplorer?: () => void; + onTogglePreview?: () => void; + onFilterChange?: (glob: string, showFolders: boolean) => void; + onSelect?: (nodes: INode[] | INode | null) => void; + searchQuery?: string; + onSearchQueryChange?: (q: string) => void; + autoFocus?: boolean; + includeSize?: boolean; + splitSizeHorizontal?: number[]; + splitSizeVertical?: number[]; + onLayoutChange?: (sizes: number[], direction: 'horizontal' | 'vertical') => void; + showStatusBar?: boolean; +} + +// ── Main Component ─────────────────────────────────────────────── + +const FileBrowserPanel: React.FC = ({ + mount: mountProp = 'machines', + path: pathProp = '/', + glob = '*.*', + mode = 'simple', + viewMode: initialViewMode = 'list', + sortBy: initialSort = 'name', + showToolbar = true, + canChangeMount = false, + allowFileViewer = true, + allowLightbox = true, + allowPreview = true, + allowDownload = true, + jail = false, + initialFile, + allowFallback = true, + autoFocus = true, + includeSize = false, + index = true, + autoSaveId, + showFolders: showFoldersProp, + showExplorer = true, + showPreview = true, + showTree = true, + onToggleExplorer, + onTogglePreview, + onPathChange, + onMountChange, + onSelect, + onFilterChange, + searchQuery, + onSearchQueryChange, + splitSizeHorizontal, + splitSizeVertical, + onLayoutChange, + showStatusBar = true +}) => { + + const { session } = useAuth(); + const accessToken = session?.access_token; + + const [readmeContent, setReadmeContent] = useState(null); + const [selectedReadmeContent, setSelectedReadmeContent] = useState(null); + + // ── Controlled / uncontrolled mode ──────────────────────────── + + const [internalMount, setInternalMount] = useState(mountProp); + const mount = onMountChange ? mountProp : internalMount; + + const [internalGlob, setInternalGlob] = useState(glob); + const [internalShowFolders, setInternalShowFolders] = useState(true); + const actualCurrentGlob = onFilterChange ? glob : internalGlob; + const showFolders = onFilterChange ? (showFoldersProp ?? true) : internalShowFolders; + + const updateFilter = useCallback((newGlob: string, newShowFolders: boolean) => { + if (onFilterChange) onFilterChange(newGlob, newShowFolders); + else { + setInternalGlob(newGlob); + setInternalShowFolders(newShowFolders); + } + }, [onFilterChange]); + + // ── Available mounts ───────────────────────────────────────── + + const [availableMounts, setAvailableMounts] = useState([]); + useEffect(() => { + const headers: Record = {}; + if (accessToken) headers['Authorization'] = `Bearer ${accessToken}`; + fetch('/api/vfs/mounts', { headers }) + .then(r => r.ok ? r.json() : []) + .then((mounts: { name: string }[]) => setAvailableMounts(mounts.map(m => m.name))) + .catch(() => { }); + }, [accessToken]); + + // ── VFS Adapter ─────────────────────────────────────────────── + + const { + nodes, + sorted, + loading, + error, + currentPath, + currentGlob, + updatePath, + updateMount, + fetchDir, + canGoUp, + goUp: rawGoUp, + breadcrumbs, + jailRoot, + isSearchMode + } = useVfsAdapter({ + mount, + pathProp, + glob: actualCurrentGlob, + showFolders, + accessToken, + index, + jail, + jailPath: pathProp, + sortBy: initialSort, + sortAsc: true, + includeSize, + searchQuery, + onPathChange, + onMountChange: (m) => { + setInternalMount(m); + if (onMountChange) onMountChange(m); + }, + onFetched: async (fetchedNodes, isSearch) => { + setReadmeContent(null); + if (index && !isSearch) { + const readmeNode = fetchedNodes.find(n => n.name.toLowerCase() === 'readme.md'); + if (readmeNode) { + const tokenParam = accessToken ? `token=${encodeURIComponent(accessToken)}` : ''; + const base = vfsUrl('get', mount, readmeNode.path); + const fileUrl = tokenParam ? `${base}?${tokenParam}` : base; + const fileRes = await fetch(fileUrl, { cache: 'no-cache' }); + if (fileRes.ok) { + const content = await fileRes.text(); + setReadmeContent(content); + } + } + } + } + }); + + // ── View Mode & Zoom ───────────────────────────────────────── + + const [internalViewMode, setInternalViewMode] = useState<'list' | 'thumbs' | 'tree'>(() => { + if (autoSaveId) { + const saved = localStorage.getItem(`${autoSaveId}-viewMode`); + if (saved === 'list' || saved === 'thumbs' || saved === 'tree') return saved; + } + return initialViewMode; + }); + + const [internalMode, setInternalMode] = useState<'simple' | 'advanced'>(() => { + if (autoSaveId) { + const saved = localStorage.getItem(`${autoSaveId}-mode`); + if (saved === 'simple' || saved === 'advanced') return saved; + } + return mode; + }); + + const setViewMode = useCallback((m: 'list' | 'thumbs' | 'tree') => { + setInternalViewMode(m); + if (autoSaveId) localStorage.setItem(`${autoSaveId}-viewMode`, m); + }, [autoSaveId]); + + const setDisplayMode = useCallback((m: 'simple' | 'advanced') => { + setInternalMode(m); + if (autoSaveId) localStorage.setItem(`${autoSaveId}-mode`, m); + }, [autoSaveId]); + + const [splitDirection, setSplitDirectionState] = useState<'horizontal' | 'vertical'>(() => { + if (autoSaveId) { + const saved = localStorage.getItem(`${autoSaveId}-splitDir`); + if (saved === 'horizontal' || saved === 'vertical') return saved; + } + return typeof window !== 'undefined' && window.innerWidth < 768 ? 'vertical' : 'horizontal'; + }); + + const setSplitDirection = useCallback((m: 'horizontal' | 'vertical') => { + setSplitDirectionState(m); + if (autoSaveId) localStorage.setItem(`${autoSaveId}-splitDir`, m); + }, [autoSaveId]); + + const viewMode = internalViewMode; + const currentMode = internalMode; + const activeSplitSize = splitDirection === 'horizontal' ? splitSizeHorizontal : splitSizeVertical; + + const [thumbSize, setThumbSize] = useState(() => { + const v = localStorage.getItem('fb-thumb-size'); + return v ? Math.max(60, Math.min(200, Number(v))) : 80; + }); + const [fontSize, setFontSize] = useState(() => { + const v = localStorage.getItem('fb-font-size'); + return v ? Math.max(10, Math.min(18, Number(v))) : 14; + }); + + const zoomIn = () => { + if (viewMode === 'thumbs') setThumbSize(s => { const n = Math.min(200, s + 20); localStorage.setItem('fb-thumb-size', String(n)); return n; }); + else setFontSize(s => { const n = Math.min(18, s + 1); localStorage.setItem('fb-font-size', String(n)); return n; }); + }; + const zoomOut = () => { + if (viewMode === 'thumbs') setThumbSize(s => { const n = Math.max(60, s - 20); localStorage.setItem('fb-thumb-size', String(n)); return n; }); + else setFontSize(s => { const n = Math.max(10, s - 1); localStorage.setItem('fb-font-size', String(n)); return n; }); + }; + + // ── Selection & Refs ───────────────────────────────────────── + + const listRef = useRef(null); + const containerRef = useRef(null); + + const { + focusIdx, + setFocusIdx, + selected, + setSelected, + itemCount, + getNode, + handleItemClick, + clearSelection + } = useSelection({ + sorted, + canGoUp, + onSelect + }); + + // Dummy Sort controls for now since useVfsAdapter uses static sortBy + const [sortBy, setSortBy] = useState(initialSort); + const [sortAsc, setSortAsc] = useState(true); + const cycleSort = () => { + const keys: SortKey[] = ['name', 'ext', 'date', 'type']; + const i = keys.indexOf(sortBy); + if (sortAsc) { setSortAsc(false); } else { setSortBy(keys[(i + 1) % keys.length]); setSortAsc(true); } + }; + + // ── Previews ───────────────────────────────────────────────── + + const { + lightboxNode, + setLightboxNode, + textLightboxNode, + setTextLightboxNode, + iframeLightboxNode, + setIframeLightboxNode, + openPreview, + closeAllPreviews + } = useFilePreview({ allowLightbox, allowFileViewer }); + + // ── Filter Dialog State ──────────────────────────────────────── + + const [filterDialogOpen, setFilterDialogOpen] = useState(false); + const [tempGlob, setTempGlob] = useState(currentGlob); + const [tempShowFolders, setTempShowFolders] = useState(showFolders); + + const applyTempFilter = () => { + updateFilter(tempGlob, tempShowFolders); + setFilterDialogOpen(false); + setTimeout(() => containerRef.current?.focus(), 0); + }; + + const mediaGlob = Array.from(new Set([...IMAGE_EXTS, ...VIDEO_EXTS])).map(ext => `*.${ext}`).join(','); + const codeGlob = Array.from(CODE_EXTS).map(ext => `*.${ext}`).join(','); + + const tokenParam = accessToken ? `token=${encodeURIComponent(accessToken)}` : ''; + + // ── Standalone scroll & grid helpers (shared by keyboard + selection hooks) ── + + const scrollItemIntoView = useCallback((idx: number) => { + if (!listRef.current) return; + const items = listRef.current.querySelectorAll('[data-fb-idx]'); + const el = items[idx] as HTMLElement | undefined; + el?.scrollIntoView({ block: 'nearest' }); + }, []); + + const getGridCols = useCallback((): number => { + if (viewMode !== 'thumbs' || !listRef.current) return 1; + const style = getComputedStyle(listRef.current); + const cols = style.gridTemplateColumns.split(' ').length; + return Math.max(1, cols); + }, [viewMode]); + + // ── Default Selection Handler (first so we get wrapped goUp) ── + + const [pendingFileSelect, setPendingFileSelect] = useState(null); + + const { goUp } = useDefaultSelectionHandler({ + sorted, + canGoUp, + rawGoUp, + currentPath, + loading, + viewMode, + autoFocus, + index, + isSearchMode, + initialFile, + allowFallback, + setFocusIdx, + setSelected, + onSelect, + pendingFileSelect, + setPendingFileSelect, + scrollItemIntoView, + containerRef, + listRef, + }); + + // ── Default Keyboard Handler (uses wrapped goUp) ───────────── + + const { + searchOpen, + setSearchOpen, + searchDisplay, + searchBufferRef, + pendingSearchSelection, + setPendingSearchSelection, + handleKeyDown + } = useDefaultKeyboardHandler({ + focusIdx, + setFocusIdx, + selected, + setSelected, + itemCount, + getNode, + clearSelection, + canGoUp, + goUp, + updatePath, + openPreview, + viewMode, + setViewMode, + setDisplayMode, + currentGlob, + showFolders, + cycleSort, + setTempGlob, + setTempShowFolders, + setFilterDialogOpen, + containerRef, + scrollItemIntoView, + getGridCols, + autoFocus, + allowFallback, + currentPath, + onSearchQueryChange, + searchQuery, + isSearchMode, + onSelect, + sorted, + }); + + // ── Default Actions ────────────────────────────────────────── + + const { + selectedFile, + getFileUrl, + handleView, + handleDownload, + handleDownloadDir, + mediaNodes, + lightboxIdx, + lightboxPrev, + lightboxNext, + closeLightbox, + closeTextLightbox, + closeIframeLightbox, + handleDoubleClick, + handleLinkClick + } = useDefaultActions({ + mount, + mountProp, + pathProp, + accessToken, + selected, + sorted, + canGoUp, + setFocusIdx, + setSelected, + lightboxNode, + setLightboxNode, + textLightboxNode, + setTextLightboxNode, + iframeLightboxNode, + setIframeLightboxNode, + openPreview, + updatePath, + setPendingFileSelect, + containerRef, + getNode, + goUp, + }); + + return ( +
+ + + {/* ═══ Toolbar ═══════════════════════════════════ */} + {showToolbar && ( + 0} + handleDownloadDir={handleDownloadDir} + allowDownloadDir={allowDownload} + sortBy={sortBy} + sortAsc={sortAsc} + cycleSort={cycleSort} + zoomIn={zoomIn} + zoomOut={zoomOut} + viewMode={viewMode} + setViewMode={setViewMode} + displayMode={currentMode} + setDisplayMode={setDisplayMode} + splitDirection={splitDirection} + setSplitDirection={setSplitDirection} + showExplorer={showExplorer} + onToggleExplorer={onToggleExplorer} + showPreview={showPreview} + onTogglePreview={onTogglePreview} + onFilterOpen={() => { + setTempGlob(currentGlob); + setTempShowFolders(showFolders); + setFilterDialogOpen(true); + }} + onSearchOpen={() => setSearchOpen(true)} + fontSize={fontSize} + isSearchMode={isSearchMode} + onClearSearch={() => onSearchQueryChange && onSearchQueryChange('')} + /> + )} + + {/* ═══ Content ═══════════════════════════════════ */} + {loading ? ( +
+ + Loading… +
+ ) : error ? ( +
+ {error} +
+ ) : itemCount === 0 ? ( +
+ Empty directory +
+ ) : ( +
+ + { + if (onLayoutChange) onLayoutChange(sizes, splitDirection); + }} + {...(activeSplitSize && activeSplitSize.length > 0 ? {} : { autoSaveId: autoSaveId ? `${autoSaveId}-split-${splitDirection}` : `pm-filebrowser-panel-layout-${splitDirection}` })} + className={`flex-1 flex overflow-hidden ${splitDirection === 'vertical' ? 'flex-col min-h-0' : 'flex-row min-w-0'}`} + > + {showExplorer && ( + +
+ {viewMode === 'tree' ? ( +
+ { + const clean = node.path.replace(/^\/+/, ''); + const base = vfsUrl('ls', mount, clean); + const url = `${base}?includeSize=true`; + const headers: Record = {}; + if (accessToken) headers['Authorization'] = `Bearer ${accessToken}`; + const res = await fetch(url, { headers }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); + }} + onSelectionChange={(nodes) => { + setSelected(nodes); + }} + onSelect={(n) => { + setSelected([n]); + }} + onActivate={(n) => { + if (getMimeCategory(n) === 'dir') { + updatePath(n.path || n.name); + } else { + openPreview(n); + } + }} + /> +
+ ) : viewMode === 'list' ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ )} + + {/* Right Pane conditionally renders if preview or fallback exists */} + {showPreview && ((!showExplorer && selected.length === 1) || (allowPreview && selected.length === 1 && getMimeCategory(selected[0]) !== 'dir') || (selected.length === 0 && readmeContent) || (allowPreview && selectedReadmeContent) || (allowPreview && allowFallback && selected.length === 1 && getMimeCategory(selected[0]) === 'dir')) && ( + <> + {showExplorer && } + 1 ? activeSplitSize[1] : (showExplorer ? 40 : 100)} minSize={15} className="relative min-w-0 bg-card/30"> +
+ {((!showExplorer && selected.length === 1 && getMimeCategory(selected[0]) !== 'dir') || (allowPreview && selected.length === 1 && getMimeCategory(selected[0]) !== 'dir')) ? ( +
+ {renderFileViewer({ + selected: selected[0], + url: getFileUrl(selected[0]), + fileName: selected[0].name, + inline: true, + isOpen: true, + onClose: () => { }, + onLinkClick: (href, e) => handleLinkClick(href, e, selected[0].parent || '/') + })} +
+ ) : ((selected.length === 0 && readmeContent) || (allowPreview && selectedReadmeContent)) ? ( +
+ { + const basePath = (selectedReadmeContent && selected.length === 1) + ? (selected[0].parent || '/') + : currentPath; + handleLinkClick(href, e, basePath); + }} + /> +
+ ) : (allowPreview && allowFallback && selected.length === 1 && getMimeCategory(selected[0]) === 'dir') ? ( +
+ +
+ ) : null} +
+
+ + )} +
+ + {/* Detail panel (advanced, desktop only) */} + + + {/* Detail panel (advanced, desktop only) */} + {mode === 'advanced' && ( + + )} +
+ )} + + {showStatusBar &&
+ sum + (n.size || 0), 0))}${selectedFile ? ` · ${selectedFile.name} (${formatSize(selectedFile.size)})` : ''}`} + style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} + > + {sorted.length} {sorted.length !== 1 ? 'items' : 'item'} + {' · '}{formatSize(sorted.reduce((sum, n) => sum + (n.size || 0), 0))} + {selectedFile ? ` · ${selectedFile.name} (${formatSize(selectedFile.size)})` : ''} + + + {mount}:{currentPath || '/'} + +
} + + {/* ═══ Lightboxes ════════════════════════════════ */} + dir === 'prev' ? lightboxPrev() : lightboxNext()} + showPrompt={false} + /> + + + + {/* ── Dialogs ───────────────────────────────────────────── */} + {filterDialogOpen && ( + { + if (!open) { + setFilterDialogOpen(false); + setTimeout(() => containerRef.current?.focus(), 0); + } + }}> + + + Filter Current View + + Enter a list of comma-separated wildcard matcher expressions (e.g., *.jpg, *.png) or use a preset below. + + +
+
+ + +
+
+ + setTempGlob(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + applyTempFilter(); + } + }} + autoFocus + placeholder="*.*" + /> +
+
+ setTempGlob('*.*')}> + All Files (*.*) + + setTempGlob(mediaGlob)}> + Media + + setTempGlob(codeGlob)}> + Code + +
+
+
+ + +
+
+
+ )} + + {/* Search dialog */} + {searchOpen && ( + { + const isDir = getMimeCategory(node) === 'dir'; + + if (isDir) { + updatePath(node.path.startsWith('/') ? node.path : `/${node.path}`); + } else { + const parentPath = node.parent || '/'; + + const currentTarget = parentPath.startsWith('/') ? parentPath : `/${parentPath}`; + const normalizedCurrent = currentPath.replace(/\/+$/, '') || '/'; + const normalizedTarget = currentTarget.replace(/\/+$/, '') || '/'; + + if (normalizedTarget !== normalizedCurrent) { + setPendingSearchSelection(node.name); + updatePath(currentTarget); + } else { + const idx = sorted.findIndex(n => n.name === node.name); + if (idx >= 0) { + const focusIndex = canGoUp ? idx + 1 : idx; + setFocusIdx(focusIndex); + const itemNode = sorted[idx]; + if (itemNode) { + const newlySelected = [itemNode]; + setSelected(newlySelected); + if (onSelect) { + onSelect(newlySelected); + } + requestAnimationFrame(() => scrollItemIntoView(focusIndex)); + } + } + } + } + }} + onClose={() => { + setSearchOpen(false); + setTimeout(() => { + if (viewMode === 'tree') { + listRef.current?.focus(); + } else { + containerRef.current?.focus(); + } + }, 0); + }} + /> + )} +
+ ); +}; + +export { FileBrowserPanel }; +export default FileBrowserPanel; diff --git a/packages/ui/src/components/AddToCollectionModal.tsx b/packages/ui/src/components/AddToCollectionModal.tsx index 4834e39e..410dd339 100644 --- a/packages/ui/src/components/AddToCollectionModal.tsx +++ b/packages/ui/src/components/AddToCollectionModal.tsx @@ -1,350 +1,350 @@ -import React, { useState, useEffect } from 'react'; -import { supabase } from '@/integrations/supabase/client'; -import { useAuth } from '@/hooks/useAuth'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; -import { Card, CardContent } from '@/components/ui/card'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Plus, Bookmark } from 'lucide-react'; -import { useToast } from '@/hooks/use-toast'; - -interface Collection { - id: string; - name: string; - description: string; - slug: string; - is_public: boolean; - created_at: string; -} - -interface AddToCollectionModalProps { - isOpen: boolean; - onClose: () => void; - pictureId?: string; - postId?: string; -} - -const AddToCollectionModal = ({ isOpen, onClose, pictureId, postId }: AddToCollectionModalProps) => { - if (!pictureId && !postId) { - console.error('AddToCollectionModal requires either pictureId or postId'); - return null; - } - const { user } = useAuth(); - const { toast } = useToast(); - const [collections, setCollections] = useState([]); - const [selectedCollections, setSelectedCollections] = useState>(new Set()); - const [showCreateForm, setShowCreateForm] = useState(false); - const [newCollection, setNewCollection] = useState({ - name: '', - description: '', - is_public: true - }); - const [loading, setLoading] = useState(false); - - useEffect(() => { - if (isOpen && user) { - fetchCollections(); - fetchItemCollections(); - } - }, [isOpen, user, pictureId, postId]); - - const fetchCollections = async () => { - if (!user) return; - - const { data, error } = await supabase - .from('collections') - .select('*') - .eq('user_id', user.id) - .order('created_at', { ascending: false }); - - if (error) { - console.error('Error fetching collections:', error); - return; - } - - setCollections(data || []); - }; - - const fetchItemCollections = async () => { - if (!user) return; - - if (postId) { - // Fetch for post - const { data, error } = await supabase - .from('collection_posts' as any) // Cast as any until types are generated - .select('collection_id') - .eq('post_id', postId); - - if (error) { - console.error('Error fetching post collections:', error); - return; - } - const collectionIds = new Set(data.map((item: any) => item.collection_id)); - setSelectedCollections(collectionIds); - } else if (pictureId) { - // Fetch for picture - const { data, error } = await supabase - .from('collection_pictures') - .select('collection_id') - .eq('picture_id', pictureId); - - if (error) { - console.error('Error fetching picture collections:', error); - return; - } - const collectionIds = new Set(data.map(item => item.collection_id)); - setSelectedCollections(collectionIds); - } - }; - - const createSlug = (name: string) => { - return name - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .trim(); - }; - - const handleCreateCollection = async () => { - if (!user || !newCollection.name.trim()) return; - - setLoading(true); - const slug = createSlug(newCollection.name); - - const { data, error } = await supabase - .from('collections') - .insert({ - user_id: user.id, - name: newCollection.name.trim(), - description: newCollection.description.trim() || null, - slug, - is_public: newCollection.is_public - }) - .select() - .single(); - - if (error) { - console.error('Error creating collection:', error); - toast({ - title: "Error", - description: "Failed to create collection", - variant: "destructive" - }); - setLoading(false); - return; - } - - // Add item to new collection - if (postId) { - await supabase - .from('collection_posts' as any) - .insert({ - collection_id: data.id, - post_id: postId - }); - } else if (pictureId) { - await supabase - .from('collection_pictures') - .insert({ - collection_id: data.id, - picture_id: pictureId - }); - } - - setCollections(prev => [data, ...prev]); - setSelectedCollections(prev => new Set([...prev, data.id])); - setNewCollection({ name: '', description: '', is_public: true }); - setShowCreateForm(false); - setLoading(false); - - toast({ - title: "Success", - description: "Collection created and photo added!" - }); - }; - - const handleToggleCollection = async (collectionId: string) => { - if (!user) return; - - const isSelected = selectedCollections.has(collectionId); - - if (isSelected) { - // Remove from collection - if (postId) { - const { error } = await supabase - .from('collection_posts' as any) - .delete() - .eq('collection_id', collectionId) - .eq('post_id', postId); - if (error) console.error('Error removing post from collection:', error); - } else if (pictureId) { - const { error } = await supabase - .from('collection_pictures') - .delete() - .eq('collection_id', collectionId) - .eq('picture_id', pictureId); - if (error) console.error('Error removing picture from collection:', error); - } - - setSelectedCollections(prev => { - const newSet = new Set(prev); - newSet.delete(collectionId); - return newSet; - }); - } else { - // Add to collection - if (postId) { - const { error } = await supabase - .from('collection_posts' as any) - .insert({ - collection_id: collectionId, - post_id: postId - }); - if (error) console.error('Error adding post to collection:', error); - } else if (pictureId) { - const { error } = await supabase - .from('collection_pictures') - .insert({ - collection_id: collectionId, - picture_id: pictureId - }); - if (error) console.error('Error adding picture to collection:', error); - } - - setSelectedCollections(prev => new Set([...prev, collectionId])); - } - }; - - const handleSave = () => { - toast({ - title: "Saved", - description: "Collection preferences updated!" - }); - onClose(); - }; - - return ( - - - - - - Add to Collection - - - -
- {/* Create new collection */} - {!showCreateForm ? ( - - ) : ( - - - setNewCollection(prev => ({ ...prev, name: e.target.value }))} - /> -