The 📻 button now opens a sidebar with the currently playing station/track, play/stop + volume control, and the saved stations list. The old focus station configuration UI (presets, custom URL input, save-to-server) and the auto-play-on-book-open behavior are removed. Closes #6 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
375 lines
17 KiB
HTML
375 lines
17 KiB
HTML
{% 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;">▶ 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()">★ 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="openRadioSidebar()" title="Radio">📻</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">⏪ 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 ⏩</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)">1¼×</button>
|
||
<button class="speed-btn" onclick="setPlaybackRate(1.5)">1½×</button>
|
||
<button class="speed-btn" onclick="setPlaybackRate(1.75)">1¾×</button>
|
||
<button class="speed-btn" onclick="setPlaybackRate(2)">2×</button>
|
||
<button class="speed-btn" onclick="setPlaybackRate(2.5)">2½×</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">★ 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)">
|
||
▶ {{ 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">
|
||
⇧ 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>★</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">★</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 }})">
|
||
▶ 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>♬</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">✓</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">A–Z</option>
|
||
<option value="alpha-desc">Z–A</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 & 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 %}
|