Skip to content

Commit

Permalink
Split trackers from properties dialog, allow multi edit
Browse files Browse the repository at this point in the history
Issue #76
  • Loading branch information
qu1ck committed Oct 26, 2023
1 parent 052785b commit 5b2e2e3
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 72 deletions.
8 changes: 3 additions & 5 deletions src/components/modals/add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { Box, Button, Checkbox, Divider, Flex, Group, Menu, SegmentedControl, Text, TextInput } from "@mantine/core";
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import type { ModalState, LocationData } from "./common";
import { HkModal, TorrentLabels, TorrentLocation, limitTorrentNames, useTorrentLocation } from "./common";
import { HkModal, LimitedNamesList, TorrentLabels, TorrentLocation, useTorrentLocation } from "./common";
import type { PriorityNumberType } from "rpc/transmission";
import { PriorityColors, PriorityStrings } from "rpc/transmission";
import type { Torrent } from "rpc/torrent";
Expand Down Expand Up @@ -558,9 +558,7 @@ export function AddTorrent(props: AddCommonModalProps) {
const names = useMemo(() => {
if (torrentData === undefined) return [];

const names = torrentData.map((td) => td.name);

return limitTorrentNames(names, 1);
return torrentData.map((td) => td.name);
}, [torrentData]);

const torrentExists = existingTorrent !== undefined;
Expand All @@ -578,7 +576,7 @@ export function AddTorrent(props: AddCommonModalProps) {
<Divider my="sm" />
{torrentExists
? <Text color="red" fw="bold" fz="lg">Torrent already exists</Text>
: names.map((name, i) => <Text key={i}>{name}</Text>)}
: <LimitedNamesList names={names} limit={1} />}
<div style={{ position: "relative" }}>
<AddCommon {...common.props} disabled={torrentExists}>
{(torrentData.length > 1 || torrentData[0].files == null)
Expand Down
34 changes: 17 additions & 17 deletions src/components/modals/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,21 @@ export function SaveCancelModal({ onSave, onClose, children, saveLoading, ...oth
);
}

export function limitTorrentNames(allNames: string[], limit: number = 5) {
const names: string[] = allNames.slice(0, limit);
export function LimitedNamesList({ names, limit }: { names: string[], limit?: number }) {
limit = limit ?? 5;
const t = names.slice(0, limit);

if (allNames.length > limit) names.push(`... and ${allNames.length - limit} more`);

return names;
return <>
{t.map((s, i) => <Text key={i} mx="md" my="xs" px="sm" sx={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
boxShadow: "inset 0 0 0 9999px rgba(133, 133, 133, 0.1)",
}}>
{s}
</Text>)}
{names.length > limit && <Text mx="xl" mb="md">{`... and ${names.length - limit} more`}</Text>}
</>;
}

export function TorrentsNames() {
Expand All @@ -84,20 +93,11 @@ export function TorrentsNames() {
if (serverData.current == null || serverSelected.size === 0) {
return ["No torrent selected"];
}

const selected = serverData.torrents.filter(
(t) => serverSelected.has(t.id));

const allNames: string[] = [];
selected.forEach((t) => allNames.push(t.name));
return allNames;
return serverData.torrents.filter(
(t) => serverSelected.has(t.id)).map((t) => t.name);
}, [serverData, serverSelected]);

const names = limitTorrentNames(allNames);

return <>
{names.map((s, i) => <Text key={i} ml="xl" mb="md">{s}</Text>)}
</>;
return <LimitedNamesList names={allNames} />;
}

export interface LocationData {
Expand Down
65 changes: 16 additions & 49 deletions src/components/modals/edittorrent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import React, { useCallback, useContext, useEffect } from "react";
import React, { useCallback, useEffect, useMemo } from "react";
import type { ModalState } from "./common";
import { SaveCancelModal } from "./common";
import { SaveCancelModal, TorrentsNames } from "./common";
import { useForm } from "@mantine/form";
import { useMutateTorrent, useTorrentDetails } from "queries";
import { notifications } from "@mantine/notifications";
import { Button, Checkbox, Grid, LoadingOverlay, NumberInput, Text, Textarea } from "@mantine/core";
import { ConfigContext } from "config";
import type { TrackerStats } from "rpc/torrent";
import { useServerRpcVersion, useServerTorrentData } from "rpc/torrent";
import { Checkbox, Grid, LoadingOverlay, NumberInput } from "@mantine/core";
import { useServerSelectedTorrents, useServerTorrentData } from "rpc/torrent";

interface FormValues {
downloadLimited?: boolean,
Expand All @@ -37,16 +35,20 @@ interface FormValues {
seedRatioLimit: number,
seedIdleMode: number,
seedIdleLimit: number,
trackerList: string,
honorsSessionLimits: boolean,
sequentialDownload: boolean,
}

export function EditTorrent(props: ModalState) {
const config = useContext(ConfigContext);
const serverData = useServerTorrentData();
const torrentId = serverData.current;
const rpcVersion = useServerRpcVersion();
const selected = useServerSelectedTorrents();

const torrentId = useMemo(() => {
if (serverData.current === undefined || !selected.has(serverData.current)) {
return [...selected][0];
}
return serverData.current;
}, [selected, serverData]);

const { data: torrent, isLoading } = useTorrentDetails(
torrentId ?? -1, torrentId !== undefined && props.opened, false, true);
Expand All @@ -66,40 +68,22 @@ export function EditTorrent(props: ModalState) {
seedRatioLimit: torrent.seedRatioLimit,
seedIdleMode: torrent.seedIdleMode,
seedIdleLimit: torrent.seedIdleLimit,
trackerList: rpcVersion >= 17
? torrent.trackerList
: torrent.trackerStats.map((s: TrackerStats) => s.announce).join("\n"),
honorsSessionLimits: torrent.honorsSessionLimits,
sequentialDownload: torrent.sequentialDownload,
});
}, [rpcVersion, setValues, torrent]);
}, [setValues, torrent]);

