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
// ---------------------------------------------------------------------------