UI link to video files

This commit is contained in:
Igor Katson 2024-04-24 22:55:56 +01:00
parent f0788f2c4a
commit 94589a21fe
11 changed files with 64 additions and 19 deletions

View file

@ -176,7 +176,6 @@ impl HttpApi {
.and_then(|s| s.parse().ok()); .and_then(|s| s.parse().ok());
if let Some(offset) = offset { if let Some(offset) = offset {
status = StatusCode::PARTIAL_CONTENT; status = StatusCode::PARTIAL_CONTENT;
info!(offset, "range request offset");
stream stream
.seek(SeekFrom::Start(offset)) .seek(SeekFrom::Start(offset))
.await .await

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-144bafe5.css", "file": "assets/index-74bd798d.css",
"src": "index.css" "src": "index.css"
}, },
"index.html": { "index.html": {
"css": [ "css": [
"assets/index-144bafe5.css" "assets/index-74bd798d.css"
], ],
"file": "assets/index-838acac9.js", "file": "assets/index-80d6e7c6.js",
"isEntry": true, "isEntry": true,
"src": "index.html" "src": "index.html"
} }

View file

@ -166,6 +166,7 @@ export interface RqbitAPI {
listTorrents: () => Promise<ListTorrentsResponse>; listTorrents: () => Promise<ListTorrentsResponse>;
getTorrentDetails: (index: number) => Promise<TorrentDetails>; getTorrentDetails: (index: number) => Promise<TorrentDetails>;
getTorrentStats: (index: number) => Promise<TorrentStats>; getTorrentStats: (index: number) => Promise<TorrentStats>;
getTorrentStreamUrl: (index: number, file_id: number) => string | null;
uploadTorrent: ( uploadTorrent: (
data: string | File, data: string | File,
opts?: AddTorrentOptions, opts?: AddTorrentOptions,

View file

@ -1,4 +1,4 @@
import { useMemo, useState } from "react"; import { useContext, useMemo, useState } from "react";
import { TorrentDetails, TorrentStats } from "../api-types"; import { TorrentDetails, TorrentStats } from "../api-types";
import { FormCheckbox } from "./forms/FormCheckbox"; import { FormCheckbox } from "./forms/FormCheckbox";
import { CiSquarePlus, CiSquareMinus } from "react-icons/ci"; import { CiSquarePlus, CiSquareMinus } from "react-icons/ci";
@ -6,6 +6,7 @@ import { IconButton } from "./buttons/IconButton";
import { formatBytes } from "../helper/formatBytes"; import { formatBytes } from "../helper/formatBytes";
import { ProgressBar } from "./ProgressBar"; import { ProgressBar } from "./ProgressBar";
import sortBy from "lodash.sortby"; import sortBy from "lodash.sortby";
import { APIContext } from "../context";
type TorrentFileForCheckbox = { type TorrentFileForCheckbox = {
id: number; id: number;
@ -86,6 +87,7 @@ const newFileTree = (
}; };
const FileTreeComponent: React.FC<{ const FileTreeComponent: React.FC<{
torrentId?: number;
tree: FileTree; tree: FileTree;
torrentDetails: TorrentDetails; torrentDetails: TorrentDetails;
torrentStats: TorrentStats | null; torrentStats: TorrentStats | null;
@ -94,7 +96,9 @@ const FileTreeComponent: React.FC<{
initialExpanded: boolean; initialExpanded: boolean;
showProgressBar?: boolean; showProgressBar?: boolean;
disabled?: boolean; disabled?: boolean;
allowStream?: boolean;
}> = ({ }> = ({
torrentId,
tree, tree,
selectedFiles, selectedFiles,
setSelectedFiles, setSelectedFiles,
@ -103,7 +107,9 @@ const FileTreeComponent: React.FC<{
torrentStats, torrentStats,
showProgressBar, showProgressBar,
disabled, disabled,
allowStream,
}) => { }) => {
const API = useContext(APIContext);
let [expanded, setExpanded] = useState(initialExpanded); let [expanded, setExpanded] = useState(initialExpanded);
let children = useMemo(() => { let children = useMemo(() => {
let getAllChildren = (tree: FileTree): number[] => { let getAllChildren = (tree: FileTree): number[] => {
@ -149,6 +155,16 @@ const FileTreeComponent: React.FC<{
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
}; };
const fileLink = (file: TorrentFileForCheckbox) => {
if (
allowStream &&
torrentId != null &&
/\.(mp4|mkv|avi)$/.test(file.filename)
) {
return API.getTorrentStreamUrl(torrentId, file.id);
}
};
return ( return (
<> <>
<div className="flex items-center"> <div className="flex items-center">
@ -170,6 +186,7 @@ const FileTreeComponent: React.FC<{
<div className="pl-5" hidden={!expanded}> <div className="pl-5" hidden={!expanded}>
{tree.dirs.map((dir) => ( {tree.dirs.map((dir) => (
<FileTreeComponent <FileTreeComponent
torrentId={torrentId}
torrentDetails={torrentDetails} torrentDetails={torrentDetails}
torrentStats={torrentStats} torrentStats={torrentStats}
key={dir.name} key={dir.name}
@ -179,6 +196,7 @@ const FileTreeComponent: React.FC<{
initialExpanded={false} initialExpanded={false}
showProgressBar={showProgressBar} showProgressBar={showProgressBar}
disabled={disabled} disabled={disabled}
allowStream={allowStream}
/> />
))} ))}
<div className="pl-1"> <div className="pl-1">
@ -197,6 +215,7 @@ const FileTreeComponent: React.FC<{
name={`file-${file.id}`} name={`file-${file.id}`}
disabled={disabled} disabled={disabled}
onChange={() => handleToggleFile(file.id)} onChange={() => handleToggleFile(file.id)}
labelLink={fileLink(file)}
></FormCheckbox> ></FormCheckbox>
{showProgressBar && ( {showProgressBar && (
<ProgressBar <ProgressBar
@ -213,19 +232,23 @@ const FileTreeComponent: React.FC<{
}; };
export const FileListInput: React.FC<{ export const FileListInput: React.FC<{
torrentId?: number;
torrentDetails: TorrentDetails; torrentDetails: TorrentDetails;
torrentStats: TorrentStats | null; torrentStats: TorrentStats | null;
selectedFiles: Set<number>; selectedFiles: Set<number>;
setSelectedFiles: (_: Set<number>) => void; setSelectedFiles: (_: Set<number>) => void;
showProgressBar?: boolean; showProgressBar?: boolean;
disabled?: boolean; disabled?: boolean;
allowStream?: boolean;
}> = ({ }> = ({
torrentId,
torrentDetails, torrentDetails,
selectedFiles, selectedFiles,
setSelectedFiles, setSelectedFiles,
torrentStats, torrentStats,
showProgressBar, showProgressBar,
disabled, disabled,
allowStream,
}) => { }) => {
let fileTree = useMemo( let fileTree = useMemo(
() => newFileTree(torrentDetails, torrentStats), () => newFileTree(torrentDetails, torrentStats),
@ -234,6 +257,7 @@ export const FileListInput: React.FC<{
return ( return (
<FileTreeComponent <FileTreeComponent
torrentId={torrentId}
torrentDetails={torrentDetails} torrentDetails={torrentDetails}
torrentStats={torrentStats} torrentStats={torrentStats}
tree={fileTree} tree={fileTree}
@ -242,6 +266,7 @@ export const FileListInput: React.FC<{
initialExpanded={true} initialExpanded={true}
showProgressBar={showProgressBar} showProgressBar={showProgressBar}
disabled={disabled} disabled={disabled}
allowStream={allowStream}
/> />
); );
}; };

View file

@ -170,11 +170,13 @@ export const TorrentRow: React.FC<{
{detailsResponse && extendedView && ( {detailsResponse && extendedView && (
<div className=""> <div className="">
<FileListInput <FileListInput
torrentId={id}
torrentDetails={detailsResponse} torrentDetails={detailsResponse}
torrentStats={statsResponse} torrentStats={statsResponse}
selectedFiles={selectedFiles} selectedFiles={selectedFiles}
setSelectedFiles={updateSelectedFiles} setSelectedFiles={updateSelectedFiles}
disabled={savingSelectedFiles} disabled={savingSelectedFiles}
allowStream
showProgressBar showProgressBar
/> />
</div> </div>

View file

@ -10,6 +10,7 @@ export const FormCheckbox: React.FC<{
onChange?: ChangeEventHandler<HTMLInputElement>; onChange?: ChangeEventHandler<HTMLInputElement>;
children?: React.ReactNode; children?: React.ReactNode;
classNames?: string; classNames?: string;
labelLink?: string | null;
}> = ({ }> = ({
checked, checked,
name, name,
@ -19,6 +20,7 @@ export const FormCheckbox: React.FC<{
help, help,
inputType, inputType,
children, children,
labelLink,
}) => { }) => {
return ( return (
<div className={`flex gap-3 items-start`}> <div className={`flex gap-3 items-start`}>
@ -34,7 +36,17 @@ 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> {labelLink ? (
<a
href={labelLink}
className="text-blue-600 dark:text-blue-500 hover:underline"
>
{label}
</a>
) : (
<label htmlFor={name}>{label}</label>
)}
{help && ( {help && (
<div className="text-xs text-slate-500 dark:text-slate-300 mb-3"> <div className="text-xs text-slate-500 dark:text-slate-300 mb-3">
{help} {help}

View file

@ -29,8 +29,8 @@ export const APIContext = createContext<RqbitAPI>({
delete: () => { delete: () => {
throw new Error("Function not implemented."); throw new Error("Function not implemented.");
}, },
getStreamLogsUrl: () => { getTorrentStreamUrl: () => {
return null; throw new Error("Function not implemented.");
}, },
}); });
export const RefreshTorrentStatsContext = createContext({ refresh: () => {} }); export const RefreshTorrentStatsContext = createContext({ refresh: () => {} });

View file

@ -140,4 +140,7 @@ export const API: RqbitAPI & { getVersion: () => Promise<string> } = {
const r = await makeRequest("GET", "/"); const r = await makeRequest("GET", "/");
return r.version; return r.version;
}, },
getTorrentStreamUrl: (index: number, file_id: number) => {
return apiUrl + `/torrents/${index}/stream/${file_id}`;
},
}; };

View file

@ -122,5 +122,8 @@ export const makeAPI = (configuration: RqbitDesktopConfig): RqbitAPI => {
delete: function (id: number): Promise<void> { delete: function (id: number): Promise<void> {
return invokeAPI<void>("torrent_action_delete", { id }); return invokeAPI<void>("torrent_action_delete", { id });
}, },
getTorrentStreamUrl: () => {
return "";
},
}; };
}; };