Einstellungen: Vollbild-Seite, Kontrast, Stundenhöhe, KW-Anzeige
- Einstellungen von Modal-Popup auf Vollbild-Seite mit Seitennavigation umgestellt - Schriftkontrast (4 Stufen) und Linienkontrast (4 Stufen) pro Benutzer gespeichert - Stundenhöhe (40/60/80/100px) in Wochen-/Tagesansicht per Einstellung steuerbar - Kalenderwoche in Monats- und Wochenansicht grösser dargestellt - CSS-Variable --hour-h für dynamische Zeitraster-Höhe in week.js und app.css - Backend: neue Felder text_contrast, line_contrast, hour_height in UserSettings
This commit is contained in:
@@ -43,6 +43,21 @@ def _migrate():
|
||||
logging.info("Migration: added sidebar_hidden to google_calendars")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
conn.execute(text("ALTER TABLE user_settings ADD COLUMN text_contrast INTEGER DEFAULT 3"))
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
conn.execute(text("ALTER TABLE user_settings ADD COLUMN line_contrast INTEGER DEFAULT 3"))
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
conn.execute(text("ALTER TABLE user_settings ADD COLUMN hour_height INTEGER DEFAULT 60"))
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_migrate()
|
||||
|
||||
|
||||
@@ -75,6 +75,9 @@ class UserSettings(Base):
|
||||
accent_color = Column(String(7), default="#ea4335")
|
||||
today_color = Column(String(7), default="#4285f4")
|
||||
dim_past_events = Column(Boolean, default=False)
|
||||
text_contrast = Column(Integer, default=3)
|
||||
line_contrast = Column(Integer, default=3)
|
||||
hour_height = Column(Integer, default=60)
|
||||
|
||||
user = relationship("User", back_populates="settings")
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ class SettingsUpdate(BaseModel):
|
||||
accent_color: Optional[str] = None
|
||||
today_color: Optional[str] = None
|
||||
dim_past_events: Optional[bool] = None
|
||||
text_contrast: Optional[int] = None
|
||||
line_contrast: Optional[int] = None
|
||||
hour_height: Optional[int] = None
|
||||
|
||||
|
||||
def _settings_dict(s: models.UserSettings) -> dict:
|
||||
@@ -28,6 +31,9 @@ def _settings_dict(s: models.UserSettings) -> dict:
|
||||
"accent_color": s.accent_color,
|
||||
"today_color": s.today_color,
|
||||
"dim_past_events": s.dim_past_events,
|
||||
"text_contrast": s.text_contrast or 3,
|
||||
"line_contrast": s.line_contrast or 3,
|
||||
"hour_height": s.hour_height or 60,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -440,12 +440,12 @@ a { color: var(--primary); text-decoration: none; }
|
||||
/* ── Month View ─────────────────────────────────────────── */
|
||||
.month-view { display: flex; flex-direction: column; height: 100%; }
|
||||
.month-header {
|
||||
display: grid; grid-template-columns: 32px repeat(7, 1fr);
|
||||
display: grid; grid-template-columns: 38px repeat(7, 1fr);
|
||||
border-bottom: 1px solid var(--border); flex-shrink: 0;
|
||||
}
|
||||
.month-kw-header {
|
||||
padding: 8px 0; text-align: center;
|
||||
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
||||
font-size: 13px; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: .3px; color: var(--text-3);
|
||||
border-right: 1px solid var(--border-light);
|
||||
}
|
||||
@@ -455,7 +455,7 @@ a { color: var(--primary); text-decoration: none; }
|
||||
letter-spacing: .5px; color: var(--text-2);
|
||||
}
|
||||
.month-grid {
|
||||
display: grid; grid-template-columns: 32px repeat(7, 1fr);
|
||||
display: grid; grid-template-columns: 38px repeat(7, 1fr);
|
||||
grid-template-rows: repeat(6, 1fr);
|
||||
flex: 1; overflow: hidden;
|
||||
}
|
||||
@@ -464,7 +464,7 @@ a { color: var(--primary); text-decoration: none; }
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: flex-start; justify-content: center;
|
||||
padding-top: 6px;
|
||||
font-size: 10px; color: var(--text-3); font-weight: 500;
|
||||
font-size: 13px; color: var(--text-3); font-weight: 700;
|
||||
cursor: default; user-select: none;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -516,7 +516,7 @@ a { color: var(--primary); text-decoration: none; }
|
||||
}
|
||||
/* KW badge in week view header gutter */
|
||||
.week-kw-badge {
|
||||
font-size: 10px; font-weight: 600; color: var(--text-3);
|
||||
font-size: 14px; font-weight: 700; color: var(--text-3);
|
||||
display: flex; align-items: flex-end; justify-content: center;
|
||||
padding-bottom: 6px;
|
||||
text-transform: uppercase; letter-spacing: .3px;
|
||||
@@ -563,7 +563,7 @@ a { color: var(--primary); text-decoration: none; }
|
||||
.week-body { display: flex; flex: 1; overflow-y: auto; position: relative; }
|
||||
.week-time-col { width: 56px; flex-shrink: 0; position: relative; }
|
||||
.time-label {
|
||||
height: 60px; display: flex; align-items: flex-start; justify-content: flex-end;
|
||||
height: var(--hour-h, 60px); display: flex; align-items: flex-start; justify-content: flex-end;
|
||||
padding-right: 8px; padding-top: 6px;
|
||||
font-size: 10px; color: var(--text-3);
|
||||
}
|
||||
@@ -571,18 +571,18 @@ a { color: var(--primary); text-decoration: none; }
|
||||
.week-day-col {
|
||||
flex: 1; border-left: 1px solid var(--border);
|
||||
position: relative;
|
||||
min-height: calc(60px * 24);
|
||||
min-height: calc(var(--hour-h, 60px) * 24);
|
||||
}
|
||||
.hour-line {
|
||||
position: absolute; left: 0; right: 0;
|
||||
border-top: 1px solid var(--border-light);
|
||||
height: 60px;
|
||||
height: var(--hour-h, 60px);
|
||||
}
|
||||
.hour-line:first-child { border-top: none; }
|
||||
.half-line {
|
||||
position: absolute; left: 0; right: 0;
|
||||
border-top: 1px dashed var(--border-light);
|
||||
top: 30px;
|
||||
top: calc(var(--hour-h, 60px) / 2);
|
||||
}
|
||||
|
||||
/* Current time indicator */
|
||||
@@ -699,7 +699,78 @@ a { color: var(--primary); text-decoration: none; }
|
||||
.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; }
|
||||
|
||||
/* ── Settings ───────────────────────────────────────────── */
|
||||
/* ── Settings Page ──────────────────────────────────────── */
|
||||
#modal-settings.modal-overlay {
|
||||
align-items: stretch; justify-content: stretch; padding: 0; background: var(--bg-app);
|
||||
}
|
||||
.settings-page-card {
|
||||
width: 100%; height: 100%;
|
||||
display: flex; flex-direction: column;
|
||||
background: var(--bg-app); overflow: hidden;
|
||||
}
|
||||
.settings-page-header {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 14px 20px; border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.settings-page-header h3 { font-size: 16px; font-weight: 600; color: var(--text-1); margin: 0; }
|
||||
.settings-page-body {
|
||||
display: flex; flex: 1; overflow: hidden;
|
||||
}
|
||||
.settings-nav {
|
||||
width: 200px; flex-shrink: 0;
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 12px 8px;
|
||||
display: flex; flex-direction: column; gap: 2px;
|
||||
}
|
||||
.settings-nav-btn {
|
||||
display: block; width: 100%; text-align: left;
|
||||
padding: 9px 14px; border-radius: var(--radius-sm);
|
||||
font-size: 14px; color: var(--text-2);
|
||||
background: none; border: none; cursor: pointer;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
.settings-nav-btn:hover { background: var(--bg-hover); color: var(--text-1); }
|
||||
.settings-nav-btn.active { background: var(--primary-dim); color: var(--primary); font-weight: 600; }
|
||||
.settings-panels {
|
||||
flex: 1; overflow-y: auto; padding: 24px 28px;
|
||||
}
|
||||
.settings-panel { display: none; }
|
||||
.settings-panel.active { display: block; }
|
||||
|
||||
/* Panel typography */
|
||||
.panel-title {
|
||||
font-size: 14px; font-weight: 600; color: var(--text-1); margin: 0 0 4px;
|
||||
}
|
||||
.panel-desc {
|
||||
font-size: 12px; color: var(--text-3); margin: 0 0 12px;
|
||||
}
|
||||
|
||||
/* Contrast / option selectors */
|
||||
.contrast-selector {
|
||||
display: flex; gap: 8px; flex-wrap: wrap;
|
||||
}
|
||||
.contrast-btn {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||||
padding: 10px 16px; border-radius: var(--radius);
|
||||
border: 1px solid var(--border); background: var(--bg-surface);
|
||||
cursor: pointer; transition: border-color .15s, background .15s;
|
||||
min-width: 70px;
|
||||
}
|
||||
.contrast-btn:hover { border-color: var(--primary); }
|
||||
.contrast-btn.active { border-color: var(--primary); background: var(--primary-dim); }
|
||||
.contrast-btn span { font-size: 18px; font-weight: 700; line-height: 1; }
|
||||
.contrast-lbl { font-size: 11px; color: var(--text-2); white-space: nowrap; }
|
||||
.line-preview {
|
||||
display: block; width: 36px; height: 0;
|
||||
border-top: 2px solid; border-radius: 1px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.hour-preview {
|
||||
font-size: 14px; line-height: 1; color: var(--text-2);
|
||||
}
|
||||
|
||||
/* ── Settings (legacy) ──────────────────────────────────── */
|
||||
.settings-section { margin-bottom: 28px; }
|
||||
.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 {
|
||||
|
||||
@@ -378,96 +378,143 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<!-- Settings Page -->
|
||||
<div id="modal-settings" class="modal-overlay hidden">
|
||||
<div class="modal-card" style="max-width:520px">
|
||||
<div class="modal-header">
|
||||
<div class="settings-page-card">
|
||||
<div class="settings-page-header">
|
||||
<button class="icon-btn modal-close" data-modal="modal-settings" style="margin-right:8px">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
|
||||
</button>
|
||||
<h3>Einstellungen</h3>
|
||||
<button class="icon-btn modal-close" data-modal="modal-settings">×</button>
|
||||
<button class="btn btn-primary" id="settings-save" style="margin-left:auto">Speichern</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="settings-section">
|
||||
<h4>Darstellung</h4>
|
||||
<div class="form-group">
|
||||
<label>Standardansicht</label>
|
||||
<select id="cfg-default-view">
|
||||
<option value="month">Monat</option>
|
||||
<option value="week">Woche</option>
|
||||
<option value="day">Tag</option>
|
||||
<option value="agenda">Termine</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Erster Wochentag</label>
|
||||
<select id="cfg-week-start">
|
||||
<option value="monday">Montag</option>
|
||||
<option value="sunday">Sonntag</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Primärfarbe</label>
|
||||
<div class="ev-color-row">
|
||||
<input type="text" id="cfg-primary-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
|
||||
<div class="ev-color-preview" id="cfg-primary-preview" title="Farbe wählen"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Akzentfarbe</label>
|
||||
<div class="ev-color-row">
|
||||
<input type="text" id="cfg-accent-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
|
||||
<div class="ev-color-preview" id="cfg-accent-preview" title="Farbe wählen"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Heutige-Tag-Farbe</label>
|
||||
<div class="ev-color-row">
|
||||
<input type="text" id="cfg-today-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
|
||||
<div class="ev-color-preview" id="cfg-today-preview" title="Farbe wählen"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="cfg-dim-past" />
|
||||
Vergangene Termine ausgrauen
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-page-body">
|
||||
<nav class="settings-nav">
|
||||
<button class="settings-nav-btn active" data-panel="appearance">Darstellung</button>
|
||||
<button class="settings-nav-btn" data-panel="view">Ansicht & Raster</button>
|
||||
<button class="settings-nav-btn" data-panel="google">Google Konten</button>
|
||||
<button class="settings-nav-btn" data-panel="hidden">Ausgeblendete Kalender</button>
|
||||
<button class="settings-nav-btn hidden" data-panel="users" id="settings-nav-users">Benutzerverwaltung</button>
|
||||
</nav>
|
||||
|
||||
<div class="settings-section" id="settings-google-section">
|
||||
<h4>Google Konten</h4>
|
||||
<div id="google-accounts-list"><span style="font-size:13px;color:var(--text-3)">Keine Google-Konten verbunden</span></div>
|
||||
</div>
|
||||
<div class="settings-panels">
|
||||
|
||||
<div class="settings-section" id="settings-hidden-cals-section">
|
||||
<h4>Ausgeblendete Kalender</h4>
|
||||
<div id="hidden-cals-list"><span style="font-size:13px;color:var(--text-3)">Keine ausgeblendeten Kalender</span></div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section" id="settings-users-section">
|
||||
<h4>Benutzerverwaltung <span class="badge-admin">Admin</span></h4>
|
||||
<div id="users-list"></div>
|
||||
<button class="btn btn-secondary" id="btn-add-user">Benutzer hinzufügen</button>
|
||||
<div id="add-user-form" class="hidden" style="margin-top:12px">
|
||||
<!-- Darstellung -->
|
||||
<div class="settings-panel active" id="settings-panel-appearance">
|
||||
<h4 class="panel-title">Farben</h4>
|
||||
<div class="form-group">
|
||||
<label>Benutzername</label>
|
||||
<input type="text" id="new-username" />
|
||||
<label>Primärfarbe</label>
|
||||
<div class="ev-color-row">
|
||||
<input type="text" id="cfg-primary-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
|
||||
<div class="ev-color-preview" id="cfg-primary-preview" title="Farbe wählen"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Passwort</label>
|
||||
<input type="password" id="new-password" />
|
||||
<label>Akzentfarbe</label>
|
||||
<div class="ev-color-row">
|
||||
<input type="text" id="cfg-accent-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
|
||||
<div class="ev-color-preview" id="cfg-accent-preview" title="Farbe wählen"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Heutige-Tag-Farbe</label>
|
||||
<div class="ev-color-row">
|
||||
<input type="text" id="cfg-today-hex" class="ev-color-hex" maxlength="7" spellcheck="false" />
|
||||
<div class="ev-color-preview" id="cfg-today-preview" title="Farbe wählen"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="panel-title" style="margin-top:24px">Schriftkontrast</h4>
|
||||
<p class="panel-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">Dunkel</span></button>
|
||||
<button class="contrast-btn" data-val="2"><span style="color:#9090a8">Aa</span><span class="contrast-lbl">Mittel</span></button>
|
||||
<button class="contrast-btn" data-val="3"><span style="color:#c8c8d8">Aa</span><span class="contrast-lbl">Hell</span></button>
|
||||
<button class="contrast-btn" data-val="4"><span style="color:#ffffff">Aa</span><span class="contrast-lbl">Maximum</span></button>
|
||||
</div>
|
||||
|
||||
<h4 class="panel-title" style="margin-top:24px">Linienkontrast</h4>
|
||||
<p class="panel-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">Kaum</span></button>
|
||||
<button class="contrast-btn" data-val="2"><span class="line-preview" style="border-color:#2a2a3c"></span><span class="contrast-lbl">Subtil</span></button>
|
||||
<button class="contrast-btn" data-val="3"><span class="line-preview" style="border-color:#3a3a52"></span><span class="contrast-lbl">Normal</span></button>
|
||||
<button class="contrast-btn" data-val="4"><span class="line-preview" style="border-color:#5a5a78"></span><span class="contrast-lbl">Stark</span></button>
|
||||
</div>
|
||||
<label class="toggle-label" style="margin-bottom:8px">
|
||||
<input type="checkbox" id="new-is-admin" /> Administrator
|
||||
</label>
|
||||
<button class="btn btn-primary" id="new-user-save">Erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-ghost" data-modal="modal-settings">Schließen</button>
|
||||
<button class="btn btn-primary" id="settings-save">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ansicht & Raster -->
|
||||
<div class="settings-panel" id="settings-panel-view">
|
||||
<h4 class="panel-title">Kalenderansicht</h4>
|
||||
<div class="form-group">
|
||||
<label>Standardansicht</label>
|
||||
<select id="cfg-default-view">
|
||||
<option value="month">Monat</option>
|
||||
<option value="week">Woche</option>
|
||||
<option value="day">Tag</option>
|
||||
<option value="agenda">Termine</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Erster Wochentag</label>
|
||||
<select id="cfg-week-start">
|
||||
<option value="monday">Montag</option>
|
||||
<option value="sunday">Sonntag</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="cfg-dim-past" />
|
||||
Vergangene Termine ausgrauen
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h4 class="panel-title" style="margin-top:24px">Stundenhöhe (Wochen- & Tagesansicht)</h4>
|
||||
<p class="panel-desc">Wie viel Platz eine Stunde in der Zeitrasteransicht einnimmt</p>
|
||||
<div class="contrast-selector" id="cfg-hour-height" data-setting="hour_height">
|
||||
<button class="contrast-btn" data-val="40"><span class="hour-preview">━━</span><span class="contrast-lbl">Kompakt</span></button>
|
||||
<button class="contrast-btn" data-val="60"><span class="hour-preview">━━━</span><span class="contrast-lbl">Normal</span></button>
|
||||
<button class="contrast-btn" data-val="80"><span class="hour-preview">━━━━</span><span class="contrast-lbl">Komfort</span></button>
|
||||
<button class="contrast-btn" data-val="100"><span class="hour-preview">━━━━━</span><span class="contrast-lbl">Gross</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Google Konten -->
|
||||
<div class="settings-panel" id="settings-panel-google">
|
||||
<h4 class="panel-title">Google Konten</h4>
|
||||
<div id="google-accounts-list"><span style="font-size:13px;color:var(--text-3)">Keine Google-Konten verbunden</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Ausgeblendete Kalender -->
|
||||
<div class="settings-panel" id="settings-panel-hidden">
|
||||
<h4 class="panel-title">Ausgeblendete Kalender</h4>
|
||||
<div id="hidden-cals-list"><span style="font-size:13px;color:var(--text-3)">Keine ausgeblendeten Kalender</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Benutzerverwaltung -->
|
||||
<div class="settings-panel" id="settings-panel-users">
|
||||
<h4 class="panel-title">Benutzerverwaltung <span class="badge-admin">Admin</span></h4>
|
||||
<div id="users-list"></div>
|
||||
<button class="btn btn-secondary" id="btn-add-user" style="margin-top:12px">Benutzer hinzufügen</button>
|
||||
<div id="add-user-form" class="hidden" style="margin-top:12px">
|
||||
<div class="form-group">
|
||||
<label>Benutzername</label>
|
||||
<input type="text" id="new-username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Passwort</label>
|
||||
<input type="password" id="new-password" />
|
||||
</div>
|
||||
<label class="toggle-label" style="margin-bottom:8px">
|
||||
<input type="checkbox" id="new-is-admin" /> Administrator
|
||||
</label>
|
||||
<button class="btn btn-primary" id="new-user-save">Erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- settings-panels -->
|
||||
</div><!-- settings-page-body -->
|
||||
</div><!-- settings-page-card -->
|
||||
</div>
|
||||
|
||||
<!-- Profile Modal -->
|
||||
|
||||
@@ -132,14 +132,16 @@ function renderView() {
|
||||
},
|
||||
showEventPopup,
|
||||
false,
|
||||
weekStartDay
|
||||
weekStartDay,
|
||||
state.settings.hour_height || 60
|
||||
);
|
||||
} else if (state.currentView === 'day') {
|
||||
renderWeek(container, state.currentDate, evs,
|
||||
(date, switchDay) => { if (!switchDay) openNewEventModal(date); },
|
||||
showEventPopup,
|
||||
true,
|
||||
weekStartDay
|
||||
weekStartDay,
|
||||
state.settings.hour_height || 60
|
||||
);
|
||||
} else {
|
||||
renderAgenda(container, state.currentDate, evs, showEventPopup);
|
||||
@@ -1043,25 +1045,41 @@ function openSettingsModal() {
|
||||
});
|
||||
document.getElementById('cfg-dim-past').checked = !!s.dim_past_events;
|
||||
|
||||
// Show users section only for admins
|
||||
// Set active contrast/hour-height buttons
|
||||
[
|
||||
{ id: 'cfg-text-contrast', val: s.text_contrast || 3 },
|
||||
{ id: 'cfg-line-contrast', val: s.line_contrast || 3 },
|
||||
{ id: 'cfg-hour-height', val: s.hour_height || 60 },
|
||||
].forEach(({ id, val }) => {
|
||||
const sel = document.getElementById(id);
|
||||
if (!sel) return;
|
||||
sel.querySelectorAll('.contrast-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', String(btn.dataset.val) === String(val));
|
||||
});
|
||||
});
|
||||
|
||||
// Show users nav button only for admins
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const usersSection = document.getElementById('settings-users-section');
|
||||
if (user.is_admin) {
|
||||
usersSection.classList.remove('hidden');
|
||||
loadUsers();
|
||||
} else {
|
||||
usersSection.classList.add('hidden');
|
||||
}
|
||||
const usersNavBtn = document.getElementById('settings-nav-users');
|
||||
if (usersNavBtn) usersNavBtn.classList.toggle('hidden', !user.is_admin);
|
||||
if (user.is_admin) loadUsers();
|
||||
|
||||
// Render Google accounts
|
||||
// Activate first panel
|
||||
const firstBtn = document.querySelector('.settings-nav-btn:not(.hidden)');
|
||||
if (firstBtn) activateSettingsPanel(firstBtn.dataset.panel);
|
||||
|
||||
// Render Google accounts and hidden calendars
|
||||
renderGoogleAccounts();
|
||||
|
||||
// Render hidden calendars
|
||||
renderHiddenCalendars();
|
||||
|
||||
openModal('modal-settings');
|
||||
}
|
||||
|
||||
function activateSettingsPanel(panel) {
|
||||
document.querySelectorAll('.settings-nav-btn').forEach(b => b.classList.toggle('active', b.dataset.panel === panel));
|
||||
document.querySelectorAll('.settings-panel').forEach(p => p.classList.toggle('active', p.id === 'settings-panel-' + panel));
|
||||
}
|
||||
|
||||
function renderGoogleAccounts() {
|
||||
const list = document.getElementById('google-accounts-list');
|
||||
if (!list) return;
|
||||
@@ -1203,6 +1221,21 @@ function bindSettingsModal() {
|
||||
});
|
||||
});
|
||||
|
||||
// Panel navigation
|
||||
document.querySelectorAll('.settings-nav-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => activateSettingsPanel(btn.dataset.panel));
|
||||
});
|
||||
|
||||
// Contrast / hour-height selectors
|
||||
document.querySelectorAll('.contrast-selector').forEach(sel => {
|
||||
sel.addEventListener('click', e => {
|
||||
const btn = e.target.closest('.contrast-btn');
|
||||
if (!btn) return;
|
||||
sel.querySelectorAll('.contrast-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('btn-add-user').onclick = () => {
|
||||
document.getElementById('add-user-form').classList.toggle('hidden');
|
||||
};
|
||||
@@ -1223,6 +1256,10 @@ function bindSettingsModal() {
|
||||
};
|
||||
|
||||
document.getElementById('settings-save').onclick = async () => {
|
||||
const getActive = (id) => {
|
||||
const btn = document.querySelector(`#${id} .contrast-btn.active`);
|
||||
return btn ? Number(btn.dataset.val) : null;
|
||||
};
|
||||
const settings = {
|
||||
default_view: document.getElementById('cfg-default-view').value,
|
||||
week_start_day: document.getElementById('cfg-week-start').value,
|
||||
@@ -1230,13 +1267,16 @@ function bindSettingsModal() {
|
||||
accent_color: document.getElementById('cfg-accent-hex').value,
|
||||
today_color: document.getElementById('cfg-today-hex').value,
|
||||
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') || 60,
|
||||
};
|
||||
try {
|
||||
await api.put('/settings/', settings);
|
||||
state.settings = { ...state.settings, ...settings };
|
||||
state.dimPast = settings.dim_past_events;
|
||||
weekStartDay = settings.week_start_day;
|
||||
applyTheme(settings);
|
||||
applyTheme(state.settings);
|
||||
showToast('Einstellungen gespeichert');
|
||||
closeModal('modal-settings');
|
||||
renderMiniCal();
|
||||
|
||||
@@ -63,12 +63,37 @@ export function getISOWeekNumber(d) {
|
||||
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');
|
||||
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');
|
||||
|
||||
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);
|
||||
|
||||
const hh = settings.hour_height || 60;
|
||||
root.style.setProperty('--hour-h', hh + 'px');
|
||||
}
|
||||
|
||||
function hexToRgba(hex, alpha) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { isToday, isPast, dayOfWeek, weekStart, getISOWeekNumber } from '../util
|
||||
|
||||
const DOW_SHORT = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
||||
|
||||
export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false, weekStartDay = 'monday') {
|
||||
export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false, weekStartDay = 'monday', hourH = 60) {
|
||||
// Build the days array (7 days for week, 1 for day)
|
||||
const days = [];
|
||||
if (isSingleDay) {
|
||||
@@ -58,7 +58,6 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
).join('');
|
||||
|
||||
// ── Day columns ───────────────────────────────────────
|
||||
// For each day, lay out timed events
|
||||
const dayCols = days.map(day => {
|
||||
const key = dayKey(day);
|
||||
const dayEvs = timedEvs.filter(ev => {
|
||||
@@ -66,18 +65,17 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
return isSameDay(s, day);
|
||||
});
|
||||
|
||||
// Compute layout columns for overlapping events
|
||||
const positioned = layoutEvents(dayEvs);
|
||||
|
||||
const hourLines = Array.from({length: 24}, (_, h) =>
|
||||
`<div class="hour-line" style="top:${h * 60}px"><div class="half-line"></div></div>`
|
||||
`<div class="hour-line" style="top:${h * hourH}px"><div class="half-line"></div></div>`
|
||||
).join('');
|
||||
|
||||
const evHtml = positioned.map(({ ev, col, cols }) => {
|
||||
const s = new Date(ev.start);
|
||||
const e = new Date(ev.end);
|
||||
const top = (s.getHours() * 60 + s.getMinutes());
|
||||
const height = Math.max(20, (e - s) / 60000);
|
||||
const top = s.getHours() * hourH + s.getMinutes() * hourH / 60;
|
||||
const height = Math.max(20, (e - s) / 60000 * hourH / 60);
|
||||
const left = (col / cols) * 100;
|
||||
const width = (1 / cols) * 100 - 0.5;
|
||||
const color = ev.color || ev.calendarColor || '#4285f4';
|
||||
@@ -93,7 +91,7 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
return `<div class="week-day-col" data-date="${key}" style="height:${60*24}px">
|
||||
return `<div class="week-day-col" data-date="${key}" style="height:${hourH * 24}px">
|
||||
${hourLines}
|
||||
${evHtml}
|
||||
</div>`;
|
||||
@@ -118,10 +116,10 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
|
||||
// Scroll to ~8:00
|
||||
const body = container.querySelector('.week-body');
|
||||
if (body) body.scrollTop = 8 * 60 - 20;
|
||||
if (body) body.scrollTop = 8 * hourH - 20;
|
||||
|
||||
// Render current-time line
|
||||
renderNowLine(container, days);
|
||||
renderNowLine(container, days, hourH);
|
||||
|
||||
// Click: slot
|
||||
container.querySelectorAll('.week-day-col').forEach(col => {
|
||||
@@ -129,9 +127,8 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
if (e.target.closest('.timed-event')) return;
|
||||
const rect = col.getBoundingClientRect();
|
||||
const y = e.clientY - rect.top + (container.querySelector('.week-body')?.scrollTop || 0);
|
||||
const mins = Math.floor(y);
|
||||
const h = Math.floor(mins / 60);
|
||||
const m = Math.round((mins % 60) / 15) * 15;
|
||||
const h = Math.floor(y / hourH);
|
||||
const m = Math.round(((y % hourH) / hourH * 60) / 15) * 15;
|
||||
const date = new Date(col.dataset.date + 'T00:00:00');
|
||||
date.setHours(h, m, 0, 0);
|
||||
onSlotClick(date);
|
||||
@@ -164,34 +161,31 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
});
|
||||
}
|
||||
|
||||
function renderNowLine(container, days) {
|
||||
function renderNowLine(container, days, hourH = 60) {
|
||||
const now = new Date();
|
||||
const todayCol = container.querySelector(`.week-day-col[data-date="${dayKey(now)}"]`);
|
||||
if (!todayCol) return;
|
||||
|
||||
const top = now.getHours() * 60 + now.getMinutes();
|
||||
const top = now.getHours() * hourH + now.getMinutes() * hourH / 60;
|
||||
const line = document.createElement('div');
|
||||
line.className = 'now-line';
|
||||
line.style.top = top + 'px';
|
||||
line.innerHTML = '<div class="now-dot"></div>';
|
||||
todayCol.appendChild(line);
|
||||
|
||||
// Update every minute
|
||||
setTimeout(() => renderNowLine(container, days), 60000);
|
||||
setTimeout(() => renderNowLine(container, days, hourH), 60000);
|
||||
}
|
||||
|
||||
function layoutEvents(events) {
|
||||
if (!events.length) return [];
|
||||
|
||||
// Sort by start time
|
||||
const sorted = events.slice().sort((a, b) => new Date(a.start) - new Date(b.start));
|
||||
const columns = []; // each column is an array of events
|
||||
const columns = [];
|
||||
|
||||
const result = sorted.map(ev => {
|
||||
const start = new Date(ev.start);
|
||||
const end = new Date(ev.end);
|
||||
|
||||
// Find the first column where the event doesn't overlap
|
||||
let placed = false;
|
||||
for (let c = 0; c < columns.length; c++) {
|
||||
const lastInCol = columns[c][columns[c].length - 1];
|
||||
@@ -209,11 +203,8 @@ function layoutEvents(events) {
|
||||
return ev;
|
||||
});
|
||||
|
||||
// Calculate how many columns each event spans
|
||||
return result.map(ev => {
|
||||
const start = new Date(ev.start);
|
||||
const end = new Date(ev.end);
|
||||
// Count overlapping events
|
||||
let maxCol = ev._col;
|
||||
sorted.forEach(other => {
|
||||
if (other === ev) return;
|
||||
|
||||
Reference in New Issue
Block a user