feat: calendar rename for all sources, group icon in sidebar, configurable share icon
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -218,6 +218,13 @@ def _migrate():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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()
|
_migrate()
|
||||||
|
|
||||||
app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None)
|
app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None)
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ class UserSettings(Base):
|
|||||||
default_reminder_minutes = Column(Integer, nullable=True)
|
default_reminder_minutes = Column(Integer, nullable=True)
|
||||||
# Default duration (in minutes) applied to a newly created event's end time.
|
# Default duration (in minutes) applied to a newly created event's end time.
|
||||||
default_event_duration_minutes = Column(Integer, default=60)
|
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")
|
user = relationship("User", back_populates="settings")
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class SettingsUpdate(BaseModel):
|
|||||||
group_visible_calendar_id: Optional[int] = None
|
group_visible_calendar_id: Optional[int] = None
|
||||||
default_reminder_minutes: Optional[int] = None # null = off
|
default_reminder_minutes: Optional[int] = None # null = off
|
||||||
default_event_duration_minutes: Optional[int] = None
|
default_event_duration_minutes: Optional[int] = None
|
||||||
|
share_calendar_icon: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
def _settings_dict(s: models.UserSettings) -> dict:
|
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,
|
"group_visible_calendar_id": s.group_visible_calendar_id,
|
||||||
"default_reminder_minutes": s.default_reminder_minutes,
|
"default_reminder_minutes": s.default_reminder_minutes,
|
||||||
"default_event_duration_minutes": s.default_event_duration_minutes or 60,
|
"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
|
# For these three override colours, an explicit null is meaningful
|
||||||
# ("reset to default") and must be persisted as NULL. All other fields
|
# ("reset to default") and must be persisted as NULL. All other fields
|
||||||
# keep the previous behaviour where a null/missing value is ignored.
|
# 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)
|
update_data = data.model_dump(exclude_unset=True)
|
||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
if field in NULLABLE_OVERRIDES:
|
if field in NULLABLE_OVERRIDES:
|
||||||
|
|||||||
@@ -789,6 +789,10 @@
|
|||||||
<button class="contrast-btn" data-val="80"><span class="hour-preview">━━━━━</span><span class="contrast-lbl" data-i18n="hour_large">Gross</span></button>
|
<button class="contrast-btn" data-val="80"><span class="hour-preview">━━━━━</span><span class="contrast-lbl" data-i18n="hour_large">Gross</span></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h4 class="panel-title" style="margin-top:24px">Teilen-Symbol</h4>
|
||||||
|
<p class="panel-desc">Symbol neben deinem geteilten Kalender in der Seitenleiste</p>
|
||||||
|
<div class="group-icon-picker" id="cfg-share-icon"></div>
|
||||||
|
|
||||||
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_colors">Farben</h4>
|
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_colors">Farben</h4>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label data-i18n="settings_primary_color">Primärfarbe</label>
|
<label data-i18n="settings_primary_color">Primärfarbe</label>
|
||||||
|
|||||||
@@ -726,7 +726,8 @@ function renderCalendarList() {
|
|||||||
entries.push({ key: `local:${cal.id}`, source: 'local', dataId: `data-cal-id="${cal.id}"`,
|
entries.push({ key: `local:${cal.id}`, source: 'local', dataId: `data-cal-id="${cal.id}"`,
|
||||||
name: cal.name, color: cal.color, enabled: cal.enabled,
|
name: cal.name, color: cal.color, enabled: cal.enabled,
|
||||||
reminders: cal.owned !== false, remindersEnabled: cal.reminders_enabled !== false,
|
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 => {
|
state.icalSubscriptions.forEach(sub => {
|
||||||
entries.push({ key: `ical:${sub.id}`, source: 'ical', dataId: `data-sub-id="${sub.id}"`,
|
entries.push({ key: `ical:${sub.id}`, source: 'ical', dataId: `data-sub-id="${sub.id}"`,
|
||||||
@@ -772,8 +773,8 @@ function renderCalendarList() {
|
|||||||
<span class="cal-drag-handle" title="${t('drag_reorder')}">⠿</span>
|
<span class="cal-drag-handle" title="${t('drag_reorder')}">⠿</span>
|
||||||
<input type="checkbox" ${e.enabled ? 'checked' : ''} data-source="${e.source}" ${e.dataId} />
|
<input type="checkbox" ${e.enabled ? 'checked' : ''} data-source="${e.source}" ${e.dataId} />
|
||||||
<div class="cal-item-dot" style="background:${e.color}" data-source="${e.source}" ${e.dataId} title="${t('change_color')}"></div>
|
<div class="cal-item-dot" style="background:${e.color}" data-source="${e.source}" ${e.dataId} title="${t('change_color')}"></div>
|
||||||
${e.isGroupCal ? `<span class="cal-shared-flag" title="${escHtml(e.sourceLabel)}">${groupIconSvg('people', 13)}</span>` : ''}
|
${e.isGroupCal ? `<span class="cal-shared-flag" title="${escHtml(e.sourceLabel)}">${groupIconSvg(e.groupIcon || 'people', 13)}</span>` : ''}
|
||||||
${e.groupVisible ? `<span class="cal-shared-flag cal-shared-flag-own" title="${t('group_visible_flag')}">${SHARE_ICON}</span>` : ''}
|
${e.groupVisible ? `<span class="cal-shared-flag cal-shared-flag-own" title="${t('group_visible_flag')}">${groupIconSvg(state.settings?.share_calendar_icon || 'people', 13)}</span>` : ''}
|
||||||
<span class="cal-item-name" data-source="${e.source}">${escHtml(e.name)}</span>
|
<span class="cal-item-name" data-source="${e.source}">${escHtml(e.name)}</span>
|
||||||
${e.reminders ? `<button class="icon-btn mini-btn cal-item-bell ${e.remindersEnabled ? '' : 'off'}" data-source="${e.source}" ${e.dataId} title="${e.remindersEnabled ? t('calendar_reminders_on') : t('calendar_reminders_off')}">${e.remindersEnabled ? BELL : BELL_OFF}</button>` : ''}
|
${e.reminders ? `<button class="icon-btn mini-btn cal-item-bell ${e.remindersEnabled ? '' : 'off'}" data-source="${e.source}" ${e.dataId} title="${e.remindersEnabled ? t('calendar_reminders_on') : t('calendar_reminders_off')}">${e.remindersEnabled ? BELL : BELL_OFF}</button>` : ''}
|
||||||
${e.remove ? `<button class="icon-btn mini-btn cal-item-remove" data-source="${e.source}" ${e.dataId} title="${e.remove.title}">${e.remove.icon}</button>` : ''}
|
${e.remove ? `<button class="icon-btn mini-btn cal-item-remove" data-source="${e.source}" ${e.dataId} title="${e.remove.title}">${e.remove.icon}</button>` : ''}
|
||||||
@@ -966,6 +967,20 @@ function renderCalendarList() {
|
|||||||
await api.put(`/ical/subscriptions/${subId}`, { name: newName });
|
await api.put(`/ical/subscriptions/${subId}`, { name: newName });
|
||||||
const sub = state.icalSubscriptions.find(s => s.id === subId);
|
const sub = state.icalSubscriptions.find(s => s.id === subId);
|
||||||
if (sub) sub.name = newName;
|
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();
|
renderCalendarList();
|
||||||
@@ -3042,6 +3057,21 @@ function openSettingsModal() {
|
|||||||
document.getElementById('cfg-private-visibility').value = s.private_event_visibility || 'busy';
|
document.getElementById('cfg-private-visibility').value = s.private_event_visibility || 'busy';
|
||||||
renderGroupVisibleList(s.group_visible_calendar_id);
|
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 =>
|
||||||
|
`<button type="button" class="group-icon-opt ${cur === k ? 'on' : ''}" data-share-icon="${k}" title="${k}">${groupIconSvg(k, 20)}</button>`
|
||||||
|
).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).
|
// Profile chapter: name (from cached user) + email (fresh from /profile).
|
||||||
const pu = JSON.parse(localStorage.getItem('user') || '{}');
|
const pu = JSON.parse(localStorage.getItem('user') || '{}');
|
||||||
document.getElementById('cfg-display-name').value = pu.display_name || pu.username || '';
|
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;
|
const gvVal = document.getElementById('cfg-group-visible-list')?.dataset.selected;
|
||||||
settings.group_visible_calendar_id = gvVal ? parseInt(gvVal) : null;
|
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 {
|
try {
|
||||||
await api.put('/settings/', settings);
|
await api.put('/settings/', settings);
|
||||||
state.settings = { ...state.settings, ...settings };
|
state.settings = { ...state.settings, ...settings };
|
||||||
@@ -3821,6 +3853,7 @@ function bindSettingsModal() {
|
|||||||
applyTheme(state.settings);
|
applyTheme(state.settings);
|
||||||
showToast(t('settings_saved'));
|
showToast(t('settings_saved'));
|
||||||
closeModal('modal-settings');
|
closeModal('modal-settings');
|
||||||
|
renderCalendarList();
|
||||||
renderMiniCal();
|
renderMiniCal();
|
||||||
fetchAndRender();
|
fetchAndRender();
|
||||||
} catch (e) { showToast(e.message, true); }
|
} catch (e) { showToast(e.message, true); }
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// Increment APP_VERSION with every code change
|
// Increment APP_VERSION with every code change
|
||||||
export const APP_VERSION = 'v60';
|
export const APP_VERSION = 'v61';
|
||||||
|
|||||||
Reference in New Issue
Block a user