feat: flache sortierbare Kalenderliste (Drag&Drop) + Fixes
- Sidebar: eine flache Kalenderliste statt Quellen-Gruppen; Quelle/Konto klein-grau inline rechts neben dem Namen; per Drag&Drop sortierbar (Reihenfolge pro Geraet in localStorage). - Gruppenkalender serverseitig auch beim Besitzer als group:true markiert -> erscheint nicht mehr in der "Fuer Gruppen sichtbar"-Auswahl und nicht in der normalen Kalenderliste (nur unter Gruppen). - Settings-URL-State: uiSettingsOpen wird beim Init aus der URL gesetzt, bevor das erste writeUrlState() es ueberschreibt -> Reload bleibt jetzt wirklich in den Einstellungen. - Auswahl-Markierungen (Mitglieder/Gruppen-Sichtbar) in Akzentfarbe, CSS-gezeichnet statt blauer Emoji. Version v28. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -91,13 +91,34 @@ def list_calendars(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: models.User = Depends(get_current_user),
|
current_user: models.User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
# Map calendar_id -> group name for every group the user belongs to, so we
|
||||||
|
# can flag group calendars as such even when the user owns them (the creator
|
||||||
|
# owns the group calendar — it must still be marked group:true).
|
||||||
|
group_cal_map = {
|
||||||
|
cal_id: name
|
||||||
|
for cal_id, name in (
|
||||||
|
db.query(models.GroupCalendar.calendar_id, models.Group.name)
|
||||||
|
.join(models.Group, models.Group.id == models.GroupCalendar.group_id)
|
||||||
|
.join(models.GroupMember, models.GroupMember.group_id == models.GroupCalendar.group_id)
|
||||||
|
.filter(models.GroupMember.user_id == current_user.id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
# Own calendars
|
# Own calendars
|
||||||
own = (
|
own = (
|
||||||
db.query(models.LocalCalendar)
|
db.query(models.LocalCalendar)
|
||||||
.filter(models.LocalCalendar.user_id == current_user.id)
|
.filter(models.LocalCalendar.user_id == current_user.id)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
result = [_cal_dict(c, owned=True) for c in own]
|
result = []
|
||||||
|
for c in own:
|
||||||
|
d = _cal_dict(c, owned=True)
|
||||||
|
if c.id in group_cal_map:
|
||||||
|
d["group"] = True
|
||||||
|
result.append(d)
|
||||||
|
|
||||||
|
seen_ids = {c.id for c in own}
|
||||||
|
|
||||||
# Calendars shared with this user
|
# Calendars shared with this user
|
||||||
shares = (
|
shares = (
|
||||||
@@ -105,33 +126,30 @@ def list_calendars(
|
|||||||
.filter(models.CalendarShare.user_id == current_user.id)
|
.filter(models.CalendarShare.user_id == current_user.id)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
seen_ids = {c.id for c in own}
|
|
||||||
for share in shares:
|
for share in shares:
|
||||||
cal = share.calendar
|
cal = share.calendar
|
||||||
if cal is None or cal.id in seen_ids:
|
if cal is None or cal.id in seen_ids:
|
||||||
continue
|
continue
|
||||||
seen_ids.add(cal.id)
|
seen_ids.add(cal.id)
|
||||||
owner = db.query(models.User).filter(models.User.id == cal.user_id).first()
|
owner = db.query(models.User).filter(models.User.id == cal.user_id).first()
|
||||||
result.append(_cal_dict(
|
d = _cal_dict(
|
||||||
cal, owned=False,
|
cal, owned=False,
|
||||||
shared_by=owner.username if owner else None,
|
shared_by=owner.username if owner else None,
|
||||||
permission=share.permission,
|
permission=share.permission,
|
||||||
))
|
)
|
||||||
|
if cal.id in group_cal_map:
|
||||||
|
d["group"] = True
|
||||||
|
result.append(d)
|
||||||
|
|
||||||
# Group calendars the user can reach via membership (read_write), so members
|
# Group calendars reached via membership (read_write) that aren't already
|
||||||
# can select the group calendar in the editor and see it in their list.
|
# listed, so members can select/see the group calendar.
|
||||||
group_cals = (
|
for cal_id, group_name in group_cal_map.items():
|
||||||
db.query(models.LocalCalendar, models.Group.name)
|
if cal_id in seen_ids:
|
||||||
.join(models.GroupCalendar, models.GroupCalendar.calendar_id == models.LocalCalendar.id)
|
|
||||||
.join(models.Group, models.Group.id == models.GroupCalendar.group_id)
|
|
||||||
.join(models.GroupMember, models.GroupMember.group_id == models.GroupCalendar.group_id)
|
|
||||||
.filter(models.GroupMember.user_id == current_user.id)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
for cal, group_name in group_cals:
|
|
||||||
if cal.id in seen_ids:
|
|
||||||
continue
|
continue
|
||||||
seen_ids.add(cal.id)
|
cal = db.query(models.LocalCalendar).filter(models.LocalCalendar.id == cal_id).first()
|
||||||
|
if not cal:
|
||||||
|
continue
|
||||||
|
seen_ids.add(cal_id)
|
||||||
d = _cal_dict(cal, owned=False, shared_by=group_name, permission="read_write")
|
d = _cal_dict(cal, owned=False, shared_by=group_name, permission="read_write")
|
||||||
d["group"] = True
|
d["group"] = True
|
||||||
result.append(d)
|
result.append(d)
|
||||||
|
|||||||
@@ -1887,8 +1887,37 @@ a { color: var(--primary); text-decoration: none; }
|
|||||||
}
|
}
|
||||||
.pick-row:last-child { border-bottom: none; }
|
.pick-row:last-child { border-bottom: none; }
|
||||||
.pick-row:hover { background: var(--bg-surface); }
|
.pick-row:hover { background: var(--bg-surface); }
|
||||||
.pick-row-sel { background: rgba(66, 133, 244, 0.12); }
|
.pick-row-sel { box-shadow: inset 3px 0 0 var(--accent); }
|
||||||
.pick-mark { flex: 0 0 auto; width: 18px; text-align: center; color: var(--primary); }
|
.pick-mark {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 18px; height: 18px;
|
||||||
|
border: 2px solid var(--text-3);
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 12px; line-height: 1; color: #fff;
|
||||||
|
}
|
||||||
|
.pick-check { border-radius: 4px; }
|
||||||
|
.pick-radio { border-radius: 50%; }
|
||||||
|
.pick-mark.on { background: var(--accent); border-color: var(--accent); }
|
||||||
.pick-dot { flex: 0 0 auto; width: 12px; height: 12px; border-radius: 50%; }
|
.pick-dot { flex: 0 0 auto; width: 12px; height: 12px; border-radius: 50%; }
|
||||||
.pick-dot-empty { background: transparent; }
|
.pick-dot-empty { background: transparent; }
|
||||||
.pick-name { flex: 1 1 auto; text-align: left; }
|
.pick-name { flex: 1 1 auto; text-align: left; }
|
||||||
|
|
||||||
|
/* Flat calendar list: inline source label + drag handle. */
|
||||||
|
.cal-source {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-3);
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 45%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
.cal-drag-handle {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
cursor: grab;
|
||||||
|
color: var(--text-3);
|
||||||
|
font-size: 14px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.cal-item.cal-dragging { opacity: .5; }
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ export async function initCalendar() {
|
|||||||
const urlState = readUrlState();
|
const urlState = readUrlState();
|
||||||
if (urlState.date) state.currentDate = urlState.date;
|
if (urlState.date) state.currentDate = urlState.date;
|
||||||
if (urlState.view) state.currentView = urlState.view;
|
if (urlState.view) state.currentView = urlState.view;
|
||||||
|
// Preserve the settings flag through the first writeUrlState() (fired by the
|
||||||
|
// initial fetchAndRender) so a reload reopens settings instead of stripping it.
|
||||||
|
uiSettingsOpen = urlState.settings === true;
|
||||||
|
|
||||||
setLang(settings.language || 'de');
|
setLang(settings.language || 'de');
|
||||||
applyTheme(settings);
|
applyTheme(settings);
|
||||||
@@ -137,7 +140,7 @@ export async function initCalendar() {
|
|||||||
loadGroups();
|
loadGroups();
|
||||||
|
|
||||||
// Reopen the settings modal after a reload if the URL says we were in it.
|
// Reopen the settings modal after a reload if the URL says we were in it.
|
||||||
if (readUrlState().settings) openSettingsModal();
|
if (urlState.settings) openSettingsModal();
|
||||||
|
|
||||||
// Browser-Back/Forward: URL-Hash → State synchronisieren
|
// Browser-Back/Forward: URL-Hash → State synchronisieren
|
||||||
window.addEventListener('hashchange', () => {
|
window.addEventListener('hashchange', () => {
|
||||||
@@ -562,122 +565,121 @@ function renderMiniCal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Calendar List ─────────────────────────────────────────
|
// ── Calendar List ─────────────────────────────────────────
|
||||||
|
const CAL_ORDER_KEY = 'cal_order';
|
||||||
|
function loadCalOrder() {
|
||||||
|
try { return JSON.parse(localStorage.getItem(CAL_ORDER_KEY) || '[]'); }
|
||||||
|
catch (e) { return []; }
|
||||||
|
}
|
||||||
|
function saveCalOrder(keys) {
|
||||||
|
localStorage.setItem(CAL_ORDER_KEY, JSON.stringify(keys));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag & drop reordering of the flat calendar list (persisted per device).
|
||||||
|
function bindCalDragReorder(container) {
|
||||||
|
let dragKey = null;
|
||||||
|
container.querySelectorAll('.cal-item').forEach(item => {
|
||||||
|
item.addEventListener('dragstart', e => {
|
||||||
|
// Don't start a drag from interactive children (checkbox, color dot, buttons).
|
||||||
|
if (e.target.closest('input, button, .cal-item-dot')) { e.preventDefault(); return; }
|
||||||
|
dragKey = item.dataset.key;
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
item.classList.add('cal-dragging');
|
||||||
|
});
|
||||||
|
item.addEventListener('dragend', () => {
|
||||||
|
dragKey = null;
|
||||||
|
item.classList.remove('cal-dragging');
|
||||||
|
});
|
||||||
|
item.addEventListener('dragover', e => { e.preventDefault(); });
|
||||||
|
item.addEventListener('drop', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const targetKey = item.dataset.key;
|
||||||
|
if (!dragKey || dragKey === targetKey) return;
|
||||||
|
const keys = [...container.querySelectorAll('.cal-item')].map(el => el.dataset.key);
|
||||||
|
const from = keys.indexOf(dragKey);
|
||||||
|
const to = keys.indexOf(targetKey);
|
||||||
|
if (from === -1 || to === -1) return;
|
||||||
|
keys.splice(to, 0, keys.splice(from, 1)[0]);
|
||||||
|
saveCalOrder(keys);
|
||||||
|
renderCalendarList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderCalendarList() {
|
function renderCalendarList() {
|
||||||
const container = document.getElementById('cal-list-items');
|
const container = document.getElementById('cal-list-items');
|
||||||
let html = '';
|
// Eye-off (hide external calendar) and trash (delete local/ical) icons.
|
||||||
|
const EYE_OFF = `<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>`;
|
||||||
|
const TRASH = `<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>`;
|
||||||
|
|
||||||
// ── CalDAV accounts ────────────────────────────────────
|
// Build a single flat list of all calendars. The source/account is shown
|
||||||
if (state.accounts.length) {
|
// inline (small, grey) next to the name and section headers are gone, so the
|
||||||
html += state.accounts.map(acc => {
|
// whole list can be freely reordered via drag & drop.
|
||||||
const visibleCals = acc.calendars.filter(c => !c.sidebar_hidden);
|
const entries = [];
|
||||||
if (!visibleCals.length) return '';
|
state.accounts.forEach(acc => {
|
||||||
return `<div class="cal-account-name">${escHtml(acc.name)}</div>` +
|
(acc.calendars || []).filter(c => !c.sidebar_hidden).forEach(cal => {
|
||||||
visibleCals.map(cal =>
|
entries.push({ key: `caldav:${cal.id}`, source: 'caldav', dataId: `data-cal-id="${cal.id}"`,
|
||||||
`<div class="cal-item" data-cal-id="${cal.id}" data-source="caldav">
|
name: cal.name, color: cal.color || '#4285f4', enabled: cal.enabled,
|
||||||
<input type="checkbox" ${cal.enabled ? 'checked' : ''} data-cal-id="${cal.id}" data-source="caldav" />
|
sourceLabel: acc.name, remove: { icon: EYE_OFF, title: t('hide_cal') } });
|
||||||
<div class="cal-item-dot" style="background:${cal.color}" data-cal-id="${cal.id}" data-source="caldav" title="${t('change_color')}"></div>
|
});
|
||||||
<span class="cal-item-name" data-source="caldav">${escHtml(cal.name)}</span>
|
});
|
||||||
<button class="icon-btn mini-btn cal-item-remove" data-cal-id="${cal.id}" data-source="caldav" title="${t('hide_cal')}">
|
state.localCalendars.filter(c => c.owned !== false && !c.group).forEach(cal => {
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>
|
entries.push({ key: `local:${cal.id}`, source: 'local', dataId: `data-cal-id="${cal.id}"`,
|
||||||
</button>
|
name: cal.name, color: cal.color, enabled: cal.enabled,
|
||||||
</div>`
|
sourceLabel: t('cal_local'), remove: { icon: TRASH, title: t('remove_cal') } });
|
||||||
).join('');
|
});
|
||||||
}).join('');
|
state.localCalendars.filter(c => c.owned === false && !c.group).forEach(cal => {
|
||||||
}
|
entries.push({ key: `local:${cal.id}`, source: 'local', dataId: `data-cal-id="${cal.id}"`,
|
||||||
|
name: cal.name, color: cal.color, enabled: cal.enabled,
|
||||||
|
sourceLabel: `${t('shared_with_me')} · ${cal.shared_by || ''}`, remove: null });
|
||||||
|
});
|
||||||
|
state.icalSubscriptions.forEach(sub => {
|
||||||
|
entries.push({ key: `ical:${sub.id}`, source: 'ical', dataId: `data-sub-id="${sub.id}"`,
|
||||||
|
name: sub.name, color: sub.color, enabled: sub.enabled,
|
||||||
|
sourceLabel: t('cal_ical'), remove: { icon: TRASH, title: t('remove_ical_sub') } });
|
||||||
|
});
|
||||||
|
state.googleAccounts.forEach(acc => {
|
||||||
|
(acc.calendars || []).filter(c => !c.sidebar_hidden).forEach(cal => {
|
||||||
|
entries.push({ key: `google:${cal.id}`, source: 'google', dataId: `data-cal-id="${cal.id}"`,
|
||||||
|
name: cal.name, color: cal.color || '#4285f4', enabled: cal.enabled,
|
||||||
|
sourceLabel: acc.email, remove: { icon: EYE_OFF, title: t('hide_cal') } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
state.haAccounts.forEach(acc => {
|
||||||
|
(acc.calendars || []).filter(c => !c.sidebar_hidden).forEach(cal => {
|
||||||
|
entries.push({ key: `homeassistant:${cal.id}`, source: 'homeassistant', dataId: `data-cal-id="${cal.id}"`,
|
||||||
|
name: cal.name, color: cal.color || '#03a9f4', enabled: cal.enabled,
|
||||||
|
sourceLabel: acc.name, remove: { icon: EYE_OFF, title: t('hide_cal') } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── Local calendars: own ones, then a separate "shared with me" group ──
|
// Apply the saved manual order (per device); unknown calendars append at end.
|
||||||
const ownLocal = state.localCalendars.filter(c => c.owned !== false);
|
const order = loadCalOrder();
|
||||||
const sharedLocal = state.localCalendars.filter(c => c.owned === false && !c.group);
|
entries.sort((a, b) => {
|
||||||
|
const ia = order.indexOf(a.key), ib = order.indexOf(b.key);
|
||||||
|
if (ia === -1 && ib === -1) return 0;
|
||||||
|
if (ia === -1) return 1;
|
||||||
|
if (ib === -1) return -1;
|
||||||
|
return ia - ib;
|
||||||
|
});
|
||||||
|
saveCalOrder(entries.map(e => e.key));
|
||||||
|
|
||||||
const renderLocalItem = (cal, withRemove) => {
|
if (!entries.length) {
|
||||||
const removeBtn = withRemove
|
|
||||||
? `<button class="icon-btn mini-btn cal-item-remove" data-cal-id="${cal.id}" data-source="local" title="${t('remove_cal')}">
|
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
||||||
</button>`
|
|
||||||
: '';
|
|
||||||
return `<div class="cal-item" data-cal-id="${cal.id}" data-source="local">
|
|
||||||
<input type="checkbox" ${cal.enabled ? 'checked' : ''} data-cal-id="${cal.id}" data-source="local" />
|
|
||||||
<div class="cal-item-dot" style="background:${cal.color}" data-cal-id="${cal.id}" data-source="local" title="${t('change_color')}"></div>
|
|
||||||
<span class="cal-item-name" data-source="local">${escHtml(cal.name)}</span>
|
|
||||||
${removeBtn}
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ownLocal.length) {
|
|
||||||
html += `<div class="cal-account-name">${t('cal_local')}</div>`;
|
|
||||||
html += ownLocal.map(c => renderLocalItem(c, true)).join('');
|
|
||||||
}
|
|
||||||
if (sharedLocal.length) {
|
|
||||||
html += `<div class="cal-account-name">${t('shared_with_me')}</div>`;
|
|
||||||
html += sharedLocal.map(cal =>
|
|
||||||
`<div class="cal-item" data-cal-id="${cal.id}" data-source="local">
|
|
||||||
<input type="checkbox" ${cal.enabled ? 'checked' : ''} data-cal-id="${cal.id}" data-source="local" />
|
|
||||||
<div class="cal-item-dot" style="background:${cal.color}" data-cal-id="${cal.id}" data-source="local" title="${t('change_color')}"></div>
|
|
||||||
<span class="cal-item-name" data-source="local">${escHtml(cal.name)}</span>
|
|
||||||
<span class="cal-badge cal-badge-shared" title="${t('shared_by', { name: cal.shared_by || '' })}">${escHtml(cal.shared_by || '')}</span>
|
|
||||||
</div>`
|
|
||||||
).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── iCal subscriptions ─────────────────────────────────
|
|
||||||
if (state.icalSubscriptions.length) {
|
|
||||||
html += `<div class="cal-account-name">${t('cal_ical')}</div>`;
|
|
||||||
html += state.icalSubscriptions.map(sub =>
|
|
||||||
`<div class="cal-item" data-sub-id="${sub.id}" data-source="ical">
|
|
||||||
<input type="checkbox" ${sub.enabled ? 'checked' : ''} data-sub-id="${sub.id}" data-source="ical" />
|
|
||||||
<div class="cal-item-dot" style="background:${sub.color}" data-sub-id="${sub.id}" data-source="ical" title="${t('change_color')}"></div>
|
|
||||||
<span class="cal-item-name" data-source="ical">${escHtml(sub.name)}</span>
|
|
||||||
<button class="icon-btn mini-btn cal-item-remove" data-sub-id="${sub.id}" data-source="ical" title="${t('remove_ical_sub')}">
|
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>`
|
|
||||||
).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Google accounts ───────────────────────────────────
|
|
||||||
if (state.googleAccounts.length) {
|
|
||||||
html += state.googleAccounts.map(acc => {
|
|
||||||
const visibleCals = acc.calendars.filter(c => !c.sidebar_hidden);
|
|
||||||
if (!visibleCals.length) return `<div class="cal-account-name">${escHtml(acc.email)}</div>`;
|
|
||||||
return `<div class="cal-account-name">${escHtml(acc.email)}</div>` +
|
|
||||||
visibleCals.map(cal =>
|
|
||||||
`<div class="cal-item" data-cal-id="${cal.id}" data-source="google">
|
|
||||||
<input type="checkbox" ${cal.enabled ? 'checked' : ''} data-cal-id="${cal.id}" data-source="google" />
|
|
||||||
<div class="cal-item-dot" style="background:${cal.color || '#4285f4'}" data-cal-id="${cal.id}" data-source="google" title="${t('change_color')}"></div>
|
|
||||||
<span class="cal-item-name" data-source="google">${escHtml(cal.name)}</span>
|
|
||||||
<button class="icon-btn mini-btn cal-item-remove" data-cal-id="${cal.id}" data-source="google" title="${t('hide_cal')}">
|
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>`
|
|
||||||
).join('');
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Home Assistant accounts ───────────────────────────
|
|
||||||
if (state.haAccounts.length) {
|
|
||||||
html += state.haAccounts.map(acc => {
|
|
||||||
const visibleCals = acc.calendars.filter(c => !c.sidebar_hidden);
|
|
||||||
if (!visibleCals.length) return `<div class="cal-account-name">${escHtml(acc.name)}</div>`;
|
|
||||||
return `<div class="cal-account-name">${escHtml(acc.name)}</div>` +
|
|
||||||
visibleCals.map(cal =>
|
|
||||||
`<div class="cal-item" data-cal-id="${cal.id}" data-source="homeassistant">
|
|
||||||
<input type="checkbox" ${cal.enabled ? 'checked' : ''} data-cal-id="${cal.id}" data-source="homeassistant" />
|
|
||||||
<div class="cal-item-dot" style="background:${cal.color || '#03a9f4'}" data-cal-id="${cal.id}" data-source="homeassistant" title="${t('change_color')}"></div>
|
|
||||||
<span class="cal-item-name" data-source="homeassistant">${escHtml(cal.name)}</span>
|
|
||||||
<button class="icon-btn mini-btn cal-item-remove" data-cal-id="${cal.id}" data-source="homeassistant" title="${t('hide_cal')}">
|
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>`
|
|
||||||
).join('');
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!html) {
|
|
||||||
container.innerHTML = `<div style="padding:8px 16px;font-size:12px;color:var(--text-3)">${t('error_no_calendars')}</div>`;
|
container.innerHTML = `<div style="padding:8px 16px;font-size:12px;color:var(--text-3)">${t('error_no_calendars')}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = entries.map(e =>
|
||||||
|
`<div class="cal-item" draggable="true" data-key="${e.key}" data-source="${e.source}" ${e.dataId}>
|
||||||
|
<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>
|
||||||
|
<span class="cal-item-name" data-source="${e.source}">${escHtml(e.name)}</span>
|
||||||
|
<span class="cal-source">${escHtml(e.sourceLabel)}</span>
|
||||||
|
${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>` : ''}
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
bindCalDragReorder(container);
|
||||||
|
|
||||||
// ── Checkbox handlers ──────────────────────────────────
|
// ── Checkbox handlers ──────────────────────────────────
|
||||||
container.querySelectorAll('input[type=checkbox]').forEach(cb => {
|
container.querySelectorAll('input[type=checkbox]').forEach(cb => {
|
||||||
@@ -2304,7 +2306,7 @@ function renderGroupMemberPicker() {
|
|||||||
? dir.map(u => {
|
? dir.map(u => {
|
||||||
const on = picked.has(u.id);
|
const on = picked.has(u.id);
|
||||||
return `<div class="pick-row ${on ? 'pick-row-sel' : ''}" data-member-id="${u.id}" role="checkbox" aria-checked="${on}">
|
return `<div class="pick-row ${on ? 'pick-row-sel' : ''}" data-member-id="${u.id}" role="checkbox" aria-checked="${on}">
|
||||||
<span class="pick-mark">${on ? '☑' : '☐'}</span>
|
<span class="pick-mark pick-check ${on ? 'on' : ''}">${on ? '✓' : ''}</span>
|
||||||
<span class="pick-name">${escHtml(u.display_name || '')}</span>
|
<span class="pick-name">${escHtml(u.display_name || '')}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('')
|
}).join('')
|
||||||
@@ -2381,7 +2383,7 @@ function renderGroupVisibleList(selectedId) {
|
|||||||
? `<span class="pick-dot" style="background:${color}"></span>`
|
? `<span class="pick-dot" style="background:${color}"></span>`
|
||||||
: `<span class="pick-dot pick-dot-empty"></span>`;
|
: `<span class="pick-dot pick-dot-empty"></span>`;
|
||||||
return `<div class="pick-row ${sel ? 'pick-row-sel' : ''}" data-pick="${val}" role="radio" aria-checked="${sel}">
|
return `<div class="pick-row ${sel ? 'pick-row-sel' : ''}" data-pick="${val}" role="radio" aria-checked="${sel}">
|
||||||
<span class="pick-mark">${sel ? '●' : '○'}</span>
|
<span class="pick-mark pick-radio ${sel ? 'on' : ''}"></span>
|
||||||
${dot}
|
${dot}
|
||||||
<span class="pick-name">${escHtml(name)}</span>
|
<span class="pick-name">${escHtml(name)}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ const translations = {
|
|||||||
settings_group_visible: 'Für Gruppen sichtbarer Kalender',
|
settings_group_visible: 'Für Gruppen sichtbarer Kalender',
|
||||||
settings_group_visible_desc: 'Wähle, welcher deiner Kalender für deine Gruppenmitglieder sichtbar ist',
|
settings_group_visible_desc: 'Wähle, welcher deiner Kalender für deine Gruppenmitglieder sichtbar ist',
|
||||||
group_visible_none: 'Keiner',
|
group_visible_none: 'Keiner',
|
||||||
|
drag_reorder: 'Zum Sortieren ziehen',
|
||||||
settings_hour_height: 'Stundenhöhe (Wochen- & Tagesansicht)',
|
settings_hour_height: 'Stundenhöhe (Wochen- & Tagesansicht)',
|
||||||
settings_hour_height_desc: 'Wie viel Platz eine Stunde in der Zeitrasteransicht einnimmt',
|
settings_hour_height_desc: 'Wie viel Platz eine Stunde in der Zeitrasteransicht einnimmt',
|
||||||
hour_compact: 'Kompakt', hour_normal: 'Normal',
|
hour_compact: 'Kompakt', hour_normal: 'Normal',
|
||||||
@@ -385,6 +386,7 @@ const translations = {
|
|||||||
settings_group_visible: 'Calendar visible to groups',
|
settings_group_visible: 'Calendar visible to groups',
|
||||||
settings_group_visible_desc: 'Choose which of your calendars your group members can see',
|
settings_group_visible_desc: 'Choose which of your calendars your group members can see',
|
||||||
group_visible_none: 'None',
|
group_visible_none: 'None',
|
||||||
|
drag_reorder: 'Drag to reorder',
|
||||||
settings_hour_height: 'Hour height (week & day view)',
|
settings_hour_height: 'Hour height (week & day view)',
|
||||||
settings_hour_height_desc: 'How much space one hour takes in the time grid',
|
settings_hour_height_desc: 'How much space one hour takes in the time grid',
|
||||||
hour_compact: 'Compact', hour_normal: 'Normal',
|
hour_compact: 'Compact', hour_normal: 'Normal',
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// Increment APP_VERSION with every code change
|
// Increment APP_VERSION with every code change
|
||||||
export const APP_VERSION = 'v27';
|
export const APP_VERSION = 'v28';
|
||||||
|
|||||||
Reference in New Issue
Block a user