feat(settings): add time format preference
This commit is contained in:
@@ -9,7 +9,10 @@ const SUPPORTED_LOCALES = ['de', 'en', 'es', 'fr', 'it', 'sv', 'el', 'ru', 'tr',
|
|||||||
const DEFAULT_LOCALE = 'de';
|
const DEFAULT_LOCALE = 'de';
|
||||||
const STORAGE_KEY = 'oikos-locale';
|
const STORAGE_KEY = 'oikos-locale';
|
||||||
const DATE_FORMAT_KEY = 'oikos-date-format';
|
const DATE_FORMAT_KEY = 'oikos-date-format';
|
||||||
|
const TIME_FORMAT_KEY = 'oikos-time-format';
|
||||||
const DEFAULT_DATE_FORMAT = 'dmy';
|
const DEFAULT_DATE_FORMAT = 'dmy';
|
||||||
|
const DEFAULT_TIME_FORMAT = '24h';
|
||||||
|
const VALID_TIME_FORMATS = ['24h', '12h'];
|
||||||
|
|
||||||
let currentLocale = DEFAULT_LOCALE;
|
let currentLocale = DEFAULT_LOCALE;
|
||||||
let translations = {};
|
let translations = {};
|
||||||
@@ -93,6 +96,15 @@ export function getDateFormat() {
|
|||||||
return getDateFormatPreference();
|
return getDateFormatPreference();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTimeFormatPreference() {
|
||||||
|
const stored = localStorage.getItem(TIME_FORMAT_KEY);
|
||||||
|
return VALID_TIME_FORMATS.includes(stored) ? stored : DEFAULT_TIME_FORMAT;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimeFormat() {
|
||||||
|
return getTimeFormatPreference();
|
||||||
|
}
|
||||||
|
|
||||||
function formatDateParts(date, useUtc = false) {
|
function formatDateParts(date, useUtc = false) {
|
||||||
const d = date instanceof Date ? date : new Date(date);
|
const d = date instanceof Date ? date : new Date(date);
|
||||||
if (isNaN(d.getTime())) return '';
|
if (isNaN(d.getTime())) return '';
|
||||||
@@ -176,9 +188,78 @@ export function formatTime(date) {
|
|||||||
if (date == null) return '';
|
if (date == null) return '';
|
||||||
const d = date instanceof Date ? date : new Date(date);
|
const d = date instanceof Date ? date : new Date(date);
|
||||||
if (isNaN(d.getTime())) return '';
|
if (isNaN(d.getTime())) return '';
|
||||||
|
if (getTimeFormatPreference() === '12h') {
|
||||||
|
const hour = d.getHours();
|
||||||
|
const minute = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
const displayHour = hour % 12 || 12;
|
||||||
|
return `${displayHour}:${minute} ${hour >= 12 ? 'PM' : 'AM'}`;
|
||||||
|
}
|
||||||
return new Intl.DateTimeFormat(currentLocale, {
|
return new Intl.DateTimeFormat(currentLocale, {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
hour12: false,
|
hour12: false,
|
||||||
}).format(d);
|
}).format(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toTimeParts(value) {
|
||||||
|
if (value == null || value === '') return null;
|
||||||
|
|
||||||
|
if (value instanceof Date) {
|
||||||
|
if (isNaN(value.getTime())) return null;
|
||||||
|
return { hour: value.getHours(), minute: value.getMinutes() };
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = String(value).trim();
|
||||||
|
if (!raw) return null;
|
||||||
|
|
||||||
|
if (/^\d{1,2}:\d{2}$/.test(raw)) {
|
||||||
|
const [hour, minute] = raw.split(':').map(Number);
|
||||||
|
if (hour >= 0 && hour < 24 && minute >= 0 && minute < 60) {
|
||||||
|
return { hour, minute };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ampmMatch = raw.match(/^(\d{1,2})(?::(\d{2}))?\s*([ap]m)$/i);
|
||||||
|
if (ampmMatch) {
|
||||||
|
let hour = Number(ampmMatch[1]);
|
||||||
|
const minute = Number(ampmMatch[2] ?? 0);
|
||||||
|
const meridiem = ampmMatch[3].toLowerCase();
|
||||||
|
if (!Number.isInteger(hour) || !Number.isInteger(minute) || minute < 0 || minute >= 60) return null;
|
||||||
|
if (hour < 1 || hour > 12) return null;
|
||||||
|
if (meridiem === 'pm' && hour !== 12) hour += 12;
|
||||||
|
if (meridiem === 'am' && hour === 12) hour = 0;
|
||||||
|
return { hour, minute };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTimeInput(value) {
|
||||||
|
const parts = toTimeParts(value);
|
||||||
|
if (!parts) return '';
|
||||||
|
const hour = String(parts.hour).padStart(2, '0');
|
||||||
|
const minute = String(parts.minute).padStart(2, '0');
|
||||||
|
if (getTimeFormatPreference() === '12h') {
|
||||||
|
const isPm = parts.hour >= 12;
|
||||||
|
const displayHour = parts.hour % 12 || 12;
|
||||||
|
return `${displayHour}:${minute} ${isPm ? 'PM' : 'AM'}`;
|
||||||
|
}
|
||||||
|
return `${hour}:${minute}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseTimeInput(value) {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
if (!raw) return '';
|
||||||
|
const parts = toTimeParts(raw);
|
||||||
|
if (!parts) return '';
|
||||||
|
return `${String(parts.hour).padStart(2, '0')}:${String(parts.minute).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTimeInputValid(value) {
|
||||||
|
return !String(value || '').trim() || !!parseTimeInput(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timeInputPlaceholder() {
|
||||||
|
return getTimeFormatPreference() === '12h' ? 'h:mm AM/PM' : 'HH:MM';
|
||||||
|
}
|
||||||
|
|||||||
@@ -632,6 +632,9 @@
|
|||||||
"dateFormatLabel": "Bevorzugtes Datumsformat",
|
"dateFormatLabel": "Bevorzugtes Datumsformat",
|
||||||
"dateFormatHint": "Wähle, wie Daten in der App angezeigt werden.",
|
"dateFormatHint": "Wähle, wie Daten in der App angezeigt werden.",
|
||||||
"dateFormatSavedToast": "Datumsformat gespeichert.",
|
"dateFormatSavedToast": "Datumsformat gespeichert.",
|
||||||
|
"timeFormatLabel": "Zeitformat",
|
||||||
|
"timeFormatHours": "Stunden",
|
||||||
|
"timeFormatSavedToast": "Zeitformat gespeichert.",
|
||||||
"themeSystem": "System",
|
"themeSystem": "System",
|
||||||
"themeSysLabel": "System-Einstellung verwenden",
|
"themeSysLabel": "System-Einstellung verwenden",
|
||||||
"themeLight": "Hell",
|
"themeLight": "Hell",
|
||||||
|
|||||||
@@ -607,6 +607,9 @@
|
|||||||
"dateFormatLabel": "Preferred date format",
|
"dateFormatLabel": "Preferred date format",
|
||||||
"dateFormatHint": "Choose how dates are displayed throughout the app.",
|
"dateFormatHint": "Choose how dates are displayed throughout the app.",
|
||||||
"dateFormatSavedToast": "Date format saved.",
|
"dateFormatSavedToast": "Date format saved.",
|
||||||
|
"timeFormatLabel": "Time format",
|
||||||
|
"timeFormatHours": "hours",
|
||||||
|
"timeFormatSavedToast": "Time format saved.",
|
||||||
"themeSystem": "System",
|
"themeSystem": "System",
|
||||||
"themeSysLabel": "Use system setting",
|
"themeSysLabel": "Use system setting",
|
||||||
"themeLight": "Light",
|
"themeLight": "Light",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { api } from '/api.js';
|
|||||||
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
||||||
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
|
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
|
||||||
import { stagger } from '/utils/ux.js';
|
import { stagger } from '/utils/ux.js';
|
||||||
import { t, formatDate as formatPreferredDate, formatTime, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid } from '/i18n.js';
|
import { t, formatDate as formatPreferredDate, formatTime, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid, formatTimeInput, parseTimeInput, timeInputPlaceholder } from '/i18n.js';
|
||||||
import { esc, fmtLocation } from '/utils/html.js';
|
import { esc, fmtLocation } from '/utils/html.js';
|
||||||
import { refresh as refreshReminders } from '/reminders.js';
|
import { refresh as refreshReminders } from '/reminders.js';
|
||||||
|
|
||||||
@@ -1204,6 +1204,15 @@ function renderCalendarReminderSection(reminder = null, event = null) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bindTimeInputs(root) {
|
||||||
|
root.querySelectorAll('.js-time-input').forEach((input) => {
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
const parsed = parseTimeInput(input.value);
|
||||||
|
if (parsed) input.value = formatTimeInput(parsed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Event-Modal (Erstellen / Bearbeiten)
|
// Event-Modal (Erstellen / Bearbeiten)
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -1265,6 +1274,7 @@ function openEventModal({ mode, event = null, date = null, reminder = null }) {
|
|||||||
if (isEdit && event?.all_day) { timeFields.style.display = 'none'; alldayFields.style.display = ''; }
|
if (isEdit && event?.all_day) { timeFields.style.display = 'none'; alldayFields.style.display = ''; }
|
||||||
|
|
||||||
bindDateInputs(panel);
|
bindDateInputs(panel);
|
||||||
|
bindTimeInputs(panel);
|
||||||
|
|
||||||
const iconInput = panel.querySelector('#modal-icon');
|
const iconInput = panel.querySelector('#modal-icon');
|
||||||
const iconTrigger = panel.querySelector('#modal-icon-trigger');
|
const iconTrigger = panel.querySelector('#modal-icon-trigger');
|
||||||
@@ -1455,7 +1465,7 @@ function buildEventModalContent({ mode, event, date, reminder = null }) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-start-time">${t('calendar.startTimeLabel')}</label>
|
<label class="form-label" for="modal-start-time">${t('calendar.startTimeLabel')}</label>
|
||||||
<input type="time" class="form-input" id="modal-start-time" value="${startTime}">
|
<input type="text" class="form-input js-time-input" id="modal-start-time" value="${formatTimeInput(startTime)}" placeholder="${timeInputPlaceholder()}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-grid modal-grid--2">
|
<div class="modal-grid modal-grid--2">
|
||||||
@@ -1465,7 +1475,7 @@ function buildEventModalContent({ mode, event, date, reminder = null }) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="modal-end-time">${t('calendar.endTimeLabel')}</label>
|
<label class="form-label" for="modal-end-time">${t('calendar.endTimeLabel')}</label>
|
||||||
<input type="time" class="form-input" id="modal-end-time" value="${endTime}">
|
<input type="text" class="form-input js-time-input" id="modal-end-time" value="${formatTimeInput(endTime)}" placeholder="${timeInputPlaceholder()}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1573,9 +1583,15 @@ async function saveEvent(overlay, mode, eventId, existingReminder = null, attach
|
|||||||
end_datetime = end_datetime || null;
|
end_datetime = end_datetime || null;
|
||||||
} else {
|
} else {
|
||||||
const sd = readDateInput(overlay, '#modal-start-date');
|
const sd = readDateInput(overlay, '#modal-start-date');
|
||||||
const st = overlay.querySelector('#modal-start-time').value;
|
const stRaw = overlay.querySelector('#modal-start-time').value;
|
||||||
|
const st = parseTimeInput(stRaw);
|
||||||
const ed = readDateInput(overlay, '#modal-end-date');
|
const ed = readDateInput(overlay, '#modal-end-date');
|
||||||
const et = overlay.querySelector('#modal-end-time').value;
|
const etRaw = overlay.querySelector('#modal-end-time').value;
|
||||||
|
const et = parseTimeInput(etRaw);
|
||||||
|
if ((stRaw && !st) || (etRaw && !et)) {
|
||||||
|
window.oikos?.showToast(t('calendar.invalidDate'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
start_datetime = st ? `${sd}T${st}` : sd;
|
start_datetime = st ? `${sd}T${st}` : sd;
|
||||||
end_datetime = ed ? (et ? `${ed}T${et}` : ed) : null;
|
end_datetime = ed ? (et ? `${ed}T${et}` : ed) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export async function render(container, { user }) {
|
|||||||
let users = [];
|
let users = [];
|
||||||
let googleStatus = { configured: false, connected: false, lastSync: null };
|
let googleStatus = { configured: false, connected: false, lastSync: null };
|
||||||
let appleStatus = { configured: false, lastSync: null };
|
let appleStatus = { configured: false, lastSync: null };
|
||||||
let prefs = { visible_meal_types: ['breakfast', 'lunch', 'dinner', 'snack'], currency: 'EUR', date_format: 'mdy', app_name: DEFAULT_APP_NAME };
|
let prefs = { visible_meal_types: ['breakfast', 'lunch', 'dinner', 'snack'], currency: 'EUR', date_format: 'mdy', time_format: '24h', app_name: DEFAULT_APP_NAME };
|
||||||
let categories = [];
|
let categories = [];
|
||||||
let icsSubscriptions = [];
|
let icsSubscriptions = [];
|
||||||
let apiTokens = [];
|
let apiTokens = [];
|
||||||
@@ -226,6 +226,9 @@ export async function render(container, { user }) {
|
|||||||
if (prefs.date_format) {
|
if (prefs.date_format) {
|
||||||
try { localStorage.setItem('oikos-date-format', prefs.date_format); } catch (_) {}
|
try { localStorage.setItem('oikos-date-format', prefs.date_format); } catch (_) {}
|
||||||
}
|
}
|
||||||
|
if (prefs.time_format) {
|
||||||
|
try { localStorage.setItem('oikos-time-format', prefs.time_format); } catch (_) {}
|
||||||
|
}
|
||||||
if (prefs.app_name) {
|
if (prefs.app_name) {
|
||||||
try { localStorage.setItem(APP_NAME_STORAGE_KEY, prefs.app_name); } catch (_) {}
|
try { localStorage.setItem(APP_NAME_STORAGE_KEY, prefs.app_name); } catch (_) {}
|
||||||
}
|
}
|
||||||
@@ -338,6 +341,11 @@ export async function render(container, { user }) {
|
|||||||
<option value="dmy"${prefs.date_format === 'dmy' ? ' selected' : ''}>DD/MM/YYYY</option>
|
<option value="dmy"${prefs.date_format === 'dmy' ? ' selected' : ''}>DD/MM/YYYY</option>
|
||||||
<option value="ymd"${prefs.date_format === 'ymd' ? ' selected' : ''}>YYYY-MM-DD</option>
|
<option value="ymd"${prefs.date_format === 'ymd' ? ' selected' : ''}>YYYY-MM-DD</option>
|
||||||
</select>
|
</select>
|
||||||
|
<label class="form-label" for="time-format-select" style="margin-top:var(--space-3)">${t('settings.timeFormatLabel')}</label>
|
||||||
|
<select class="form-input" id="time-format-select">
|
||||||
|
<option value="24h"${prefs.time_format === '24h' ? ' selected' : ''}>24 ${t('settings.timeFormatHours')}</option>
|
||||||
|
<option value="12h"${prefs.time_format === '12h' ? ' selected' : ''}>AM/PM</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -858,6 +866,20 @@ function bindEvents(container, user, users, categories, icsSubscriptions, apiTok
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timeFormatSelect = container.querySelector('#time-format-select');
|
||||||
|
if (timeFormatSelect) {
|
||||||
|
timeFormatSelect.addEventListener('change', async () => {
|
||||||
|
try {
|
||||||
|
await api.put('/preferences', { time_format: timeFormatSelect.value });
|
||||||
|
try { localStorage.setItem('oikos-time-format', timeFormatSelect.value); } catch (_) {}
|
||||||
|
window.dispatchEvent(new CustomEvent('time-format-changed', { detail: { timeFormat: timeFormatSelect.value } }));
|
||||||
|
window.oikos?.showToast(t('settings.timeFormatSavedToast'), 'success');
|
||||||
|
} catch (err) {
|
||||||
|
window.oikos?.showToast(err.message ?? t('common.errorGeneric'), 'danger');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const appNameForm = container.querySelector('#app-name-form');
|
const appNameForm = container.querySelector('#app-name-form');
|
||||||
if (appNameForm) {
|
if (appNameForm) {
|
||||||
appNameForm.addEventListener('submit', async (e) => {
|
appNameForm.addEventListener('submit', async (e) => {
|
||||||
|
|||||||
+19
-4
@@ -8,7 +8,7 @@ import { api } from '/api.js';
|
|||||||
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
||||||
import { openModal as openSharedModal, closeModal, wireBlurValidation, validateAll, btnSuccess, btnError, promptModal } from '/components/modal.js';
|
import { openModal as openSharedModal, closeModal, wireBlurValidation, validateAll, btnSuccess, btnError, promptModal } from '/components/modal.js';
|
||||||
import { stagger, vibrate } from '/utils/ux.js';
|
import { stagger, vibrate } from '/utils/ux.js';
|
||||||
import { t, formatDate, formatTime, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid } from '/i18n.js';
|
import { t, formatDate, formatTime, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid, formatTimeInput, parseTimeInput, timeInputPlaceholder } from '/i18n.js';
|
||||||
import { esc } from '/utils/html.js';
|
import { esc } from '/utils/html.js';
|
||||||
import { refresh as refreshReminders } from '/reminders.js';
|
import { refresh as refreshReminders } from '/reminders.js';
|
||||||
|
|
||||||
@@ -361,8 +361,8 @@ function renderModalContent({ task = null, users = [], reminder = null } = {}) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="label" for="task-due-time">${t('tasks.dueTimeLabel')}</label>
|
<label class="label" for="task-due-time">${t('tasks.dueTimeLabel')}</label>
|
||||||
<input class="input" type="time" id="task-due-time" name="due_time"
|
<input class="input js-time-input" type="text" id="task-due-time" name="due_time"
|
||||||
value="${task?.due_time ?? ''}">
|
value="${formatTimeInput(task?.due_time ?? '')}" placeholder="${timeInputPlaceholder()}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -566,6 +566,12 @@ function openTaskModal({ task = null, users = [], reminder = null } = {}, contai
|
|||||||
if (parsed) input.value = formatDateInput(parsed);
|
if (parsed) input.value = formatDateInput(parsed);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
panel.querySelectorAll('.js-time-input').forEach((input) => {
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
const parsed = parseTimeInput(input.value);
|
||||||
|
if (parsed) input.value = formatTimeInput(parsed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Form-Events
|
// Form-Events
|
||||||
panel.querySelector('#task-form')
|
panel.querySelector('#task-form')
|
||||||
@@ -614,11 +620,20 @@ async function handleFormSubmit(e, container) {
|
|||||||
priority: form.priority.value,
|
priority: form.priority.value,
|
||||||
category: form.category.value,
|
category: form.category.value,
|
||||||
due_date: dueDate || null,
|
due_date: dueDate || null,
|
||||||
due_time: form.due_time?.value || null,
|
|
||||||
assigned_to: form.assigned_to.value ? Number(form.assigned_to.value) : null,
|
assigned_to: form.assigned_to.value ? Number(form.assigned_to.value) : null,
|
||||||
is_recurring: rrule.is_recurring ? 1 : 0,
|
is_recurring: rrule.is_recurring ? 1 : 0,
|
||||||
recurrence_rule: rrule.recurrence_rule,
|
recurrence_rule: rrule.recurrence_rule,
|
||||||
};
|
};
|
||||||
|
const dueTimeRaw = form.due_time?.value || '';
|
||||||
|
const dueTime = parseTimeInput(dueTimeRaw);
|
||||||
|
if (dueTimeRaw && !dueTime) {
|
||||||
|
errorEl.textContent = t('calendar.invalidDate');
|
||||||
|
errorEl.hidden = false;
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = originalLabel;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body.due_time = dueTime || null;
|
||||||
if (form.status) body.status = form.status.value;
|
if (form.status) body.status = form.status.value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -300,6 +300,10 @@ async function syncPreferencesOnce() {
|
|||||||
if (dateFormat) {
|
if (dateFormat) {
|
||||||
localStorage.setItem('oikos-date-format', dateFormat);
|
localStorage.setItem('oikos-date-format', dateFormat);
|
||||||
}
|
}
|
||||||
|
const timeFormat = res?.data?.time_format;
|
||||||
|
if (timeFormat) {
|
||||||
|
localStorage.setItem('oikos-time-format', timeFormat);
|
||||||
|
}
|
||||||
if (res?.data?.app_name) {
|
if (res?.data?.app_name) {
|
||||||
setAppName(res.data.app_name);
|
setAppName(res.data.app_name);
|
||||||
updateBranding();
|
updateBranding();
|
||||||
@@ -1332,6 +1336,17 @@ window.addEventListener('app-name-changed', () => {
|
|||||||
updateBranding(currentPath || '/');
|
updateBranding(currentPath || '/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function refreshCurrentRoute() {
|
||||||
|
if (!currentPath) return;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!currentPath) return;
|
||||||
|
navigate(currentPath, false);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('date-format-changed', refreshCurrentRoute);
|
||||||
|
window.addEventListener('time-format-changed', refreshCurrentRoute);
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Virtuelle Tastatur: FAB ausblenden wenn Keyboard offen
|
// Virtuelle Tastatur: FAB ausblenden wenn Keyboard offen
|
||||||
// Erkennung via visualViewport - Höhe < 75% des Fensters = Keyboard aktiv.
|
// Erkennung via visualViewport - Höhe < 75% des Fensters = Keyboard aktiv.
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const DEFAULT_APP_NAME = 'Oikos';
|
|||||||
|
|
||||||
const VALID_DATE_FORMATS = ['mdy', 'dmy', 'ymd'];
|
const VALID_DATE_FORMATS = ['mdy', 'dmy', 'ymd'];
|
||||||
const DEFAULT_DATE_FORMAT = 'mdy';
|
const DEFAULT_DATE_FORMAT = 'mdy';
|
||||||
|
const VALID_TIME_FORMATS = ['24h', '12h'];
|
||||||
|
const DEFAULT_TIME_FORMAT = '24h';
|
||||||
|
|
||||||
const VALID_WIDGET_IDS = ['tasks', 'calendar', 'birthdays', 'budget', 'family', 'weather', 'shopping', 'meals', 'notes'];
|
const VALID_WIDGET_IDS = ['tasks', 'calendar', 'birthdays', 'budget', 'family', 'weather', 'shopping', 'meals', 'notes'];
|
||||||
const DEFAULT_WIDGET_CONFIG = JSON.stringify(VALID_WIDGET_IDS.map((id) => ({ id, visible: true })));
|
const DEFAULT_WIDGET_CONFIG = JSON.stringify(VALID_WIDGET_IDS.map((id) => ({ id, visible: true })));
|
||||||
@@ -88,6 +90,7 @@ router.get('/', (req, res) => {
|
|||||||
const visibleMealTypes = raw.split(',').filter((t) => VALID_MEAL_TYPES.includes(t));
|
const visibleMealTypes = raw.split(',').filter((t) => VALID_MEAL_TYPES.includes(t));
|
||||||
const currency = cfgGet('currency') ?? DEFAULT_CURRENCY;
|
const currency = cfgGet('currency') ?? DEFAULT_CURRENCY;
|
||||||
const dateFormat = VALID_DATE_FORMATS.includes(cfgGet('date_format')) ? cfgGet('date_format') : DEFAULT_DATE_FORMAT;
|
const dateFormat = VALID_DATE_FORMATS.includes(cfgGet('date_format')) ? cfgGet('date_format') : DEFAULT_DATE_FORMAT;
|
||||||
|
const timeFormat = VALID_TIME_FORMATS.includes(cfgGet('time_format')) ? cfgGet('time_format') : DEFAULT_TIME_FORMAT;
|
||||||
const appName = cfgGet('app_name') ?? DEFAULT_APP_NAME;
|
const appName = cfgGet('app_name') ?? DEFAULT_APP_NAME;
|
||||||
const dashboardWidgets = parseWidgetConfig(cfgGet('dashboard_widgets'));
|
const dashboardWidgets = parseWidgetConfig(cfgGet('dashboard_widgets'));
|
||||||
|
|
||||||
@@ -96,6 +99,7 @@ router.get('/', (req, res) => {
|
|||||||
visible_meal_types: visibleMealTypes,
|
visible_meal_types: visibleMealTypes,
|
||||||
currency,
|
currency,
|
||||||
date_format: dateFormat,
|
date_format: dateFormat,
|
||||||
|
time_format: timeFormat,
|
||||||
app_name: appName,
|
app_name: appName,
|
||||||
dashboard_widgets: dashboardWidgets,
|
dashboard_widgets: dashboardWidgets,
|
||||||
},
|
},
|
||||||
@@ -115,7 +119,7 @@ router.get('/', (req, res) => {
|
|||||||
|
|
||||||
router.put('/', (req, res) => {
|
router.put('/', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { visible_meal_types, currency, date_format, app_name, dashboard_widgets } = req.body;
|
const { visible_meal_types, currency, date_format, time_format, app_name, dashboard_widgets } = req.body;
|
||||||
|
|
||||||
if (visible_meal_types !== undefined) {
|
if (visible_meal_types !== undefined) {
|
||||||
if (!Array.isArray(visible_meal_types)) {
|
if (!Array.isArray(visible_meal_types)) {
|
||||||
@@ -142,6 +146,13 @@ router.put('/', (req, res) => {
|
|||||||
cfgSet('date_format', date_format);
|
cfgSet('date_format', date_format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (time_format !== undefined) {
|
||||||
|
if (!VALID_TIME_FORMATS.includes(time_format)) {
|
||||||
|
return res.status(400).json({ error: `Ungültiges Zeitformat. Erlaubt: ${VALID_TIME_FORMATS.join(', ')}`, code: 400 });
|
||||||
|
}
|
||||||
|
cfgSet('time_format', time_format);
|
||||||
|
}
|
||||||
|
|
||||||
if (app_name !== undefined) {
|
if (app_name !== undefined) {
|
||||||
const vAppName = str(app_name, 'Application name', { max: MAX_SHORT, required: false });
|
const vAppName = str(app_name, 'Application name', { max: MAX_SHORT, required: false });
|
||||||
if (vAppName.error) return res.status(400).json({ error: vAppName.error, code: 400 });
|
if (vAppName.error) return res.status(400).json({ error: vAppName.error, code: 400 });
|
||||||
@@ -161,6 +172,7 @@ router.put('/', (req, res) => {
|
|||||||
const savedMealTypes = rawMealTypes.split(',').filter((t) => VALID_MEAL_TYPES.includes(t));
|
const savedMealTypes = rawMealTypes.split(',').filter((t) => VALID_MEAL_TYPES.includes(t));
|
||||||
const savedCurrency = cfgGet('currency') ?? DEFAULT_CURRENCY;
|
const savedCurrency = cfgGet('currency') ?? DEFAULT_CURRENCY;
|
||||||
const savedDateFormat = VALID_DATE_FORMATS.includes(cfgGet('date_format')) ? cfgGet('date_format') : DEFAULT_DATE_FORMAT;
|
const savedDateFormat = VALID_DATE_FORMATS.includes(cfgGet('date_format')) ? cfgGet('date_format') : DEFAULT_DATE_FORMAT;
|
||||||
|
const savedTimeFormat = VALID_TIME_FORMATS.includes(cfgGet('time_format')) ? cfgGet('time_format') : DEFAULT_TIME_FORMAT;
|
||||||
const savedAppName = cfgGet('app_name') ?? DEFAULT_APP_NAME;
|
const savedAppName = cfgGet('app_name') ?? DEFAULT_APP_NAME;
|
||||||
const savedWidgets = parseWidgetConfig(cfgGet('dashboard_widgets'));
|
const savedWidgets = parseWidgetConfig(cfgGet('dashboard_widgets'));
|
||||||
|
|
||||||
@@ -169,6 +181,7 @@ router.put('/', (req, res) => {
|
|||||||
visible_meal_types: savedMealTypes,
|
visible_meal_types: savedMealTypes,
|
||||||
currency: savedCurrency,
|
currency: savedCurrency,
|
||||||
date_format: savedDateFormat,
|
date_format: savedDateFormat,
|
||||||
|
time_format: savedTimeFormat,
|
||||||
app_name: savedAppName,
|
app_name: savedAppName,
|
||||||
dashboard_widgets: savedWidgets,
|
dashboard_widgets: savedWidgets,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user