Browse Source

Add resizable sidebar and text wrap option to File Manager (#160)
- Add draggable resize handle to folder sidebar (200-500px range)
- Persist sidebar width in localStorage
- Double-click resize handle to reset to default width
- Add "Wrap" toggle button in Folders header for text wrapping
- When wrap enabled: folder names break across lines, menu always visible
- When wrap disabled: folder names truncate with tooltip on hover
- Persist wrap preference in localStorage

Closes #160

maziggy 4 tháng trước cách đây
mục cha
commit
3a03cfe928

+ 102 - 9
frontend/src/pages/FileManagerPage.tsx

@@ -693,9 +693,10 @@ interface FolderTreeItemProps {
   onLink: (folder: LibraryFolderTree) => void;
   onRename: (folder: LibraryFolderTree) => void;
   depth?: number;
+  wrapNames?: boolean;
 }
 
-function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink, onRename, depth = 0 }: FolderTreeItemProps) {
+function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink, onRename, depth = 0, wrapNames = false }: FolderTreeItemProps) {
   const [expanded, setExpanded] = useState(true);
   const [showActions, setShowActions] = useState(false);
   const hasChildren = folder.children.length > 0;
@@ -726,12 +727,12 @@ function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink,
           <div className="w-4.5" />
         )}
         <FolderOpen className="w-4 h-4 text-bambu-green flex-shrink-0" />
-        <span className="text-sm truncate flex-1" title={folder.name}>{folder.name}</span>
+        <span className={`text-sm flex-1 min-w-0 ${wrapNames ? 'break-all' : 'truncate'}`} title={folder.name}>{folder.name}</span>
         {/* Link indicator - clickable to change link */}
         {isLinked && (
           <button
             onClick={(e) => { e.stopPropagation(); onLink(folder); }}
-            className="flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 transition-colors"
+            className="flex-shrink-0 flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 transition-colors"
             title={`${folder.project_name ? `Project: ${folder.project_name}` : `Archive: ${folder.archive_name}`} (click to change)`}
           >
             <Link2 className="w-3 h-3" />
@@ -743,19 +744,19 @@ function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink,
           </button>
         )}
         {folder.file_count > 0 && (
-          <span className="text-xs text-bambu-gray">{folder.file_count}</span>
+          <span className="flex-shrink-0 text-xs text-bambu-gray">{folder.file_count}</span>
         )}
         {/* Quick link button - always visible for unlinked folders */}
         {!isLinked && (
           <button
             onClick={(e) => { e.stopPropagation(); onLink(folder); }}
-            className="p-1 rounded hover:bg-bambu-dark-tertiary"
+            className="flex-shrink-0 p-1 rounded hover:bg-bambu-dark-tertiary"
             title="Link to project or archive"
           >
             <Link2 className="w-3.5 h-3.5 text-bambu-gray hover:text-bambu-green" />
           </button>
         )}
-        <div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity" onClick={(e) => e.stopPropagation()}>
+        <div className={`flex-shrink-0 flex items-center gap-0.5 transition-opacity ${wrapNames ? '' : 'opacity-0 group-hover:opacity-100'}`} onClick={(e) => e.stopPropagation()}>
           <div className="relative">
             <button
               onClick={() => setShowActions(!showActions)}
@@ -806,6 +807,7 @@ function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink,
               onLink={onLink}
               onRename={onRename}
               depth={depth + 1}
+              wrapNames={wrapNames}
             />
           ))}
         </div>
