From 3152c744a0a994a23c61a76133775c4a4e18d9f0 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sun, 10 May 2026 13:15:28 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20URL-State=20=E2=80=93=20Reload=20erh?= =?UTF-8?q?=C3=A4lt=20View=20und=20Datum=20statt=20auf=20heute=20zu=20spri?= =?UTF-8?q?ngen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aktuelle View und Datum werden als URL-Hash gespiegelt (#date=YYYY-MM-DD&view=). Beim Init liest initCalendar() den Hash und überschreibt damit die Defaults (settings.default_view + today). fetchAndRender() schreibt nach jedem Render den aktuellen State zurück (replaceState, damit prev/next-Clicks keinen History-Müll erzeugen). Browser-Back/Forward funktioniert via hashchange-Listener. Edge case: HA-OAuth-Callback erhält jetzt den Hash beim URL-Cleanup (window.location.pathname + window.location.hash statt nur pathname). Komplett Frontend-only — kein Backend-Touch. Co-Authored-By: Claude Opus 4.6 --- frontend/js/calendar.js | 59 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index 061431e..e5fac64 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -40,6 +40,38 @@ let state = { selectedEventColor: '', // '' = use calendar color }; +// ── URL state ──────────────────────────────────────────── +// View + Date werden in der URL als #date=YYYY-MM-DD&view= gespiegelt, +// damit Reload/PWA-restore den letzten Stand wiederherstellen statt auf +// heute zu springen. +const VALID_VIEWS = ['month', 'week', 'day', 'quarter', 'agenda']; + +function readUrlState() { + const hash = window.location.hash.replace(/^#/, ''); + if (!hash) return {}; + const params = new URLSearchParams(hash); + const out = {}; + const view = params.get('view'); + if (view && VALID_VIEWS.includes(view)) out.view = view; + const date = params.get('date'); + if (date && /^\d{4}-\d{2}-\d{2}$/.test(date)) { + const d = new Date(date + 'T00:00:00'); + if (!isNaN(d.getTime())) out.date = d; + } + return out; +} + +function writeUrlState() { + const d = state.currentDate; + const dateStr = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; + const newHash = `date=${dateStr}&view=${state.currentView}`; + if (window.location.hash.replace(/^#/,'') !== newHash) { + // replaceState statt pushState: prev/next-Klicks sollen nicht jeden + // einzelnen Tag in den Browser-History-Stack drücken + window.history.replaceState(null, '', '#' + newHash); + } +} + // ── Public init ─────────────────────────────────────────── export async function initCalendar() { const [settings, accounts, localCalendars, icalSubscriptions, googleAccounts, haAccounts] = await Promise.all([ @@ -61,6 +93,11 @@ export async function initCalendar() { state.dimPast = settings.dim_past_events; weekStartDay = settings.week_start_day || 'monday'; + // URL state takes precedence over defaults (settings + today) + const urlState = readUrlState(); + if (urlState.date) state.currentDate = urlState.date; + if (urlState.view) state.currentView = urlState.view; + setLang(settings.language || 'de'); applyTheme(settings); updateViewButtons(); @@ -78,6 +115,22 @@ export async function initCalendar() { bindProfileModal(); bindSwipeNavigation(); handleHAOAuthReturn(); + + // Browser-Back/Forward: URL-Hash → State synchronisieren + window.addEventListener('hashchange', () => { + const u = readUrlState(); + let changed = false; + if (u.view && u.view !== state.currentView) { + state.currentView = u.view; + updateViewButtons(); + changed = true; + } + if (u.date && !isSameDay(u.date, state.currentDate)) { + state.currentDate = u.date; + changed = true; + } + if (changed) fetchAndRender(); + }); } function handleHAOAuthReturn() { @@ -91,7 +144,7 @@ function handleHAOAuthReturn() { }; if (params.has('ha_connected')) { showToast('Home Assistant verbunden'); - window.history.replaceState({}, '', window.location.pathname); + window.history.replaceState({}, '', window.location.pathname + window.location.hash); fetchAndRender(true); api.get('/homeassistant/accounts').then(accs => { state.haAccounts = accs || []; @@ -101,7 +154,7 @@ function handleHAOAuthReturn() { } else if (params.has('ha_error')) { const code = params.get('ha_error'); showToast(errMap[code] || `HA-Anmeldung fehlgeschlagen: ${code}`, true); - window.history.replaceState({}, '', window.location.pathname); + window.history.replaceState({}, '', window.location.pathname + window.location.hash); } } @@ -196,6 +249,7 @@ async function fetchAndRender(force = false, silent = false) { renderView(); updateTitle(); renderMiniCal(); + writeUrlState(); prefetchIfNeeded(start, end); // extend cache in background if approaching an edge return; } @@ -224,6 +278,7 @@ async function fetchAndRender(force = false, silent = false) { renderView(); updateTitle(); renderMiniCal(); + writeUrlState(); } function getViewRange() {