Fix file selection window on Windows that didnt split file with proper separator

This commit is contained in:
Igor Katson 2024-01-03 14:36:16 +00:00
parent b808382169
commit b289f1eeaa
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
6 changed files with 47 additions and 40 deletions

View file

@ -243,6 +243,7 @@ pub struct TorrentListResponse {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct TorrentDetailsResponseFile { pub struct TorrentDetailsResponseFile {
pub name: String, pub name: String,
pub components: Vec<String>,
pub length: u64, pub length: u64,
pub included: bool, pub included: bool,
} }
@ -253,6 +254,7 @@ pub struct EmptyJsonResponse {}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct TorrentDetailsResponse { pub struct TorrentDetailsResponse {
pub info_hash: String, pub info_hash: String,
pub name: Option<String>,
pub files: Vec<TorrentDetailsResponseFile>, pub files: Vec<TorrentDetailsResponseFile>,
} }
@ -281,9 +283,11 @@ fn make_torrent_details(
"<INVALID NAME>".to_string() "<INVALID NAME>".to_string()
} }
}; };
let components = filename_it.to_vec().unwrap_or_default();
let included = only_files.map(|o| o.contains(&idx)).unwrap_or(true); let included = only_files.map(|o| o.contains(&idx)).unwrap_or(true);
TorrentDetailsResponseFile { TorrentDetailsResponseFile {
name, name,
components,
length, length,
included, included,
} }
@ -291,6 +295,7 @@ fn make_torrent_details(
.collect(); .collect();
Ok(TorrentDetailsResponse { Ok(TorrentDetailsResponse {
info_hash: info_hash.as_string(), info_hash: info_hash.as_string(),
name: info.name.as_ref().map(|b| b.to_string()),
files, files,
}) })
} }

View file

