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';