Better loading/empty visuals

This commit is contained in:
Dustin Brett 2025-05-17 14:01:31 -07:00
parent 8708b3a230
commit e992b6b0ec
13 changed files with 161 additions and 129 deletions

View File

@ -1,5 +1,5 @@
import styled from "styled-components"; import styled from "styled-components";
import StyledLoading from "components/system/Files/FileManager/StyledLoading"; import StyledLoading from "components/system/Apps/StyledLoading";
import StyledDetailsFileManager from "components/system/Files/Views/Details/StyledFileManager"; import StyledDetailsFileManager from "components/system/Files/Views/Details/StyledFileManager";
import StyledIconFileManager from "components/system/Files/Views/Icon/StyledFileManager"; import StyledIconFileManager from "components/system/Files/Views/Icon/StyledFileManager";
@ -21,6 +21,7 @@ const StyledFileExplorer = styled.div`
${StyledLoading} { ${StyledLoading} {
height: ${({ theme }) => height: ${({ theme }) =>
`calc(100% - ${theme.sizes.fileExplorer.navBarHeight} - ${theme.sizes.fileExplorer.statusBarHeight})`}; `calc(100% - ${theme.sizes.fileExplorer.navBarHeight} - ${theme.sizes.fileExplorer.statusBarHeight})`};
position: absolute;
} }
`; `;

View File

@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { getNetworkConfig } from "components/apps/IRC/config"; import { getNetworkConfig } from "components/apps/IRC/config";
import { type ComponentProcessProps } from "components/system/Apps/RenderComponent"; import { type ComponentProcessProps } from "components/system/Apps/RenderComponent";
import StyledLoading from "components/system/Files/FileManager/StyledLoading"; import StyledLoading from "components/system/Apps/StyledLoading";
import { useProcesses } from "contexts/process"; import { useProcesses } from "contexts/process";
import processDirectory from "contexts/process/directory"; import processDirectory from "contexts/process/directory";
import { IFRAME_CONFIG } from "utils/constants"; import { IFRAME_CONFIG } from "utils/constants";

View File

@ -2,7 +2,7 @@ import { basename, dirname, join } from "path";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import StyledPaint from "components/apps/Paint/StyledPaint"; import StyledPaint from "components/apps/Paint/StyledPaint";
import { type ComponentProcessProps } from "components/system/Apps/RenderComponent"; import { type ComponentProcessProps } from "components/system/Apps/RenderComponent";
import StyledLoading from "components/system/Files/FileManager/StyledLoading"; import StyledLoading from "components/system/Apps/StyledLoading";
import useFileDrop from "components/system/Files/FileManager/useFileDrop"; import useFileDrop from "components/system/Files/FileManager/useFileDrop";
import useTitle from "components/system/Window/useTitle"; import useTitle from "components/system/Window/useTitle";
import { useFileSystem } from "contexts/fileSystem"; import { useFileSystem } from "contexts/fileSystem";

View File

@ -1,7 +1,7 @@
import { memo, useMemo, useRef, useState } from "react"; import { memo, useMemo, useRef, useState } from "react";
import styled, { type IStyledComponent } from "styled-components"; import styled, { type IStyledComponent } from "styled-components";
import { type FastOmit } from "styled-components/dist/types"; import { type FastOmit } from "styled-components/dist/types";
import StyledLoading from "components/system/Files/FileManager/StyledLoading"; import StyledLoading from "components/system/Apps/StyledLoading";
import useFileDrop from "components/system/Files/FileManager/useFileDrop"; import useFileDrop from "components/system/Files/FileManager/useFileDrop";
import { useProcesses } from "contexts/process"; import { useProcesses } from "contexts/process";

View File

@ -0,0 +1,29 @@
import styled from "styled-components";
type StyledLoadingProps = {
$hasColumns?: boolean;
};
const StyledLoading = styled.div<StyledLoadingProps>`
cursor: wait;
height: 100%;
width: 100%;
&::before {
color: #fff;
content: "Working on it...";
display: flex;
font-size: 12px;
font-weight: 200;
justify-content: center;
letter-spacing: 0.3px;
mix-blend-mode: difference;
padding-top: ${({ $hasColumns, theme }) =>
$hasColumns
? theme.sizes.window.textTopPadding +
theme.sizes.fileManager.columnHeight
: theme.sizes.window.textTopPadding}px;
}
`;
export default StyledLoading;

View File

@ -12,7 +12,7 @@ const StyledColumns = styled.span`
ol { ol {
display: flex; display: flex;
height: ${({ theme }) => theme.sizes.fileManager.columnHeight}; height: ${({ theme }) => theme.sizes.fileManager.columnHeight}px;
li { li {
color: rgb(222 222 222); color: rgb(222 222 222);
@ -46,7 +46,7 @@ const StyledColumns = styled.span`
.resize { .resize {
border-left: 1px solid rgb(99 99 99); border-left: 1px solid rgb(99 99 99);
cursor: col-resize; cursor: col-resize;
height: 25px; height: ${({ theme }) => theme.sizes.fileManager.columnHeight}px;
padding-left: ${({ theme }) => padding-left: ${({ theme }) =>
theme.sizes.fileManager.columnResizeWidth}px; theme.sizes.fileManager.columnResizeWidth}px;
position: absolute; position: absolute;

View File

@ -1,4 +1,4 @@
import { memo, useRef } from "react"; import { memo, useCallback, useRef } from "react";
import { useTheme } from "styled-components"; import { useTheme } from "styled-components";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { sortFiles } from "components/system/Files/FileManager/functions"; import { sortFiles } from "components/system/Files/FileManager/functions";
@ -35,6 +35,64 @@ const Columns: FC<ColumnsProps> = ({
const lastClientX = useRef(0); const lastClientX = useRef(0);
const { setSortOrder, sortOrders } = useSession(); const { setSortOrder, sortOrders } = useSession();
const [, sortedBy = "name", ascending] = sortOrders[directory] ?? []; const [, sortedBy = "name", ascending] = sortOrders[directory] ?? [];
const onPointerDownCapture = useCallback(
(name: string) => (event: React.PointerEvent<HTMLLIElement>) => {
if (event.button !== 0) return;
draggingRef.current =
(event.target as HTMLElement).className === "resize" ? name : "";
lastClientX.current = event.clientX;
},
[]
);
const onPointerMoveCapture = useCallback(
(event: React.PointerEvent<HTMLLIElement>) => {
if (draggingRef.current) {
const dragName = draggingRef.current as ColumnName;
setColumns((currentColumns) => {
if (!currentColumns?.[dragName]) return currentColumns;
const newColumns = { ...currentColumns };
const newSize =
newColumns[dragName].width + event.clientX - lastClientX.current;
if (
newSize < sizes.fileManager.columnMinWidth ||
Math.abs(lastClientX.current - event.clientX) > MAX_STEPS_PER_RESIZE
) {
return newColumns;
}
newColumns[dragName].width = newSize;
lastClientX.current = event.clientX;
return newColumns;
});
}
},
[setColumns, sizes.fileManager.columnMinWidth]
);
const onPointerUpCapture = useCallback(
(name: string) => (event: React.PointerEvent<HTMLLIElement>) => {
if (event.button !== 0) return;
if (draggingRef.current) {
draggingRef.current = "";
lastClientX.current = 0;
} else {
const sortBy = name as SortBy;
setSortOrder(
directory,
Object.keys(sortFiles(directory, files, sortBy, !ascending)),
sortBy,
!ascending
);
}
},
[ascending, directory, files, setSortOrder]
);
return ( return (
<StyledColumns> <StyledColumns>
@ -42,60 +100,9 @@ const Columns: FC<ColumnsProps> = ({
{DEFAULT_COLUMN_ORDER.map((name) => ( {DEFAULT_COLUMN_ORDER.map((name) => (
<li <li
key={columns[name].name} key={columns[name].name}
onPointerDownCapture={(event) => { onPointerDownCapture={onPointerDownCapture(name)}
if (event.button !== 0) return; onPointerMoveCapture={onPointerMoveCapture}
onPointerUpCapture={onPointerUpCapture(name)}
draggingRef.current =
(event.target as HTMLElement).className === "resize"
? name
: "";
lastClientX.current = event.clientX;
}}
onPointerMoveCapture={(event) => {
if (draggingRef.current) {
const dragName = draggingRef.current as ColumnName;
setColumns((currentColumns) => {
if (!currentColumns?.[dragName]) return currentColumns;
const newColumns = { ...currentColumns };
const newSize =
newColumns[dragName].width +
event.clientX -
lastClientX.current;
if (
newSize < sizes.fileManager.columnMinWidth ||
Math.abs(lastClientX.current - event.clientX) >
MAX_STEPS_PER_RESIZE
) {
return newColumns;
}
newColumns[dragName].width = newSize;
lastClientX.current = event.clientX;
return newColumns;
});
}
}}
onPointerUpCapture={(event) => {
if (event.button !== 0) return;
if (draggingRef.current) {
draggingRef.current = "";
lastClientX.current = 0;
} else {
const sortBy = name as SortBy;
setSortOrder(
directory,
Object.keys(sortFiles(directory, files, sortBy, !ascending)),
sortBy,
!ascending
);
}
}}
style={{ width: `${columns[name].width}px` }} style={{ width: `${columns[name].width}px` }}
> >
{sortedBy === name && <Down flip={ascending} />} {sortedBy === name && <Down flip={ascending} />}

View File

@ -1,6 +1,10 @@
import styled from "styled-components"; import styled from "styled-components";
const StyledEmpty = styled.div` type StyledEmptyProps = {
$hasColumns?: boolean;
};
const StyledEmpty = styled.div<StyledEmptyProps>`
position: absolute; position: absolute;
width: 100%; width: 100%;
@ -13,7 +17,11 @@ const StyledEmpty = styled.div`
justify-content: center; justify-content: center;
letter-spacing: 0.3px; letter-spacing: 0.3px;
mix-blend-mode: difference; mix-blend-mode: difference;
padding-top: 14px; padding-top: ${({ $hasColumns, theme }) =>
$hasColumns
? theme.sizes.window.textTopPadding +
theme.sizes.fileManager.columnHeight
: theme.sizes.window.textTopPadding}px;
} }
`; `;

View File

@ -1,19 +0,0 @@
import styled from "styled-components";
const StyledLoading = styled.div`
cursor: wait;
height: 100%;
width: 100%;
&::before {
color: #fff;
content: "Working on it...";
display: flex;
font-size: 12px;
justify-content: center;
mix-blend-mode: difference;
padding-top: 18px;
}
`;
export default StyledLoading;

View File

@ -1,7 +1,7 @@
import { basename, join } from "path"; import { basename, join } from "path";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import StyledLoading from "components/system/Files/FileManager/StyledLoading"; import StyledLoading from "components/system/Apps/StyledLoading";
import StatusBar from "components/system/Files/FileManager/StatusBar"; import StatusBar from "components/system/Files/FileManager/StatusBar";
import { import {
DEFAULT_COLUMNS, DEFAULT_COLUMNS,
@ -85,11 +85,12 @@ const FileManager: FC<FileManagerProps> = ({
const [renaming, setRenaming] = useState(""); const [renaming, setRenaming] = useState("");
const [mounted, setMounted] = useState<boolean>(false); const [mounted, setMounted] = useState<boolean>(false);
const fileManagerRef = useRef<HTMLOListElement | null>(null); const fileManagerRef = useRef<HTMLOListElement | null>(null);
const isFileExplorerIconView = useMemo(
() => !isStartMenu && !isDesktop && !isDetailsView,
[isDesktop, isDetailsView, isStartMenu]
);
const { focusedEntries, focusableEntry, ...focusFunctions } = const { focusedEntries, focusableEntry, ...focusFunctions } =
useFocusableEntries( useFocusableEntries(fileManagerRef, isFileExplorerIconView);
fileManagerRef,
!isStartMenu && !isDesktop && !isDetailsView
);
const { fileActions, files, folderActions, isLoading, updateFiles } = const { fileActions, files, folderActions, isLoading, updateFiles } =
useFolder(url, setRenaming, focusFunctions, { useFolder(url, setRenaming, focusFunctions, {
hideFolders, hideFolders,
@ -121,7 +122,11 @@ const FileManager: FC<FileManagerProps> = ({
isDesktop, isDesktop,
isStartMenu isStartMenu
); );
const loading = (!hideLoading && isLoading) || url !== currentUrl; const loading = useMemo(() => {
if (hideLoading) return false;
return isLoading || url !== currentUrl;
}, [currentUrl, hideLoading, isLoading, url]);
const setView = useCallback( const setView = useCallback(
(newView: FileManagerViewNames) => { (newView: FileManagerViewNames) => {
setViews((currentViews) => ({ ...currentViews, [url]: newView })); setViews((currentViews) => ({ ...currentViews, [url]: newView }));
@ -151,12 +156,10 @@ const FileManager: FC<FileManagerProps> = ({
[keyShortcuts, renaming] [keyShortcuts, renaming]
); );
const fileKeys = useMemo(() => Object.keys(files), [files]); const fileKeys = useMemo(() => Object.keys(files), [files]);
const isEmptyFolder = const isEmptyFolder = useMemo(
!isDesktop && () => !isDesktop && !isStartMenu && !loading && fileKeys.length === 0,
!isStartMenu && [fileKeys.length, isDesktop, isStartMenu, loading]
!loading && );
view !== "list" &&
fileKeys.length === 0;
useEffect(() => { useEffect(() => {
if ( if (
@ -231,34 +234,33 @@ const FileManager: FC<FileManagerProps> = ({
return ( return (
<> <>
{loading ? ( {loading && <StyledLoading $hasColumns={isDetailsView} />}
<StyledLoading /> {!loading && isEmptyFolder && <StyledEmpty $hasColumns={isDetailsView} />}
) : ( <StyledFileManager
<> ref={fileManagerRef}
{isEmptyFolder && <StyledEmpty />} $isEmptyFolder={isEmptyFolder}
<StyledFileManager $scrollable={!hideScrolling}
ref={fileManagerRef} onKeyDownCapture={loading ? undefined : onKeyDown}
$isEmptyFolder={isEmptyFolder} {...(loading || readOnly
$scrollable={!hideScrolling} ? { onContextMenu: haltEvent }
onKeyDownCapture={onKeyDown} : {
{...(readOnly $selecting: isSelecting,
? { onContextMenu: haltEvent } ...fileDrop,
: { ...folderContextMenu,
$selecting: isSelecting, ...selectionEvents,
...fileDrop, })}
...folderContextMenu, {...FOCUSABLE_ELEMENT}
...selectionEvents, >
})} {isDetailsView && columns && (
{...FOCUSABLE_ELEMENT} <Columns
> columns={columns}
{isDetailsView && columns && ( directory={url}
<Columns files={files}
columns={columns} setColumns={setColumns}
directory={url} />
files={files} )}
setColumns={setColumns} {!loading && (
/> <>
)}
{isSelecting && <StyledSelection style={selectionStyling} />} {isSelecting && <StyledSelection style={selectionStyling} />}
{fileKeys.map((file) => ( {fileKeys.map((file) => (
<StyledFileEntry <StyledFileEntry
@ -294,9 +296,9 @@ const FileManager: FC<FileManagerProps> = ({
/> />
</StyledFileEntry> </StyledFileEntry>
))} ))}
</StyledFileManager> </>
</> )}
)} </StyledFileManager>
{showStatusBar && ( {showStatusBar && (
<StatusBar <StatusBar
count={loading ? 0 : fileKeys.length} count={loading ? 0 : fileKeys.length}

View File

@ -168,8 +168,10 @@ const useFolder = (
}, },
[directory, readFile] [directory, readFile]
); );
const isSimpleSort = const isSimpleSort = useMemo(
skipSorting || !sortBy || sortBy === "name" || sortBy === "type"; () => skipSorting || !sortBy || sortBy === "name" || sortBy === "type",
[skipSorting, sortBy]
);
const updateFiles = useCallback( const updateFiles = useCallback(
async (newFile?: string, oldFile?: string) => { async (newFile?: string, oldFile?: string) => {
if (oldFile) { if (oldFile) {
@ -739,6 +741,7 @@ const useFolder = (
useEffect(() => { useEffect(() => {
if (directory !== currentDirectory) { if (directory !== currentDirectory) {
setIsLoading(true);
setCurrentDirectory(directory); setCurrentDirectory(directory);
setFiles(NO_FILES); setFiles(NO_FILES);
} }

View File

@ -1,6 +1,6 @@
import { m as motion } from "motion/react"; import { m as motion } from "motion/react";
import styled from "styled-components"; import styled from "styled-components";
import StyledLoading from "components/system/Files/FileManager/StyledLoading"; import StyledLoading from "components/system/Apps/StyledLoading";
type StyledWindowProps = { type StyledWindowProps = {
$backgroundBlur?: string; $backgroundBlur?: string;

View File

@ -26,7 +26,7 @@ const sizes = {
}, },
fileManager: { fileManager: {
columnGap: "1px", columnGap: "1px",
columnHeight: "25px", columnHeight: 25,
columnMinWidth: 70, columnMinWidth: 70,
columnResizeWidth: 7, columnResizeWidth: 7,
detailsEndPadding: 16, detailsEndPadding: 16,
@ -87,6 +87,7 @@ const sizes = {
window: { window: {
cascadeOffset: 26, cascadeOffset: 26,
outline: "1px", outline: "1px",
textTopPadding: 14,
}, },
}; };