From d6159f06390bd1b95dc608580cfe413fcb9257e7 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 15 Jun 2026 21:56:24 +0200 Subject: [PATCH] feat: calendar rename for all sources, group icon in sidebar, configurable share icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename: Add google and homeassistant branches to the dblclick-rename save() handler; backend already accepted name updates for both sources - Group icon: Pass groupIconForLocalCal(cal.id) into sidebar entries so the group's custom icon (home/heart/work/…) shows instead of always defaulting to the people icon - Share icon: New share_calendar_icon field in UserSettings (+ migration) replaces the hardcoded SHARE_ICON SVG; users pick from GROUP_ICON_KEYS in Settings → Darstellung via a new icon-picker row Co-Authored-By: Claude Sonnet 4.6 --- backend/main.py | 7 ++++++ backend/models.py | 2 ++ backend/routers/settings_router.py | 4 ++- frontend/index.html | 4 +++ frontend/js/calendar.js | 39 +++++++++++++++++++++++++++--- frontend/js/version.js | 2 +- 6 files changed, 53 insertions(+), 5 deletions(-) diff --git a/backend/main.py b/backend/main.py index 5d85994..17cce1a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -218,6 +218,13 @@ def _migrate(): except Exception: pass + try: + conn.execute(text("ALTER TABLE user_settings ADD COLUMN share_calendar_icon VARCHAR(16)")) + conn.commit() + logging.info("Migration: added share_calendar_icon to user_settings") + except Exception: + pass + _migrate() app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None) diff --git a/backend/models.py b/backend/models.py index a12de57..f4cdac3 100644 --- a/backend/models.py +++ b/backend/models.py @@ -108,6 +108,8 @@ class UserSettings(Base): default_reminder_minutes = Column(Integer, nullable=True) # Default duration (in minutes) applied to a newly created event's end time. default_event_duration_minutes = Column(Integer, default=60) + # Icon key (from GROUP_ICON_KEYS) shown next to calendars this user shares with groups. + share_calendar_icon = Column(String(16), nullable=True) user = relationship("User", back_populates="settings") diff --git a/backend/routers/settings_router.py b/backend/routers/settings_router.py index 6c4fc27..049b279 100644 --- a/backend/routers/settings_router.py +++ b/backend/routers/settings_router.py @@ -31,6 +31,7 @@ class SettingsUpdate(BaseModel): group_visible_calendar_id: Optional[int] = None default_reminder_minutes: Optional[int] = None # null = off default_event_duration_minutes: Optional[int] = None + share_calendar_icon: Optional[str] = None def _settings_dict(s: models.UserSettings) -> dict: @@ -54,6 +55,7 @@ def _settings_dict(s: models.UserSettings) -> dict: "group_visible_calendar_id": s.group_visible_calendar_id, "default_reminder_minutes": s.default_reminder_minutes, "default_event_duration_minutes": s.default_event_duration_minutes or 60, + "share_calendar_icon": s.share_calendar_icon, } @@ -96,7 +98,7 @@ def update_settings( # 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", "group_visible_calendar_id", "default_reminder_minutes", "default_event_duration_minutes"} + NULLABLE_OVERRIDES = {"text_color", "line_color", "bg_color", "group_visible_calendar_id", "default_reminder_minutes", "default_event_duration_minutes", "share_calendar_icon"} update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): if field in NULLABLE_OVERRIDES: diff --git a/frontend/index.html b/frontend/index.html index e87ba2b..2d30710 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -789,6 +789,10 @@ +

Teilen-Symbol

+

Symbol neben deinem geteilten Kalender in der Seitenleiste

+
+

Farben

diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index 8dcc551..63c1adf 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -726,7 +726,8 @@ function renderCalendarList() { entries.push({ key: `local:${cal.id}`, source: 'local', dataId: `data-cal-id="${cal.id}"`, name: cal.name, color: cal.color, enabled: cal.enabled, reminders: cal.owned !== false, remindersEnabled: cal.reminders_enabled !== false, - sourceLabel: `${t('groups_title')} · ${cal.shared_by || ''}`, isGroupCal: true, remove: null }); + sourceLabel: `${t('groups_title')} · ${cal.shared_by || ''}`, + isGroupCal: true, groupIcon: groupIconForLocalCal(cal.id), remove: null }); }); state.icalSubscriptions.forEach(sub => { entries.push({ key: `ical:${sub.id}`, source: 'ical', dataId: `data-sub-id="${sub.id}"`, @@ -772,8 +773,8 @@ function renderCalendarList() {
- ${e.isGroupCal ? `${groupIconSvg('people', 13)}` : ''} - ${e.groupVisible ? `${SHARE_ICON}` : ''} + ${e.isGroupCal ? `${groupIconSvg(e.groupIcon || 'people', 13)}` : ''} + ${e.groupVisible ? `${groupIconSvg(state.settings?.share_calendar_icon || 'people', 13)}` : ''} ${escHtml(e.name)} ${e.reminders ? `` : ''} ${e.remove ? `` : ''} @@ -966,6 +967,20 @@ function renderCalendarList() { await api.put(`/ical/subscriptions/${subId}`, { name: newName }); const sub = state.icalSubscriptions.find(s => s.id === subId); if (sub) sub.name = newName; + } else if (source === 'google') { + const calId = parseInt(item.dataset.calId); + await api.put(`/google/calendars/${calId}`, { name: newName }); + for (const acc of state.googleAccounts) { + const cal = (acc.calendars || []).find(c => c.id === calId); + if (cal) cal.name = newName; + } + } else if (source === 'homeassistant') { + const calId = parseInt(item.dataset.calId); + await api.put(`/homeassistant/calendars/${calId}`, { name: newName }); + for (const acc of state.haAccounts) { + const cal = (acc.calendars || []).find(c => c.id === calId); + if (cal) cal.name = newName; + } } } renderCalendarList(); @@ -3042,6 +3057,21 @@ function openSettingsModal() { document.getElementById('cfg-private-visibility').value = s.private_event_visibility || 'busy'; renderGroupVisibleList(s.group_visible_calendar_id); + // Share-Icon-Picker in Darstellung + const shareIconPicker = document.getElementById('cfg-share-icon'); + if (shareIconPicker) { + const cur = s.share_calendar_icon || 'people'; + shareIconPicker.innerHTML = GROUP_ICON_KEYS.map(k => + `` + ).join(''); + shareIconPicker.querySelectorAll('.group-icon-opt').forEach(btn => { + btn.addEventListener('click', () => { + shareIconPicker.querySelectorAll('.group-icon-opt').forEach(b => b.classList.remove('on')); + btn.classList.add('on'); + }); + }); + } + // Profile chapter: name (from cached user) + email (fresh from /profile). const pu = JSON.parse(localStorage.getItem('user') || '{}'); document.getElementById('cfg-display-name').value = pu.display_name || pu.username || ''; @@ -3812,6 +3842,8 @@ function bindSettingsModal() { }; const gvVal = document.getElementById('cfg-group-visible-list')?.dataset.selected; settings.group_visible_calendar_id = gvVal ? parseInt(gvVal) : null; + const shareIconPicker = document.getElementById('cfg-share-icon'); + settings.share_calendar_icon = shareIconPicker?.querySelector('.group-icon-opt.on')?.dataset.shareIcon || null; try { await api.put('/settings/', settings); state.settings = { ...state.settings, ...settings }; @@ -3821,6 +3853,7 @@ function bindSettingsModal() { applyTheme(state.settings); showToast(t('settings_saved')); closeModal('modal-settings'); + renderCalendarList(); renderMiniCal(); fetchAndRender(); } catch (e) { showToast(e.message, true); } diff --git a/frontend/js/version.js b/frontend/js/version.js index 3992b03..76ae50b 100644 --- a/frontend/js/version.js +++ b/frontend/js/version.js @@ -1,2 +1,2 @@ // Increment APP_VERSION with every code change -export const APP_VERSION = 'v60'; +export const APP_VERSION = 'v61';