@ -6,12 +6,14 @@ export interface TorrentId {
export interface TorrentFile { export interface TorrentFile {
name: string; name: string;
components: string[];
length: number; length: number;
included: boolean; included: boolean;
} }
// Interface for the Torrent Details API response // Interface for the Torrent Details API response
export interface TorrentDetails { export interface TorrentDetails {
name: string | null;
info_hash: string; info_hash: string;
files: Array<TorrentFile>; files: Array<TorrentFile>;
} }

View file

@ -1,5 +1,5 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { AddTorrentResponse } from "../api-types"; import { AddTorrentResponse, TorrentFile } 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";
import { IconButton } from "./buttons/IconButton"; import { IconButton } from "./buttons/IconButton";
@ -7,7 +7,8 @@ import { formatBytes } from "../helper/formatBytes";
type TorrentFileForCheckbox = { type TorrentFileForCheckbox = {
id: number; id: number;
name: string; filename: string;
pathComponents: string[];
length: number; length: number;
}; };
@ -18,19 +19,12 @@ type FileTree = {
files: TorrentFileForCheckbox[]; files: TorrentFileForCheckbox[];
}; };
const splitOnce = (s: string, sep: string): [string, string | undefined] => {
if (s.indexOf(sep) === -1) {
return [s, undefined];
}
return [s.slice(0, s.indexOf(sep)), s.slice(s.indexOf(sep) + 1)];
};
const newFileTree = (listTorrentResponse: AddTorrentResponse): FileTree => { const newFileTree = (listTorrentResponse: AddTorrentResponse): FileTree => {
const separator = "/";
const newFileTreeInner = ( const newFileTreeInner = (
name: string, name: string,
id: string, id: string,
files: TorrentFileForCheckbox[] files: TorrentFileForCheckbox[],
depth: number
): FileTree => { ): FileTree => {
let directFiles: TorrentFileForCheckbox[] = []; let directFiles: TorrentFileForCheckbox[] = [];
let groups: FileTree[] = []; let groups: FileTree[] = [];
@ -41,22 +35,17 @@ const newFileTree = (listTorrentResponse: AddTorrentResponse): FileTree => {
return groupsByName[prefix]; return groupsByName[prefix];
}; };
files.forEach((file) => { files.forEach((file: TorrentFileForCheckbox) => {
let [prefix, name] = splitOnce(file.name, separator); if (depth == file.pathComponents.length - 1) {
if (name === undefined) {
directFiles.push(file); directFiles.push(file);
return; return;
} }
getGroup(prefix).push({ getGroup(file.pathComponents[0]).push(file);
id: file.id,
name: name,
length: file.length,
});
}); });
let childId = 0; let childId = 0;
for (const [key, value] of Object.entries(groupsByName)) { for (const [key, value] of Object.entries(groupsByName)) {
groups.push(newFileTreeInner(key, id + "." + childId, value)); groups.push(newFileTreeInner(key, id + "." + childId, value, depth + 1));
childId += 1; childId += 1;
} }
return { return {
@ -70,9 +59,15 @@ const newFileTree = (listTorrentResponse: AddTorrentResponse): FileTree => {
return newFileTreeInner( return newFileTreeInner(
"", "",
"filetree-root", "filetree-root",
listTorrentResponse.details.files.map((data, id) => { listTorrentResponse.details.files.map((file, id) => {
return { id, name: data.name, length: data.length }; return {
}) id,
filename: file.components[file.components.length - 1],
pathComponents: file.components,
length: file.length,
};
}),
0
); );
}; };
@ -168,7 +163,7 @@ const FileTreeComponent: React.FC<{
<FormCheckbox <FormCheckbox
checked={selectedFiles.has(file.id)} checked={selectedFiles.has(file.id)}
key={file.id} key={file.id}
label={`${file.name} (${formatBytes(file.length)})`} label={`${file.filename} (${formatBytes(file.length)})`}
name={`file-${file.id}`} name={`file-${file.id}`}
onChange={() => handleToggleFile(file.id)} onChange={() => handleToggleFile(file.id)}
></FormCheckbox> ></FormCheckbox>

View file

@ -9,7 +9,7 @@ import { TorrentActions } from "./buttons/TorrentActions";
import { ProgressBar } from "./ProgressBar"; import { ProgressBar } from "./ProgressBar";
import { Speed } from "./Speed"; import { Speed } from "./Speed";
import { formatBytes } from "../helper/formatBytes"; import { formatBytes } from "../helper/formatBytes";
import { getLargestFileName } from "../helper/getLargestFileName"; import { torrentDisplayName } from "../helper/getTorrentDisplayName";
import { getCompletionETA } from "../helper/getCompletionETA"; import { getCompletionETA } from "../helper/getCompletionETA";
import { StatusIcon } from "./StatusIcon"; import { StatusIcon } from "./StatusIcon";
@ -54,7 +54,7 @@ export const TorrentRow: React.FC<{
<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 dark:text-slate-200"> <div className="text-left text-lg text-gray-900 text-ellipsis break-all dark:text-slate-200">
{getLargestFileName(detailsResponse)} {torrentDisplayName(detailsResponse)}
</div> </div>
</div> </div>
)} )}

View file

@ -1,6 +1,6 @@
import { TorrentDetails } from "../api-types"; import { TorrentDetails } from "../api-types";
export function getLargestFileName(torrentDetails: TorrentDetails): string { function getLargestFileName(torrentDetails: TorrentDetails): string {
const largestFile = torrentDetails.files const largestFile = torrentDetails.files
.filter((f) => f.included) .filter((f) => f.included)
.reduce((prev: any, current: any) => .reduce((prev: any, current: any) =>
@ -8,3 +8,7 @@ export function getLargestFileName(torrentDetails: TorrentDetails): string {
); );
return largestFile.name; return largestFile.name;
} }
export function torrentDisplayName(torrentDetails: TorrentDetails): string {
return torrentDetails.name ?? getLargestFileName(torrentDetails);
}

View file

@ -74,6 +74,7 @@ pub struct TorrentMetaV1Info<BufType> {
pub files: Option<Vec<TorrentMetaV1File<BufType>>>, pub files: Option<Vec<TorrentMetaV1File<BufType>>>,
} }
#[derive(Clone, Copy)]
pub enum FileIteratorName<'a, ByteBuf> { pub enum FileIteratorName<'a, ByteBuf> {
Single(Option<&'a ByteBuf>), Single(Option<&'a ByteBuf>),
Tree(&'a [ByteBuf]), Tree(&'a [ByteBuf]),
@ -91,11 +92,17 @@ where
} }
} }
impl<'a, ByteBuf> FileIteratorName<'a, ByteBuf> { impl<'a, ByteBuf> FileIteratorName<'a, ByteBuf>
pub fn to_string(&self) -> anyhow::Result<String> where
where ByteBuf: AsRef<[u8]>,
ByteBuf: AsRef<[u8]>, {
{ pub fn to_vec(&self) -> anyhow::Result<Vec<String>> {
self.iter_components()
.map(|c| c.map(|s| s.to_owned()))
.collect()
}
pub fn to_string(&self) -> anyhow::Result<String> {
let mut buf = String::new(); let mut buf = String::new();
for (idx, bit) in self.iter_components().enumerate() { for (idx, bit) in self.iter_components().enumerate() {
let bit = bit?; let bit = bit?;
@ -106,10 +113,7 @@ impl<'a, ByteBuf> FileIteratorName<'a, ByteBuf> {
} }
Ok(buf) Ok(buf)
} }
pub fn to_pathbuf(&self) -> anyhow::Result<PathBuf> pub fn to_pathbuf(&self) -> anyhow::Result<PathBuf> {
where
ByteBuf: AsRef<[u8]>,
{
let mut buf = PathBuf::new(); let mut buf = PathBuf::new();
for bit in self.iter_components() { for bit in self.iter_components() {
let bit = bit?; let bit = bit?;
@ -117,10 +121,7 @@ impl<'a, ByteBuf> FileIteratorName<'a, ByteBuf> {
} }
Ok(buf) Ok(buf)
} }
pub fn iter_components(&self) -> impl Iterator<Item = anyhow::Result<&'a str>> pub fn iter_components(&self) -> impl Iterator<Item = anyhow::Result<&'a str>> {
where
ByteBuf: AsRef<[u8]>,
{
let it = match self { let it = match self {
FileIteratorName::Single(None) => return Either::Left(once(Ok("torrent-content"))), FileIteratorName::Single(None) => return Either::Left(once(Ok("torrent-content"))),
FileIteratorName::Single(Some(name)) => Either::Left(once((*name).as_ref())), FileIteratorName::Single(Some(name)) => Either::Left(once((*name).as_ref())),