Skip to content

Commit 5b2e2e3

Browse files
committed
Split trackers from properties dialog, allow multi edit
Issue #76
1 parent 052785b commit 5b2e2e3

6 files changed

Lines changed: 188 additions & 72 deletions

File tree

src/components/modals/add.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import { Box, Button, Checkbox, Divider, Flex, Group, Menu, SegmentedControl, Text, TextInput } from "@mantine/core";
2020
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
2121
import type { ModalState, LocationData } from "./common";
22-
import { HkModal, TorrentLabels, TorrentLocation, limitTorrentNames, useTorrentLocation } from "./common";
22+
import { HkModal, LimitedNamesList, TorrentLabels, TorrentLocation, useTorrentLocation } from "./common";
2323
import type { PriorityNumberType } from "rpc/transmission";
2424
import { PriorityColors, PriorityStrings } from "rpc/transmission";
2525
import type { Torrent } from "rpc/torrent";
@@ -558,9 +558,7 @@ export function AddTorrent(props: AddCommonModalProps) {
558558
const names = useMemo(() => {
559559
if (torrentData === undefined) return [];
560560

561-
const names = torrentData.map((td) => td.name);
562-
563-
return limitTorrentNames(names, 1);
561+
return torrentData.map((td) => td.name);
564562
}, [torrentData]);
565563

566564
const torrentExists = existingTorrent !== undefined;
@@ -578,7 +576,7 @@ export function AddTorrent(props: AddCommonModalProps) {
578576
<Divider my="sm" />
579577
{torrentExists
580578
? <Text color="red" fw="bold" fz="lg">Torrent already exists</Text>
581-
: names.map((name, i) => <Text key={i}>{name}</Text>)}
579+
: <LimitedNamesList names={names} limit={1} />}
582580
<div style={{ position: "relative" }}>
583581
<AddCommon {...common.props} disabled={torrentExists}>
584582
{(torrentData.length > 1 || torrentData[0].files == null)

src/components/modals/common.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,21 @@ export function SaveCancelModal({ onSave, onClose, children, saveLoading, ...oth
6868
);
6969
}
7070

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

74-
if (allNames.length > limit) names.push(`... and ${allNames.length - limit} more`);
75-
76-
return names;
75+
return <>
76+
{t.map((s, i) => <Text key={i} mx="md" my="xs" px="sm" sx={{
77+
whiteSpace: "nowrap",
78+
overflow: "hidden",
79+
textOverflow: "ellipsis",
80+
boxShadow: "inset 0 0 0 9999px rgba(133, 133, 133, 0.1)",
81+
}}>
82+
{s}
83+
</Text>)}
84+
{names.length > limit && <Text mx="xl" mb="md">{`... and ${names.length - limit} more`}</Text>}
85+
</>;
7786
}
7887

7988
export function TorrentsNames() {
@@ -84,20 +93,11 @@ export function TorrentsNames() {
8493
if (serverData.current == null || serverSelected.size === 0) {
8594
return ["No torrent selected"];
8695
}
87-
88-
const selected = serverData.torrents.filter(
89-
(t) => serverSelected.has(t.id));
90-
91-
const allNames: string[] = [];
92-
selected.forEach((t) => allNames.push(t.name));
93-
return allNames;
96+
return serverData.torrents.filter(
97+
(t) => serverSelected.has(t.id)).map((t) => t.name);
9498
}, [serverData, serverSelected]);
9599

96-
const names = limitTorrentNames(allNames);
97-
98-
return <>
99-
{names.map((s, i) => <Text key={i} ml="xl" mb="md">{s}</Text>)}
100-
</>;
100+
return <LimitedNamesList names={allNames} />;
101101
}
102102

