feat(settings): Schriftfarbe, Linienfarbe und Hintergrundfarbe per Color-Picker
Die bisherigen Stufen-Wähler ("Dunkel/Mittel/Hell/Maximum" und
"Kaum/Subtil/Normal/Stark") für Schrift- bzw. Linienkontrast sind durch
echte Hex-Color-Picker ersetzt. Zusätzlich kann jetzt auch die
Hintergrundfarbe der Seite frei gewählt werden.
Wenn ein Override gesetzt ist:
- text_color → setzt --text-1 direkt, --text-2/--text-3 werden
daraus per shadeHex(-0.25 / -0.55) abgeleitet, damit der Hue passt
- line_color → setzt --border, --border-light wird leicht abgedunkelt
- bg_color → setzt --bg-app, daraus werden Topbar/Sidebar/Surface/
Hover/Active per shadeHex(+0.10…+0.40) konsistent hochskaliert
Per "Reset"-Knopf wird der Override geleert und die alte Stufen-Logik
(falls noch vorhanden) bzw. der Default-Theme greift wieder.
Backend:
- 3 neue nullable VARCHAR(7)-Spalten in user_settings (text_color,
line_color, bg_color) inkl. Migrationen in main.py
- settings_router nutzt model_dump(exclude_unset=True) und respektiert
explizite null-Werte nur für diese 3 Override-Felder, damit Reset
funktioniert
Auch enthalten: Auflösen der Merge-Konflikte in sw.js, index.html,
version.js (HEAD-Stand v17 behalten) und Bump auf v18.
This commit is contained in:
@@ -114,6 +114,24 @@ def _migrate():
|
||||
except Exception:
|
||||
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()
|
||||
|
||||
app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None)
|
||||
|
||||
@@ -84,6 +84,9 @@ class UserSettings(Base):
|
||||
language = Column(String(5), default="de")
|
||||
month_divider_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")
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ class SettingsUpdate(BaseModel):
|
||||
language: Optional[str] = None
|
||||
month_divider_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:
|
||||
@@ -40,6 +43,9 @@ def _settings_dict(s: models.UserSettings) -> dict:
|
||||
"language": s.language or "de",
|
||||
"month_divider_color": s.month_divider_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,
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +82,16 @@ def update_settings(
|
||||
settings = models.UserSettings(user_id=current_user.id)
|
||||
db.add(settings)
|
||||
|
||||
for field, value in data.model_dump(exclude_none=True).items():
|
||||
setattr(settings, field, value)
|
||||
# For these three override colours, an explicit null is meaningful
|
||||
# ("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)
|
||||
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<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" />
|
||||
<!-- APP_VERSION: update here + version.js on every release -->
|
||||
<<<<<<< HEAD
|
||||
<title>Calendarr v17</title>
|
||||
=======
|
||||
<title>Calendarr v11</title>
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
<title>Calendarr v18</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="#4285f4" />
|
||||
@@ -84,11 +80,7 @@
|
||||
<button type="submit" class="btn btn-primary btn-full">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
<<<<<<< HEAD
|
||||
<button class="impressum-link" onclick="openImpressum()">© 2026 Scarriffleservices · v17</button>
|
||||
=======
|
||||
<button class="impressum-link" onclick="openImpressum()">© 2026 Scarriffleservices · v11</button>
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
<button class="impressum-link" onclick="openImpressum()">© 2026 Scarriffleservices · v18</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── MAIN APP ──────────────────────────────────────────── -->
|
||||
@@ -207,11 +199,7 @@
|
||||
<div id="cal-list-items"></div>
|
||||
</div>
|
||||
</div>
|
||||
<<<<<<< HEAD
|
||||
<button class="sidebar-copyright" onclick="openImpressum()">© 2026 Scarriffleservices · v17</button>
|
||||
=======
|
||||
<button class="sidebar-copyright" onclick="openImpressum()">© 2026 Scarriffleservices · v11</button>
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
<button class="sidebar-copyright" onclick="openImpressum()">© 2026 Scarriffleservices · v18</button>
|
||||
</aside>
|
||||
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
|
||||
|
||||
@@ -247,11 +235,7 @@
|
||||
<input type="hidden" id="ev-start" />
|
||||
<div class="dt-display" id="ev-start-display" tabindex="0" role="button">
|
||||
<span class="dt-display-text">—</span>
|
||||
<<<<<<< HEAD
|
||||
<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>
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group half">
|
||||
@@ -259,11 +243,7 @@
|
||||
<input type="hidden" id="ev-end" />
|
||||
<div class="dt-display" id="ev-end-display" tabindex="0" role="button">
|
||||
<span class="dt-display-text">—</span>
|
||||
<<<<<<< HEAD
|
||||
<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>
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -273,11 +253,7 @@
|
||||
<input type="hidden" id="ev-start-date" />
|
||||
<div class="dt-display" id="ev-start-date-display" tabindex="0" role="button">
|
||||
<span class="dt-display-text">—</span>
|
||||
<<<<<<< HEAD
|
||||
<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>
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group half">
|
||||
@@ -285,11 +261,7 @@
|
||||
<input type="hidden" id="ev-end-date" />
|
||||
<div class="dt-display" id="ev-end-date-display" tabindex="0" role="button">
|
||||
<span class="dt-display-text">—</span>
|
||||
<<<<<<< HEAD
|
||||
<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>
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -339,11 +311,7 @@
|
||||
<input type="hidden" id="ev-rec-until" />
|
||||
<div class="dt-display" id="ev-rec-until-display" tabindex="0" role="button">
|
||||
<span class="dt-display-text">—</span>
|
||||
<<<<<<< HEAD
|
||||
<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>
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -407,7 +375,6 @@
|
||||
<div class="popup-header">
|
||||
<div class="popup-color-dot" id="popup-color-dot"></div>
|
||||
<h4 id="popup-title"></h4>
|
||||
<<<<<<< HEAD
|
||||
<div class="popup-toolbar">
|
||||
<button class="popup-icon-btn" id="popup-edit" title="Bearbeiten" aria-label="Bearbeiten">
|
||||
<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>
|
||||
@@ -422,18 +389,6 @@
|
||||
<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>
|
||||
=======
|
||||
<button class="icon-btn popup-action" id="popup-edit" title="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>
|
||||
</button>
|
||||
<button class="icon-btn popup-action" id="popup-copy" title="Kopieren nach…">
|
||||
<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 class="icon-btn popup-action" id="popup-delete" title="Löschen">
|
||||
<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 class="icon-btn popup-close" id="popup-close">×</button>
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-time" id="popup-time"></div>
|
||||
@@ -667,22 +622,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_text_contrast">Schriftkontrast</h4>
|
||||
<p class="panel-desc" data-i18n="settings_text_contrast_desc">Helligkeit der Beschriftungen und Texte</p>
|
||||
<div class="contrast-selector" id="cfg-text-contrast" data-setting="text_contrast">
|
||||
<button class="contrast-btn" data-val="1"><span style="color:#606070">Aa</span><span class="contrast-lbl" data-i18n="contrast_dark">Dunkel</span></button>
|
||||
<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 class="contrast-btn" data-val="3"><span style="color:#c8c8d8">Aa</span><span class="contrast-lbl" data-i18n="contrast_light">Hell</span></button>
|
||||
<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 class="form-group">
|
||||
<label data-i18n="settings_text_color">Schriftfarbe</label>
|
||||
<div class="ev-color-row">
|
||||
<input type="text" id="cfg-text-color-hex" class="ev-color-hex" maxlength="7" spellcheck="false" placeholder="auto" />
|
||||
<div class="ev-color-preview" id="cfg-text-color-preview" data-i18n-title="color_pick" title="Farbe wählen"></div>
|
||||
<button type="button" class="btn btn-ghost btn-sm" id="cfg-text-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 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_calendar_view">Kalenderansicht</h4>
|
||||
@@ -933,11 +895,7 @@
|
||||
<a href="mailto:scarriffleservices@gmail.com">scarriffleservices@gmail.com</a></p>
|
||||
</div>
|
||||
<div class="modal-footer" style="justify-content:space-between;align-items:center">
|
||||
<<<<<<< HEAD
|
||||
<span style="font-size:12px;color:var(--text-3)">Calendarr v17</span>
|
||||
=======
|
||||
<span style="font-size:12px;color:var(--text-3)">Calendarr v11</span>
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
<span style="font-size:12px;color:var(--text-3)">Calendarr v18</span>
|
||||
<button class="btn btn-ghost" onclick="closeImpressum()">Schliessen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2045,6 +2045,24 @@ function openSettingsModal() {
|
||||
document.getElementById(id + '-hex').value = val.toUpperCase();
|
||||
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-language').value = getLang();
|
||||
|
||||
@@ -2376,6 +2394,32 @@ 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
|
||||
document.querySelectorAll('.settings-nav-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => activateSettingsPanel(btn.dataset.panel));
|
||||
@@ -2415,6 +2459,11 @@ function bindSettingsModal() {
|
||||
const btn = document.querySelector(`#${id} .contrast-btn.active`);
|
||||
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 = {
|
||||
default_view: document.getElementById('cfg-default-view').value,
|
||||
week_start_day: document.getElementById('cfg-week-start').value,
|
||||
@@ -2423,9 +2472,10 @@ function bindSettingsModal() {
|
||||
today_color: document.getElementById('cfg-today-hex').value,
|
||||
month_divider_color: document.getElementById('cfg-month-divider-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,
|
||||
text_contrast: getActive('cfg-text-contrast') || 3,
|
||||
line_contrast: getActive('cfg-line-contrast') || 3,
|
||||
hour_height: getActive('cfg-hour-height') || 44,
|
||||
language: document.getElementById('cfg-language').value,
|
||||
};
|
||||
|
||||
@@ -67,6 +67,10 @@ const translations = {
|
||||
settings_today_color: 'Heutige-Tag-Farbe',
|
||||
settings_month_divider_color: 'Monatswechsel-Linie',
|
||||
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_desc: 'Helligkeit der Beschriftungen und Texte',
|
||||
contrast_dark: 'Dunkel', contrast_medium: 'Mittel',
|
||||
@@ -282,6 +286,10 @@ const translations = {
|
||||
settings_today_color: 'Today highlight color',
|
||||
settings_month_divider_color: 'Month divider line',
|
||||
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_desc: 'Brightness of labels and text',
|
||||
contrast_dark: 'Dark', contrast_medium: 'Medium',
|
||||
|
||||
@@ -83,14 +83,47 @@ export function applyTheme(settings) {
|
||||
root.style.setProperty('--accent', settings.accent_color || '#ea4335');
|
||||
root.style.setProperty('--today-color', settings.today_color || '#4285f4');
|
||||
|
||||
const tc = TEXT_CONTRAST[settings.text_contrast || 3];
|
||||
root.style.setProperty('--text-1', tc.t1);
|
||||
root.style.setProperty('--text-2', tc.t2);
|
||||
root.style.setProperty('--text-3', tc.t3);
|
||||
// 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];
|
||||
root.style.setProperty('--text-1', tc.t1);
|
||||
root.style.setProperty('--text-2', tc.t2);
|
||||
root.style.setProperty('--text-3', tc.t3);
|
||||
}
|
||||
|
||||
const lc = LINE_CONTRAST[settings.line_contrast || 3];
|
||||
root.style.setProperty('--border', lc.border);
|
||||
root.style.setProperty('--border-light', lc.light);
|
||||
// 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];
|
||||
root.style.setProperty('--border', lc.border);
|
||||
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;
|
||||
root.style.setProperty('--hour-h', hh + 'px');
|
||||
@@ -105,3 +138,24 @@ function hexToRgba(hex, alpha) {
|
||||
const b = parseInt(hex.slice(5,7), 16);
|
||||
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,6 +1,2 @@
|
||||
// Increment APP_VERSION with every code change
|
||||
<<<<<<< HEAD
|
||||
export const APP_VERSION = 'v17';
|
||||
=======
|
||||
export const APP_VERSION = 'v11';
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
export const APP_VERSION = 'v18';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
// Calendarr Service Worker — minimal-cache strategy
|
||||
//
|
||||
// Strategy: network-first for everything. The cache is only used as a
|
||||
@@ -8,52 +7,15 @@
|
||||
// the entry HTML / version files). New releases take effect on the next
|
||||
// reload, no manual SW unregister required.
|
||||
|
||||
const CACHE_VERSION = 'calendarr-v17';
|
||||
const CACHE_VERSION = 'calendarr-v18';
|
||||
const OFFLINE_SHELL = ['/', '/index.html'];
|
||||
=======
|
||||
// Calendarr Service Worker
|
||||
// Cache-first for static assets, network-first for /api/* (graceful offline)
|
||||
|
||||
const CACHE_VERSION = 'calendarr-v11';
|
||||
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',
|
||||
];
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_VERSION).then(cache =>
|
||||
<<<<<<< HEAD
|
||||
Promise.all(OFFLINE_SHELL.map(url =>
|
||||
cache.add(url).catch(err => console.warn('[SW] skip', url, err))
|
||||
))
|
||||
=======
|
||||
// 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))
|
||||
)
|
||||
)
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
).then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
@@ -72,12 +34,8 @@ self.addEventListener('fetch', event => {
|
||||
|
||||
const url = new URL(req.url);
|
||||
|
||||
<<<<<<< HEAD
|
||||
// API routes: always go to the network, no offline fallback (we'd just
|
||||
// be returning stale account/event data otherwise).
|
||||
=======
|
||||
// Network-first for API routes — fail silently if offline
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(
|
||||
fetch(req).catch(() =>
|
||||
@@ -90,7 +48,6 @@ self.addEventListener('fetch', event => {
|
||||
return;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
// Everything else: network-first. The browser's HTTP cache (driven by
|
||||
// the server's Cache-Control headers) already throttles re-fetches —
|
||||
// the SW just makes sure offline still works for the entry HTML.
|
||||
@@ -114,47 +71,6 @@ self.addEventListener('fetch', event => {
|
||||
return caches.match(req).then(c => c || caches.match('/index.html'));
|
||||
}
|
||||
return new Response('', { status: 503 });
|
||||
=======
|
||||
// Network-first for navigation (HTML) and the version-defining files —
|
||||
// ensures users always get the freshest entry point so new releases
|
||||
// take effect on the next reload without a manual SW unregister.
|
||||
const isHtml = req.mode === 'navigate'
|
||||
|| url.pathname === '/'
|
||||
|| url.pathname === '/index.html';
|
||||
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();
|
||||
caches.open(CACHE_VERSION).then(c => c.put(req, clone)).catch(() => {});
|
||||
}
|
||||
return resp;
|
||||
}).catch(() => {
|
||||
// Offline fallback for navigation requests
|
||||
if (req.mode === 'navigate') return caches.match('/index.html');
|
||||
return new Response('', { status: 503 });
|
||||
});
|
||||
>>>>>>> e744b1829e99db6b80922f75542ced329138e474
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user