templates/_settings_modal.html
Rama main · réplica de lectura del repositorio original.
<div id="settings-modal" class="action-modal" hidden>
<div class="action-modal-panel settings-modal-panel" role="dialog" aria-modal="true" aria-labelledby="settings-modal-title">
<div class="settings-modal-header">
<h3 id="settings-modal-title" class="action-modal-title">Settings</h3>
<button type="button" class="modal-close" data-close-modal="settings-modal" aria-label="Close"><span class="material-symbols-rounded" aria-hidden="true">close</span></button>
</div>
<div class="hl-tab-bar settings-tab-bar" role="tablist">
<button type="button" class="hl-tab-btn hl-tab-btn--active" data-settings-tab="account" role="tab" aria-selected="true">Account</button>
<button type="button" class="hl-tab-btn" data-settings-tab="contacts" role="tab" aria-selected="false">Contacts</button>
<button type="button" class="hl-tab-btn" data-settings-tab="integrations" role="tab" aria-selected="false">Integrations<span id="settings-tab-dot-integrations" class="settings-tab-dot" hidden></span></button>
<button type="button" class="hl-tab-btn" data-settings-tab="feeds" role="tab" aria-selected="false">Feeds</button>
<button type="button" class="hl-tab-btn" data-settings-tab="automation" role="tab" aria-selected="false">Automation</button>
<button type="button" class="hl-tab-btn" data-settings-tab="importexport" role="tab" aria-selected="false">Import/Export</button>
<button type="button" class="hl-tab-btn" data-settings-tab="utilities" role="tab" aria-selected="false">Utilities</button>
<button type="button" class="hl-tab-btn" data-settings-tab="tags" role="tab" aria-selected="false">Tags</button>
<button type="button" class="hl-tab-btn" data-settings-tab="stats" role="tab" aria-selected="false">Stats</button>
</div>
<!-- Account tab (profile + appearance + app settings) -->
<div id="settings-tab-account" class="settings-tab-panel">
<div class="sett-account-head">
<img id="sett-avatar-img" class="sett-avatar-img" src="https://www.gravatar.com/avatar/?d=identicon&s=128" alt="Avatar" />
<div class="sett-account-head-rows">
<div class="settings-section settings-section--row sett-profile-row">
<label class="settings-label" for="sett-profile-name">Name</label>
<input type="text" id="sett-profile-name" class="settings-input" placeholder="Your name" maxlength="200" />
<label class="settings-label" for="sett-profile-email">Email</label>
<input type="email" id="sett-profile-email" class="settings-input" placeholder="you@example.com" maxlength="200" />
</div>
<form class="settings-section settings-section--row" method="post" action="/account/username">
{{ csrf_input(request) | safe }}
<label class="settings-label" for="sett-new-username">Username</label>
<input type="text" id="sett-new-username" name="new_username" class="settings-input" value="{{ current_user }}" minlength="4" maxlength="10" required />
<button type="submit" class="settings-save-btn">Change</button>
<p class="settings-hint">Changing your login name doesn't affect your data or API token.</p>
</form>
</div>
</div>
<form class="settings-section settings-section--row settings-section--spaced" method="post" action="/account/password">
{{ csrf_input(request) | safe }}
<label class="settings-label" for="sett-current-password">Password</label>
<div class="settings-row-controls">
<input type="password" id="sett-current-password" name="current_password" class="settings-input" placeholder="Current" autocomplete="current-password" required />
<input type="password" name="new_password" class="settings-input" placeholder="New" autocomplete="new-password" minlength="6" maxlength="36" required />
<input type="password" name="confirm_password" class="settings-input" placeholder="Confirm new" autocomplete="new-password" minlength="6" maxlength="36" required />
</div>
<button type="submit" class="settings-save-btn">Change</button>
</form>
<form class="settings-section settings-section--row settings-section--spaced" method="post" action="/account/api-token/regenerate" onsubmit="return confirm('Regenerate token? Existing clients stop syncing until you update them.');">
{{ csrf_input(request) | safe }}
<label class="settings-label" for="sett-api-token">API token <span class="settings-label-sub">(Fever / GReader)</span></label>
<div class="settings-token-col">
<p class="settings-hint">Configure RSS clients (Capy, Reeder, …) with username <code>{{ current_user }}</code> and this token as the password.</p>
<div class="settings-row-controls">
<span id="sett-api-token" class="settings-current settings-token" title="Your API token">{{ current_api_token }}</span>
<button type="button" id="sett-api-token-copy" class="settings-icon-btn" title="Copy token" aria-label="Copy token"><span class="material-symbols-rounded" aria-hidden="true">content_copy</span></button>
<button type="submit" class="settings-icon-btn" title="Regenerate token" aria-label="Regenerate token"><span class="material-symbols-rounded" aria-hidden="true">refresh</span></button>
</div>
</div>
</form>
<div class="settings-section settings-section--row settings-section--spaced">
<label class="settings-label">Save Article</label>
<div class="settings-token-col">
<p class="settings-hint">Drag <a href="javascript:location.href='{{ request.base_url }}articles/save?url='+encodeURIComponent(location.href)" onclick="return false" title="Drag me to your bookmarks bar">Save to Lectio</a> to your bookmarks bar to save the page you're reading into <strong>Saved Articles</strong>. From a phone share sheet or shortcut, request <code>{{ request.base_url }}api/save?username={{ current_user }}&token=<API token>&url=<article URL></code>.</p>
<p class="settings-hint"><strong>Best capture — browser extension:</strong> install the <a href="https://github.com/mahmoudalwadia/readit-extension" target="_blank" rel="noopener">Readit extension</a> and in its Options set <em>Backend</em> to <code>{{ (request.base_url | string).rstrip('/') }}</code> and <em>Save token</em> to your API token above. One-click saves then ship the <em>rendered</em> page from your browser, so paywalled and bot-walled articles arrive with full text.</p>
</div>
</div>
<h4 class="settings-section-title" style="margin-top:1rem">Appearance</h4>
<div class="settings-section">
<label class="settings-label">Theme</label>
<div class="sett-theme-btns">
<button type="button" id="theme-btn-dark" class="sett-theme-btn" title="Dark mode" aria-label="Dark mode" aria-pressed="false"><span class="material-symbols-rounded" aria-hidden="true">dark_mode</span></button>
<button type="button" id="theme-btn-light" class="sett-theme-btn" title="Light mode" aria-label="Light mode" aria-pressed="false"><span class="material-symbols-rounded" aria-hidden="true">light_mode</span></button>
</div>
<label class="settings-label" for="sett-tz-display">Timezone display</label>
<input type="text" id="sett-tz-display" class="settings-input" list="sett-tz-list" placeholder="Server default" autocomplete="off" />
<datalist id="sett-tz-list"></datalist>
<label class="settings-label" for="sett-auto-read-days">Auto-read old posts (days)</label>
<input type="number" id="sett-auto-read-days" class="settings-input" min="0" step="1" placeholder="0" autocomplete="off" />
<p class="settings-hint" id="sett-auto-read-hint">New posts dated older than this are marked read when fetched. Folders and feeds can only shorten it. 0 disables.</p>
<label class="settings-label" for="sett-portrait-img-width">Portrait image max width (px)</label>
<input type="number" id="sett-portrait-img-width" class="settings-input" min="0" step="10" placeholder="650" autocomplete="off" />
<p class="settings-hint">Caps tall (portrait) article images to this width; wide images stay full-width. 0 disables.</p>
<label class="settings-toggle-row" style="display:flex;align-items:center;gap:0.5rem;grid-column:1 / -1;margin-top:1rem">
<input type="checkbox" id="sett-mark-read-on-new-tab" />
<span class="settings-label" style="margin:0">Mark read when opened in a new tab</span>
</label>
<p class="settings-hint">Ctrl/Cmd-click or middle-click on a post title opens the article in a new tab without opening it here.
Off leaves the post unread.</p>
<label class="settings-toggle-row" style="display:flex;align-items:center;gap:0.5rem;grid-column:1 / -1;margin-top:1rem">
<input type="checkbox" id="sett-proxy-body-images" />
<span class="settings-label" style="margin:0">Proxy article images through the cache</span>
</label>
<p class="settings-hint">Loads every image in an article's body through Lectio's image cache instead of directly from the source site. Stops content blockers from breaking articles whose images they filter, at the cost of more cache traffic and losing high-DPI (srcset) images.</p>
</div>
<h4 class="settings-section-title" style="margin-top:1rem">Feed filtering</h4>
<div class="settings-section">
<label class="settings-toggle-row" style="display:flex;align-items:center;gap:0.5rem;grid-column:1 / -1">
<input type="checkbox" id="sett-hide-locked-comics" />
<span class="settings-label" style="margin:0">Hide webcomic strips locked behind a supporter tier until their unlock date (overrides per-feed setting)</span>
</label>
</div>
<h4 class="settings-section-title" style="margin-top:1rem">Network</h4>
<div class="settings-section">
<label class="settings-label" for="sett-proxy-mode">Outbound proxy</label>
<select id="sett-proxy-mode" class="settings-input">
<option value="">Inherit instance default</option>
<option value="off">Off</option>
<option value="as_needed">As needed</option>
<option value="always">Always</option>
</select>
<p class="settings-hint" id="sett-proxy-mode-hint">Routes your feed fetches through the instance's configured proxy (set by an admin in Administration). "As needed" only escalates a feed after it's been refused outright.</p>
</div>
<div class="settings-actions">
<button type="button" class="settings-save-btn" data-settings-save="account">Save</button>
</div>
</div>
<!-- Contacts tab -->
<div id="settings-tab-contacts" class="settings-tab-panel" hidden>
<p class="settings-hint">Email addresses available when sending articles. <strong>Me</strong> is always available and uses your Profile email. Use ★ to set the default.</p>
<div id="sett-contacts-list" class="sett-contacts-list"></div>
<div class="sett-contact-form">
<input type="text" id="sett-contact-label" class="settings-input" placeholder="Label (e.g. Work, Family)" maxlength="80" />
<input type="email" id="sett-contact-email" class="settings-input" placeholder="address@example.com" maxlength="200" />
<div class="settings-actions">
<button type="button" id="sett-contact-save" class="settings-save-btn">Add</button>
</div>
</div>
</div>
<!-- Integrations tab -->
<div id="settings-tab-integrations" class="settings-tab-panel" hidden>
<div class="feeds-tab-bar" role="tablist">
<button type="button" class="feeds-tab-btn feeds-tab-btn--active" data-int-view="youtube" role="tab" aria-selected="true">YouTube</button>
<button type="button" class="feeds-tab-btn" data-int-view="deviantart" role="tab" aria-selected="false">DeviantArt<span id="settings-tab-dot-deviantart" class="settings-tab-dot" hidden></span></button>
<button type="button" class="feeds-tab-btn" data-int-view="instapaper" role="tab" aria-selected="false">Instapaper</button>
<button type="button" class="feeds-tab-btn" data-int-view="pinterest" role="tab" aria-selected="false">Pinterest</button>
<button type="button" class="feeds-tab-btn" data-int-view="quire" role="tab" aria-selected="false">Quire</button>
<button type="button" class="feeds-tab-btn" data-int-view="reddit" role="tab" aria-selected="false">Reddit</button>
<button type="button" class="feeds-tab-btn" data-int-view="onstar" role="tab" aria-selected="false">On Star</button>
</div>
<div class="feeds-tab-panel" data-int-panel="youtube">
<div class="settings-section settings-section--inline">
<p class="settings-hint" style="grid-column:1 / -1;margin-top:0">API key enables video durations in titles. Add your channel ID / handle to sync subscriptions as feeds — both fields required.</p>
<div class="settings-field">
<label class="settings-label" for="sett-yt-key">API key</label>
<input type="password" id="sett-yt-key" class="settings-input settings-input--secret" placeholder="AIza…" autocomplete="new-password" />
</div>
<div class="settings-field">
<label class="settings-label" for="sett-yt-channel">Channel ID / handle</label>
<input type="text" id="sett-yt-channel" class="settings-input" placeholder="@yourhandle or UCxxxxxx" maxlength="200" />
</div>
<div class="settings-field">
<label class="settings-label" for="sett-yt-folder">Subscription folder</label>
<input type="text" id="sett-yt-folder" class="settings-input" placeholder="YouTube Subscriptions" maxlength="200" />
</div>
<h4 class="settings-section-title" style="flex:0 0 100%;margin-top:1rem">All YouTube feeds (overrides per-feed setting)</h4>
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:0.5rem 1.5rem;flex:0 0 100%">
<label class="settings-toggle-row" style="display:flex;align-items:center;gap:0.5rem">
<input type="checkbox" id="sett-yt-hide-shorts" />
<span class="settings-label" style="margin:0">Hide Shorts</span>
</label>
<label class="settings-toggle-row" style="display:flex;align-items:center;gap:0.5rem">
<input type="checkbox" id="sett-yt-hide-unpremiered" />
<span class="settings-label" style="margin:0">Hide upcoming videos until they premiere</span>
</label>
<label class="settings-toggle-row" style="display:flex;align-items:center;gap:0.5rem">
<input type="checkbox" id="sett-yt-hide-members-only" />
<span class="settings-label" style="margin:0">Hide members-only videos</span>
</label>
</div>
<label class="settings-toggle-row" style="display:flex;align-items:center;gap:0.5rem;grid-column:1 / -1;margin-top:1rem">
<input type="checkbox" id="sett-yt-account-features" />
<span class="settings-label" style="margin:0">Account features in embeds (Share / Watch Later) — requires OAuth below</span>
</label>
<p class="settings-hint" style="grid-column:1 / -1">Register a Web app at <a href="https://console.cloud.google.com/" target="_blank" rel="noopener">Google Cloud Console</a>, add redirect URI <code id="sett-yt-callback-url"></code>, then paste below and Save to enable account features and playlist adds.</p>
<div class="settings-field">
<label class="settings-label" for="sett-yt-oauth-client-id">OAuth Client ID</label>
<input type="text" id="sett-yt-oauth-client-id" class="settings-input" placeholder="166660….apps.googleusercontent.com" maxlength="200" autocomplete="off" />
</div>
<div class="settings-field">
<label class="settings-label" for="sett-yt-oauth-client-secret">OAuth Client Secret</label>
<input type="password" id="sett-yt-oauth-client-secret" class="settings-input settings-input--secret" placeholder="GOCSPX-…" autocomplete="new-password" />
</div>
<div class="settings-da-account" style="grid-column:1 / -1;margin-top:0.25rem" id="sett-yt-oauth-row">
<span id="sett-yt-oauth-status" class="settings-inline-status"></span>
<a id="sett-yt-oauth-connect" class="settings-inline-btn" href="/integrations/youtube/oauth/connect" hidden>Connect YouTube account</a>
<button type="button" id="sett-yt-oauth-disconnect" class="settings-inline-btn" hidden>Disconnect</button>
</div>
<div id="sett-yt-quota" class="settings-yt-quota" style="grid-column:1 / -1" hidden></div>
<p class="settings-hint" style="grid-column:1 / -1" id="sett-yt-quota-hint" hidden>Lectio's own tally — Google gives no live quota read. Resets midnight Pacific.</p>
</div>
</div>
<div class="feeds-tab-panel" data-int-panel="deviantart" hidden>
<div class="settings-section settings-section--inline">
<p class="settings-hint">Register an app at <a href="https://www.deviantart.com/studio/apps" target="_blank" rel="noopener">deviantart.com/studio/apps</a> (use a <strong>Confidential</strong> client so the secret stays visible) to get a Client ID + Secret, then Save. Add <code id="sett-da-callback-url"></code> to the app's OAuth2 Redirect URI whitelist to enable watch-list sync. Subscribe to a gallery by adding a <code>deviantart.com/<user></code> URL like any feed. <button type="button" id="sett-da-verify" class="settings-inline-btn">Verify</button> <span id="sett-da-verify-status" class="settings-inline-status"></span></p>
<div class="settings-field">
<label class="settings-label" for="sett-da-id">Client ID</label>
<input type="text" id="sett-da-id" class="settings-input" placeholder="12345" maxlength="200" autocomplete="off" />
</div>
<div class="settings-field">
<label class="settings-label" for="sett-da-secret">Client Secret</label>
<input type="password" id="sett-da-secret" class="settings-input settings-input--secret" placeholder="your client secret" autocomplete="new-password" />
</div>
<div class="settings-field">
<label class="settings-label" for="sett-da-folder">Watch-feed folder</label>
<input type="text" id="sett-da-folder" class="settings-input" placeholder="DeviantArt" maxlength="200" autocomplete="off" />
</div>
<div class="settings-da-account" style="margin-top:0.5rem">
<span id="sett-da-status" class="settings-inline-status"></span>
<a id="sett-da-connect" class="settings-inline-btn" href="/deviantart/connect" hidden>Connect DeviantArt account</a>
<button type="button" id="sett-da-watchfeed" class="settings-inline-btn" hidden>Add Watch feed</button>
<button type="button" id="sett-da-sync" class="settings-inline-btn" hidden>Sync Watch List</button>
<button type="button" id="sett-da-disconnect" class="settings-inline-btn" hidden>Disconnect</button>
<span id="sett-da-action-status" class="settings-inline-status"></span>
</div>
<div id="sett-da-sync-detail" class="settings-da-sync-detail" hidden style="margin-top:0.6rem">
<div id="sett-da-failed-wrap" hidden>
<p class="settings-hint" style="margin-bottom:0.2rem"><strong>Failed to add</strong> — click to view the artist on DeviantArt:</p>
<ul id="sett-da-failed-list" class="settings-da-artist-list"></ul>
</div>
<div id="sett-da-deactivated-wrap" hidden>
<p class="settings-hint" style="margin-bottom:0.2rem"><strong>Watched but deactivated</strong> — the artist deactivated their DeviantArt account; skipped until it's reactivated (re-checked daily):</p>
<ul id="sett-da-deactivated-list" class="settings-da-artist-list"></ul>
</div>
<div id="sett-da-unwatched-wrap" hidden>
<p class="settings-hint" style="margin-bottom:0.2rem"><strong>Subscribed but no longer watched</strong> — updates paused automatically the first time this was noticed (nothing lost or unsubscribed); re-enable one from Feed Properties to keep following it live, unsubscribe below, or leave it paused:</p>
<ul id="sett-da-unwatched-list" class="settings-da-artist-list"></ul>
<button type="button" id="sett-da-unsubscribe-unwatched" class="settings-inline-btn">Unsubscribe all unwatched</button>
<span id="sett-da-unsubscribe-unwatched-status" class="settings-inline-status"></span>
</div>
</div>
</div>
</div>
<div class="feeds-tab-panel" data-int-panel="instapaper" hidden>
<div class="settings-section settings-section--inline">
<p class="settings-hint">Lectio saves articles via the <a href="https://www.instapaper.com/api/simple" target="_blank" rel="noopener">Instapaper Simple API</a> using your account credentials. No separate API key is needed.</p>
<div class="settings-field">
<label class="settings-label" for="sett-ip-user">Username</label>
<input type="text" id="sett-ip-user" class="settings-input" placeholder="your@email.com" maxlength="200" autocomplete="off" />
</div>
<div class="settings-field">
<label class="settings-label" for="sett-ip-pass">Password</label>
<input type="password" id="sett-ip-pass" class="settings-input settings-input--secret" placeholder="your Instapaper password" autocomplete="new-password" />
</div>
</div>
</div>
<div class="feeds-tab-panel" data-int-panel="pinterest" hidden>
<div class="settings-section settings-section--inline">
<p class="settings-hint" style="grid-column:1 / -1">Connect your Pinterest account to get a <strong>Pin</strong> button on each article that saves it (its lead image, linked back to the source) to a board you pick. Register an app at <a href="https://developers.pinterest.com/apps/" target="_blank" rel="noopener">Pinterest Developers</a> with redirect URI <code id="sett-pinterest-callback-url"></code>, or use the shared instance credentials set in the Administration panel.</p>
<div class="settings-field">
<label class="settings-label" for="sett-pinterest-client-id">OAuth Client ID</label>
<input type="text" id="sett-pinterest-client-id" class="settings-input" placeholder="your app ID" maxlength="200" autocomplete="off" />
</div>
<div class="settings-field">
<label class="settings-label" for="sett-pinterest-client-secret">OAuth Client Secret</label>
<input type="password" id="sett-pinterest-client-secret" class="settings-input settings-input--secret" placeholder="your client secret" autocomplete="new-password" />
</div>
<div class="settings-da-account" style="grid-column:1 / -1;margin-top:0.25rem" id="sett-pinterest-oauth-row">
<span id="sett-pinterest-oauth-status" class="settings-inline-status"></span>
<a id="sett-pinterest-oauth-connect" class="settings-inline-btn" href="/integrations/pinterest/oauth/connect" hidden>Connect Pinterest account</a>
<button type="button" id="sett-pinterest-oauth-disconnect" class="settings-inline-btn" hidden>Disconnect</button>
</div>
</div>
</div>
<div class="feeds-tab-panel" data-int-panel="quire" hidden>
<div class="settings-section settings-section--inline">
<p class="settings-hint" style="grid-column:1 / -1">Connect <a href="https://quire.io" target="_blank" rel="noopener">Quire</a> to get an <strong>Add to Quire</strong> button on each article that creates a task in a project you pick. Register an app at <a href="https://quire.io/apps/dev" target="_blank" rel="noopener">quire.io/apps/dev</a> to get a Client ID + Secret, then Save. Add <code id="sett-quire-callback-url"></code> to the app's Redirect URI whitelist.</p>
<div class="settings-field">
<label class="settings-label" for="sett-quire-id">Client ID</label>
<input type="text" id="sett-quire-id" class="settings-input" placeholder="your client ID" maxlength="200" autocomplete="off" />
</div>
<div class="settings-field">
<label class="settings-label" for="sett-quire-secret">Client Secret</label>
<input type="password" id="sett-quire-secret" class="settings-input settings-input--secret" placeholder="your client secret" autocomplete="new-password" />
</div>
<div class="settings-da-account" style="grid-column:1 / -1;margin-top:0.5rem">
<span id="sett-quire-status" class="settings-inline-status"></span>
<a id="sett-quire-connect" class="settings-inline-btn" href="/quire/connect" hidden>Connect Quire account</a>
<button type="button" id="sett-quire-disconnect" class="settings-inline-btn" hidden>Disconnect</button>
</div>
<div class="settings-field" id="sett-quire-project-row" style="grid-column:1 / -1" hidden>
<label class="settings-label" for="sett-quire-project">Destination project</label>
<select id="sett-quire-project" class="settings-input"><option value="">— pick a project —</option></select>
</div>
<div id="sett-quire-usage" class="settings-yt-quota" style="grid-column:1 / -1" hidden></div>
<p class="settings-hint" id="sett-quire-usage-hint" style="grid-column:1 / -1" hidden>Quire limits API calls per organization by minute and hour; this is Lectio's own running tally of its calls in the last minute/hour. Automations back off as it fills.</p>
</div>
</div>
<div class="feeds-tab-panel" data-int-panel="reddit" hidden>
<div class="settings-section settings-section--inline">
<p class="settings-hint" style="grid-column:1 / -1">Connect your Reddit account to get a <strong>Submit to Reddit</strong> button on each article, and to fetch Reddit feeds via the authenticated API (60 req/min instead of anonymous limits). Create a <strong>web app</strong> at <a href="https://www.reddit.com/prefs/apps" target="_blank" rel="noopener">reddit.com/prefs/apps</a> with redirect URI <code id="sett-reddit-callback-url"></code>, then enter the client ID and secret below and Save. Shared instance credentials are set in the <a href="/administration">Administration panel</a>.</p>
<div class="settings-field">
<label class="settings-label" for="sett-reddit-client-id">Client ID</label>
<input type="text" id="sett-reddit-client-id" class="settings-input" placeholder="your app client ID" maxlength="200" autocomplete="off" />
</div>
<div class="settings-field">
<label class="settings-label" for="sett-reddit-client-secret">Client Secret</label>
<input type="password" id="sett-reddit-client-secret" class="settings-input settings-input--secret" placeholder="your client secret" autocomplete="new-password" />
</div>
<div class="settings-da-account" style="grid-column:1 / -1;margin-top:0.25rem" id="sett-reddit-oauth-row">
<span id="sett-reddit-oauth-status" class="settings-inline-status"></span>
<a id="sett-reddit-oauth-connect" class="settings-inline-btn" href="/integrations/reddit/oauth/connect" hidden>Connect Reddit account</a>
<button type="button" id="sett-reddit-oauth-disconnect" class="settings-inline-btn" hidden>Disconnect</button>
</div>
</div>
</div>
<div class="feeds-tab-panel" data-int-panel="onstar" hidden>
<div class="settings-section">
<p class="settings-hint" style="grid-column:1 / -1">When you ★ star an article, also send it to these destinations. One-way — un-starring doesn't remove it. Each is available once its integration is configured/connected.</p>
<label class="settings-toggle-row" id="sett-star-ip-row" style="display:flex;align-items:center;gap:0.5rem" hidden>
<input type="checkbox" id="sett-star-ip" />
<span class="settings-label" style="margin:0">Save to Instapaper</span>
</label>
<label class="settings-toggle-row" id="sett-star-quire-row" style="display:flex;align-items:center;gap:0.5rem" hidden>
<input type="checkbox" id="sett-star-quire" />
<span class="settings-label" style="margin:0">Add to Quire</span>
</label>
<div class="settings-field" id="sett-star-yt-row" style="grid-column:1 / -1" hidden>
<label class="settings-label" for="sett-star-yt-playlist">Add to YouTube playlist</label>
<select id="sett-star-yt-playlist" class="settings-input"><option value="">— off —</option></select>
</div>
<div class="settings-field" id="sett-star-email-row" style="grid-column:1 / -1" hidden>
<label class="settings-label" for="sett-star-email">Email to (blank = off)</label>
<input type="email" id="sett-star-email" class="settings-input" placeholder="you@example.com" maxlength="200" autocomplete="off" />
</div>
<div class="settings-field" id="sett-star-reddit-row" style="grid-column:1 / -1" hidden>
<label class="settings-label" for="sett-star-reddit-sub">Submit to subreddit (blank = off)</label>
<input type="text" id="sett-star-reddit-sub" class="settings-input" placeholder="r/subreddit" maxlength="100" autocomplete="off" />
</div>
<p class="settings-hint" id="sett-star-none" style="grid-column:1 / -1" hidden>Configure Instapaper, connect YouTube, or set up Email (above / in Settings) to enable on-star destinations.</p>
</div>
</div>
<div class="settings-actions">
<button type="button" class="settings-save-btn" data-settings-save="integrations">Save</button>
</div>
</div>
<!-- Feeds tab -->
<div id="settings-tab-feeds" class="settings-tab-panel" hidden>
{% set needs_replacement_feeds = problematic_feeds | selectattr("needs_replacement") | list %}
{% set _active_problem_feeds = problematic_feeds | rejectattr("needs_replacement") | list %}
{% set failing_feeds = _active_problem_feeds | selectattr("acknowledged_at", "none") | list %}
{% set acked_feeds = _active_problem_feeds | rejectattr("acknowledged_at", "none") | list %}
<div class="feeds-tab-bar" role="tablist">
<button type="button" class="feeds-tab-btn feeds-tab-btn--active" data-feeds-view="folders" role="tab" aria-selected="true">Folders</button>
<button type="button" class="feeds-tab-btn" data-feeds-view="failing" role="tab" aria-selected="false">Failing{% if failing_feeds %} <span class="feeds-tab-count">{{ failing_feeds | length }}</span>{% endif %}</button>
<button type="button" class="feeds-tab-btn" data-feeds-view="deactivated" role="tab" aria-selected="false">Deactivated{% if inactive_feed_count > 0 %} <span class="feeds-tab-count">{{ inactive_feed_count }}</span>{% endif %}</button>
<button type="button" class="feeds-tab-btn" data-feeds-view="stale" role="tab" aria-selected="false">Stale</button>
<button type="button" class="feeds-tab-btn" data-feeds-view="fetch-tiers" role="tab" aria-selected="false">Fetch Tiers</button>
</div>
<div class="feeds-tab-panel" data-feeds-panel="folders">
{# The folders table (a hidden row per feed — megabytes at thousands
of feeds) is NOT rendered inline: it loads on first open of the
Feeds tab from /settings/feeds/panel/folders into the container
below. The search input and sfc toolbar stay server-rendered —
their JS binds to them by id at DOMContentLoaded. #}
<div class="feeds-folder-search">
<span class="material-symbols-rounded feeds-folder-search-icon" aria-hidden="true">search</span>
<input type="search" id="feeds-folder-search-input" class="feeds-folder-search-input" placeholder="Filter folders and feeds…" autocomplete="off" aria-label="Filter folders and feeds" />
</div>
<div id="settings-folders-lazy" data-lazy-src="/settings/feeds/panel/folders">
<p class="muted" style="padding:0.5rem 0">Loading…</p>
</div>
<div class="sfc-toolbar" id="sfc-toolbar" hidden>
<span class="sfc-count" id="sfc-count"></span>
<span class="sfc-actions">
<select id="sfc-move-folder" class="sfc-move-select" aria-label="Move selected feeds to folder">
<option value="">Move to…</option>
{% for opt in folder_options if opt.depth == 1 %}
<option value="{{ opt.id }}">{{ opt.name }}</option>
{% endfor %}
</select>
<button type="button" class="sfc-action-btn" id="sfc-mark-read-btn">Mark read</button>
<button type="button" class="sfc-action-btn" id="sfc-refresh-btn">Refresh</button>
<button type="button" class="sfc-action-btn" id="sfc-disable-btn">Disable</button>
<button type="button" class="sfc-action-btn" id="sfc-enable-btn">Enable</button>
<button type="button" class="sfc-action-btn sfc-action-danger" id="sfc-unsub-btn">Unsubscribe</button>
<button type="button" class="sfc-action-btn" id="sfc-combine-btn" disabled>Combine…</button>
<button type="button" class="sfc-compare-btn" id="sfc-compare-btn" disabled>Compare</button>
</span>
</div>
<div class="sfc-panel" id="sfc-panel" hidden></div>
</div>
<div class="feeds-tab-panel" data-feeds-panel="failing" hidden>
{# Up to LECTIO_FAILING_FEEDS_LIMIT (default 500) rows of markup —
loads on first open of this view, not inline with the page. #}
<div id="settings-failing-lazy" data-lazy-src="/settings/feeds/panel/failing">
<p class="muted" style="padding:0.5rem 0">Loading…</p>
</div>
</div>
<div id="inactive-feeds-modal" class="feeds-tab-panel" data-feeds-panel="deactivated" hidden>
{% if inactive_feeds %}
{% set auto_feeds = inactive_feeds|selectattr("reason")|list %}
{% set own_feeds = inactive_feeds|rejectattr("reason")|list %}
{% for group_title, group in (("Disabled automatically", auto_feeds), ("Disabled by you", own_feeds)) %}
{% if group %}
{% if auto_feeds and own_feeds %}<h4 class="inactive-feed-group">{{ group_title }}</h4>{% endif %}
<ul class="inactive-feed-list">
{% for feed in group %}
<li class="inactive-feed-row">
{% if feed.folder_name %}<span class="inactive-feed-folder">{{ feed.folder_name }}</span>{% endif %}
<button type="button" class="inactive-feed-name"
data-feed-properties-url="{{ feed.feed_url }}" title="{{ feed.feed_url }}">{{ feed.feed_title }}</button>
<span class="inactive-feed-reason{% if feed.reason %} inactive-feed-auto{% endif %}"
>Disabled ({{ feed.disabled_label }})</span>
<form method="post" action="/feeds/enable" style="display:contents">
<input type="hidden" name="feed_url" value="{{ feed.feed_url }}" />
{% if feed.folder_id %}<input type="hidden" name="folder_id" value="{{ feed.folder_id }}" />{% endif %}
<button type="submit" class="inactive-feed-enable-btn">Enable</button>
</form>
</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
{% else %}
<p class="muted">No deactivated feeds.</p>
{% endif %}
</div>
<div class="feeds-tab-panel" data-feeds-panel="stale" hidden>
<p class="muted feeds-stale-intro">Active feeds ranked by how long since their newest post — oldest first. Feeds that have never posted appear at the top.</p>
{# Ranks EVERY active feed — megabytes at thousands of feeds — so the
list loads on first open of this view, not inline with the page. #}
<div id="settings-stale-lazy" data-lazy-src="/settings/feeds/panel/stale">
<p class="muted">Loading…</p>
</div>
</div>
<div class="feeds-tab-panel" data-feeds-panel="fetch-tiers" hidden>
<p class="muted feeds-stale-intro">Which feeds are routing through each outbound-fetch escalation tier right now.</p>
<div id="settings-fetch-tiers-lazy" data-lazy-src="/settings/feeds/panel/fetch-tiers">
<p class="muted">Loading…</p>
</div>
</div>
</div>
<!-- Import/Export tab -->
<div id="settings-tab-importexport" class="settings-tab-panel" hidden>
<div class="feeds-tab-bar" role="tablist" id="migrator-tab-bar">
<button type="button" class="feeds-tab-btn feeds-tab-btn--active" data-migrator-view="general" role="tab" aria-selected="true">General</button>
<button type="button" class="feeds-tab-btn" data-migrator-view="inoreader" role="tab" aria-selected="false">Inoreader</button>
<button type="button" class="feeds-tab-btn" data-migrator-view="miniflux" role="tab" aria-selected="false">Miniflux</button>
<button type="button" class="feeds-tab-btn" data-migrator-view="freshrss" role="tab" aria-selected="false">FreshRSS</button>
<button type="button" class="feeds-tab-btn" data-migrator-view="ttrss" role="tab" aria-selected="false">tt-rss</button>
<button type="button" class="feeds-tab-btn" data-migrator-view="instapaper" role="tab" aria-selected="false">Instapaper</button>
</div>
<!-- General: OPML + Takeout -->
<div class="feeds-tab-panel" data-migrator-panel="general" style="padding:0">
<h4 class="settings-section-title">OPML</h4>
<div class="settings-section settings-importexport-group">
<label for="opml-file-input" class="settings-secondary-btn" style="cursor:pointer">Import OPML</label>
<a class="settings-secondary-btn" href="/opml/export">Export OPML</a>
</div>
<h4 class="settings-section-title" style="margin-top:1.25rem">Takeout</h4>
<div class="settings-section settings-importexport-group">
<a class="settings-secondary-btn" href="/takeout/export"><span class="material-symbols-rounded" style="font-size:15px;vertical-align:-2px" aria-hidden="true">download</span> Export ZIP</a>
<label for="takeout-file-input" class="settings-secondary-btn" style="cursor:pointer"><span class="material-symbols-rounded" style="font-size:15px;vertical-align:-2px" aria-hidden="true">upload</span> Import ZIP</label>
</div>
</div>
<!-- Inoreader migrator -->
<div class="feeds-tab-panel settings-section" data-migrator-panel="inoreader" hidden style="margin-top:0">
<!-- OPML note -->
<p class="settings-hint" style="grid-column:1 / -1;margin-top:0">To import your Inoreader subscriptions list, use <strong>Import OPML</strong> on the General tab. Come back here after feeds are subscribed to import starred items and labels.</p>
<!-- File import -->
<div style="grid-column:1 / -1">
<p class="settings-hint" style="margin:0 0 0.5rem"><strong>Import from files</strong> — Select one or more <code>.json</code> files from the <a href="https://github.com/joshg253/InoreaderExportTool" target="_blank" rel="noopener">ExportTool</a> (one per label), or the <code>.zip</code> from Inoreader's <em>Export everything</em>. Stars and labels apply to articles already fetched by Lectio.</p>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
<input type="file" id="mig-ino-file-input" accept=".json,.zip,application/json,application/zip" multiple style="display:none" />
<button type="button" id="mig-ino-file-pick" class="settings-inline-btn">Browse…</button>
<span id="mig-ino-file-label" class="settings-hint" style="margin:0;flex:1">No files selected</span>
<button type="button" id="mig-ino-upload-btn" class="settings-inline-btn" hidden>Upload & Import</button>
<button type="button" id="mig-ino-local-cancel" class="settings-inline-btn" hidden>Reset</button>
</div>
<div style="margin-top:0.4rem">
<span id="mig-ino-local-status" class="settings-inline-status"></span>
</div>
<div id="mig-ino-progress" class="settings-hint" style="margin:0.25rem 0 0;white-space:pre-wrap" hidden></div>
</div>
<!-- API section -->
<div id="mig-ino-api-section" style="grid-column:1 / -1;border-top:1px solid var(--border-color,#e0e0e0);margin-top:0.75rem;padding-top:0.75rem">
<p class="settings-hint" style="margin:0 0 0.5rem"><strong>API sync (trickle import)</strong> — Pulls subscriptions, labels, and starred items directly from Inoreader. Rate-limited to ~10 calls per scheduler cycle (~250/day free tier). Register an app at <a href="https://www.inoreader.com/developers/" target="_blank" rel="noopener">inoreader.com/developers</a> with redirect URI <code id="sett-inoreader-callback-url"></code>, then save credentials and connect.</p>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;margin-bottom:0.4rem" id="sett-inoreader-oauth-row">
<input type="text" id="sett-inoreader-client-id" class="settings-input" placeholder="Client ID" maxlength="200" autocomplete="off" style="flex:1;min-width:8rem" />
<input type="password" id="sett-inoreader-client-secret" class="settings-input settings-input--secret" placeholder="Client Secret" autocomplete="new-password" style="flex:1;min-width:8rem" />
<button type="button" class="settings-inline-btn" id="mig-ino-creds-save" style="flex:none">Save</button>
<span id="sett-inoreader-status" class="settings-inline-status"></span>
<a id="sett-inoreader-connect" class="settings-inline-btn" href="/integrations/inoreader/oauth/connect" hidden>Connect</a>
<button type="button" id="sett-inoreader-disconnect" class="settings-inline-btn" hidden>Disconnect</button>
</div>
<div id="mig-ino-api-controls">
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
<label style="font-size:0.85em;display:flex;gap:0.35rem;align-items:center">
Since <input type="date" id="mig-ino-api-since" style="font-size:inherit" title="Only fetch items newer than this date. Leave blank to import everything.">
</label>
<button type="button" id="mig-ino-api-start" class="settings-inline-btn">Start API import</button>
<button type="button" id="mig-ino-api-run" class="settings-inline-btn" hidden>Step now</button>
<button type="button" id="mig-ino-api-reset" class="settings-inline-btn" hidden>Reset API import</button>
<span id="mig-ino-api-status" class="settings-inline-status"></span>
</div>
</div>
<p id="mig-ino-api-not-connected" class="settings-hint" style="margin:0.25rem 0 0" hidden>Save credentials and connect to enable API import.</p>
</div>
</div>
<!-- Miniflux migrator -->
<div class="feeds-tab-panel settings-section" data-migrator-panel="miniflux" hidden style="margin-top:0">
<p class="settings-hint" style="grid-column:1 / -1;margin-top:0">Import subscriptions (with folder assignments), starred articles, and article tags from a Miniflux instance. Enter your Miniflux URL and an API token (Settings → API Keys in Miniflux).</p>
<div style="grid-column:1 / -1">
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;margin-bottom:0.4rem">
<input type="url" id="sett-miniflux-url" class="settings-input" placeholder="https://miniflux.example.com" maxlength="500" autocomplete="off" style="flex:2;min-width:12rem" />
<input type="password" id="sett-miniflux-token" class="settings-input settings-input--secret" placeholder="API Token" autocomplete="new-password" style="flex:1;min-width:8rem" />
<button type="button" id="mig-miniflux-save" class="settings-inline-btn" style="flex:none">Save</button>
<button type="button" id="mig-miniflux-test" class="settings-inline-btn" style="flex:none">Test</button>
<span id="mig-miniflux-creds-status" class="settings-inline-status"></span>
</div>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
<button type="button" id="mig-miniflux-start" class="settings-inline-btn">Import</button>
<button type="button" id="mig-miniflux-reset" class="settings-inline-btn" hidden>Reset</button>
<span id="mig-miniflux-status" class="settings-inline-status"></span>
</div>
</div>
</div>
<!-- FreshRSS migrator -->
<div class="feeds-tab-panel settings-section" data-migrator-panel="freshrss" hidden style="margin-top:0">
<p class="settings-hint" style="grid-column:1 / -1;margin-top:0">Import subscriptions (with folder assignments), labels (as article tags), and starred articles from a FreshRSS instance. Use your FreshRSS <strong>API password</strong> (not your regular password) — configure it in FreshRSS → Profile.</p>
<div style="grid-column:1 / -1">
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;margin-bottom:0.4rem">
<input type="url" id="sett-freshrss-url" class="settings-input" placeholder="https://freshrss.example.com" maxlength="500" autocomplete="off" style="flex:2;min-width:12rem" />
<input type="text" id="sett-freshrss-username" class="settings-input" placeholder="Username" maxlength="200" autocomplete="off" style="flex:1;min-width:8rem" />
<input type="password" id="sett-freshrss-password" class="settings-input settings-input--secret" placeholder="API Password" autocomplete="new-password" style="flex:1;min-width:8rem" />
<button type="button" id="mig-freshrss-save" class="settings-inline-btn" style="flex:none">Save</button>
<button type="button" id="mig-freshrss-test" class="settings-inline-btn" style="flex:none">Test</button>
<span id="mig-freshrss-creds-status" class="settings-inline-status"></span>
</div>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
<button type="button" id="mig-freshrss-start" class="settings-inline-btn">Import</button>
<button type="button" id="mig-freshrss-reset" class="settings-inline-btn" hidden>Reset</button>
<span id="mig-freshrss-status" class="settings-inline-status"></span>
</div>
</div>
</div>
<!-- tt-rss migrator -->
<div class="feeds-tab-panel settings-section" data-migrator-panel="ttrss" hidden style="margin-top:0">
<p class="settings-hint" style="grid-column:1 / -1;margin-top:0">Import subscriptions (with category folders), starred articles, and labels from a Tiny Tiny RSS instance. Uses the tt-rss JSON API.</p>
<div style="grid-column:1 / -1">
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;margin-bottom:0.4rem">
<input type="url" id="sett-ttrss-url" class="settings-input" placeholder="https://ttrss.example.com" maxlength="500" autocomplete="off" style="flex:2;min-width:12rem" />
<input type="text" id="sett-ttrss-username" class="settings-input" placeholder="Username" maxlength="200" autocomplete="off" style="flex:1;min-width:8rem" />
<input type="password" id="sett-ttrss-password" class="settings-input settings-input--secret" placeholder="Password" autocomplete="new-password" style="flex:1;min-width:8rem" />
<button type="button" id="mig-ttrss-save" class="settings-inline-btn" style="flex:none">Save</button>
<button type="button" id="mig-ttrss-test" class="settings-inline-btn" style="flex:none">Test</button>
<span id="mig-ttrss-creds-status" class="settings-inline-status"></span>
</div>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
<button type="button" id="mig-ttrss-start" class="settings-inline-btn">Import</button>
<button type="button" id="mig-ttrss-reset" class="settings-inline-btn" hidden>Reset</button>
<span id="mig-ttrss-status" class="settings-inline-status"></span>
</div>
</div>
</div>
<!-- Instapaper migrator: import saved bookmarks into Saved Items -->
<div class="feeds-tab-panel settings-section" data-migrator-panel="instapaper" hidden style="margin-top:0">
<p class="settings-hint" style="grid-column:1 / -1;margin-top:0">Import your Instapaper bookmarks into <strong>Saved Items</strong>. In Instapaper, go to <em>Settings → Export → Download .CSV file</em>, then upload it here. Archived Instapaper articles import as archived saves; custom folders and the Liked/Starred flag become tags. Article text is fetched in the background after import.</p>
<div style="grid-column:1 / -1;display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap">
<label for="instapaper-file-input" class="settings-inline-btn" style="cursor:pointer"><span class="material-symbols-rounded" style="font-size:15px;vertical-align:-2px" aria-hidden="true">upload</span> Upload CSV</label>
</div>
</div>
</div>
<!-- Automation tab -->
<div id="settings-tab-automation" class="settings-tab-panel" hidden>
<div class="hl-tab-bar" role="tablist">
<button type="button" class="hl-tab-btn hl-tab-btn--active" data-hl-tab="rules" role="tab" aria-selected="true">Rules</button>
<button type="button" class="hl-tab-btn" data-hl-tab="history" role="tab" aria-selected="false">History</button>
</div>
<div id="hl-tab-rules" class="hl-tab-panel">
<p class="highlights-hint">Rules match article titles (or body) and trigger actions automatically. Matches are case-insensitive. Enable <strong>.*</strong> for regex.</p>
{# Filter the list to one rule type. Populated by hlRenderTypeFilter —
only types that actually have rules get a chip, so the row stays
short on a library that only uses three of them. #}
<div id="hl-rule-type-filter" class="hl-type-filter" role="tablist" aria-label="Filter rules by type"></div>
<div id="hl-rules-list" class="hl-rules-list"></div>
<button type="button" id="hl-add-rule-btn" class="hl-add-rule-btn">+ Add Rule</button>
{# Suggestion-with-preview, never automatic (decided 2026-08-24) —
see Plan.md "Utilities: find rules that could be one rule". Hidden
until hlLoadSuggestions() finds something; nothing here is ever
pre-checked or applied without a click. #}
<div id="hl-suggestions" class="hl-suggestions" hidden>
<div class="hl-section-label">Suggestions</div>
<div id="hl-suggestions-list"></div>
</div>
</div>
<div id="hl-tab-history" class="hl-tab-panel" hidden>
<div id="hl-history-list" class="hl-history-list"><p class="hl-empty">Loading…</p></div>
</div>
</div>
<!-- Stats tab -->
<!-- Utilities tab -->
<div id="settings-tab-utilities" class="settings-tab-panel" hidden>
<div class="feeds-dedup-panel">
<div class="feeds-dedup-controls">
<div class="utility-group">
<h4 class="utility-group-title">Feeds</h4>
<button type="button" id="dedup-feeds-btn" class="settings-secondary-btn" title="Find feeds subscribed more than once under URL variants">Scan for duplicate feeds</button>
<button type="button" id="lazy-titles-btn" class="settings-secondary-btn" title="Queue a title refresh for feeds titled with a raw URL, and list feeds titled just "News", "Updates", etc. so you can prefix the site name">Fix feed titles</button>
<button type="button" id="multi-folder-btn" class="settings-secondary-btn" title="Find feeds that ended up in more than one folder and keep only one">Fix multi-folder feeds</button>
</div>
<div class="utility-group">
<h4 class="utility-group-title">Saved articles</h4>
<button type="button" id="saved-dedup-btn" class="settings-secondary-btn" title="Find the same article saved more than once under different URLs">Scan Saved for duplicates</button>
<button type="button" id="saved-autofile-btn" class="settings-secondary-btn" title="File unfiled saved articles onto the subscribed feed they came from">File saved articles</button>
<button type="button" id="unstar-tagged-btn" class="settings-secondary-btn" title="Find starred articles that also carry a tag — the tag already keeps them, so the star is redundant">Unstar tagged articles</button>
<button type="button" id="archive-old-btn" class="settings-secondary-btn" title="Clear the Saved Inbox of stars you are plainly not getting to — archives them, keeping tags and the offline copy">Archive old stars</button>
</div>
<div class="utility-group">
<h4 class="utility-group-title">Clean-up</h4>
<button type="button" id="purge-old-btn" class="settings-secondary-btn" title="Delete old posts from selected folders (starred and tagged posts are kept)">Purge old posts</button>
</div>
<div class="utility-group">
<h4 class="utility-group-title">YouTube</h4>
<button type="button" id="yt-dupes-btn" class="settings-secondary-btn" data-requires="youtube" title="Remove duplicate videos from your YouTube playlists, and optionally deleted, private and members-only ones">Clean up YouTube playlists</button>
<button type="button" id="backfill-hide-shorts-btn" class="settings-secondary-btn" title="Mark all unread Shorts as read on feeds that have Hide Shorts enabled">Clean up Shorts</button>
</div>
</div>
<div id="dedup-inline-results" hidden>
<p class="action-modal-label dedup-modal-intro"></p>
<details class="saved-dedup-section dedup-same-section" open hidden>
<summary>Same folder <span class="saved-dedup-section-count"></span></summary>
<p class="action-modal-hint">Nothing is pre-selected — Compare before combining. Click a feed to open its Properties.</p>
<div id="dedup-same-list" class="dedup-title-list"></div>
</details>
<details class="saved-dedup-section dedup-cross-section" open hidden>
<summary>Different folders <span class="saved-dedup-section-count"></span></summary>
<p class="action-modal-hint">Nothing is pre-selected — Compare before combining. Click a feed to open its Properties.</p>
<div id="dedup-cross-list" class="dedup-title-list"></div>
</details>
<details class="saved-dedup-section dedup-upgrade-section" open hidden>
<summary>Format upgrade <span class="saved-dedup-section-count"></span></summary>
<p class="action-modal-hint">A format-selector URL (?alt=rss, ?type=atom, ...) isn't always worse than its default — compare before switching. Same-family alternates (e.g. atom for a WordPress ?feed=rss2 subscription) are included too.</p>
<div id="dedup-upgrade-list" class="dedup-title-list"></div>
</details>
<details class="saved-dedup-section dedup-query-section" open hidden>
<summary>Same address, different query <span class="saved-dedup-section-count"></span></summary>
<p class="action-modal-hint">A query param can be a real duplicate (format selector) or genuinely different content (a category/tag feed) — nothing here is pre-selected. Compare before combining.</p>
<div id="dedup-query-list" class="dedup-title-list"></div>
</details>
<details class="saved-dedup-section dedup-title-section" open hidden>
<summary>Same title, different feed <span class="saved-dedup-section-count"></span></summary>
<p class="action-modal-hint">Review by hand — a same-title pair can legitimately be a site's blog and its own podcast, so nothing here is ever auto-applied. Click a feed to open its Properties.</p>
<div id="dedup-title-list" class="dedup-title-list"></div>
</details>
<details class="saved-dedup-section dedup-dismissed-section" hidden>
<summary>Dismissed as "not dupes" <span class="saved-dedup-section-count"></span></summary>
<p class="action-modal-hint">Un-dismiss to let a group be suggested again.</p>
<div id="dedup-dismissed-list" class="dedup-title-list"></div>
</details>
</div>
<div id="lazy-titles-results" hidden>
<p class="action-modal-label lazy-titles-intro"></p>
<div id="lazy-titles-list" class="lazy-titles-list"></div>
</div>
<div id="saved-dedup-results" hidden>
<p class="action-modal-label saved-dedup-intro"></p>
<details class="saved-dedup-section saved-dedup-confirmed-section" open hidden>
<summary>Confirmed <span class="saved-dedup-section-count"></span></summary>
<div class="saved-dedup-confirmed-list"></div>
</details>
<details class="saved-dedup-section saved-dedup-possible-section" open hidden>
<summary>Possible <span class="saved-dedup-section-count"></span></summary>
<p class="action-modal-label saved-dedup-possible-intro"></p>
<p class="action-modal-hint">Every copy starts on Keep — switch the ones you want removed to Delete.</p>
<div class="saved-dedup-possible-list"></div>
</details>
<div class="dedup-inline-actions">
<button type="button" id="saved-dedup-checkall" class="settings-secondary-btn" hidden title="Probe every group's URLs (throttled, one group at a time) — a confirmed-dead copy is switched to Delete (never the sole copy with real stored content); live and inconclusive copies stay on Keep">Check all URLs</button>
<button type="button" id="saved-dedup-ok" class="danger" hidden>Delete copies marked Delete</button>
</div>
</div>
<div id="yt-dupes-panel" hidden>
<p class="action-modal-label">Remove duplicate videos from the playlists you pick, keeping the first copy of each. Nothing is pre-selected.</p>
<p id="yt-dupes-status" class="action-modal-hint"></p>
<div class="dedup-inline-actions yt-dupes-actions">
<label class="dedup-rescue-label"><input type="checkbox" id="yt-dupes-select-all"> Select all</label>
</div>
<div id="yt-dupes-list" class="dedup-title-list"></div>
<label class="dedup-rescue-label"><input type="checkbox" id="yt-dupes-unavailable"> Also remove deleted and private videos, and members-only ones Lectio has already identified</label>
<label class="dedup-rescue-label"><input type="checkbox" id="yt-dupes-region-on"> Also remove videos blocked in my country: <input type="text" id="yt-dupes-region" value="US" maxlength="2" class="feed-prop-title-input" style="width:3rem;display:inline-block" aria-label="Two-letter country code" disabled></label>
<p class="action-modal-hint">Uses the country blocks YouTube lists for each video. It can't see every rights-holder block, so a few unwatchable videos may remain.</p>
<div class="dedup-inline-actions">
<button type="button" id="yt-dupes-run" class="danger" disabled>Clean up selected playlists</button>
</div>
</div>
<div id="purge-old-panel" hidden>
<p class="action-modal-label">Delete posts older than a date from the selected folders. Starred and manually tagged posts are never deleted.</p>
<div class="purge-old-controls">
<select id="purge-folders" multiple size="6" class="feed-prop-strategy-select" aria-label="Folders to purge">
{% for opt in folder_options if opt.depth > 0 %}
<option value="{{ opt.id }}">{{ opt.name }}</option>
{% endfor %}
</select>
<div class="purge-old-opts">
<label>Older than <input type="date" id="purge-before" class="feed-prop-title-input" style="width:auto"></label>
<label class="dedup-rescue-label"><input type="checkbox" id="purge-include-unread"> Also delete unread posts</label>
<div class="dedup-inline-actions">
<button type="button" id="purge-preview-btn" class="settings-secondary-btn">Preview</button>
<button type="button" id="purge-run-btn" class="danger" disabled>Purge</button>
</div>
<p id="purge-result" class="action-modal-hint"></p>
</div>
</div>
</div>
<div id="multi-folder-results" hidden>
<p class="action-modal-label multi-folder-intro"></p>
<div class="multi-folder-list"></div>
<div class="dedup-inline-actions">
<button type="button" id="multi-folder-ok" class="danger" hidden>Keep selected folders</button>
</div>
</div>
<!-- Unstar tagged: the checkbox selects a tag to CLEAR, not to keep.
The API takes an opt-out (keep_tags), but rendering 58 tags
pre-checked would make "unstar everything" the default action and
unchecking the destructive act. Positive selection keeps the
bulk-action rule intact: nothing checked, nothing happens. The JS
inverts the selection into keep_tags before calling. -->
<div id="unstar-tagged-results" hidden>
<p class="action-modal-label unstar-tagged-intro"></p>
<p class="action-modal-hint">Nothing is preselected — check the tags whose articles should lose their star. The tag still keeps the article: it stays archived, tagged, and safe from pruning. Only the star is removed.</p>
<p class="action-modal-hint unstar-tagged-warning" hidden></p>
<div class="unstar-tagged-list"></div>
<div class="dedup-inline-actions">
<button type="button" id="unstar-tagged-selectall" class="settings-secondary-btn" hidden title="Check every topical tag, leaving any queue-like tag unchecked">Select all topical tags</button>
<button type="button" id="unstar-tagged-ok" class="danger" hidden disabled>Unstar selected</button>
</div>
</div>
<!-- Archive old stars: the Inbox bankruptcy pass. A cutoff rather than
a checklist, because the whole point is that these are items you
are never going to review individually. Archiving (not unstarring)
is what keeps the tag, the offline copy and pruning-exemption, and
it is reversible per item — so this is safe in a way #5's unstar
sweep was not. -->
<div id="archive-old-results" hidden>
<p class="action-modal-label archive-old-intro"></p>
<p class="action-modal-hint">A star means <em>to-do</em>. Anything starred long ago is not something you are still getting to, so this archives it: it leaves the Inbox and is marked read, while its tags, its offline copy and its protection from cleanup all stay. Un-archive any item to put it back.</p>
<p class="action-modal-hint archive-old-caveat" hidden><strong>Check the age spread before running this.</strong> The star date was rewritten for everything carried over by an import or migration — so a large cluster in one month usually means “when the data moved”, not when you starred it.</p>
<div class="archive-old-basis">
<span class="action-modal-label">Age by</span>
<span id="archive-old-basis-choices" class="archive-old-choices">
<button type="button" class="settings-secondary-btn archive-old-basis-choice active" data-basis="published" aria-pressed="true" title="How long ago the article was published">Publish date</button>
<button type="button" class="settings-secondary-btn archive-old-basis-choice" data-basis="saved" aria-pressed="false" title="How long ago it was starred — unreliable for anything carried over by an import or migration">Star date</button>
</span>
</div>
<div class="archive-old-cutoff">
<span class="action-modal-label">Archive stars older than</span>
<span id="archive-old-choices" class="archive-old-choices"></span>
</div>
<div class="archive-old-list"></div>
<div class="dedup-inline-actions">
<button type="button" id="archive-old-ok" class="danger" hidden disabled>Archive</button>
</div>
</div>
<div id="saved-autofile-results" hidden>
<p class="action-modal-label saved-autofile-intro"></p>
<p class="action-modal-hint">Nothing is preselected — check the hosts you want to file. Largest first, and each row says how strong the match is. Filing a batch and re-scanning is the intended way to work through this.</p>
<div class="saved-autofile-list"></div>
<div class="dedup-inline-actions">
<button type="button" id="saved-autofile-ok" class="settings-secondary-btn" hidden>File selected</button>
</div>
</div>
</div>
</div>
<!-- Tags tab: fold one spelling of a tag into another, and see what you have. -->
<div id="settings-tab-tags" class="settings-tab-panel" hidden>
<div class="settings-section">
<div class="settings-label">Tag aliases</div>
<p class="settings-hint">Publishers disagree about the same subject, so one topic arrives under several spellings and filtering on one silently misses the rest. An alias folds one into another everywhere — manual tags, captured feed tags, filter rules and imports.</p>
<div class="settings-row-controls tag-alias-form-row">
<input type="text" id="tag-alias-from" class="settings-input" placeholder="cpp" aria-label="Tag to fold away" autocomplete="off" />
<span class="tag-alias-arrow" aria-hidden="true">→</span>
<input type="text" id="tag-alias-to" class="settings-input" placeholder="c++" aria-label="Fold it into this tag" autocomplete="off" />
<button type="button" id="tag-alias-preview-btn" class="settings-secondary-btn">Preview</button>
<button type="button" id="tag-alias-create-btn" class="settings-save-btn" disabled>Create alias</button>
</div>
<p id="tag-alias-preview-out" class="settings-hint" role="status" aria-live="polite"></p>
<ul id="tag-alias-list" class="feed-prop-alias-list"></ul>
<p id="tag-alias-empty" class="settings-hint">No aliases yet.</p>
</div>
<div class="settings-section">
<div class="settings-label">Ignored suggested tags</div>
<p class="settings-hint">A tag value that's never worth filing under, on any feed — filters it out of the suggestion chips themselves, not a rule that acts on entries carrying it. Distinct from the × on a single feed's chip (Feed Properties → Hidden tags), which only dismisses it there.</p>
<div class="settings-row-controls">
<input type="text" id="global-tag-suppress-input" class="settings-input" placeholder="comments" aria-label="Tag value to never suggest" autocomplete="off" />
<button type="button" id="global-tag-suppress-add-btn" class="settings-save-btn">Ignore</button>
</div>
<ul id="global-tag-suppress-list" class="feed-prop-alias-list"></ul>
<p id="global-tag-suppress-empty" class="settings-hint">Nothing ignored globally yet.</p>
</div>
<div class="settings-section">
<div class="settings-label">Your tags</div>
<p class="settings-hint">Counts are kept apart on purpose: <strong>manual</strong> is what you applied by hand, <strong>feed</strong> is what publishers sent. An alias rewrites both, and one combined number would hide which half is which.</p>
<div class="settings-row-controls">
<input type="search" id="tag-inventory-q" class="settings-input" placeholder="Search tags, e.g. c" aria-label="Search tags" autocomplete="off" />
<label class="settings-hint tag-inventory-scope">
<input type="checkbox" id="tag-inventory-all" />
Only tags I applied
</label>
<span id="tag-inventory-count" class="settings-inline-status"></span>
</div>
<div id="tag-inventory-wrap" class="tag-inventory-wrap">
<table class="tag-inventory-table">
<thead><tr><th>Tag</th><th class="num">Manual</th><th class="num">Feed</th><th></th></tr></thead>
<tbody id="tag-inventory-body"></tbody>
</table>
</div>
</div>
</div>
<div id="settings-tab-stats" class="settings-tab-panel" hidden>
<dl class="feed-properties-grid">
<dt>Feeds</dt><dd id="stat-feed-count">—</dd>
<dt>Folders</dt><dd id="stat-folder-count">—</dd>
<dt>Total articles</dt><dd id="stat-entry-total">—</dd>
<dt>Unread</dt><dd id="stat-entry-unread">—</dd>
<dt>Read</dt><dd id="stat-entry-read">—</dd>
<dt>Saved</dt><dd id="stat-archive-summary">—</dd>
<dt>Thumb cache</dt><dd id="stat-thumb-summary">—</dd>
<dt>Image cache</dt><dd id="stat-img-cache-summary">—</dd>
<dt>Reader DB</dt><dd id="stat-reader-db-size">—</dd>
<dt>Meta DB</dt><dd id="stat-meta-db-size">—</dd>
<dt class="stat-yt-sync-row"{% if not youtube_sync_last_at %} hidden{% endif %}>YT sync</dt>
<dd class="stat-yt-sync-row"{% if not youtube_sync_last_at %} hidden{% endif %}><span id="stat-yt-sync-at">{{ youtube_sync_last_at }}</span><br><small id="stat-yt-sync-result">{{ youtube_sync_last_result }}</small></dd>
</dl>
</div>
</div>
</div>