diff --git a/browse/js/app.js b/browse/js/app.js index e26ecd8..59cac74 100644 --- a/browse/js/app.js +++ b/browse/js/app.js @@ -55,6 +55,8 @@ if (previewBody) previewBody.innerHTML = ''; var previewTitle = document.getElementById('previewTitle'); if (previewTitle) previewTitle.textContent = 'No file selected'; + // Reapply view mode for the new URL (incoming/ → grid, etc). + if (events.applyResolvedViewMode) events.applyResolvedViewMode(); } catch (_e) { /* swallow — leave the tree as-is */ } }); } diff --git a/browse/js/events.js b/browse/js/events.js index 0a53b8a..af93d20 100644 --- a/browse/js/events.js +++ b/browse/js/events.js @@ -136,11 +136,10 @@ }); } - // View-mode toggle (Browse vs Grid) - var btnBrowse = document.getElementById('viewModeBrowse'); - var btnGrid = document.getElementById('viewModeGrid'); - if (btnBrowse) btnBrowse.addEventListener('click', function () { setViewMode('browse'); }); - if (btnGrid) btnGrid.addEventListener('click', function () { setViewMode('grid'); }); + // No view-mode buttons; mode is derived from the URL on every + // scope change (resolveViewMode below). Pass-through for the + // initial path. + applyResolvedViewMode(); // Pop-out preview button — opens the current preview in a separate window. var popout = document.getElementById('previewPopout'); @@ -287,25 +286,43 @@ } } - function setViewMode(mode) { + // View mode is URL-driven, not UI-driven. + // + // ?view=grid → grid mode (only honored where classifier is + // available; otherwise falls back to browse) + // ?view=browse → browse mode (always) + // default → path-based: grid when inside an incoming/ + // subtree, browse everywhere else + // + // resolveViewMode reads the current location and returns the mode + // to render; applyResolvedViewMode toggles the panes accordingly. + // Called on initial load and on every client-side rescope. + function resolveViewMode() { + var qs = new URLSearchParams(window.location.search); + var explicit = (qs.get('view') || '').toLowerCase(); + var grid = window.app.modules.grid; + var classifierHere = !!(grid && grid.availableHere && grid.availableHere()); + if (explicit === 'grid') return classifierHere ? 'grid' : 'browse'; + if (explicit === 'browse') return 'browse'; + return classifierHere ? 'grid' : 'browse'; + } + + function applyResolvedViewMode() { + var mode = resolveViewMode(); state.viewMode = mode; var browseView = document.getElementById('browseView'); var gridView = document.getElementById('gridView'); - var btnBrowse = document.getElementById('viewModeBrowse'); - var btnGrid = document.getElementById('viewModeGrid'); if (mode === 'grid') { if (browseView) browseView.classList.add('hidden'); if (gridView) gridView.classList.remove('hidden'); - if (btnBrowse) btnBrowse.setAttribute('aria-selected', 'false'); - if (btnGrid) btnGrid.setAttribute('aria-selected', 'true'); - // Lazily mount classifier on first activation. var grid = window.app.modules.grid; - if (grid && grid.activate) grid.activate(); + if (grid) { + if (grid.reset) grid.reset(); + if (grid.activate) grid.activate(); + } } else { if (browseView) browseView.classList.remove('hidden'); if (gridView) gridView.classList.add('hidden'); - if (btnBrowse) btnBrowse.setAttribute('aria-selected', 'true'); - if (btnGrid) btnGrid.setAttribute('aria-selected', 'false'); } } @@ -376,6 +393,10 @@ history.pushState({ zddcBrowse: true, path: url }, '', url); } catch (_e) { /* private browsing edge cases */ } statusInfo('Entered ' + displayName); + // The new scope may have a different default view (grid inside + // incoming/, browse elsewhere). Re-resolve from the URL now + // that pushState has updated it. + applyResolvedViewMode(); } // Public API @@ -384,6 +405,7 @@ statusError: statusError, statusInfo: statusInfo, statusClear: statusClear, - showBrowseRoot: showBrowseRoot + showBrowseRoot: showBrowseRoot, + applyResolvedViewMode: applyResolvedViewMode }; })(); diff --git a/browse/js/grid.js b/browse/js/grid.js index 20dfed9..b5c9763 100644 --- a/browse/js/grid.js +++ b/browse/js/grid.js @@ -1,81 +1,38 @@ -// grid.js — "Grid mode" plugin for browse. Activated by the -// view-mode toggle in the toolbar. Loads the standalone classifier -// tool as an iframe scoped to the current directory; the user gets -// classifier's full bulk-rename workflow without leaving browse. +// grid.js — "Grid mode" plugin for browse. Loads the classifier tool +// as an iframe scoped to the current directory so users get classifier's +// full bulk-rename workflow without leaving browse. // -// This is a v1 — a future iteration could bundle classifier's -// modules directly into browse for tighter integration (shared -// state, no iframe chrome). For now the iframe is a clean separation -// that preserves classifier's full feature set. +// Availability: only inside an `incoming/` subtree (case-insensitive). +// Working/staging support the classifier tool at the URL level, but +// they're file-staging contexts in normal use, not rename surfaces — +// the Grid toggle is for the inbound side. Outside an incoming/ path, +// the Grid button is hidden entirely (no explanatory empty state). // -// Iframe src resolution: -// - server mode: /classifier.html. classifier is -// auto-served at any working/staging/incoming subtree per -// zddc-server's apps/availability.go. Outside those locations the -// iframe will 404 — we surface a friendly message instead of an -// opaque blank page. -// - file:// or unknown: show a "switch to server mode for grid" -// hint. classifier needs FS-API access; embedding it via file:// -// iframe is blocked by browser security. +// Iframe src resolution: /classifier.html. Iframe +// embedding only works in server mode; file:// pages don't get the +// Grid toggle. (function () { 'use strict'; var state = window.app.state; var mounted = false; - function escapeHtml(s) { - return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - } - function classifierAvailableHere() { - // classifier auto-serves under any path containing a segment - // named working / staging / incoming (case-insensitive). - // browse mode-toggle should reflect that. + // Grid is the classifier-embedded view. Only meaningful in + // incoming/ — that's where bulk-rename actually happens. var path = (window.location && window.location.pathname) || ''; - return /\/(working|staging|incoming)(\/|$)/i.test(path); + return /\/incoming(\/|$)/i.test(path); } function activate() { var host = document.getElementById('gridView'); if (!host) return; - if (mounted) return; - - host.innerHTML = ''; - - if (state.source !== 'server') { - host.innerHTML = - '
' - + '

