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:
|
||||
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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
</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>
|
||||
<div class="form-group">
|
||||
<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}"`,
|
||||
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() {
|
||||
<span class="cal-drag-handle" title="${t('drag_reorder')}">⠿</span>
|
||||
<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>
|
||||
${e.isGroupCal ? `<span class="cal-shared-flag" title="${escHtml(e.sourceLabel)}">${groupIconSvg('people', 13)}</span>` : ''}
|
||||
${e.groupVisible ? `<span class="cal-shared-flag cal-shared-flag-own" title="${t('group_visible_flag')}">${SHARE_ICON}</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')}">${groupIconSvg(state.settings?.share_calendar_icon || 'people', 13)}</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.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 });
|
||||
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 =>
|
||||
`<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).
|
||||
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); }
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Increment APP_VERSION with every code change
|
||||
export const APP_VERSION = 'v60';
|
||||
export const APP_VERSION = 'v61';
|
||||
|
||||
Reference in New Issue
Block a user