mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2026-01-10 09:24:59 -06:00
WebUI: Support creating new torrents
Implemented the torrent creator using WebAPI from #20366 in WebUI, the interface is mostly inspired by GUI and VueTorrent. Closes #5614. PR #22459.
This commit is contained in:
233
src/webui/www/private/views/createtorrent.html
Normal file
233
src/webui/www/private/views/createtorrent.html
Normal file
@@ -0,0 +1,233 @@
|
||||
<style>
|
||||
#createTorrentForm {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#createTorrentForm input[type="text"],
|
||||
#createTorrentForm textarea,
|
||||
#createTorrentForm table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#createTorrentForm fieldset {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#sourcePathBox {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
#sourcePath {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#createTorrentButton {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<form id="createTorrentForm" autocorrect="off" autocapitalize="none">
|
||||
<fieldset class="settings">
|
||||
<legend>QBT_TR(Select file/folder to share:)QBT_TR[CONTEXT=TorrentCreator]</legend>
|
||||
<div id="sourcePathBox">
|
||||
<label for="sourcePath">QBT_TR(Path:)QBT_TR[CONTEXT=TorrentCreator]</label>
|
||||
<input type="text" id="sourcePath" name="sourcePath" class="pathDirectory">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="settings">
|
||||
<legend>QBT_TR(Settings)QBT_TR[CONTEXT=TorrentCreator]</legend>
|
||||
<div id="torrentFormatBox">
|
||||
<label for="torrentFormat">QBT_TR(Torrent format:)QBT_TR[CONTEXT=TorrentCreator]</label>
|
||||
<select id="torrentFormat" name="format">
|
||||
<option value="v2">V2</option>
|
||||
<option selected value="hybrid">QBT_TR(Hybrid)QBT_TR[CONTEXT=TorrentCreator]</option>
|
||||
<option value="v1">V1</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="pieceSize">QBT_TR(Piece size:)QBT_TR[CONTEXT=TorrentCreator]</label>
|
||||
<select id="pieceSize" name="pieceSize">
|
||||
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<input type="hidden" id="privateTorrentHidden" name="private" value="0">
|
||||
<input type="checkbox" id="privateTorrent"><label for="privateTorrent">QBT_TR(Private
|
||||
torrent (Won't distribute on DHT network))QBT_TR[CONTEXT=TorrentCreator]</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="hidden" id="startSeedingHidden" name="startSeeding" value="0">
|
||||
<input type="checkbox" id="startSeeding"><label for="startSeeding">QBT_TR(Start
|
||||
seeding
|
||||
immediately)QBT_TR[CONTEXT=TorrentCreator]</label>
|
||||
</div>
|
||||
<fieldset id="optimizeAlignmentBox">
|
||||
<legend><input type="hidden" id="optimizeAlignmentHidden" name="optimizeAlignment" value="0"><input type="checkbox" id="optimizeAlignment"><label for="optimizeAlignment">QBT_TR(Optimize
|
||||
alignment)QBT_TR[CONTEXT=TorrentCreator]</label></legend>
|
||||
<label for="paddedFileSizeLimit">QBT_TR(Align to piece boundary for files larger
|
||||
than:)QBT_TR[CONTEXT=TorrentCreator]</label>
|
||||
<input type="number" id="paddedFileSizeLimit" name="paddedFileSizeLimit" disabled value="0">QBT_TR(KiB)QBT_TR[CONTEXT=OptionsDialog]
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
<fieldset class="settings">
|
||||
<legend>QBT_TR(Fields)QBT_TR[CONTEXT=TorrentCreator]</legend>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="trackerURLs">QBT_TR(Tracker URLs:)QBT_TR[CONTEXT=TorrentCreator]</label>
|
||||
</td>
|
||||
<td>
|
||||
<textarea rows="7" type="text" id="trackerURLs" name="trackers"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="webSeedURLs">QBT_TR(Web seed URLs:)QBT_TR[CONTEXT=TorrentCreator]</label>
|
||||
</td>
|
||||
<td>
|
||||
<textarea rows="7" type="text" id="webSeedURLs" name="urlSeeds"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="comments">QBT_TR(Comments:)QBT_TR[CONTEXT=TorrentCreator]</label>
|
||||
</td>
|
||||
<td>
|
||||
<textarea rows="7" type="text" id="comments" name="comment"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<label for="source">QBT_TR(Source:)QBT_TR[CONTEXT=TorrentCreator]</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="source" name="source">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</fieldset>
|
||||
<button type="submit" id="createTorrentButton">QBT_TR(Create Torrent)QBT_TR[CONTEXT=TorrentCreator]</button>
|
||||
</form>
|
||||
<div id="download_spinner" class="mochaSpinner"></div>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
window.qBittorrent ??= {};
|
||||
window.qBittorrent.CreateTorrent ??= (() => {
|
||||
const exports = () => {
|
||||
return {
|
||||
init: init,
|
||||
savePreferences: savePreferences,
|
||||
};
|
||||
};
|
||||
|
||||
const formatUrls = (urls) => {
|
||||
return urls.split("\n").map(encodeURIComponent).join("|");
|
||||
};
|
||||
|
||||
const createSizeOption = (size) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = size;
|
||||
option.textContent = (size === 0) ? "QBT_TR(Auto)QBT_TR[CONTEXT=TorrentCreator]" : window.qBittorrent.Misc.friendlyUnit(size, false);
|
||||
return option;
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
const pieceSizeSelect = document.getElementById("pieceSize");
|
||||
pieceSizeSelect.appendChild(createSizeOption(0));
|
||||
for (let i = 4; i <= 17; ++i)
|
||||
pieceSizeSelect.appendChild(createSizeOption(1024 << i));
|
||||
|
||||
const buildInfo = window.qBittorrent.Cache.buildInfo.get();
|
||||
const libtorrentVersion = window.qBittorrent.Misc.parseVersion(buildInfo.libtorrent);
|
||||
if (libtorrentVersion.valid) {
|
||||
if (libtorrentVersion.major >= 2) {
|
||||
document.getElementById("optimizeAlignmentBox").style.display = "none";
|
||||
}
|
||||
else {
|
||||
document.getElementById("torrentFormatBox").style.display = "none";
|
||||
document.getElementById("optimizeAlignment").addEventListener("change", (e) => {
|
||||
document.getElementById("paddedFileSizeLimit").disabled = !e.target.checked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("createTorrentForm").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
submit();
|
||||
});
|
||||
|
||||
loadPreference();
|
||||
window.qBittorrent.pathAutofill.attachPathAutofill();
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
document.getElementById("privateTorrentHidden").value = document.getElementById("privateTorrent").checked ? "true" : "false";
|
||||
document.getElementById("startSeedingHidden").value = document.getElementById("startSeeding").checked ? "true" : "false";
|
||||
document.getElementById("optimizeAlignmentHidden").value = document.getElementById("optimizeAlignment").checked ? "true" : "false";
|
||||
|
||||
document.getElementById("download_spinner").style.display = "block";
|
||||
|
||||
const formData = new FormData(document.getElementById("createTorrentForm"));
|
||||
if (formData.has("trackers"))
|
||||
formData.set("trackers", formatUrls(formData.get("trackers")));
|
||||
if (formData.has("urlSeeds"))
|
||||
formData.set("urlSeeds", formatUrls(formData.get("urlSeeds")));
|
||||
|
||||
fetch("api/v2/torrentcreator/addTask", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
}).then((response) => {
|
||||
if (!response.ok) {
|
||||
alert("QBT_TR(Unable to create torrent.)QBT_TR[CONTEXT=TorrentCreator]");
|
||||
return;
|
||||
}
|
||||
window.qBittorrent.Client.closeWindow(document.getElementById("createTorrentPage"));
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const savePreferences = () => {
|
||||
const preference = {
|
||||
sourcePath: document.getElementById("sourcePath").value,
|
||||
torrentFormat: document.getElementById("torrentFormat").value,
|
||||
pieceSize: document.getElementById("pieceSize").value,
|
||||
privateTorrent: document.getElementById("privateTorrent").checked,
|
||||
startSeeding: document.getElementById("startSeeding").checked,
|
||||
optimizeAlignment: document.getElementById("optimizeAlignment").checked,
|
||||
paddedFileSizeLimit: document.getElementById("paddedFileSizeLimit").value,
|
||||
trackerURLs: document.getElementById("trackerURLs").value,
|
||||
webSeedURLs: document.getElementById("webSeedURLs").value,
|
||||
comments: document.getElementById("comments").value,
|
||||
source: document.getElementById("source").value,
|
||||
};
|
||||
LocalPreferences.set("torrent_creator", JSON.stringify(preference));
|
||||
};
|
||||
|
||||
const loadPreference = () => {
|
||||
const preference = JSON.parse(LocalPreferences.get("torrent_creator") ?? "{}");
|
||||
document.getElementById("sourcePath").value = preference.sourcePath ?? "";
|
||||
document.getElementById("torrentFormat").value = preference.torrentFormat ?? "hybrid";
|
||||
document.getElementById("pieceSize").value = preference.pieceSize ?? 0;
|
||||
document.getElementById("privateTorrent").checked = preference.privateTorrent ?? false;
|
||||
document.getElementById("startSeeding").checked = preference.startSeeding ?? false;
|
||||
document.getElementById("optimizeAlignment").checked = preference.optimizeAlignment ?? false;
|
||||
document.getElementById("paddedFileSizeLimit").value = preference.paddedFileSizeLimit ?? 0;
|
||||
document.getElementById("trackerURLs").value = preference.trackerURLs ?? "";
|
||||
document.getElementById("webSeedURLs").value = preference.webSeedURLs ?? "";
|
||||
document.getElementById("comments").value = preference.comments ?? "";
|
||||
document.getElementById("source").value = preference.source ?? "";
|
||||
};
|
||||
|
||||
return exports();
|
||||
})();
|
||||
Object.freeze(window.qBittorrent.CreateTorrent);
|
||||
|
||||
window.qBittorrent.CreateTorrent.init();
|
||||
</script>
|
||||
282
src/webui/www/private/views/torrentcreator.html
Normal file
282
src/webui/www/private/views/torrentcreator.html
Normal file
@@ -0,0 +1,282 @@
|
||||
<style>
|
||||
#torrentCreatorTopBar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#addTaskButton {
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
#addTaskButton img {
|
||||
margin: 0 5px -3px 0;
|
||||
}
|
||||
|
||||
#torrentCreatorContainer {
|
||||
height: calc(100% - 20px);
|
||||
}
|
||||
|
||||
#torrentCreatorContentView {
|
||||
width: 100%;
|
||||
height: calc(100% - 16px);
|
||||
border: 1px solid var(--color-border-default);
|
||||
}
|
||||
|
||||
#torrentCreationTasksTableFixedHeaderDiv {
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div id="torrentCreatorContainer">
|
||||
<ul id="torrentCreationTasksTableMenu" class="contextMenu">
|
||||
<li><a href="#exportTorrent"><img src="images/edit-copy.svg" alt="QBT_TR(Download Torrent)QBT_TR[CONTEXT=TorrentCreator]">
|
||||
QBT_TR(Export Torrent)QBT_TR[CONTEXT=TorrentCreator]</a></li>
|
||||
<li><a href="#deleteTask"><img src="images/list-remove.svg" alt="QBT_TR(Remove Task)QBT_TR[CONTEXT=TorrentCreator]">
|
||||
QBT_TR(Remove Task)QBT_TR[CONTEXT=TorrentCreator]</a></li>
|
||||
</ul>
|
||||
<div id="torrentCreatorTopBar">
|
||||
<button type="button" id="addTaskButton">
|
||||
<img alt="QBT_TR(Create New Torrent)QBT_TR[CONTEXT=TorrentCreator]" src="images/list-add.svg" width="16" height="16">QBT_TR(Create New Torrent)QBT_TR[CONTEXT=TorrentCreator]
|
||||
</button>
|
||||
</div>
|
||||
<div id="torrentCreatorContentView">
|
||||
<div id="torrentCreationTasksTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
|
||||
<table class="dynamicTable" style="position:relative;">
|
||||
<thead>
|
||||
<tr class="dynamicTableHeader"></tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div id="torrentCreationTasksTableDiv" class="dynamicTableDiv">
|
||||
<table class="dynamicTable">
|
||||
<thead>
|
||||
<tr class="dynamicTableHeader"></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
window.qBittorrent ??= {};
|
||||
window.qBittorrent.TorrentCreator ??= (() => {
|
||||
const exports = () => {
|
||||
return {
|
||||
init: init,
|
||||
unload: unload,
|
||||
exportTorrents: exportTorrents,
|
||||
};
|
||||
};
|
||||
|
||||
let table;
|
||||
let contextMenu;
|
||||
let prevOffsetLeft;
|
||||
let prevOffsetTop;
|
||||
let timer = -1;
|
||||
|
||||
const init = () => {
|
||||
table = new window.qBittorrent.DynamicTable.TorrentCreationTasksTable();
|
||||
|
||||
contextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
|
||||
targets: "#torrentCreationTasksTableDiv",
|
||||
menu: "torrentCreationTasksTableMenu",
|
||||
actions: {
|
||||
deleteTask: () => {
|
||||
const selectedTasks = table.selectedRowsIds();
|
||||
if (selectedTasks.length === 0)
|
||||
return;
|
||||
|
||||
if (!confirm("QBT_TR(Are you sure you want to delete selected tasks?)QBT_TR[CONTEXT=TorrentCreator]"))
|
||||
return;
|
||||
|
||||
selectedTasks.forEach(task => {
|
||||
fetch("api/v2/torrentcreator/deleteTask", {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
taskID: task,
|
||||
})
|
||||
}).then((response) => {
|
||||
load();
|
||||
});
|
||||
});
|
||||
},
|
||||
exportTorrent: exportTorrents,
|
||||
},
|
||||
offsets: calculateContextMenuOffsets(),
|
||||
});
|
||||
contextMenu.updateMenuItems = () => {
|
||||
const selectedRows = table.selectedRowsIds();
|
||||
switch (selectedRows.length) {
|
||||
case 0:
|
||||
contextMenu.hideItem("exportTorrent");
|
||||
contextMenu.hideItem("deleteTask");
|
||||
break;
|
||||
case 1: {
|
||||
const row = table.getRow(selectedRows[0]);
|
||||
if (row.full_data.status === "Finished")
|
||||
contextMenu.showItem("exportTorrent");
|
||||
else
|
||||
contextMenu.hideItem("exportTorrent");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
contextMenu.showItem("exportTorrent");
|
||||
contextMenu.showItem("deleteTask");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
table.setup("torrentCreationTasksTableDiv", "torrentCreationTasksTableFixedHeaderDiv", contextMenu);
|
||||
|
||||
table.dynamicTableDiv.addEventListener("contextmenu", (e) => {
|
||||
updateContextMenuOffset();
|
||||
}, true);
|
||||
|
||||
document.getElementById("addTaskButton").addEventListener("click", (event) => {
|
||||
showCreateTorrentPage();
|
||||
});
|
||||
|
||||
load();
|
||||
};
|
||||
|
||||
const calculateContextMenuOffsets = () => {
|
||||
prevOffsetLeft = document.getElementById("torrentCreatorPage").getBoundingClientRect().left;
|
||||
prevOffsetTop = document.getElementById("torrentCreatorPage").getBoundingClientRect().top;
|
||||
|
||||
return {
|
||||
x: -prevOffsetLeft,
|
||||
y: -prevOffsetTop
|
||||
};
|
||||
};
|
||||
|
||||
const updateContextMenuOffset = () => {
|
||||
// only re-calculate if window has moved
|
||||
if ((prevOffsetLeft !== document.getElementById("torrentCreatorPage").getBoundingClientRect().left) || (prevOffsetTop !== document.getElementById("torrentCreatorPage").getBoundingClientRect().top))
|
||||
contextMenu.options.offsets = calculateContextMenuOffsets();
|
||||
};
|
||||
|
||||
const showCreateTorrentPage = () => {
|
||||
const id = "createTorrentPage";
|
||||
|
||||
new MochaUI.Window({
|
||||
id: id,
|
||||
icon: "images/list-add.svg",
|
||||
title: "QBT_TR(Create New Torrent)QBT_TR[CONTEXT=TorrentCreator]",
|
||||
loadMethod: "xhr",
|
||||
contentURL: "views/createtorrent.html",
|
||||
scrollbars: true,
|
||||
maximizable: false,
|
||||
closable: true,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0,
|
||||
width: loadWindowWidth(id, 500),
|
||||
height: loadWindowHeight(id, 600),
|
||||
onResize: () => {
|
||||
saveWindowSize(id);
|
||||
},
|
||||
onClose: () => {
|
||||
window.qBittorrent.CreateTorrent.savePreferences();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const exportTorrents = async () => {
|
||||
const selectedTasks = table.selectedRowsIds();
|
||||
if (selectedTasks.length === 0)
|
||||
return;
|
||||
|
||||
for (const task of selectedTasks) {
|
||||
const row = table.getRow(task);
|
||||
if (row.full_data.status !== "Finished")
|
||||
continue;
|
||||
|
||||
const url = new URL("api/v2/torrentcreator/torrentFile", window.location);
|
||||
url.search = new URLSearchParams({
|
||||
taskID: task
|
||||
});
|
||||
|
||||
// download response to file
|
||||
await window.qBittorrent.Misc.downloadFile(url, `${task}.torrent`, "QBT_TR(Unable to export torrent file)QBT_TR[CONTEXT=TorrentCreator]");
|
||||
|
||||
// https://stackoverflow.com/questions/53560991/automatic-file-downloads-limited-to-10-files-on-chrome-browser
|
||||
await window.qBittorrent.Misc.sleep(200);
|
||||
}
|
||||
};
|
||||
|
||||
const load = () => {
|
||||
syncTaskWithInterval(100);
|
||||
};
|
||||
|
||||
const unload = () => {
|
||||
clearTimeout(timer);
|
||||
timer = -1;
|
||||
table = null;
|
||||
contextMenu = null;
|
||||
};
|
||||
|
||||
const syncTaskWithInterval = (interval) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(syncTaskData, interval);
|
||||
};
|
||||
|
||||
const syncTaskData = () => {
|
||||
fetch("api/v2/torrentcreator/status", {
|
||||
method: "GET",
|
||||
cache: "no-store"
|
||||
}).then(async (response) => {
|
||||
if (!response.ok) {
|
||||
let error = "QBT_TR(Unable to load torrent creation tasks)QBT_TR[CONTEXT=TorrentCreator]";
|
||||
const responseText = await response.text();
|
||||
if (responseText.length > 0)
|
||||
error += `: ${responseText}`;
|
||||
alert(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Awaiting the json before clearing the table to prevent flickering
|
||||
const responseJSON = await response.json();
|
||||
|
||||
const selectedTasks = table.selectedRowsIds();
|
||||
table.clear();
|
||||
|
||||
if (responseJSON.length > 0) {
|
||||
for (let i = 0; i < responseJSON.length; ++i) {
|
||||
const status = responseJSON[i].status;
|
||||
const row = {
|
||||
rowId: responseJSON[i].taskID,
|
||||
source_path: responseJSON[i].sourcePath,
|
||||
progress: status === "Finished" ? 100 : responseJSON[i].progress,
|
||||
status: status,
|
||||
torrent_format: responseJSON[i].format,
|
||||
piece_size: responseJSON[i].pieceSize,
|
||||
private: responseJSON[i].private,
|
||||
added_on: responseJSON[i].timeAdded,
|
||||
start_on: responseJSON[i].timeStarted,
|
||||
completion_on: responseJSON[i].timeFinished,
|
||||
trackers: responseJSON[i].trackers,
|
||||
comment: responseJSON[i].comment,
|
||||
source: responseJSON[i].source,
|
||||
error_message: responseJSON[i].errorMessage,
|
||||
};
|
||||
|
||||
table.updateRowData(row);
|
||||
}
|
||||
|
||||
table.updateTable(false);
|
||||
|
||||
if (selectedTasks.length > 0)
|
||||
table.reselectRows(selectedTasks);
|
||||
}
|
||||
}).finally(() => {
|
||||
syncTaskWithInterval(serverSyncMainDataInterval);
|
||||
});
|
||||
};
|
||||
|
||||
return exports();
|
||||
})();
|
||||
Object.freeze(window.qBittorrent.TorrentCreator);
|
||||
|
||||
window.qBittorrent.TorrentCreator.init();
|
||||
</script>
|
||||
Reference in New Issue
Block a user