diora-web/templates/radio/player.html
Marwin Schulz a314643588
All checks were successful
Build and push Docker image / build (push) Successful in 12s
Test / test (push) Successful in 15s
Fix JS crash: serialize saved_stations/featured_stations to proper JSON
Python booleans (True/False) in saved_stations (is_favorite field) and
history (scrobbled field) were being rendered literally into JS via
|safe, causing 'True is not defined' ReferenceError that broke all JS
including book loading.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 12:58:38 +01:00

375 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}diora — radio player{% endblock %}
{% block content %}
<!-- ===== NOW PLAYING BAR ===== -->
<section class="now-playing-bar" id="now-playing-bar">
<div class="now-playing-info">
<span class="now-playing-station" id="now-playing-station">— no station —</span>
<span class="now-playing-track" id="now-playing-track"></span>
</div>
<div class="now-playing-controls">
<button class="btn btn-play" id="play-stop-btn" onclick="togglePlayStop()" style="display:none;">&#9654; Play</button>
<label class="volume-label">
<span>vol</span>
<input type="range" id="volume" min="0" max="255" value="204" class="volume-slider">
<input type="number" id="volume-num" min="0" max="255" value="204" class="volume-num">
</label>
<button class="btn btn-save" id="save-station-btn" style="display:none;" onclick="saveCurrentStation()">&#9733; Save</button>
<button class="btn-icon" id="dnd-btn" onclick="toggleDND()" title="Focus mode (hides UI, press Esc to exit)"></button>
<button class="btn-icon" id="focus-station-btn" onclick="openFocusStationSidebar()" title="Focus station">📻</button>
</div>
<div class="podcast-seek-bar" id="podcast-seek-bar" style="display:none;">
<button class="btn-icon skip-btn" onclick="skipBack()" title="Back 15s">&thinsp;15</button>
<span class="seek-time" id="seek-current">0:00</span>
<input type="range" id="seek-slider" class="seek-slider" min="0" max="100" value="0">
<span class="seek-time" id="seek-duration">0:00</span>
<button class="btn-icon skip-btn" onclick="skipForward()" title="Forward 30s">30&thinsp;</button>
<div class="speed-btns" id="speed-btns">
<button class="speed-btn" onclick="setPlaybackRate(0.75)">¾×</button>
<button class="speed-btn active" onclick="setPlaybackRate(1)">1×</button>
<button class="speed-btn" onclick="setPlaybackRate(1.25)">×</button>
<button class="speed-btn" onclick="setPlaybackRate(1.5)">×</button>
<button class="speed-btn" onclick="setPlaybackRate(1.75)">×</button>
<button class="speed-btn" onclick="setPlaybackRate(2)">2×</button>
<button class="speed-btn" onclick="setPlaybackRate(2.5)">×</button>
</div>
<button class="btn-icon sleep-timer-btn" id="sleep-timer-btn" onclick="openSleepTimerMenu()" title="Sleep timer">Sleep</button>
</div>
<div class="timer-widget" id="timer-widget">
<span class="timer-phase" id="timer-phase-label">focus</span>
<span class="timer-display" id="timer-display">25:00</span>
<button class="btn-icon" id="timer-toggle-btn" onclick="toggleTimer()" title="Start/pause timer"></button>
<button class="btn-icon" id="timer-reset-btn" onclick="resetTimer()" title="Reset timer"></button>
<span class="focus-today" id="focus-today-widget" style="display:none;"></span>
<button class="btn-icon dnd-only" id="dnd-light-btn" onclick="toggleDNDLight()" title="Toggle black background">💡</button>
</div>
</section>
<!-- ===== AFFILIATE / TRACK INFO ===== -->
<section class="affiliate-section" id="affiliate-section" style="display:none;"{% if not amazon_enabled %} data-disabled="true"{% endif %}>
<img class="affiliate-artwork" id="affiliate-artwork" src="" alt="Album art">
<div class="affiliate-info">
<div class="affiliate-track" id="affiliate-track-name"></div>
<div class="affiliate-artist" id="affiliate-artist-name"></div>
<div class="affiliate-album" id="affiliate-album-name"></div>
<a class="btn btn-amazon" id="affiliate-amazon-link" href="#" target="_blank" rel="noopener noreferrer">
Buy on Amazon Music
</a>
</div>
</section>
<!-- ===== TABS ===== -->
<div class="tabs" id="tabs">
<button class="tab-btn active" onclick="showTab('radio')">Radio</button>
<button class="tab-btn" onclick="showTab('focus')">Focus</button>
<button class="tab-btn" onclick="showTab('podcasts')">Podcasts</button>
<button class="tab-btn" onclick="showTab('books')">Books</button>
</div>
<!-- ===== RADIO TAB ===== -->
<section class="tab-panel" id="tab-radio">
<div class="tabs sub-tabs" id="radio-sub-tabs">
<button class="tab-btn" onclick="showRadioTab('search')">Search</button>
<button class="tab-btn active" onclick="showRadioTab('saved')">Saved</button>
<button class="tab-btn" onclick="showRadioTab('history')">History</button>
</div>
<!-- ===== SEARCH SUB-PANEL ===== -->
<div class="sub-tab-panel" id="tab-search" style="display:none;">
<div class="search-bar">
<input type="text" id="search-input" class="search-input" placeholder="Search radio-browser.info…" onkeydown="if(event.key==='Enter') doSearch()">
<button class="btn" onclick="doSearch()">Search</button>
</div>
<div class="mood-chips" id="mood-chips"></div>
<div id="curated-lists" class="curated-lists-container"></div>
<div id="search-status" class="status-msg"></div>
<table class="data-table" id="search-results-table" style="display:none;">
<thead>
<tr>
<th>Name</th>
<th>Bitrate</th>
<th>Country</th>
<th>Tags</th>
<th></th>
</tr>
</thead>
<tbody id="search-results-body"></tbody>
</table>
</div>
<!-- ===== SAVED SUB-PANEL ===== -->
<div class="sub-tab-panel" id="tab-saved">
{% if featured_stations %}
<div class="featured-section">
<p class="featured-label">&#9733; Featured</p>
<ul class="featured-list">
{% for s in featured_stations %}
<li>
{% if s.favicon_url %}<img src="{{ s.favicon_url }}" class="station-favicon" alt="">{% endif %}
<button class="btn btn-sm" onclick="playStation('{{ s.url|escapejs }}', '{{ s.name|escapejs }}', null)">
&#9654; {{ s.name }}
</button>
{% if s.description %}<span class="muted">{{ s.description }}</span>{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if user.is_authenticated %}
<div id="recommendations" class="recommendations-section">
<!-- populated by JS -->
</div>
<div class="import-bar">
<label class="btn btn-sm" for="m3u-file-input" title="Import .m3u / .m3u8 from the desktop app">
&#8679; Import M3U
</label>
<input type="file" id="m3u-file-input" accept=".m3u,.m3u8" style="display:none;" onchange="importM3U(this)">
<span id="import-status" class="muted"></span>
</div>
<table class="data-table" id="saved-table">
<thead>
<tr>
<th>&#9733;</th>
<th>Name</th>
<th>Bitrate</th>
<th>Country</th>
<th title="Notes"></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody id="saved-tbody">
{% for station in saved_stations %}
<tr id="saved-row-{{ station.id }}" data-id="{{ station.id }}" data-url="{{ station.url }}" data-name="{{ station.name }}">
<td>
<button class="btn-icon fav-btn {% if station.is_favorite %}active{% endif %}"
onclick="toggleFav({{ station.id }})"
title="Toggle favorite">&#9733;</button>
</td>
<td class="station-name-cell">{{ station.name }}</td>
<td>{{ station.bitrate }}</td>
<td>{{ station.country }}</td>
<td class="notes-cell" onclick="editNotes({{ station.id }}, this.textContent.trim())" title="{{ station.notes|default:'' }}" style="cursor:pointer; color:#666; max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ station.notes }}</td>
<td>
<button class="btn btn-sm"
onclick="playStation('{{ station.url }}', '{{ station.name|escapejs }}', {{ station.id }})">
&#9654; Play
</button>
</td>
<td>
<button class="btn btn-sm btn-danger"
onclick="removeStation({{ station.id }})">
Remove
</button>
</td>
</tr>
{% empty %}
<tr id="saved-empty-row"><td colspan="7" class="empty-msg">No saved stations yet.</td></tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="auth-prompt">
<a href="{% url 'login' %}">Log in</a> or <a href="{% url 'register' %}">register</a>
to save stations and sync across devices.
</p>
{% endif %}
</div>
<!-- ===== HISTORY SUB-PANEL ===== -->
<div class="sub-tab-panel" id="tab-history" style="display:none;">
<table class="data-table" id="history-table">
<thead>
<tr>
<th>Time</th>
<th>Station</th>
<th>Track</th>
<th>&#9836;</th>
<th></th>
</tr>
</thead>
<tbody id="history-tbody">
{% for entry in history %}
<tr data-id="{{ entry.id }}">
<td class="history-time">{{ entry.played_at|slice:":16"|cut:"T" }}</td>
<td>{{ entry.station_name }}</td>
<td>{{ entry.track }}</td>
<td>{% if entry.scrobbled %}<span title="Scrobbled to Last.fm">&#10003;</span>{% endif %}</td>
<td><button class="btn-delete-history" onclick="deleteHistoryEntry({{ entry.id }}, this)" title="Remove"></button></td>
</tr>
{% empty %}
<tr id="history-empty-row"><td colspan="5" class="empty-msg">No history yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<!-- ===== FOCUS TAB ===== -->
<section class="tab-panel" id="tab-focus" style="display:none;">
<table class="data-table" id="focus-table">
<thead>
<tr>
<th>Time</th>
<th>Station</th>
<th>Duration</th>
</tr>
</thead>
<tbody id="focus-tbody">
<tr><td colspan="3" class="empty-msg">No focus sessions yet. Start the timer!</td></tr>
</tbody>
</table>
</section>
<!-- ===== PODCASTS TAB ===== -->
<section class="tab-panel" id="tab-podcasts" style="display:none;">
{% if user.is_authenticated %}
<div class="podcast-toolbar">
<button class="btn btn-sm" onclick="showPodcastView('feeds')">Feeds</button>
<button class="btn btn-sm" onclick="showPodcastView('inbox')">Inbox</button>
<button class="btn btn-sm" onclick="showPodcastView('queue')">Queue</button>
<button class="btn btn-sm" onclick="podcastSearchOpen()">+ Search</button>
<button class="btn btn-sm" id="refresh-all-btn" onclick="refreshAllFeedsBtn(this)" title="Refresh all feeds">↻ All</button>
<label class="btn btn-sm" for="opml-file-input">Import OPML</label>
<input type="file" id="opml-file-input" accept=".opml,.xml" style="display:none;" onchange="importOPML(this)">
<span id="opml-status" class="muted"></span>
</div>
<!-- Search pane -->
<div class="podcast-pane" id="podcast-search-pane" style="display:none;">
<div class="search-bar">
<input type="text" id="podcast-search-input" class="search-input" placeholder="Search podcasts…"
onkeydown="if(event.key==='Enter') doPodcastSearch()">
<button class="btn" onclick="doPodcastSearch()">Search</button>
</div>
<div id="podcast-search-status" class="status-msg"></div>
<div id="podcast-search-list" class="podcast-search-list"></div>
</div>
<!-- Feeds pane -->
<div class="podcast-pane" id="podcast-feeds-pane">
<div class="feed-list-toolbar">
<input type="text" id="feed-filter-input" class="search-input" placeholder="Search subscriptions…"
oninput="filterFeeds(this.value)">
<select id="feed-sort-select" onchange="sortFeeds(this.value)" class="feed-sort-select">
<option value="alpha">AZ</option>
<option value="alpha-desc">ZA</option>
<option value="added">Recently added</option>
<option value="latest_episode">Most recent episode</option>
</select>
</div>
<div id="podcast-feed-list" class="podcast-feed-list">
<p class="muted">Loading…</p>
</div>
</div>
<!-- Inbox pane -->
<div class="podcast-pane" id="podcast-inbox-pane" style="display:none;">
<div class="inbox-toolbar">
<label class="inbox-select-all-label">
<input type="checkbox" id="inbox-select-all" onchange="inboxSelectAll(this.checked)">
<span>All</span>
</label>
<div class="inbox-bulk-actions" id="inbox-bulk-actions" style="display:none;">
<span id="inbox-selection-count" class="muted"></span>
<button class="btn btn-sm" onclick="inboxBulkQueueAdd()">+Queue</button>
<button class="btn btn-sm" onclick="inboxBulkMarkPlayed()">✓ Played</button>
<button class="btn btn-sm" onclick="inboxBulkDownload()">⬇ Download</button>
<button class="btn btn-sm btn-danger" onclick="inboxBulkDismiss()">✕ Dismiss</button>
</div>
</div>
<div id="podcast-inbox-list" class="episode-list"></div>
<div class="inbox-load-more" id="inbox-load-more-bar" style="display:none;">
<button class="btn btn-sm" onclick="inboxLoadMore()">Load more</button>
<span id="inbox-count-label" class="muted"></span>
</div>
</div>
<!-- Episodes pane -->
<div class="podcast-pane" id="podcast-episodes-pane" style="display:none;">
<div class="episode-search-bar" id="episode-search-bar" style="display:none;">
<input type="text" id="episode-filter-input" class="search-input"
placeholder="Filter episodes…" oninput="filterEpisodes(this.value)">
</div>
<div id="podcast-feed-header" class="podcast-feed-header"></div>
<div id="podcast-episode-list" class="episode-list"></div>
</div>
<!-- Queue pane -->
<div class="podcast-pane" id="podcast-queue-pane" style="display:none;">
<ol id="podcast-queue-ol" class="podcast-queue-ol"></ol>
</div>
{% else %}
<p class="auth-prompt">
<a href="{% url 'login' %}">Log in</a> or <a href="{% url 'register' %}">register</a>
to subscribe to podcasts.
</p>
{% endif %}
</section>
<!-- ===== BOOKS TAB ===== -->
<section class="tab-panel" id="tab-books" style="display:none;">
{% if user.is_authenticated %}
<div id="book-upload-area">
<div class="book-drop-zone" id="book-drop-zone">
<span>Drop .epub or .pdf here or <label for="book-file-input" style="cursor:pointer;text-decoration:underline;">browse</label></span>
<input type="file" id="book-file-input" accept=".epub,.pdf" style="display:none;" onchange="bookFileSelected(this)">
<span id="book-upload-status" class="muted"></span>
</div>
</div>
<div id="book-list" class="book-list"></div>
{% else %}
<p class="auth-prompt">
<a href="{% url 'login' %}">Log in</a> or <a href="{% url 'register' %}">register</a>
to use the encrypted ebook reader.
</p>
{% endif %}
</section>
<!-- ===== READER OVERLAY ===== -->
<div id="reader-overlay" class="reader-overlay" style="display:none;">
<div class="reader-header">
<span id="reader-title" class="reader-title"></span>
<div class="reader-header-actions">
<span class="reader-progress-wrap">
<input type="number" id="reader-progress-input" class="volume-num" min="0" max="100" value="0" style="display:none;">
<span id="reader-progress-suffix" class="muted"></span>
</span>
<button class="btn-icon" id="reader-search-btn" onclick="toggleReaderSearch()" title="Search">🔍</button>
<button class="btn-icon" id="reader-settings-btn" onclick="toggleSettingsPanel()" title="Font &amp; layout"></button>
<button class="btn-icon" id="reader-bookmark-btn" onclick="addBookmark()" title="Bookmark"></button>
<button class="btn-icon" id="reader-bm-list-btn" onclick="openBookmarksSidebar()" title="Bookmarks"></button>
<button class="btn-icon" id="reader-toc-btn" onclick="openTocSidebar()" title="Table of contents"></button>
<button class="btn-icon" onclick="closeReader()" title="Close (Esc)"></button>
</div>
</div>
<div id="reader-content" class="reader-content"></div>
</div>
<!-- ===== SIDEBAR ===== -->
<div id="sidebar-overlay" class="sidebar-overlay" onclick="closeSidebar()" style="display:none;"></div>
<aside id="sidebar" class="sidebar">
<div class="sidebar-header">
<span id="sidebar-title" class="sidebar-title"></span>
<button class="btn-icon sidebar-close" onclick="closeSidebar()" title="Close"></button>
</div>
<div id="sidebar-body" class="sidebar-body"></div>
</aside>
{% endblock %}
{% block extra_js %}
<script>
// Pass Django context into JS
const INITIAL_SAVED = {{ saved_stations_json|safe }};
const INITIAL_FEATURED = {{ featured_stations_json|safe }};
const IS_AUTHENTICATED = {{ user.is_authenticated|yesno:"true,false" }};
const INITIAL_PODCAST_FEEDS = {{ initial_podcast_feeds|safe }};
window.USER_ID = {{ user.id|default:"null" }};
let USER_FOCUS_STATION = {{ focus_station_json|safe }};
</script>
<script src="/static/js/app.js"></script>
{% endblock %}