commit
17ddaa3427
29 changed files with 148 additions and 118 deletions
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
2
crates/librqbit/webui/dist/assets/index.css
vendored
2
crates/librqbit/webui/dist/assets/index.css
vendored
File diff suppressed because one or more lines are too long
18
crates/librqbit/webui/dist/assets/index.js
vendored
18
crates/librqbit/webui/dist/assets/index.js
vendored
File diff suppressed because one or more lines are too long
6
crates/librqbit/webui/dist/manifest.json
vendored
6
crates/librqbit/webui/dist/manifest.json
vendored
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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}%` }}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
MdCheck,
|
|
||||||
MdCheckCircle,
|
MdCheckCircle,
|
||||||
MdDownload,
|
MdDownload,
|
||||||
MdError,
|
MdError,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
21
crates/librqbit/webui/src/helper/darkMode.ts
Normal file
21
crates/librqbit/webui/src/helper/darkMode.ts
Normal 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);
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
4
desktop/src-tauri/Cargo.lock
generated
4
desktop/src-tauri/Cargo.lock
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: [],
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue