ui(settings): collapsible sections + Lidarr-style polish for Indexers tab

Restructure the Indexers & Downloaders tab to mirror the
Paths & Organization / Post-Processing / Library Preferences
pattern on the Library page — each subsystem (Indexers / Torrent
Client / Usenet Client) gets its own collapsible section header
with a status dot, hint, and animated arrow.

Visual cues borrowed from Lidarr but rendered in SoulSync's
existing dark-glass theme:
- Intro hero card at the top of the tab with a 1-2-3 flow:
  Indexers find releases → Downloader fetches → SoulSync imports.
  Accent-color stepper pills + sub-copy summarising what's
  optional vs required.
- Status dot in each section header — grey 'unknown' before
  testing, green after Test Connection succeeds, red on failure.
  Driven by _setIndStatusDot() helper called from each test
  handler. Soft glow on the active states.
- Per-service service-title color accents matching existing
  spotify-title / tidal-title pattern: prowlarr-title (orange,
  Prowlarr brand), torrent-title (sky blue, qBit family),
  usenet-title (violet).
- Indexer list cards replace the inline-emoji list — proper
  protocol badges (Torrent vs Usenet pill), monospace id chip,
  privacy tag, dimmed appearance when the indexer is disabled
  in Prowlarr.
- Indexers section starts open; Torrent + Usenet start collapsed
  since most users only configure one protocol.

No behavior changes — same fields, same endpoints, same save
flow. Pure visual restructure of the panels added in the previous
three commits.
This commit is contained in:
Broque Thomas 2026-05-20 16:07:35 -07:00
parent e4ca56b499
commit 9b36d421ee
4 changed files with 396 additions and 146 deletions

View file

@ -4947,160 +4947,206 @@
</div> </div>
<!-- ═══ INDEXERS & DOWNLOADERS ═══ --> <!-- ═══ INDEXERS & DOWNLOADERS ═══ -->
<div class="settings-group" data-stg="indexers"> <!-- Intro hero — explains the search → fetch flow in one glance -->
<h3>🔎 Prowlarr (Indexer Aggregator)</h3> <div class="settings-group ind-hero" data-stg="indexers">
<div class="setting-help-text" style="margin-bottom: 10px;"> <div class="ind-hero-title">Indexers &amp; Downloaders</div>
Prowlarr manages your Usenet and torrent indexers and exposes them through one API. SoulSync uses Prowlarr to search across every indexer at once for the Torrent and Usenet download sources. <div class="ind-hero-flow">
<br><br> <span class="ind-hero-step"><span class="ind-hero-step-num">1</span> Indexers find releases</span>
Don't have it? Grab Prowlarr from <code>prowlarr.com</code> (or your *arr stack). You point Prowlarr at your indexers, then point SoulSync at Prowlarr. <span class="ind-hero-arrow"></span>
<span class="ind-hero-step"><span class="ind-hero-step-num">2</span> Downloader fetches them</span>
<span class="ind-hero-arrow"></span>
<span class="ind-hero-step"><span class="ind-hero-step-num">3</span> SoulSync imports to library</span>
</div> </div>
<div class="form-group"> <div class="ind-hero-sub">
<label>Prowlarr URL:</label> Configure your Prowlarr instance to feed search results, and pick a torrent client and/or usenet client to handle the actual downloads. You only need to set up the protocols you actually use.
<input type="text" id="prowlarr-url" placeholder="http://localhost:9696">
<div class="setting-help-text">
Full URL to your Prowlarr instance (e.g. <code>http://192.168.1.100:9696</code>).
</div>
</div> </div>
<div class="form-group"> </div>
<label>API Key:</label>
<input type="password" id="prowlarr-api-key" placeholder="Your Prowlarr API key"> <!-- ─── INDEXERS section ─── -->
<div class="setting-help-text"> <div class="settings-section-header" data-stg="indexers" onclick="this.classList.toggle('collapsed'); const b=this.nextElementSibling; b.classList.toggle('collapsed'); b.style.display=b.classList.contains('collapsed')?'none':''">
Found in Prowlarr → Settings → General → Security → API Key. <span class="settings-section-arrow">&#9660;</span>
</div> <h3>🔎 Indexers</h3>
</div> <span class="settings-section-hint">Prowlarr — search aggregator</span>
<div class="form-group"> <span class="ind-status-dot ind-status-dot-unknown" id="prowlarr-status-dot" title="Not tested"></span>
<label>Prowlarr Status:</label> </div>
<div class="form-actions" style="margin-top: 4px;"> <div class="settings-section-body" data-stg="indexers">
<button class="test-button" id="prowlarr-test-btn" onclick="testProwlarrConnection()"> <div class="settings-group" data-stg="indexers">
Test Connection <div class="api-service-frame">
</button> <h4 class="service-title prowlarr-title">⬢ Prowlarr</h4>
<button class="test-button" id="prowlarr-refresh-indexers-btn" onclick="loadProwlarrIndexers()" style="margin-left: 6px;"> <div class="setting-help-text" style="margin-bottom: 14px;">
Refresh Indexer List Prowlarr manages your Usenet and torrent indexers and exposes them through one API. SoulSync uses Prowlarr to search across every indexer at once for the Torrent and Usenet download sources.
</button> <br><br>
<span id="prowlarr-connection-status" class="setting-help-text" style="margin-left: 8px;"></span> Don't have it? Grab Prowlarr from <code>prowlarr.com</code> (or your *arr stack). You point Prowlarr at your indexers, then point SoulSync at Prowlarr.
</div> </div>
</div> <div class="form-group">
<div class="form-group"> <label>Prowlarr URL:</label>
<label>Configured Indexers:</label> <input type="text" id="prowlarr-url" placeholder="http://localhost:9696">
<div id="prowlarr-indexer-list" class="setting-help-text" style="margin-top: 4px;"> <div class="setting-help-text">
Connect to Prowlarr and click <strong>Refresh Indexer List</strong> to see the indexers SoulSync can search. Full URL to your Prowlarr instance (e.g. <code>http://192.168.1.100:9696</code>).
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Restrict to indexer IDs (optional):</label> <label>API Key:</label>
<input type="text" id="prowlarr-indexer-ids" placeholder="e.g. 1,3,7 (blank = all enabled indexers)"> <input type="password" id="prowlarr-api-key" placeholder="Your Prowlarr API key">
<div class="setting-help-text"> <div class="setting-help-text">
Comma-separated Prowlarr indexer IDs. Leave blank to search every enabled indexer. Restrict when you have a private tracker you want to prioritise or a noisy public one to exclude. Found in Prowlarr → Settings → General → Security → API Key.
</div>
</div>
<div class="form-group">
<label>Restrict to indexer IDs (optional):</label>
<input type="text" id="prowlarr-indexer-ids" placeholder="e.g. 1,3,7 (blank = all enabled indexers)">
<div class="setting-help-text">
Comma-separated Prowlarr indexer IDs. Leave blank to search every enabled indexer. Restrict when you have a private tracker you want to prioritise or a noisy public one to exclude.
</div>
</div>
<div class="form-group">
<label>Status:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="test-button" id="prowlarr-test-btn" onclick="testProwlarrConnection()">
Test Connection
</button>
<button class="test-button" id="prowlarr-refresh-indexers-btn" onclick="loadProwlarrIndexers()" style="margin-left: 6px;">
Refresh Indexer List
</button>
<span id="prowlarr-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
<div class="form-group">
<label>Configured Indexers:</label>
<div id="prowlarr-indexer-list" class="ind-indexer-list">
Connect to Prowlarr and click <strong>Refresh Indexer List</strong> to see the indexers SoulSync can search.
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- ═══ TORRENT CLIENT ═══ --> <!-- ─── TORRENT CLIENT section ─── -->
<div class="settings-group" data-stg="indexers"> <div class="settings-section-header collapsed" data-stg="indexers" onclick="this.classList.toggle('collapsed'); const b=this.nextElementSibling; b.classList.toggle('collapsed'); b.style.display=b.classList.contains('collapsed')?'none':''">
<span class="settings-section-arrow">&#9660;</span>
<h3>🧲 Torrent Client</h3> <h3>🧲 Torrent Client</h3>
<div class="setting-help-text" style="margin-bottom: 10px;"> <span class="settings-section-hint">qBittorrent · Transmission · Deluge</span>
Where SoulSync sends torrents once Prowlarr finds them. Pick the client you already use — only one can be active at a time. Add the URL of the client's WebUI, log in if it asks for credentials, and hit Test Connection. <span class="ind-status-dot ind-status-dot-unknown" id="torrent-client-status-dot" title="Not tested"></span>
</div> </div>
<div class="form-group"> <div class="settings-section-body collapsed" data-stg="indexers">
<label>Client Type:</label> <div class="settings-group" data-stg="indexers">
<select id="torrent-client-type" class="form-select"> <div class="api-service-frame">
<option value="qbittorrent">qBittorrent</option> <h4 class="service-title torrent-title">⬢ Torrent Downloader</h4>
<option value="transmission">Transmission</option> <div class="setting-help-text" style="margin-bottom: 14px;">
<option value="deluge">Deluge 2.x</option> Where SoulSync sends torrents once Prowlarr finds them. Pick the client you already use — only one can be active at a time. Add the URL of the client's WebUI, log in if it asks for credentials, and hit Test Connection.
</select> </div>
<div class="setting-help-text" id="torrent-client-help"> <div class="form-group">
qBittorrent: WebUI port (default 8080). Transmission: RPC port (default 9091). Deluge: WebUI port (default 8112). <label>Client Type:</label>
</div> <select id="torrent-client-type" class="form-select">
</div> <option value="qbittorrent">qBittorrent</option>
<div class="form-group"> <option value="transmission">Transmission</option>
<label>WebUI URL:</label> <option value="deluge">Deluge 2.x</option>
<input type="text" id="torrent-client-url" placeholder="http://localhost:8080"> </select>
</div> <div class="setting-help-text" id="torrent-client-help">
<div class="form-group" id="torrent-client-username-group"> qBittorrent: WebUI port (default 8080). Transmission: RPC port (default 9091). Deluge: WebUI port (default 8112).
<label>Username:</label> </div>
<input type="text" id="torrent-client-username" placeholder="leave blank if WebUI doesn't require auth"> </div>
<div class="setting-help-text"> <div class="form-group">
qBittorrent and Transmission use username + password. Deluge uses password only — paste it in the password field below. <label>WebUI URL:</label>
</div> <input type="text" id="torrent-client-url" placeholder="http://localhost:8080">
</div> </div>
<div class="form-group"> <div class="form-group" id="torrent-client-username-group">
<label>Password:</label> <label>Username:</label>
<input type="password" id="torrent-client-password" placeholder="WebUI password"> <input type="text" id="torrent-client-username" placeholder="leave blank if WebUI doesn't require auth">
</div> <div class="setting-help-text">
<div class="form-group"> qBittorrent and Transmission use username + password. Deluge uses password only — paste it in the password field below.
<label>Category / Label:</label> </div>
<input type="text" id="torrent-client-category" placeholder="soulsync" value="soulsync"> </div>
<div class="setting-help-text"> <div class="form-group">
SoulSync tags every torrent it adds with this label so it's easy to spot in your client. Deluge needs the Label plugin installed for this to stick. <label>Password:</label>
</div> <input type="password" id="torrent-client-password" placeholder="WebUI password">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Save Path (optional):</label> <label>Category / Label:</label>
<input type="text" id="torrent-client-save-path" placeholder="leave blank to use the client's default"> <input type="text" id="torrent-client-category" placeholder="soulsync" value="soulsync">
<div class="setting-help-text"> <div class="setting-help-text">
Override where the torrent client writes downloads. This path is on the <strong>torrent client's</strong> machine, not SoulSync's. SoulSync tags every torrent it adds with this label so it's easy to spot in your client. Deluge needs the Label plugin installed for this to stick.
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Torrent Client Status:</label> <label>Save Path (optional):</label>
<div class="form-actions" style="margin-top: 4px;"> <input type="text" id="torrent-client-save-path" placeholder="leave blank to use the client's default">
<button class="test-button" id="torrent-client-test-btn" onclick="testTorrentClientConnection()"> <div class="setting-help-text">
Test Connection Override where the torrent client writes downloads. This path is on the <strong>torrent client's</strong> machine, not SoulSync's.
</button> </div>
<span id="torrent-client-connection-status" class="setting-help-text" style="margin-left: 8px;"></span> </div>
<div class="form-group">
<label>Status:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="test-button" id="torrent-client-test-btn" onclick="testTorrentClientConnection()">
Test Connection
</button>
<span id="torrent-client-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- ═══ USENET CLIENT ═══ --> <!-- ─── USENET CLIENT section ─── -->
<div class="settings-group" data-stg="indexers"> <div class="settings-section-header collapsed" data-stg="indexers" onclick="this.classList.toggle('collapsed'); const b=this.nextElementSibling; b.classList.toggle('collapsed'); b.style.display=b.classList.contains('collapsed')?'none':''">
<span class="settings-section-arrow">&#9660;</span>
<h3>📰 Usenet Client</h3> <h3>📰 Usenet Client</h3>
<div class="setting-help-text" style="margin-bottom: 10px;"> <span class="settings-section-hint">SABnzbd · NZBGet</span>
Where SoulSync sends NZBs once Prowlarr finds them. Pick one usenet downloader. SABnzbd uses an API key for auth; NZBGet uses a username + password. <span class="ind-status-dot ind-status-dot-unknown" id="usenet-client-status-dot" title="Not tested"></span>
</div> </div>
<div class="form-group"> <div class="settings-section-body collapsed" data-stg="indexers">
<label>Client Type:</label> <div class="settings-group" data-stg="indexers">
<select id="usenet-client-type" class="form-select" onchange="updateUsenetClientUI()"> <div class="api-service-frame">
<option value="sabnzbd">SABnzbd</option> <h4 class="service-title usenet-title">⬢ Usenet Downloader</h4>
<option value="nzbget">NZBGet</option> <div class="setting-help-text" style="margin-bottom: 14px;">
</select> Where SoulSync sends NZBs once Prowlarr finds them. Pick one usenet downloader. SABnzbd uses an API key for auth; NZBGet uses a username + password.
<div class="setting-help-text"> </div>
SABnzbd: default WebUI port 8080. NZBGet: default WebUI port 6789. <div class="form-group">
</div> <label>Client Type:</label>
</div> <select id="usenet-client-type" class="form-select" onchange="updateUsenetClientUI()">
<div class="form-group"> <option value="sabnzbd">SABnzbd</option>
<label>WebUI URL:</label> <option value="nzbget">NZBGet</option>
<input type="text" id="usenet-client-url" placeholder="http://localhost:8080"> </select>
</div> <div class="setting-help-text">
<div class="form-group" id="usenet-apikey-group"> SABnzbd: default WebUI port 8080. NZBGet: default WebUI port 6789.
<label>API Key:</label> </div>
<input type="password" id="usenet-client-api-key" placeholder="SABnzbd API key"> </div>
<div class="setting-help-text"> <div class="form-group">
SABnzbd → Config → General → API Key. Used by SABnzbd only. <label>WebUI URL:</label>
</div> <input type="text" id="usenet-client-url" placeholder="http://localhost:8080">
</div> </div>
<div class="form-group" id="usenet-username-group" style="display: none;"> <div class="form-group" id="usenet-apikey-group">
<label>Username:</label> <label>API Key:</label>
<input type="text" id="usenet-client-username" placeholder="NZBGet WebUI username"> <input type="password" id="usenet-client-api-key" placeholder="SABnzbd API key">
</div> <div class="setting-help-text">
<div class="form-group" id="usenet-password-group" style="display: none;"> SABnzbd → Config → General → API Key. Used by SABnzbd only.
<label>Password:</label> </div>
<input type="password" id="usenet-client-password" placeholder="NZBGet WebUI password"> </div>
</div> <div class="form-group" id="usenet-username-group" style="display: none;">
<div class="form-group"> <label>Username:</label>
<label>Category / Label:</label> <input type="text" id="usenet-client-username" placeholder="NZBGet WebUI username">
<input type="text" id="usenet-client-category" placeholder="soulsync" value="soulsync"> </div>
<div class="setting-help-text"> <div class="form-group" id="usenet-password-group" style="display: none;">
SoulSync tags every NZB with this category so it ends up in a predictable post-processing folder. <label>Password:</label>
</div> <input type="password" id="usenet-client-password" placeholder="NZBGet WebUI password">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Usenet Client Status:</label> <label>Category / Label:</label>
<div class="form-actions" style="margin-top: 4px;"> <input type="text" id="usenet-client-category" placeholder="soulsync" value="soulsync">
<button class="test-button" id="usenet-client-test-btn" onclick="testUsenetClientConnection()"> <div class="setting-help-text">
Test Connection SoulSync tags every NZB with this category so it ends up in a predictable post-processing folder.
</button> </div>
<span id="usenet-client-connection-status" class="setting-help-text" style="margin-left: 8px;"></span> </div>
<div class="form-group">
<label>Status:</label>
<div class="form-actions" style="margin-top: 4px;">
<button class="test-button" id="usenet-client-test-btn" onclick="testUsenetClientConnection()">
Test Connection
</button>
<span id="usenet-client-connection-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -3415,6 +3415,7 @@ function closeHelperSearch() {
const WHATS_NEW = { const WHATS_NEW = {
'2.6.0': [ '2.6.0': [
{ unreleased: true }, { unreleased: true },
{ title: 'Indexers & Downloaders tab restyled with collapsible sections', desc: 'tab now opens with an intro hero card explaining the flow (indexers find releases → downloader fetches them → SoulSync imports) and folds the rest into three collapsible sections: Indexers, Torrent Client, Usenet Client. each section gets a per-service color accent (Prowlarr orange, torrent sky-blue, usenet violet), a status dot in the header (green when Test Connection succeeds, red on failure, grey before testing), and Lidarr-style indexer cards with protocol badges instead of the inline emoji list. Indexers section is open by default; the downloader sections start collapsed since not everyone uses both protocols.' },
{ title: 'Regression tests for the new indexer + downloader plumbing', desc: 'mocked unit tests for Prowlarr + all five downloader adapters (qBittorrent, Transmission, Deluge, SABnzbd, NZBGet). 54 tests pin the state-mapping tables, the parse logic, and the protocol quirks each client needs handled (qBit Referer header, Transmission session-id renegotiation, Deluge magnet-vs-URL method split, SAB queue+history merge, NZBGet 64-bit size fields). next time anyone touches one of these adapters, CI catches breakage before it hits a real downloader.' }, { title: 'Regression tests for the new indexer + downloader plumbing', desc: 'mocked unit tests for Prowlarr + all five downloader adapters (qBittorrent, Transmission, Deluge, SABnzbd, NZBGet). 54 tests pin the state-mapping tables, the parse logic, and the protocol quirks each client needs handled (qBit Referer header, Transmission session-id renegotiation, Deluge magnet-vs-URL method split, SAB queue+history merge, NZBGet 64-bit size fields). next time anyone touches one of these adapters, CI catches breakage before it hits a real downloader.' },
{ title: 'Usenet client adapters (SABnzbd, NZBGet)', desc: 'third commit in the torrent + usenet rollout. SoulSync now talks to SABnzbd and NZBGet through a sibling adapter contract that mirrors the torrent adapter set — pick one downloader in Settings → Indexers & Downloaders, fill in its API key (SABnzbd) or username + password (NZBGet), and Test Connection confirms the link. all three layers are now stood up: Prowlarr finds releases, the torrent adapter and the usenet adapter each know how to ship work to the underlying client. next commit wires Prowlarr search → adapter dispatch → archive extraction so the new sources actually download. job state mapping covers SABnzbd queue + history and NZBGet groups + history, including the verify/repair/unpack phases that are unique to usenet.' }, { title: 'Usenet client adapters (SABnzbd, NZBGet)', desc: 'third commit in the torrent + usenet rollout. SoulSync now talks to SABnzbd and NZBGet through a sibling adapter contract that mirrors the torrent adapter set — pick one downloader in Settings → Indexers & Downloaders, fill in its API key (SABnzbd) or username + password (NZBGet), and Test Connection confirms the link. all three layers are now stood up: Prowlarr finds releases, the torrent adapter and the usenet adapter each know how to ship work to the underlying client. next commit wires Prowlarr search → adapter dispatch → archive extraction so the new sources actually download. job state mapping covers SABnzbd queue + history and NZBGet groups + history, including the verify/repair/unpack phases that are unique to usenet.' },
{ title: 'Torrent client adapters (qBittorrent, Transmission, Deluge)', desc: 'second commit in the torrent + usenet rollout. SoulSync can now talk to any of the three big torrent clients through a single adapter contract — pick which one you use in Settings → Indexers & Downloaders, paste your WebUI URL and credentials, and Test Connection confirms the link. each adapter handles its own auth quirk (qBit cookies, Transmission session-id, Deluge JSON-RPC) and maps native state strings onto a uniform set so the rest of the app stays client-agnostic. no download wiring yet — that gets layered on once the usenet client adapters land in the next commit.' }, { title: 'Torrent client adapters (qBittorrent, Transmission, Deluge)', desc: 'second commit in the torrent + usenet rollout. SoulSync can now talk to any of the three big torrent clients through a single adapter contract — pick which one you use in Settings → Indexers & Downloaders, paste your WebUI URL and credentials, and Test Connection confirms the link. each adapter handles its own auth quirk (qBit cookies, Transmission session-id, Deluge JSON-RPC) and maps native state strings onto a uniform set so the rest of the app stays client-agnostic. no download wiring yet — that gets layered on once the usenet client adapters land in the next commit.' },

View file

@ -3582,6 +3582,22 @@ async function testLidarrConnection() {
} }
} }
function _setIndStatusDot(dotId, state) {
const dot = document.getElementById(dotId);
if (!dot) return;
dot.classList.remove('ind-status-dot-unknown', 'ind-status-dot-connected', 'ind-status-dot-error');
if (state === 'connected') {
dot.classList.add('ind-status-dot-connected');
dot.title = 'Connected';
} else if (state === 'error') {
dot.classList.add('ind-status-dot-error');
dot.title = 'Connection failed';
} else {
dot.classList.add('ind-status-dot-unknown');
dot.title = 'Not tested';
}
}
async function testProwlarrConnection() { async function testProwlarrConnection() {
const statusEl = document.getElementById('prowlarr-connection-status'); const statusEl = document.getElementById('prowlarr-connection-status');
if (!statusEl) return; if (!statusEl) return;
@ -3598,14 +3614,17 @@ async function testProwlarrConnection() {
if (data.success) { if (data.success) {
statusEl.textContent = data.message || 'Connected'; statusEl.textContent = data.message || 'Connected';
statusEl.style.color = '#4caf50'; statusEl.style.color = '#4caf50';
_setIndStatusDot('prowlarr-status-dot', 'connected');
loadProwlarrIndexers(); loadProwlarrIndexers();
} else { } else {
statusEl.textContent = data.error || 'Connection failed'; statusEl.textContent = data.error || 'Connection failed';
statusEl.style.color = '#f44336'; statusEl.style.color = '#f44336';
_setIndStatusDot('prowlarr-status-dot', 'error');
} }
} catch (e) { } catch (e) {
statusEl.textContent = 'Connection error'; statusEl.textContent = 'Connection error';
statusEl.style.color = '#f44336'; statusEl.style.color = '#f44336';
_setIndStatusDot('prowlarr-status-dot', 'error');
} }
} }
@ -3641,13 +3660,16 @@ async function testUsenetClientConnection() {
if (data.success) { if (data.success) {
statusEl.textContent = data.message || 'Connected'; statusEl.textContent = data.message || 'Connected';
statusEl.style.color = '#4caf50'; statusEl.style.color = '#4caf50';
_setIndStatusDot('usenet-client-status-dot', 'connected');
} else { } else {
statusEl.textContent = data.error || 'Connection failed'; statusEl.textContent = data.error || 'Connection failed';
statusEl.style.color = '#f44336'; statusEl.style.color = '#f44336';
_setIndStatusDot('usenet-client-status-dot', 'error');
} }
} catch (e) { } catch (e) {
statusEl.textContent = 'Connection error'; statusEl.textContent = 'Connection error';
statusEl.style.color = '#f44336'; statusEl.style.color = '#f44336';
_setIndStatusDot('usenet-client-status-dot', 'error');
} }
} }
@ -3667,13 +3689,16 @@ async function testTorrentClientConnection() {
if (data.success) { if (data.success) {
statusEl.textContent = data.message || 'Connected'; statusEl.textContent = data.message || 'Connected';
statusEl.style.color = '#4caf50'; statusEl.style.color = '#4caf50';
_setIndStatusDot('torrent-client-status-dot', 'connected');
} else { } else {
statusEl.textContent = data.error || 'Connection failed'; statusEl.textContent = data.error || 'Connection failed';
statusEl.style.color = '#f44336'; statusEl.style.color = '#f44336';
_setIndStatusDot('torrent-client-status-dot', 'error');
} }
} catch (e) { } catch (e) {
statusEl.textContent = 'Connection error'; statusEl.textContent = 'Connection error';
statusEl.style.color = '#f44336'; statusEl.style.color = '#f44336';
_setIndStatusDot('torrent-client-status-dot', 'error');
} }
} }
@ -3694,10 +3719,16 @@ async function loadProwlarrIndexers() {
} }
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const rows = data.indexers.map(idx => { const rows = data.indexers.map(idx => {
const proto = idx.protocol === 'usenet' ? '📰 Usenet' : '🧲 Torrent'; const protoClass = idx.protocol === 'usenet' ? 'ind-indexer-card-proto-usenet' : 'ind-indexer-card-proto-torrent';
const enabled = idx.enable ? '✅' : '⛔'; const protoLabel = idx.protocol === 'usenet' ? 'Usenet' : 'Torrent';
const privacy = idx.privacy ? `<span style="opacity:0.6;">(${esc(idx.privacy)})</span>` : ''; const privacyHTML = idx.privacy ? `<span class="ind-indexer-card-privacy">${esc(idx.privacy)}</span>` : '';
return `<div style="padding:3px 0;">${enabled} <strong>#${esc(idx.id)}</strong> ${esc(idx.name)}${proto} ${privacy}</div>`; const disabledClass = idx.enable ? '' : ' ind-indexer-card-disabled';
return `<div class="ind-indexer-card${disabledClass}">
<span class="ind-indexer-card-id">#${esc(idx.id)}</span>
<span class="ind-indexer-card-name">${esc(idx.name)}</span>
${privacyHTML}
<span class="ind-indexer-card-proto ${protoClass}">${protoLabel}</span>
</div>`;
}).join(''); }).join('');
listEl.innerHTML = rows; listEl.innerHTML = rows;
} catch (e) { } catch (e) {

View file

@ -3243,6 +3243,178 @@ body.helper-mode-active #dashboard-activity-feed:hover {
color: #eb743b; color: #eb743b;
} }
/* Indexers & Downloaders tab — service title colors */
.prowlarr-title {
color: #e07712; /* Prowlarr brand orange */
}
.torrent-title {
color: #5dade2; /* Torrent — sky blue (qBit family) */
}
.usenet-title {
color: #a78bfa; /* Usenet — violet */
}
/* Lidarr-style intro hero card on Indexers & Downloaders tab */
.ind-hero {
padding: 18px 22px;
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.08), rgba(35, 35, 35, 0.6));
border: 1px solid rgba(var(--accent-rgb), 0.15);
}
.ind-hero-title {
font-size: 13px;
font-weight: 700;
letter-spacing: 0.8px;
text-transform: uppercase;
color: rgb(var(--accent-rgb));
margin-bottom: 12px;
}
.ind-hero-flow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
margin-bottom: 12px;
font-size: 13px;
color: rgba(255, 255, 255, 0.85);
}
.ind-hero-step {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 999px;
}
.ind-hero-step-num {
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(var(--accent-rgb), 0.2);
color: rgb(var(--accent-rgb));
font-weight: 700;
font-size: 11px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.ind-hero-arrow {
color: rgba(var(--accent-rgb), 0.55);
font-weight: 700;
}
.ind-hero-sub {
font-size: 12.5px;
color: rgba(255, 255, 255, 0.55);
line-height: 1.55;
}
/* Section-header status dots — Lidarr-style green/red/grey indicator */
.ind-status-dot {
width: 9px;
height: 9px;
border-radius: 50%;
margin-left: 8px;
flex-shrink: 0;
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.2);
}
.ind-status-dot-unknown {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.ind-status-dot-connected {
background: #2ecc71;
box-shadow: 0 0 8px rgba(46, 204, 113, 0.6), 0 0 0 2px rgba(0, 0, 0, 0.2);
}
.ind-status-dot-error {
background: #e74c3c;
box-shadow: 0 0 8px rgba(231, 76, 60, 0.5), 0 0 0 2px rgba(0, 0, 0, 0.2);
}
/* Indexer list — card-per-indexer, Lidarr-style */
.ind-indexer-list {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 4px;
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
}
.ind-indexer-card {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 8px;
transition: all 0.15s ease;
}
.ind-indexer-card:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
}
.ind-indexer-card-id {
font-family: 'SF Mono', Monaco, monospace;
font-size: 11px;
color: rgba(255, 255, 255, 0.4);
background: rgba(0, 0, 0, 0.3);
padding: 2px 6px;
border-radius: 4px;
flex-shrink: 0;
}
.ind-indexer-card-name {
flex: 1;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
}
.ind-indexer-card-proto {
font-size: 11px;
padding: 3px 8px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
flex-shrink: 0;
}
.ind-indexer-card-proto-torrent {
background: rgba(93, 173, 226, 0.15);
color: #5dade2;
border: 1px solid rgba(93, 173, 226, 0.3);
}
.ind-indexer-card-proto-usenet {
background: rgba(167, 139, 250, 0.15);
color: #a78bfa;
border: 1px solid rgba(167, 139, 250, 0.3);
}
.ind-indexer-card-privacy {
font-size: 10.5px;
color: rgba(255, 255, 255, 0.4);
flex-shrink: 0;
}
.ind-indexer-card-disabled {
opacity: 0.45;
}
.acoustid-title { .acoustid-title {
color: #ba55d3; color: #ba55d3;
} }