diora-web/templates/radio/player.html
marwin 3f40a4078a
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 14s
Reader: Notiz-Eingabe direkt im Rand statt Sprung zur Sidebar (SW v25)
Notizen setzen/bearbeiten passiert jetzt inline an der Stelle im Notizen-Rand,
wo die Notiz auch angezeigt wird — kein Wechsel mehr zur rechten Sidebar.
Textfeld wird beim Öffnen automatisch fokussiert und ausgewählt. Enter
speichert, Escape verwirft die Änderung.

Stift-Button (Marker-Modus) zieht in die Titelzeile des Notizen-Rands um,
neben "Notes" — vorher musste man dafür ständig zur Hauptleiste zurück, die
im Immersive-Modus eingeklappt ist.

Nebenbei zwei Bugs beim Testen gefunden und behoben: ein Klick auf leere
Randfläche während ein Editor bereits offen war, legte durchs Blur-Event
gleichzeitig eine zweite, ungewollte Notiz an; und Escape im Notizfeld
bubbled zum globalen Escape-Handler durch, der dann den ganzen Reader
schloss (und damit die gerade bearbeiteten Highlights aus dem Speicher warf).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 11:23:16 +02:00

401 lines
18 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="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">«&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 title="Favorite">&#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" id="reader-margin-btn" onclick="toggleAnnotationsMargin()" title="Notes &amp; highlights"></button>
<button class="btn-icon" id="reader-reset-pos-btn" onclick="saveReaderProgress(true)" title="Diese Position als Lesefortschritt setzen (überschreibt gespeicherten Fortschritt)"></button>
<button class="btn-icon" onclick="closeReader()" title="Close (Esc)"></button>
</div>
</div>
<div id="reader-body-row" class="reader-body-row">
<aside id="reader-margin" class="reader-margin">
<div class="reader-margin-header">
<span class="reader-margin-title">Notes</span>
<span class="reader-margin-header-actions">
<button class="btn-icon" id="reader-marker-btn" onclick="toggleMarkerMode()" title="Mark text"></button>
<button class="btn-icon" id="reader-margin-list-btn" onclick="openAnnotationsSidebar()" title="List all highlights &amp; notes, jump to any of them"></button>
<button class="btn-icon" id="reader-margin-export-btn" onclick="exportAnnotations()" title="Export as text file"></button>
</span>
</div>
<div id="reader-margin-markers" class="reader-margin-markers"></div>
</aside>
<div id="reader-content" class="reader-content"></div>
</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>
<!-- ===== MODAL DIALOG (replaces native alert/confirm/prompt) ===== -->
<div id="modal-overlay" class="modal-overlay" style="display:none;"></div>
<div id="modal-dialog" class="modal-dialog" style="display:none;" role="alertdialog" aria-modal="true">
<p id="modal-message" class="modal-message"></p>
<input type="text" id="modal-input" class="search-input modal-input" style="display:none;">
<div class="modal-actions">
<button type="button" id="modal-cancel-btn" class="btn" style="display:none;">Cancel</button>
<button type="button" id="modal-ok-btn" class="btn btn-primary">OK</button>
</div>
</div>
{% 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 %}