web: a row's menu is the tree's own and speaks only of that row, and only the row being read admits to it
The menu that hangs off a folder is a menu read mid-scan, with the eye still moving down the column, and it was drawn to the measure of a page rather than of a list: a box wide enough to be a destination, type the size of a heading. A catalogue's own context menu is a thing passed over on the way somewhere else — it is sized to its words and to the row it hangs off, and the reader's eye steps across it without stopping. The type comes down a point, the rows tighten to the tree's own measure, and the frame around them thickens to a radius the rows themselves use. The clamp that keeps it in the window follows the smaller box, so a menu opened near the right or the bottom edge no longer floats an inch off the pointer for room it does not use. A row that is being read has exactly one thing to be asked of it — that it stop — where a row at rest has the reading that brings it up to date. The menu said RESCAN either way, greyed while a scan was up, which is a command offered and refused in the same breath. The two never both apply, so the menu now carries the one the row is actually in: a roll being read offers STOP SCAN and stops that roll; a roll at rest offers UPDATE... and reads it again. A row with folders under it grows a third thing — a fold that closes the branch and not merely the row, because a reader who is done with a tree means the whole of it shut, and closing only the row they happened to point at leaves the children standing open underneath it. The last is a matter of what a row is allowed to claim. Every row of a roll spanning the tree spun while that roll was being read, so a reader looking at a tree of a hundred folders saw a hundred rows each insisting it was the one being read, when the reading had been pointed at one of them. A row now says it is being read only when it is the row the reading was kept to — the folder it was pointed at, which for a whole roll is the roll's own row. A tree that says all of itself is busy when one branch of it is says nothing a reader can trust.
This commit is contained in:
@@ -195,7 +195,7 @@ export function Library() {
|
||||
// right click landed on the empty part of the column, which is the way in for
|
||||
// one more folder; `root` means it landed on a row at the top level — the head
|
||||
// of a tree — which also carries folding the whole tree.
|
||||
const [menu, setMenu] = useState<{ folder: LibraryFolder | null; root?: boolean; rel?: string; x: number; y: number } | null>(null);
|
||||
const [menu, setMenu] = useState<{ folder: LibraryFolder | null; root?: boolean; rel?: string; node?: string; x: number; y: number } | null>(null);
|
||||
// Where the drag of the column divider started, and how wide the column was
|
||||
// then — the pointer only ever reports a position, not a distance moved.
|
||||
const grip = useRef<{ x: number; w: number } | null>(null);
|
||||
@@ -1060,14 +1060,17 @@ export function Library() {
|
||||
onContextMenu={(e) => {
|
||||
if (e.target !== e.currentTarget) return;
|
||||
e.preventDefault();
|
||||
setMenu({ folder: null, x: Math.min(e.clientX, window.innerWidth - 190), y: Math.min(e.clientY, window.innerHeight - 96) });
|
||||
setMenu({ folder: null, x: Math.min(e.clientX, window.innerWidth - 150), y: Math.min(e.clientY, window.innerHeight - 150) });
|
||||
}}
|
||||
>
|
||||
{rows.map((n) => {
|
||||
const folder = folders.find((f) => f.name === n.key.split('/')[0]) ?? null;
|
||||
// A roll being read says so on its own row: while a scan runs
|
||||
// the toolbar's progress line is nowhere near the eye.
|
||||
const scanning = !!folder && live?.folder === folder.name;
|
||||
// The row being read says so, and no other: a roll is read
|
||||
// from one folder down, and every row of that roll spinning
|
||||
// is a tree that says all of itself is being read when one
|
||||
// branch of it is. The row is the folder the reading was
|
||||
// kept to, which for a whole roll is the roll's own row.
|
||||
const scanning = !!folder && live?.folder === folder.name && relOf(n.key) === live.progress.from;
|
||||
const under = parents.has(n.key);
|
||||
return (
|
||||
<button
|
||||
@@ -1120,8 +1123,9 @@ export function Library() {
|
||||
folder,
|
||||
root: n.depth === 0,
|
||||
rel: under ? `${under}/` : '',
|
||||
x: Math.min(e.clientX || box.left, window.innerWidth - 190),
|
||||
y: Math.min(e.clientY || box.top, window.innerHeight - 96),
|
||||
node: n.key,
|
||||
x: Math.min(e.clientX || box.left, window.innerWidth - 150),
|
||||
y: Math.min(e.clientY || box.top, window.innerHeight - 150),
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -1431,19 +1435,61 @@ export function Library() {
|
||||
<span className="lib-menu-icon" aria-hidden="true">✎</span>
|
||||
{t('lib.rename')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="lib-menu-item"
|
||||
data-key={`lib-rescan-${menu.folder.name}`}
|
||||
disabled={!!live}
|
||||
onClick={() => {
|
||||
setMenu(null);
|
||||
void scan(menu.folder!, false, menu.rel ?? '');
|
||||
}}
|
||||
>
|
||||
<span className="lib-menu-icon" aria-hidden="true">↻</span>
|
||||
{t('lib.rescan')}
|
||||
</button>
|
||||
{/* A roll being read has one thing to be asked of it — that it
|
||||
stop — where a roll at rest has the reading that brings it up
|
||||
to date; the two never both apply, so the menu carries the one
|
||||
the row is actually in. */}
|
||||
{live?.folder === menu.folder.name ? (
|
||||
<button
|
||||
type="button"
|
||||
className="lib-menu-item"
|
||||
data-key={`lib-menu-stop-${menu.folder.name}`}
|
||||
onClick={() => {
|
||||
setMenu(null);
|
||||
stopScan();
|
||||
}}
|
||||
>
|
||||
<span className="lib-menu-icon" aria-hidden="true">■</span>
|
||||
{t('lib.stop')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="lib-menu-item"
|
||||
data-key={`lib-rescan-${menu.folder.name}`}
|
||||
disabled={!!live}
|
||||
onClick={() => {
|
||||
setMenu(null);
|
||||
void scan(menu.folder!, false, menu.rel ?? '');
|
||||
}}
|
||||
>
|
||||
<span className="lib-menu-icon" aria-hidden="true">↻</span>
|
||||
{t('lib.update')}
|
||||
</button>
|
||||
)}
|
||||
{/* A row with folders under it is a row that folds, and the branch
|
||||
it holds goes with it: a reader who is done with a tree means
|
||||
the whole of it shut, not the one row they happened to point
|
||||
at with its children left open underneath. */}
|
||||
{menu.node && parents.has(menu.node) ? (
|
||||
<button
|
||||
type="button"
|
||||
className="lib-menu-item"
|
||||
data-key={`lib-fold-${menu.node}`}
|
||||
onClick={() => {
|
||||
const key = menu.node!;
|
||||
setMenu(null);
|
||||
setExpanded((prev) => {
|
||||
const next = new Set(prev);
|
||||
for (const k of next) if (k === key || k.startsWith(`${key}/`)) next.delete(k);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="lib-menu-icon" aria-hidden="true">▴</span>
|
||||
{t('lib.collapse')}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="lib-menu-item"
|
||||
|
||||
@@ -241,11 +241,12 @@ export const en: Dict = {
|
||||
'lib.stop': 'STOP SCAN',
|
||||
'lib.rename': 'RENAME FOLDER',
|
||||
'lib.renameAsk': 'Name shown for this folder (the folder on the disk is not renamed):',
|
||||
'lib.rescan': 'RESCAN',
|
||||
'lib.update': 'UPDATE...',
|
||||
'lib.remove': 'REMOVE',
|
||||
'lib.reconnect': 'GRANT ACCESS AGAIN',
|
||||
'lib.nodeHint': 'Right-click to rename, rescan or remove the folder.',
|
||||
'lib.nodeHint': 'Right-click to rename, update, collapse or remove the folder.',
|
||||
'lib.collapseAll': 'COLLAPSE ALL',
|
||||
'lib.collapse': 'COLLAPSE',
|
||||
'lib.colHint': 'Right-click the empty part of this column to add a folder.',
|
||||
'lib.colWidth': 'Drag to set the width of the folder column (arrow keys when focused)',
|
||||
'lib.viewPreview': 'SINGLE',
|
||||
|
||||
@@ -253,11 +253,12 @@ export const vi = {
|
||||
'lib.stop': 'DỪNG QUÉT',
|
||||
'lib.rename': 'ĐỔI TÊN THƯ MỤC',
|
||||
'lib.renameAsk': 'Tên hiển thị cho thư mục này (thư mục trên máy không bị đổi tên):',
|
||||
'lib.rescan': 'QUÉT LẠI',
|
||||
'lib.update': 'CẬP NHẬT...',
|
||||
'lib.remove': 'BỎ',
|
||||
'lib.reconnect': 'CẤP LẠI QUYỀN',
|
||||
'lib.nodeHint': 'Chuột phải để đổi tên, quét lại hay bỏ thư mục.',
|
||||
'lib.nodeHint': 'Chuột phải để đổi tên, cập nhật, thu gọn hay bỏ thư mục.',
|
||||
'lib.collapseAll': 'THU GỌN TẤT CẢ',
|
||||
'lib.collapse': 'THU GỌN',
|
||||
'lib.colHint': 'Chuột phải vào vùng trống của cột này để thêm thư mục.',
|
||||
'lib.colWidth': 'Kéo để chỉnh độ rộng cột thư mục (mũi tên trái/phải khi đang chọn)',
|
||||
'lib.viewPreview': 'MỘT ẢNH',
|
||||
|
||||
@@ -1210,26 +1210,31 @@ button.hint:disabled { cursor: default; }
|
||||
when the window is narrow. */
|
||||
.lib-icon { padding: 5px 8px; justify-content: center; }
|
||||
/* A folder's own menu, on a right click. Fixed, so the tree's own scroll cannot
|
||||
clip it; the position is clamped to the window when it is drawn. */
|
||||
clip it; the position is clamped to the window when it is drawn.
|
||||
Sized after a catalogue's own menu rather than after a page's: it hangs off a
|
||||
row of a list the reader is scanning with their eyes, and a box of commands as
|
||||
loud as the list is a box that stops the scanning. The type and the rows are
|
||||
the tree's own, a shade tighter. */
|
||||
.lib-menu {
|
||||
position: fixed;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 170px;
|
||||
padding: 4px;
|
||||
min-width: 148px;
|
||||
padding: 3px;
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.lib-menu-item {
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 4px 7px;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
letter-spacing: 0.03em;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
/* Each command leads with a mark of its own — rename, scan, forget, fold — the
|
||||
@@ -1237,9 +1242,9 @@ button.hint:disabled { cursor: default; }
|
||||
column of marks rather than a block of uppercase. */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 7px;
|
||||
}
|
||||
.lib-menu-icon { flex: 0 0 auto; width: 12px; text-align: center; opacity: 0.85; }
|
||||
.lib-menu-icon { flex: 0 0 auto; width: 10px; text-align: center; opacity: 0.85; }
|
||||
.lib-menu-item:hover:not(:disabled) { background: var(--accent-soft); color: var(--accent); }
|
||||
.lib-menu-item:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user