usermanagement/translation/calendar
This commit is contained in:
301
arr_api/templates/arr_api/calendar.html
Normal file
301
arr_api/templates/arr_api/calendar.html
Normal file
@@ -0,0 +1,301 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Calendar – Subscribarr{% endblock %}
|
||||
|
||||
{% block extra_style %}
|
||||
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #3fb950; /* Subscribarr green */
|
||||
--bg-soft: #0f172a;
|
||||
--card: #0b1224;
|
||||
--text: #e6edf3;
|
||||
--muted: #94a3b8;
|
||||
--border: #1f2a44;
|
||||
}
|
||||
.wrap { max-width: 1200px; margin: 0 auto; padding: 12px; }
|
||||
h1 { margin: 10px 0 18px; font-size: 22px; }
|
||||
|
||||
/* FullCalendar theme tweaks */
|
||||
.fc { --fc-border-color: var(--border); color: var(--text); }
|
||||
.fc-theme-standard { --fc-page-bg-color: var(--card); --fc-neutral-bg-color: #0f172a; }
|
||||
.fc .fc-scrollgrid, .fc .fc-scrollgrid-section > td { background: var(--card); }
|
||||
.fc .fc-col-header, .fc .fc-col-header-cell { background: #0f172a; }
|
||||
.fc .fc-daygrid-day, .fc .fc-timegrid-slot { background: transparent; }
|
||||
.fc .fc-toolbar { gap: 8px; margin-bottom: 12px; display: flex; flex-wrap: wrap; }
|
||||
.fc .fc-toolbar-title { font-size: 18px; }
|
||||
.fc .fc-button { background: #121b33; border: 1px solid var(--border); color: var(--text); padding: 6px 10px; border-radius: 8px; }
|
||||
.fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||
.fc .fc-button-primary:not(:disabled):active { background: #1a2542; border-color: var(--border); }
|
||||
.fc .fc-button:hover { filter: brightness(1.1); }
|
||||
.fc .fc-button:focus { box-shadow: 0 0 0 2px rgba(63,185,80,.35); }
|
||||
.fc .fc-daygrid-day-number { color: var(--muted); }
|
||||
.fc .fc-day-today { background: rgba(63,185,80,0.08); }
|
||||
.fc .fc-col-header-cell-cushion { color: var(--muted); }
|
||||
|
||||
.fc .fc-daygrid-event, .fc .fc-timegrid-event, .fc .fc-list-event { cursor: pointer; }
|
||||
.fc .fc-daygrid-event { border-radius: 6px; padding: 2px 4px; border: 1px solid var(--border); }
|
||||
.fc .subscribed-event { border-left: 4px solid var(--primary) !important; background: rgba(63,185,80,0.06); }
|
||||
.fc .event-series { border-left-color: #60a5fa; }
|
||||
.fc .event-movie { border-left-color: #f59e0b; }
|
||||
.fc .fc-list {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px; overflow: hidden;
|
||||
}
|
||||
.fc .fc-list-event:hover { background: rgba(255,255,255,0.03); }
|
||||
.event-poster { width: 24px; height: 36px; object-fit: cover; border-radius: 2px; margin-right: 6px; }
|
||||
#calendar { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 8px; width: 100%; }
|
||||
|
||||
/* Compact tweaks for phones */
|
||||
@media (max-width: 640px) {
|
||||
.fc .fc-toolbar-title { font-size: 16px; }
|
||||
.fc .fc-button { padding: 5px 8px; border-radius: 8px; }
|
||||
.fc .fc-col-header-cell-cushion { font-size: 12px; }
|
||||
.fc .fc-daygrid-day-number { font-size: 12px; }
|
||||
.fc .fc-daygrid-event { font-size: 12px; padding: 1px 3px; }
|
||||
.event-poster { width: 20px; height: 30px; }
|
||||
}
|
||||
|
||||
/* Modal minor polish in this page */
|
||||
.modal { background: #0b1224; border: 1px solid var(--border); }
|
||||
.btn-subscribe { background: var(--primary); color: #06210e; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="{% static 'css/index.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrap">
|
||||
<h1>Calendar</h1>
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
|
||||
<!-- Modal (same as on the homepage) -->
|
||||
<div id="modalBackdrop" class="modal-backdrop" role="dialog" aria-modal="true" aria-hidden="true" style="display:none">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<div class="m-poster-wrap">
|
||||
<div class="m-poster"><img id="mPoster" alt=""></div>
|
||||
<button id="subscribeBtn" class="btn-subscribe" type="button">Subscribe</button>
|
||||
</div>
|
||||
<div>
|
||||
<div id="mTitle" class="m-title"></div>
|
||||
<div id="mSub" class="m-sub"></div>
|
||||
<div id="mBadges" class="badges"></div>
|
||||
</div>
|
||||
<button class="modal-close" title="Close" aria-label="Close">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="section-block">
|
||||
<div class="section-title">Overview</div>
|
||||
<div id="mOverview" class="desc muted"></div>
|
||||
</div>
|
||||
<div class="section-block">
|
||||
<div class="section-title">Upcoming episodes</div>
|
||||
<div class="section-divider"></div>
|
||||
<div id="mEpisodes"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% csrf_token %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.js"></script>
|
||||
<script>
|
||||
(function(){
|
||||
const $ = (s, r=document) => r.querySelector(s);
|
||||
const backdrop = $("#modalBackdrop");
|
||||
const closeBtn = backdrop.querySelector(".modal-close");
|
||||
const mPoster = $("#mPoster");
|
||||
const mTitle = $("#mTitle");
|
||||
const mOverview = $("#mOverview");
|
||||
const mEpisodes = $("#mEpisodes");
|
||||
const mSub = $("#mSub");
|
||||
const subscribeBtn = $("#subscribeBtn");
|
||||
const epSection = mEpisodes.closest(".section-block");
|
||||
const csrf = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
const bc = ('BroadcastChannel' in window) ? new BroadcastChannel('subscribarr-sub') : null;
|
||||
|
||||
function openModal(){ backdrop.style.display = 'flex'; backdrop.setAttribute('aria-hidden','false'); document.body.style.overflow='hidden'; }
|
||||
function closeModal(){ backdrop.style.display = 'none'; backdrop.setAttribute('aria-hidden','true'); document.body.style.overflow=''; }
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
backdrop.addEventListener('click', (e)=>{ if(e.target===backdrop) closeModal(); });
|
||||
window.addEventListener('keydown', e=>{ if(e.key==='Escape') closeModal(); });
|
||||
|
||||
// subscription cache
|
||||
const subCache = new Map();
|
||||
async function loadAllSubs(){
|
||||
try {
|
||||
const [s,m] = await Promise.all([
|
||||
fetch('/api/series/subscriptions/'),
|
||||
fetch('/api/movies/subscriptions/')
|
||||
]);
|
||||
if(s.ok){ (await s.json()).forEach(id => subCache.set(`series:${id}`, true)); }
|
||||
if(m.ok){ (await m.json()).forEach(title => subCache.set(`movie:${title}`, true)); }
|
||||
} catch(err){ console.warn('subs load failed', err); }
|
||||
}
|
||||
function isSub(kind, idOrTitle){
|
||||
const key = kind==='series' ? `series:${idOrTitle}` : `movie:${idOrTitle}`;
|
||||
return subCache.has(key);
|
||||
}
|
||||
async function toggleSub(kind, idOrTitle, on){
|
||||
const url = kind==='series'
|
||||
? `/api/series/${on?'subscribe':'unsubscribe'}/${encodeURIComponent(idOrTitle)}/`
|
||||
: `/api/movies/${on?'subscribe':'unsubscribe'}/${encodeURIComponent(idOrTitle)}/`;
|
||||
const resp = await fetch(url, {method:'POST', headers:{'X-CSRFToken': csrf}});
|
||||
if(!resp.ok){ throw new Error('HTTP '+resp.status); }
|
||||
const key = kind==='series' ? `series:${idOrTitle}` : `movie:${idOrTitle}`;
|
||||
if(on) subCache.set(key, true); else subCache.delete(key);
|
||||
}
|
||||
|
||||
let currentEvent = null;
|
||||
let calendar = null;
|
||||
function showEvent(ev){
|
||||
currentEvent = ev;
|
||||
const p = ev.extendedProps || {};
|
||||
const kind = p.kind;
|
||||
const poster = p.poster || 'https://via.placeholder.com/130x195?text=No+Poster';
|
||||
mPoster.src = poster;
|
||||
if(kind==='series'){
|
||||
mTitle.textContent = `${p.seriesTitle} — S${p.seasonNumber}E${p.episodeNumber}${p.episodeTitle?(' · '+p.episodeTitle):''}`;
|
||||
mOverview.textContent = p.overview || '';
|
||||
epSection.style.display = 'none';
|
||||
mEpisodes.innerHTML = '';
|
||||
mSub.textContent = new Date(ev.start).toLocaleString();
|
||||
} else {
|
||||
mTitle.textContent = p.title || ev.title;
|
||||
mOverview.textContent = p.overview || '';
|
||||
epSection.style.display = 'none';
|
||||
mEpisodes.innerHTML = '';
|
||||
mSub.textContent = new Date(ev.start).toLocaleDateString();
|
||||
}
|
||||
subscribeBtn.textContent = isSub(kind, kind==='series'?p.seriesId:(p.title||'')) ? 'Unsubscribe' : 'Subscribe';
|
||||
openModal();
|
||||
}
|
||||
subscribeBtn.addEventListener('click', async ()=>{
|
||||
if(!currentEvent) return;
|
||||
const p = currentEvent.extendedProps || {};
|
||||
const kind = p.kind;
|
||||
const key = kind==='series'? p.seriesId : (p.title||'');
|
||||
const newState = !isSub(kind, key);
|
||||
// Optimistic UI
|
||||
subscribeBtn.textContent = newState ? 'Unsubscribe' : 'Subscribe';
|
||||
// Update event visuals immediately
|
||||
try {
|
||||
if(currentEvent){
|
||||
currentEvent.setExtendedProp('subscribed', newState);
|
||||
// Also tag by kind for subtle color; ensure classNames contains baseline kind
|
||||
const base = [];
|
||||
if((currentEvent.extendedProps||{}).kind === 'series') base.push('event-series');
|
||||
if((currentEvent.extendedProps||{}).kind === 'movie') base.push('event-movie');
|
||||
if(newState) base.push('subscribed-event');
|
||||
currentEvent.setProp('classNames', base);
|
||||
if(calendar) calendar.rerenderEvents();
|
||||
}
|
||||
} catch(e) { /* ignore */ }
|
||||
try {
|
||||
await toggleSub(kind, key, newState);
|
||||
if(bc) bc.postMessage({ type:'sub_change', kind, key, on:newState });
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
// revert visual/state on failure
|
||||
try {
|
||||
if(currentEvent){
|
||||
currentEvent.setExtendedProp('subscribed', !newState);
|
||||
const base = [];
|
||||
if((currentEvent.extendedProps||{}).kind === 'series') base.push('event-series');
|
||||
if((currentEvent.extendedProps||{}).kind === 'movie') base.push('event-movie');
|
||||
if(!newState) base.push('subscribed-event');
|
||||
currentEvent.setProp('classNames', base);
|
||||
if(calendar) calendar.rerenderEvents();
|
||||
}
|
||||
} catch(e) { /* ignore */ }
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
await loadAllSubs();
|
||||
const calendarEl = document.getElementById('calendar');
|
||||
const isCompact = () => (calendarEl?.clientWidth || window.innerWidth) < 720;
|
||||
const compact = isCompact();
|
||||
calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: compact ? 'listWeek' : 'dayGridMonth',
|
||||
height: 'auto',
|
||||
locale: 'en',
|
||||
headerToolbar: compact
|
||||
? { left:'prev,next', center:'title', right:'listWeek,dayGridMonth' }
|
||||
: { left:'prev,next today', center:'title', right:'dayGridMonth,timeGridWeek,listWeek' },
|
||||
buttonText: { listWeek: 'List', dayGridMonth: 'Month', timeGridWeek: 'Week', today: 'Today' },
|
||||
firstDay: 1,
|
||||
nowIndicator: true,
|
||||
dayMaxEventRows: compact ? 2 : 5,
|
||||
expandRows: true,
|
||||
handleWindowResize: true,
|
||||
windowResize: function(){
|
||||
const c = isCompact();
|
||||
const should = c ? 'listWeek' : 'dayGridMonth';
|
||||
if(calendar.view.type !== should){ calendar.changeView(should); }
|
||||
calendar.setOption('headerToolbar', c
|
||||
? { left:'prev,next', center:'title', right:'listWeek,dayGridMonth' }
|
||||
: { left:'prev,next today', center:'title', right:'dayGridMonth,timeGridWeek,listWeek' }
|
||||
);
|
||||
calendar.setOption('dayMaxEventRows', c ? 2 : 5);
|
||||
calendar.setOption('displayEventTime', !c);
|
||||
},
|
||||
eventClassNames: function(arg){
|
||||
const p = arg.event.extendedProps || {};
|
||||
const classes = [];
|
||||
if(p.kind === 'series') classes.push('event-series');
|
||||
if(p.kind === 'movie') classes.push('event-movie');
|
||||
if(p.subscribed) classes.push('subscribed-event');
|
||||
return classes;
|
||||
},
|
||||
events: async (info, success, failure) => {
|
||||
try {
|
||||
const url = `/api/calendar/events/?days={{ days|default:60 }}`;
|
||||
const resp = await fetch(url);
|
||||
const data = await resp.json();
|
||||
const evs = (data.events||[]);
|
||||
success(evs);
|
||||
} catch(err){ failure(err); }
|
||||
},
|
||||
eventClick: function(arg){ arg.jsEvent.preventDefault(); showEvent(arg.event); },
|
||||
eventDidMount: function(info){
|
||||
const p = info.event.extendedProps || {};
|
||||
if(info.view.type.startsWith('list') && p.poster){
|
||||
const img = document.createElement('img');
|
||||
img.src = p.poster; img.className = 'event-poster';
|
||||
const titleEl = info.el.querySelector('.fc-list-event-title');
|
||||
if(titleEl){ titleEl.prepend(img); }
|
||||
}
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
// Listen for sub changes from other tabs/pages
|
||||
if(bc){
|
||||
bc.onmessage = (evt)=>{
|
||||
const msg = evt.data || {};
|
||||
if(msg.type !== 'sub_change') return;
|
||||
const {kind, key, on} = msg;
|
||||
try {
|
||||
calendar.getEvents().forEach(ev=>{
|
||||
const p = ev.extendedProps || {};
|
||||
const match = (kind==='series' && String(p.seriesId)===String(key)) || (kind==='movie' && (p.title||'')===key);
|
||||
if(match){
|
||||
ev.setExtendedProp('subscribed', on);
|
||||
const base = [];
|
||||
if(p.kind==='series') base.push('event-series');
|
||||
if(p.kind==='movie') base.push('event-movie');
|
||||
if(on) base.push('subscribed-event');
|
||||
ev.setProp('classNames', base);
|
||||
}
|
||||
});
|
||||
calendar.rerenderEvents();
|
||||
} catch(e) { /* ignore */ }
|
||||
};
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Subscribarr – Übersicht{% endblock %}
|
||||
{% block title %}Subscribarr – Overview{% endblock %}
|
||||
|
||||
{% block extra_style %}
|
||||
<link rel="stylesheet" href="{% static 'css/index.css' %}">
|
||||
@@ -20,26 +20,30 @@
|
||||
<div class="controls">
|
||||
<form method="get" class="controls-form">
|
||||
<input type="hidden" name="kind" value="{{ kind|default:'all' }}">
|
||||
<input type="text" name="q" placeholder="Suche Serien/Filme…" value="{{ query|default:'' }}">
|
||||
<input type="number" name="days" min="1" max="365" value="{{ days|default:30 }}" title="Zeitraum in Tagen">
|
||||
<button type="submit">Suchen</button>
|
||||
<input type="text" name="q" placeholder="Search series/movies…" value="{{ query|default:'' }}">
|
||||
<input type="number" name="days" min="1" max="365" value="{{ days|default:30 }}" title="Time range (days)">
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
|
||||
<nav class="seg" aria-label="Typ filtern">
|
||||
<nav class="seg" aria-label="Filter type">
|
||||
{% with qs=query|urlencode %}
|
||||
<a href="?kind=all&days={{ days }}{% if qs %}&q={{ qs }}{% endif %}"
|
||||
class="{% if kind == 'all' %}active{% endif %}">Alle</a>
|
||||
class="{% if kind == 'all' %}active{% endif %}">All</a>
|
||||
<a href="?kind=series&days={{ days }}{% if qs %}&q={{ qs }}{% endif %}"
|
||||
class="{% if kind == 'series' %}active{% endif %}">Serien</a>
|
||||
class="{% if kind == 'series' %}active{% endif %}">Series</a>
|
||||
<a href="?kind=movies&days={{ days }}{% if qs %}&q={{ qs }}{% endif %}"
|
||||
class="{% if kind == 'movies' %}active{% endif %}">Filme</a>
|
||||
class="{% if kind == 'movies' %}active{% endif %}">Movies</a>
|
||||
{% endwith %}
|
||||
</nav>
|
||||
|
||||
<div class="controls-actions">
|
||||
<a href="/calendar/" class="btn btn-accent" title="Open calendar">📅 Calendar</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if show_series %}
|
||||
<div class="section">
|
||||
<h2>Laufende Serien</h2>
|
||||
<h2>Ongoing series</h2>
|
||||
<div class="grid">
|
||||
{% for s in series_grouped %}
|
||||
<div class="card" data-series-id="{{ s.seriesId }}" data-title="{{ s.seriesTitle|escape }}"
|
||||
@@ -60,7 +64,7 @@
|
||||
<span class="muted" data-dt="{{ e.airDateUtc|default:'' }}"></span>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="muted">Keine kommenden Episoden.</div>
|
||||
<div class="muted">No upcoming episodes.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,7 +76,7 @@
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="muted">Keine Serien gefunden.</p>
|
||||
<p class="muted">No series found.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,7 +84,7 @@
|
||||
|
||||
{% if show_movies %}
|
||||
<div class="section">
|
||||
<h2>Anstehende Filme</h2>
|
||||
<h2>Upcoming movies</h2>
|
||||
<div class="grid">
|
||||
{% for m in movies %}
|
||||
<div class="movie-card" data-kind="movie" data-title="{{ m.title|escape }}"
|
||||
@@ -92,13 +96,13 @@
|
||||
{% endif %}
|
||||
<div class="title">{{ m.title }}{% if m.year %} ({{ m.year }}){% endif %}</div>
|
||||
<div class="muted">
|
||||
{% if m.inCinemas %}Kino: <span data-dt="{{ m.inCinemas }}"></span>{% endif %}
|
||||
{% if m.inCinemas %}In theaters: <span data-dt="{{ m.inCinemas }}"></span>{% endif %}
|
||||
{% if m.digitalRelease %}<br>Digital: <span data-dt="{{ m.digitalRelease }}"></span>{% endif %}
|
||||
{% if m.physicalRelease %}<br>Disc: <span data-dt="{{ m.physicalRelease }}"></span>{% endif %}
|
||||
{% if m.physicalRelease %}<br>Physical: <span data-dt="{{ m.physicalRelease }}"></span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="muted">Keine Filme gefunden.</p>
|
||||
<p class="muted">No movies found.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,17 +122,17 @@
|
||||
<div id="mSub" class="m-sub"></div>
|
||||
<div id="mBadges" class="badges"></div>
|
||||
</div>
|
||||
<button class="modal-close" title="Schließen" aria-label="Schließen">×</button>
|
||||
<button class="modal-close" title="Close" aria-label="Close">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="section-block">
|
||||
<div class="section-title">Beschreibung</div>
|
||||
<div class="section-title">Overview</div>
|
||||
<div id="mOverview" class="desc muted"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-block">
|
||||
<div class="section-title">Kommende Episoden</div>
|
||||
<div class="section-title">Upcoming episodes</div>
|
||||
<div class="section-divider"></div>
|
||||
<div id="mEpisodes"></div>
|
||||
</div>
|
||||
@@ -153,6 +157,7 @@
|
||||
const mSub = $("#mSub");
|
||||
const epSection = mEpisodes.closest(".section-block");
|
||||
const subscribeBtn = $("#subscribeBtn");
|
||||
const bc = ('BroadcastChannel' in window) ? new BroadcastChannel('subscribarr-sub') : null;
|
||||
|
||||
let lastClickedCard = null;
|
||||
|
||||
@@ -178,7 +183,7 @@
|
||||
return "movie:" + (card.dataset.title || "");
|
||||
}
|
||||
|
||||
// Cache für Abonnement-Status
|
||||
// Cache for subscription state
|
||||
const subCache = new Map();
|
||||
|
||||
async function loadAllSubs() {
|
||||
@@ -207,7 +212,7 @@
|
||||
return k ? subCache.get(k) || false : false;
|
||||
}
|
||||
|
||||
async function saveSub(card, on) {
|
||||
async function saveSub(card, on) {
|
||||
const k = subKey(card);
|
||||
if (!k) return;
|
||||
const [type, id] = k.split(":");
|
||||
@@ -221,45 +226,48 @@
|
||||
});
|
||||
|
||||
if (resp.status === 403) {
|
||||
// Nicht eingeloggt
|
||||
// Not logged in
|
||||
window.location.href = '/accounts/login/?next=' + encodeURIComponent(window.location.pathname);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
|
||||
// Cache aktualisieren
|
||||
// Update cache
|
||||
if (on) {
|
||||
subCache.set(k, true);
|
||||
} else {
|
||||
subCache.delete(k);
|
||||
}
|
||||
|
||||
// Cross-tab/page notify
|
||||
if (bc) bc.postMessage({ type: 'sub_change', kind: type, key: id, on });
|
||||
} catch (err) {
|
||||
console.error("Failed to update subscription:", err);
|
||||
// Cache-Update rückgängig machen bei Fehler
|
||||
// Revert optimistic cache on error
|
||||
if (on) {
|
||||
subCache.delete(k);
|
||||
} else {
|
||||
subCache.set(k, true);
|
||||
}
|
||||
|
||||
// Fehlermeldung anzeigen
|
||||
// Show error
|
||||
const errorMsg = document.createElement('div');
|
||||
errorMsg.className = 'error-message';
|
||||
errorMsg.textContent = 'Fehler beim Aktualisieren des Abonnements. Bitte versuchen Sie es später erneut.';
|
||||
errorMsg.textContent = 'Failed to update subscription. Please try again later.';
|
||||
document.body.appendChild(errorMsg);
|
||||
setTimeout(() => errorMsg.remove(), 3000);
|
||||
}
|
||||
}
|
||||
function applySubUI(card, on) {
|
||||
if (card) card.classList.toggle("subscribed", !!on); // grüne Outline via .subscribed (CSS)
|
||||
if (card) card.classList.toggle("subscribed", !!on); // green outline via .subscribed (CSS)
|
||||
if (subscribeBtn) {
|
||||
subscribeBtn.textContent = on ? "Unsubscribe" : "Subscribe";
|
||||
subscribeBtn.setAttribute("aria-pressed", on ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
// Beim Laden: Alle Abonnements in einem API-Call laden
|
||||
// On load: fetch all subscriptions in a single API call
|
||||
(async () => {
|
||||
await loadAllSubs();
|
||||
const cards = $$(".card, .movie-card");
|
||||
@@ -268,7 +276,30 @@
|
||||
});
|
||||
})();
|
||||
|
||||
// ===== Serien-Karten öffnen =====
|
||||
// Listen to subscription changes from other pages (e.g., calendar)
|
||||
if (bc) {
|
||||
bc.onmessage = (evt) => {
|
||||
const msg = evt.data || {};
|
||||
if (msg.type !== 'sub_change') return;
|
||||
const { kind, key, on } = msg;
|
||||
// Keep cache in sync
|
||||
const cacheKey = `${kind}:${key}`;
|
||||
if (on) subCache.set(cacheKey, true); else subCache.delete(cacheKey);
|
||||
|
||||
// Update matching cards
|
||||
const cards = $$(".card, .movie-card");
|
||||
cards.forEach(card => {
|
||||
const isSeries = card.classList.contains('card') && card.dataset.seriesId;
|
||||
const isMovie = card.classList.contains('movie-card') && card.dataset.title;
|
||||
let match = false;
|
||||
if (kind === 'series' && isSeries && String(card.dataset.seriesId) === String(key)) match = true;
|
||||
if (kind === 'movie' && isMovie && (card.dataset.title || '') === key) match = true;
|
||||
if (match) applySubUI(card, on);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// ===== Open series cards =====
|
||||
$$(".card").forEach(card => {
|
||||
card.addEventListener("click", () => {
|
||||
lastClickedCard = card;
|
||||
@@ -278,22 +309,22 @@
|
||||
const poster = card.dataset.poster || "";
|
||||
const overview = card.dataset.overview || "";
|
||||
|
||||
// Episoden aus eingebettetem JSON <script id="eps-<id>">
|
||||
// Episodes from embedded JSON <script id="eps-<id>">
|
||||
let episodes = [];
|
||||
const script = document.getElementById("eps-" + id);
|
||||
if (script) { try { episodes = JSON.parse(script.textContent); } catch { } }
|
||||
|
||||
// Modal befüllen
|
||||
// Fill modal
|
||||
mTitle.textContent = title;
|
||||
mPoster.src = poster || "https://via.placeholder.com/130x195?text=No+Poster";
|
||||
mPoster.alt = title;
|
||||
mOverview.textContent = overview || "Keine Beschreibung verfügbar.";
|
||||
mOverview.textContent = overview || "No overview available.";
|
||||
|
||||
mSub.textContent = episodes.length
|
||||
? `${episodes.length} kommende Episode(n)`
|
||||
: "Keine kommenden Episoden";
|
||||
? `${episodes.length} upcoming episode(s)`
|
||||
: "No upcoming episodes";
|
||||
|
||||
// Genres-Badges, falls data-genres vorhanden
|
||||
// Genre badges if data-genres is present
|
||||
mBadges.innerHTML = "";
|
||||
if (card.dataset.genres) {
|
||||
card.dataset.genres.split(",").map(s => s.trim()).filter(Boolean).forEach(g => {
|
||||
@@ -304,7 +335,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Episodenbereich
|
||||
// Episodes section
|
||||
epSection.style.display = "";
|
||||
mEpisodes.innerHTML = "";
|
||||
if (!episodes.length) {
|
||||
@@ -323,10 +354,10 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe-UI für diese Karte setzen
|
||||
// Set subscribe UI for this card
|
||||
applySubUI(lastClickedCard, loadSub(lastClickedCard));
|
||||
|
||||
// Status nochmal aktualisieren zur Sicherheit
|
||||
// Refresh status again for safety
|
||||
loadAllSubs().then(() => {
|
||||
applySubUI(lastClickedCard, loadSub(lastClickedCard));
|
||||
});
|
||||
@@ -335,7 +366,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ===== Film-Karten öffnen =====
|
||||
// ===== Open movie cards =====
|
||||
$$(".movie-card").forEach(card => {
|
||||
card.addEventListener("click", () => {
|
||||
lastClickedCard = card;
|
||||
@@ -347,19 +378,19 @@
|
||||
mTitle.textContent = title;
|
||||
mPoster.src = poster || "https://via.placeholder.com/130x195?text=No+Poster";
|
||||
mPoster.alt = title;
|
||||
mOverview.textContent = overview || "Keine Beschreibung verfügbar.";
|
||||
mOverview.textContent = overview || "No overview available.";
|
||||
|
||||
mSub.textContent = "";
|
||||
mBadges.innerHTML = "";
|
||||
|
||||
// Episodenbereich ausblenden
|
||||
// Hide episodes section
|
||||
epSection.style.display = "none";
|
||||
mEpisodes.innerHTML = "";
|
||||
|
||||
// Subscribe-UI für diese Karte setzen
|
||||
// Set subscribe UI for this card
|
||||
applySubUI(lastClickedCard, loadSub(lastClickedCard));
|
||||
|
||||
// Status nochmal aktualisieren zur Sicherheit
|
||||
// Refresh status again for safety
|
||||
loadAllSubs().then(() => {
|
||||
applySubUI(lastClickedCard, loadSub(lastClickedCard));
|
||||
});
|
||||
@@ -368,7 +399,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ===== Subscribe-Button im Modal mit Backend-Sync =====
|
||||
// ===== Subscribe button in modal with backend sync =====
|
||||
if (subscribeBtn) {
|
||||
subscribeBtn.addEventListener("click", async () => {
|
||||
if (!lastClickedCard) return;
|
||||
@@ -378,16 +409,16 @@
|
||||
// Optimistic UI update
|
||||
applySubUI(lastClickedCard, newState);
|
||||
|
||||
// Backend-Sync
|
||||
// Backend sync
|
||||
await saveSub(lastClickedCard, newState);
|
||||
|
||||
// Status neu laden zur Sicherheit
|
||||
// Refresh status again for safety
|
||||
const finalState = await loadSub(lastClickedCard);
|
||||
applySubUI(lastClickedCard, finalState);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Datumsangaben in der Übersicht formatieren =====
|
||||
// ===== Format date/time labels in the overview =====
|
||||
document.querySelectorAll("[data-dt]").forEach(el => {
|
||||
const v = el.getAttribute("data-dt");
|
||||
if (!v) return;
|
||||
|
Reference in New Issue
Block a user