@@ -980,6 +982,55 @@ export function FileManagerPage() {
   const [viewMode, setViewMode] = useState<'grid' | 'list'>(() => {
     return (localStorage.getItem('library-view-mode') as 'grid' | 'list') || 'grid';
   });
+  const [wrapFolderNames, setWrapFolderNames] = useState(() => {
+    return localStorage.getItem('library-wrap-folders') === 'true';
+  });
+
+  // Resizable sidebar state
+  const [sidebarWidth, setSidebarWidth] = useState(() => {
+    const saved = localStorage.getItem('library-sidebar-width');
+    return saved ? parseInt(saved, 10) : 256; // Default w-64 = 256px
+  });
+  const [isResizing, setIsResizing] = useState(false);
+  const sidebarRef = useRef<HTMLDivElement>(null);
+
+  // Handle sidebar resize
+  useEffect(() => {
+    if (!isResizing) return;
+
+    // Prevent text selection during resize
+    document.body.style.userSelect = 'none';
+    document.body.style.cursor = 'col-resize';
+
+    const handleMouseMove = (e: MouseEvent) => {
+      if (!sidebarRef.current) return;
+      const containerRect = sidebarRef.current.parentElement?.getBoundingClientRect();
+      if (!containerRect) return;
+      // Calculate new width based on mouse position relative to container
+      const newWidth = e.clientX - containerRect.left;
+      // Clamp between 200px and 500px
+      const clampedWidth = Math.min(500, Math.max(200, newWidth));
+      setSidebarWidth(clampedWidth);
+    };
+
+    const handleMouseUp = () => {
+      setIsResizing(false);
+      document.body.style.userSelect = '';
+      document.body.style.cursor = '';
+      // Save to localStorage
+      localStorage.setItem('library-sidebar-width', String(sidebarWidth));
+    };
+
+    document.addEventListener('mousemove', handleMouseMove);
+    document.addEventListener('mouseup', handleMouseUp);
+
+    return () => {
+      document.removeEventListener('mousemove', handleMouseMove);
+      document.removeEventListener('mouseup', handleMouseUp);
+      document.body.style.userSelect = '';
+      document.body.style.cursor = '';
+    };
+  }, [isResizing, sidebarWidth]);
 
   // Filter and sort state
   const [searchQuery, setSearchQuery] = useState('');
@@ -1372,10 +1423,51 @@ export function FileManagerPage() {
 
       {/* Main content */}
       <div className="flex-1 flex gap-6 min-h-0">
-        {/* Folder sidebar */}
-        <div className="w-64 flex-shrink-0 bg-bambu-card rounded-lg border border-bambu-dark-tertiary overflow-hidden flex flex-col">
-          <div className="p-3 border-b border-bambu-dark-tertiary">
+        {/* Folder sidebar - resizable */}
+        <div
+          ref={sidebarRef}
+          className="flex-shrink-0 bg-bambu-card rounded-lg border border-bambu-dark-tertiary overflow-hidden flex flex-col relative"
+          style={{ width: `${sidebarWidth}px` }}
+        >
+          {/* Resize handle - drag to resize, double-click to reset */}
+          <div
+            className={`absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize z-10 group/resize flex items-center justify-center transition-colors ${
+              isResizing ? 'bg-bambu-green' : 'hover:bg-bambu-green/50'
+            }`}
+            onMouseDown={(e) => {
+              e.preventDefault();
+              setIsResizing(true);
+            }}
+            onDoubleClick={() => {
+              setSidebarWidth(256); // Reset to default w-64
+              localStorage.setItem('library-sidebar-width', '256');
+            }}
+            title="Drag to resize, double-click to reset"
+          >
+            {/* Grip dots */}
+            <div className={`flex flex-col gap-1 opacity-0 group-hover/resize:opacity-100 transition-opacity ${isResizing ? 'opacity-100' : ''}`}>
+              <div className="w-0.5 h-0.5 rounded-full bg-white/70" />
+              <div className="w-0.5 h-0.5 rounded-full bg-white/70" />
+              <div className="w-0.5 h-0.5 rounded-full bg-white/70" />
+            </div>
+          </div>
+          <div className="p-3 border-b border-bambu-dark-tertiary flex items-center justify-between">
             <h2 className="text-sm font-medium text-white">Folders</h2>
+            <button
+              onClick={() => {
+                const newValue = !wrapFolderNames;
+                setWrapFolderNames(newValue);
+                localStorage.setItem('library-wrap-folders', String(newValue));
+              }}
+              className={`text-xs px-1.5 py-0.5 rounded transition-colors ${
+                wrapFolderNames
+                  ? 'bg-bambu-green/20 text-bambu-green'
+                  : 'text-bambu-gray hover:text-white hover:bg-bambu-dark'
+              }`}
+              title={wrapFolderNames ? 'Disable text wrapping' : 'Enable text wrapping'}
+            >
+              Wrap
+            </button>
           </div>
           <div className="flex-1 overflow-y-auto p-2">
             {/* All Files (root) */}
@@ -1401,6 +1493,7 @@ export function FileManagerPage() {
                 onDelete={(id) => setDeleteConfirm({ type: 'folder', id })}
                 onLink={setLinkFolder}
                 onRename={(f) => setRenameItem({ type: 'folder', id: f.id, name: f.name })}
+                wrapNames={wrapFolderNames}
               />
             ))}
           </div>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
static/assets/index-CGiVeylo.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
static/assets/index-CwW-gspw.css


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
static/assets/index-nwJjDqT-.css


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-nyN_9kgu.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-CwW-gspw.css">
+    <script type="module" crossorigin src="/assets/index-CGiVeylo.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-nwJjDqT-.css">
   </head>
   <body>
     <div id="root"></div>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác