Files
myDrive/src/components/Files/Files.tsx
T
oxmc ecc340e7b1
Release Please / release-please (push) Has been cancelled
Docker Build and Push (Development) / build-and-push-dev (push) Has been cancelled
feat: add dark mode UI and SQLite as alternate database backend
Dark mode: CSS custom properties + .dark class toggled via Redux/localStorage,
with a theme toggle in the Header and Settings page. Foundation didn't exist
yet despite prior assumption, so it was built from scratch.

SQLite: new DB_ENGINE=mongo|sqlite env var selects the metadata backend at
runtime (Mongo stays default, no behavior change unless opted in). File/thumbnail
bytes continue to use the existing fs/S3 storage path. Implemented via a shared
DB interface (dbTypes.ts) with parallel mongoDB/ and sqliteDB/ implementations,
selected through dbFactory.ts, with JWT/encryption logic extracted into
userCrypto.ts so both engines share it.
2026-07-05 04:18:33 -07:00

137 lines
5.6 KiB
TypeScript

import { useFiles } from "../../hooks/files";
import { useUtils } from "../../hooks/utils";
import React, { memo } from "react";
import FileItem from "../FileItem/FileItem";
import classNames from "classnames";
import { useAppDispatch, useAppSelector } from "../../hooks/store";
import { toggleListView } from "../../reducers/general";
const Files = memo(() => {
const { data: files } = useFiles(false);
const listView = useAppSelector((state) => state.general.listView);
const { isHome } = useUtils();
const dispatch = useAppDispatch();
const changeListViewMode = () => {
dispatch(toggleListView());
};
return (
<div className="mt-8 select-none">
<div>
<div className="flex justify-between items-center mb-5">
<h2 className="m-0 text-xl font-medium">
{isHome ? "Home" : ""} Files
</h2>
<div>
<ul className="flex items-center list-none m-0 p-0">
<li className="mr-4" onClick={changeListViewMode}>
<a
className={classNames({
"text-gray-primary dark:text-gray-300": listView,
"text-primary": !listView,
})}
>
<svg
className="w-4 h-4 cursor-pointer"
aria-hidden="true"
focusable="false"
data-prefix="fas"
data-icon="th-large"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
data-fa-i2svg=""
>
<path
fill="currentColor"
d="M296 32h192c13.255 0 24 10.745 24 24v160c0 13.255-10.745 24-24 24H296c-13.255 0-24-10.745-24-24V56c0-13.255 10.745-24 24-24zm-80 0H24C10.745 32 0 42.745 0 56v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24zM0 296v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H24c-13.255 0-24 10.745-24 24zm296 184h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H296c-13.255 0-24 10.745-24 24v160c0 13.255 10.745 24 24 24z"
></path>
</svg>
</a>
</li>
<li className="mr-2" onClick={changeListViewMode}>
<a
className={classNames({
"text-gray-primary dark:text-gray-300": !listView,
"text-primary": listView,
})}
>
<svg
className="w-4 h-4 cursor-pointer"
aria-hidden="true"
focusable="false"
data-prefix="fas"
data-icon="list"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
data-fa-i2svg=""
>
<path
fill="currentColor"
d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"
></path>
</svg>
</a>
</li>
</ul>
</div>
</div>
{!listView ? (
<div
className={classNames(
"grid grid-cols-[repeat(auto-fit,minmax(47%,45%))] xs:grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-[16px]",
files?.pages[0]?.length === 1
? "justify-normal"
: "justify-center xs:justify-normal"
)}
>
{files?.pages.map((filePage, index) => (
<React.Fragment key={index}>
{filePage.map((file) => (
<FileItem file={file} key={file._id} />
))}
</React.Fragment>
))}
</div>
) : (
<table className="w-full">
<thead>
<tr>
<th>
<div className="flex flex-row items-center mb-2">
<p className="text-black dark:text-gray-100 text-sm font-medium">Name</p>
</div>
</th>
<th className="hidden fileListShowDetails:table-cell">
<p className="text-black dark:text-gray-100 text-sm font-medium mb-2">Size</p>
</th>
<th className="hidden fileListShowDetails:table-cell">
<p className="text-black dark:text-gray-100 text-sm font-medium mb-2">Created</p>
</th>
<th>
<p className="text-black dark:text-gray-100 text-sm font-medium mb-2">Actions</p>
</th>
</tr>
</thead>
<tbody>
{files?.pages.map((filePage, index) => (
<React.Fragment key={index}>
{filePage.map((file) => (
<FileItem file={file} key={file._id} />
))}
</React.Fragment>
))}
</tbody>
</table>
)}
</div>
</div>
);
});
export default Files;