Einige kleine verbesserungen #1

Open
Scarriffle wants to merge 115 commits from beta into master
5 changed files with 181 additions and 130 deletions
Showing only changes of commit 8d2f487607 - Show all commits

View File

@@ -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)

View File

@@ -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; }

View File

@@ -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>`;

View File

@@ -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',

View File

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