Skip to content

Commit c8d44fc

Browse files
authored
quick old mac aesthetic borrowed from hypertalking (#4)
* file manager - allow file entry icon override * app - alt file explorer - old mac aesthetic borrowed from hypertalking * fs - use OldMacFileExplorer for portfolio etc dir * lint - fix
1 parent b2a51ba commit c8d44fc

File tree

18 files changed

+816
-1
lines changed

18 files changed

+816
-1
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import Navigation from "components/apps/FileExplorer/Navigation";
2+
import type { ComponentProcessProps } from "components/system/Apps/RenderComponent";
3+
import { getIconFromIni } from "components/system/Files/FileEntry/functions";
4+
import { useFileSystem } from "contexts/fileSystem";
5+
import { useProcesses } from "contexts/process";
6+
import { basename } from "path";
7+
import { useCallback, useEffect, useRef, useState } from "react";
8+
import {
9+
COMPRESSED_FOLDER_ICON,
10+
MOUNTED_FOLDER_ICON,
11+
PREVENT_SCROLL,
12+
ROOT_NAME,
13+
} from "utils/constants";
14+
import { haltEvent } from "utils/functions";
15+
import FileManager from "./FileManager";
16+
import StyledFileExplorer from "./StyledFileExplorer";
17+
18+
const FileExplorer: FC<ComponentProcessProps> = ({ id }) => {
19+
const {
20+
icon: setProcessIcon,
21+
title,
22+
processes: { [id]: process },
23+
url: setProcessUrl,
24+
} = useProcesses();
25+
const { componentWindow, closing, icon = "", url = "" } = process || {};
26+
const { fs, rootFs } = useFileSystem();
27+
const [currentUrl, setCurrentUrl] = useState(url);
28+
const inputRef = useRef<HTMLInputElement | null>(null);
29+
const directoryName = basename(url);
30+
const isMounted = Boolean(rootFs?.mntMap[url] && directoryName);
31+
const onKeyDown = useCallback((event: KeyboardEvent): void => {
32+
if (event.altKey && event.key.toUpperCase() === "D") {
33+
haltEvent(event);
34+
inputRef?.current?.focus(PREVENT_SCROLL);
35+
}
36+
}, []);
37+
38+
useEffect(() => {
39+
if (url) {
40+
title(id, directoryName || ROOT_NAME);
41+
42+
if (
43+
!icon ||
44+
url !== currentUrl ||
45+
(isMounted && icon !== MOUNTED_FOLDER_ICON)
46+
) {
47+
if (isMounted) {
48+
setProcessIcon(
49+
id,
50+
rootFs?.mntMap[url].getName() === "FileSystemAccess"
51+
? MOUNTED_FOLDER_ICON
52+
: COMPRESSED_FOLDER_ICON
53+
);
54+
} else if (fs) {
55+
setProcessIcon(
56+
id,
57+
`/System/Icons/${directoryName ? "folder" : "pc"}.webp`
58+
);
59+
getIconFromIni(fs, url).then((iconFile) => {
60+
if (iconFile) setProcessIcon(id, iconFile);
61+
});
62+
}
63+
64+
setCurrentUrl(url);
65+
}
66+
}
67+
}, [
68+
currentUrl,
69+
directoryName,
70+
fs,
71+
icon,
72+
id,
73+
isMounted,
74+
rootFs?.mntMap,
75+
setProcessIcon,
76+
setProcessUrl,
77+
title,
78+
url,
79+
]);
80+
81+
useEffect(() => {
82+
if (componentWindow && !closing && !url) {
83+
setProcessUrl(id, "/");
84+
setProcessIcon(id, "/System/Icons/pc.webp");
85+
}
86+
}, [closing, id, componentWindow, setProcessIcon, setProcessUrl, url]);
87+
88+
useEffect(() => {
89+
componentWindow?.addEventListener("keydown", onKeyDown);
90+
91+
return () => componentWindow?.removeEventListener("keydown", onKeyDown);
92+
}, [componentWindow, onKeyDown]);
93+
94+
const showStatusBar = false;
95+
const showNavigation = false;
96+
97+
return url ? (
98+
<StyledFileExplorer
99+
showNavigation={showNavigation}
100+
showStatusBar={showStatusBar}
101+
>
102+
{showNavigation && (
103+
<Navigation ref={inputRef} hideSearch={isMounted} id={id} />
104+
)}
105+
<FileManager
106+
id={id}
107+
showStatusBar={showStatusBar}
108+
url={url}
109+
view="icon"
110+
/>
111+
</StyledFileExplorer>
112+
) : // eslint-disable-next-line unicorn/no-null
113+
null;
114+
};
115+
116+
export default FileExplorer;
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import FileEntry from "components/system/Files/FileEntry";
2+
import StyledSelection from "components/system/Files/FileManager/Selection/StyledSelection";
3+
import useSelection from "components/system/Files/FileManager/Selection/useSelection";
4+
import useDraggableEntries from "components/system/Files/FileManager/useDraggableEntries";
5+
import useFileDrop from "components/system/Files/FileManager/useFileDrop";
6+
import useFileKeyboardShortcuts from "components/system/Files/FileManager/useFileKeyboardShortcuts";
7+
import useFocusableEntries from "components/system/Files/FileManager/useFocusableEntries";
8+
import useFolder from "components/system/Files/FileManager/useFolder";
9+
import useFolderContextMenu from "components/system/Files/FileManager/useFolderContextMenu";
10+
import type { FileManagerViewNames } from "components/system/Files/Views";
11+
import StyledIconFileManager from "components/system/Files/Views/Icon/StyledFileManager";
12+
import { useFileSystem } from "contexts/fileSystem";
13+
import { requestPermission } from "contexts/fileSystem/functions";
14+
import dynamic from "next/dynamic";
15+
import { basename, join } from "path";
16+
import { useEffect, useMemo, useRef, useState } from "react";
17+
import {
18+
FOCUSABLE_ELEMENT,
19+
MOUNTABLE_EXTENSIONS,
20+
PREVENT_SCROLL,
21+
SHORTCUT_EXTENSION,
22+
} from "utils/constants";
23+
import { getExtension } from "utils/functions";
24+
import StyledIconFileEntry from "./StyledFileEntry";
25+
26+
const StatusBar = dynamic(
27+
() => import("components/system/Files/FileManager/StatusBar")
28+
);
29+
30+
const StyledLoading = dynamic(
31+
() => import("components/system/Files/FileManager/StyledLoading")
32+
);
33+
34+
const macIcon =
35+
"https://www.hypertalking.com/wp-content/themes/hypertalking/images/icon-folder.png";
36+
37+
type FileManagerProps = {
38+
allowMovingDraggableEntries?: boolean;
39+
hideFolders?: boolean;
40+
hideLoading?: boolean;
41+
hideScrolling?: boolean;
42+
hideShortcutIcons?: boolean;
43+
id?: string;
44+
isDesktop?: boolean;
45+
loadIconsImmediately?: boolean;
46+
preloadShortcuts?: boolean;
47+
readOnly?: boolean;
48+
showStatusBar?: boolean;
49+
skipFsWatcher?: boolean;
50+
skipSorting?: boolean;
51+
url: string;
52+
useNewFolderIcon?: boolean;
53+
view: FileManagerViewNames;
54+
};
55+
56+
const FileManager: FC<FileManagerProps> = ({
57+
allowMovingDraggableEntries,
58+
hideFolders,
59+
hideLoading,
60+
hideScrolling,
61+
hideShortcutIcons,
62+
id,
63+
isDesktop,
64+
loadIconsImmediately,
65+
preloadShortcuts,
66+
readOnly,
67+
showStatusBar,
68+
skipFsWatcher,
69+
skipSorting,
70+
url,
71+
useNewFolderIcon,
72+
view,
73+
}) => {
74+
const [currentUrl, setCurrentUrl] = useState(url);
75+
const [renaming, setRenaming] = useState("");
76+
const [mounted, setMounted] = useState<boolean>(false);
77+
const fileManagerRef = useRef<HTMLOListElement | null>(null);
78+
const { focusedEntries, focusableEntry, ...focusFunctions } =
79+
useFocusableEntries(fileManagerRef);
80+
const { fileActions, files, folderActions, isLoading, updateFiles } =
81+
useFolder(url, setRenaming, focusFunctions, {
82+
hideFolders,
83+
hideLoading,
84+
preloadShortcuts,
85+
skipFsWatcher,
86+
skipSorting,
87+
});
88+
const { mountFs, rootFs, stat } = useFileSystem();
89+
90+
const StyledFileEntry = StyledIconFileEntry;
91+
const StyledFileManager = StyledIconFileManager;
92+
93+
const { isSelecting, selectionRect, selectionStyling, selectionEvents } =
94+
useSelection(fileManagerRef);
95+
const draggableEntry = useDraggableEntries(
96+
focusedEntries,
97+
focusFunctions,
98+
fileManagerRef,
99+
isSelecting,
100+
allowMovingDraggableEntries
101+
);
102+
const fileDrop = useFileDrop({
103+
callback: folderActions.newPath,
104+
directory: url,
105+
updatePositions: allowMovingDraggableEntries,
106+
});
107+
const folderContextMenu = useFolderContextMenu(url, folderActions, isDesktop);
108+
const loading = (!hideLoading && isLoading) || url !== currentUrl;
109+
const keyShortcuts = useFileKeyboardShortcuts(
110+
files,
111+
url,
112+
focusedEntries,
113+
setRenaming,
114+
focusFunctions,
115+
folderActions,
116+
updateFiles,
117+
fileManagerRef,
118+
id,
119+
view
120+
);
121+
const [permission, setPermission] = useState<PermissionState>("prompt");
122+
const requestingPermissions = useRef(false);
123+
const onKeyDown = useMemo(
124+
() => (renaming === "" ? keyShortcuts() : undefined),
125+
[keyShortcuts, renaming]
126+
);
127+
128+
useEffect(() => {
129+
if (
130+
!requestingPermissions.current &&
131+
permission !== "granted" &&
132+
rootFs?.mntMap[currentUrl]?.getName() === "FileSystemAccess"
133+
) {
134+
requestingPermissions.current = true;
135+
requestPermission(currentUrl)
136+
.then((permissions) => {
137+
const isGranted = permissions === "granted";
138+
139+
if (!permissions || isGranted) {
140+
setPermission("granted");
141+
142+
if (isGranted) updateFiles();
143+
}
144+
})
145+
.catch((error: Error) => {
146+
if (error.message === "Permission already granted") {
147+
setPermission("granted");
148+
}
149+
})
150+
.finally(() => {
151+
requestingPermissions.current = false;
152+
});
153+
}
154+
}, [currentUrl, permission, rootFs?.mntMap, updateFiles]);
155+
156+
useEffect(() => {
157+
if (!mounted && MOUNTABLE_EXTENSIONS.has(getExtension(url))) {
158+
const mountUrl = async (): Promise<void> => {
159+
if (!(await stat(url)).isDirectory()) {
160+
setMounted((currentlyMounted) => {
161+
if (!currentlyMounted) {
162+
mountFs(url)
163+
.then(() => setTimeout(updateFiles, 100))
164+
.catch(() => {
165+
// Ignore race-condtion failures
166+
});
167+
}
168+
return true;
169+
});
170+
}
171+
};
172+
173+
mountUrl();
174+
}
175+
}, [mountFs, mounted, stat, updateFiles, url]);
176+
177+
useEffect(() => {
178+
if (url !== currentUrl) {
179+
folderActions.resetFiles();
180+
setCurrentUrl(url);
181+
setPermission("denied");
182+
}
183+
}, [currentUrl, folderActions, url]);
184+
185+
useEffect(() => {
186+
if (!loading) fileManagerRef.current?.focus(PREVENT_SCROLL);
187+
}, [loading]);
188+
189+
return (
190+
<>
191+
{loading ? (
192+
<StyledLoading />
193+
) : (
194+
<StyledFileManager
195+
ref={fileManagerRef}
196+
$scrollable={!hideScrolling}
197+
onKeyDown={onKeyDown}
198+
{...(!readOnly && {
199+
$selecting: isSelecting,
200+
...fileDrop,
201+
...folderContextMenu,
202+
...selectionEvents,
203+
})}
204+
{...FOCUSABLE_ELEMENT}
205+
>
206+
{isSelecting && <StyledSelection style={selectionStyling} />}
207+
{Object.keys(files).map((file) => (
208+
<StyledFileEntry
209+
key={file}
210+
$selecting={isSelecting}
211+
$visible={!isLoading}
212+
{...(!readOnly && draggableEntry(url, file, renaming === file))}
213+
{...(renaming === "" && { onKeyDown: keyShortcuts(file) })}
214+
{...focusableEntry(file)}
215+
>
216+
<FileEntry
217+
fileActions={fileActions}
218+
fileManagerId={id}
219+
fileManagerRef={fileManagerRef}
220+
focusFunctions={focusFunctions}
221+
focusedEntries={focusedEntries}
222+
hideShortcutIcon={hideShortcutIcons}
223+
iconOverride={macIcon}
224+
isLoadingFileManager={isLoading}
225+
loadIconImmediately={loadIconsImmediately}
226+
name={basename(file, SHORTCUT_EXTENSION)}
227+
path={join(url, file)}
228+
readOnly={readOnly}
229+
renaming={renaming === file}
230+
selectionRect={selectionRect}
231+
setRenaming={setRenaming}
232+
stats={files[file]}
233+
useNewFolderIcon={useNewFolderIcon}
234+
view={view}
235+
/>
236+
</StyledFileEntry>
237+
))}
238+
</StyledFileManager>
239+
)}
240+
{showStatusBar && (
241+
<StatusBar
242+
count={loading ? 0 : Object.keys(files).length}
243+
directory={url}
244+
fileDrop={fileDrop}
245+
selected={focusedEntries}
246+
/>
247+
)}
248+
</>
249+
);
250+
};
251+
252+
export default FileManager;

0 commit comments

Comments
 (0)