From 678912020c2051fc82d21542efee21eda19e2d7a Mon Sep 17 00:00:00 2001 From: marwin Date: Mon, 16 Mar 2026 20:57:07 +0100 Subject: [PATCH] Add donation hint toast after 10 plays of same station --- radio/views.py | 10 ++++++++++ static/css/app.css | 44 ++++++++++++++++++++++++++++++++++++++++++++ static/js/app.js | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/radio/views.py b/radio/views.py index 78bd409..98d29f3 100644 --- a/radio/views.py +++ b/radio/views.py @@ -4,6 +4,7 @@ import urllib.parse from datetime import datetime import requests +from django.db.models import Count from django.conf import settings from django.http import ( HttpResponse, @@ -30,11 +31,20 @@ def index(request): history = [] if request.user.is_authenticated: + play_counts = { + sp['station_url']: sp['count'] + for sp in StationPlay.objects + .filter(user=request.user) + .values('station_url') + .annotate(count=Count('id')) + } saved_stations = list( request.user.saved_stations.values( 'id', 'name', 'url', 'bitrate', 'country', 'tags', 'favicon_url', 'is_favorite' ) ) + for s in saved_stations: + s['play_count'] = play_counts.get(s['url'], 0) history = list( request.user.track_history.values( 'id', 'station_name', 'track', 'played_at', 'scrobbled' diff --git a/static/css/app.css b/static/css/app.css index f843666..95c409e 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -911,3 +911,47 @@ body.dnd-mode .timer-display { opacity: 0.5; } .btn-delete-history:hover { opacity: 1; color: #e55; } + +#donation-hint { + position: fixed; + bottom: 24px; + right: 24px; + background: var(--surface, #1e1e2e); + border: 1px solid var(--border, #333); + border-radius: 8px; + padding: 12px 16px; + display: flex; + align-items: center; + gap: 12px; + font-size: 0.85rem; + box-shadow: 0 4px 16px rgba(0,0,0,0.4); + z-index: 999; + animation: hint-in 0.3s ease; + max-width: 320px; +} + +#donation-hint.hiding { + animation: hint-out 0.4s ease forwards; +} + +#donation-hint button { + background: none; + border: none; + cursor: pointer; + color: var(--muted, #888); + font-size: 0.8rem; + padding: 0; + flex-shrink: 0; +} + +#donation-hint button:hover { color: var(--fg, #fff); } + +@keyframes hint-in { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes hint-out { + from { opacity: 1; transform: translateY(0); } + to { opacity: 0; transform: translateY(12px); } +} diff --git a/static/js/app.js b/static/js/app.js index 6917697..2659dac 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -71,6 +71,7 @@ function playStation(url, name, stationId) { startMetadataSSE(url); startPlaySession(name, url); + maybeShowDonationHint(url, name); } function stopPlayback(clearStation = true) { @@ -829,6 +830,41 @@ function initCuratedLists() { }); } +// --------------------------------------------------------------------------- +// Donation hint +// --------------------------------------------------------------------------- + +const DONATION_HINT_THRESHOLD = 10; +const DONATION_HINT_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days + +function maybeShowDonationHint(stationUrl, stationName) { + const station = INITIAL_SAVED.find(s => s.url === stationUrl); + if (!station || station.play_count < DONATION_HINT_THRESHOLD) return; + + const key = `diora_donation_hint_${stationUrl}`; + const last = parseInt(localStorage.getItem(key) || '0', 10); + if (Date.now() - last < DONATION_HINT_COOLDOWN_MS) return; + + const existing = document.getElementById('donation-hint'); + if (existing) existing.remove(); + + const el = document.createElement('div'); + el.id = 'donation-hint'; + el.innerHTML = ` + You listen to ${escapeHtml(stationName)} a lot — consider supporting them ❤️ + + `; + document.body.appendChild(el); + + setTimeout(() => dismissDonationHint(stationUrl), 12000); +} + +function dismissDonationHint(stationUrl) { + localStorage.setItem(`diora_donation_hint_${stationUrl}`, Date.now()); + const el = document.getElementById('donation-hint'); + if (el) { el.classList.add('hiding'); setTimeout(() => el.remove(), 400); } +} + // --------------------------------------------------------------------------- // Station notes // ---------------------------------------------------------------------------