Rewrite all styles to tailwind CSS from Bootstrap by @arccik (#58)
* add tailwindcss * add header component with logo and add torrent buttons * remove bootstrap from few files replace it with tailwindcss classes, add card which diplay all nessesarry information about torrent and current state * Add modal component and reorganize components folder * add useModal hook to render modal though react portal, remove UrlPromptModal and replace it with useModal. * add taliwindcss to Desctop app * removed bootstrap from deleteTorrentModal replace it with useModal * replacing bootstrap with useModal * saving * Saving * Header and cards now look good * Modals still broken... * still doesnt work * Finally it scrolls * Continuing to fix bugs * Continuing to fix bugs * Aler * Getting better * Desktop doesnt work with tailwind somehow * Desktop now works with tailwind * Styles fully work * (De)select all buttons * fix alert styles * Animate progress bar * Progress bar + error colors * Fix error message * Torrent status icon (#56) * add statusIcon component to display icon of the torrent status * change props name and remove isDownloading variable * Tweak styles for icon * Tweak styles * Update styles --------- Co-authored-by: Artur Lozovski <arccik@gmail.com>
This commit is contained in:
parent
911bf3a0d5
commit
50fc7f2f01
62 changed files with 7454 additions and 1776 deletions
31
crates/librqbit/webui/src/components/buttons/Button.tsx
Normal file
31
crates/librqbit/webui/src/components/buttons/Button.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { ReactNode } from "react";
|
||||
|
||||
export const Button: React.FC<{
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
variant?: "cancel" | "primary" | "secondary" | "danger" | "none";
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
children: ReactNode;
|
||||
}> = ({ onClick, children, className, disabled, variant }) => {
|
||||
let variantClassNames = {
|
||||
secondary:
|
||||
"hover:bg-blue-600 transition-colors duration-100 hover:text-white",
|
||||
danger:
|
||||
"bg-red-500 text-white border-green-50 hover:border-red-700 hover:bg-red-600",
|
||||
primary: "bg-blue-400 text-white hover:bg-blue-600",
|
||||
cancel: "bg-slate-50 hover:bg-slate-200",
|
||||
none: "",
|
||||
}[variant ?? "secondary"];
|
||||
return (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick(e);
|
||||
}}
|
||||
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}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
52
crates/librqbit/webui/src/components/buttons/FileInput.tsx
Normal file
52
crates/librqbit/webui/src/components/buttons/FileInput.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { RefObject, useRef, useState } from "react";
|
||||
import { UploadButton } from "./UploadButton";
|
||||
import { CgFileAdd } from "react-icons/cg";
|
||||
|
||||
export const FileInput = ({ className }: { className?: string }) => {
|
||||
const inputRef = useRef<HTMLInputElement>() as RefObject<HTMLInputElement>;
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const onFileChange = async () => {
|
||||
if (!inputRef?.current?.files) {
|
||||
return;
|
||||
}
|
||||
const file = inputRef.current.files[0];
|
||||
setFile(file);
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
if (!inputRef?.current) {
|
||||
return;
|
||||
}
|
||||
inputRef.current.value = "";
|
||||
setFile(null);
|
||||
};
|
||||
|
||||
const onClick = () => {
|
||||
if (!inputRef?.current) {
|
||||
return;
|
||||
}
|
||||
inputRef.current.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
ref={inputRef}
|
||||
accept=".torrent"
|
||||
onChange={onFileChange}
|
||||
hidden
|
||||
/>
|
||||
<UploadButton
|
||||
onClick={onClick}
|
||||
data={file}
|
||||
resetData={reset}
|
||||
className={className}
|
||||
>
|
||||
<CgFileAdd color="blue" />
|
||||
<div>Upload .torrent File</div>
|
||||
</UploadButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
30
crates/librqbit/webui/src/components/buttons/IconButton.tsx
Normal file
30
crates/librqbit/webui/src/components/buttons/IconButton.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { MouseEventHandler } from "react";
|
||||
|
||||
export const IconButton: React.FC<{
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
color?: string;
|
||||
children: any;
|
||||
}> = (props) => {
|
||||
const { onClick, disabled, color, children, className, ...otherProps } =
|
||||
props;
|
||||
const onClickStopPropagation: MouseEventHandler<HTMLAnchorElement> = (e) => {
|
||||
e.stopPropagation();
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
onClick();
|
||||
};
|
||||
const colorClassName = color ? `text-${color}` : "";
|
||||
return (
|
||||
<a
|
||||
className={`block p-1 ${colorClassName} ${className}`}
|
||||
onClick={onClickStopPropagation}
|
||||
href="#"
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
65
crates/librqbit/webui/src/components/buttons/MagnetInput.tsx
Normal file
65
crates/librqbit/webui/src/components/buttons/MagnetInput.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { useState } from "react";
|
||||
import { CgLink } from "react-icons/cg";
|
||||
import { UploadButton } from "./UploadButton";
|
||||
import { Modal } from "../modal/Modal";
|
||||
import { Button } from "./Button";
|
||||
import { ModalBody } from "../modal/ModalBody";
|
||||
import { ModalFooter } from "../modal/ModalFooter";
|
||||
import { FormInput } from "../forms/FormInput";
|
||||
|
||||
export const MagnetInput = ({ className }: { className?: string }) => {
|
||||
const [magnet, setMagnet] = useState<string | null>(null);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
|
||||
const clear = () => {
|
||||
setModalIsOpen(false);
|
||||
setMagnet(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<UploadButton
|
||||
onClick={() => {
|
||||
setModalIsOpen(true);
|
||||
}}
|
||||
data={magnet}
|
||||
className={className}
|
||||
resetData={() => setMagnet(null)}
|
||||
>
|
||||
<CgLink color="blue" />
|
||||
<div>Add Torrent from Magnet / URL</div>
|
||||
</UploadButton>
|
||||
|
||||
<Modal isOpen={modalIsOpen} onClose={clear} title="Add torrent">
|
||||
<ModalBody>
|
||||
<FormInput
|
||||
autoFocus
|
||||
value={inputValue}
|
||||
name="magnet"
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
placeholder="magnet:?xt=urn:btih:..."
|
||||
help="Enter magnet or HTTP(S) URL to the .torrent"
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant="cancel" onClick={clear}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!inputValue}
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setMagnet(inputValue);
|
||||
setInputValue("");
|
||||
setModalIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { useContext, useState } from "react";
|
||||
import { TorrentStats } from "../../api-types";
|
||||
import {
|
||||
AppContext,
|
||||
APIContext,
|
||||
RefreshTorrentStatsContext,
|
||||
} from "../../context";
|
||||
import { IconButton } from "./IconButton";
|
||||
import { DeleteTorrentModal } from "../modal/DeleteTorrentModal";
|
||||
import { FaPause, FaPlay, FaTrash } from "react-icons/fa";
|
||||
|
||||
export const TorrentActions: React.FC<{
|
||||
id: number;
|
||||
statsResponse: TorrentStats;
|
||||
}> = ({ id, statsResponse }) => {
|
||||
let state = statsResponse.state;
|
||||
|
||||
let [disabled, setDisabled] = useState<boolean>(false);
|
||||
let [deleting, setDeleting] = useState<boolean>(false);
|
||||
|
||||
let refreshCtx = useContext(RefreshTorrentStatsContext);
|
||||
|
||||
const canPause = state == "live";
|
||||
const canUnpause = state == "paused" || state == "error";
|
||||
|
||||
const ctx = useContext(AppContext);
|
||||
const API = useContext(APIContext);
|
||||
|
||||
const unpause = () => {
|
||||
setDisabled(true);
|
||||
API.start(id)
|
||||
.then(
|
||||
() => {
|
||||
refreshCtx.refresh();
|
||||
},
|
||||
(e) => {
|
||||
ctx.setCloseableError({
|
||||
text: `Error starting torrent id=${id}`,
|
||||
details: e,
|
||||
});
|
||||
}
|
||||
)
|
||||
.finally(() => setDisabled(false));
|
||||
};
|
||||
|
||||
const pause = () => {
|
||||
setDisabled(true);
|
||||
API.pause(id)
|
||||
.then(
|
||||
() => {
|
||||
refreshCtx.refresh();
|
||||
},
|
||||
(e) => {
|
||||
ctx.setCloseableError({
|
||||
text: `Error pausing torrent id=${id}`,
|
||||
details: e,
|
||||
});
|
||||
}
|
||||
)
|
||||
.finally(() => setDisabled(false));
|
||||
};
|
||||
|
||||
const startDeleting = () => {
|
||||
setDisabled(true);
|
||||
setDeleting(true);
|
||||
};
|
||||
|
||||
const cancelDeleting = () => {
|
||||
setDisabled(false);
|
||||
setDeleting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-center gap-2">
|
||||
{canUnpause && (
|
||||
<IconButton onClick={unpause} disabled={disabled}>
|
||||
<FaPlay className="hover:text-green-500 transition-colors duration-300" />
|
||||
</IconButton>
|
||||
)}
|
||||
{canPause && (
|
||||
<IconButton onClick={pause} disabled={disabled}>
|
||||
<FaPause className="hover:text-yellow-500 transition-colors duration-300" />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton onClick={startDeleting} disabled={disabled}>
|
||||
<FaTrash className="hover:text-red-500 transition-colors duration-500" />
|
||||
</IconButton>
|
||||
<DeleteTorrentModal id={id} show={deleting} onHide={cancelDeleting} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { ReactNode, useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
AddTorrentResponse,
|
||||
ErrorDetails as ApiErrorDetails,
|
||||
} from "../../api-types";
|
||||
import { APIContext } from "../../context";
|
||||
import { ErrorWithLabel } from "../../rqbit-web";
|
||||
import { FileSelectionModal } from "../modal/FileSelectionModal";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export const UploadButton: React.FC<{
|
||||
onClick: () => void;
|
||||
data: string | File | null;
|
||||
resetData: () => void;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}> = ({ onClick, data, resetData, children, className }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [listTorrentResponse, setListTorrentResponse] =
|
||||
useState<AddTorrentResponse | null>(null);
|
||||
const [listTorrentError, setListTorrentError] =
|
||||
useState<ErrorWithLabel | null>(null);
|
||||
const API = useContext(APIContext);
|
||||
|
||||
// Get the torrent file list if there's data.
|
||||
useEffect(() => {
|
||||
if (data === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let t = setTimeout(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await API.uploadTorrent(data, { list_only: true });
|
||||
setListTorrentResponse(response);
|
||||
} catch (e) {
|
||||
setListTorrentError({
|
||||
text: "Error listing torrent files",
|
||||
details: e as ApiErrorDetails,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, 0);
|
||||
return () => clearTimeout(t);
|
||||
}, [data]);
|
||||
|
||||
const clear = () => {
|
||||
resetData();
|
||||
setListTorrentError(null);
|
||||
setListTorrentResponse(null);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={onClick} className={className}>
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
{data && (
|
||||
<FileSelectionModal
|
||||
onHide={clear}
|
||||
listTorrentError={listTorrentError}
|
||||
listTorrentResponse={listTorrentResponse}
|
||||
data={data}
|
||||
listTorrentLoading={loading}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue