diff --git a/src/common/exists.ts b/src/common/exists.ts new file mode 100644 index 000000000..cecdd9f4f --- /dev/null +++ b/src/common/exists.ts @@ -0,0 +1,7 @@ +// Based on https://github.com/wilsonzlin/edgesearch/blob/d03816dd4b18d3d2eb6d08cb1ae14f96f046141d/demo/wiki/client/src/util/util.ts + +// Ensures value is not null or undefined. +// != does no type validation so we don't need to explcitly check for undefined. +export function exists(value: T | null | undefined): value is T { + return value != null; +} diff --git a/src/renderer/containers/ReplayBrowser/ReplayBrowser.tsx b/src/renderer/containers/ReplayBrowser/ReplayBrowser.tsx index 9d894a840..f7aab8779 100644 --- a/src/renderer/containers/ReplayBrowser/ReplayBrowser.tsx +++ b/src/renderer/containers/ReplayBrowser/ReplayBrowser.tsx @@ -9,6 +9,7 @@ import Typography from "@material-ui/core/Typography"; import FolderIcon from "@material-ui/icons/Folder"; import SearchIcon from "@material-ui/icons/Search"; import { colors } from "common/colors"; +import { exists } from "common/exists"; import { shell } from "electron"; import React from "react"; import { useToasts } from "react-toast-notifications"; @@ -22,6 +23,7 @@ import { useDolphin } from "@/lib/hooks/useDolphin"; import { useReplayBrowserList, useReplayBrowserNavigation } from "@/lib/hooks/useReplayBrowserList"; import { useReplayFilter } from "@/lib/hooks/useReplayFilter"; import { useReplays, useReplaySelection } from "@/lib/hooks/useReplays"; +import { humanReadableBytes } from "@/lib/utils"; import { FileList } from "./FileList"; import { FileSelectionToolbar } from "./FileSelectionToolbar"; @@ -41,6 +43,7 @@ export const ReplayBrowser: React.FC = () => { const netplaySlpFolder = useReplays((store) => store.netplaySlpFolder); const extraFolders = useReplays((store) => store.extraFolders); const selectedFiles = useReplays((store) => store.selectedFiles); + const totalBytes = useReplays((store) => store.totalBytes); const fileSelection = useReplaySelection(); const fileErrorCount = useReplays((store) => store.fileErrorCount); const { addToast } = useToasts(); @@ -202,7 +205,8 @@ export const ReplayBrowser: React.FC = () => {
{filteredFiles.length} files found. {hiddenFileCount} files filtered.{" "} - {fileErrorCount > 0 ? `${fileErrorCount} files had errors.` : ""} + {fileErrorCount > 0 ? `${fileErrorCount} files had errors. ` : ""} + {exists(totalBytes) ? `Total size: ${humanReadableBytes(totalBytes)}` : ""}
diff --git a/src/renderer/lib/hooks/useReplays.ts b/src/renderer/lib/hooks/useReplays.ts index fd9c82320..76ad6455e 100644 --- a/src/renderer/lib/hooks/useReplays.ts +++ b/src/renderer/lib/hooks/useReplays.ts @@ -15,6 +15,7 @@ type StoreState = { progress: Progress | null; files: FileResult[]; netplaySlpFolder: FolderResult | null; + totalBytes: number | null; extraFolders: FolderResult[]; currentRoot: string | null; currentFolder: string; @@ -45,6 +46,7 @@ const initialState: StoreState = { loading: false, progress: null, files: [], + totalBytes: null, netplaySlpFolder: null, extraFolders: [], currentRoot: null, @@ -145,6 +147,7 @@ export const useReplays = create((set, get) => ({ files: result.files, loading: false, fileErrorCount: result.fileErrorCount, + totalBytes: result.totalBytes, }); } catch (err) { set({ loading: false, progress: null }); diff --git a/src/renderer/lib/utils.ts b/src/renderer/lib/utils.ts index e42d68048..dc1032489 100644 --- a/src/renderer/lib/utils.ts +++ b/src/renderer/lib/utils.ts @@ -44,3 +44,16 @@ export const toOrdinal = (i: number): string => { } return i + "th"; }; + +// Converts number of bytes into a human readable format. +// Based on code available from: +// https://coderrocketfuel.com/article/get-the-total-size-of-all-files-in-a-directory-using-node-js +export const humanReadableBytes = (bytes: number): string => { + const sizes = ["bytes", "KB", "MB", "GB", "TB"]; + if (bytes > 0) { + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; + } + + return `0 ${sizes[0]}`; +}; diff --git a/src/replays/loadFolder.ts b/src/replays/loadFolder.ts index d12c7c0f2..fa4f9e0e0 100644 --- a/src/replays/loadFolder.ts +++ b/src/replays/loadFolder.ts @@ -1,3 +1,4 @@ +import { exists } from "common/exists"; import * as fs from "fs-extra"; import path from "path"; @@ -13,14 +14,16 @@ export async function loadFolder( return { files: [], fileErrorCount: 0, + totalBytes: 0, }; } const results = await fs.readdir(folder, { withFileTypes: true }); - const slpFiles = results.filter((dirent) => dirent.isFile() && path.extname(dirent.name) === ".slp"); - const total = slpFiles.length; + const fullSlpPaths = results + .filter((dirent) => dirent.isFile() && path.extname(dirent.name) === ".slp") + .map((dirent) => path.resolve(folder, dirent.name)); + const total = fullSlpPaths.length; - let fileErrorCount = 0; let fileValidCount = 0; callback(0, total); @@ -33,27 +36,34 @@ export async function loadFolder( callback(fileValidCount, total); resolve(res); } catch (err) { - fileErrorCount += 1; resolve(null); } }); }); }; - const slpGames = ( - await Promise.all( - slpFiles.map((dirent) => { - const fullPath = path.resolve(folder, dirent.name); - return process(fullPath); - }), - ) - ).filter((g) => g !== null) as FileResult[]; + const slpGamesPromise = Promise.all( + fullSlpPaths.map((fullPath) => { + return process(fullPath); + }), + ); + const fileSizesPromise = Promise.all( + fullSlpPaths.map( + async (fullPath): Promise => { + const stat = await fs.stat(fullPath); + return stat.size; + }, + ), + ); + + const [slpGames, fileSizes] = await Promise.all([slpGamesPromise, fileSizesPromise]); // Indicate that loading is complete callback(total, total); return { - files: slpGames, - fileErrorCount, + files: slpGames.filter(exists), + fileErrorCount: total - fileValidCount, + totalBytes: fileSizes.reduce((acc, size) => acc + size, 0), }; } diff --git a/src/replays/types.ts b/src/replays/types.ts index bbffa37e9..8e9c90901 100644 --- a/src/replays/types.ts +++ b/src/replays/types.ts @@ -18,6 +18,7 @@ export interface FolderResult { export interface FileLoadResult { files: FileResult[]; + totalBytes: number; fileErrorCount: number; }