Copy playlist to clipboard, native UI (not alert)
This commit is contained in:
parent
e485844d86
commit
65e4f1b0a6
9 changed files with 135 additions and 24 deletions
|
|
@ -162,6 +162,7 @@ export interface JSONLogLine {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RqbitAPI {
|
export interface RqbitAPI {
|
||||||
|
getPlaylistUrl: (index: number) => string | null;
|
||||||
getStreamLogsUrl: () => string | null;
|
getStreamLogsUrl: () => string | null;
|
||||||
listTorrents: () => Promise<ListTorrentsResponse>;
|
listTorrents: () => Promise<ListTorrentsResponse>;
|
||||||
getTorrentDetails: (index: number) => Promise<TorrentDetails>;
|
getTorrentDetails: (index: number) => Promise<TorrentDetails>;
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ export const IconButton: React.FC<{
|
||||||
className?: string;
|
className?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
children: any;
|
children: any;
|
||||||
|
href?: string;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { onClick, disabled, color, children, className, ...otherProps } =
|
const { onClick, disabled, color, children, className, href, ...otherProps } =
|
||||||
props;
|
props;
|
||||||
const onClickStopPropagation: MouseEventHandler<HTMLAnchorElement> = (e) => {
|
const onClickStopPropagation: MouseEventHandler<HTMLAnchorElement> = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -22,7 +23,7 @@ export const IconButton: React.FC<{
|
||||||
<a
|
<a
|
||||||
className={`p-1 text-blue-500 flex items-center justify-center ${colorClassName} ${className}`}
|
className={`p-1 text-blue-500 flex items-center justify-center ${colorClassName} ${className}`}
|
||||||
onClick={onClickStopPropagation}
|
onClick={onClickStopPropagation}
|
||||||
href="#"
|
href={href ?? "#"}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,13 @@ import { TorrentStats } from "../../api-types";
|
||||||
import { APIContext, RefreshTorrentStatsContext } from "../../context";
|
import { APIContext, RefreshTorrentStatsContext } from "../../context";
|
||||||
import { IconButton } from "./IconButton";
|
import { IconButton } from "./IconButton";
|
||||||
import { DeleteTorrentModal } from "../modal/DeleteTorrentModal";
|
import { DeleteTorrentModal } from "../modal/DeleteTorrentModal";
|
||||||
import { FaCog, FaPause, FaPlay, FaTrash, FaClipboardList } from "react-icons/fa";
|
import {
|
||||||
|
FaCog,
|
||||||
|
FaPause,
|
||||||
|
FaPlay,
|
||||||
|
FaTrash,
|
||||||
|
FaClipboardList,
|
||||||
|
} from "react-icons/fa";
|
||||||
import { useErrorStore } from "../../stores/errorStore";
|
import { useErrorStore } from "../../stores/errorStore";
|
||||||
|
|
||||||
export const TorrentActions: React.FC<{
|
export const TorrentActions: React.FC<{
|
||||||
|
|
@ -39,7 +45,7 @@ export const TorrentActions: React.FC<{
|
||||||
text: `Error starting torrent id=${id}`,
|
text: `Error starting torrent id=${id}`,
|
||||||
details: e,
|
details: e,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
.finally(() => setDisabled(false));
|
.finally(() => setDisabled(false));
|
||||||
};
|
};
|
||||||
|
|
@ -56,7 +62,7 @@ export const TorrentActions: React.FC<{
|
||||||
text: `Error pausing torrent id=${id}`,
|
text: `Error pausing torrent id=${id}`,
|
||||||
details: e,
|
details: e,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
.finally(() => setDisabled(false));
|
.finally(() => setDisabled(false));
|
||||||
};
|
};
|
||||||
|
|
@ -71,6 +77,32 @@ export const TorrentActions: React.FC<{
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const playlistUrl = API.getPlaylistUrl(id);
|
||||||
|
|
||||||
|
const setAlert = useErrorStore((state) => state.setAlert);
|
||||||
|
|
||||||
|
const copyPlaylistUrlToClipboard = async () => {
|
||||||
|
if (!playlistUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(playlistUrl);
|
||||||
|
} catch (e) {
|
||||||
|
setCloseableError({
|
||||||
|
text: "Error",
|
||||||
|
details: { text: `Error copying playlist URL to clipboard: ${e}` },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAlert({
|
||||||
|
text: "Copied",
|
||||||
|
details: {
|
||||||
|
text: `Playlist URL copied to clipboard. Paste into e.g. VLC to play.`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full justify-center gap-2 dark:text-slate-300">
|
<div className="flex w-full justify-center gap-2 dark:text-slate-300">
|
||||||
{canUnpause && (
|
{canUnpause && (
|
||||||
|
|
@ -94,10 +126,11 @@ export const TorrentActions: React.FC<{
|
||||||
<IconButton onClick={startDeleting} disabled={disabled}>
|
<IconButton onClick={startDeleting} disabled={disabled}>
|
||||||
<FaTrash className="hover:text-red-500" />
|
<FaTrash className="hover:text-red-500" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton onClick={() => {alert("Open this playlist link in external player like VLC")}}>
|
<IconButton
|
||||||
<a target="_blank" href={"/torrents/"+id+"/playlist"}>
|
href={playlistUrl ?? "#"}
|
||||||
<FaClipboardList className="hover:text-green-500"/>
|
onClick={copyPlaylistUrlToClipboard}
|
||||||
</a>
|
>
|
||||||
|
<FaClipboardList className="hover:text-green-500" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<DeleteTorrentModal id={id} show={deleting} onHide={cancelDeleting} />
|
<DeleteTorrentModal id={id} show={deleting} onHide={cancelDeleting} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
33
crates/librqbit/webui/src/components/modal/AlertModal.tsx
Normal file
33
crates/librqbit/webui/src/components/modal/AlertModal.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { ErrorWithLabel } from "../../rqbit-web";
|
||||||
|
import { useErrorStore } from "../../stores/errorStore";
|
||||||
|
import { Button } from "../buttons/Button";
|
||||||
|
import { Modal } from "./Modal";
|
||||||
|
import { ModalBody } from "./ModalBody";
|
||||||
|
import { ModalFooter } from "./ModalFooter";
|
||||||
|
|
||||||
|
export const AlertModal: React.FC<{}> = () => {
|
||||||
|
let alert = useErrorStore((store) => store.alert);
|
||||||
|
let removeAlert = useErrorStore((store) => store.removeAlert);
|
||||||
|
|
||||||
|
if (alert) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={true} onClose={removeAlert} title={alert.text}>
|
||||||
|
<ModalBody>
|
||||||
|
{alert.details?.statusText && (
|
||||||
|
<div className="pb-2 text-md">{alert.details?.statusText}</div>
|
||||||
|
)}
|
||||||
|
<div className="whitespace-pre-wrap text-sm">
|
||||||
|
{alert.details?.text}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="cancel" onClick={removeAlert}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -151,4 +151,7 @@ export const API: RqbitAPI & { getVersion: () => Promise<string> } = {
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
|
getPlaylistUrl: (index: number) => {
|
||||||
|
return apiUrl + `/torrents/${index}/playlist`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { Header } from "./components/Header";
|
||||||
import { DarkMode } from "./helper/darkMode";
|
import { DarkMode } from "./helper/darkMode";
|
||||||
import { useTorrentStore } from "./stores/torrentStore";
|
import { useTorrentStore } from "./stores/torrentStore";
|
||||||
import { useErrorStore } from "./stores/errorStore";
|
import { useErrorStore } from "./stores/errorStore";
|
||||||
|
import { AlertModal } from "./components/modal/AlertModal";
|
||||||
|
|
||||||
export interface ErrorWithLabel {
|
export interface ErrorWithLabel {
|
||||||
text: string;
|
text: string;
|
||||||
|
|
@ -33,16 +34,16 @@ export const RqbitWebUI = (props: {
|
||||||
|
|
||||||
const setTorrents = useTorrentStore((state) => state.setTorrents);
|
const setTorrents = useTorrentStore((state) => state.setTorrents);
|
||||||
const setTorrentsLoading = useTorrentStore(
|
const setTorrentsLoading = useTorrentStore(
|
||||||
(state) => state.setTorrentsLoading,
|
(state) => state.setTorrentsLoading
|
||||||
);
|
);
|
||||||
const setRefreshTorrents = useTorrentStore(
|
const setRefreshTorrents = useTorrentStore(
|
||||||
(state) => state.setRefreshTorrents,
|
(state) => state.setRefreshTorrents
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshTorrents = async () => {
|
const refreshTorrents = async () => {
|
||||||
setTorrentsLoading(true);
|
setTorrentsLoading(true);
|
||||||
let torrents = await API.listTorrents().finally(() =>
|
let torrents = await API.listTorrents().finally(() =>
|
||||||
setTorrentsLoading(false),
|
setTorrentsLoading(false)
|
||||||
);
|
);
|
||||||
setTorrents(torrents.torrents);
|
setTorrents(torrents.torrents);
|
||||||
};
|
};
|
||||||
|
|
@ -60,9 +61,9 @@ export const RqbitWebUI = (props: {
|
||||||
setOtherError({ text: "Error refreshing torrents", details: e });
|
setOtherError({ text: "Error refreshing torrents", details: e });
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return 5000;
|
return 5000;
|
||||||
},
|
}
|
||||||
),
|
),
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -86,6 +87,7 @@ export const RqbitWebUI = (props: {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LogStreamModal show={logsOpened} onClose={() => setLogsOpened(false)} />
|
<LogStreamModal show={logsOpened} onClose={() => setLogsOpened(false)} />
|
||||||
|
<AlertModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ export interface ErrorWithLabel {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useErrorStore = create<{
|
export const useErrorStore = create<{
|
||||||
|
alert: ErrorWithLabel | null;
|
||||||
|
setAlert: (alert: ErrorWithLabel) => void;
|
||||||
|
removeAlert: () => void;
|
||||||
|
|
||||||
closeableError: ErrorWithLabel | null;
|
closeableError: ErrorWithLabel | null;
|
||||||
setCloseableError: (error: ErrorWithLabel | null) => void;
|
setCloseableError: (error: ErrorWithLabel | null) => void;
|
||||||
|
|
||||||
|
|
@ -18,4 +22,8 @@ export const useErrorStore = create<{
|
||||||
|
|
||||||
otherError: null,
|
otherError: null,
|
||||||
setOtherError: (otherError) => set(() => ({ otherError })),
|
setOtherError: (otherError) => set(() => ({ otherError })),
|
||||||
|
|
||||||
|
alert: null,
|
||||||
|
setAlert: (alert) => set(() => ({alert})),
|
||||||
|
removeAlert: () => set(() => ({alert: null}))
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
14
desktop/src-tauri/Cargo.lock
generated
14
desktop/src-tauri/Cargo.lock
generated
|
|
@ -1881,6 +1881,7 @@ dependencies = [
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"size_format",
|
"size_format",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-socks",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
|
@ -2991,6 +2992,7 @@ dependencies = [
|
||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
|
"tokio-socks",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|
@ -4013,6 +4015,18 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-socks"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"futures-util",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.15"
|
version = "0.1.15"
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ interface InvokeErrorResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorToUIError(
|
function errorToUIError(
|
||||||
path: string,
|
path: string
|
||||||
): (e: InvokeErrorResponse) => Promise<never> {
|
): (e: InvokeErrorResponse) => Promise<never> {
|
||||||
return (e: InvokeErrorResponse) => {
|
return (e: InvokeErrorResponse) => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
|
|
@ -35,11 +35,11 @@ function errorToUIError(
|
||||||
|
|
||||||
export async function invokeAPI<Response>(
|
export async function invokeAPI<Response>(
|
||||||
name: string,
|
name: string,
|
||||||
params?: InvokeArgs,
|
params?: InvokeArgs
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
console.log("invoking", name, params);
|
console.log("invoking", name, params);
|
||||||
const result = await invoke<Response>(name, params).catch(
|
const result = await invoke<Response>(name, params).catch(
|
||||||
errorToUIError(name),
|
errorToUIError(name)
|
||||||
);
|
);
|
||||||
console.log(result);
|
console.log(result);
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -68,16 +68,26 @@ async function readFileAsBase64(file: File): Promise<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeAPI = (configuration: RqbitDesktopConfig): RqbitAPI => {
|
export const makeAPI = (configuration: RqbitDesktopConfig): RqbitAPI => {
|
||||||
|
const getHttpBase = () => {
|
||||||
|
if (!configuration.http_api.listen_addr) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let port = configuration.http_api.listen_addr.split(":")[1];
|
||||||
|
if (!port) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `http://127.0.0.1:${port}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let httpBase = getHttpBase();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getStreamLogsUrl: () => {
|
getStreamLogsUrl: () => {
|
||||||
if (!configuration.http_api.listen_addr) {
|
if (!httpBase) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let port = configuration.http_api.listen_addr.split(":")[1];
|
return `${httpBase}/stream_logs`;
|
||||||
if (!port) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return `http://127.0.0.1:${port}/stream_logs`;
|
|
||||||
},
|
},
|
||||||
listTorrents: async function (): Promise<ListTorrentsResponse> {
|
listTorrents: async function (): Promise<ListTorrentsResponse> {
|
||||||
return await invokeAPI<ListTorrentsResponse>("torrents_list");
|
return await invokeAPI<ListTorrentsResponse>("torrents_list");
|
||||||
|
|
@ -96,7 +106,7 @@ export const makeAPI = (configuration: RqbitDesktopConfig): RqbitAPI => {
|
||||||
{
|
{
|
||||||
contents,
|
contents,
|
||||||
opts: opts ?? {},
|
opts: opts ?? {},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return await invokeAPI<AddTorrentResponse>("torrent_create_from_url", {
|
return await invokeAPI<AddTorrentResponse>("torrent_create_from_url", {
|
||||||
|
|
@ -125,5 +135,11 @@ export const makeAPI = (configuration: RqbitDesktopConfig): RqbitAPI => {
|
||||||
getTorrentStreamUrl: () => {
|
getTorrentStreamUrl: () => {
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
getPlaylistUrl: (index: number) => {
|
||||||
|
if (!httpBase) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${httpBase}/torrents/${index}/playlist`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue