Skip to content

Commit 692d005

Browse files
committed
implement qbt client-specific configurable setting
1 parent c9b6e77 commit 692d005

File tree

6 files changed

+116
-6
lines changed

6 files changed

+116
-6
lines changed

src/models/torrent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ export interface TorrentUploadConfig {
1616
dir?: string;
1717
label?: string;
1818
addPaused?: boolean;
19+
// Arbitrary per-client keys and values, implementations may read these when handling uploads
20+
clientSpecificSettings?: Record<string, any>;
1921
}

src/models/webui.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,16 @@ export interface WebUISettings {
2929

3030
export abstract class TorrentWebUI {
3131
_settings: WebUISettings;
32+
// Static per-implementation defaults. Implementations may override this.
33+
// Example: class RuTorrentWebUI extends TorrentWebUI { static clientSpecificDefaults = { dontAddNamePath: false } }
34+
static clientSpecificDefaults: Record<string, any> = {};
3235

3336
constructor(settings: WebUISettings) {
3437
this._settings = settings;
38+
// Ensure the settings record exists
39+
this._settings.clientSpecificSettings = this._settings.clientSpecificSettings || {};
40+
// Apply defaults declared on the concrete implementation (if any)
41+
this.ensureClientSpecificDefaults();
3542
}
3643

3744
get name(): string {
@@ -99,6 +106,37 @@ export abstract class TorrentWebUI {
99106
return config.addPaused ?? this.settings.addPaused ?? false;
100107
}
101108

109+
// Client-specific settings API
110+
// Get the raw stored client-specific value (if any)
111+
public getClientSpecific<T = any>(key: string): T | undefined {
112+
return this._settings.clientSpecificSettings ? this._settings.clientSpecificSettings[key] as T : undefined;
113+
}
114+
115+
// Get stored value or fallback to implementation default (if declared)
116+
public getClientSpecificOrDefault<T = any>(key: string): T | undefined {
117+
const stored = this.getClientSpecific<T>(key);
118+
if (stored !== undefined) return stored;
119+
const ctor = this.constructor as typeof TorrentWebUI;
120+
return (ctor.clientSpecificDefaults ? ctor.clientSpecificDefaults[key] : undefined) as T | undefined;
121+
}
122+
123+
// Set and persist a client-specific setting into WebUISettings.clientSpecificSettings
124+
public setClientSpecific<T = any>(key: string, value: T): void {
125+
this._settings.clientSpecificSettings = this._settings.clientSpecificSettings || {};
126+
this._settings.clientSpecificSettings[key] = value;
127+
}
128+
129+
// Initialize missing keys from the concrete implementation defaults
130+
protected ensureClientSpecificDefaults(): void {
131+
const ctor = this.constructor as typeof TorrentWebUI;
132+
const defaults = ctor.clientSpecificDefaults || {};
133+
for (const k of Object.keys(defaults)) {
134+
if (this._settings.clientSpecificSettings[k] === undefined) {
135+
this._settings.clientSpecificSettings[k] = defaults[k];
136+
}
137+
}
138+
}
139+
102140
}
103141

104142
export interface TorrentAddingResult {

src/options/pages/WebUIsPage.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,39 @@ function WebUIEditor({ webui, onChange, onRemove, onPromote, isPrimary }: WebUIE
201201
dirs={webui.dirs}
202202
/>
203203
)}
204-
{/* <div style={{ marginTop: 16, color: "var(--rta-text-muted, #888)" }}>
205-
<div><b>ClientSpecificSettings:</b> <span>TODO: implement</span></div>
206-
</div> */}
204+
{/* Client-specific settings editor (uses defaults declared by the concrete WebUI class) */}
205+
<div style={{ marginTop: 16 }}>
206+
<label style={{ fontWeight: 600, display: "block", marginBottom: 8 }}>Client-specific settings</label>
207+
<div style={{ color: "var(--rta-text-muted, #888)", padding: 8, borderRadius: 8, border: "1px dashed var(--rta-border, #b7c9a7)" }}>
208+
{(() => {
209+
const ctor = (webUiInstance?.constructor as any) || {};
210+
const defaults: Record<string, any> = ctor.clientSpecificDefaults || {};
211+
const stored: Record<string, any> = webui.clientSpecificSettings || {};
212+
const keys = Array.from(new Set([...Object.keys(defaults), ...Object.keys(stored)]));
213+
if (keys.length === 0) {
214+
return <div style={{ color: "var(--rta-text-muted, #888)" }}>No client-specific settings declared for this client.</div>;
215+
}
216+
return keys.map(key => {
217+
const def = defaults[key];
218+
const val = stored[key] !== undefined ? stored[key] : def;
219+
const type = def !== undefined ? typeof def : typeof val;
220+
if (type === 'boolean') {
221+
return (
222+
<div key={key} style={{ marginBottom: 8 }}>
223+
<Toggle checked={!!val} onChange={v => onChange({ ...webui, clientSpecificSettings: { ...(webui.clientSpecificSettings || {}), [key]: v } })} label={key} />
224+
</div>
225+
);
226+
}
227+
return (
228+
<div key={key} style={{ marginBottom: 8 }}>
229+
<label style={{ fontWeight: 500, marginBottom: 4, display: "block" }}>{key}</label>
230+
<input type="text" value={val ?? ''} onChange={e => onChange({ ...webui, clientSpecificSettings: { ...(webui.clientSpecificSettings || {}), [key]: e.target.value } })} style={{ fontSize: 15, borderRadius: 8, padding: "6px 12px", border: "1px solid var(--rta-border, #b7c9a7)", background: "var(--rta-input-bg, #fff)", color: "var(--rta-text, #1b241d)", minWidth: 180 }} />
231+
</div>
232+
);
233+
});
234+
})()}
235+
</div>
236+
</div>
207237
</div>
208238
);
209239
}

src/popup/app/page.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Toggle } from '../components/ui/toggle';
77
import { Torrent } from '../../models/torrent';
88

99

10-
export type AddTorrentCallback = (webUiId: string, torrent: Torrent, label: string, dir: string, paused: boolean, labelOptions: string[], directoryOptions: string[]) => Promise<void>;
10+
export type AddTorrentCallback = (webUiId: string, torrent: Torrent, label: string, dir: string, paused: boolean, labelOptions: string[], directoryOptions: string[], clientSpecificSettings: Record<string, any>) => Promise<void>;
1111

1212
// Initial options
1313
const initialLabelOptions = ['Movies', 'TV Shows', 'Music', 'Games', 'Software', 'Books']
@@ -25,6 +25,7 @@ export default function Home() {
2525
const [label, setLabel] = useState('')
2626
const [directory, setDirectory] = useState('')
2727
const [paused, setPaused] = useState(false)
28+
const [clientSpecificSettings, setClientSpecificSettings] = useState<Record<string, any>>({});
2829

2930
// Dynamic options state
3031
const [labelOptions, setLabelOptions] = useState(initialLabelOptions)
@@ -39,7 +40,7 @@ export default function Home() {
3940
const handleSubmit = () => {
4041
const augmentedLabels = label && !labelOptions.includes(label) ? [label, ...labelOptions] : labelOptions;
4142
const augmentedDirectories = directory && !directoryOptions.includes(directory) ? [directory, ...directoryOptions] : directoryOptions;
42-
addTorrentCallback(webUi.id, torrent, label || null, directory || null, paused || false, augmentedLabels, augmentedDirectories).then(() => {
43+
addTorrentCallback(webUi.id, torrent, label || null, directory || null, paused || false, augmentedLabels, augmentedDirectories, clientSpecificSettings).then(() => {
4344
window.close();
4445
});
4546
}
@@ -60,6 +61,7 @@ export default function Home() {
6061
paused: setShowPaused
6162
},
6263
webUiSettings: setWebUi,
64+
clientSpecificSettings: setClientSpecificSettings,
6365
addTorrentCb: (callback: AddTorrentCallback) => {
6466
setAddTorrentCallback(() => callback);
6567
}
@@ -128,6 +130,28 @@ export default function Home() {
128130
/>
129131
)}
130132

133+
{/* client-specific settings UI */}
134+
{Object.keys(clientSpecificSettings || {}).map(key => {
135+
const val = clientSpecificSettings[key];
136+
if (typeof val === 'boolean') {
137+
return (
138+
<Toggle
139+
key={key}
140+
label={key}
141+
checked={!!val}
142+
onChange={(v) => setClientSpecificSettings(prev => ({ ...prev, [key]: v }))}
143+
/>
144+
)
145+
}
146+
// default to text input for string/other
147+
return (
148+
<div key={key} className="space-y-1">
149+
<label className="text-sm font-medium">{key}</label>
150+
<input className="w-full rounded-md border" value={val ?? ''} onChange={e => setClientSpecificSettings(prev => ({ ...prev, [key]: e.target.value }))} />
151+
</div>
152+
)
153+
})}
154+
131155
<Button
132156
onClick={handleSubmit}
133157
className="w-full"
@@ -157,5 +181,6 @@ export interface FormControl {
157181
paused: (visible: boolean) => void;
158182
};
159183
webUiSettings: (webUiSettings: WebUISettings) => void;
184+
clientSpecificSettings: (settings: Record<string, any>) => void;
160185
addTorrentCb: (callback: AddTorrentCallback) => void;
161186
}

src/popup/chrome-messaging.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ function setPopupStateForMessage(message: IGetPreAddedTorrentAndSettingsResponse
3636
popupControl.visibility.paused(webUi.isAddPausedSupported);
3737

3838
popupControl.webUiSettings(message.webUiSettings);
39+
// initialize client-specific settings for this web UI in the popup
40+
popupControl.clientSpecificSettings({ ...(message.webUiSettings.clientSpecificSettings || {}) });
3941

4042
popupControl.addTorrentCb(sendAddTorrentAndLabelDirSettingsMessage);
4143
}
4244

43-
function sendAddTorrentAndLabelDirSettingsMessage(webUiId: string, torrent: Torrent, label: string, dir: string, paused: boolean, labelOptions: string[], directoryOptions: string[]): Promise<void> {
45+
function sendAddTorrentAndLabelDirSettingsMessage(webUiId: string, torrent: Torrent, label: string, dir: string, paused: boolean, labelOptions: string[], directoryOptions: string[], clientSpecificSettings: Record<string, any>): Promise<void> {
4446
return new Promise((resolve, reject) => {
4547
convertTorrentToSerialized(torrent).then((serializedTorrent: SerializedTorrent) => {
4648
chrome.runtime.sendMessage({
@@ -51,6 +53,7 @@ function sendAddTorrentAndLabelDirSettingsMessage(webUiId: string, torrent: Torr
5153
label,
5254
dir,
5355
addPaused: paused,
56+
clientSpecificSettings: clientSpecificSettings || {}
5457
} as TorrentUploadConfig,
5558
labels: labelOptions,
5659
directories: directoryOptions

src/webuis/qbittorrent-webui.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { Torrent, TorrentUploadConfig } from "../models/torrent";
22
import { TorrentAddingResult, TorrentWebUI } from "../models/webui";
33

44
export class QBittorrentWebUI extends TorrentWebUI {
5+
// Default client-specific settings for qBittorrent
6+
static override clientSpecificDefaults: Record<string, any> = {
7+
// Force Start: when true, add torrent with force start mode
8+
forceStart: false,
9+
};
510
public override async sendTorrent(torrent: Torrent, config: TorrentUploadConfig): Promise<TorrentAddingResult> {
611
return new Promise((resolve, reject) => {
712
const url = this.createBaseUrl() + "/api/v2/torrents/add";
@@ -63,6 +68,13 @@ export class QBittorrentWebUI extends TorrentWebUI {
6368
fetchOpts["body"].append("stopped", this.getAddPaused(config).toString());
6469
}
6570

71+
// qBittorrent supports a "force_start" parameter when adding torrents
72+
const forceStart = this.getClientSpecificOrDefault<boolean>("forceStart");
73+
if (forceStart) {
74+
// API accepts 'force_start' as 'true' / 'false' or '1' / '0'
75+
fetchOpts["body"].append("force_start", "true");
76+
}
77+
6678
resolve(fetchOpts);
6779
});
6880
}

0 commit comments

Comments
 (0)