From 65e4f1b0a6b8cdb6232188a557545fbb56694cdc Mon Sep 17 00:00:00 2001 From: Igor Katson Date: Thu, 8 Aug 2024 09:56:16 +0100 Subject: [PATCH] Copy playlist to clipboard, native UI (not alert) --- crates/librqbit/webui/src/api-types.ts | 1 + .../src/components/buttons/IconButton.tsx | 5 +- .../src/components/buttons/TorrentActions.tsx | 47 ++++++++++++++++--- .../webui/src/components/modal/AlertModal.tsx | 33 +++++++++++++ crates/librqbit/webui/src/http-api.ts | 3 ++ crates/librqbit/webui/src/rqbit-web.tsx | 12 +++-- .../librqbit/webui/src/stores/errorStore.ts | 8 ++++ desktop/src-tauri/Cargo.lock | 14 ++++++ desktop/src/api.tsx | 36 ++++++++++---- 9 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 crates/librqbit/webui/src/components/modal/AlertModal.tsx diff --git a/crates/librqbit/webui/src/api-types.ts b/crates/librqbit/webui/src/api-types.ts index 71e7a31..6865bbb 100644 --- a/crates/librqbit/webui/src/api-types.ts +++ b/crates/librqbit/webui/src/api-types.ts @@ -162,6 +162,7 @@ export interface JSONLogLine { } export interface RqbitAPI { + getPlaylistUrl: (index: number) => string | null; getStreamLogsUrl: () => string | null; listTorrents: () => Promise; getTorrentDetails: (index: number) => Promise; diff --git a/crates/librqbit/webui/src/components/buttons/IconButton.tsx b/crates/librqbit/webui/src/components/buttons/IconButton.tsx index 5af6497..9500e2f 100644 --- a/crates/librqbit/webui/src/components/buttons/IconButton.tsx +++ b/crates/librqbit/webui/src/components/buttons/IconButton.tsx @@ -6,8 +6,9 @@ export const IconButton: React.FC<{ className?: string; color?: string; children: any; + href?: string; }> = (props) => { - const { onClick, disabled, color, children, className, ...otherProps } = + const { onClick, disabled, color, children, className, href, ...otherProps } = props; const onClickStopPropagation: MouseEventHandler = (e) => { e.stopPropagation(); @@ -22,7 +23,7 @@ export const IconButton: React.FC<{ {children} diff --git a/crates/librqbit/webui/src/components/buttons/TorrentActions.tsx b/crates/librqbit/webui/src/components/buttons/TorrentActions.tsx index f2bbe11..9ab7100 100644 --- a/crates/librqbit/webui/src/components/buttons/TorrentActions.tsx +++ b/crates/librqbit/webui/src/components/buttons/TorrentActions.tsx @@ -3,7 +3,13 @@ import { TorrentStats } from "../../api-types"; import { APIContext, RefreshTorrentStatsContext } from "../../context"; import { IconButton } from "./IconButton"; 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"; export const TorrentActions: React.FC<{ @@ -39,7 +45,7 @@ export const TorrentActions: React.FC<{ text: `Error starting torrent id=${id}`, details: e, }); - }, + } ) .finally(() => setDisabled(false)); }; @@ -56,7 +62,7 @@ export const TorrentActions: React.FC<{ text: `Error pausing torrent id=${id}`, details: e, }); - }, + } ) .finally(() => setDisabled(false)); }; @@ -71,6 +77,32 @@ export const TorrentActions: React.FC<{ 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 (
{canUnpause && ( @@ -94,10 +126,11 @@ export const TorrentActions: React.FC<{ - {alert("Open this playlist link in external player like VLC")}}> - - - + +
diff --git a/crates/librqbit/webui/src/components/modal/AlertModal.tsx b/crates/librqbit/webui/src/components/modal/AlertModal.tsx new file mode 100644 index 0000000..5ed5585 --- /dev/null +++ b/crates/librqbit/webui/src/components/modal/AlertModal.tsx @@ -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 ( + + + {alert.details?.statusText && ( +
{alert.details?.statusText}
+ )} +
+ {alert.details?.text} +
+
+ + + +
+ ); + } else { + return <>; + } +}; diff --git a/crates/librqbit/webui/src/http-api.ts b/crates/librqbit/webui/src/http-api.ts index 44f8ca6..16a3ea2 100644 --- a/crates/librqbit/webui/src/http-api.ts +++ b/crates/librqbit/webui/src/http-api.ts @@ -151,4 +151,7 @@ export const API: RqbitAPI & { getVersion: () => Promise } = { } return url; }, + getPlaylistUrl: (index: number) => { + return apiUrl + `/torrents/${index}/playlist`; + }, }; diff --git a/crates/librqbit/webui/src/rqbit-web.tsx b/crates/librqbit/webui/src/rqbit-web.tsx index 06e8c17..e2ac3a3 100644 --- a/crates/librqbit/webui/src/rqbit-web.tsx +++ b/crates/librqbit/webui/src/rqbit-web.tsx @@ -10,6 +10,7 @@ import { Header } from "./components/Header"; import { DarkMode } from "./helper/darkMode"; import { useTorrentStore } from "./stores/torrentStore"; import { useErrorStore } from "./stores/errorStore"; +import { AlertModal } from "./components/modal/AlertModal"; export interface ErrorWithLabel { text: string; @@ -33,16 +34,16 @@ export const RqbitWebUI = (props: { const setTorrents = useTorrentStore((state) => state.setTorrents); const setTorrentsLoading = useTorrentStore( - (state) => state.setTorrentsLoading, + (state) => state.setTorrentsLoading ); const setRefreshTorrents = useTorrentStore( - (state) => state.setRefreshTorrents, + (state) => state.setRefreshTorrents ); const refreshTorrents = async () => { setTorrentsLoading(true); let torrents = await API.listTorrents().finally(() => - setTorrentsLoading(false), + setTorrentsLoading(false) ); setTorrents(torrents.torrents); }; @@ -60,9 +61,9 @@ export const RqbitWebUI = (props: { setOtherError({ text: "Error refreshing torrents", details: e }); console.error(e); return 5000; - }, + } ), - 0, + 0 ); }, []); @@ -86,6 +87,7 @@ export const RqbitWebUI = (props: { setLogsOpened(false)} /> + ); }; diff --git a/crates/librqbit/webui/src/stores/errorStore.ts b/crates/librqbit/webui/src/stores/errorStore.ts index 648be2f..9947211 100644 --- a/crates/librqbit/webui/src/stores/errorStore.ts +++ b/crates/librqbit/webui/src/stores/errorStore.ts @@ -7,6 +7,10 @@ export interface ErrorWithLabel { } export const useErrorStore = create<{ + alert: ErrorWithLabel | null; + setAlert: (alert: ErrorWithLabel) => void; + removeAlert: () => void; + closeableError: ErrorWithLabel | null; setCloseableError: (error: ErrorWithLabel | null) => void; @@ -18,4 +22,8 @@ export const useErrorStore = create<{ otherError: null, setOtherError: (otherError) => set(() => ({ otherError })), + + alert: null, + setAlert: (alert) => set(() => ({alert})), + removeAlert: () => set(() => ({alert: null})) })); diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index b229ad3..14f6de2 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1881,6 +1881,7 @@ dependencies = [ "serde_with", "size_format", "tokio", + "tokio-socks", "tokio-stream", "tokio-util", "tower-http", @@ -2991,6 +2992,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-socks", "tower-service", "url", "wasm-bindgen", @@ -4013,6 +4015,18 @@ dependencies = [ "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]] name = "tokio-stream" version = "0.1.15" diff --git a/desktop/src/api.tsx b/desktop/src/api.tsx index 1742b95..6e0f9e8 100644 --- a/desktop/src/api.tsx +++ b/desktop/src/api.tsx @@ -18,7 +18,7 @@ interface InvokeErrorResponse { } function errorToUIError( - path: string, + path: string ): (e: InvokeErrorResponse) => Promise { return (e: InvokeErrorResponse) => { console.log(e); @@ -35,11 +35,11 @@ function errorToUIError( export async function invokeAPI( name: string, - params?: InvokeArgs, + params?: InvokeArgs ): Promise { console.log("invoking", name, params); const result = await invoke(name, params).catch( - errorToUIError(name), + errorToUIError(name) ); console.log(result); return result; @@ -68,16 +68,26 @@ async function readFileAsBase64(file: File): Promise { } 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 { getStreamLogsUrl: () => { - if (!configuration.http_api.listen_addr) { + if (!httpBase) { return null; } - let port = configuration.http_api.listen_addr.split(":")[1]; - if (!port) { - return null; - } - return `http://127.0.0.1:${port}/stream_logs`; + return `${httpBase}/stream_logs`; }, listTorrents: async function (): Promise { return await invokeAPI("torrents_list"); @@ -96,7 +106,7 @@ export const makeAPI = (configuration: RqbitDesktopConfig): RqbitAPI => { { contents, opts: opts ?? {}, - }, + } ); } return await invokeAPI("torrent_create_from_url", { @@ -125,5 +135,11 @@ export const makeAPI = (configuration: RqbitDesktopConfig): RqbitAPI => { getTorrentStreamUrl: () => { return ""; }, + getPlaylistUrl: (index: number) => { + if (!httpBase) { + return null; + } + return `${httpBase}/torrents/${index}/playlist`; + }, }; };