Implementing a streaming setup with Usenet
This commit is contained in:
@@ -400,7 +400,7 @@ class DecypharrUtils {
|
||||
|
||||
if (data.channel === 'beta') {
|
||||
versionBadge.classList.add('badge-warning');
|
||||
} else if (data.channel === 'nightly') {
|
||||
} else if (data.channel === 'experimental') {
|
||||
versionBadge.classList.add('badge-error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ class ConfigManager {
|
||||
constructor() {
|
||||
this.debridCount = 0;
|
||||
this.arrCount = 0;
|
||||
this.usenetProviderCount = 0;
|
||||
this.debridDirectoryCounts = {};
|
||||
this.directoryFilterCounts = {};
|
||||
|
||||
@@ -11,8 +12,10 @@ class ConfigManager {
|
||||
loadingOverlay: document.getElementById('loadingOverlay'),
|
||||
debridConfigs: document.getElementById('debridConfigs'),
|
||||
arrConfigs: document.getElementById('arrConfigs'),
|
||||
usenetConfigs: document.getElementById('usenetConfigs'),
|
||||
addDebridBtn: document.getElementById('addDebridBtn'),
|
||||
addArrBtn: document.getElementById('addArrBtn')
|
||||
addArrBtn: document.getElementById('addArrBtn'),
|
||||
addUsenetBtn: document.getElementById('addUsenetBtn')
|
||||
};
|
||||
|
||||
this.init();
|
||||
@@ -40,6 +43,7 @@ class ConfigManager {
|
||||
// Add buttons
|
||||
this.refs.addDebridBtn.addEventListener('click', () => this.addDebridConfig());
|
||||
this.refs.addArrBtn.addEventListener('click', () => this.addArrConfig());
|
||||
this.refs.addUsenetBtn.addEventListener('click', () => this.addUsenetConfig());
|
||||
|
||||
// WebDAV toggle handlers
|
||||
document.addEventListener('change', (e) => {
|
||||
@@ -82,6 +86,12 @@ class ConfigManager {
|
||||
config.arrs.forEach(arr => this.addArrConfig(arr));
|
||||
}
|
||||
|
||||
// Load usenet config
|
||||
this.populateUsenetSettings(config.usenet);
|
||||
|
||||
// Load SABnzbd config
|
||||
this.populateSABnzbdSettings(config.sabnzbd);
|
||||
|
||||
// Load repair config
|
||||
this.populateRepairSettings(config.repair);
|
||||
}
|
||||
@@ -139,6 +149,26 @@ class ConfigManager {
|
||||
});
|
||||
}
|
||||
|
||||
populateUsenetSettings(usenetConfig) {
|
||||
if (!usenetConfig) return;
|
||||
// Populate general Usenet settings
|
||||
let fields = ["mount_folder", "chunks", "skip_pre_cache", "rc_url", "rc_user", "rc_pass"];
|
||||
fields.forEach(field => {
|
||||
const element = document.querySelector(`[name="usenet.${field}"]`);
|
||||
if (element && usenetConfig[field] !== undefined) {
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = usenetConfig[field];
|
||||
} else {
|
||||
element.value = usenetConfig[field];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (usenetConfig.providers && Array.isArray(usenetConfig.providers)) {
|
||||
usenetConfig.providers.forEach(usenet => this.addUsenetConfig(usenet));
|
||||
}
|
||||
}
|
||||
|
||||
addDebridConfig(data = {}) {
|
||||
const debridHtml = this.getDebridTemplate(this.debridCount, data);
|
||||
this.refs.debridConfigs.insertAdjacentHTML('beforeend', debridHtml);
|
||||
@@ -228,7 +258,7 @@ class ConfigManager {
|
||||
<span class="label-text font-medium">API Key</span>
|
||||
</label>
|
||||
<div class="password-toggle-container">
|
||||
<input type="password" class="input input-bordered input-has-toggle"
|
||||
<input autocomplete="off" type="password" class="input input-bordered input-has-toggle"
|
||||
name="debrid[${index}].api_key" id="debrid[${index}].api_key" required>
|
||||
<button type="button" class="password-toggle-btn">
|
||||
<i class="bi bi-eye" id="debrid[${index}].api_key_icon"></i>
|
||||
@@ -448,7 +478,7 @@ class ConfigManager {
|
||||
<span class="label-text font-medium">RC Password</span>
|
||||
</label>
|
||||
<div class="password-toggle-container">
|
||||
<input type="password" class="input input-bordered webdav-field input-has-toggle"
|
||||
<input autocomplete="off" type="password" class="input input-bordered webdav-field input-has-toggle"
|
||||
name="debrid[${index}].rc_pass" id="debrid[${index}].rc_pass">
|
||||
<button type="button" class="password-toggle-btn">
|
||||
<i class="bi bi-eye" id="debrid[${index}].rc_pass_icon"></i>
|
||||
@@ -745,9 +775,9 @@ class ConfigManager {
|
||||
modal.className = 'modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-box max-w-2xl">
|
||||
<form method="dialog">
|
||||
<div method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
</div>
|
||||
<h3 class="font-bold text-lg mb-4">Directory Filter Types</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
@@ -779,7 +809,7 @@ class ConfigManager {
|
||||
<li>Examples: 24h, 7d, 30d</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<span>Negative filters (Not...) will exclude matches instead of including them.</span>
|
||||
</div>
|
||||
@@ -868,7 +898,7 @@ class ConfigManager {
|
||||
<span class="label-text font-medium">API Token</span>
|
||||
</label>
|
||||
<div class="password-toggle-container">
|
||||
<input type="password" class="input input-bordered input-has-toggle ${isAutoDetected ? 'input-disabled' : ''}"
|
||||
<input autocomplete="off" type="password" class="input input-bordered input-has-toggle ${isAutoDetected ? 'input-disabled' : ''}"
|
||||
name="arr[${index}].token" id="arr[${index}].token"
|
||||
${isAutoDetected ? 'readonly' : 'required'}>
|
||||
<button type="button" class="password-toggle-btn ${isAutoDetected ? 'opacity-50 cursor-not-allowed' : ''}"
|
||||
@@ -882,7 +912,7 @@ class ConfigManager {
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="arr[${index}].selected_debrid">
|
||||
<span class="label-text font-medium">Preferred Debrid Service</span>
|
||||
<span class="label-text font-medium">Preferred Service</span>
|
||||
</label>
|
||||
<select class="select select-bordered" name="arr[${index}].selected_debrid" id="arr[${index}].selected_debrid">
|
||||
<option value="" selected>Auto-select</option>
|
||||
@@ -890,6 +920,7 @@ class ConfigManager {
|
||||
<option value="alldebrid">AllDebrid</option>
|
||||
<option value="debrid_link">Debrid Link</option>
|
||||
<option value="torbox">Torbox</option>
|
||||
<option value="usenet">Usenet</option>
|
||||
</select>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Which debrid service this Arr should prefer</span>
|
||||
@@ -990,6 +1021,23 @@ class ConfigManager {
|
||||
}
|
||||
});
|
||||
|
||||
// Validate Usenet servers
|
||||
if (config.usenet) {
|
||||
config.usenet.providers.forEach((usenet, index) => {
|
||||
if (!usenet.host) {
|
||||
errors.push(`Usenet server #${index + 1}: Host is required`);
|
||||
}
|
||||
|
||||
if (usenet.port && (usenet.port < 1 || usenet.port > 65535)) {
|
||||
errors.push(`Usenet server #${index + 1}: Port must be between 1 and 65535`);
|
||||
}
|
||||
|
||||
if (usenet.connections && (usenet.connections < 1 )) {
|
||||
errors.push(`Usenet server #${index + 1}: Connections must be more than 0`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Validate repair settings
|
||||
if (config.repair.enabled) {
|
||||
if (!config.repair.interval) {
|
||||
@@ -1038,6 +1086,12 @@ class ConfigManager {
|
||||
// Arr configurations
|
||||
arrs: this.collectArrConfigs(),
|
||||
|
||||
// Usenet configurations
|
||||
usenet: this.collectUsenetConfig(),
|
||||
|
||||
// SABnzbd configuration
|
||||
sabnzbd: this.collectSABnzbdConfig(),
|
||||
|
||||
// Repair configuration
|
||||
repair: this.collectRepairConfig()
|
||||
};
|
||||
@@ -1153,6 +1207,211 @@ class ConfigManager {
|
||||
return arrs;
|
||||
}
|
||||
|
||||
addUsenetConfig(data = {}) {
|
||||
const usenetHtml = this.getUsenetTemplate(this.usenetProviderCount, data);
|
||||
this.refs.usenetConfigs.insertAdjacentHTML('beforeend', usenetHtml);
|
||||
|
||||
// Populate data if provided
|
||||
if (Object.keys(data).length > 0) {
|
||||
this.populateUsenetData(this.usenetProviderCount, data);
|
||||
}
|
||||
|
||||
this.usenetProviderCount++;
|
||||
}
|
||||
|
||||
populateUsenetData(index, data) {
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
const input = document.querySelector(`[name="usenet[${index}].${key}"]`);
|
||||
if (input) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = value;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getUsenetTemplate(index, data = {}) {
|
||||
return `
|
||||
<div class="card bg-base-100 border border-base-300 shadow-sm usenet-config" data-index="${index}">
|
||||
<div class="card-body">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<h3 class="card-title text-lg">
|
||||
<i class="bi bi-globe mr-2 text-info"></i>
|
||||
Usenet Server #${index + 1}
|
||||
</h3>
|
||||
<button type="button" class="btn btn-error btn-sm" onclick="this.closest('.usenet-config').remove();">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="usenet[${index}].name">
|
||||
<span class="label-text font-medium">Name</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered"
|
||||
name="usenet[${index}].name" id="usenet[${index}].name"
|
||||
placeholder="provider name, e.g easynews" required>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Usenet Name</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="usenet[${index}].host">
|
||||
<span class="label-text font-medium">Host</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered"
|
||||
name="usenet[${index}].host" id="usenet[${index}].host"
|
||||
placeholder="news.provider.com" required>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Usenet server hostname</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="usenet[${index}].port">
|
||||
<span class="label-text font-medium">Port</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered"
|
||||
name="usenet[${index}].port" id="usenet[${index}].port"
|
||||
placeholder="119" value="119" min="1" max="65535">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Server port (119 for standard, 563 for SSL)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="usenet[${index}].connections">
|
||||
<span class="label-text font-medium">Connections</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered"
|
||||
name="usenet[${index}].connections" id="usenet[${index}].connections"
|
||||
placeholder="30" value="30" min="1" max="50">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Maximum simultaneous connections</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="usenet[${index}].username">
|
||||
<span class="label-text font-medium">Username</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered"
|
||||
name="usenet[${index}].username" id="usenet[${index}].username">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Username for authentication</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="usenet[${index}].password">
|
||||
<span class="label-text font-medium">Password</span>
|
||||
</label>
|
||||
<div class="password-toggle-container">
|
||||
<input autocomplete="off" type="password" class="input input-bordered input-has-toggle"
|
||||
name="usenet[${index}].password" id="usenet[${index}].password">
|
||||
<button type="button" class="password-toggle-btn">
|
||||
<i class="bi bi-eye" id="usenet[${index}].password_icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Password for authentication</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mt-4">
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox"
|
||||
name="usenet[${index}].ssl" id="usenet[${index}].ssl">
|
||||
<span class="label-text font-medium">Use SSL</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Use SSL encryption</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-2">
|
||||
<input type="checkbox" class="checkbox"
|
||||
name="usenet[${index}].use_tls" id="usenet[${index}].use_tls">
|
||||
<span class="label-text font-medium">Use TLS</span>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Use TLS encryption</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
populateSABnzbdSettings(sabnzbdConfig) {
|
||||
if (!sabnzbdConfig) return;
|
||||
|
||||
const fields = ['download_folder', 'refresh_interval'];
|
||||
|
||||
fields.forEach(field => {
|
||||
const element = document.querySelector(`[name="sabnzbd.${field}"]`);
|
||||
if (element && sabnzbdConfig[field] !== undefined) {
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = sabnzbdConfig[field];
|
||||
} else {
|
||||
element.value = sabnzbdConfig[field];
|
||||
}
|
||||
}
|
||||
});
|
||||
const categoriesEl = document.querySelector('[name="sabnzbd.categories"]');
|
||||
if (categoriesEl && sabnzbdConfig.categories) {
|
||||
categoriesEl.value = sabnzbdConfig.categories.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
collectUsenetConfig() {
|
||||
const providers = [];
|
||||
|
||||
for (let i = 0; i < this.usenetProviderCount; i++) {
|
||||
const hostEl = document.querySelector(`[name="usenet[${i}].host"]`);
|
||||
if (!hostEl || !hostEl.closest('.usenet-config')) continue;
|
||||
|
||||
const usenet = {
|
||||
host: hostEl.value,
|
||||
port: parseInt(document.querySelector(`[name="usenet[${i}].port"]`).value) || 119,
|
||||
username: document.querySelector(`[name="usenet[${i}].username"]`).value,
|
||||
password: document.querySelector(`[name="usenet[${i}].password"]`).value,
|
||||
connections: parseInt(document.querySelector(`[name="usenet[${i}].connections"]`).value) || 30,
|
||||
name: document.querySelector(`[name="usenet[${i}].name"]`).value,
|
||||
ssl: document.querySelector(`[name="usenet[${i}].ssl"]`).checked,
|
||||
use_tls: document.querySelector(`[name="usenet[${i}].use_tls"]`).checked,
|
||||
};
|
||||
|
||||
if (usenet.host) {
|
||||
providers.push(usenet);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"providers": providers,
|
||||
"chunks": parseInt(document.querySelector('[name="usenet.chunks"]').value) || 15,
|
||||
"mount_folder": document.querySelector('[name="usenet.mount_folder"]').value,
|
||||
"skip_pre_cache": document.querySelector('[name="usenet.skip_pre_cache"]').checked,
|
||||
"rc_url": document.querySelector('[name="usenet.rc_url"]').value,
|
||||
"rc_user": document.querySelector('[name="usenet.rc_user"]').value,
|
||||
"rc_pass": document.querySelector('[name="usenet.rc_pass"]').value,
|
||||
};
|
||||
}
|
||||
|
||||
collectSABnzbdConfig() {
|
||||
return {
|
||||
download_folder: document.querySelector('[name="sabnzbd.download_folder"]').value,
|
||||
refresh_interval: parseInt(document.querySelector('[name="sabnzbd.refresh_interval"]').value) || 15,
|
||||
categories: document.querySelector('[name="sabnzbd.categories"]').value
|
||||
.split(',').map(ext => ext.trim()).filter(Boolean)
|
||||
};
|
||||
}
|
||||
|
||||
collectRepairConfig() {
|
||||
return {
|
||||
enabled: document.querySelector('[name="repair.enabled"]').checked,
|
||||
|
||||
@@ -1,32 +1,51 @@
|
||||
// Dashboard functionality for torrent management
|
||||
class TorrentDashboard {
|
||||
// Dashboard functionality for torrent and NZB management
|
||||
class Dashboard {
|
||||
constructor() {
|
||||
this.state = {
|
||||
mode: 'torrents', // 'torrents' or 'nzbs'
|
||||
torrents: [],
|
||||
selectedTorrents: new Set(),
|
||||
nzbs: [],
|
||||
selectedItems: new Set(),
|
||||
categories: new Set(),
|
||||
filteredTorrents: [],
|
||||
filteredItems: [],
|
||||
selectedCategory: '',
|
||||
selectedState: '',
|
||||
sortBy: 'added_on',
|
||||
itemsPerPage: 20,
|
||||
currentPage: 1,
|
||||
selectedTorrentContextMenu: null
|
||||
selectedItemContextMenu: null
|
||||
};
|
||||
|
||||
this.refs = {
|
||||
torrentsList: document.getElementById('torrentsList'),
|
||||
// Mode switching
|
||||
torrentsMode: document.getElementById('torrentsMode'),
|
||||
nzbsMode: document.getElementById('nzbsMode'),
|
||||
|
||||
// Table elements
|
||||
dataList: document.getElementById('dataList'),
|
||||
torrentsHeaders: document.getElementById('torrentsHeaders'),
|
||||
nzbsHeaders: document.getElementById('nzbsHeaders'),
|
||||
|
||||
// Controls
|
||||
categoryFilter: document.getElementById('categoryFilter'),
|
||||
stateFilter: document.getElementById('stateFilter'),
|
||||
sortSelector: document.getElementById('sortSelector'),
|
||||
selectAll: document.getElementById('selectAll'),
|
||||
selectAllNzb: document.getElementById('selectAllNzb'),
|
||||
batchDeleteBtn: document.getElementById('batchDeleteBtn'),
|
||||
batchDeleteDebridBtn: document.getElementById('batchDeleteDebridBtn'),
|
||||
refreshBtn: document.getElementById('refreshBtn'),
|
||||
|
||||
// Context menus
|
||||
torrentContextMenu: document.getElementById('torrentContextMenu'),
|
||||
nzbContextMenu: document.getElementById('nzbContextMenu'),
|
||||
|
||||
// Pagination and empty state
|
||||
paginationControls: document.getElementById('paginationControls'),
|
||||
paginationInfo: document.getElementById('paginationInfo'),
|
||||
emptyState: document.getElementById('emptyState')
|
||||
emptyState: document.getElementById('emptyState'),
|
||||
emptyStateTitle: document.getElementById('emptyStateTitle'),
|
||||
emptyStateMessage: document.getElementById('emptyStateMessage')
|
||||
};
|
||||
|
||||
this.init();
|
||||
@@ -34,20 +53,26 @@ class TorrentDashboard {
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.loadTorrents();
|
||||
this.loadModeFromURL();
|
||||
this.loadData();
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Mode switching
|
||||
this.refs.torrentsMode.addEventListener('click', () => this.switchMode('torrents'));
|
||||
this.refs.nzbsMode.addEventListener('click', () => this.switchMode('nzbs'));
|
||||
|
||||
// Refresh button
|
||||
this.refs.refreshBtn.addEventListener('click', () => this.loadTorrents());
|
||||
this.refs.refreshBtn.addEventListener('click', () => this.loadData());
|
||||
|
||||
// Batch delete
|
||||
this.refs.batchDeleteBtn.addEventListener('click', () => this.deleteSelectedTorrents());
|
||||
this.refs.batchDeleteDebridBtn.addEventListener('click', () => this.deleteSelectedTorrents(true));
|
||||
this.refs.batchDeleteBtn.addEventListener('click', () => this.deleteSelectedItems());
|
||||
this.refs.batchDeleteDebridBtn.addEventListener('click', () => this.deleteSelectedItems(true));
|
||||
|
||||
// Select all checkbox
|
||||
// Select all checkboxes
|
||||
this.refs.selectAll.addEventListener('change', (e) => this.toggleSelectAll(e.target.checked));
|
||||
this.refs.selectAllNzb.addEventListener('change', (e) => this.toggleSelectAll(e.target.checked));
|
||||
|
||||
// Filters
|
||||
this.refs.categoryFilter.addEventListener('change', (e) => this.setFilter('category', e.target.value));
|
||||
@@ -57,18 +82,333 @@ class TorrentDashboard {
|
||||
// Context menu
|
||||
this.bindContextMenu();
|
||||
|
||||
// Torrent selection
|
||||
this.refs.torrentsList.addEventListener('change', (e) => {
|
||||
if (e.target.classList.contains('torrent-select')) {
|
||||
this.toggleTorrentSelection(e.target.dataset.hash, e.target.checked);
|
||||
// Item selection
|
||||
this.refs.dataList.addEventListener('change', (e) => {
|
||||
if (e.target.classList.contains('item-select')) {
|
||||
this.toggleItemSelection(e.target.dataset.id, e.target.checked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
switchMode(mode) {
|
||||
if (this.state.mode === mode) return;
|
||||
|
||||
this.state.mode = mode;
|
||||
this.state.selectedItems.clear();
|
||||
|
||||
// Update URL parameter
|
||||
this.updateURL(mode);
|
||||
|
||||
// Update button states
|
||||
if (mode === 'torrents') {
|
||||
this.refs.torrentsMode.classList.remove('btn-outline');
|
||||
this.refs.torrentsMode.classList.add('btn-primary');
|
||||
this.refs.nzbsMode.classList.remove('btn-primary');
|
||||
this.refs.nzbsMode.classList.add('btn-outline');
|
||||
|
||||
// Show torrent headers, hide NZB headers
|
||||
this.refs.torrentsHeaders.classList.remove('hidden');
|
||||
this.refs.nzbsHeaders.classList.add('hidden');
|
||||
|
||||
// Update empty state
|
||||
this.refs.emptyStateTitle.textContent = 'No Torrents Found';
|
||||
this.refs.emptyStateMessage.textContent = "You haven't added any torrents yet. Start by adding your first download!";
|
||||
|
||||
// Show debrid batch delete button
|
||||
this.refs.batchDeleteDebridBtn.classList.remove('hidden');
|
||||
} else {
|
||||
this.refs.nzbsMode.classList.remove('btn-outline');
|
||||
this.refs.nzbsMode.classList.add('btn-primary');
|
||||
this.refs.torrentsMode.classList.remove('btn-primary');
|
||||
this.refs.torrentsMode.classList.add('btn-outline');
|
||||
|
||||
// Show NZB headers, hide torrent headers
|
||||
this.refs.nzbsHeaders.classList.remove('hidden');
|
||||
this.refs.torrentsHeaders.classList.add('hidden');
|
||||
|
||||
// Update empty state
|
||||
this.refs.emptyStateTitle.textContent = 'No NZBs Found';
|
||||
this.refs.emptyStateMessage.textContent = "You haven't added any NZB downloads yet. Start by adding your first NZB!";
|
||||
|
||||
// Hide debrid batch delete button (not relevant for NZBs)
|
||||
this.refs.batchDeleteDebridBtn.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Reset filters and reload data
|
||||
this.state.selectedCategory = '';
|
||||
this.state.selectedState = '';
|
||||
this.state.currentPage = 1;
|
||||
this.refs.categoryFilter.value = '';
|
||||
this.refs.stateFilter.value = '';
|
||||
|
||||
this.loadData();
|
||||
this.updateBatchActions();
|
||||
}
|
||||
|
||||
updateBatchActions() {
|
||||
const hasSelection = this.state.selectedItems.size > 0;
|
||||
|
||||
// Show/hide batch delete button
|
||||
if (this.refs.batchDeleteBtn) {
|
||||
this.refs.batchDeleteBtn.classList.toggle('hidden', !hasSelection);
|
||||
}
|
||||
|
||||
// Show/hide debrid batch delete button (only for torrents)
|
||||
if (this.refs.batchDeleteDebridBtn) {
|
||||
const showDebridButton = hasSelection && this.state.mode === 'torrents';
|
||||
this.refs.batchDeleteDebridBtn.classList.toggle('hidden', !showDebridButton);
|
||||
}
|
||||
|
||||
// Update button text with count
|
||||
if (hasSelection) {
|
||||
const count = this.state.selectedItems.size;
|
||||
const itemType = this.state.mode === 'torrents' ? 'Torrent' : 'NZB';
|
||||
const itemTypePlural = this.state.mode === 'torrents' ? 'Torrents' : 'NZBs';
|
||||
|
||||
if (this.refs.batchDeleteBtn) {
|
||||
const deleteText = count === 1 ? `Delete ${itemType}` : `Delete ${count} ${itemTypePlural}`;
|
||||
const deleteSpan = this.refs.batchDeleteBtn.querySelector('span');
|
||||
if (deleteSpan) {
|
||||
deleteSpan.textContent = deleteText;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.refs.batchDeleteDebridBtn && this.state.mode === 'torrents') {
|
||||
const debridText = count === 1 ? 'Remove From Debrid' : `Remove ${count} From Debrid`;
|
||||
const debridSpan = this.refs.batchDeleteDebridBtn.querySelector('span');
|
||||
if (debridSpan) {
|
||||
debridSpan.textContent = debridText;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Reset button text when no selection
|
||||
if (this.refs.batchDeleteBtn) {
|
||||
const deleteSpan = this.refs.batchDeleteBtn.querySelector('span');
|
||||
if (deleteSpan) {
|
||||
deleteSpan.textContent = 'Delete Selected';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.refs.batchDeleteDebridBtn) {
|
||||
const debridSpan = this.refs.batchDeleteDebridBtn.querySelector('span');
|
||||
if (debridSpan) {
|
||||
debridSpan.textContent = 'Remove From Debrid';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadData() {
|
||||
if (this.state.mode === 'torrents') {
|
||||
this.loadTorrents();
|
||||
} else {
|
||||
this.loadNZBs();
|
||||
}
|
||||
}
|
||||
|
||||
async loadNZBs() {
|
||||
try {
|
||||
const response = await window.decypharrUtils.fetcher('/api/nzbs');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch NZBs');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.state.nzbs = data.nzbs || [];
|
||||
|
||||
this.updateCategories();
|
||||
this.applyFilters();
|
||||
this.renderData();
|
||||
} catch (error) {
|
||||
console.error('Error loading NZBs:', error);
|
||||
window.decypharrUtils.createToast('Error loading NZBs', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
updateCategories() {
|
||||
const items = this.state.mode === 'torrents' ? this.state.torrents : this.state.nzbs;
|
||||
this.state.categories = new Set(items.map(item => item.category).filter(Boolean));
|
||||
}
|
||||
|
||||
applyFilters() {
|
||||
if (this.state.mode === 'torrents') {
|
||||
this.filterTorrents();
|
||||
} else {
|
||||
this.filterNZBs();
|
||||
}
|
||||
}
|
||||
|
||||
filterNZBs() {
|
||||
let filtered = [...this.state.nzbs];
|
||||
|
||||
if (this.state.selectedCategory) {
|
||||
filtered = filtered.filter(n => n.category === this.state.selectedCategory);
|
||||
}
|
||||
|
||||
if (this.state.selectedState) {
|
||||
filtered = filtered.filter(n => n.status === this.state.selectedState);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
filtered.sort((a, b) => {
|
||||
switch (this.state.sortBy) {
|
||||
case 'added_on':
|
||||
return new Date(b.added_on) - new Date(a.added_on);
|
||||
case 'added_on_asc':
|
||||
return new Date(a.added_on) - new Date(b.added_on);
|
||||
case 'name_asc':
|
||||
return a.name.localeCompare(b.name);
|
||||
case 'name_desc':
|
||||
return b.name.localeCompare(a.name);
|
||||
case 'size_desc':
|
||||
return (b.total_size || 0) - (a.total_size || 0);
|
||||
case 'size_asc':
|
||||
return (a.total_size || 0) - (b.total_size || 0);
|
||||
case 'progress_desc':
|
||||
return (b.progress || 0) - (a.progress || 0);
|
||||
case 'progress_asc':
|
||||
return (a.progress || 0) - (b.progress || 0);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
this.state.filteredItems = filtered;
|
||||
}
|
||||
|
||||
renderData() {
|
||||
if (this.state.mode === 'torrents') {
|
||||
this.renderTorrents();
|
||||
} else {
|
||||
this.renderNZBs();
|
||||
}
|
||||
}
|
||||
|
||||
renderNZBs() {
|
||||
const startIndex = (this.state.currentPage - 1) * this.state.itemsPerPage;
|
||||
const endIndex = startIndex + this.state.itemsPerPage;
|
||||
const pageItems = this.state.filteredItems.slice(startIndex, endIndex);
|
||||
|
||||
const tbody = this.refs.dataList;
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (pageItems.length === 0) {
|
||||
this.refs.emptyState.classList.remove('hidden');
|
||||
} else {
|
||||
this.refs.emptyState.classList.add('hidden');
|
||||
pageItems.forEach(nzb => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover cursor-pointer';
|
||||
row.setAttribute('data-id', nzb.id);
|
||||
row.setAttribute('data-name', nzb.name);
|
||||
row.setAttribute('data-category', nzb.category || '');
|
||||
|
||||
const progressPercent = Math.round(nzb.progress || 0);
|
||||
const sizeFormatted = this.formatBytes(nzb.total_size || 0);
|
||||
const etaFormatted = this.formatETA(nzb.eta || 0);
|
||||
const ageFormatted = this.formatAge(nzb.date_posted);
|
||||
const statusBadge = this.getStatusBadge(nzb.status);
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="w-12">
|
||||
<label class="cursor-pointer">
|
||||
<input type="checkbox" class="checkbox checkbox-sm item-select" data-id="${nzb.id}">
|
||||
</label>
|
||||
</td>
|
||||
<td class="font-medium max-w-xs">
|
||||
<div class="truncate" title="${nzb.name}">${nzb.name}</div>
|
||||
</td>
|
||||
<td>${sizeFormatted}</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-16 bg-base-300 rounded-full h-2">
|
||||
<div class="bg-primary h-2 rounded-full transition-all duration-300" style="width: ${progressPercent}%"></div>
|
||||
</div>
|
||||
<span class="text-sm font-medium">${progressPercent}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>${etaFormatted}</td>
|
||||
<td>
|
||||
<span class="badge badge-ghost badge-sm">${nzb.category || 'N/A'}</span>
|
||||
</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${ageFormatted}</td>
|
||||
<td>
|
||||
<div class="flex gap-1">
|
||||
<button class="btn btn-ghost btn-xs" onclick="window.dashboard.deleteNZB('${nzb.id}');" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
this.updatePagination();
|
||||
this.updateSelectionUI();
|
||||
}
|
||||
|
||||
getStatusBadge(status) {
|
||||
const statusMap = {
|
||||
'downloading': '<span class="badge badge-info badge-sm">Downloading</span>',
|
||||
'completed': '<span class="badge badge-success badge-sm">Completed</span>',
|
||||
'paused': '<span class="badge badge-warning badge-sm">Paused</span>',
|
||||
'failed': '<span class="badge badge-error badge-sm">Failed</span>',
|
||||
'queued': '<span class="badge badge-ghost badge-sm">Queued</span>',
|
||||
'processing': '<span class="badge badge-info badge-sm">Processing</span>',
|
||||
'verifying': '<span class="badge badge-info badge-sm">Verifying</span>',
|
||||
'repairing': '<span class="badge badge-warning badge-sm">Repairing</span>',
|
||||
'extracting': '<span class="badge badge-info badge-sm">Extracting</span>'
|
||||
};
|
||||
return statusMap[status] || '<span class="badge badge-ghost badge-sm">Unknown</span>';
|
||||
}
|
||||
|
||||
formatETA(seconds) {
|
||||
if (!seconds || seconds <= 0) return 'N/A';
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
formatAge(datePosted) {
|
||||
if (!datePosted) return 'N/A';
|
||||
|
||||
const now = new Date();
|
||||
const posted = new Date(datePosted);
|
||||
const diffMs = now - posted;
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return 'Today';
|
||||
} else if (diffDays === 1) {
|
||||
return '1 day';
|
||||
} else {
|
||||
return `${diffDays} days`;
|
||||
}
|
||||
}
|
||||
|
||||
formatBytes(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
bindContextMenu() {
|
||||
// Show context menu
|
||||
this.refs.torrentsList.addEventListener('contextmenu', (e) => {
|
||||
const row = e.target.closest('tr[data-hash]');
|
||||
this.refs.dataList.addEventListener('contextmenu', (e) => {
|
||||
const row = e.target.closest('tr[data-id]');
|
||||
if (!row) return;
|
||||
|
||||
e.preventDefault();
|
||||
@@ -77,12 +417,14 @@ class TorrentDashboard {
|
||||
|
||||
// Hide context menu
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.refs.torrentContextMenu.contains(e.target)) {
|
||||
const torrentMenu = this.refs.torrentContextMenu;
|
||||
const nzbMenu = this.refs.nzbContextMenu;
|
||||
if (!torrentMenu.contains(e.target) && !nzbMenu.contains(e.target)) {
|
||||
this.hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Context menu actions
|
||||
// Context menu actions for torrents
|
||||
this.refs.torrentContextMenu.addEventListener('click', (e) => {
|
||||
const action = e.target.closest('[data-action]')?.dataset.action;
|
||||
if (action) {
|
||||
@@ -90,37 +432,72 @@ class TorrentDashboard {
|
||||
this.hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Context menu actions for NZBs
|
||||
this.refs.nzbContextMenu.addEventListener('click', (e) => {
|
||||
const action = e.target.closest('[data-action]')?.dataset.action;
|
||||
if (action) {
|
||||
this.handleContextAction(action);
|
||||
this.hideContextMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showContextMenu(event, row) {
|
||||
this.state.selectedTorrentContextMenu = {
|
||||
hash: row.dataset.hash,
|
||||
name: row.dataset.name,
|
||||
category: row.dataset.category || ''
|
||||
};
|
||||
|
||||
this.refs.torrentContextMenu.querySelector('.torrent-name').textContent =
|
||||
this.state.selectedTorrentContextMenu.name;
|
||||
|
||||
const { pageX, pageY } = event;
|
||||
const { clientWidth, clientHeight } = document.documentElement;
|
||||
const menu = this.refs.torrentContextMenu;
|
||||
|
||||
if (this.state.mode === 'torrents') {
|
||||
this.state.selectedItemContextMenu = {
|
||||
id: row.dataset.hash,
|
||||
name: row.dataset.name,
|
||||
category: row.dataset.category || '',
|
||||
type: 'torrent'
|
||||
};
|
||||
|
||||
// Position the menu
|
||||
menu.style.left = `${Math.min(pageX, clientWidth - 200)}px`;
|
||||
menu.style.top = `${Math.min(pageY, clientHeight - 150)}px`;
|
||||
const menu = this.refs.torrentContextMenu;
|
||||
menu.querySelector('.torrent-name').textContent = this.state.selectedItemContextMenu.name;
|
||||
|
||||
// Position the menu
|
||||
menu.style.left = `${Math.min(pageX, clientWidth - 200)}px`;
|
||||
menu.style.top = `${Math.min(pageY, clientHeight - 150)}px`;
|
||||
menu.classList.remove('hidden');
|
||||
} else {
|
||||
this.state.selectedItemContextMenu = {
|
||||
id: row.dataset.id,
|
||||
name: row.dataset.name,
|
||||
category: row.dataset.category || '',
|
||||
type: 'nzb'
|
||||
};
|
||||
|
||||
menu.classList.remove('hidden');
|
||||
const menu = this.refs.nzbContextMenu;
|
||||
menu.querySelector('.nzb-name').textContent = this.state.selectedItemContextMenu.name;
|
||||
|
||||
// Position the menu
|
||||
menu.style.left = `${Math.min(pageX, clientWidth - 200)}px`;
|
||||
menu.style.top = `${Math.min(pageY, clientHeight - 150)}px`;
|
||||
menu.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
hideContextMenu() {
|
||||
this.refs.torrentContextMenu.classList.add('hidden');
|
||||
this.state.selectedTorrentContextMenu = null;
|
||||
this.refs.nzbContextMenu.classList.add('hidden');
|
||||
this.state.selectedItemContextMenu = null;
|
||||
}
|
||||
|
||||
async handleContextAction(action) {
|
||||
const torrent = this.state.selectedTorrentContextMenu;
|
||||
if (!torrent) return;
|
||||
const item = this.state.selectedItemContextMenu;
|
||||
if (!item) return;
|
||||
|
||||
if (item.type === 'torrent') {
|
||||
await this.handleTorrentAction(action, item);
|
||||
} else {
|
||||
await this.handleNZBAction(action, item);
|
||||
}
|
||||
}
|
||||
|
||||
async handleTorrentAction(action, torrent) {
|
||||
|
||||
const actions = {
|
||||
'copy-magnet': async () => {
|
||||
@@ -149,6 +526,87 @@ class TorrentDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
async handleNZBAction(action, nzb) {
|
||||
const actions = {
|
||||
'pause': async () => {
|
||||
try {
|
||||
const response = await window.decypharrUtils.fetcher(`/api/nzbs/${nzb.id}/pause`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (response.ok) {
|
||||
window.decypharrUtils.createToast('NZB paused successfully');
|
||||
this.loadData();
|
||||
} else {
|
||||
throw new Error('Failed to pause NZB');
|
||||
}
|
||||
} catch (error) {
|
||||
window.decypharrUtils.createToast('Failed to pause NZB', 'error');
|
||||
}
|
||||
},
|
||||
'resume': async () => {
|
||||
try {
|
||||
const response = await window.decypharrUtils.fetcher(`/api/nzbs/${nzb.id}/resume`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (response.ok) {
|
||||
window.decypharrUtils.createToast('NZB resumed successfully');
|
||||
this.loadData();
|
||||
} else {
|
||||
throw new Error('Failed to resume NZB');
|
||||
}
|
||||
} catch (error) {
|
||||
window.decypharrUtils.createToast('Failed to resume NZB', 'error');
|
||||
}
|
||||
},
|
||||
'retry': async () => {
|
||||
try {
|
||||
const response = await window.decypharrUtils.fetcher(`/api/nzbs/${nzb.id}/retry`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (response.ok) {
|
||||
window.decypharrUtils.createToast('NZB retry started successfully');
|
||||
this.loadData();
|
||||
} else {
|
||||
throw new Error('Failed to retry NZB');
|
||||
}
|
||||
} catch (error) {
|
||||
window.decypharrUtils.createToast('Failed to retry NZB', 'error');
|
||||
}
|
||||
},
|
||||
'copy-name': async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(nzb.name);
|
||||
window.decypharrUtils.createToast('NZB name copied to clipboard');
|
||||
} catch (error) {
|
||||
window.decypharrUtils.createToast('Failed to copy NZB name', 'error');
|
||||
}
|
||||
},
|
||||
'delete': async () => {
|
||||
await this.deleteNZB(nzb.id);
|
||||
}
|
||||
};
|
||||
|
||||
if (actions[action]) {
|
||||
await actions[action]();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteNZB(nzbId) {
|
||||
try {
|
||||
const response = await window.decypharrUtils.fetcher(`/api/nzbs/${nzbId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (response.ok) {
|
||||
window.decypharrUtils.createToast('NZB deleted successfully');
|
||||
this.loadData();
|
||||
} else {
|
||||
throw new Error('Failed to delete NZB');
|
||||
}
|
||||
} catch (error) {
|
||||
window.decypharrUtils.createToast('Failed to delete NZB', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async loadTorrents() {
|
||||
try {
|
||||
// Show loading state
|
||||
@@ -173,14 +631,14 @@ class TorrentDashboard {
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
// Filter torrents
|
||||
this.filterTorrents();
|
||||
// Apply filters based on current mode
|
||||
this.applyFilters();
|
||||
|
||||
// Update category dropdown
|
||||
this.updateCategoryFilter();
|
||||
|
||||
// Render torrents table
|
||||
this.renderTorrents();
|
||||
// Render data table
|
||||
this.renderData();
|
||||
|
||||
// Update pagination
|
||||
this.updatePagination();
|
||||
@@ -206,7 +664,7 @@ class TorrentDashboard {
|
||||
// Sort torrents
|
||||
filtered = this.sortTorrents(filtered);
|
||||
|
||||
this.state.filteredTorrents = filtered;
|
||||
this.state.filteredItems = filtered;
|
||||
}
|
||||
|
||||
sortTorrents(torrents) {
|
||||
@@ -253,27 +711,27 @@ class TorrentDashboard {
|
||||
|
||||
renderTorrents() {
|
||||
const startIndex = (this.state.currentPage - 1) * this.state.itemsPerPage;
|
||||
const endIndex = Math.min(startIndex + this.state.itemsPerPage, this.state.filteredTorrents.length);
|
||||
const pageItems = this.state.filteredTorrents.slice(startIndex, endIndex);
|
||||
const endIndex = Math.min(startIndex + this.state.itemsPerPage, this.state.filteredItems.length);
|
||||
const pageItems = this.state.filteredItems.slice(startIndex, endIndex);
|
||||
|
||||
this.refs.torrentsList.innerHTML = pageItems.map(torrent => this.torrentRowTemplate(torrent)).join('');
|
||||
this.refs.dataList.innerHTML = pageItems.map(torrent => this.torrentRowTemplate(torrent)).join('');
|
||||
}
|
||||
|
||||
torrentRowTemplate(torrent) {
|
||||
const progressPercent = (torrent.progress * 100).toFixed(1);
|
||||
const isSelected = this.state.selectedTorrents.has(torrent.hash);
|
||||
const isSelected = this.state.selectedItems.has(torrent.hash);
|
||||
let addedOn = new Date(torrent.added_on).toLocaleString();
|
||||
|
||||
return `
|
||||
<tr data-hash="${torrent.hash}"
|
||||
<tr data-id="${torrent.hash}"
|
||||
data-name="${this.escapeHtml(torrent.name)}"
|
||||
data-category="${torrent.category || ''}"
|
||||
class="hover:bg-base-200 transition-colors">
|
||||
<td>
|
||||
<label class="cursor-pointer">
|
||||
<input type="checkbox"
|
||||
class="checkbox checkbox-sm torrent-select"
|
||||
data-hash="${torrent.hash}"
|
||||
class="checkbox checkbox-sm item-select"
|
||||
data-id="${torrent.hash}"
|
||||
${isSelected ? 'checked' : ''}>
|
||||
</label>
|
||||
</td>
|
||||
@@ -358,13 +816,13 @@ class TorrentDashboard {
|
||||
}
|
||||
|
||||
updatePagination() {
|
||||
const totalPages = Math.ceil(this.state.filteredTorrents.length / this.state.itemsPerPage);
|
||||
const totalPages = Math.ceil(this.state.filteredItems.length / this.state.itemsPerPage);
|
||||
const startIndex = (this.state.currentPage - 1) * this.state.itemsPerPage;
|
||||
const endIndex = Math.min(startIndex + this.state.itemsPerPage, this.state.filteredTorrents.length);
|
||||
const endIndex = Math.min(startIndex + this.state.itemsPerPage, this.state.filteredItems.length);
|
||||
|
||||
// Update pagination info
|
||||
this.refs.paginationInfo.textContent =
|
||||
`Showing ${this.state.filteredTorrents.length > 0 ? startIndex + 1 : 0}-${endIndex} of ${this.state.filteredTorrents.length} torrents`;
|
||||
`Showing ${this.state.filteredItems.length > 0 ? startIndex + 1 : 0}-${endIndex} of ${this.state.filteredItems.length} torrents`;
|
||||
|
||||
// Clear pagination controls
|
||||
this.refs.paginationControls.innerHTML = '';
|
||||
@@ -412,33 +870,42 @@ class TorrentDashboard {
|
||||
|
||||
updateSelectionUI() {
|
||||
// Clean up selected torrents that no longer exist
|
||||
const currentHashes = new Set(this.state.filteredTorrents.map(t => t.hash));
|
||||
this.state.selectedTorrents.forEach(hash => {
|
||||
const currentHashes = new Set(this.state.filteredItems.map(t => t.hash));
|
||||
this.state.selectedItems.forEach(hash => {
|
||||
if (!currentHashes.has(hash)) {
|
||||
this.state.selectedTorrents.delete(hash);
|
||||
this.state.selectedItems.delete(hash);
|
||||
}
|
||||
});
|
||||
|
||||
// Update batch delete button
|
||||
this.refs.batchDeleteBtn.classList.toggle('hidden', this.state.selectedTorrents.size === 0);
|
||||
this.refs.batchDeleteDebridBtn.classList.toggle('hidden', this.state.selectedTorrents.size === 0);
|
||||
this.refs.batchDeleteBtn.classList.toggle('hidden', this.state.selectedItems.size === 0);
|
||||
this.refs.batchDeleteDebridBtn.classList.toggle('hidden', this.state.selectedItems.size === 0);
|
||||
|
||||
// Update select all checkbox
|
||||
const visibleTorrents = this.state.filteredTorrents.slice(
|
||||
const visibleTorrents = this.state.filteredItems.slice(
|
||||
(this.state.currentPage - 1) * this.state.itemsPerPage,
|
||||
this.state.currentPage * this.state.itemsPerPage
|
||||
);
|
||||
|
||||
this.refs.selectAll.checked = visibleTorrents.length > 0 &&
|
||||
visibleTorrents.every(torrent => this.state.selectedTorrents.has(torrent.hash));
|
||||
this.refs.selectAll.indeterminate = visibleTorrents.some(torrent => this.state.selectedTorrents.has(torrent.hash)) &&
|
||||
!visibleTorrents.every(torrent => this.state.selectedTorrents.has(torrent.hash));
|
||||
visibleTorrents.every(torrent => this.state.selectedItems.has(torrent.hash));
|
||||
this.refs.selectAll.indeterminate = visibleTorrents.some(torrent => this.state.selectedItems.has(torrent.hash)) &&
|
||||
!visibleTorrents.every(torrent => this.state.selectedItems.has(torrent.hash));
|
||||
}
|
||||
|
||||
toggleEmptyState() {
|
||||
const isEmpty = this.state.torrents.length === 0;
|
||||
this.refs.emptyState.classList.toggle('hidden', !isEmpty);
|
||||
document.querySelector('.card:has(#torrentsList)').classList.toggle('hidden', isEmpty);
|
||||
const items = this.state.mode === 'torrents' ? this.state.torrents : this.state.nzbs;
|
||||
const isEmpty = items.length === 0;
|
||||
|
||||
if (this.refs.emptyState) {
|
||||
this.refs.emptyState.classList.toggle('hidden', !isEmpty);
|
||||
}
|
||||
|
||||
// Find the main data table card and toggle its visibility
|
||||
const dataTableCard = document.querySelector('.card:has(#dataList)');
|
||||
if (dataTableCard) {
|
||||
dataTableCard.classList.toggle('hidden', isEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
@@ -459,29 +926,30 @@ class TorrentDashboard {
|
||||
}
|
||||
|
||||
toggleSelectAll(checked) {
|
||||
const visibleTorrents = this.state.filteredTorrents.slice(
|
||||
const visibleTorrents = this.state.filteredItems.slice(
|
||||
(this.state.currentPage - 1) * this.state.itemsPerPage,
|
||||
this.state.currentPage * this.state.itemsPerPage
|
||||
);
|
||||
|
||||
visibleTorrents.forEach(torrent => {
|
||||
if (checked) {
|
||||
this.state.selectedTorrents.add(torrent.hash);
|
||||
this.state.selectedItems.add(torrent.hash);
|
||||
} else {
|
||||
this.state.selectedTorrents.delete(torrent.hash);
|
||||
this.state.selectedItems.delete(torrent.hash);
|
||||
}
|
||||
});
|
||||
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
toggleTorrentSelection(hash, checked) {
|
||||
toggleItemSelection(id, checked) {
|
||||
if (checked) {
|
||||
this.state.selectedTorrents.add(hash);
|
||||
this.state.selectedItems.add(id);
|
||||
} else {
|
||||
this.state.selectedTorrents.delete(hash);
|
||||
this.state.selectedItems.delete(id);
|
||||
}
|
||||
this.updateSelectionUI();
|
||||
this.updateBatchActions();
|
||||
}
|
||||
|
||||
async deleteTorrent(hash, category, removeFromDebrid = false) {
|
||||
@@ -504,38 +972,55 @@ class TorrentDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSelectedTorrents(removeFromDebrid = false) {
|
||||
const count = this.state.selectedTorrents.size;
|
||||
async deleteSelectedItems(removeFromDebrid = false) {
|
||||
const count = this.state.selectedItems.size;
|
||||
if (count === 0) {
|
||||
window.decypharrUtils.createToast('No torrents selected for deletion', 'warning');
|
||||
const itemType = this.state.mode === 'torrents' ? 'torrents' : 'NZBs';
|
||||
window.decypharrUtils.createToast(`No ${itemType} selected for deletion`, 'warning');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Are you sure you want to delete ${count} torrent${count > 1 ? 's' : ''}${removeFromDebrid ? ' from debrid' : ''}?`)) {
|
||||
|
||||
const itemType = this.state.mode === 'torrents' ? 'torrent' : 'NZB';
|
||||
const itemTypePlural = this.state.mode === 'torrents' ? 'torrents' : 'NZBs';
|
||||
|
||||
if (!confirm(`Are you sure you want to delete ${count} ${count > 1 ? itemTypePlural : itemType}${removeFromDebrid ? ' from debrid' : ''}?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const hashes = Array.from(this.state.selectedTorrents).join(',');
|
||||
const response = await window.decypharrUtils.fetcher(
|
||||
`/api/torrents/?hashes=${encodeURIComponent(hashes)}&removeFromDebrid=${removeFromDebrid}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
if (this.state.mode === 'torrents') {
|
||||
const hashes = Array.from(this.state.selectedItems).join(',');
|
||||
const response = await window.decypharrUtils.fetcher(
|
||||
`/api/torrents/?hashes=${encodeURIComponent(hashes)}&removeFromDebrid=${removeFromDebrid}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
} else {
|
||||
// Delete NZBs one by one
|
||||
const promises = Array.from(this.state.selectedItems).map(id =>
|
||||
window.decypharrUtils.fetcher(`/api/nzbs/${id}`, { method: 'DELETE' })
|
||||
);
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
for (const response of responses) {
|
||||
if (!response.ok) throw new Error(await response.text());
|
||||
}
|
||||
}
|
||||
|
||||
window.decypharrUtils.createToast(`${count} torrent${count > 1 ? 's' : ''} deleted successfully`);
|
||||
this.state.selectedTorrents.clear();
|
||||
await this.loadTorrents();
|
||||
window.decypharrUtils.createToast(`${count} ${count > 1 ? itemTypePlural : itemType} deleted successfully`);
|
||||
this.state.selectedItems.clear();
|
||||
await this.loadData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting torrents:', error);
|
||||
window.decypharrUtils.createToast(`Failed to delete some torrents: ${error.message}`, 'error');
|
||||
console.error(`Error deleting ${itemTypePlural}:`, error);
|
||||
window.decypharrUtils.createToast(`Failed to delete some ${itemTypePlural}: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
startAutoRefresh() {
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.loadTorrents();
|
||||
this.loadData();
|
||||
}, 5000);
|
||||
|
||||
// Clean up on page unload
|
||||
@@ -556,4 +1041,54 @@ class TorrentDashboard {
|
||||
};
|
||||
return text ? text.replace(/[&<>"']/g, (m) => map[m]) : '';
|
||||
}
|
||||
|
||||
loadModeFromURL() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const mode = urlParams.get('mode');
|
||||
|
||||
if (mode === 'nzbs' || mode === 'torrents') {
|
||||
this.state.mode = mode;
|
||||
} else {
|
||||
this.state.mode = 'torrents'; // Default mode
|
||||
}
|
||||
|
||||
// Set the initial UI state without triggering reload
|
||||
this.setModeUI(this.state.mode);
|
||||
}
|
||||
|
||||
setModeUI(mode) {
|
||||
if (mode === 'torrents') {
|
||||
this.refs.torrentsMode.classList.remove('btn-outline');
|
||||
this.refs.torrentsMode.classList.add('btn-primary');
|
||||
this.refs.nzbsMode.classList.remove('btn-primary');
|
||||
this.refs.nzbsMode.classList.add('btn-outline');
|
||||
|
||||
this.refs.torrentsHeaders.classList.remove('hidden');
|
||||
this.refs.nzbsHeaders.classList.add('hidden');
|
||||
|
||||
this.refs.emptyStateTitle.textContent = 'No Torrents Found';
|
||||
this.refs.emptyStateMessage.textContent = "You haven't added any torrents yet. Start by adding your first download!";
|
||||
|
||||
this.refs.batchDeleteDebridBtn.classList.remove('hidden');
|
||||
} else {
|
||||
this.refs.nzbsMode.classList.remove('btn-outline');
|
||||
this.refs.nzbsMode.classList.add('btn-primary');
|
||||
this.refs.torrentsMode.classList.remove('btn-primary');
|
||||
this.refs.torrentsMode.classList.add('btn-outline');
|
||||
|
||||
this.refs.nzbsHeaders.classList.remove('hidden');
|
||||
this.refs.torrentsHeaders.classList.add('hidden');
|
||||
|
||||
this.refs.emptyStateTitle.textContent = 'No NZBs Found';
|
||||
this.refs.emptyStateMessage.textContent = "You haven't added any NZB downloads yet. Start by adding your first NZB!";
|
||||
|
||||
this.refs.batchDeleteDebridBtn.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
updateURL(mode) {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('mode', mode);
|
||||
window.history.replaceState({}, '', url);
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,29 @@
|
||||
class DownloadManager {
|
||||
constructor(downloadFolder) {
|
||||
this.downloadFolder = downloadFolder;
|
||||
this.currentMode = 'torrent'; // Default mode
|
||||
this.refs = {
|
||||
downloadForm: document.getElementById('downloadForm'),
|
||||
// Mode controls
|
||||
torrentMode: document.getElementById('torrentMode'),
|
||||
nzbMode: document.getElementById('nzbMode'),
|
||||
// Torrent inputs
|
||||
magnetURI: document.getElementById('magnetURI'),
|
||||
torrentFiles: document.getElementById('torrentFiles'),
|
||||
torrentInputs: document.getElementById('torrentInputs'),
|
||||
// NZB inputs
|
||||
nzbURLs: document.getElementById('nzbURLs'),
|
||||
nzbFiles: document.getElementById('nzbFiles'),
|
||||
nzbInputs: document.getElementById('nzbInputs'),
|
||||
// Common form elements
|
||||
arr: document.getElementById('arr'),
|
||||
downloadAction: document.getElementById('downloadAction'),
|
||||
downloadUncached: document.getElementById('downloadUncached'),
|
||||
downloadFolder: document.getElementById('downloadFolder'),
|
||||
downloadFolderHint: document.getElementById('downloadFolderHint'),
|
||||
debrid: document.getElementById('debrid'),
|
||||
submitBtn: document.getElementById('submitDownload'),
|
||||
submitButtonText: document.getElementById('submitButtonText'),
|
||||
activeCount: document.getElementById('activeCount'),
|
||||
completedCount: document.getElementById('completedCount'),
|
||||
totalSize: document.getElementById('totalSize')
|
||||
@@ -24,12 +37,17 @@ class DownloadManager {
|
||||
this.loadSavedOptions();
|
||||
this.bindEvents();
|
||||
this.handleMagnetFromURL();
|
||||
this.loadModeFromURL();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Form submission
|
||||
this.refs.downloadForm.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||
|
||||
// Mode switching
|
||||
this.refs.torrentMode.addEventListener('click', () => this.switchMode('torrent'));
|
||||
this.refs.nzbMode.addEventListener('click', () => this.switchMode('nzb'));
|
||||
|
||||
// Save options on change
|
||||
this.refs.arr.addEventListener('change', () => this.saveOptions());
|
||||
this.refs.downloadAction.addEventListener('change', () => this.saveOptions());
|
||||
@@ -38,6 +56,7 @@ class DownloadManager {
|
||||
|
||||
// File input enhancement
|
||||
this.refs.torrentFiles.addEventListener('change', (e) => this.handleFileSelection(e));
|
||||
this.refs.nzbFiles.addEventListener('change', (e) => this.handleFileSelection(e));
|
||||
|
||||
// Drag and drop
|
||||
this.setupDragAndDrop();
|
||||
@@ -48,13 +67,15 @@ class DownloadManager {
|
||||
category: localStorage.getItem('downloadCategory') || '',
|
||||
action: localStorage.getItem('downloadAction') || 'symlink',
|
||||
uncached: localStorage.getItem('downloadUncached') === 'true',
|
||||
folder: localStorage.getItem('downloadFolder') || this.downloadFolder
|
||||
folder: localStorage.getItem('downloadFolder') || this.downloadFolder,
|
||||
mode: localStorage.getItem('downloadMode') || 'torrent'
|
||||
};
|
||||
|
||||
this.refs.arr.value = savedOptions.category;
|
||||
this.refs.downloadAction.value = savedOptions.action;
|
||||
this.refs.downloadUncached.checked = savedOptions.uncached;
|
||||
this.refs.downloadFolder.value = savedOptions.folder;
|
||||
this.currentMode = savedOptions.mode;
|
||||
}
|
||||
|
||||
saveOptions() {
|
||||
@@ -62,6 +83,7 @@ class DownloadManager {
|
||||
localStorage.setItem('downloadAction', this.refs.downloadAction.value);
|
||||
localStorage.setItem('downloadUncached', this.refs.downloadUncached.checked.toString());
|
||||
localStorage.setItem('downloadFolder', this.refs.downloadFolder.value);
|
||||
localStorage.setItem('downloadMode', this.currentMode);
|
||||
}
|
||||
|
||||
handleMagnetFromURL() {
|
||||
@@ -81,31 +103,57 @@ class DownloadManager {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
let urls = [];
|
||||
let files = [];
|
||||
let endpoint = '/api/add';
|
||||
let itemType = 'torrent';
|
||||
|
||||
// Get URLs
|
||||
const urls = this.refs.magnetURI.value
|
||||
.split('\n')
|
||||
.map(url => url.trim())
|
||||
.filter(url => url.length > 0);
|
||||
if (this.currentMode === 'torrent') {
|
||||
// Get torrent URLs
|
||||
urls = this.refs.magnetURI.value
|
||||
.split('\n')
|
||||
.map(url => url.trim())
|
||||
.filter(url => url.length > 0);
|
||||
|
||||
if (urls.length > 0) {
|
||||
formData.append('urls', urls.join('\n'));
|
||||
}
|
||||
if (urls.length > 0) {
|
||||
formData.append('urls', urls.join('\n'));
|
||||
}
|
||||
|
||||
// Get files
|
||||
for (let i = 0; i < this.refs.torrentFiles.files.length; i++) {
|
||||
formData.append('files', this.refs.torrentFiles.files[i]);
|
||||
// Get torrent files
|
||||
for (let i = 0; i < this.refs.torrentFiles.files.length; i++) {
|
||||
formData.append('files', this.refs.torrentFiles.files[i]);
|
||||
files.push(this.refs.torrentFiles.files[i]);
|
||||
}
|
||||
} else if (this.currentMode === 'nzb') {
|
||||
// Get NZB URLs
|
||||
urls = this.refs.nzbURLs.value
|
||||
.split('\n')
|
||||
.map(url => url.trim())
|
||||
.filter(url => url.length > 0);
|
||||
|
||||
if (urls.length > 0) {
|
||||
formData.append('nzbUrls', urls.join('\n'));
|
||||
}
|
||||
|
||||
// Get NZB files
|
||||
for (let i = 0; i < this.refs.nzbFiles.files.length; i++) {
|
||||
formData.append('nzbFiles', this.refs.nzbFiles.files[i]);
|
||||
files.push(this.refs.nzbFiles.files[i]);
|
||||
}
|
||||
|
||||
endpoint = '/api/nzbs/add';
|
||||
itemType = 'NZB';
|
||||
}
|
||||
|
||||
// Validation
|
||||
const totalItems = urls.length + this.refs.torrentFiles.files.length;
|
||||
const totalItems = urls.length + files.length;
|
||||
if (totalItems === 0) {
|
||||
window.decypharrUtils.createToast('Please provide at least one torrent', 'warning');
|
||||
window.decypharrUtils.createToast(`Please provide at least one ${itemType}`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalItems > 100) {
|
||||
window.decypharrUtils.createToast('Please submit up to 100 torrents at a time', 'warning');
|
||||
window.decypharrUtils.createToast(`Please submit up to 100 ${itemType}s at a time`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,7 +171,7 @@ class DownloadManager {
|
||||
// Set loading state
|
||||
window.decypharrUtils.setButtonLoading(this.refs.submitBtn, true);
|
||||
|
||||
const response = await window.decypharrUtils.fetcher('/api/add', {
|
||||
const response = await window.decypharrUtils.fetcher(endpoint, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {} // Remove Content-Type to let browser set it for FormData
|
||||
@@ -137,19 +185,19 @@ class DownloadManager {
|
||||
|
||||
// Handle partial success
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
console.log(result.errors);
|
||||
let errorMessage = ` ${result.errors.join('\n')}`;
|
||||
if (result.results.length > 0) {
|
||||
window.decypharrUtils.createToast(
|
||||
`Added ${result.results.length} torrents with ${result.errors.length} errors`,
|
||||
`Added ${result.results.length} ${itemType}s with ${result.errors.length} errors \n${errorMessage}`,
|
||||
'warning'
|
||||
);
|
||||
this.showErrorDetails(result.errors);
|
||||
} else {
|
||||
window.decypharrUtils.createToast('Failed to add torrents', 'error');
|
||||
this.showErrorDetails(result.errors);
|
||||
window.decypharrUtils.createToast(`Failed to add ${itemType}s \n${errorMessage}`, 'error');
|
||||
}
|
||||
} else {
|
||||
window.decypharrUtils.createToast(
|
||||
`Successfully added ${result.results.length} torrent${result.results.length > 1 ? 's' : ''}!`
|
||||
`Successfully added ${result.results.length} ${itemType}${result.results.length > 1 ? 's' : ''}!`
|
||||
);
|
||||
this.clearForm();
|
||||
}
|
||||
@@ -162,22 +210,49 @@ class DownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
showErrorDetails(errors) {
|
||||
// Create a modal or detailed view for errors
|
||||
const errorList = errors.map(error => `• ${error}`).join('\n');
|
||||
console.error('Download errors:', errorList);
|
||||
switchMode(mode) {
|
||||
this.currentMode = mode;
|
||||
this.saveOptions();
|
||||
this.updateURL(mode);
|
||||
|
||||
// You could also show this in a modal for better UX
|
||||
setTimeout(() => {
|
||||
if (confirm('Some torrents failed to add. Would you like to see the details?')) {
|
||||
alert(errorList);
|
||||
}
|
||||
}, 1000);
|
||||
// Update button states
|
||||
if (mode === 'torrent') {
|
||||
this.refs.torrentMode.classList.remove('btn-outline');
|
||||
this.refs.torrentMode.classList.add('btn-primary');
|
||||
this.refs.nzbMode.classList.remove('btn-primary');
|
||||
this.refs.nzbMode.classList.add('btn-outline');
|
||||
|
||||
// Show/hide sections
|
||||
this.refs.torrentInputs.classList.remove('hidden');
|
||||
this.refs.nzbInputs.classList.add('hidden');
|
||||
|
||||
// Update UI text
|
||||
this.refs.submitButtonText.textContent = 'Add to Download Queue';
|
||||
this.refs.downloadFolderHint.textContent = 'Leave empty to use default qBittorrent folder';
|
||||
} else {
|
||||
this.refs.nzbMode.classList.remove('btn-outline');
|
||||
this.refs.nzbMode.classList.add('btn-primary');
|
||||
this.refs.torrentMode.classList.remove('btn-primary');
|
||||
this.refs.torrentMode.classList.add('btn-outline');
|
||||
|
||||
// Show/hide sections
|
||||
this.refs.nzbInputs.classList.remove('hidden');
|
||||
this.refs.torrentInputs.classList.add('hidden');
|
||||
|
||||
// Update UI text
|
||||
this.refs.submitButtonText.textContent = 'Add to NZB Queue';
|
||||
this.refs.downloadFolderHint.textContent = 'Leave empty to use default SABnzbd folder';
|
||||
}
|
||||
}
|
||||
|
||||
clearForm() {
|
||||
this.refs.magnetURI.value = '';
|
||||
this.refs.torrentFiles.value = '';
|
||||
if (this.currentMode === 'torrent') {
|
||||
this.refs.magnetURI.value = '';
|
||||
this.refs.torrentFiles.value = '';
|
||||
} else {
|
||||
this.refs.nzbURLs.value = '';
|
||||
this.refs.nzbFiles.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
handleFileSelection(e) {
|
||||
@@ -226,20 +301,84 @@ class DownloadManager {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
|
||||
// Filter for .torrent files
|
||||
const torrentFiles = Array.from(files).filter(file =>
|
||||
file.name.toLowerCase().endsWith('.torrent')
|
||||
);
|
||||
if (this.currentMode === 'torrent') {
|
||||
// Filter for .torrent files
|
||||
const torrentFiles = Array.from(files).filter(file =>
|
||||
file.name.toLowerCase().endsWith('.torrent')
|
||||
);
|
||||
|
||||
if (torrentFiles.length > 0) {
|
||||
// Create a new FileList-like object
|
||||
const dataTransfer = new DataTransfer();
|
||||
torrentFiles.forEach(file => dataTransfer.items.add(file));
|
||||
this.refs.torrentFiles.files = dataTransfer.files;
|
||||
if (torrentFiles.length > 0) {
|
||||
// Create a new FileList-like object
|
||||
const dataTransfer = new DataTransfer();
|
||||
torrentFiles.forEach(file => dataTransfer.items.add(file));
|
||||
this.refs.torrentFiles.files = dataTransfer.files;
|
||||
|
||||
this.handleFileSelection({ target: { files: torrentFiles } });
|
||||
this.handleFileSelection({ target: { files: torrentFiles } });
|
||||
} else {
|
||||
window.decypharrUtils.createToast('Please drop .torrent files only', 'warning');
|
||||
}
|
||||
} else {
|
||||
window.decypharrUtils.createToast('Please drop .torrent files only', 'warning');
|
||||
// Filter for .nzb files
|
||||
const nzbFiles = Array.from(files).filter(file =>
|
||||
file.name.toLowerCase().endsWith('.nzb')
|
||||
);
|
||||
|
||||
if (nzbFiles.length > 0) {
|
||||
// Create a new FileList-like object
|
||||
const dataTransfer = new DataTransfer();
|
||||
nzbFiles.forEach(file => dataTransfer.items.add(file));
|
||||
this.refs.nzbFiles.files = dataTransfer.files;
|
||||
|
||||
this.handleFileSelection({ target: { files: nzbFiles } });
|
||||
} else {
|
||||
window.decypharrUtils.createToast('Please drop .nzb files only', 'warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadModeFromURL() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const mode = urlParams.get('mode');
|
||||
|
||||
if (mode === 'nzb' || mode === 'torrent') {
|
||||
this.currentMode = mode;
|
||||
} else {
|
||||
this.currentMode = this.currentMode || 'torrent'; // Use saved preference or default
|
||||
}
|
||||
|
||||
// Initialize the mode without updating URL again
|
||||
this.setModeUI(this.currentMode);
|
||||
}
|
||||
|
||||
setModeUI(mode) {
|
||||
if (mode === 'torrent') {
|
||||
this.refs.torrentMode.classList.remove('btn-outline');
|
||||
this.refs.torrentMode.classList.add('btn-primary');
|
||||
this.refs.nzbMode.classList.remove('btn-primary');
|
||||
this.refs.nzbMode.classList.add('btn-outline');
|
||||
|
||||
this.refs.torrentInputs.classList.remove('hidden');
|
||||
this.refs.nzbInputs.classList.add('hidden');
|
||||
|
||||
this.refs.submitButtonText.textContent = 'Add to Download Queue';
|
||||
this.refs.downloadFolderHint.textContent = 'Leave empty to use default qBittorrent folder';
|
||||
} else {
|
||||
this.refs.nzbMode.classList.remove('btn-outline');
|
||||
this.refs.nzbMode.classList.add('btn-primary');
|
||||
this.refs.torrentMode.classList.remove('btn-primary');
|
||||
this.refs.torrentMode.classList.add('btn-outline');
|
||||
|
||||
this.refs.nzbInputs.classList.remove('hidden');
|
||||
this.refs.torrentInputs.classList.add('hidden');
|
||||
|
||||
this.refs.submitButtonText.textContent = 'Add to NZB Queue';
|
||||
this.refs.downloadFolderHint.textContent = 'Leave empty to use default SABnzbd folder';
|
||||
}
|
||||
}
|
||||
|
||||
updateURL(mode) {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('mode', mode);
|
||||
window.history.replaceState({}, '', url);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user