diora-web/templates/radio/player.html
marwin 5ce9cec581
All checks were successful
Build and push Docker image / build (push) Successful in 11s
Test / test (push) Successful in 16s
Show enc-key-prompt by default, hide only when key exists via JS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 21:57:41 +01:00

348 lines
15 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(2)">2×</button>
</div>
</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>
<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 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 id="podcast-inbox-list" class="episode-list"></div>
</div>
<!-- Episodes pane -->
<div class="podcast-pane" id="podcast-episodes-pane" style="display:none;">
<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="enc-key-prompt" class="enc-key-prompt">
<p class="muted">Enter your password to unlock encrypted storage on this device.</p>
<div class="search-bar">
<input type="password" id="enc-key-password" class="search-input" placeholder="Your password…">
<button class="btn" onclick="deriveAndStoreKey()">Unlock</button>
</div>
<span id="enc-key-status" class="muted"></span>
</div>
<div id="book-upload-area" style="display:none;">
<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|safe }};
const INITIAL_FEATURED = {{ featured_stations|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 %}