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.
162 lines
6.2 KiB
JavaScript
162 lines
6.2 KiB
JavaScript
export function isToday(d) {
|
||
const now = new Date();
|
||
return d.getFullYear() === now.getFullYear() &&
|
||
d.getMonth() === now.getMonth() &&
|
||
d.getDate() === now.getDate();
|
||
}
|
||
|
||
export function isSameDay(a, b) {
|
||
return a.getFullYear() === b.getFullYear() &&
|
||
a.getMonth() === b.getMonth() &&
|
||
a.getDate() === b.getDate();
|
||
}
|
||
|
||
export function isPast(ev) {
|
||
const end = new Date(ev.end);
|
||
return end < new Date();
|
||
}
|
||
|
||
export function formatDate(d, opts = {}) {
|
||
return d.toLocaleDateString('de', opts);
|
||
}
|
||
|
||
export function dateKey(d) {
|
||
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
||
}
|
||
|
||
export function toLocalDatetimeInput(d) {
|
||
const Y = d.getFullYear();
|
||
const M = String(d.getMonth()+1).padStart(2,'0');
|
||
const D = String(d.getDate()).padStart(2,'0');
|
||
const h = String(d.getHours()).padStart(2,'0');
|
||
const m = String(d.getMinutes()).padStart(2,'0');
|
||
return `${Y}-${M}-${D}T${h}:${m}`;
|
||
}
|
||
|
||
export function toDateInput(d) {
|
||
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
||
}
|
||
|
||
// Monday-first: returns 0=Mo, 1=Di, ..., 6=So
|
||
export function dayOfWeek(d, weekStartDay = 'monday') {
|
||
if (weekStartDay === 'sunday') {
|
||
return d.getDay(); // 0=So, 1=Mo, ..., 6=Sa
|
||
}
|
||
return (d.getDay() + 6) % 7; // 0=Mo, 1=Di, ..., 6=So
|
||
}
|
||
|
||
// Returns the start-of-week date for d
|
||
export function weekStart(d, weekStartDay = 'monday') {
|
||
const m = new Date(d);
|
||
m.setDate(m.getDate() - dayOfWeek(m, weekStartDay));
|
||
m.setHours(0, 0, 0, 0);
|
||
return m;
|
||
}
|
||
|
||
// Returns the ISO week number (Monday-based, ISO 8601)
|
||
export function getISOWeekNumber(d) {
|
||
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
||
// ISO week: weeks start on Monday, week 1 contains the first Thursday
|
||
const day = date.getUTCDay() || 7; // make Sunday = 7
|
||
date.setUTCDate(date.getUTCDate() + 4 - day);
|
||
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
|
||
return Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
|
||
}
|
||
|
||
const TEXT_CONTRAST = {
|
||
1: { t1: '#606070', t2: '#484858', t3: '#303040' },
|
||
2: { t1: '#9090a8', t2: '#6a6a80', t3: '#484860' },
|
||
3: { t1: '#c8c8d8', t2: '#9090aa', t3: '#55556a' },
|
||
4: { t1: '#ffffff', t2: '#c0c0d8', t3: '#8888a0' },
|
||
};
|
||
const LINE_CONTRAST = {
|
||
1: { border: '#1e1e2c', light: '#181826' },
|
||
2: { border: '#2a2a3c', light: '#222230' },
|
||
3: { border: '#3a3a52', light: '#2e2e40' },
|
||
4: { border: '#5a5a78', light: '#484860' },
|
||
};
|
||
|
||
export function applyTheme(settings) {
|
||
const root = document.documentElement;
|
||
root.style.setProperty('--primary', settings.primary_color || '#4285f4');
|
||
root.style.setProperty('--primary-dim', hexToRgba(settings.primary_color || '#4285f4', 0.15));
|
||
root.style.setProperty('--accent', settings.accent_color || '#ea4335');
|
||
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];
|
||
root.style.setProperty('--text-1', tc.t1);
|
||
root.style.setProperty('--text-2', tc.t2);
|
||
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];
|
||
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');
|
||
|
||
root.style.setProperty('--month-divider-color', settings.month_divider_color || '#7090c0');
|
||
root.style.setProperty('--month-label-color', settings.month_label_color || '#7090c0');
|
||
}
|
||
|
||
function hexToRgba(hex, alpha) {
|
||
const r = parseInt(hex.slice(1,3), 16);
|
||
const g = parseInt(hex.slice(3,5), 16);
|
||
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);
|
||
}
|