Merge pull request #61 from arccik/dark-theme

Dark mode
This commit is contained in:
Igor Katson 2023-12-16 11:18:11 +00:00 committed by GitHub
commit 17ddaa3427
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 148 additions and 118 deletions

View file

@ -103,6 +103,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[ignore]
async fn read_metainfo_from_dht() { async fn read_metainfo_from_dht() {
init_logging(); init_logging();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,14 +4,14 @@
"src": "assets/logo.svg" "src": "assets/logo.svg"
}, },
"index.css": { "index.css": {
"file": "assets/index-458d2033.css", "file": "assets/index-8d563632.css",
"src": "index.css" "src": "index.css"
}, },
"index.html": { "index.html": {
"css": [ "css": [
"assets/index-458d2033.css" "assets/index-8d563632.css"
], ],
"file": "assets/index-1daa8daf.js", "file": "assets/index-c39122a8.js",
"isEntry": true, "isEntry": true,
"src": "index.html" "src": "index.html"
} }

View file

@ -7,7 +7,7 @@ const AlertDanger: React.FC<{
onClose?: () => void; onClose?: () => void;
}> = ({ title, children, onClose }) => { }> = ({ title, children, onClose }) => {
return ( return (
<div className="bg-red-200 p-3 rounded-md mb-3"> <div className="bg-red-200 p-3 rounded-md mb-3 dark:bg-red-800/60">
<div className="flex justify-between mb-2"> <div className="flex justify-between mb-2">
<h2 className="text-lg font-semibold">{title}</h2> <h2 className="text-lg font-semibold">{title}</h2>
{onClose && ( {onClose && (

View file

@ -7,19 +7,19 @@ import Logo from "../../assets/logo.svg?react";
export const Header = ({ title }: { title: string }) => { export const Header = ({ title }: { title: string }) => {
const [name, version] = title.split("-"); const [name, version] = title.split("-");
return ( return (
<header className="bg-slate-50 drop-shadow-lg flex flex-wrap justify-center lg:justify-between items-center mb-3"> <header className="bg-slate-50 drop-shadow-lg flex flex-wrap justify-center lg:justify-between items-center dark:bg-slate-800 mb-3">
<div className="flex flex-nowrap items-center justify-between m-2"> <div className="flex flex-nowrap items-center justify-between m-2">
<Logo className="w-10 h-10 p-1" alt="logo" /> <Logo className="w-10 h-10 p-1" alt="logo" />
<h1 className="flex items-center"> <h1 className="flex items-center dark:text-white">
<div className="text-3xl">{name}</div> <div className="text-3xl">{name}</div>
<div className="bg-blue-100 text-blue-800 text-xl font-semibold me-2 px-2.5 py-0.5 rounded ms-2"> <div className="bg-blue-100 text-blue-800 text-xl font-semibold me-2 px-2.5 py-0.5 rounded ms-2 dark:bg-blue-900 dark:text-white">
{version} {version}
</div> </div>
</h1> </h1>
</div> </div>
<div className="flex flex-wrap gap-1 m-2"> <div className="flex flex-wrap gap-1 m-2">
<MagnetInput className="flex-grow justify-center" /> <MagnetInput className="flex-grow justify-center dark:text-white" />
<FileInput className="flex-grow justify-center" /> <FileInput className="flex-grow justify-center dark:text-white" />
</div> </div>
</header> </header>
); );

View file

@ -31,13 +31,16 @@ const LogSpan: React.FC<{ span: Span }> = ({ span }) => (
<> <>
<span className="font-bold">{span.name}</span> <span className="font-bold">{span.name}</span>
<SpanFields span={span} /> <SpanFields span={span} />
<span className="font-bold">:</span>
</> </>
); );
const Fields: React.FC<{ fields: JSONLogLine["fields"] }> = ({ fields }) => ( const Fields: React.FC<{ fields: JSONLogLine["fields"] }> = ({ fields }) => (
<span <span
className={`m-1 ${ className={`m-1 ${
fields.message.match(/error|fail/g) ? "text-red-500" : "text-slate-500" fields.message.match(/error|fail/g)
? "text-red-500"
: "text-slate-500 dark:text-slate-200"
}`} }`}
> >
{fields.message} {fields.message}
@ -72,7 +75,9 @@ export const LogLine: React.FC<{ line: JSONLogLine }> = React.memo(
return ( return (
<p className="font-mono m-0 text-break text-[10px]"> <p className="font-mono m-0 text-break text-[10px]">
<span className="m-1 text-slate-500">{parsed.timestamp}</span> <span className="m-1 text-slate-500 dark:text-slate-400">
{parsed.timestamp}
</span>
<span className={`m-1 ${classNameByLevel(parsed.level)}`}> <span className={`m-1 ${classNameByLevel(parsed.level)}`}>
{parsed.level} {parsed.level}
</span> </span>
@ -80,7 +85,9 @@ export const LogLine: React.FC<{ line: JSONLogLine }> = React.memo(
<span className="m-1"> <span className="m-1">
{parsed.spans?.map((span, i) => <LogSpan key={i} span={span} />)} {parsed.spans?.map((span, i) => <LogSpan key={i} span={span} />)}
</span> </span>
<span className="m-1 text-slate-500">{parsed.target}</span> <span className="m-1 text-slate-500 dark:text-slate-400">
{parsed.target}
</span>
<Fields fields={parsed.fields} /> <Fields fields={parsed.fields} />
</p> </p>
); );

View file

@ -15,7 +15,7 @@ export const ProgressBar = ({ now, variant, label }: Props) => {
}[variant ?? "info"]; }[variant ?? "info"];
return ( return (
<div className={"w-full bg-gray-200 rounded-full"}> <div className={"w-full bg-gray-200 rounded-full dark:bg-gray-500"}>
<div <div
className={`text-xs bg-blue-500 font-medium transition-all text-center p-0.5 leading-none rounded-full ${variantClassName}`} className={`text-xs bg-blue-500 font-medium transition-all text-center p-0.5 leading-none rounded-full ${variantClassName}`}
style={{ width: `${now}%` }} style={{ width: `${now}%` }}

View file

@ -1,9 +1,9 @@
export const Spinner = () => { export const Spinner = ({ label }: { label?: string }) => {
return ( return (
<div role="status"> <div className="flex gap-2 items-center w-full justify-center">
<svg <svg
aria-hidden="true" aria-hidden="true"
className="inline w-8 h-8 text-gray-200 animate-spin fill-blue-600" className="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
viewBox="0 0 100 101" viewBox="0 0 100 101"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -17,7 +17,11 @@ export const Spinner = () => {
fill="currentFill" fill="currentFill"
/> />
</svg> </svg>
<span className="sr-only">Loading...</span> {label ? (
<span className="text-sm">{label} ...</span>
) : (
<span className="sr-only">Loading...</span>
)}
</div> </div>
); );
}; };

View file

@ -1,5 +1,4 @@
import { import {
MdCheck,
MdCheckCircle, MdCheckCircle,
MdDownload, MdDownload,
MdError, MdError,

View file

@ -45,7 +45,7 @@ export const TorrentRow: React.FC<{
}; };
return ( return (
<section className="flex flex-col sm:flex-row items-center gap-2 border p-2 border-gray-200 rounded-xl shadow-xs hover:drop-shadow-sm"> <section className="flex flex-col sm:flex-row items-center gap-2 border p-2 border-gray-200 rounded-xl shadow-xs hover:drop-shadow-sm dark:bg-slate-800 dark:border-slate-900">
{/* Icon */} {/* Icon */}
<div className="hidden md:block">{statusIcon("w-10 h-10")}</div> <div className="hidden md:block">{statusIcon("w-10 h-10")}</div>
{/* Name, progress, stats */} {/* Name, progress, stats */}
@ -53,7 +53,7 @@ export const TorrentRow: React.FC<{
{detailsResponse && ( {detailsResponse && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="md:hidden">{statusIcon("w-5 h-5")}</div> <div className="md:hidden">{statusIcon("w-5 h-5")}</div>
<div className="text-left text-lg text-gray-900 text-ellipsis break-all"> <div className="text-left text-lg text-gray-900 text-ellipsis break-all dark:text-slate-200">
{getLargestFileName(detailsResponse)} {getLargestFileName(detailsResponse)}
</div> </div>
</div> </div>

View file

@ -6,26 +6,21 @@ export const TorrentsList = (props: {
torrents: Array<TorrentId> | null; torrents: Array<TorrentId> | null;
loading: boolean; loading: boolean;
}) => { }) => {
if (props.torrents === null && props.loading) {
return <Spinner />;
}
// The app either just started, or there was an error loading torrents.
if (props.torrents === null) {
return;
}
if (props.torrents.length === 0) {
return (
<div className="text-center">
<p>No existing torrents found.</p>
</div>
);
}
return ( return (
<div className="flex flex-col gap-2 mx-2"> <div className="flex flex-col gap-2 mx-2 pb-3 sm:px-7">
{props.torrents.map((t: TorrentId) => ( {props.torrents === null ? (
<Torrent id={t.id} key={t.id} torrent={t} /> props.loading ? (
))} <Spinner label="Loading torrent list" />
) : null
) : props.torrents.length === 0 ? (
<p className="text-center">No existing torrents found.</p>
) : (
props.torrents.map((t: TorrentId) => (
<>
<Torrent id={t.id} key={t.id} torrent={t} />
</>
))
)}
</div> </div>
); );
}; };

View file

@ -8,11 +8,14 @@ export const Button: React.FC<{
children: ReactNode; children: ReactNode;
}> = ({ onClick, children, className, disabled, variant }) => { }> = ({ onClick, children, className, disabled, variant }) => {
let variantClassNames = { let variantClassNames = {
secondary: "hover:bg-blue-500 transition-colors hover:text-white", secondary:
"hover:bg-blue-500 transition-colors hover:text-white dark:hover:bg-blue-900/50",
danger: danger:
"bg-red-400 text-white border-green-50 hover:border-red-700 hover:bg-red-600", "bg-red-400 text-white border-green-50 hover:border-red-700 hover:bg-red-600 dark:bg-red-800 dark:border-none dark:hover:bg-red-900",
primary: "bg-blue-600 text-white hover:bg-blue-800 disabled:bg-blue-200", primary:
cancel: "hover:bg-slate-200", "bg-blue-600 text-white hover:bg-blue-800 disabled:bg-blue-200 dark:disabled:bg-slate-600 dark:disabled:text-slate-300 dark:border-none",
cancel:
"hover:bg-slate-200 dark:bg-slate-600 dark:hover:bg-slate-700 dark:border-none",
none: "", none: "",
}[variant ?? "secondary"]; }[variant ?? "secondary"];
return ( return (
@ -22,7 +25,7 @@ export const Button: React.FC<{
e.preventDefault(); e.preventDefault();
onClick(); onClick();
}} }}
className={`flex inline-flex items-center gap-1 border rounded-lg border disabled:cursor-not-allowed px-2 py-1 transition-colors duration-300 ${variantClassNames} ${className}`} className={`inline-flex items-center gap-1 border rounded-lg disabled:cursor-not-allowed px-2 py-1 transition-colors duration-300 dark:border-slate-700 ${variantClassNames} ${className}`}
> >
{children} {children}
</button> </button>

View file

@ -44,7 +44,7 @@ export const FileInput = ({ className }: { className?: string }) => {
resetData={reset} resetData={reset}
className={className} className={className}
> >
<CgFileAdd color="blue" /> <CgFileAdd className="text-blue-500 dark:text-white" />
<div>Upload .torrent File</div> <div>Upload .torrent File</div>
</UploadButton> </UploadButton>
</> </>

View file

@ -33,7 +33,7 @@ export const MagnetInput = ({ className }: { className?: string }) => {
className={className} className={className}
resetData={() => setMagnet(null)} resetData={() => setMagnet(null)}
> >
<CgLink color="blue" /> <CgLink className="text-blue-500 dark:text-white" />
<div>Add Torrent from Magnet / URL</div> <div>Add Torrent from Magnet / URL</div>
</UploadButton> </UploadButton>

View file

@ -71,7 +71,7 @@ export const TorrentActions: React.FC<{
}; };
return ( return (
<div className="flex w-full justify-center gap-2"> <div className="flex w-full justify-center gap-2 dark:text-slate-300">
{canUnpause && ( {canUnpause && (
<IconButton onClick={unpause} disabled={disabled}> <IconButton onClick={unpause} disabled={disabled}>
<FaPlay className="hover:text-green-500 transition-colors duration-300" /> <FaPlay className="hover:text-green-500 transition-colors duration-300" />

View file

@ -24,7 +24,11 @@ export const FormCheckbox: React.FC<{
</div> </div>
<div className="text-sm flex flex-col gap-1"> <div className="text-sm flex flex-col gap-1">
<label htmlFor={name}>{label}</label> <label htmlFor={name}>{label}</label>
{help && <div className="text-xs text-slate-500 mb-3">{help}</div>} {help && (
<div className="text-xs text-slate-500 dark:text-slate-300 mb-3">
{help}
</div>
)}
</div> </div>
</div> </div>
); );

View file

@ -25,11 +25,13 @@ export const FormInput: React.FC<{
}) => { }) => {
return ( return (
<div className="flex flex-col gap-2 text-sm mb-2"> <div className="flex flex-col gap-2 text-sm mb-2">
<label htmlFor={name}>{label}</label> <label htmlFor={name} className="dark:text-white">
{label}
</label>
<input <input
autoFocus={autoFocus} autoFocus={autoFocus}
type={inputType} type={inputType}
className="block border rounded bg-transparent py-1.5 pl-2 text-gray-800 focus:ring-0 sm:text-sm sm:leading-6" className="block border rounded bg-transparent py-1.5 pl-2 text-gray-800 focus:ring-0 sm:text-sm sm:leading-6 dark:text-slate-300"
id={name} id={name}
name={name} name={name}
disabled={disabled} disabled={disabled}
@ -38,7 +40,9 @@ export const FormInput: React.FC<{
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onChange={onChange} onChange={onChange}
/> />
{help && <div className="text-xs text-slate-500">{help}</div>} {help && (
<div className="text-xs text-slate-500 dark:text-slate-300">{help}</div>
)}
</div> </div>
); );
}; };

View file

@ -52,7 +52,7 @@ export const DeleteTorrentModal: React.FC<{
return ( return (
<Modal isOpen={show} onClose={onHide} title="Delete torrent"> <Modal isOpen={show} onClose={onHide} title="Delete torrent">
<ModalBody> <ModalBody>
<p className="text-gray-700"> <p className="text-gray-700 dark:text-slate-300">
Are you sure you want to delete the torrent? Are you sure you want to delete the torrent?
</p> </p>
@ -65,7 +65,10 @@ export const DeleteTorrentModal: React.FC<{
checked={deleteFiles} checked={deleteFiles}
placeholder="Also delete files" placeholder="Also delete files"
/> />
<label htmlFor="deleteFiles" className="ml-2 text-gray-700"> <label
htmlFor="deleteFiles"
className="ml-2 text-gray-700 dark:text-slate-300"
>
Also delete files Also delete files
</label> </label>
</div> </div>

View file

@ -101,7 +101,7 @@ export const FileSelectionModal = (props: {
const getBody = () => { const getBody = () => {
if (listTorrentLoading) { if (listTorrentLoading) {
return <Spinner />; return <Spinner label="Loading torrent contents" />;
} else if (listTorrentError) { } else if (listTorrentError) {
return <ErrorComponent error={listTorrentError}></ErrorComponent>; return <ErrorComponent error={listTorrentError}></ErrorComponent>;
} else if (listTorrentResponse) { } else if (listTorrentResponse) {

View file

@ -16,8 +16,8 @@ const ModalHeader: React.FC<{
title: string; title: string;
}> = ({ onClose, title }) => { }> = ({ onClose, title }) => {
return ( return (
<div className="flex p-3 justify-between items-center border-b"> <div className="flex p-3 justify-between items-center border-b dark:border-slate-600">
<h2 className="text-xl font-semibold">{title}</h2> <h2 className="text-xl font-semibold dark:slate-300">{title}</h2>
{onClose && ( {onClose && (
<button <button
className="text-gray-500 hover:text-gray-700" className="text-gray-500 hover:text-gray-700"
@ -39,17 +39,19 @@ export const Modal: React.FC<ModalProps> = ({
className, className,
}) => { }) => {
const renderBackdrop = () => { const renderBackdrop = () => {
return <div className="fixed inset-0 bg-black/30 z-[300]"></div>; return (
<div className="fixed inset-0 bg-black/30 z-[300] dark:bg-black/60 backdrop-blur"></div>
);
}; };
return ( return (
<RestartModal <RestartModal
show={isOpen} show={isOpen}
onHide={onClose} onHide={onClose}
renderBackdrop={renderBackdrop} renderBackdrop={renderBackdrop}
className={`fixed z-[301] top-0 left-0 w-full h-full block overflow-x-hidden overflow-y-auto`} className="fixed z-[301] top-0 left-0 w-full h-full block overflow-x-hidden overflow-y-auto"
> >
<div <div
className={`bg-white shadow-lg my-8 mx-auto max-w-2xl rounded ${className}`} className={`bg-white shadow-lg my-8 mx-auto max-w-2xl rounded ${className} dark:bg-slate-800 dark:text-zinc-50`}
> >
<ModalHeader onClose={onClose} title={title} /> <ModalHeader onClose={onClose} title={title} />
{children} {children}

View file

@ -1,5 +1,5 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
export const ModalBody = ({ children }: { children: ReactNode }) => { export const ModalBody = ({ children }: { children: ReactNode }) => {
return <div className="p-3 border-b">{children}</div>; return <div className="p-3 border-b dark:border-slate-500">{children}</div>;
}; };

View file

@ -0,0 +1,21 @@
let darkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (event) => {
DarkMode.setDark(event.matches);
});
export const DarkMode = {
isDark: () => darkMode,
setDark: (value: boolean) => {
darkMode = value;
document.body.classList.toggle("dark", darkMode);
return darkMode;
},
toggle: () => {
DarkMode.setDark(!darkMode);
},
};
DarkMode.setDark(darkMode);

View file

@ -4,9 +4,10 @@ import { AppContext, APIContext } from "./context";
import { RootContent } from "./components/RootContent"; import { RootContent } from "./components/RootContent";
import { customSetInterval } from "./helper/customSetInterval"; import { customSetInterval } from "./helper/customSetInterval";
import { IconButton } from "./components/buttons/IconButton"; import { IconButton } from "./components/buttons/IconButton";
import { BsBodyText } from "react-icons/bs"; import { BsBodyText, BsMoon } from "react-icons/bs";
import { LogStreamModal } from "./components/modal/LogStreamModal"; import { LogStreamModal } from "./components/modal/LogStreamModal";
import { Header } from "./components/Header"; import { Header } from "./components/Header";
import { DarkMode } from "./helper/darkMode";
export interface ErrorWithLabel { export interface ErrorWithLabel {
text: string; text: string;
@ -66,26 +67,34 @@ export const RqbitWebUI = (props: {
return ( return (
<AppContext.Provider value={context}> <AppContext.Provider value={context}>
<Header title={props.title} /> <div className="dark:bg-gray-900 dark:text-zinc-50 min-h-screen">
<div className="relative"> <Header title={props.title} />
{/* Menu buttons */} <div className="relative">
<div className="absolute top-0 start-0 pl-2 z-10"> {/* Menu buttons */}
{props.menuButtons && <div className="absolute top-0 start-0 pl-2 z-10">
props.menuButtons.map((b, i) => <span key={i}>{b}</span>)} {props.menuButtons &&
<IconButton onClick={() => setLogsOpened(true)}> props.menuButtons.map((b, i) => <span key={i}>{b}</span>)}
<BsBodyText /> <IconButton onClick={() => setLogsOpened(true)}>
</IconButton> <BsBodyText />
</IconButton>
<IconButton onClick={DarkMode.toggle}>
<BsMoon />
</IconButton>
</div>
<RootContent
closeableError={closeableError}
otherError={otherError}
torrents={torrents}
torrentsLoading={torrentsLoading}
/>
</div> </div>
<RootContent <LogStreamModal
closeableError={closeableError} show={logsOpened}
otherError={otherError} onClose={() => setLogsOpened(false)}
torrents={torrents}
torrentsLoading={torrentsLoading}
/> />
</div> </div>
<LogStreamModal show={logsOpened} onClose={() => setLogsOpened(false)} />
</AppContext.Provider> </AppContext.Provider>
); );
}; };

View file

@ -1,25 +1,5 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
"./index.html", darkMode: "class",
"./src/**/*.{js,ts,jsx,tsx}", };
],
theme: {
extend: {
fadeIn: {
from: { opacity: 0 },
to: { opacity: 1 },
},
fadeOut: {
from: { opacity: 1 },
to: { opacity: 0 },
},
},
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
'fade-out': 'fadeOut 0.3s ease-in-out',
},
},
plugins: [],
}

View file

@ -1867,7 +1867,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit" name = "librqbit"
version = "5.1.0" version = "5.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@ -3012,7 +3012,7 @@ dependencies = [
[[package]] [[package]]
name = "rqbit-desktop" name = "rqbit-desktop"
version = "5.1.0" version = "5.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.21.5", "base64 0.21.5",

View file

@ -184,7 +184,8 @@ export const ConfigModal: React.FC<{
const isActive = t === tab; const isActive = t === tab;
let classNames = "text-slate-300"; let classNames = "text-slate-300";
if (isActive) { if (isActive) {
classNames = "text-slate-800 border-b-2 border-blue-800"; classNames =
"text-slate-800 border-b-2 border-blue-800 dark:border-blue-200 dark:text-white";
} }
return ( return (
<button <button

View file

@ -4,8 +4,5 @@ export default {
"./src/**/*.{js,ts,jsx,tsx,mdx}", "./src/**/*.{js,ts,jsx,tsx,mdx}",
"../crates/librqbit/webui/src/**/*.{js,ts,jsx,tsx,mdx}", "../crates/librqbit/webui/src/**/*.{js,ts,jsx,tsx,mdx}",
], ],
theme: { darkMode: "class",
extend: {},
},
plugins: [],
}; };