const mutation = useMutateTorrent();

const onSave = useCallback(() => {
if (torrentId === undefined || torrent === undefined) return;
let toAdd;
let toRemove;
if (rpcVersion < 17) {
const trackers = form.values.trackerList.split("\n").filter((s) => s !== "");
const currentTrackers = Object.fromEntries(
torrent.trackerStats.map((s: TrackerStats) => [s.announce, s.id]));

toAdd = trackers.filter((t) => !Object.hasOwn(currentTrackers, t));
toRemove = (torrent.trackerStats as TrackerStats[])
.filter((s: TrackerStats) => !trackers.includes(s.announce))
.map((s: TrackerStats) => s.id as number);
if (toAdd.length === 0) toAdd = undefined;
if (toRemove.length === 0) toRemove = undefined;
}
mutation.mutate(
{
torrentIds: [torrentId],
torrentIds: [...selected],
fields: {
...form.values,
"peer-limit": form.values.peerLimit,
trackerAdd: toAdd,
trackerRemove: toRemove,
},
},
{
Expand All @@ -113,14 +97,7 @@ export function EditTorrent(props: ModalState) {
},
);
props.close();
}, [form.values, mutation, torrent, props, rpcVersion, torrentId]);

const addDefaultTrackers = useCallback(() => {
let list = form.values.trackerList;
if (!list.endsWith("\n")) list += "\n";
list += config.values.interface.defaultTrackers.join("\n");
form.setFieldValue("trackerList", list);
}, [config, form]);
}, [torrentId, torrent, mutation, selected, form.values, props]);

