From 1737c686c2e81f5c515e127177d894acde9745f9 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 2 Oct 2024 17:21:34 -0700 Subject: [PATCH] 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 --- .../app/view/preview/directorypreview.less | 21 ++- .../app/view/preview/directorypreview.tsx | 137 ++++++++---------- frontend/app/view/preview/preview.less | 2 +- 3 files changed, 80 insertions(+), 80 deletions(-) diff --git a/frontend/app/view/preview/directorypreview.less b/frontend/app/view/preview/directorypreview.less index 9cb19651b..3e04918dc 100644 --- a/frontend/app/view/preview/directorypreview.less +++ b/frontend/app/view/preview/directorypreview.less @@ -7,18 +7,28 @@ display: flex; flex-direction: column; height: 100%; + --min-row-width: 35rem; .dir-table { height: 100%; - min-width: 600px; + width: 100%; --col-size-size: 0.2rem; - border-radius: 3px; display: flex; flex-direction: column; - font: var(--base-font); + + &:not([data-scroll-height="0"]) .dir-table-head { + background: rgba(10, 10, 10, 0.5); + backdrop-filter: blur(4px); + } .dir-table-head { + position: sticky; + top: 0; + z-index: 10; + width: fit-content; + border-bottom: 1px solid var(--border-color); + .dir-table-head-row { display: flex; - border-bottom: 1px solid var(--border-color); + min-width: var(--min-row-width); padding: 4px 6px; font-size: 0.75rem; @@ -68,10 +78,8 @@ } .dir-table-body { - flex: 1 1 auto; display: flex; flex-direction: column; - overflow: hidden; .dir-table-body-search-display { display: flex; border-radius: 3px; @@ -94,6 +102,7 @@ align-items: center; border-radius: 5px; padding: 0 6px; + min-width: var(--min-row-width); &.focused { background-color: rgb(from var(--accent-color) r g b / 0.5); diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index 73c8a098b..89552b31d 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -3,10 +3,10 @@ import { ContextMenuModel } from "@/app/store/contextmenu"; import { atoms, createBlock, getApi } from "@/app/store/global"; +import { FileService } from "@/app/store/services"; import type { PreviewModel } from "@/app/view/preview/preview"; -import * as services from "@/store/services"; -import * as keyutil from "@/util/keyutil"; -import * as util from "@/util/util"; +import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil"; +import { base64ToString, isBlank } from "@/util/util"; import { Column, Row, @@ -19,14 +19,11 @@ import { } from "@tanstack/react-table"; import clsx from "clsx"; import dayjs from "dayjs"; -import * as jotai from "jotai"; -import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; +import { useAtom, useAtomValue } from "jotai"; +import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { quote as shellQuote } from "shell-quote"; - -import { OverlayScrollbars } from "overlayscrollbars"; - -import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions"; +import { debounce } from "throttle-debounce"; import "./directorypreview.less"; interface DirectoryTableProps { @@ -95,7 +92,7 @@ function getLastModifiedTime(unixMillis: number, column: Column { while (mimeType.length > 0) { - let icon = fullConfig.mimetypes?.[mimeType]?.icon ?? null; + const icon = fullConfig.mimetypes?.[mimeType]?.icon ?? null; if (isIconValid(icon)) { return `fa fa-solid fa-${icon} fa-fw`; } @@ -149,10 +146,7 @@ function DirectoryTable({ [fullConfig.mimetypes] ); const getIconColor = useCallback( - (mimeType: string): string => { - let iconColor = fullConfig.mimetypes?.[mimeType]?.color ?? "inherit"; - return iconColor; - }, + (mimeType: string): string => fullConfig.mimetypes?.[mimeType]?.color ?? "inherit", [fullConfig.mimetypes] ); const columns = useMemo( @@ -261,8 +255,25 @@ function DirectoryTable({ return colSizes; }, [table.getState().columnSizingInfo]); + const osRef = useRef(); + const bodyRef = useRef(); + const [scrollHeight, setScrollHeight] = useState(0); + + const onScroll = useCallback( + debounce(2, () => { + setScrollHeight(osRef.current.osInstance().elements().viewport.scrollTop); + }), + [] + ); return ( -
+
{table.getHeaderGroups().map((headerGroup) => (
@@ -295,6 +306,7 @@ function DirectoryTable({
{table.getState().columnSizingInfo.isResizingColumn ? ( ) : ( )} -
+
); } interface TableBodyProps { + bodyRef: React.RefObject; model: PreviewModel; data: Array; table: Table; @@ -332,48 +348,32 @@ interface TableBodyProps { setSearch: (_: string) => void; setSelectedPath: (_: string) => void; setRefreshVersion: React.Dispatch>; + osRef: OverlayScrollbarsComponentRef; } function TableBody({ + bodyRef, model, - data, table, search, focusIndex, setFocusIndex, setSearch, - setSelectedPath, setRefreshVersion, + osRef, }: TableBodyProps) { - const [bodyHeight, setBodyHeight] = useState(0); - - const dummyLineRef = useRef(null); - const parentRef = useRef(null); - const warningBoxRef = useRef(null); - const osInstanceRef = useRef(null); + const dummyLineRef = useRef(); + const warningBoxRef = useRef(); const rowRefs = useRef([]); - const domRect = useDimensionsWithExistingRef(parentRef, 30); - const parentHeight = domRect?.height ?? 0; - const conn = jotai.useAtomValue(model.connection); + const conn = useAtomValue(model.connection); useEffect(() => { - if (dummyLineRef.current && data && parentRef.current) { - const rowHeight = dummyLineRef.current.offsetHeight; - const fullTBodyHeight = rowHeight * data.length; - const warningBoxHeight = warningBoxRef.current?.offsetHeight ?? 0; - const maxHeightLessHeader = parentHeight - warningBoxHeight; - const tbodyHeight = Math.min(maxHeightLessHeader, fullTBodyHeight); - setBodyHeight(tbodyHeight); - } - }, [data, parentHeight]); - - useEffect(() => { - if (focusIndex !== null && rowRefs.current[focusIndex] && parentRef.current) { - const viewport = osInstanceRef.current.elements().viewport; + if (focusIndex !== null && rowRefs.current[focusIndex] && bodyRef.current && osRef) { + const viewport = osRef.osInstance().elements().viewport; const viewportHeight = viewport.offsetHeight; const rowElement = rowRefs.current[focusIndex]; const rowRect = rowElement.getBoundingClientRect(); - const parentRect = parentRef.current.getBoundingClientRect(); + const parentRect = bodyRef.current.getBoundingClientRect(); const viewportScrollTop = viewport.scrollTop; const rowTopRelativeToViewport = rowRect.top - parentRect.top + viewportScrollTop; @@ -387,7 +387,7 @@ function TableBody({ viewport.scrollTo({ top: rowBottomRelativeToViewport - viewportHeight }); } } - }, [focusIndex, parentHeight]); + }, [focusIndex]); const handleFileContextMenu = useCallback( (e: any, path: string, mimetype: string) => { @@ -455,7 +455,7 @@ function TableBody({ menu.push({ label: "Delete File", click: async () => { - await services.FileService.DeleteFile(conn, path).catch((e) => console.log(e)); + await FileService.DeleteFile(conn, path).catch((e) => console.log(e)); setRefreshVersion((current) => current + 1); }, }); @@ -492,12 +492,8 @@ function TableBody({ [setSearch, handleFileContextMenu, setFocusIndex, focusIndex] ); - const handleScrollbarInitialized = (instance) => { - osInstanceRef.current = instance; - }; - return ( -
+
{search !== "" && (
Searching for "{search}" @@ -507,18 +503,13 @@ function TableBody({
)} - -
-
-
dummy-data
-
- {table.getTopRows().map(displayRow)} - {table.getCenterRows().map((row, idx) => displayRow(row, idx + table.getTopRows().length))} +
+
+
dummy-data
- + {table.getTopRows().map(displayRow)} + {table.getCenterRows().map((row, idx) => displayRow(row, idx + table.getTopRows().length))} +
); } @@ -537,11 +528,11 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { const [focusIndex, setFocusIndex] = useState(0); const [unfilteredData, setUnfilteredData] = useState([]); const [filteredData, setFilteredData] = useState([]); - const fileName = jotai.useAtomValue(model.metaFilePath); - const showHiddenFiles = jotai.useAtomValue(model.showHiddenFiles); + const fileName = useAtomValue(model.metaFilePath); + const showHiddenFiles = useAtomValue(model.showHiddenFiles); const [selectedPath, setSelectedPath] = useState(""); - const [refreshVersion, setRefreshVersion] = jotai.useAtom(model.refreshVersion); - const conn = jotai.useAtomValue(model.connection); + const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion); + const conn = useAtomValue(model.connection); useEffect(() => { model.refreshCallback = () => { @@ -554,8 +545,8 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { useEffect(() => { const getContent = async () => { - const file = await services.FileService.ReadFile(conn, fileName); - const serializedContent = util.base64ToString(file?.data64); + const file = await FileService.ReadFile(conn, fileName); + const serializedContent = base64ToString(file?.data64); const content: FileInfo[] = JSON.parse(serializedContent); setUnfilteredData(content); }; @@ -574,19 +565,19 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { useEffect(() => { model.directoryKeyDownHandler = (waveEvent: WaveKeyboardEvent): boolean => { - if (keyutil.checkKeyPressed(waveEvent, "Escape")) { + if (checkKeyPressed(waveEvent, "Escape")) { setSearchText(""); return; } - if (keyutil.checkKeyPressed(waveEvent, "ArrowUp")) { + if (checkKeyPressed(waveEvent, "ArrowUp")) { setFocusIndex((idx) => Math.max(idx - 1, 0)); return true; } - if (keyutil.checkKeyPressed(waveEvent, "ArrowDown")) { + if (checkKeyPressed(waveEvent, "ArrowDown")) { setFocusIndex((idx) => Math.min(idx + 1, filteredData.length - 1)); return true; } - if (keyutil.checkKeyPressed(waveEvent, "Enter")) { + if (checkKeyPressed(waveEvent, "Enter")) { if (filteredData.length == 0) { return; } @@ -594,14 +585,14 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { setSearchText(""); return true; } - if (keyutil.checkKeyPressed(waveEvent, "Backspace")) { + if (checkKeyPressed(waveEvent, "Backspace")) { if (searchText.length == 0) { return true; } setSearchText((current) => current.slice(0, -1)); return true; } - if (keyutil.isCharacterKeyEvent(waveEvent)) { + if (isCharacterKeyEvent(waveEvent)) { setSearchText((current) => current + waveEvent.key); return true; } diff --git a/frontend/app/view/preview/preview.less b/frontend/app/view/preview/preview.less index 58f924827..0d29f58e5 100644 --- a/frontend/app/view/preview/preview.less +++ b/frontend/app/view/preview/preview.less @@ -79,5 +79,5 @@ .full-preview-content { flex-grow: 1; - overflow-y: hidden; + overflow: hidden; }