Compare commits
3 Commits
8f9eafe561
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58faf3876c | ||
|
|
639d7f3c9c | ||
|
|
a60c27f66f |
@@ -4,16 +4,11 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI, HTTPException, Request
|
from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
# How long the browser may keep static assets before revalidating.
|
|
||||||
STATIC_MAX_AGE_SECONDS = 2 * 60 * 60 # 2 hours
|
|
||||||
NO_CACHE = "no-cache, no-store, must-revalidate"
|
|
||||||
STATIC_CACHE = f"public, max-age={STATIC_MAX_AGE_SECONDS}, must-revalidate"
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
from database import Base, engine
|
from database import Base, engine
|
||||||
@@ -114,56 +109,10 @@ def _migrate():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
|
||||||
conn.execute(text("ALTER TABLE user_settings ADD COLUMN text_color VARCHAR(7)"))
|
|
||||||
conn.commit()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
conn.execute(text("ALTER TABLE user_settings ADD COLUMN line_color VARCHAR(7)"))
|
|
||||||
conn.commit()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
conn.execute(text("ALTER TABLE user_settings ADD COLUMN bg_color VARCHAR(7)"))
|
|
||||||
conn.commit()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
_migrate()
|
_migrate()
|
||||||
|
|
||||||
app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None)
|
app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None)
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
|
||||||
async def add_cache_headers(request: Request, call_next):
|
|
||||||
"""Force ≤ 2h browser cache for static assets and disable cache for the
|
|
||||||
entry HTML / SW / version file. API responses are left alone (handlers
|
|
||||||
decide their own caching)."""
|
|
||||||
response = await call_next(request)
|
|
||||||
path = request.url.path
|
|
||||||
|
|
||||||
# Never cache: entry HTML, manifest, service worker, version marker
|
|
||||||
if (
|
|
||||||
path in ("/", "/index.html", "/manifest.json", "/sw.js")
|
|
||||||
or path == "/static/js/version.js"
|
|
||||||
):
|
|
||||||
response.headers["Cache-Control"] = NO_CACHE
|
|
||||||
response.headers["Pragma"] = "no-cache"
|
|
||||||
response.headers["Expires"] = "0"
|
|
||||||
# 2h cache for the rest of the frontend (JS/CSS/icons/etc.)
|
|
||||||
elif path.startswith("/static/") or path.startswith("/icons/"):
|
|
||||||
response.headers["Cache-Control"] = STATIC_CACHE
|
|
||||||
# SPA fallback (everything else that isn't an API route) returns HTML;
|
|
||||||
# don't let the browser cache that either.
|
|
||||||
elif not path.startswith("/api/"):
|
|
||||||
response.headers["Cache-Control"] = NO_CACHE
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
app.include_router(auth_router.router, prefix="/api/auth", tags=["auth"])
|
app.include_router(auth_router.router, prefix="/api/auth", tags=["auth"])
|
||||||
app.include_router(users_router.router, prefix="/api/users", tags=["users"])
|
app.include_router(users_router.router, prefix="/api/users", tags=["users"])
|
||||||
app.include_router(caldav_router.router, prefix="/api/caldav", tags=["caldav"])
|
app.include_router(caldav_router.router, prefix="/api/caldav", tags=["caldav"])
|
||||||
|
|||||||
@@ -84,9 +84,6 @@ class UserSettings(Base):
|
|||||||
language = Column(String(5), default="de")
|
language = Column(String(5), default="de")
|
||||||
month_divider_color = Column(String(7), default="#7090c0")
|
month_divider_color = Column(String(7), default="#7090c0")
|
||||||
month_label_color = Column(String(7), default="#7090c0")
|
month_label_color = Column(String(7), default="#7090c0")
|
||||||
text_color = Column(String(7), nullable=True) # Override für --text-1 (NULL = nutze text_contrast)
|
|
||||||
line_color = Column(String(7), nullable=True) # Override für --border (NULL = nutze line_contrast)
|
|
||||||
bg_color = Column(String(7), nullable=True) # Override für --bg-app (NULL = Default)
|
|
||||||
|
|
||||||
user = relationship("User", back_populates="settings")
|
user = relationship("User", back_populates="settings")
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ class SettingsUpdate(BaseModel):
|
|||||||
language: Optional[str] = None
|
language: Optional[str] = None
|
||||||
month_divider_color: Optional[str] = None
|
month_divider_color: Optional[str] = None
|
||||||
month_label_color: Optional[str] = None
|
month_label_color: Optional[str] = None
|
||||||
text_color: Optional[str] = None
|
|
||||||
line_color: Optional[str] = None
|
|
||||||
bg_color: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
def _settings_dict(s: models.UserSettings) -> dict:
|
def _settings_dict(s: models.UserSettings) -> dict:
|
||||||
@@ -43,9 +40,6 @@ def _settings_dict(s: models.UserSettings) -> dict:
|
|||||||
"language": s.language or "de",
|
"language": s.language or "de",
|
||||||
"month_divider_color": s.month_divider_color or "#7090c0",
|
"month_divider_color": s.month_divider_color or "#7090c0",
|
||||||
"month_label_color": s.month_label_color or "#7090c0",
|
"month_label_color": s.month_label_color or "#7090c0",
|
||||||
"text_color": s.text_color,
|
|
||||||
"line_color": s.line_color,
|
|
||||||
"bg_color": s.bg_color,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -82,15 +76,7 @@ def update_settings(
|
|||||||
settings = models.UserSettings(user_id=current_user.id)
|
settings = models.UserSettings(user_id=current_user.id)
|
||||||
db.add(settings)
|
db.add(settings)
|
||||||
|
|
||||||
# For these three override colours, an explicit null is meaningful
|
for field, value in data.model_dump(exclude_none=True).items():
|
||||||
# ("reset to default") and must be persisted as NULL. All other fields
|
|
||||||
# keep the previous behaviour where a null/missing value is ignored.
|
|
||||||
NULLABLE_OVERRIDES = {"text_color", "line_color", "bg_color"}
|
|
||||||
update_data = data.model_dump(exclude_unset=True)
|
|
||||||
for field, value in update_data.items():
|
|
||||||
if field in NULLABLE_OVERRIDES:
|
|
||||||
setattr(settings, field, value or None)
|
|
||||||
elif value is not None:
|
|
||||||
setattr(settings, field, value)
|
setattr(settings, field, value)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -9,38 +9,50 @@
|
|||||||
--accent: #ea4335;
|
--accent: #ea4335;
|
||||||
--today-color: #4285f4;
|
--today-color: #4285f4;
|
||||||
|
|
||||||
--bg-app: #0e0e14;
|
/* Layered surfaces — subtly cool-tinted, slightly more depth between layers
|
||||||
--bg-topbar: #18181f;
|
so panels read via elevation instead of hard table-like 1px borders */
|
||||||
--bg-sidebar: #18181f;
|
--bg-app: #0c0c13;
|
||||||
--bg-surface: #22222c;
|
--bg-topbar: #14141d;
|
||||||
--bg-hover: #2a2a38;
|
--bg-sidebar: #14141d;
|
||||||
--bg-active: #32324a;
|
--bg-surface: #1d1d2a;
|
||||||
|
--bg-2: #16161f; /* used by quarter month cards */
|
||||||
|
--bg-card: #1d1d2a; /* used by recurrence day buttons */
|
||||||
|
--bg-hover: #262636;
|
||||||
|
--bg-active: #31314f;
|
||||||
|
|
||||||
--text-1: #e8e8f0;
|
--text-1: #e8e8f0;
|
||||||
--text-2: #9090aa;
|
--text-2: #9090aa;
|
||||||
--text-3: #55556a;
|
--text-3: #55556a;
|
||||||
--border: #2e2e40;
|
--border: #2e2e40;
|
||||||
--border-light: #242438;
|
--border-light: #242438;
|
||||||
--scrollbar: #30303c;
|
--scrollbar: #36364a;
|
||||||
|
|
||||||
--topbar-h: 64px;
|
--topbar-h: 64px;
|
||||||
--sidebar-w: 256px;
|
--sidebar-w: 256px;
|
||||||
--shadow: 0 2px 12px rgba(0,0,0,.45);
|
--shadow-sm: 0 1px 3px rgba(0,0,0,.30);
|
||||||
--shadow-lg: 0 8px 28px rgba(0,0,0,.55);
|
--shadow: 0 6px 20px rgba(0,0,0,.40);
|
||||||
--radius: 8px;
|
--shadow-lg: 0 16px 44px rgba(0,0,0,.55);
|
||||||
--radius-sm: 4px;
|
--radius-lg: 20px;
|
||||||
--transition: .15s ease;
|
--radius: 14px;
|
||||||
|
--radius-sm: 9px;
|
||||||
|
--transition: .18s cubic-bezier(.4, 0, .2, 1);
|
||||||
|
--ring: 0 0 0 3px var(--primary-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Reset ──────────────────────────────────────────────── */
|
/* ── Reset ──────────────────────────────────────────────── */
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
html, body { height: 100%; overflow: hidden; }
|
html, body { height: 100%; overflow: hidden; }
|
||||||
body {
|
body {
|
||||||
font-family: 'Google Sans', 'Roboto', system-ui, sans-serif;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'Helvetica Neue', system-ui, sans-serif;
|
||||||
background: var(--bg-app);
|
background: var(--bg-app);
|
||||||
color: var(--text-1);
|
color: var(--text-1);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
letter-spacing: .1px;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
input, select, textarea, button { font-family: inherit; font-size: inherit; color: inherit; }
|
input, select, textarea, button { font-family: inherit; font-size: inherit; color: inherit; }
|
||||||
button { cursor: pointer; border: none; background: none; }
|
button { cursor: pointer; border: none; background: none; }
|
||||||
@@ -55,129 +67,59 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
.flex-col { display: flex; flex-direction: column; }
|
.flex-col { display: flex; flex-direction: column; }
|
||||||
.gap-8 { gap: 8px; }
|
.gap-8 { gap: 8px; }
|
||||||
|
|
||||||
/* ── Buttons ──────────────────────────────────────────────
|
/* ── Buttons ────────────────────────────────────────────── */
|
||||||
Modern pill style: fully rounded, subtle coloured shadow on the
|
|
||||||
prominent variants, lift on hover, snap back on press. The
|
|
||||||
primary-coloured glow follows --primary via color-mix(), so it adapts
|
|
||||||
when the user changes the theme colour in settings. */
|
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
gap: 8px;
|
padding: 8px 16px; border-radius: 20px;
|
||||||
padding: 10px 22px;
|
font-weight: 500;
|
||||||
border-radius: 999px;
|
transition: background var(--transition), color var(--transition),
|
||||||
font-weight: 500; font-size: 14px;
|
box-shadow var(--transition), transform var(--transition),
|
||||||
letter-spacing: .1px;
|
|
||||||
white-space: nowrap;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
transition:
|
|
||||||
background var(--transition),
|
|
||||||
color var(--transition),
|
|
||||||
border-color var(--transition),
|
|
||||||
box-shadow .18s ease,
|
|
||||||
transform .12s ease,
|
|
||||||
filter var(--transition);
|
filter var(--transition);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.btn:active { transform: translateY(0) scale(.985); transition-duration: .05s; }
|
.btn:active { transform: translateY(1px); }
|
||||||
.btn:focus-visible {
|
.btn:focus-visible { outline: none; box-shadow: var(--ring); }
|
||||||
outline: 2px solid var(--primary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
|
background: linear-gradient(135deg, var(--primary), color-mix(in srgb, var(--primary) 80%, #14141d));
|
||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 2px 8px rgba(66,133,244,.28);
|
box-shadow: 0 2px 10px var(--primary-dim);
|
||||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--primary) 30%, transparent);
|
|
||||||
}
|
}
|
||||||
.btn-primary:hover {
|
.btn-primary:hover { filter: brightness(1.10); box-shadow: 0 5px 18px var(--primary-dim); }
|
||||||
filter: brightness(1.08);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 6px 18px rgba(66,133,244,.42);
|
|
||||||
box-shadow: 0 6px 18px color-mix(in srgb, var(--primary) 45%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
color: var(--text-1);
|
color: var(--text-1);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover { background: var(--bg-hover); }
|
||||||
background: var(--bg-hover);
|
.btn-ghost { color: var(--primary); }
|
||||||
border-color: var(--primary);
|
.btn-ghost:hover { background: var(--primary-dim); }
|
||||||
transform: translateY(-1px);
|
.btn-danger { background: var(--accent); color: #fff; }
|
||||||
}
|
.btn-danger:hover { filter: brightness(1.1); }
|
||||||
|
.btn-full { width: 100%; justify-content: center; }
|
||||||
.btn-ghost {
|
|
||||||
color: var(--primary);
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.btn-ghost:hover {
|
|
||||||
background: var(--primary-dim);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 2px 8px rgba(234,67,53,.28);
|
|
||||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 30%, transparent);
|
|
||||||
}
|
|
||||||
.btn-danger:hover {
|
|
||||||
filter: brightness(1.08);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 6px 18px rgba(234,67,53,.42);
|
|
||||||
box-shadow: 0 6px 18px color-mix(in srgb, var(--accent) 45%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-full { width: 100%; }
|
|
||||||
|
|
||||||
/* The big sidebar "Erstellen" button: same pill aesthetic, primary tinted,
|
|
||||||
lives in the calm dark sidebar so the shadow is a touch stronger. */
|
|
||||||
.btn-fab {
|
.btn-fab {
|
||||||
display: flex; align-items: center; gap: 10px;
|
display: flex; align-items: center; gap: 10px;
|
||||||
padding: 12px 22px;
|
padding: 12px 20px; border-radius: 24px;
|
||||||
border-radius: 999px;
|
background: var(--bg-surface);
|
||||||
background: var(--primary);
|
color: var(--text-1);
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
margin: 16px 12px 8px;
|
margin: 16px 12px 8px;
|
||||||
box-shadow: 0 4px 14px rgba(66,133,244,.32);
|
transition: background var(--transition), box-shadow var(--transition);
|
||||||
box-shadow: 0 4px 14px color-mix(in srgb, var(--primary) 35%, transparent);
|
|
||||||
transition:
|
|
||||||
background var(--transition),
|
|
||||||
box-shadow .18s ease,
|
|
||||||
transform .12s ease,
|
|
||||||
filter var(--transition);
|
|
||||||
}
|
}
|
||||||
.btn-fab:hover {
|
.btn-fab:hover { background: var(--bg-hover); box-shadow: var(--shadow-lg); }
|
||||||
filter: brightness(1.08);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 8px 22px rgba(66,133,244,.5);
|
|
||||||
box-shadow: 0 8px 22px color-mix(in srgb, var(--primary) 50%, transparent);
|
|
||||||
}
|
|
||||||
.btn-fab:active { transform: translateY(0) scale(.985); }
|
|
||||||
|
|
||||||
/* Circular icon buttons (topbar nav, modal close, etc.) */
|
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
width: 40px; height: 40px;
|
width: 40px; height: 40px; border-radius: 50%;
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--text-2);
|
color: var(--text-2);
|
||||||
|
transition: background var(--transition), color var(--transition), transform var(--transition);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
transition:
|
|
||||||
background var(--transition),
|
|
||||||
color var(--transition),
|
|
||||||
transform .1s ease;
|
|
||||||
}
|
}
|
||||||
.icon-btn svg { width: 20px; height: 20px; fill: currentColor; }
|
.icon-btn svg { width: 20px; height: 20px; fill: currentColor; }
|
||||||
.icon-btn:hover { background: var(--bg-hover); color: var(--text-1); }
|
.icon-btn:hover { background: var(--bg-hover); color: var(--text-1); }
|
||||||
.icon-btn:active { transform: scale(.92); }
|
.icon-btn:active { transform: scale(.92); }
|
||||||
.icon-btn:focus-visible {
|
.icon-btn:focus-visible { outline: none; box-shadow: var(--ring); }
|
||||||
outline: 2px solid var(--primary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Auth Screens ───────────────────────────────────────── */
|
/* ── Auth Screens ───────────────────────────────────────── */
|
||||||
.auth-screen {
|
.auth-screen {
|
||||||
@@ -216,26 +158,20 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
display: flex; flex-direction: column; gap: 6px;
|
display: flex; flex-direction: column; gap: 6px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.form-group label { color: var(--text-2); font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: .5px; }
|
.form-group label { color: var(--text-2); font-size: 13px; font-weight: 500; letter-spacing: 0; }
|
||||||
.form-group input, .form-group select, .form-group textarea {
|
.form-group input, .form-group select, .form-group textarea {
|
||||||
background: var(--bg-app);
|
background: var(--bg-app);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-sm);
|
||||||
padding: 11px 14px;
|
padding: 10px 12px;
|
||||||
color: var(--text-1);
|
color: var(--text-1);
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color var(--transition), box-shadow var(--transition);
|
transition: border-color var(--transition), box-shadow var(--transition);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.form-group input:hover:not(:focus),
|
|
||||||
.form-group select:hover:not(:focus),
|
|
||||||
.form-group textarea:hover:not(:focus) {
|
|
||||||
border-color: var(--text-3);
|
|
||||||
}
|
|
||||||
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
box-shadow: 0 0 0 3px rgba(66,133,244,.18);
|
box-shadow: var(--ring);
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
|
|
||||||
}
|
}
|
||||||
.form-group textarea { resize: vertical; }
|
.form-group textarea { resize: vertical; }
|
||||||
|
|
||||||
@@ -376,7 +312,8 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||||
height: var(--topbar-h);
|
height: var(--topbar-h);
|
||||||
background: var(--bg-topbar);
|
background: var(--bg-topbar);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
display: flex; align-items: center;
|
display: flex; align-items: center;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -389,15 +326,16 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
.view-title { font-size: 20px; font-weight: 400; color: var(--text-1); white-space: nowrap; padding-left: 8px; }
|
.view-title { font-size: 20px; font-weight: 400; color: var(--text-1); white-space: nowrap; padding-left: 8px; }
|
||||||
.topbar-right { display: flex; align-items: center; gap: 4px; }
|
.topbar-right { display: flex; align-items: center; gap: 4px; }
|
||||||
|
|
||||||
.view-switcher { display: flex; background: var(--bg-surface); border-radius: 20px; overflow: hidden; border: 1px solid var(--border); }
|
.view-switcher { display: flex; gap: 2px; padding: 3px; background: var(--bg-surface); border-radius: 22px; border: 1px solid var(--border-light); }
|
||||||
.view-btn {
|
.view-btn {
|
||||||
padding: 6px 14px; font-size: 13px; font-weight: 500;
|
padding: 6px 14px; font-size: 13px; font-weight: 500;
|
||||||
color: var(--text-2);
|
color: var(--text-2);
|
||||||
transition: background var(--transition), color var(--transition);
|
transition: background var(--transition), color var(--transition), box-shadow var(--transition);
|
||||||
border-radius: 20px;
|
border-radius: 18px;
|
||||||
}
|
}
|
||||||
.view-btn:hover { background: var(--bg-hover); color: var(--text-1); }
|
.view-btn:hover { background: var(--bg-hover); color: var(--text-1); }
|
||||||
.view-btn.active { background: var(--primary-dim); color: var(--primary); }
|
.view-btn:focus-visible { outline: none; box-shadow: var(--ring); }
|
||||||
|
.view-btn.active { background: var(--primary-dim); color: var(--primary); font-weight: 600; box-shadow: inset 0 0 0 1px var(--primary-dim); }
|
||||||
|
|
||||||
.user-menu-wrapper { position: relative; }
|
.user-menu-wrapper { position: relative; }
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
@@ -440,7 +378,7 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
width: var(--sidebar-w);
|
width: var(--sidebar-w);
|
||||||
background: var(--bg-sidebar);
|
background: var(--bg-sidebar);
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border-light);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
@@ -557,8 +495,7 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
}
|
}
|
||||||
.month-kw-cell {
|
.month-kw-cell {
|
||||||
position: absolute; left: 0; top: 0; bottom: 0; width: 38px;
|
position: absolute; left: 0; top: 0; bottom: 0; width: 38px;
|
||||||
display: flex; align-items: flex-start; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
padding-top: 6px;
|
|
||||||
font-size: 13px; color: var(--text-3); font-weight: 700;
|
font-size: 13px; color: var(--text-3); font-weight: 700;
|
||||||
border-right: 1px solid var(--border-light);
|
border-right: 1px solid var(--border-light);
|
||||||
cursor: default; user-select: none; z-index: 1;
|
cursor: default; user-select: none; z-index: 1;
|
||||||
@@ -572,7 +509,7 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
}
|
}
|
||||||
.month-col:last-child { border-right: none; }
|
.month-col:last-child { border-right: none; }
|
||||||
.month-col:hover { background: var(--bg-hover); }
|
.month-col:hover { background: var(--bg-hover); }
|
||||||
.month-col.today { background: rgba(66,133,244,.08); }
|
.month-col.today { background: color-mix(in srgb, var(--today-color) 9%, transparent); }
|
||||||
.month-col.month-selected { background: var(--primary-dim); }
|
.month-col.month-selected { background: var(--primary-dim); }
|
||||||
.month-col.month-selected .cell-day { border: 2px solid var(--primary); color: var(--primary); font-weight: 700; }
|
.month-col.month-selected .cell-day { border: 2px solid var(--primary); color: var(--primary); font-weight: 700; }
|
||||||
.month-col.month-selected .cell-day.today { background: var(--today-color); color: #fff; border: none; }
|
.month-col.month-selected .cell-day.today { background: var(--today-color); color: #fff; border: none; }
|
||||||
@@ -686,8 +623,7 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
/* KW badge in week view header gutter */
|
/* KW badge in week view header gutter */
|
||||||
.week-kw-badge {
|
.week-kw-badge {
|
||||||
font-size: 14px; font-weight: 700; color: var(--text-3);
|
font-size: 14px; font-weight: 700; color: var(--text-3);
|
||||||
display: flex; align-items: flex-end; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
padding-bottom: 6px;
|
|
||||||
text-transform: uppercase; letter-spacing: .3px;
|
text-transform: uppercase; letter-spacing: .3px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
@@ -727,7 +663,7 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
height: 18px; line-height: 18px;
|
height: 18px; line-height: 18px;
|
||||||
font-size: 11px; font-weight: 500;
|
font-size: 11px; font-weight: 500;
|
||||||
padding: 0 6px; border-radius: 3px;
|
padding: 0 6px; border-radius: 6px;
|
||||||
cursor: pointer; pointer-events: all;
|
cursor: pointer; pointer-events: all;
|
||||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
@@ -778,7 +714,7 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
|
|
||||||
/* Timed events */
|
/* Timed events */
|
||||||
.timed-event {
|
.timed-event {
|
||||||
position: absolute; border-radius: 4px;
|
position: absolute; border-radius: 7px;
|
||||||
padding: 2px 4px; cursor: pointer; overflow: hidden;
|
padding: 2px 4px; cursor: pointer; overflow: hidden;
|
||||||
font-size: 11px; font-weight: 500;
|
font-size: 11px; font-weight: 500;
|
||||||
border-left: 3px solid rgba(0,0,0,.25);
|
border-left: 3px solid rgba(0,0,0,.25);
|
||||||
@@ -810,16 +746,19 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
background: var(--bg-2);
|
background: var(--bg-2);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border-light);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: box-shadow var(--transition), transform var(--transition);
|
||||||
}
|
}
|
||||||
|
.qtr-month:hover { box-shadow: var(--shadow); transform: translateY(-2px); }
|
||||||
.qtr-month-name {
|
.qtr-month-name {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-1);
|
color: var(--text-1);
|
||||||
padding: 10px 12px 8px;
|
padding: 10px 12px 8px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border-light);
|
||||||
letter-spacing: .3px;
|
letter-spacing: .3px;
|
||||||
}
|
}
|
||||||
.qtr-month-grid { padding: 6px 8px 8px; }
|
.qtr-month-grid { padding: 6px 8px 8px; }
|
||||||
@@ -922,24 +861,34 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
/* ── Modals ─────────────────────────────────────────────── */
|
/* ── Modals ─────────────────────────────────────────────── */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed; inset: 0; z-index: 500;
|
position: fixed; inset: 0; z-index: 500;
|
||||||
background: rgba(0,0,0,.6);
|
background: rgba(8,8,14,.55);
|
||||||
|
-webkit-backdrop-filter: blur(5px);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.modal-card {
|
.modal-card {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border-light);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 90vh; overflow-y: auto;
|
max-height: 90vh; overflow-y: auto;
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
|
animation: card-in .2s cubic-bezier(.4, 0, .2, 1);
|
||||||
|
}
|
||||||
|
@keyframes card-in {
|
||||||
|
from { opacity: 0; transform: translateY(8px) scale(.985); }
|
||||||
|
to { opacity: 1; transform: none; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.modal-card { animation: none; }
|
||||||
}
|
}
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex; align-items: center;
|
display: flex; align-items: center;
|
||||||
padding: 16px 20px; border-bottom: 1px solid var(--border);
|
padding: 16px 20px; border-bottom: 1px solid var(--border-light);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.modal-header h3 { font-size: 18px; font-weight: 500; flex: 1; }
|
.modal-header h3 { font-size: 18px; font-weight: 600; flex: 1; letter-spacing: -.2px; }
|
||||||
.modal-close { font-size: 24px; }
|
.modal-close { font-size: 24px; }
|
||||||
.modal-body { padding: 20px; }
|
.modal-body { padding: 20px; }
|
||||||
.modal-body p { margin: 0 0 14px; font-size: 14px; color: var(--text-1); }
|
.modal-body p { margin: 0 0 14px; font-size: 14px; color: var(--text-1); }
|
||||||
@@ -948,7 +897,7 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
.modal-body a:hover { text-decoration: underline; }
|
.modal-body a:hover { text-decoration: underline; }
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
display: flex; align-items: center; gap: 8px;
|
display: flex; align-items: center; gap: 8px;
|
||||||
padding: 12px 20px; border-top: 1px solid var(--border);
|
padding: 14px 20px; border-top: 1px solid var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Recurrence UI ─────────────────────────────────────── */
|
/* ── Recurrence UI ─────────────────────────────────────── */
|
||||||
@@ -975,79 +924,26 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
}
|
}
|
||||||
.ctx-item:hover { background: var(--bg-hover); }
|
.ctx-item:hover { background: var(--bg-hover); }
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
/* ── Event Popup ──────────────────────────────────────────
|
|
||||||
Layout: Color-Dot + Title links, kleine Icon-Toolbar rechts oben.
|
|
||||||
Icons sind im Ruhezustand transparent (nur das SVG selbst sichtbar),
|
|
||||||
bekommen erst beim Hover einen runden farbigen Hintergrund. Wirkt
|
|
||||||
modern und lässt dem Titel die meiste Breite. */
|
|
||||||
=======
|
|
||||||
/* ── Event Popup ────────────────────────────────────────── */
|
/* ── Event Popup ────────────────────────────────────────── */
|
||||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
|
||||||
.event-popup {
|
.event-popup {
|
||||||
position: fixed; z-index: 600;
|
position: fixed; z-index: 600;
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border-light);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
width: 360px;
|
width: 340px;
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: card-in .16s cubic-bezier(.4, 0, .2, 1);
|
||||||
}
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) { .event-popup { animation: none; } }
|
||||||
.popup-header {
|
.popup-header {
|
||||||
display: flex; align-items: flex-start; gap: 10px;
|
display: flex; align-items: flex-start; gap: 10px;
|
||||||
padding: 12px 10px 12px 16px;
|
padding: 14px 12px 12px 16px; border-bottom: 1px solid var(--border-light);
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
}
|
||||||
.popup-color-dot {
|
.popup-color-dot { width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0; margin-top: 5px; }
|
||||||
width: 11px; height: 11px; border-radius: 50%;
|
.popup-header h4 { flex: 1; min-width: 0; font-size: 15px; font-weight: 600; line-height: 1.35; overflow-wrap: anywhere; padding-top: 1px; }
|
||||||
flex-shrink: 0;
|
.popup-action, .popup-close { width: 30px; height: 30px; font-size: 16px; flex-shrink: 0; }
|
||||||
margin-top: 6px;
|
.popup-close { margin-left: 2px; }
|
||||||
}
|
|
||||||
.popup-header h4 {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 14px; font-weight: 500;
|
|
||||||
line-height: 1.4;
|
|
||||||
word-break: break-word;
|
|
||||||
padding-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 2px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
.popup-icon-btn {
|
|
||||||
width: 30px; height: 30px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-3);
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
transition:
|
|
||||||
background var(--transition),
|
|
||||||
color var(--transition),
|
|
||||||
transform .1s ease;
|
|
||||||
}
|
|
||||||
.popup-icon-btn svg { width: 15px; height: 15px; fill: currentColor; flex-shrink: 0; }
|
|
||||||
.popup-icon-btn:hover {
|
|
||||||
background: rgba(66,133,244,.16);
|
|
||||||
background: color-mix(in srgb, var(--primary) 16%, transparent);
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
.popup-icon-btn-danger:hover {
|
|
||||||
background: rgba(234,67,53,.16);
|
|
||||||
background: color-mix(in srgb, var(--accent) 16%, transparent);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
.popup-icon-btn-close:hover {
|
|
||||||
background: var(--bg-hover);
|
|
||||||
color: var(--text-1);
|
|
||||||
}
|
|
||||||
.popup-icon-btn:active { transform: scale(.9); }
|
|
||||||
|
|
||||||
.popup-body { padding: 12px 16px; }
|
.popup-body { padding: 12px 16px; }
|
||||||
.popup-time, .popup-location, .popup-calendar { font-size: 13px; color: var(--text-2); margin-bottom: 6px; }
|
.popup-time, .popup-location, .popup-calendar { font-size: 13px; color: var(--text-2); margin-bottom: 6px; }
|
||||||
.popup-description { font-size: 13px; color: var(--text-1); margin-bottom: 6px; white-space: pre-wrap; }
|
.popup-description { font-size: 13px; color: var(--text-1); margin-bottom: 6px; white-space: pre-wrap; }
|
||||||
@@ -1161,7 +1057,7 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
.settings-section h4 { font-size: 14px; font-weight: 600; color: var(--text-1); margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
|
.settings-section h4 { font-size: 14px; font-weight: 600; color: var(--text-1); margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
|
||||||
.badge-admin {
|
.badge-admin {
|
||||||
font-size: 10px; padding: 2px 6px;
|
font-size: 10px; padding: 2px 6px;
|
||||||
background: rgba(66,133,244,.2); color: var(--primary);
|
background: var(--primary-dim); color: var(--primary);
|
||||||
border-radius: 10px; font-weight: 600;
|
border-radius: 10px; font-weight: 600;
|
||||||
}
|
}
|
||||||
.users-list-item {
|
.users-list-item {
|
||||||
@@ -1289,7 +1185,7 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
.month-span-event {
|
.month-span-event {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 18px; line-height: 18px;
|
height: 18px; line-height: 18px;
|
||||||
border-radius: 3px; padding: 0 6px;
|
border-radius: 6px; padding: 0 6px;
|
||||||
font-size: 11px; font-weight: 500;
|
font-size: 11px; font-weight: 500;
|
||||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
cursor: pointer; color: #fff;
|
cursor: pointer; color: #fff;
|
||||||
@@ -1701,14 +1597,6 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
.topbar-left { gap: 0; }
|
.topbar-left { gap: 0; }
|
||||||
.topbar-right { gap: 0; }
|
.topbar-right { gap: 0; }
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
/* Event-Popup auf Mobile: an Viewport-Breite anpassen */
|
|
||||||
.event-popup { width: min(94vw, 380px); max-width: 94vw; }
|
|
||||||
.popup-header { padding: 10px 8px 10px 14px; }
|
|
||||||
.popup-header h4 { font-size: 13.5px; }
|
|
||||||
.popup-icon-btn { width: 32px; height: 32px; }
|
|
||||||
.popup-icon-btn svg { width: 16px; height: 16px; }
|
|
||||||
=======
|
|
||||||
/* Event-Popup: Buttons kompakt halten, kein 44px-Override ───── */
|
/* Event-Popup: Buttons kompakt halten, kein 44px-Override ───── */
|
||||||
.event-popup .icon-btn {
|
.event-popup .icon-btn {
|
||||||
min-width: 32px !important;
|
min-width: 32px !important;
|
||||||
@@ -1718,7 +1606,6 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
}
|
}
|
||||||
.event-popup .popup-header { gap: 2px; padding: 10px 12px; }
|
.event-popup .popup-header { gap: 2px; padding: 10px 12px; }
|
||||||
.event-popup { width: min(92vw, 340px); max-width: 92vw; }
|
.event-popup { width: min(92vw, 340px); max-width: 92vw; }
|
||||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
|
||||||
|
|
||||||
/* Monatsansicht: Startzeit ausblenden — nur Titel anzeigen ──── */
|
/* Monatsansicht: Startzeit ausblenden — nur Titel anzeigen ──── */
|
||||||
.month-event-time { display: none; }
|
.month-event-time { display: none; }
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
<!-- APP_VERSION: update here + version.js on every release -->
|
<!-- APP_VERSION: update here + version.js on every release -->
|
||||||
<title>Calendarr v18</title>
|
<title>Calendarr v11</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="theme-color" content="#4285f4" />
|
<meta name="theme-color" content="#4285f4" />
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<button type="submit" class="btn btn-primary btn-full">Anmelden</button>
|
<button type="submit" class="btn btn-primary btn-full">Anmelden</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<button class="impressum-link" onclick="openImpressum()">© 2026 Scarriffleservices · v18</button>
|
<button class="impressum-link" onclick="openImpressum()">© 2026 Scarriffleservices · v11</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ─── MAIN APP ──────────────────────────────────────────── -->
|
<!-- ─── MAIN APP ──────────────────────────────────────────── -->
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
<div id="cal-list-items"></div>
|
<div id="cal-list-items"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="sidebar-copyright" onclick="openImpressum()">© 2026 Scarriffleservices · v18</button>
|
<button class="sidebar-copyright" onclick="openImpressum()">© 2026 Scarriffleservices · v11</button>
|
||||||
</aside>
|
</aside>
|
||||||
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
|
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@
|
|||||||
<input type="hidden" id="ev-start" />
|
<input type="hidden" id="ev-start" />
|
||||||
<div class="dt-display" id="ev-start-display" tabindex="0" role="button">
|
<div class="dt-display" id="ev-start-display" tabindex="0" role="button">
|
||||||
<span class="dt-display-text">—</span>
|
<span class="dt-display-text">—</span>
|
||||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v17H7z"/></svg>
|
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v11H7z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group half">
|
<div class="form-group half">
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
<input type="hidden" id="ev-end" />
|
<input type="hidden" id="ev-end" />
|
||||||
<div class="dt-display" id="ev-end-display" tabindex="0" role="button">
|
<div class="dt-display" id="ev-end-display" tabindex="0" role="button">
|
||||||
<span class="dt-display-text">—</span>
|
<span class="dt-display-text">—</span>
|
||||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v17H7z"/></svg>
|
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v11H7z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,7 +253,7 @@
|
|||||||
<input type="hidden" id="ev-start-date" />
|
<input type="hidden" id="ev-start-date" />
|
||||||
<div class="dt-display" id="ev-start-date-display" tabindex="0" role="button">
|
<div class="dt-display" id="ev-start-date-display" tabindex="0" role="button">
|
||||||
<span class="dt-display-text">—</span>
|
<span class="dt-display-text">—</span>
|
||||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v17H7z"/></svg>
|
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v11H7z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group half">
|
<div class="form-group half">
|
||||||
@@ -261,7 +261,7 @@
|
|||||||
<input type="hidden" id="ev-end-date" />
|
<input type="hidden" id="ev-end-date" />
|
||||||
<div class="dt-display" id="ev-end-date-display" tabindex="0" role="button">
|
<div class="dt-display" id="ev-end-date-display" tabindex="0" role="button">
|
||||||
<span class="dt-display-text">—</span>
|
<span class="dt-display-text">—</span>
|
||||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v17H7z"/></svg>
|
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v11H7z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,7 +311,7 @@
|
|||||||
<input type="hidden" id="ev-rec-until" />
|
<input type="hidden" id="ev-rec-until" />
|
||||||
<div class="dt-display" id="ev-rec-until-display" tabindex="0" role="button">
|
<div class="dt-display" id="ev-rec-until-display" tabindex="0" role="button">
|
||||||
<span class="dt-display-text">—</span>
|
<span class="dt-display-text">—</span>
|
||||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v17H7z"/></svg>
|
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v11H7z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -375,20 +375,16 @@
|
|||||||
<div class="popup-header">
|
<div class="popup-header">
|
||||||
<div class="popup-color-dot" id="popup-color-dot"></div>
|
<div class="popup-color-dot" id="popup-color-dot"></div>
|
||||||
<h4 id="popup-title"></h4>
|
<h4 id="popup-title"></h4>
|
||||||
<div class="popup-toolbar">
|
<button class="icon-btn popup-action" id="popup-edit" title="Bearbeiten">
|
||||||
<button class="popup-icon-btn" id="popup-edit" title="Bearbeiten" aria-label="Bearbeiten">
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
|
||||||
</button>
|
</button>
|
||||||
<button class="popup-icon-btn" id="popup-copy" title="Kopieren" aria-label="Kopieren">
|
<button class="icon-btn popup-action" id="popup-copy" title="Kopieren nach…">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v24h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v24c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v24z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="popup-icon-btn popup-icon-btn-danger" id="popup-delete" title="Löschen" aria-label="Löschen">
|
<button class="icon-btn popup-action" id="popup-delete" title="Löschen">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v22zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="popup-icon-btn popup-icon-btn-close" id="popup-close" title="Schließen" aria-label="Schließen">
|
<button class="icon-btn popup-close" id="popup-close">×</button>
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="popup-body">
|
<div class="popup-body">
|
||||||
<div class="popup-time" id="popup-time"></div>
|
<div class="popup-time" id="popup-time"></div>
|
||||||
@@ -622,29 +618,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_text_contrast">Schriftkontrast</h4>
|
||||||
<label data-i18n="settings_text_color">Schriftfarbe</label>
|
<p class="panel-desc" data-i18n="settings_text_contrast_desc">Helligkeit der Beschriftungen und Texte</p>
|
||||||
<div class="ev-color-row">
|
<div class="contrast-selector" id="cfg-text-contrast" data-setting="text_contrast">
|
||||||
<input type="text" id="cfg-text-color-hex" class="ev-color-hex" maxlength="7" spellcheck="false" placeholder="auto" />
|
<button class="contrast-btn" data-val="1"><span style="color:#606070">Aa</span><span class="contrast-lbl" data-i18n="contrast_dark">Dunkel</span></button>
|
||||||
<div class="ev-color-preview" id="cfg-text-color-preview" data-i18n-title="color_pick" title="Farbe wählen"></div>
|
<button class="contrast-btn" data-val="2"><span style="color:#9090a8">Aa</span><span class="contrast-lbl" data-i18n="contrast_medium">Mittel</span></button>
|
||||||
<button type="button" class="btn btn-ghost btn-sm" id="cfg-text-color-reset" data-i18n="reset" title="Zurücksetzen">Reset</button>
|
<button class="contrast-btn" data-val="3"><span style="color:#c8c8d8">Aa</span><span class="contrast-lbl" data-i18n="contrast_light">Hell</span></button>
|
||||||
</div>
|
<button class="contrast-btn" data-val="4"><span style="color:#ffffff">Aa</span><span class="contrast-lbl" data-i18n="contrast_max">Maximum</span></button>
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label data-i18n="settings_line_color">Linienfarbe</label>
|
|
||||||
<div class="ev-color-row">
|
|
||||||
<input type="text" id="cfg-line-color-hex" class="ev-color-hex" maxlength="7" spellcheck="false" placeholder="auto" />
|
|
||||||
<div class="ev-color-preview" id="cfg-line-color-preview" data-i18n-title="color_pick" title="Farbe wählen"></div>
|
|
||||||
<button type="button" class="btn btn-ghost btn-sm" id="cfg-line-color-reset" data-i18n="reset" title="Zurücksetzen">Reset</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label data-i18n="settings_bg_color">Hintergrundfarbe</label>
|
|
||||||
<div class="ev-color-row">
|
|
||||||
<input type="text" id="cfg-bg-color-hex" class="ev-color-hex" maxlength="7" spellcheck="false" placeholder="auto" />
|
|
||||||
<div class="ev-color-preview" id="cfg-bg-color-preview" data-i18n-title="color_pick" title="Farbe wählen"></div>
|
|
||||||
<button type="button" class="btn btn-ghost btn-sm" id="cfg-bg-color-reset" data-i18n="reset" title="Zurücksetzen">Reset</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_line_contrast">Linienkontrast</h4>
|
||||||
|
<p class="panel-desc" data-i18n="settings_line_contrast_desc">Sichtbarkeit von Trennlinien und Rahmen</p>
|
||||||
|
<div class="contrast-selector" id="cfg-line-contrast" data-setting="line_contrast">
|
||||||
|
<button class="contrast-btn" data-val="1"><span class="line-preview" style="border-color:#1e1e2c"></span><span class="contrast-lbl" data-i18n="line_barely">Kaum</span></button>
|
||||||
|
<button class="contrast-btn" data-val="2"><span class="line-preview" style="border-color:#2a2a3c"></span><span class="contrast-lbl" data-i18n="line_subtle">Subtil</span></button>
|
||||||
|
<button class="contrast-btn" data-val="3"><span class="line-preview" style="border-color:#3a3a52"></span><span class="contrast-lbl" data-i18n="line_normal">Normal</span></button>
|
||||||
|
<button class="contrast-btn" data-val="4"><span class="line-preview" style="border-color:#5a5a78"></span><span class="contrast-lbl" data-i18n="line_strong">Stark</span></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_calendar_view">Kalenderansicht</h4>
|
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_calendar_view">Kalenderansicht</h4>
|
||||||
@@ -895,7 +884,7 @@
|
|||||||
<a href="mailto:scarriffleservices@gmail.com">scarriffleservices@gmail.com</a></p>
|
<a href="mailto:scarriffleservices@gmail.com">scarriffleservices@gmail.com</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer" style="justify-content:space-between;align-items:center">
|
<div class="modal-footer" style="justify-content:space-between;align-items:center">
|
||||||
<span style="font-size:12px;color:var(--text-3)">Calendarr v18</span>
|
<span style="font-size:12px;color:var(--text-3)">Calendarr v11</span>
|
||||||
<button class="btn btn-ghost" onclick="closeImpressum()">Schliessen</button>
|
<button class="btn btn-ghost" onclick="closeImpressum()">Schliessen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,38 +40,6 @@ let state = {
|
|||||||
selectedEventColor: '', // '' = use calendar color
|
selectedEventColor: '', // '' = use calendar color
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── URL state ────────────────────────────────────────────
|
|
||||||
// View + Date werden in der URL als #date=YYYY-MM-DD&view=<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 ───────────────────────────────────────────
|
// ── Public init ───────────────────────────────────────────
|
||||||
export async function initCalendar() {
|
export async function initCalendar() {
|
||||||
const [settings, accounts, localCalendars, icalSubscriptions, googleAccounts, haAccounts] = await Promise.all([
|
const [settings, accounts, localCalendars, icalSubscriptions, googleAccounts, haAccounts] = await Promise.all([
|
||||||
@@ -93,11 +61,6 @@ export async function initCalendar() {
|
|||||||
state.dimPast = settings.dim_past_events;
|
state.dimPast = settings.dim_past_events;
|
||||||
weekStartDay = settings.week_start_day || 'monday';
|
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');
|
setLang(settings.language || 'de');
|
||||||
applyTheme(settings);
|
applyTheme(settings);
|
||||||
updateViewButtons();
|
updateViewButtons();
|
||||||
@@ -115,25 +78,6 @@ export async function initCalendar() {
|
|||||||
bindProfileModal();
|
bindProfileModal();
|
||||||
bindSwipeNavigation();
|
bindSwipeNavigation();
|
||||||
handleHAOAuthReturn();
|
handleHAOAuthReturn();
|
||||||
<<<<<<< HEAD
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
});
|
|
||||||
=======
|
|
||||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleHAOAuthReturn() {
|
function handleHAOAuthReturn() {
|
||||||
@@ -147,11 +91,7 @@ function handleHAOAuthReturn() {
|
|||||||
};
|
};
|
||||||
if (params.has('ha_connected')) {
|
if (params.has('ha_connected')) {
|
||||||
showToast('Home Assistant verbunden');
|
showToast('Home Assistant verbunden');
|
||||||
<<<<<<< HEAD
|
|
||||||
window.history.replaceState({}, '', window.location.pathname + window.location.hash);
|
|
||||||
=======
|
|
||||||
window.history.replaceState({}, '', window.location.pathname);
|
window.history.replaceState({}, '', window.location.pathname);
|
||||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
|
||||||
fetchAndRender(true);
|
fetchAndRender(true);
|
||||||
api.get('/homeassistant/accounts').then(accs => {
|
api.get('/homeassistant/accounts').then(accs => {
|
||||||
state.haAccounts = accs || [];
|
state.haAccounts = accs || [];
|
||||||
@@ -161,11 +101,7 @@ function handleHAOAuthReturn() {
|
|||||||
} else if (params.has('ha_error')) {
|
} else if (params.has('ha_error')) {
|
||||||
const code = params.get('ha_error');
|
const code = params.get('ha_error');
|
||||||
showToast(errMap[code] || `HA-Anmeldung fehlgeschlagen: ${code}`, true);
|
showToast(errMap[code] || `HA-Anmeldung fehlgeschlagen: ${code}`, true);
|
||||||
<<<<<<< HEAD
|
|
||||||
window.history.replaceState({}, '', window.location.pathname + window.location.hash);
|
|
||||||
=======
|
|
||||||
window.history.replaceState({}, '', window.location.pathname);
|
window.history.replaceState({}, '', window.location.pathname);
|
||||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,10 +196,6 @@ async function fetchAndRender(force = false, silent = false) {
|
|||||||
renderView();
|
renderView();
|
||||||
updateTitle();
|
updateTitle();
|
||||||
renderMiniCal();
|
renderMiniCal();
|
||||||
<<<<<<< HEAD
|
|
||||||
writeUrlState();
|
|
||||||
=======
|
|
||||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
|
||||||
prefetchIfNeeded(start, end); // extend cache in background if approaching an edge
|
prefetchIfNeeded(start, end); // extend cache in background if approaching an edge
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -292,7 +224,6 @@ async function fetchAndRender(force = false, silent = false) {
|
|||||||
renderView();
|
renderView();
|
||||||
updateTitle();
|
updateTitle();
|
||||||
renderMiniCal();
|
renderMiniCal();
|
||||||
writeUrlState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getViewRange() {
|
function getViewRange() {
|
||||||
@@ -2045,24 +1976,6 @@ function openSettingsModal() {
|
|||||||
document.getElementById(id + '-hex').value = val.toUpperCase();
|
document.getElementById(id + '-hex').value = val.toUpperCase();
|
||||||
document.getElementById(id + '-preview').style.background = val;
|
document.getElementById(id + '-preview').style.background = val;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optional colour overrides — empty hex input means "auto"
|
|
||||||
[
|
|
||||||
{ id: 'cfg-text-color', val: s.text_color },
|
|
||||||
{ id: 'cfg-line-color', val: s.line_color },
|
|
||||||
{ id: 'cfg-bg-color', val: s.bg_color },
|
|
||||||
].forEach(({ id, val }) => {
|
|
||||||
const hex = document.getElementById(id + '-hex');
|
|
||||||
const prev = document.getElementById(id + '-preview');
|
|
||||||
if (!hex || !prev) return;
|
|
||||||
if (val) {
|
|
||||||
hex.value = String(val).toUpperCase();
|
|
||||||
prev.style.background = val;
|
|
||||||
} else {
|
|
||||||
hex.value = '';
|
|
||||||
prev.style.background = 'transparent';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.getElementById('cfg-dim-past').checked = !!s.dim_past_events;
|
document.getElementById('cfg-dim-past').checked = !!s.dim_past_events;
|
||||||
document.getElementById('cfg-language').value = getLang();
|
document.getElementById('cfg-language').value = getLang();
|
||||||
|
|
||||||
@@ -2394,32 +2307,6 @@ function bindSettingsModal() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optional override colours (text / line / background) — empty = use default
|
|
||||||
[
|
|
||||||
{ prefix: 'cfg-text-color', defaultColor: '#c8c8d8' },
|
|
||||||
{ prefix: 'cfg-line-color', defaultColor: '#3a3a52' },
|
|
||||||
{ prefix: 'cfg-bg-color', defaultColor: '#0e0e14' },
|
|
||||||
].forEach(({ prefix, defaultColor }) => {
|
|
||||||
const preview = document.getElementById(prefix + '-preview');
|
|
||||||
const hex = document.getElementById(prefix + '-hex');
|
|
||||||
const reset = document.getElementById(prefix + '-reset');
|
|
||||||
if (!preview || !hex || !reset) return;
|
|
||||||
preview.addEventListener('click', async () => {
|
|
||||||
const picked = await openColorPicker(preview, hex.value || defaultColor);
|
|
||||||
if (picked) { hex.value = picked.toUpperCase(); preview.style.background = picked; }
|
|
||||||
});
|
|
||||||
hex.addEventListener('change', () => {
|
|
||||||
let val = hex.value.trim();
|
|
||||||
if (!val) { preview.style.background = 'transparent'; return; }
|
|
||||||
if (!val.startsWith('#')) val = '#' + val;
|
|
||||||
if (/^#[0-9a-fA-F]{6}$/.test(val)) { hex.value = val.toUpperCase(); preview.style.background = val; }
|
|
||||||
});
|
|
||||||
reset.addEventListener('click', () => {
|
|
||||||
hex.value = '';
|
|
||||||
preview.style.background = 'transparent';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Panel navigation
|
// Panel navigation
|
||||||
document.querySelectorAll('.settings-nav-btn').forEach(btn => {
|
document.querySelectorAll('.settings-nav-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => activateSettingsPanel(btn.dataset.panel));
|
btn.addEventListener('click', () => activateSettingsPanel(btn.dataset.panel));
|
||||||
@@ -2459,11 +2346,6 @@ function bindSettingsModal() {
|
|||||||
const btn = document.querySelector(`#${id} .contrast-btn.active`);
|
const btn = document.querySelector(`#${id} .contrast-btn.active`);
|
||||||
return btn ? Number(btn.dataset.val) : null;
|
return btn ? Number(btn.dataset.val) : null;
|
||||||
};
|
};
|
||||||
// Optional override colours: empty input → null (use default)
|
|
||||||
const colourOrNull = (id) => {
|
|
||||||
const v = (document.getElementById(id).value || '').trim();
|
|
||||||
return /^#[0-9a-fA-F]{6}$/.test(v) ? v : null;
|
|
||||||
};
|
|
||||||
const settings = {
|
const settings = {
|
||||||
default_view: document.getElementById('cfg-default-view').value,
|
default_view: document.getElementById('cfg-default-view').value,
|
||||||
week_start_day: document.getElementById('cfg-week-start').value,
|
week_start_day: document.getElementById('cfg-week-start').value,
|
||||||
@@ -2472,10 +2354,9 @@ function bindSettingsModal() {
|
|||||||
today_color: document.getElementById('cfg-today-hex').value,
|
today_color: document.getElementById('cfg-today-hex').value,
|
||||||
month_divider_color: document.getElementById('cfg-month-divider-hex').value,
|
month_divider_color: document.getElementById('cfg-month-divider-hex').value,
|
||||||
month_label_color: document.getElementById('cfg-month-label-hex').value,
|
month_label_color: document.getElementById('cfg-month-label-hex').value,
|
||||||
text_color: colourOrNull('cfg-text-color-hex'),
|
|
||||||
line_color: colourOrNull('cfg-line-color-hex'),
|
|
||||||
bg_color: colourOrNull('cfg-bg-color-hex'),
|
|
||||||
dim_past_events: document.getElementById('cfg-dim-past').checked,
|
dim_past_events: document.getElementById('cfg-dim-past').checked,
|
||||||
|
text_contrast: getActive('cfg-text-contrast') || 3,
|
||||||
|
line_contrast: getActive('cfg-line-contrast') || 3,
|
||||||
hour_height: getActive('cfg-hour-height') || 44,
|
hour_height: getActive('cfg-hour-height') || 44,
|
||||||
language: document.getElementById('cfg-language').value,
|
language: document.getElementById('cfg-language').value,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,10 +67,6 @@ const translations = {
|
|||||||
settings_today_color: 'Heutige-Tag-Farbe',
|
settings_today_color: 'Heutige-Tag-Farbe',
|
||||||
settings_month_divider_color: 'Monatswechsel-Linie',
|
settings_month_divider_color: 'Monatswechsel-Linie',
|
||||||
settings_month_label_color: 'Monatskürzel-Farbe',
|
settings_month_label_color: 'Monatskürzel-Farbe',
|
||||||
settings_text_color: 'Schriftfarbe',
|
|
||||||
settings_line_color: 'Linienfarbe',
|
|
||||||
settings_bg_color: 'Hintergrundfarbe',
|
|
||||||
reset: 'Reset',
|
|
||||||
settings_text_contrast: 'Schriftkontrast',
|
settings_text_contrast: 'Schriftkontrast',
|
||||||
settings_text_contrast_desc: 'Helligkeit der Beschriftungen und Texte',
|
settings_text_contrast_desc: 'Helligkeit der Beschriftungen und Texte',
|
||||||
contrast_dark: 'Dunkel', contrast_medium: 'Mittel',
|
contrast_dark: 'Dunkel', contrast_medium: 'Mittel',
|
||||||
@@ -158,11 +154,7 @@ const translations = {
|
|||||||
rec_every: 'Alle', rec_days: 'Tage', rec_weeks: 'Wochen', rec_months: 'Monate',
|
rec_every: 'Alle', rec_days: 'Tage', rec_weeks: 'Wochen', rec_months: 'Monate',
|
||||||
rec_ends: 'Endet', rec_never: 'Nie', rec_after_count: 'Nach Anzahl',
|
rec_ends: 'Endet', rec_never: 'Nie', rec_after_count: 'Nach Anzahl',
|
||||||
rec_on_date: 'Am Datum', rec_occurrences: 'Termine',
|
rec_on_date: 'Am Datum', rec_occurrences: 'Termine',
|
||||||
<<<<<<< HEAD
|
|
||||||
copy_to_calendar: 'Kopieren nach…', event_copied: 'Termin kopiert', copy: 'Kopieren',
|
|
||||||
=======
|
|
||||||
copy_to_calendar: 'Kopieren nach…', event_copied: 'Termin kopiert',
|
copy_to_calendar: 'Kopieren nach…', event_copied: 'Termin kopiert',
|
||||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
|
||||||
edit_before_copy: 'Vor dem Kopieren bearbeiten',
|
edit_before_copy: 'Vor dem Kopieren bearbeiten',
|
||||||
event_updated: 'Termin aktualisiert', event_created: 'Termin erstellt',
|
event_updated: 'Termin aktualisiert', event_created: 'Termin erstellt',
|
||||||
confirm_delete_event: '"{title}" wirklich löschen?',
|
confirm_delete_event: '"{title}" wirklich löschen?',
|
||||||
@@ -286,10 +278,6 @@ const translations = {
|
|||||||
settings_today_color: 'Today highlight color',
|
settings_today_color: 'Today highlight color',
|
||||||
settings_month_divider_color: 'Month divider line',
|
settings_month_divider_color: 'Month divider line',
|
||||||
settings_month_label_color: 'Month label color',
|
settings_month_label_color: 'Month label color',
|
||||||
settings_text_color: 'Text color',
|
|
||||||
settings_line_color: 'Line color',
|
|
||||||
settings_bg_color: 'Background color',
|
|
||||||
reset: 'Reset',
|
|
||||||
settings_text_contrast: 'Text contrast',
|
settings_text_contrast: 'Text contrast',
|
||||||
settings_text_contrast_desc: 'Brightness of labels and text',
|
settings_text_contrast_desc: 'Brightness of labels and text',
|
||||||
contrast_dark: 'Dark', contrast_medium: 'Medium',
|
contrast_dark: 'Dark', contrast_medium: 'Medium',
|
||||||
@@ -377,11 +365,7 @@ const translations = {
|
|||||||
rec_every: 'Every', rec_days: 'days', rec_weeks: 'weeks', rec_months: 'months',
|
rec_every: 'Every', rec_days: 'days', rec_weeks: 'weeks', rec_months: 'months',
|
||||||
rec_ends: 'Ends', rec_never: 'Never', rec_after_count: 'After count',
|
rec_ends: 'Ends', rec_never: 'Never', rec_after_count: 'After count',
|
||||||
rec_on_date: 'On date', rec_occurrences: 'occurrences',
|
rec_on_date: 'On date', rec_occurrences: 'occurrences',
|
||||||
<<<<<<< HEAD
|
|
||||||
copy_to_calendar: 'Copy to…', event_copied: 'Event copied', copy: 'Copy',
|
|
||||||
=======
|
|
||||||
copy_to_calendar: 'Copy to…', event_copied: 'Event copied',
|
copy_to_calendar: 'Copy to…', event_copied: 'Event copied',
|
||||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
|
||||||
edit_before_copy: 'Edit before copying',
|
edit_before_copy: 'Edit before copying',
|
||||||
event_updated: 'Event updated', event_created: 'Event created',
|
event_updated: 'Event updated', event_created: 'Event created',
|
||||||
confirm_delete_event: 'Really delete "{title}"?',
|
confirm_delete_event: 'Really delete "{title}"?',
|
||||||
@@ -458,26 +442,15 @@ export function t(key, vars = {}) {
|
|||||||
return val.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? '');
|
return val.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up a translation but return null if the key is undefined in both
|
|
||||||
// the current language and German. Lets callers fall back to the existing
|
|
||||||
// HTML default rather than displaying the raw key.
|
|
||||||
function tOrNull(key) {
|
|
||||||
const dict = translations[currentLang] ?? translations.de;
|
|
||||||
const val = dict[key] ?? translations.de[key];
|
|
||||||
return typeof val === 'string' ? val : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyLang() {
|
export function applyLang() {
|
||||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||||
const v = tOrNull(el.dataset.i18n);
|
const v = t(el.dataset.i18n);
|
||||||
if (v != null) el.textContent = v;
|
if (typeof v === 'string') el.textContent = v;
|
||||||
});
|
});
|
||||||
document.querySelectorAll('[data-i18n-ph]').forEach(el => {
|
document.querySelectorAll('[data-i18n-ph]').forEach(el => {
|
||||||
const v = tOrNull(el.dataset.i18nPh);
|
el.placeholder = t(el.dataset.i18nPh);
|
||||||
if (v != null) el.placeholder = v;
|
|
||||||
});
|
});
|
||||||
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||||
const v = tOrNull(el.dataset.i18nTitle);
|
el.title = t(el.dataset.i18nTitle);
|
||||||
if (v != null) el.title = v;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,47 +83,14 @@ export function applyTheme(settings) {
|
|||||||
root.style.setProperty('--accent', settings.accent_color || '#ea4335');
|
root.style.setProperty('--accent', settings.accent_color || '#ea4335');
|
||||||
root.style.setProperty('--today-color', settings.today_color || '#4285f4');
|
root.style.setProperty('--today-color', settings.today_color || '#4285f4');
|
||||||
|
|
||||||
// Text colour: a custom hex (settings.text_color) wins over the legacy
|
|
||||||
// 1–4 contrast step. We derive --text-2/--text-3 by darkening the
|
|
||||||
// chosen colour so the secondary/tertiary text stays in the same hue.
|
|
||||||
if (settings.text_color) {
|
|
||||||
root.style.setProperty('--text-1', settings.text_color);
|
|
||||||
root.style.setProperty('--text-2', shadeHex(settings.text_color, -0.25));
|
|
||||||
root.style.setProperty('--text-3', shadeHex(settings.text_color, -0.55));
|
|
||||||
} else {
|
|
||||||
const tc = TEXT_CONTRAST[settings.text_contrast || 3];
|
const tc = TEXT_CONTRAST[settings.text_contrast || 3];
|
||||||
root.style.setProperty('--text-1', tc.t1);
|
root.style.setProperty('--text-1', tc.t1);
|
||||||
root.style.setProperty('--text-2', tc.t2);
|
root.style.setProperty('--text-2', tc.t2);
|
||||||
root.style.setProperty('--text-3', tc.t3);
|
root.style.setProperty('--text-3', tc.t3);
|
||||||
}
|
|
||||||
|
|
||||||
// Line colour: custom hex overrides the legacy contrast step.
|
|
||||||
if (settings.line_color) {
|
|
||||||
root.style.setProperty('--border', settings.line_color);
|
|
||||||
root.style.setProperty('--border-light', shadeHex(settings.line_color, -0.25));
|
|
||||||
} else {
|
|
||||||
const lc = LINE_CONTRAST[settings.line_contrast || 3];
|
const lc = LINE_CONTRAST[settings.line_contrast || 3];
|
||||||
root.style.setProperty('--border', lc.border);
|
root.style.setProperty('--border', lc.border);
|
||||||
root.style.setProperty('--border-light', lc.light);
|
root.style.setProperty('--border-light', lc.light);
|
||||||
}
|
|
||||||
|
|
||||||
// Background colour: optional. If set, also tint the topbar/sidebar
|
|
||||||
// and surface variants so the whole UI stays coherent.
|
|
||||||
if (settings.bg_color) {
|
|
||||||
root.style.setProperty('--bg-app', settings.bg_color);
|
|
||||||
root.style.setProperty('--bg-topbar', shadeHex(settings.bg_color, 0.10));
|
|
||||||
root.style.setProperty('--bg-sidebar', shadeHex(settings.bg_color, 0.10));
|
|
||||||
root.style.setProperty('--bg-surface', shadeHex(settings.bg_color, 0.18));
|
|
||||||
root.style.setProperty('--bg-hover', shadeHex(settings.bg_color, 0.26));
|
|
||||||
root.style.setProperty('--bg-active', shadeHex(settings.bg_color, 0.40));
|
|
||||||
} else {
|
|
||||||
root.style.removeProperty('--bg-app');
|
|
||||||
root.style.removeProperty('--bg-topbar');
|
|
||||||
root.style.removeProperty('--bg-sidebar');
|
|
||||||
root.style.removeProperty('--bg-surface');
|
|
||||||
root.style.removeProperty('--bg-hover');
|
|
||||||
root.style.removeProperty('--bg-active');
|
|
||||||
}
|
|
||||||
|
|
||||||
const hh = settings.hour_height || 44;
|
const hh = settings.hour_height || 44;
|
||||||
root.style.setProperty('--hour-h', hh + 'px');
|
root.style.setProperty('--hour-h', hh + 'px');
|
||||||
@@ -138,24 +105,3 @@ function hexToRgba(hex, alpha) {
|
|||||||
const b = parseInt(hex.slice(5,7), 16);
|
const b = parseInt(hex.slice(5,7), 16);
|
||||||
return `rgba(${r},${g},${b},${alpha})`;
|
return `rgba(${r},${g},${b},${alpha})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Brighten (positive amount) or darken (negative) a hex colour.
|
|
||||||
// Used to derive supporting shades (sidebar bg, hover bg, secondary text…)
|
|
||||||
// from a single user-picked colour so the whole UI stays in the same family.
|
|
||||||
function shadeHex(hex, amount) {
|
|
||||||
let r = parseInt(hex.slice(1,3), 16);
|
|
||||||
let g = parseInt(hex.slice(3,5), 16);
|
|
||||||
let b = parseInt(hex.slice(5,7), 16);
|
|
||||||
if (amount >= 0) {
|
|
||||||
r = Math.round(r + (255 - r) * amount);
|
|
||||||
g = Math.round(g + (255 - g) * amount);
|
|
||||||
b = Math.round(b + (255 - b) * amount);
|
|
||||||
} else {
|
|
||||||
const a = 1 + amount; // amount is negative: e.g. -0.25 → keep 75%
|
|
||||||
r = Math.round(r * a);
|
|
||||||
g = Math.round(g * a);
|
|
||||||
b = Math.round(b * a);
|
|
||||||
}
|
|
||||||
const h = n => Math.max(0, Math.min(255, n)).toString(16).padStart(2, '0');
|
|
||||||
return '#' + h(r) + h(g) + h(b);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// Increment APP_VERSION with every code change
|
// Increment APP_VERSION with every code change
|
||||||
export const APP_VERSION = 'v18';
|
export const APP_VERSION = 'v11';
|
||||||
|
|||||||
@@ -63,24 +63,8 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
|||||||
const color = ev.color || ev.calendarColor || '#4285f4';
|
const color = ev.color || ev.calendarColor || '#4285f4';
|
||||||
const pastCls = isPast(ev) ? 'past' : '';
|
const pastCls = isPast(ev) ? 'past' : '';
|
||||||
const multiCls = isMultiTimed ? 'multiday-timed' : '';
|
const multiCls = isMultiTimed ? 'multiday-timed' : '';
|
||||||
<<<<<<< HEAD
|
|
||||||
// continues-left/right: compute on date-only basis for all-day events
|
|
||||||
let evStart = new Date(ev.start);
|
|
||||||
let evEnd = new Date(ev.end);
|
|
||||||
if (ev.allDay) {
|
|
||||||
evStart.setHours(0, 0, 0, 0);
|
|
||||||
evEnd.setHours(0, 0, 0, 0);
|
|
||||||
if (evEnd > evStart) evEnd.setDate(evEnd.getDate() - 1);
|
|
||||||
}
|
|
||||||
const firstDay = new Date(days[0]); firstDay.setHours(0, 0, 0, 0);
|
|
||||||
const lastDayMidnight = new Date(days[n-1]); lastDayMidnight.setHours(24, 0, 0, 0);
|
|
||||||
const lastDay = new Date(days[n-1]); lastDay.setHours(0, 0, 0, 0);
|
|
||||||
const cL = evStart < firstDay ? 'continues-left' : '';
|
|
||||||
const cR = (ev.allDay ? evEnd > lastDay : evEnd > lastDayMidnight) ? 'continues-right' : '';
|
|
||||||
=======
|
|
||||||
const cL = new Date(ev.start) < new Date(days[0]) ? 'continues-left' : '';
|
const cL = new Date(ev.start) < new Date(days[0]) ? 'continues-left' : '';
|
||||||
const cR = new Date(ev.end) > (() => { const d = new Date(days[n-1]); d.setHours(24,0,0,0); return d; })() ? 'continues-right' : '';
|
const cR = new Date(ev.end) > (() => { const d = new Date(days[n-1]); d.setHours(24,0,0,0); return d; })() ? 'continues-right' : '';
|
||||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
|
||||||
const label = isMultiTimed && isSameDay(new Date(ev.start), days[colStart])
|
const label = isMultiTimed && isSameDay(new Date(ev.start), days[colStart])
|
||||||
? `${fmtTime(new Date(ev.start))} ${ev.title}`
|
? `${fmtTime(new Date(ev.start))} ${ev.title}`
|
||||||
: ev.title;
|
: ev.title;
|
||||||
@@ -252,36 +236,11 @@ function renderNowLine(container, days, hourH = 60) {
|
|||||||
function layoutWeekAllDay(evs, days) {
|
function layoutWeekAllDay(evs, days) {
|
||||||
const items = [];
|
const items = [];
|
||||||
evs.forEach(ev => {
|
evs.forEach(ev => {
|
||||||
<<<<<<< HEAD
|
|
||||||
// For all-day events, normalize to date-only with inclusive end-day
|
|
||||||
// (iCal stores exclusive end → subtract 1). For timed events, keep
|
|
||||||
// the original strict-overlap logic so events ending exactly at
|
|
||||||
// midnight don't bleed into the next day.
|
|
||||||
let ns, ne;
|
|
||||||
if (ev.allDay) {
|
|
||||||
ns = new Date(ev.start); ns.setHours(0, 0, 0, 0);
|
|
||||||
ne = new Date(ev.end); ne.setHours(0, 0, 0, 0);
|
|
||||||
if (ne > ns) ne.setDate(ne.getDate() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let colStart = -1, colEnd = -1;
|
|
||||||
days.forEach((day, i) => {
|
|
||||||
const ds = new Date(day); ds.setHours(0, 0, 0, 0);
|
|
||||||
let matches;
|
|
||||||
if (ev.allDay) {
|
|
||||||
matches = ds >= ns && ds <= ne;
|
|
||||||
} else {
|
|
||||||
const de = new Date(day); de.setHours(24, 0, 0, 0);
|
|
||||||
matches = new Date(ev.start) < de && new Date(ev.end) > ds;
|
|
||||||
}
|
|
||||||
if (matches) {
|
|
||||||
=======
|
|
||||||
let colStart = -1, colEnd = -1;
|
let colStart = -1, colEnd = -1;
|
||||||
days.forEach((day, i) => {
|
days.forEach((day, i) => {
|
||||||
const ds = new Date(day); ds.setHours(0, 0, 0, 0);
|
const ds = new Date(day); ds.setHours(0, 0, 0, 0);
|
||||||
const de = new Date(day); de.setHours(24, 0, 0, 0);
|
const de = new Date(day); de.setHours(24, 0, 0, 0);
|
||||||
if (new Date(ev.start) < de && new Date(ev.end) > ds) {
|
if (new Date(ev.start) < de && new Date(ev.end) > ds) {
|
||||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
|
||||||
if (colStart === -1) colStart = i;
|
if (colStart === -1) colStart = i;
|
||||||
colEnd = i;
|
colEnd = i;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,39 @@
|
|||||||
// Calendarr Service Worker — minimal-cache strategy
|
// Calendarr Service Worker
|
||||||
//
|
// Cache-first for static assets, network-first for /api/* (graceful offline)
|
||||||
// Strategy: network-first for everything. The cache is only used as a
|
|
||||||
// last-resort fallback when offline (so the app shell still opens). This
|
|
||||||
// means every online request hits the network and respects the
|
|
||||||
// server's Cache-Control headers (≤ 2h for static assets, no-cache for
|
|
||||||
// the entry HTML / version files). New releases take effect on the next
|
|
||||||
// reload, no manual SW unregister required.
|
|
||||||
|
|
||||||
const CACHE_VERSION = 'calendarr-v18';
|
const CACHE_VERSION = 'calendarr-v11';
|
||||||
const OFFLINE_SHELL = ['/', '/index.html'];
|
const STATIC_ASSETS = [
|
||||||
|
'/',
|
||||||
|
'/index.html',
|
||||||
|
'/manifest.json',
|
||||||
|
'/static/css/app.css',
|
||||||
|
'/static/favicon.svg',
|
||||||
|
'/static/js/app.js',
|
||||||
|
'/static/js/api.js',
|
||||||
|
'/static/js/calendar.js',
|
||||||
|
'/static/js/color-picker.js',
|
||||||
|
'/static/js/date-picker.js',
|
||||||
|
'/static/js/i18n.js',
|
||||||
|
'/static/js/utils.js',
|
||||||
|
'/static/js/version.js',
|
||||||
|
'/static/js/views/agenda.js',
|
||||||
|
'/static/js/views/month.js',
|
||||||
|
'/static/js/views/quarter.js',
|
||||||
|
'/static/js/views/week.js',
|
||||||
|
'/icons/icon-192.png',
|
||||||
|
'/icons/icon-512.png',
|
||||||
|
'/icons/icon.svg',
|
||||||
|
];
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open(CACHE_VERSION).then(cache =>
|
caches.open(CACHE_VERSION).then(cache =>
|
||||||
Promise.all(OFFLINE_SHELL.map(url =>
|
// Use addAll with a fallback so a single missing file doesn't abort install
|
||||||
|
Promise.all(
|
||||||
|
STATIC_ASSETS.map(url =>
|
||||||
cache.add(url).catch(err => console.warn('[SW] skip', url, err))
|
cache.add(url).catch(err => console.warn('[SW] skip', url, err))
|
||||||
))
|
)
|
||||||
|
)
|
||||||
).then(() => self.skipWaiting())
|
).then(() => self.skipWaiting())
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -34,8 +52,7 @@ self.addEventListener('fetch', event => {
|
|||||||
|
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
|
|
||||||
// API routes: always go to the network, no offline fallback (we'd just
|
// Network-first for API routes — fail silently if offline
|
||||||
// be returning stale account/event data otherwise).
|
|
||||||
if (url.pathname.startsWith('/api/')) {
|
if (url.pathname.startsWith('/api/')) {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
fetch(req).catch(() =>
|
fetch(req).catch(() =>
|
||||||
@@ -48,29 +65,45 @@ self.addEventListener('fetch', event => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything else: network-first. The browser's HTTP cache (driven by
|
// Network-first for navigation (HTML) and the version-defining files —
|
||||||
// the server's Cache-Control headers) already throttles re-fetches —
|
// ensures users always get the freshest entry point so new releases
|
||||||
// the SW just makes sure offline still works for the entry HTML.
|
// take effect on the next reload without a manual SW unregister.
|
||||||
event.respondWith(
|
const isHtml = req.mode === 'navigate'
|
||||||
fetch(req).then(resp => {
|
|
||||||
// Keep a fresh copy of navigation requests / index.html for offline
|
|
||||||
const isNavigation = req.mode === 'navigate'
|
|
||||||
|| url.pathname === '/'
|
|| url.pathname === '/'
|
||||||
|| url.pathname === '/index.html';
|
|| url.pathname === '/index.html';
|
||||||
if (isNavigation && resp && resp.status === 200) {
|
const isVersionFile = url.pathname === '/static/js/version.js';
|
||||||
|
|
||||||
|
if (isHtml || isVersionFile) {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(req).then(resp => {
|
||||||
|
if (resp && resp.status === 200) {
|
||||||
|
const clone = resp.clone();
|
||||||
|
caches.open(CACHE_VERSION).then(c => c.put(req, clone)).catch(() => {});
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}).catch(() =>
|
||||||
|
caches.match(req).then(c => c || caches.match('/index.html'))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache-first for everything else (static)
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(req).then(cached => {
|
||||||
|
if (cached) return cached;
|
||||||
|
return fetch(req).then(resp => {
|
||||||
|
// Only cache successful, basic-origin responses
|
||||||
|
if (resp && resp.status === 200 && resp.type === 'basic') {
|
||||||
const clone = resp.clone();
|
const clone = resp.clone();
|
||||||
caches.open(CACHE_VERSION).then(c => c.put(req, clone)).catch(() => {});
|
caches.open(CACHE_VERSION).then(c => c.put(req, clone)).catch(() => {});
|
||||||
}
|
}
|
||||||
return resp;
|
return resp;
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// Offline fallback: only the HTML shell is served from cache, so the
|
// Offline fallback for navigation requests
|
||||||
// app at least renders and can show its own offline UI.
|
if (req.mode === 'navigate') return caches.match('/index.html');
|
||||||
if (req.mode === 'navigate'
|
|
||||||
|| url.pathname === '/'
|
|
||||||
|| url.pathname === '/index.html') {
|
|
||||||
return caches.match(req).then(c => c || caches.match('/index.html'));
|
|
||||||
}
|
|
||||||
return new Response('', { status: 503 });
|
return new Response('', { status: 503 });
|
||||||
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user