feat: Web-Frontend – Sharing, iCal Import/Export, Ersteller & Privat-Flag
- Ersteller-Zeile im Event-Popup (nur wenn Ersteller != aktueller User). - Privat-Toggle im Event-Editor (nur lokale Kalender) + Sichtbarkeits- Auswahl (hidden|busy) in den Einstellungen. - Lokale Kalender in Settings & Sidebar: Teilen/Importieren/Exportieren- Aktionen (nur eigene; geteilte mit "geteilt von"-Badge, kein Loeschen). - Share-Modal: Benutzerverzeichnis mit Suche, read/read_write, Freigaben entfernen. - api.js: download()-Helper fuer iCal-Export (Blob). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1764,3 +1764,40 @@ a { color: var(--primary); text-decoration: none; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ── Collaboration: sharing badges & user picker ───────────── */
|
||||
.cal-badge {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-2);
|
||||
border: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cal-badge-shared {
|
||||
background: rgba(66, 133, 244, 0.15);
|
||||
color: var(--primary);
|
||||
border-color: transparent;
|
||||
}
|
||||
.share-user-picker {
|
||||
margin-top: 8px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.share-user-item {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.share-user-item:last-child { border-bottom: none; }
|
||||
.share-user-item:hover { background: var(--bg-surface); }
|
||||
.popup-creator {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-2);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -319,6 +319,11 @@
|
||||
<label>Kalender</label>
|
||||
<select id="ev-calendar"></select>
|
||||
</div>
|
||||
<div class="form-row" id="ev-private-row" style="display:none">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="ev-private" /> <span data-i18n="event_private">Privat</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ort</label>
|
||||
<input type="text" id="ev-location" placeholder="Ort hinzufügen" />
|
||||
@@ -344,6 +349,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Share Calendar Modal -->
|
||||
<div id="modal-share" class="modal-overlay hidden">
|
||||
<div class="modal-card" style="max-width:480px">
|
||||
<div class="modal-header">
|
||||
<h3 data-i18n="share_title">Kalender teilen</h3>
|
||||
<button class="icon-btn modal-close" data-modal="modal-share">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h4 class="panel-title" data-i18n="share_current">Aktuelle Freigaben</h4>
|
||||
<div id="share-current-list" class="accounts-list"></div>
|
||||
|
||||
<h4 class="panel-title" style="margin-top:20px" data-i18n="share_add">Benutzer hinzufügen</h4>
|
||||
<div class="form-row" style="gap:8px;align-items:center">
|
||||
<input type="text" id="share-user-search" data-i18n-placeholder="share_search" placeholder="Benutzer suchen…" style="flex:1" />
|
||||
<select id="share-permission">
|
||||
<option value="read" data-i18n="perm_read">Nur lesen</option>
|
||||
<option value="read_write" data-i18n="perm_read_write">Lesen & schreiben</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="share-user-picker" class="share-user-picker"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn btn-primary" data-modal="modal-share" data-i18n="done">Fertig</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirm Modal -->
|
||||
<div id="modal-delete-confirm" class="modal-overlay hidden">
|
||||
<div class="modal-card" style="max-width:400px">
|
||||
@@ -395,6 +428,7 @@
|
||||
<div class="popup-location" id="popup-location"></div>
|
||||
<div class="popup-description" id="popup-description"></div>
|
||||
<div class="popup-calendar" id="popup-calendar"></div>
|
||||
<div class="popup-creator" id="popup-creator" style="display:none"></div>
|
||||
</div>
|
||||
<div id="popup-copy-menu" class="popup-copy-menu hidden"></div>
|
||||
</div>
|
||||
@@ -672,6 +706,16 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_privacy">Privatsphäre</h4>
|
||||
<p class="panel-desc" data-i18n="settings_private_visibility_desc">Wie private Termine für andere Gruppenmitglieder erscheinen</p>
|
||||
<div class="form-group">
|
||||
<label data-i18n="settings_private_visibility">Private Termine für Gruppenmitglieder</label>
|
||||
<select id="cfg-private-visibility">
|
||||
<option value="busy" data-i18n="private_visibility_busy">Als „Beschäftigt“ anzeigen</option>
|
||||
<option value="hidden" data-i18n="private_visibility_hidden">Ausblenden</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_hour_height">Stundenhöhe (Wochen- & Tagesansicht)</h4>
|
||||
<p class="panel-desc" data-i18n="settings_hour_height_desc">Wie viel Platz eine Stunde in der Zeitrasteransicht einnimmt</p>
|
||||
<div class="contrast-selector" id="cfg-hour-height" data-setting="hour_height">
|
||||
|
||||
@@ -57,12 +57,46 @@ async function uploadRequest(path, formData) {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function downloadRequest(path, fallbackName) {
|
||||
const token = localStorage.getItem('token');
|
||||
const headers = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`${BASE}${path}`, { method: 'GET', headers });
|
||||
if (res.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.reload();
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ detail: t('unknown_error') }));
|
||||
throw new Error(err.detail || `HTTP ${res.status}`);
|
||||
}
|
||||
// Derive filename from Content-Disposition if present.
|
||||
let filename = fallbackName || 'calendar.ics';
|
||||
const cd = res.headers.get('Content-Disposition') || '';
|
||||
const m = cd.match(/filename="?([^"]+)"?/);
|
||||
if (m) filename = m[1];
|
||||
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: (path) => request('GET', path),
|
||||
post: (path, body) => request('POST', path, body),
|
||||
put: (path, body) => request('PUT', path, body),
|
||||
delete: (path) => request('DELETE', path),
|
||||
upload: (path, form) => uploadRequest(path, form),
|
||||
download: (path, name) => downloadRequest(path, name),
|
||||
|
||||
login: (username, password, totp_code = null, remember_me = false) =>
|
||||
request('POST', '/auth/login', { username, password, totp_code, remember_me }),
|
||||
|
||||
@@ -538,19 +538,28 @@ function renderCalendarList() {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Local calendars ────────────────────────────────────
|
||||
// ── Local calendars (own + shared with me) ─────────────
|
||||
if (state.localCalendars.length) {
|
||||
html += `<div class="cal-account-name">${t('cal_local')}</div>`;
|
||||
html += state.localCalendars.map(cal =>
|
||||
`<div class="cal-item" data-cal-id="${cal.id}" data-source="local">
|
||||
html += state.localCalendars.map(cal => {
|
||||
const owned = cal.owned !== false;
|
||||
// Shared calendars get an owner badge and no delete button (owner-only).
|
||||
const sharedBadge = !owned
|
||||
? `<span class="cal-badge cal-badge-shared" title="${t('shared_by', { name: cal.shared_by || '' })}">${escHtml(cal.shared_by || '')}</span>`
|
||||
: '';
|
||||
const removeBtn = owned
|
||||
? `<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>
|
||||
<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>
|
||||
</div>`
|
||||
).join('');
|
||||
${sharedBadge}
|
||||
${removeBtn}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── iCal subscriptions ─────────────────────────────────
|
||||
@@ -1203,6 +1212,16 @@ function showEventPopup(ev, anchor) {
|
||||
document.getElementById('popup-description').style.display = ev.description ? '' : 'none';
|
||||
document.getElementById('popup-calendar').textContent = ev.calendar_name || '';
|
||||
|
||||
// Creator — only shown when it isn't the current user.
|
||||
const creatorEl = document.getElementById('popup-creator');
|
||||
const me = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
if (ev.creator && ev.creator.display_name && ev.creator.id !== me.id) {
|
||||
creatorEl.textContent = t('created_by', { name: ev.creator.display_name });
|
||||
creatorEl.style.display = '';
|
||||
} else {
|
||||
creatorEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// Position near anchor
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const pw = 300, ph = 200;
|
||||
@@ -1375,6 +1394,7 @@ function openNewEventModal(date) {
|
||||
|
||||
toggleAlldayFields(false);
|
||||
populateCalendarSelect(null);
|
||||
updatePrivateRow(false);
|
||||
resetColorPicker('');
|
||||
resetRecurrenceUI();
|
||||
document.getElementById('ev-delete').classList.add('hidden');
|
||||
@@ -1410,6 +1430,7 @@ function openCopyEditModal(ev, targetCal) {
|
||||
if (targetCal.type === 'caldav') selectedId = targetCal.id;
|
||||
else selectedId = `${targetCal.type}-${targetCal.id}`;
|
||||
populateCalendarSelect(selectedId);
|
||||
updatePrivateRow(ev.private);
|
||||
|
||||
resetColorPicker(ev.color || '');
|
||||
resetRecurrenceUI();
|
||||
@@ -1442,6 +1463,7 @@ function openEditEventModal(ev) {
|
||||
}
|
||||
|
||||
populateCalendarSelect(ev.calendar_id);
|
||||
updatePrivateRow(ev.private);
|
||||
resetColorPicker(ev.color || '');
|
||||
|
||||
// Recurrence
|
||||
@@ -1469,6 +1491,18 @@ function toggleAlldayFields(allDay) {
|
||||
document.getElementById('ev-date-row').style.display = allDay ? '' : 'none';
|
||||
}
|
||||
|
||||
// The "Privat" toggle only applies to local calendars; hide it otherwise.
|
||||
function updatePrivateRow(isPrivate) {
|
||||
const calVal = document.getElementById('ev-calendar').value || '';
|
||||
const isLocal = calVal.startsWith('local-');
|
||||
const row = document.getElementById('ev-private-row');
|
||||
row.style.display = isLocal ? '' : 'none';
|
||||
if (isPrivate !== undefined) {
|
||||
document.getElementById('ev-private').checked = !!isPrivate;
|
||||
}
|
||||
if (!isLocal) document.getElementById('ev-private').checked = false;
|
||||
}
|
||||
|
||||
function resetColorPicker(color) {
|
||||
state.selectedEventColor = color;
|
||||
const hex = document.getElementById('ev-color-hex');
|
||||
@@ -1559,6 +1593,9 @@ function bindEventModal() {
|
||||
toggleAlldayFields(e.target.checked);
|
||||
});
|
||||
|
||||
// The "Privat" toggle is only relevant for local calendars.
|
||||
document.getElementById('ev-calendar').addEventListener('change', () => updatePrivateRow());
|
||||
|
||||
// Date/time pickers with auto-adjustment logic
|
||||
[
|
||||
{ displayId: 'ev-start-display', inputId: 'ev-start', mode: 'datetime', role: 'start' },
|
||||
@@ -1684,6 +1721,7 @@ function bindEventModal() {
|
||||
const desc = document.getElementById('ev-description').value.trim();
|
||||
const color = state.selectedEventColor;
|
||||
const rrule = buildRruleFromUI();
|
||||
const isPrivate = isLocal && document.getElementById('ev-private').checked;
|
||||
|
||||
let start, end;
|
||||
if (allDay) {
|
||||
@@ -1711,7 +1749,7 @@ function bindEventModal() {
|
||||
);
|
||||
} else if (ev.source === 'local') {
|
||||
await api.put(`/local/events/${encodeURIComponent(ev.id)}`,
|
||||
{ title, start, end, allDay, location: loc, description: desc, color: color || null, rrule: rrule || '' }
|
||||
{ title, start, end, allDay, location: loc, description: desc, color: color || null, rrule: rrule || '', private: isPrivate }
|
||||
);
|
||||
} else if (ev.source === 'ical') {
|
||||
showToast(t('event_readonly'), true);
|
||||
@@ -1733,6 +1771,7 @@ function bindEventModal() {
|
||||
location: loc, description: desc,
|
||||
color: color || null,
|
||||
rrule: rrule || null,
|
||||
private: ev.source === 'local' ? isPrivate : ev.private,
|
||||
});
|
||||
showToast(t('event_updated'));
|
||||
} else if (isGoogle) {
|
||||
@@ -1747,7 +1786,7 @@ function bindEventModal() {
|
||||
await api.post('/local/events', {
|
||||
calendar_id: calId, title, start, end, allDay,
|
||||
location: loc, description: desc, color: color || null,
|
||||
rrule: rrule || null,
|
||||
rrule: rrule || null, private: isPrivate,
|
||||
});
|
||||
showToast(t('event_created'));
|
||||
} else if (isHA) {
|
||||
@@ -2025,6 +2064,100 @@ function bindICalSubModal() {
|
||||
};
|
||||
}
|
||||
|
||||
// ── iCal Import ───────────────────────────────────────────
|
||||
// Open a file picker and import the chosen .ics into the given local calendar.
|
||||
function triggerIcsImport(calendarId) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.ics,text/calendar';
|
||||
input.style.display = 'none';
|
||||
document.body.appendChild(input);
|
||||
input.addEventListener('change', async () => {
|
||||
const file = input.files && input.files[0];
|
||||
input.remove();
|
||||
if (!file) return;
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
try {
|
||||
showToast(t('importing'));
|
||||
const res = await api.upload(`/local/calendars/${calendarId}/import`, form);
|
||||
showToast(t('import_result', { imported: res.imported, skipped: res.skipped }));
|
||||
fetchAndRender(true);
|
||||
} catch (e) { showToast(e.message, true); }
|
||||
});
|
||||
input.click();
|
||||
}
|
||||
|
||||
// ── Sharing ───────────────────────────────────────────────
|
||||
async function openShareModal(calendarId) {
|
||||
const modal = document.getElementById('modal-share');
|
||||
modal.dataset.calId = String(calendarId);
|
||||
const search = document.getElementById('share-user-search');
|
||||
search.value = '';
|
||||
search.oninput = renderShareUserPicker;
|
||||
document.getElementById('share-permission').value = 'read';
|
||||
openModal('modal-share');
|
||||
await refreshShareModal(calendarId);
|
||||
}
|
||||
|
||||
async function refreshShareModal(calendarId) {
|
||||
// Load current shares + the user directory (for the picker).
|
||||
let shares = [], users = [];
|
||||
try { shares = await api.get(`/local/calendars/${calendarId}/shares`); } catch (e) { showToast(e.message, true); }
|
||||
try { users = await api.get('/users/directory'); } catch (e) { /* ignore */ }
|
||||
|
||||
const sharedIds = new Set(shares.map(s => s.user_id));
|
||||
const listEl = document.getElementById('share-current-list');
|
||||
listEl.innerHTML = shares.length
|
||||
? shares.map(s =>
|
||||
`<div class="accounts-row">
|
||||
<span class="accounts-row-name">${escHtml(s.display_name || '')}</span>
|
||||
<div class="accounts-row-actions">
|
||||
<span class="cal-badge">${s.permission === 'read_write' ? t('perm_read_write') : t('perm_read')}</span>
|
||||
<button class="btn btn-ghost btn-sm" data-share-remove="${s.user_id}">${t('remove')}</button>
|
||||
</div>
|
||||
</div>`
|
||||
).join('')
|
||||
: `<span class="accounts-section-empty">${t('share_none')}</span>`;
|
||||
|
||||
listEl.querySelectorAll('[data-share-remove]').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
try {
|
||||
await api.delete(`/local/calendars/${calendarId}/shares/${btn.dataset.shareRemove}`);
|
||||
await refreshShareModal(calendarId);
|
||||
} catch (e) { showToast(e.message, true); }
|
||||
});
|
||||
});
|
||||
|
||||
// Store the directory (minus already-shared users) for the picker.
|
||||
document.getElementById('modal-share').__users = users.filter(u => !sharedIds.has(u.id));
|
||||
renderShareUserPicker();
|
||||
}
|
||||
|
||||
function renderShareUserPicker() {
|
||||
const modal = document.getElementById('modal-share');
|
||||
const users = modal.__users || [];
|
||||
const q = (document.getElementById('share-user-search').value || '').toLowerCase();
|
||||
const filtered = users.filter(u => (u.display_name || '').toLowerCase().includes(q));
|
||||
const picker = document.getElementById('share-user-picker');
|
||||
picker.innerHTML = filtered.length
|
||||
? filtered.map(u =>
|
||||
`<div class="share-user-item" data-user-id="${u.id}">${escHtml(u.display_name || '')}</div>`
|
||||
).join('')
|
||||
: `<span class="accounts-section-empty">${t('share_no_users')}</span>`;
|
||||
picker.querySelectorAll('.share-user-item').forEach(el => {
|
||||
el.addEventListener('click', async () => {
|
||||
const calId = parseInt(modal.dataset.calId);
|
||||
const permission = document.getElementById('share-permission').value;
|
||||
try {
|
||||
await api.post(`/local/calendars/${calId}/shares`,
|
||||
{ user_id: parseInt(el.dataset.userId), permission });
|
||||
await refreshShareModal(calId);
|
||||
} catch (e) { showToast(e.message, true); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Settings Modal ────────────────────────────────────────
|
||||
function openSettingsModal() {
|
||||
const s = state.settings;
|
||||
@@ -2057,6 +2190,7 @@ function openSettingsModal() {
|
||||
});
|
||||
document.getElementById('cfg-dim-past').checked = !!s.dim_past_events;
|
||||
document.getElementById('cfg-language').value = getLang();
|
||||
document.getElementById('cfg-private-visibility').value = s.private_event_visibility || 'busy';
|
||||
|
||||
// Set active contrast/hour-height buttons
|
||||
[
|
||||
@@ -2189,14 +2323,40 @@ function renderAllAccounts() {
|
||||
if (!state.localCalendars.length) {
|
||||
localList.innerHTML = `<span class="accounts-section-empty">${t('settings_no_local_cals')}</span>`;
|
||||
} else {
|
||||
localList.innerHTML = state.localCalendars.map(cal =>
|
||||
`<div class="accounts-row">
|
||||
localList.innerHTML = state.localCalendars.map(cal => {
|
||||
const owned = cal.owned !== false;
|
||||
const sharedBadge = !owned
|
||||
? `<span class="cal-badge cal-badge-shared">${t('shared_by', { name: cal.shared_by || '' })}</span>`
|
||||
: '';
|
||||
const canWrite = owned || cal.permission === 'read_write';
|
||||
const actions = [];
|
||||
if (owned) actions.push(`<button class="btn btn-ghost btn-sm" data-local-share="${cal.id}">${t('share')}</button>`);
|
||||
if (canWrite) actions.push(`<button class="btn btn-ghost btn-sm" data-local-import="${cal.id}">${t('import')}</button>`);
|
||||
actions.push(`<button class="btn btn-ghost btn-sm" data-local-export="${cal.id}" data-local-name="${escHtml(cal.name)}">${t('export')}</button>`);
|
||||
return `<div class="accounts-row">
|
||||
<div style="display:flex;align-items:center;gap:8px;min-width:0">
|
||||
<span class="accounts-local-dot" style="background:${cal.color || '#34a853'}"></span>
|
||||
<span class="accounts-row-name">${escHtml(cal.name)}</span>
|
||||
${sharedBadge}
|
||||
</div>
|
||||
</div>`
|
||||
).join('');
|
||||
<div class="accounts-row-actions">${actions.join('')}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
localList.querySelectorAll('[data-local-export]').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
try {
|
||||
await api.download(`/local/calendars/${btn.dataset.localExport}/export`,
|
||||
`${btn.dataset.localName || 'calendar'}.ics`);
|
||||
} catch (e) { showToast(e.message, true); }
|
||||
});
|
||||
});
|
||||
localList.querySelectorAll('[data-local-import]').forEach(btn => {
|
||||
btn.addEventListener('click', () => triggerIcsImport(parseInt(btn.dataset.localImport)));
|
||||
});
|
||||
localList.querySelectorAll('[data-local-share]').forEach(btn => {
|
||||
btn.addEventListener('click', () => openShareModal(parseInt(btn.dataset.localShare)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2509,6 +2669,7 @@ function bindSettingsModal() {
|
||||
dim_past_events: document.getElementById('cfg-dim-past').checked,
|
||||
hour_height: getActive('cfg-hour-height') || 44,
|
||||
language: document.getElementById('cfg-language').value,
|
||||
private_event_visibility: document.getElementById('cfg-private-visibility').value,
|
||||
};
|
||||
try {
|
||||
await api.put('/settings/', settings);
|
||||
|
||||
@@ -84,6 +84,29 @@ const translations = {
|
||||
settings_week_start: 'Erster Wochentag',
|
||||
week_start_monday: 'Montag', week_start_sunday: 'Sonntag',
|
||||
settings_dim_past: 'Vergangene Termine ausgrauen',
|
||||
settings_privacy: 'Privatsphäre',
|
||||
settings_private_visibility: 'Private Termine für Gruppenmitglieder',
|
||||
settings_private_visibility_desc: 'Wie private Termine für andere Gruppenmitglieder erscheinen',
|
||||
private_visibility_busy: 'Als „Beschäftigt“ anzeigen',
|
||||
private_visibility_hidden: 'Ausblenden',
|
||||
created_by: 'Erstellt von: {name}',
|
||||
event_private: 'Privat',
|
||||
share: 'Teilen',
|
||||
import: 'Importieren',
|
||||
export: 'Exportieren',
|
||||
importing: 'Importiere…',
|
||||
import_result: '{imported} importiert, {skipped} übersprungen',
|
||||
shared_by: 'geteilt von {name}',
|
||||
share_title: 'Kalender teilen',
|
||||
share_current: 'Aktuelle Freigaben',
|
||||
share_add: 'Benutzer hinzufügen',
|
||||
share_search: 'Benutzer suchen…',
|
||||
share_none: 'Noch nicht geteilt',
|
||||
share_no_users: 'Keine Benutzer gefunden',
|
||||
perm_read: 'Nur lesen',
|
||||
perm_read_write: 'Lesen & schreiben',
|
||||
remove: 'Entfernen',
|
||||
done: 'Fertig',
|
||||
settings_hour_height: 'Stundenhöhe (Wochen- & Tagesansicht)',
|
||||
settings_hour_height_desc: 'Wie viel Platz eine Stunde in der Zeitrasteransicht einnimmt',
|
||||
hour_compact: 'Kompakt', hour_normal: 'Normal',
|
||||
@@ -299,6 +322,29 @@ const translations = {
|
||||
settings_week_start: 'First day of week',
|
||||
week_start_monday: 'Monday', week_start_sunday: 'Sunday',
|
||||
settings_dim_past: 'Dim past events',
|
||||
settings_privacy: 'Privacy',
|
||||
settings_private_visibility: 'Private events for group members',
|
||||
settings_private_visibility_desc: 'How your private events appear to other group members',
|
||||
private_visibility_busy: 'Show as "Busy"',
|
||||
private_visibility_hidden: 'Hide completely',
|
||||
created_by: 'Created by: {name}',
|
||||
event_private: 'Private',
|
||||
share: 'Share',
|
||||
import: 'Import',
|
||||
export: 'Export',
|
||||
importing: 'Importing…',
|
||||
import_result: '{imported} imported, {skipped} skipped',
|
||||
shared_by: 'shared by {name}',
|
||||
share_title: 'Share calendar',
|
||||
share_current: 'Current shares',
|
||||
share_add: 'Add user',
|
||||
share_search: 'Search users…',
|
||||
share_none: 'Not shared yet',
|
||||
share_no_users: 'No users found',
|
||||
perm_read: 'Read only',
|
||||
perm_read_write: 'Read & write',
|
||||
remove: 'Remove',
|
||||
done: 'Done',
|
||||
settings_hour_height: 'Hour height (week & day view)',
|
||||
settings_hour_height_desc: 'How much space one hour takes in the time grid',
|
||||
hour_compact: 'Compact', hour_normal: 'Normal',
|
||||
|
||||
Reference in New Issue
Block a user