Grid mode

' - + '

The classifier (bulk ZDDC rename) workflow runs as an embedded' - + ' iframe and requires the page be served by zddc-server.

' - + '

If you opened this file directly (file://), open the standalone' - + ' classifier.html tool instead — it provides the same' - + ' workflow against a local folder you pick from the file system.

' - + '
'; - return; - } - - if (!classifierAvailableHere()) { - host.innerHTML = - '
' - + '

Grid mode

' - + '

The classifier (bulk ZDDC rename) workflow auto-serves at' - + ' working/, staging/, and' - + ' incoming/ URLs. The current page' - + ' (' + escapeHtml(window.location.pathname) + ') isn\'t' - + ' inside any of those, so classifier isn\'t available here.

' - + '

Navigate browse into a working/ or staging/ folder, then' - + ' switch to Grid.

' - + '
'; - return; - } + if (state.source !== 'server' || !classifierAvailableHere()) return; // Compute the iframe src: current page's directory + classifier.html. var pathname = window.location.pathname || '/'; if (!pathname.endsWith('/')) { - // Strip trailing /.html or similar — keep up to the last "/". var lastSlash = pathname.lastIndexOf('/'); pathname = lastSlash >= 0 ? pathname.substring(0, lastSlash + 1) : '/'; } @@ -91,12 +48,19 @@ mounted = true; } + // When the user navigates between scopes (client-side rescope on + // dblclick), the iframe needs to be reloaded for the new path. + // Callers reset before re-activating. + function reset() { + mounted = false; + var host = document.getElementById('gridView'); + if (host) host.innerHTML = ''; + } + window.app.modules.grid = { activate: activate, - // Hook the toggle button visibility / hint to the activation - // predicate so users at non-classifier paths see the button - // in a disabled state with explanation. Callers run this - // after the initial directory is loaded. + reset: reset, + // Hook for events.js to show/hide the Grid toggle button. availableHere: function () { return state.source === 'server' && classifierAvailableHere(); } diff --git a/browse/template.html b/browse/template.html index fb6804b..27f49a3 100644 --- a/browse/template.html +++ b/browse/template.html @@ -53,10 +53,6 @@