103103
export interface LocationData {

src/components/modals/edittorrent.tsx

Lines changed: 16 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,14 @@
1616
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
1818

19-
import React, { useCallback, useContext, useEffect } from "react";
19+
import React, { useCallback, useEffect, useMemo } from "react";
2020
import type { ModalState } from "./common";
21-
import { SaveCancelModal } from "./common";
21+
import { SaveCancelModal, TorrentsNames } from "./common";
2222
import { useForm } from "@mantine/form";
2323
import { useMutateTorrent, useTorrentDetails } from "queries";
2424
import { notifications } from "@mantine/notifications";
25-
import { Button, Checkbox, Grid, LoadingOverlay, NumberInput, Text, Textarea } from "@mantine/core";
26-
import { ConfigContext } from "config";
27-
import type { TrackerStats } from "rpc/torrent";
28-
import { useServerRpcVersion, useServerTorrentData } from "rpc/torrent";
25+
import { Checkbox, Grid, LoadingOverlay, NumberInput } from "@mantine/core";
26+
import { useServerSelectedTorrents, useServerTorrentData } from "rpc/torrent";
2927

3028
interface FormValues {
3129
downloadLimited?: boolean,
@@ -37,16 +35,20 @@ interface FormValues {
3735
seedRatioLimit: number,
3836
seedIdleMode: number,
3937
seedIdleLimit: number,
40-
trackerList: string,
4138
honorsSessionLimits: boolean,
4239
sequentialDownload: boolean,
4340
}
4441

4542
export function EditTorrent(props: ModalState) {
46-
const config = useContext(ConfigContext);
4743
const serverData = useServerTorrentData();
48-
const torrentId = serverData.current;
49-
const rpcVersion = useServerRpcVersion();
44+
const selected = useServerSelectedTorrents();
45+
46+
const torrentId = useMemo(() => {
47+
if (serverData.current === undefined || !selected.has(serverData.current)) {
48+
return [...selected][0];
49+
}
50+
return serverData.current;
51+
}, [selected, serverData]);
5052

5153
const { data: torrent, isLoading } = useTorrentDetails(
5254
torrentId ?? -1, torrentId !== undefined && props.opened, false, true);
@@ -66,40 +68,22 @@ export function EditTorrent(props: ModalState) {
6668
seedRatioLimit: torrent.seedRatioLimit,
6769
seedIdleMode: torrent.seedIdleMode,
6870
seedIdleLimit: torrent.seedIdleLimit,
69-
trackerList: rpcVersion >= 17
70-
? torrent.trackerList
71-
: torrent.trackerStats.map((s: TrackerStats) => s.announce).join("\n"),
7271
honorsSessionLimits: torrent.honorsSessionLimits,
7372
sequentialDownload: torrent.sequentialDownload,
7473
});
75-
}, [rpcVersion, setValues, torrent]);
74+
}, [setValues, torrent]);
7675

7776
const mutation = useMutateTorrent();
7877

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

88-
toAdd = trackers.filter((t) => !Object.hasOwn(currentTrackers, t));
89-
toRemove = (torrent.trackerStats as TrackerStats[])
90-
.filter((s: TrackerStats) => !trackers.includes(s.announce))
91-
.map((s: TrackerStats) => s.id as number);
92-
if (toAdd.length === 0) toAdd = undefined;
93-
if (toRemove.length === 0) toRemove = undefined;
94-
}
9581
mutation.mutate(
9682
{
97-
torrentIds: [torrentId],
83+
torrentIds: [...selected],
9884
fields: {
9985
...form.values,
10086
"peer-limit": form.values.peerLimit,
101-
trackerAdd: toAdd,
102-
trackerRemove: toRemove,
10387
},
10488
},
10589
{
@@ -113,14 +97,7 @@ export function EditTorrent(props: ModalState) {
11397
},
11498
);
11599
props.close();
116-
}, [form.values, mutation, torrent, props, rpcVersion, torrentId]);
117-
118-
const addDefaultTrackers = useCallback(() => {
119-
let list = form.values.trackerList;
120-
if (!list.endsWith("\n")) list += "\n";
121-
list += config.values.interface.defaultTrackers.join("\n");
122-
form.setFieldValue("trackerList", list);
123-
}, [config, form]);
100+
}, [torrentId, torrent, mutation, selected, form.values, props]);
124101

