Skip to content

Commit 1737c68

Browse files
authored
Fix double scrollbars in dir preview (#932)
Phew this took a while, but I think it's a good compromise. All the scrolling for a preview view now must happen inside the individual views, rather than at the root level. Now, the scrollbars render in the right places and are always visible inside the block. I don't love the blurred header for the table, but it was make it blurry or make it even more opaque, which would ruin the transparency
1 parent c34fa96 commit 1737c68

File tree

3 files changed

+80
-80
lines changed

3 files changed

+80
-80
lines changed

frontend/app/view/preview/directorypreview.less

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,28 @@
77
display: flex;
88
flex-direction: column;
99
height: 100%;
10+
--min-row-width: 35rem;
1011
.dir-table {
1112
height: 100%;
12-
min-width: 600px;
13+
width: 100%;
1314
--col-size-size: 0.2rem;
14-
border-radius: 3px;
1515
display: flex;
1616
flex-direction: column;
17-
font: var(--base-font);
17+
18+
&:not([data-scroll-height="0"]) .dir-table-head {
19+
background: rgba(10, 10, 10, 0.5);
20+
backdrop-filter: blur(4px);
21+
}
1822
.dir-table-head {
23+
position: sticky;
24+
top: 0;
25+
z-index: 10;
26+
width: fit-content;
27+
border-bottom: 1px solid var(--border-color);
28+
1929
.dir-table-head-row {
2030
display: flex;
21-
border-bottom: 1px solid var(--border-color);
31+
min-width: var(--min-row-width);
2232
padding: 4px 6px;
2333
font-size: 0.75rem;
2434

@@ -68,10 +78,8 @@
6878
}
6979

7080
.dir-table-body {
71-
flex: 1 1 auto;
7281
display: flex;
7382
flex-direction: column;
74-
overflow: hidden;
7583
.dir-table-body-search-display {
7684
display: flex;
7785
border-radius: 3px;
@@ -94,6 +102,7 @@
94102
align-items: center;
95103
border-radius: 5px;
96104
padding: 0 6px;
105+
min-width: var(--min-row-width);
97106

98107
&.focused {
99108
background-color: rgb(from var(--accent-color) r g b / 0.5);

frontend/app/view/preview/directorypreview.tsx

Lines changed: 64 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
import { ContextMenuModel } from "@/app/store/contextmenu";
55
import { atoms, createBlock, getApi } from "@/app/store/global";
6+
import { FileService } from "@/app/store/services";
67
import type { PreviewModel } from "@/app/view/preview/preview";
7-
import * as services from "@/store/services";
8-
import * as keyutil from "@/util/keyutil";
9-
import * as util from "@/util/util";
8+
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
9+
import { base64ToString, isBlank } from "@/util/util";
1010
import {
1111
Column,
1212
Row,
@@ -19,14 +19,11 @@ import {
1919
} from "@tanstack/react-table";
2020
import clsx from "clsx";
2121
import dayjs from "dayjs";
22-
import * as jotai from "jotai";
23-
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
22+
import { useAtom, useAtomValue } from "jotai";
23+
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
2424
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2525
import { quote as shellQuote } from "shell-quote";
26-
27-
import { OverlayScrollbars } from "overlayscrollbars";
28-
29-
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
26+
import { debounce } from "throttle-debounce";
3027
import "./directorypreview.less";
3128

3229
interface DirectoryTableProps {
@@ -95,7 +92,7 @@ function getLastModifiedTime(unixMillis: number, column: Column<FileInfo, number
9592
const iconRegex = /^[a-z0-9- ]+$/;
9693

9794
function isIconValid(icon: string): boolean {
98-
if (util.isBlank(icon)) {
95+
if (isBlank(icon)) {
9996
return false;
10097
}
10198
return icon.match(iconRegex) != null;
@@ -134,11 +131,11 @@ function DirectoryTable({
134131
setSelectedPath,
135132
setRefreshVersion,
136133
}: DirectoryTableProps) {
137-
const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom);
134+
const fullConfig = useAtomValue(atoms.fullConfigAtom);
138135
const getIconFromMimeType = useCallback(
139136
(mimeType: string): string => {
140137
while (mimeType.length > 0) {
141-
let icon = fullConfig.mimetypes?.[mimeType]?.icon ?? null;
138+
const icon = fullConfig.mimetypes?.[mimeType]?.icon ?? null;
142139
if (isIconValid(icon)) {
143140
return `fa fa-solid fa-${icon} fa-fw`;
144141
}
@@ -149,10 +146,7 @@ function DirectoryTable({
149146
[fullConfig.mimetypes]
150147
);
151148
const getIconColor = useCallback(
152-
(mimeType: string): string => {
153-
let iconColor = fullConfig.mimetypes?.[mimeType]?.color ?? "inherit";
154-
return iconColor;
155-
},
149+
(mimeType: string): string => fullConfig.mimetypes?.[mimeType]?.color ?? "inherit",
156150
[fullConfig.mimetypes]
157151
);
158152
const columns = useMemo(
@@ -261,8 +255,25 @@ function DirectoryTable({
261255
return colSizes;
262256
}, [table.getState().columnSizingInfo]);
263257

258+
const osRef = useRef<OverlayScrollbarsComponentRef>();
259+
const bodyRef = useRef<HTMLDivElement>();
260+
const [scrollHeight, setScrollHeight] = useState(0);
261+
262+
const onScroll = useCallback(
263+
debounce(2, () => {
264+
setScrollHeight(osRef.current.osInstance().elements().viewport.scrollTop);
265+
}),
266+
[]
267+
);
264268
return (
265-
<div className="dir-table" style={{ ...columnSizeVars }}>
269+
<OverlayScrollbarsComponent
270+
options={{ scrollbars: { autoHide: "leave" } }}
271+
events={{ scroll: onScroll }}
272+
className="dir-table"
273+
style={{ ...columnSizeVars }}
274+
ref={osRef}
275+
data-scroll-height={scrollHeight}
276+
>
266277
<div className="dir-table-head">
267278
{table.getHeaderGroups().map((headerGroup) => (
268279
<div className="dir-table-head-row" key={headerGroup.id}>
@@ -295,6 +306,7 @@ function DirectoryTable({
295306
</div>
296307
{table.getState().columnSizingInfo.isResizingColumn ? (
297308
<MemoizedTableBody
309+
bodyRef={bodyRef}
298310
model={model}
299311
data={data}
300312
table={table}
@@ -304,9 +316,11 @@ function DirectoryTable({
304316
setSearch={setSearch}
305317
setSelectedPath={setSelectedPath}
306318
setRefreshVersion={setRefreshVersion}
319+
osRef={osRef.current}
307320
/>
308321
) : (
309322
<TableBody
323+
bodyRef={bodyRef}
310324
model={model}
311325
data={data}
312326
table={table}
@@ -316,13 +330,15 @@ function DirectoryTable({
316330
setSearch={setSearch}
317331
setSelectedPath={setSelectedPath}
318332
setRefreshVersion={setRefreshVersion}
333+
osRef={osRef.current}
319334
/>
320335
)}
321-
</div>
336+
</OverlayScrollbarsComponent>
322337
);
323338
}
324339

325340
interface TableBodyProps {
341+
bodyRef: React.RefObject<HTMLDivElement>;
326342
model: PreviewModel;
327343
data: Array<FileInfo>;
328344
table: Table<FileInfo>;
@@ -332,48 +348,32 @@ interface TableBodyProps {
332348
setSearch: (_: string) => void;
333349
setSelectedPath: (_: string) => void;
334350
setRefreshVersion: React.Dispatch<React.SetStateAction<number>>;
351+
osRef: OverlayScrollbarsComponentRef;
335352
}
336353

337354
function TableBody({
355+
bodyRef,
338356
model,
339-
data,
340357
table,
341358
search,
342359
focusIndex,
343360
setFocusIndex,
344361
setSearch,
345-
setSelectedPath,
346362
setRefreshVersion,
363+
osRef,
347364
}: TableBodyProps) {
348-
const [bodyHeight, setBodyHeight] = useState(0);
349-
350-
const dummyLineRef = useRef<HTMLDivElement>(null);
351-
const parentRef = useRef<HTMLDivElement>(null);
352-
const warningBoxRef = useRef<HTMLDivElement>(null);
353-
const osInstanceRef = useRef<OverlayScrollbars>(null);
365+
const dummyLineRef = useRef<HTMLDivElement>();
366+
const warningBoxRef = useRef<HTMLDivElement>();
354367
const rowRefs = useRef<HTMLDivElement[]>([]);
355-
const domRect = useDimensionsWithExistingRef(parentRef, 30);
356-
const parentHeight = domRect?.height ?? 0;
357-
const conn = jotai.useAtomValue(model.connection);
358-
359-
useEffect(() => {
360-
if (dummyLineRef.current && data && parentRef.current) {
361-
const rowHeight = dummyLineRef.current.offsetHeight;
362-
const fullTBodyHeight = rowHeight * data.length;
363-
const warningBoxHeight = warningBoxRef.current?.offsetHeight ?? 0;
364-
const maxHeightLessHeader = parentHeight - warningBoxHeight;
365-
const tbodyHeight = Math.min(maxHeightLessHeader, fullTBodyHeight);
366-
setBodyHeight(tbodyHeight);
367-
}
368-
}, [data, parentHeight]);
368+
const conn = useAtomValue(model.connection);
369369

370370
useEffect(() => {
371-
if (focusIndex !== null && rowRefs.current[focusIndex] && parentRef.current) {
372-
const viewport = osInstanceRef.current.elements().viewport;
371+
if (focusIndex !== null && rowRefs.current[focusIndex] && bodyRef.current && osRef) {
372+
const viewport = osRef.osInstance().elements().viewport;
373373
const viewportHeight = viewport.offsetHeight;
374374
const rowElement = rowRefs.current[focusIndex];
375375
const rowRect = rowElement.getBoundingClientRect();
376-
const parentRect = parentRef.current.getBoundingClientRect();
376+
const parentRect = bodyRef.current.getBoundingClientRect();
377377
const viewportScrollTop = viewport.scrollTop;
378378

379379
const rowTopRelativeToViewport = rowRect.top - parentRect.top + viewportScrollTop;
@@ -387,7 +387,7 @@ function TableBody({
387387
viewport.scrollTo({ top: rowBottomRelativeToViewport - viewportHeight });
388388
}
389389
}
390-
}, [focusIndex, parentHeight]);
390+
}, [focusIndex]);
391391

392392
const handleFileContextMenu = useCallback(
393393
(e: any, path: string, mimetype: string) => {
@@ -455,7 +455,7 @@ function TableBody({
455455
menu.push({
456456
label: "Delete File",
457457
click: async () => {
458-
await services.FileService.DeleteFile(conn, path).catch((e) => console.log(e));
458+
await FileService.DeleteFile(conn, path).catch((e) => console.log(e));
459459
setRefreshVersion((current) => current + 1);
460460
},
461461
});
@@ -492,12 +492,8 @@ function TableBody({
492492
[setSearch, handleFileContextMenu, setFocusIndex, focusIndex]
493493
);
494494

495-
const handleScrollbarInitialized = (instance) => {
496-
osInstanceRef.current = instance;
497-
};
498-
499495
return (
500-
<div className="dir-table-body" ref={parentRef}>
496+
<div className="dir-table-body" ref={bodyRef}>
501497
{search !== "" && (
502498
<div className="dir-table-body-search-display" ref={warningBoxRef}>
503499
<span>Searching for "{search}"</span>
@@ -507,18 +503,13 @@ function TableBody({
507503
</div>
508504
</div>
509505
)}
510-
<OverlayScrollbarsComponent
511-
options={{ scrollbars: { autoHide: "leave" } }}
512-
events={{ initialized: handleScrollbarInitialized }}
513-
>
514-
<div className="dir-table-body-scroll-box" style={{ height: bodyHeight }}>
515-
<div className="dummy dir-table-body-row" ref={dummyLineRef}>
516-
<div className="dir-table-body-cell">dummy-data</div>
517-
</div>
518-
{table.getTopRows().map(displayRow)}
519-
{table.getCenterRows().map((row, idx) => displayRow(row, idx + table.getTopRows().length))}
506+
<div className="dir-table-body-scroll-box">
507+
<div className="dummy dir-table-body-row" ref={dummyLineRef}>
508+
<div className="dir-table-body-cell">dummy-data</div>
520509
</div>
521-
</OverlayScrollbarsComponent>
510+
{table.getTopRows().map(displayRow)}
511+
{table.getCenterRows().map((row, idx) => displayRow(row, idx + table.getTopRows().length))}
512+
</div>
522513
</div>
523514
);
524515
}
@@ -537,11 +528,11 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
537528
const [focusIndex, setFocusIndex] = useState(0);
538529
const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]);
539530
const [filteredData, setFilteredData] = useState<FileInfo[]>([]);
540-
const fileName = jotai.useAtomValue(model.metaFilePath);
541-
const showHiddenFiles = jotai.useAtomValue(model.showHiddenFiles);
531+
const fileName = useAtomValue(model.metaFilePath);
532+
const showHiddenFiles = useAtomValue(model.showHiddenFiles);
542533
const [selectedPath, setSelectedPath] = useState("");
543-
const [refreshVersion, setRefreshVersion] = jotai.useAtom(model.refreshVersion);
544-
const conn = jotai.useAtomValue(model.connection);
534+
const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion);
535+
const conn = useAtomValue(model.connection);
545536

546537
useEffect(() => {
547538
model.refreshCallback = () => {
@@ -554,8 +545,8 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
554545

555546
useEffect(() => {
556547
const getContent = async () => {
557-
const file = await services.FileService.ReadFile(conn, fileName);
558-
const serializedContent = util.base64ToString(file?.data64);
548+
const file = await FileService.ReadFile(conn, fileName);
549+
const serializedContent = base64ToString(file?.data64);
559550
const content: FileInfo[] = JSON.parse(serializedContent);
560551
setUnfilteredData(content);
561552
};
@@ -574,34 +565,34 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
574565

575566
useEffect(() => {
576567
model.directoryKeyDownHandler = (waveEvent: WaveKeyboardEvent): boolean => {
577-
if (keyutil.checkKeyPressed(waveEvent, "Escape")) {
568+
if (checkKeyPressed(waveEvent, "Escape")) {
578569
setSearchText("");
579570
return;
580571
}
581-
if (keyutil.checkKeyPressed(waveEvent, "ArrowUp")) {
572+
if (checkKeyPressed(waveEvent, "ArrowUp")) {
582573
setFocusIndex((idx) => Math.max(idx - 1, 0));
583574
return true;
584575
}
585-
if (keyutil.checkKeyPressed(waveEvent, "ArrowDown")) {
576+
if (checkKeyPressed(waveEvent, "ArrowDown")) {
586577
setFocusIndex((idx) => Math.min(idx + 1, filteredData.length - 1));
587578
return true;
588579
}
589-
if (keyutil.checkKeyPressed(waveEvent, "Enter")) {
580+
if (checkKeyPressed(waveEvent, "Enter")) {
590581
if (filteredData.length == 0) {
591582
return;
592583
}
593584
model.goHistory(selectedPath);
594585
setSearchText("");
595586
return true;
596587
}
597-
if (keyutil.checkKeyPressed(waveEvent, "Backspace")) {
588+
if (checkKeyPressed(waveEvent, "Backspace")) {
598589
if (searchText.length == 0) {
599590
return true;
600591
}
601592
setSearchText((current) => current.slice(0, -1));
602593
return true;
603594
}
604-
if (keyutil.isCharacterKeyEvent(waveEvent)) {
595+
if (isCharacterKeyEvent(waveEvent)) {
605596
setSearchText((current) => current + waveEvent.key);
606597
return true;
607598
}

frontend/app/view/preview/preview.less

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,5 @@
7979

8080
.full-preview-content {
8181
flex-grow: 1;
82-
overflow-y: hidden;
82+
overflow: hidden;
8383
}

0 commit comments

Comments
 (0)