return <>{props.opened &&
<SaveCancelModal
Expand All @@ -135,7 +112,7 @@ export function EditTorrent(props: ModalState) {
<LoadingOverlay visible={isLoading} />
<Grid align="center">
<Grid.Col>
Torrent: {torrent?.name}
<TorrentsNames />
</Grid.Col>
<Grid.Col span={8}>
<Checkbox my="sm"
Expand Down Expand Up @@ -216,16 +193,6 @@ export function EditTorrent(props: ModalState) {
<Grid.Col span={2}>
minutes
</Grid.Col>
<Grid.Col span={8}>
<Text>Tracker list, one per line, empty line between tiers</Text>
</Grid.Col>
<Grid.Col span={4}>
<Button onClick={addDefaultTrackers}>Add default list</Button>
</Grid.Col>
<Grid.Col>
<Textarea minRows={6}
{...form.getInputProps("trackerList")} />
</Grid.Col>
</Grid>
</SaveCancelModal>}
</>;
Expand Down
137 changes: 137 additions & 0 deletions src/components/modals/edittrackers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* TrguiNG - next gen remote GUI for transmission torrent daemon
* Copyright (C) 2023 qu1ck (mail at qu1ck.org)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import React, { useCallback, useContext, useEffect, useMemo } from "react";
import type { ModalState } from "./common";
import { SaveCancelModal, TorrentsNames } from "./common";
import { useForm } from "@mantine/form";
import { useMutateTorrent, useTorrentDetails } from "queries";
import { notifications } from "@mantine/notifications";
import { Button, Grid, LoadingOverlay, Text, Textarea } from "@mantine/core";
import { ConfigContext } from "config";
import type { TrackerStats } from "rpc/torrent";
import { useServerRpcVersion, useServerSelectedTorrents, useServerTorrentData } from "rpc/torrent";

interface FormValues {
trackerList: string,
}

export function EditTrackers(props: ModalState) {
const rpcVersion = useServerRpcVersion();
const config = useContext(ConfigContext);
const serverData = useServerTorrentData();
const selected = useServerSelectedTorrents();

const torrentId = useMemo(() => {
if (serverData.current === undefined || !selected.has(serverData.current)) {
return [...selected][0];
}
return serverData.current;
}, [selected, serverData]);

const { data: torrent, isLoading } = useTorrentDetails(
torrentId ?? -1, torrentId !== undefined && props.opened, false, true);

const form = useForm<FormValues>({});

const { setValues } = form;
useEffect(() => {
if (torrent === undefined) return;
setValues({
trackerList: rpcVersion >= 17
? torrent.trackerList
: torrent.trackerStats.map((s: TrackerStats) => s.announce).join("\n"),
});
}, [rpcVersion, setValues, torrent]);

const mutation = useMutateTorrent();

const onSave = useCallback(() => {
if (torrentId === undefined || torrent === undefined) return;
let toAdd;
let toRemove;
if (rpcVersion < 17) {
const trackers = form.values.trackerList.split("\n").filter((s) => s !== "");
const currentTrackers = Object.fromEntries(
torrent.trackerStats.map((s: TrackerStats) => [s.announce, s.id]));

toAdd = trackers.filter((t) => !Object.prototype.hasOwnProperty.call(currentTrackers, t));
toRemove = (torrent.trackerStats as TrackerStats[])
.filter((s: TrackerStats) => !trackers.includes(s.announce))
.map((s: TrackerStats) => s.id as number);
if (toAdd.length === 0) toAdd = undefined;
if (toRemove.length === 0) toRemove = undefined;
}
mutation.mutate(
{
torrentIds: [...selected],
fields: {
...form.values,
trackerAdd: toAdd,
trackerRemove: toRemove,
},
},
{
onError: (e) => {
console.error("Failed to update torrent properties", e);
notifications.show({
message: "Error updating torrent",
color: "red",
});
},
},
);
props.close();
}, [torrentId, torrent, rpcVersion, mutation, selected, form.values, props]);

const addDefaultTrackers = useCallback(() => {
let list = form.values.trackerList;
if (!list.endsWith("\n")) list += "\n";
list += config.values.interface.defaultTrackers.join("\n");
form.setFieldValue("trackerList", list);
}, [config, form]);

return <>{props.opened &&
<SaveCancelModal
opened={props.opened}
size="lg"
onClose={props.close}
onSave={onSave}
centered
title="Edit torrent trackers"
mih="25rem"
>
<LoadingOverlay visible={isLoading} />
<Grid align="center">
<Grid.Col>
<TorrentsNames />
</Grid.Col>
<Grid.Col span={8}>
<Text>Tracker list, one per line, empty line between tiers</Text>
</Grid.Col>
<Grid.Col span={4}>
<Button onClick={addDefaultTrackers}>Add default list</Button>
</Grid.Col>
<Grid.Col>
<Textarea minRows={10}
{...form.getInputProps("trackerList")} />
</Grid.Col>
</Grid>
</SaveCancelModal>}
</>;
}
6 changes: 6 additions & 0 deletions src/components/modals/servermodals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { AddMagnet, AddTorrent } from "./add";
import { DaemonSettingsModal } from "./daemon";
import { EditTorrent } from "./edittorrent";
import type { ServerTabsRef } from "components/servertabs";
import { EditTrackers } from "./edittrackers";
const { TAURI, appWindow } = await import(/* webpackChunkName: "taurishim" */"taurishim");

export interface ModalCallbacks {
Expand All @@ -34,6 +35,7 @@ export interface ModalCallbacks {
addMagnet: () => void,
addTorrent: () => void,
daemonSettings: () => void,
editTrackers: () => void,
editTorrent: () => void,
}

Expand Down Expand Up @@ -67,6 +69,7 @@ const ServerModals = React.forwardRef<ModalCallbacks, ServerModalsProps>(functio
const [showRemoveModal, openRemoveModal, closeRemoveModal] = usePausingModalState(props.runUpdates);
const [showMoveModal, openMoveModal, closeMoveModal] = usePausingModalState(props.runUpdates);
const [showDaemonSettingsModal, openDaemonSettingsModal, closeDaemonSettingsModal] = usePausingModalState(props.runUpdates);
const [showEditTrackersModal, openEditTrackersModal, closeEditTrackersModal] = usePausingModalState(props.runUpdates);
const [showEditTorrentModal, openEditTorrentModal, closeEditTorrentModal] = usePausingModalState(props.runUpdates);

useImperativeHandle(ref, () => ({
Expand All @@ -76,6 +79,7 @@ const ServerModals = React.forwardRef<ModalCallbacks, ServerModalsProps>(functio
addMagnet: openAddMagnetModal,
addTorrent: openAddTorrentModal,
daemonSettings: openDaemonSettingsModal,
editTrackers: openEditTrackersModal,
editTorrent: openEditTorrentModal,
}));

Expand Down Expand Up @@ -189,6 +193,8 @@ const ServerModals = React.forwardRef<ModalCallbacks, ServerModalsProps>(functio
opened={showAddTorrentModal} close={closeAddTorrentModalAndPop} />
<DaemonSettingsModal
opened={showDaemonSettingsModal} close={closeDaemonSettingsModal} />
<EditTrackers
opened={showEditTrackersModal} close={closeEditTrackersModal} />
<EditTorrent
opened={showEditTorrentModal} close={closeEditTorrentModal} />
</>;
Expand Down
Loading

0 comments on commit 5b2e2e3

Please sign in to comment.