125102
return <>{props.opened &&
126103
<SaveCancelModal
@@ -135,7 +112,7 @@ export function EditTorrent(props: ModalState) {
135112
<LoadingOverlay visible={isLoading} />
136113
<Grid align="center">
137114
<Grid.Col>
138-
Torrent: {torrent?.name}
115+
<TorrentsNames />
139116
</Grid.Col>
140117
<Grid.Col span={8}>
141118
<Checkbox my="sm"
@@ -216,16 +193,6 @@ export function EditTorrent(props: ModalState) {
216193
<Grid.Col span={2}>
217194
minutes
218195
</Grid.Col>
219-
<Grid.Col span={8}>
220-
<Text>Tracker list, one per line, empty line between tiers</Text>
221-
</Grid.Col>
222-
<Grid.Col span={4}>
223-
<Button onClick={addDefaultTrackers}>Add default list</Button>
224-
</Grid.Col>
225-
<Grid.Col>
226-
<Textarea minRows={6}
227-
{...form.getInputProps("trackerList")} />
228-
</Grid.Col>
229196
</Grid>
230197
</SaveCancelModal>}
231198
</>;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* TrguiNG - next gen remote GUI for transmission torrent daemon
3+
* Copyright (C) 2023 qu1ck (mail at qu1ck.org)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published
7+
* by the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
import React, { useCallback, useContext, useEffect, useMemo } from "react";
20+
import type { ModalState } from "./common";
21+
import { SaveCancelModal, TorrentsNames } from "./common";
22+
import { useForm } from "@mantine/form";
23+
import { useMutateTorrent, useTorrentDetails } from "queries";
24+
import { notifications } from "@mantine/notifications";
25+
import { Button, Grid, LoadingOverlay, Text, Textarea } from "@mantine/core";
26+
import { ConfigContext } from "config";
27+
import type { TrackerStats } from "rpc/torrent";
28+
import { useServerRpcVersion, useServerSelectedTorrents, useServerTorrentData } from "rpc/torrent";
29+
30+
interface FormValues {
31+
trackerList: string,
32+
}
33+
34+
export function EditTrackers(props: ModalState) {
35+
const rpcVersion = useServerRpcVersion();
36+
const config = useContext(ConfigContext);
37+
const serverData = useServerTorrentData();
38+
const selected = useServerSelectedTorrents();
39+
40+
const torrentId = useMemo(() => {
41+
if (serverData.current === undefined || !selected.has(serverData.current)) {
42+
return [...selected][0];
43+
}
44+
return serverData.current;
45+
}, [selected, serverData]);
46+
47+
const { data: torrent, isLoading } = useTorrentDetails(
48+
torrentId ?? -1, torrentId !== undefined && props.opened, false, true);
49+
50+
const form = useForm<FormValues>({});
51+
52+
const { setValues } = form;
53+
useEffect(() => {
54+
if (torrent === undefined) return;
55+
setValues({
56+
trackerList: rpcVersion >= 17
57+
? torrent.trackerList
58+
: torrent.trackerStats.map((s: TrackerStats) => s.announce).join("\n"),
59+
});
60+
}, [rpcVersion, setValues, torrent]);
61+
62+
const mutation = useMutateTorrent();
63+
64+
const onSave = useCallback(() => {
65+
if (torrentId === undefined || torrent === undefined) return;
66+
let toAdd;
67+
let toRemove;
68+
if (rpcVersion < 17) {
69+
const trackers = form.values.trackerList.split("\n").filter((s) => s !== "");
70+
const currentTrackers = Object.fromEntries(
71+
torrent.trackerStats.map((s: TrackerStats) => [s.announce, s.id]));
72+
73+
toAdd = trackers.filter((t) => !Object.prototype.hasOwnProperty.call(currentTrackers, t));
74+
toRemove = (torrent.trackerStats as TrackerStats[])
75+
.filter((s: TrackerStats) => !trackers.includes(s.announce))
76+
.map((s: TrackerStats) => s.id as number);
77+
if (toAdd.length === 0) toAdd = undefined;
78+
if (toRemove.length === 0) toRemove = undefined;
79+
}
80+
mutation.mutate(
81+
{
82+
torrentIds: [...selected],
83+
fields: {
84+
...form.values,
85+
trackerAdd: toAdd,
86+
trackerRemove: toRemove,
87+
},
88+
},
89+
{
90+
onError: (e) => {
91+
console.error("Failed to update torrent properties", e);
92+
notifications.show({
93+
message: "Error updating torrent",
94+
color: "red",
95+
});
96+
},
97+
},
98+
);
99+
props.close();
100+
}, [torrentId, torrent, rpcVersion, mutation, selected, form.values, props]);
101+
102+
const addDefaultTrackers = useCallback(() => {
103+
let list = form.values.trackerList;
104+
if (!list.endsWith("\n")) list += "\n";
105+
list += config.values.interface.defaultTrackers.join("\n");
106+
form.setFieldValue("trackerList", list);
107+
}, [config, form]);
108+
109+
return <>{props.opened &&
110+
<SaveCancelModal
111+
opened={props.opened}
112+
size="lg"
113+
onClose={props.close}
114+
onSave={onSave}
115+
centered
116+
title="Edit torrent trackers"
117+
mih="25rem"
118+
>
119+
<LoadingOverlay visible={isLoading} />
120+
<Grid align="center">
121+
<Grid.Col>
122+
<TorrentsNames />
123+
</Grid.Col>
124+
<Grid.Col span={8}>
125+
<Text>Tracker list, one per line, empty line between tiers</Text>
126+
</Grid.Col>
127+
<Grid.Col span={4}>
128+
<Button onClick={addDefaultTrackers}>Add default list</Button>
129+
</Grid.Col>
130+
<Grid.Col>
131+
<Textarea minRows={10}
132+
{...form.getInputProps("trackerList")} />
133+
</Grid.Col>
134+
</Grid>
135+
</SaveCancelModal>}
136+
</>;
137+
}

src/components/modals/servermodals.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { AddMagnet, AddTorrent } from "./add";
2525
import { DaemonSettingsModal } from "./daemon";
2626
import { EditTorrent } from "./edittorrent";
2727
import type { ServerTabsRef } from "components/servertabs";
28+
import { EditTrackers } from "./edittrackers";
2829
const { TAURI, appWindow } = await import(/* webpackChunkName: "taurishim" */"taurishim");
2930

3031
export interface ModalCallbacks {
@@ -34,6 +35,7 @@ export interface ModalCallbacks {
3435
addMagnet: () => void,
3536
addTorrent: () => void,
3637
daemonSettings: () => void,
38+
editTrackers: () => void,
3739
editTorrent: () => void,
3840
}
3941

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

7275
useImperativeHandle(ref, () => ({
@@ -76,6 +79,7 @@ const ServerModals = React.forwardRef<ModalCallbacks, ServerModalsProps>(functio
7679
addMagnet: openAddMagnetModal,
7780
addTorrent: openAddTorrentModal,
7881
daemonSettings: openDaemonSettingsModal,
82+
editTrackers: openEditTrackersModal,
7983
editTorrent: openEditTorrentModal,
8084
}));
8185

@@ -189,6 +193,8 @@ const ServerModals = React.forwardRef<ModalCallbacks, ServerModalsProps>(functio
189193
opened={showAddTorrentModal} close={closeAddTorrentModalAndPop} />
190194
<DaemonSettingsModal
191195
opened={showDaemonSettingsModal} close={closeDaemonSettingsModal} />
196+
<EditTrackers
197+
opened={showEditTrackersModal} close={closeEditTrackersModal} />
192198
<EditTorrent
193199
opened={showEditTorrentModal} close={closeEditTorrentModal} />
194200
</>;

0 commit comments

Comments
 (0)