--- /dev/null
+<script>
+ import { API_URL } from '$lib/APIurl';
+ import { getTranslate } from '@tolgee/svelte';
+ import axios from 'axios';
+ import { onMount } from 'svelte';
+ import Tag from '$lib/Tag.svelte';
+ import { tags } from '$lib/tagStore';
+ import { get } from 'svelte/store';
+ import * as bootstrap from 'bootstrap';
+ import { mount } from 'svelte';
+ import { goto } from '$app/navigation';
+ import { selectedDate } from '$lib/calendarStore.js';
+
+ const { t } = getTranslate();
+
+ // Raw day stats from backend
+ let dayStats = [];
+
+ // Derived years list & selected year
+ let years = [];
+ let selectedYear = new Date().getFullYear();
+
+ // Heatmap data (weeks -> days)
+ let weeks = [];
+ let maxWordCountYear = 0;
+ let minWordCountYear = 0; // smallest > 0 value
+ // Filter stats for selected year
+ // Iterate all days of the year
+ let legendRanges = [];
+ // Bootstrap tooltip support
+ let heatmapEl;
+ let dayMap = new Map(); // key -> day data
+
+ const buildYearData = () => {
+ if (!years.includes(selectedYear)) return;
+
+ // Filter stats for selected year
+ const yearDays = dayStats.filter((d) => d.year === selectedYear);
+ const mapByKey = new Map();
+ let localMax = 0;
+ let localMin = Infinity;
+ for (const d of yearDays) {
+ const key = `${d.year}-${String(d.month).padStart(2, '0')}-${String(d.day).padStart(2, '0')}`;
+ mapByKey.set(key, d);
+ if (d.wordCount > localMax) localMax = d.wordCount;
+ if (d.wordCount > 0 && d.wordCount < localMin) localMin = d.wordCount;
+ }
+ maxWordCountYear = localMax;
+ minWordCountYear = localMin === Infinity ? 0 : localMin;
+
+ buildLegendRanges();
+
+ // Iterate all days of the year
+ const first = new Date(selectedYear, 0, 1);
+ const last = new Date(selectedYear, 11, 31);
+ // Build sequential weeks starting Monday (GitHub uses Sunday start; we use Monday for EU style)
+ // Pre-fill leading empty cells
+ // If week complete, push and start a new one
+ // Push trailing week if it contains any filled cells
+ const thresholds = [0.15, 0.35, 0.65]; // thresholds between intensity levels
+ // Map ratio ranges to integer word ranges
+ const weekdayIndex = (d) => (d.getDay() + 6) % 7;
+ let current = new Date(first);
+ weeks = [];
+ let currentWeek = new Array(7).fill(null);
+ // Pre-fill leading empty cells
+ for (let i = 0; i < weekdayIndex(current); i++) {
+ currentWeek[i] = { empty: true };
+ }
+ while (current <= last) {
+ const idx = weekdayIndex(current);
+ const key = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, '0')}-${String(current.getDate()).padStart(2, '0')}`;
+ const stat = mapByKey.get(key);
+ const wordCount = stat ? stat.wordCount : 0;
+ const isBookmarked = stat ? stat.isBookmarked : false;
+ currentWeek[idx] = {
+ date: new Date(current),
+ wordCount,
+ isBookmarked,
+ tags: stat ? stat.tags : [],
+ fileCount: stat ? stat.fileCount : 0
+ };
+ // If week complete, push and start new
+ if (idx === 6) {
+ weeks.push(currentWeek);
+ currentWeek = new Array(7).fill(null);
+ }
+ // Advance to the next day
+ current.setDate(current.getDate() + 1);
+ }
+ // Push trailing week if contains any filled cells
+ if (currentWeek.some((c) => c !== null)) weeks.push(currentWeek);
+
+ // Rebuild day map for tooltips
+ dayMap.clear();
+ for (let wi = 0; wi < weeks.length; wi++) {
+ const w = weeks[wi];
+ for (let di = 0; di < w.length; di++) {
+ const d = w[di];
+ if (d && !d.empty) dayMap.set(`${wi}-${di}`, d);
+ }
+ }
+
+ // Initialize / refresh bootstrap tooltips after DOM update
+ setTimeout(initTooltips, 0);
+ };
+
+ const thresholds = [0.15, 0.35, 0.65]; // thresholds between levels
+ function buildLegendRanges() {
+ legendRanges = [];
+ if (maxWordCountYear === 0) {
+ legendRanges = [
+ { level: 0, from: 0, to: 0 },
+ { level: 1, from: 0, to: 0 },
+ { level: 2, from: 0, to: 0 },
+ { level: 3, from: 0, to: 0 },
+ { level: 4, from: 0, to: 0 }
+ ];
+ return;
+ }
+ // Map ratio ranges to integer word ranges
+ const segments = [
+ { level: 0, rFrom: 0, rTo: 0 },
+ { level: 1, rFrom: 0.0000001, rTo: thresholds[0] },
+ { level: 2, rFrom: thresholds[0], rTo: thresholds[1] },
+ { level: 3, rFrom: thresholds[1], rTo: thresholds[2] },
+ { level: 4, rFrom: thresholds[2], rTo: 1 }
+ ];
+ legendRanges = segments.map((s) => {
+ const from =
+ s.level === 0 ? 0 : Math.max(minWordCountYear, Math.floor(s.rFrom * maxWordCountYear) || 1);
+ let to = Math.floor(s.rTo * maxWordCountYear);
+ if (s.level === 4) to = maxWordCountYear; // last covers top
+ if (to < from) to = from; // ensure non-inverted
+ return { level: s.level, from, to };
+ });
+ }
+
+ const colorLevel = (wc) => {
+ if (wc <= 0) return 0;
+ if (maxWordCountYear <= 0) return 0;
+ const r = wc / maxWordCountYear;
+ if (r < thresholds[0]) return 1;
+ if (r < thresholds[1]) return 2;
+ if (r < thresholds[2]) return 3;
+ return 4;
+ };
+
+ function selectYear(year) {
+ selectedYear = year;
+ buildYearData();
+ }
+
+ function prevYear() {
+ const idx = years.indexOf(selectedYear);
+ if (idx > 0) {
+ selectedYear = years[idx - 1];
+ buildYearData();
+ }
+ }
+ function nextYear() {
+ const idx = years.indexOf(selectedYear);
+ if (idx !== -1 && idx < years.length - 1) {
+ selectedYear = years[idx + 1];
+ buildYearData();
+ }
+ }
+
+ const fmtDate = (d) =>
+ d.toLocaleDateString(undefined, {
+ weekday: 'long',
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ });
+
+ onMount(async () => {
+ try {
+ const resp = await axios.get(API_URL + '/users/statistics');
+ dayStats = resp.data;
+ // Normalize key names to camelCase if backend sent lower-case
+ dayStats = dayStats.map((d) => ({
+ year: d.year ?? d.Year,
+ month: d.month ?? d.Month,
+ day: d.day ?? d.Day,
+ wordCount: d.wordCount ?? d.WordCount ?? 0,
+ fileCount: d.fileCount ?? d.FileCount ?? 0,
+ tags: d.tags ?? d.Tags ?? [],
+ isBookmarked: d.isBookmarked ?? d.IsBookmarked ?? false
+ }));
+ // Collect years
+ years = Array.from(new Set(dayStats.map((d) => d.year))).sort((a, b) => a - b);
+ if (!years.includes(selectedYear) && years.length) selectedYear = years[years.length - 1];
+ buildYearData();
+ } catch (e) {
+ console.error('Failed loading statistics', e);
+ }
+ });
+
+ // Build HTML skeleton for popover (tags get injected after show)
+ function tooltipHTML(day) {
+ const html = `
+ <div class="popover-day-content">
+ <div class="tt-head"><b>${fmtDate(day.date)}</b></div>
+ <div class="tt-line">${$t('settings.statistics.wordCount', { wordCount: day.wordCount })}</div>
+ ${day.fileCount ? `<div class='tt-line'>${$t('settings.statistics.fileCount', { fileCount: day.fileCount })}</div>` : ''}
+ ${day.isBookmarked ? `<div class='tt-line'>★ ${$t('settings.statistics.bookmarked')}</div>` : ''}
+ <div class="tt-tags"></div>
+ <div class="tt-footer">
+ <button type="button" class="tt-open-btn" data-year="${day.date.getFullYear()}" data-month="${day.date.getMonth() + 1}" data-day="${day.date.getDate()}">
+ 📝 ${$t('settings.statistics.open')}
+ </button>
+ </div>
+ </div>`;
+ console.log('Generated popover HTML:', html);
+ return html;
+ }
+
+ // Function to open a specific date in /write
+ function openDate(year, month, day) {
+ console.log('Open date', year, month, day);
+
+ // Set the selected date
+ $selectedDate = {
+ year: parseInt(year),
+ month: parseInt(month),
+ day: parseInt(day)
+ };
+
+ // Close the settings modal (assuming it's a Bootstrap modal)
+ const settingsModal = document.querySelector('#settingsModal');
+ if (settingsModal) {
+ const modalInstance = bootstrap.Modal.getInstance(settingsModal);
+ if (modalInstance) {
+ modalInstance.hide();
+ }
+ }
+
+ // Navigate to write page
+ goto('/write');
+ }
+
+ function initTooltips() {
+ // Dispose previous instances for day cells
+ document.querySelectorAll('.day-cell[data-bs-toggle="popover"]').forEach((el) => {
+ const inst = bootstrap.Popover.getInstance(el);
+ if (inst) inst.dispose();
+ });
+
+ // Dispose previous instances for legend cells (keep as tooltips)
+ document.querySelectorAll('.legend-cell[data-bs-toggle="tooltip"]').forEach((el) => {
+ const inst = bootstrap.Tooltip.getInstance(el);
+ if (inst) inst.dispose();
+ });
+
+ // Initialize day cell popovers
+ const cells = heatmapEl?.querySelectorAll('.day-cell[data-day-key]') || [];
+ console.log('Initializing popovers for', cells.length, 'cells');
+ cells.forEach((el, index) => {
+ const key = el.getAttribute('data-day-key');
+ const day = dayMap.get(key);
+ if (!day) return;
+
+ const htmlContent = tooltipHTML(day);
+ el.setAttribute('data-bs-content', htmlContent);
+ el.setAttribute('data-bs-toggle', 'popover');
+
+ console.log(`Initializing popover ${index} with content:`, htmlContent);
+
+ const popover = new bootstrap.Popover(el, {
+ html: true,
+ placement: 'top',
+ trigger: 'click',
+ animation: false,
+ container: 'body',
+ sanitize: false // Disable sanitization to allow our HTML
+ });
+
+ // Test if popover is created
+ console.log('Popover instance created:', popover);
+
+ // Close other popovers when this one is shown
+ el.addEventListener('show.bs.popover', () => {
+ console.log('Popover showing for key:', key);
+ // Close all other popovers
+ document.querySelectorAll('.day-cell[data-bs-toggle="popover"]').forEach((otherEl) => {
+ if (otherEl !== el) {
+ const otherInst = bootstrap.Popover.getInstance(otherEl);
+ if (otherInst) otherInst.hide();
+ }
+ });
+ });
+
+ // After popover is shown, mount Tag components and setup button handlers
+ const populate = () => {
+ console.log('Popover populate called for key:', key);
+ const inst = bootstrap.Popover.getInstance(el);
+ if (!inst) {
+ console.log('No popover instance found');
+ return;
+ }
+ const popoverEl = typeof inst.getTipElement === 'function' ? inst.getTipElement() : inst.tip;
+ console.log('Popover element:', popoverEl);
+
+ const tagContainer = popoverEl?.querySelector('.tt-tags');
+ const openBtn = popoverEl?.querySelector('.tt-open-btn');
+ console.log('Found elements - tags:', tagContainer, 'button:', openBtn);
+
+ if (!tagContainer || tagContainer.dataset.populated === '1') return;
+ // Mark to avoid double work
+ tagContainer.dataset.populated = '1';
+ const allTagsNow = get(tags) || [];
+ (day.tags || []).forEach((tid) => {
+ const tagObj = allTagsNow.find((t) => t.id == tid);
+ if (tagObj) {
+ mount(Tag, { target: tagContainer, props: { tag: tagObj } });
+ }
+ });
+
+ // Setup button handler
+ if (openBtn && !openBtn.dataset.handlerAdded) {
+ console.log('Adding click handler to button');
+ openBtn.dataset.handlerAdded = '1';
+ openBtn.addEventListener('click', (e) => {
+ console.log('Open date click', e.target, e.currentTarget);
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Get attributes from the clicked element or fallback to currentTarget
+ const target = e.target.closest('.tt-open-btn') || e.currentTarget;
+ const year = target.getAttribute('data-year');
+ const month = target.getAttribute('data-month');
+ const day = target.getAttribute('data-day');
+
+ console.log('Extracted date:', { year, month, day });
+
+ if (year && month && day) {
+ // Hide popover first
+ const inst = bootstrap.Popover.getInstance(el);
+ if (inst) inst.hide();
+
+ openDate(year, month, day);
+ }
+ });
+ }
+ };
+
+ // Listen for popover events
+ el.addEventListener('inserted.bs.popover', populate);
+ el.addEventListener('shown.bs.popover', populate);
+ });
+
+ // Initialize legend cell tooltips (keep as simple tooltips)
+ const legendCells = document.querySelectorAll('.legend-cell[data-bs-toggle="tooltip"]');
+ legendCells.forEach((el) => {
+ new bootstrap.Tooltip(el, {
+ placement: 'top',
+ trigger: 'hover focus'
+ });
+ });
+
+ // Close popovers when clicking outside
+ document.addEventListener('click', (e) => {
+ // Check if click is outside any day-cell and outside any popover
+ if (!e.target.closest('.day-cell[data-bs-toggle="popover"]') && !e.target.closest('.popover')) {
+ document.querySelectorAll('.day-cell[data-bs-toggle="popover"]').forEach((el) => {
+ const inst = bootstrap.Popover.getInstance(el);
+ if (inst) inst.hide();
+ });
+ }
+ }, { capture: true }); // Use capture to ensure this runs before other handlers
+ }
+</script>
+
+<div class="settings-stats">
+ <h2 class="h4 mb-3">{$t('settings.statistics.title')}</h2>
+
+ {#if years.length === 0}
+ <div class="spinner-border" role="status">
+ <span class="visually-hidden">Loading...</span>
+ </div>
+ {:else}
+ <div class="year-selector d-flex align-items-center gap-2 mb-3 flex-wrap">
+ <button
+ class="btn btn-sm btn-outline-secondary"
+ on:click={prevYear}
+ disabled={years.indexOf(selectedYear) === 0}
+ aria-label="previous year">«</button
+ >
+ <select
+ class="form-select form-select-sm year-dropdown"
+ bind:value={selectedYear}
+ on:change={(e) => selectYear(+e.target.value)}
+ >
+ {#each years as y}
+ <option value={y}>{y}</option>
+ {/each}
+ </select>
+ <button
+ class="btn btn-sm btn-outline-secondary"
+ on:click={nextYear}
+ disabled={years.indexOf(selectedYear) === years.length - 1}
+ aria-label="next year">»</button
+ >
+ <div class="legend ms-auto d-flex align-items-center gap-1">
+ <span class="legend-label small">{$t('settings.statistics.legend')}</span>
+ <div class="legend-colors d-flex align-items-center gap-1">
+ {#each legendRanges as seg}
+ <span
+ class="legend-cell level-{seg.level}"
+ data-bs-toggle="tooltip"
+ data-bs-title={`${seg.from} – ${seg.to} ${$t('settings.statistics.words')}`}
+ ></span>
+ {/each}
+ </div>
+ </div>
+ </div>
+
+ <div class="heatmap" aria-label="Year-Heatmap" bind:this={heatmapEl}>
+ <div class="weeks d-flex">
+ {#each weeks as week, wi}
+ <div class="week-column d-flex flex-column">
+ {#each week as day, di}
+ {#if day === null || day.empty}
+ <div class="day-cell empty" aria-hidden="true"></div>
+ {:else}
+ <div
+ class="day-cell level-{colorLevel(day.wordCount)}"
+ role="button"
+ tabindex="0"
+ data-bs-toggle="popover"
+ data-day-key={`${wi}-${di}`}
+ >
+ {#if day.isBookmarked}
+ <span class="bookmark" aria-label="Bookmarked">★</span>
+ {/if}
+ </div>
+ {/if}
+ {/each}
+ </div>
+ {/each}
+ </div>
+ </div>
+ {/if}
+</div>
+
+<style>
+ .settings-stats {
+ min-height: 40vh;
+ }
+ .year-selector .year-dropdown {
+ width: auto;
+ }
+ .heatmap {
+ overflow-x: auto;
+ position: relative; /* ensure tooltip absolute coords are relative to heatmap */
+ border: 1px solid var(--bs-border-color, #ddd);
+ padding: 0.5rem;
+ border-radius: 0.5rem;
+ background: var(--bs-body-bg, #fff);
+ }
+ .week-column {
+ gap: 3px;
+ }
+ .weeks {
+ gap: 3px;
+ }
+ .day-cell {
+ width: 14px;
+ height: 14px;
+ border-radius: 3px;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ .day-cell.empty {
+ background: transparent;
+ }
+ /* Color scale (adjust to theme) */
+ .level-0 {
+ background: var(--heatmap-empty, #ebedf0);
+ }
+ .level-1 {
+ background: #c6e48b;
+ }
+ .level-2 {
+ background: #7bc96f;
+ }
+ .level-3 {
+ background: #239a3b;
+ }
+ .level-4 {
+ background: #196127;
+ }
+ .day-cell:hover {
+ outline: 1px solid #333;
+ z-index: 2;
+ }
+ .legend-cell {
+ width: 14px;
+ height: 14px;
+ border-radius: 3px;
+ display: inline-block;
+ }
+ .legend-cell.level-0 {
+ background: var(--heatmap-empty, #ebedf0);
+ }
+ .legend-cell.level-1 {
+ background: #c6e48b;
+ }
+ .legend-cell.level-2 {
+ background: #7bc96f;
+ }
+ .legend-cell.level-3 {
+ background: #239a3b;
+ }
+ .legend-cell.level-4 {
+ background: #196127;
+ }
+ .bookmark {
+ font-size: 9px;
+ line-height: 1;
+ color: #fff;
+ text-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
+ }
+ .day-cell.level-0 .bookmark {
+ color: #555;
+ text-shadow: none;
+ }
+ /* Popover styling (applies inside Bootstrap popover) */
+ :global(.popover-day-content) {
+ min-width: 200px;
+ }
+ :global(.popover-day-content .tt-head) {
+ margin-bottom: 4px;
+ font-size: 14px;
+ }
+ :global(.popover-day-content .tt-tags) {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ }
+ :global(.popover-day-content .tt-tags .badge) {
+ font-size: 12px;
+ margin-right: 4px;
+ }
+ :global(.popover-day-content .tt-footer) {
+ text-align: center;
+ padding-top: 8px;
+ }
+ :global(.popover-day-content .tt-open-btn) {
+ font-size: 12px !important;
+ padding: 6px 12px !important;
+ background: #007bff;
+ color: white;
+ border-radius: 4px;
+ cursor: pointer;
+ border: none;
+ transition: background-color 0.2s;
+ }
+ :global(.popover-day-content .tt-open-btn:hover) {
+ background: #0056b3;
+ color: white;
+ }
+
+ /* Old tooltip styling (now removed as we use popovers) */
+
+ /* Desktop: Add pointer cursor for day cells */
+ @media (pointer: fine) {
+ .day-cell[data-day-key] {
+ cursor: pointer;
+ }
+ }
+ @media (max-width: 600px) {
+ .day-cell,
+ .legend-cell {
+ width: 11px;
+ height: 11px;
+ }
+ }
+</style>
import axios from 'axios';
import { page } from '$app/state';
import { blur, slide, fade } from 'svelte/transition';
+ import Statistics from '$lib/settings/Statistics.svelte';
+ import Admin from '$lib/settings/Admin.svelte';
import { T, getTranslate, getTolgee } from '@tolgee/svelte';
const { t } = getTranslate();
let inDuration = 150;
let outDuration = 150;
+ // Active sub-view of settings modal: 'settings' | 'stats' | 'admin'
+ let activeSettingsView = $state('settings');
+
$effect(() => {
if ($readingMode === true && page.url.pathname !== '/read') {
goto('/read');
}
function openSettingsModal() {
+ activeSettingsView = 'settings';
$tempSettings = JSON.parse(JSON.stringify($settings));
aLookBackYears = $settings.aLookBackYears.toString();
>
<!-- -->
<div class="modal-content shadow-lg glass">
- <div class="modal-header">
- <h1>{$t('settings.title')}</h1>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ <div class="modal-header flex-wrap gap-2">
+ <div class="btn-group w-100" role="group" aria-label="Settings views">
+ <button
+ type="button"
+ class="btn btn-outline-primary {activeSettingsView === 'settings' ? 'active' : ''}"
+ onclick={() => (activeSettingsView = 'settings')}
+ >
+ {$t('settings.title')}
+ </button>
+ <button
+ type="button"
+ class="btn btn-outline-primary {activeSettingsView === 'stats' ? 'active' : ''}"
+ onclick={() => (activeSettingsView = 'stats')}
+ >
+ {$t('settings.statistics.title')}
+ </button>
+ <button
+ type="button"
+ class="btn btn-outline-primary {activeSettingsView === 'admin' ? 'active' : ''}"
+ onclick={() => (activeSettingsView = 'admin')}
+ >
+ {$t('settings.admin.title')}
+ </button>
+ <button type="button" class="btn-close ms-auto" data-bs-dismiss="modal" aria-label="Close"
+ ></button>
+ </div>
</div>
<div class="modal-body" id="modal-body">
<div class="row">
- <div class="col-4 overflow-y-auto d-none d-md-block">
- <nav class="flex-column align-items-stretch" id="settings-nav">
- <nav class="nav nav-pills flex-column custom-scrollspy-nav">
- <a
- class="nav-link mb-1 {activeSettingsSection === 'appearance' ? 'active' : ''}"
- href="#appearance">{$t('settings.appearance')}</a
- >
- <a
- class="nav-link mb-1 {activeSettingsSection === 'functions' ? 'active' : ''}"
- href="#functions">{$t('settings.functions')}</a
- >
- <a
- class="nav-link mb-1 {activeSettingsSection === 'tags' ? 'active' : ''}"
- href="#tags">{$t('settings.tags')}</a
- >
- <a
- class="nav-link mb-1 {activeSettingsSection === 'templates' ? 'active' : ''}"
- href="#templates">{$t('settings.templates')}</a
- >
- <a
- class="nav-link mb-1 {activeSettingsSection === 'data' ? 'active' : ''}"
- href="#data">{$t('settings.data')}</a
- >
- <a
- class="nav-link mb-1 {activeSettingsSection === 'security' ? 'active' : ''}"
- href="#security">{$t('settings.security')}</a
- >
- <a
- class="nav-link mb-1 {activeSettingsSection === 'about' ? 'active' : ''}"
- href="#about">{$t('settings.about')}</a
- >
+ {#if activeSettingsView === 'settings'}
+ <div class="col-4 overflow-y-auto d-none d-md-block">
+ <nav class="flex-column align-items-stretch" id="settings-nav">
+ <nav class="nav nav-pills flex-column custom-scrollspy-nav">
+ <a
+ class="nav-link mb-1 {activeSettingsSection === 'appearance' ? 'active' : ''}"
+ href="#appearance">{$t('settings.appearance')}</a
+ >
+ <a
+ class="nav-link mb-1 {activeSettingsSection === 'functions' ? 'active' : ''}"
+ href="#functions">{$t('settings.functions')}</a
+ >
+ <a
+ class="nav-link mb-1 {activeSettingsSection === 'tags' ? 'active' : ''}"
+ href="#tags">{$t('settings.tags')}</a
+ >
+ <a
+ class="nav-link mb-1 {activeSettingsSection === 'templates' ? 'active' : ''}"
+ href="#templates">{$t('settings.templates')}</a
+ >
+ <a
+ class="nav-link mb-1 {activeSettingsSection === 'data' ? 'active' : ''}"
+ href="#data">{$t('settings.data')}</a
+ >
+ <a
+ class="nav-link mb-1 {activeSettingsSection === 'security' ? 'active' : ''}"
+ href="#security">{$t('settings.security')}</a
+ >
+ <a
+ class="nav-link mb-1 {activeSettingsSection === 'about' ? 'active' : ''}"
+ href="#about">{$t('settings.about')}</a
+ >
+ </nav>
</nav>
- </nav>
- </div>
- <div class="col-12 col-md-8">
- <div
- class="settings-content overflow-y-auto"
- data-bs-spy="scroll"
- data-bs-target="#settings-nav"
- data-bs-smooth-scroll="true"
- id="settings-content"
- >
- <!-- Mobile dropdown (visible on < md) -->
- <div class="d-md-none position-sticky top-0 p-1 mobile-settings-dropdown">
- <select
- id="settingsSectionSelect"
- class="form-select form-select-sm"
- bind:value={activeSettingsSection}
- onchange={() => scrollToSection(activeSettingsSection)}
- >
- {#each settingsSectionsMeta as sec}
- <option value={sec.id}>{$t(sec.labelKey)}</option>
- {/each}
- </select>
- </div>
- <div id="appearance">
- <h3 class="text-primary">🎨 {$t('settings.appearance')}</h3>
- <div id="lightdark">
- {#if $tempSettings.darkModeAutoDetect !== $settings.darkModeAutoDetect || $tempSettings.useDarkMode !== $settings.useDarkMode}
- {@render unsavedChanges()}
- {/if}
- <h5>{$t('settings.light_dark_mode')}</h5>
- <div class="form-check mt-2">
- <input
- class="form-check-input"
- type="radio"
- name="darkMode"
- id="darkModeAutoTrue"
- value={true}
- bind:group={$tempSettings.darkModeAutoDetect}
- />
- <label class="form-check-label" for="darkModeAutoTrue">
- {$t('settings.light_dark_auto_detect')}
- {#if window.matchMedia('(prefers-color-scheme: dark)').matches}
- <b><u>{$t('settings.dark_mode')} <Fa icon={faMoon} /></u></b>
- {:else}
- <b><u>{$t('settings.light_mode')} <Fa icon={faSun} /></u></b>
+ </div>
+ <div class="col-12 col-md-8">
+ <div
+ class="settings-content overflow-y-auto"
+ data-bs-spy="scroll"
+ data-bs-target="#settings-nav"
+ data-bs-smooth-scroll="true"
+ id="settings-content"
+ >
+ <!-- Mobile dropdown (visible on < md) -->
+ <div class="d-md-none position-sticky top-0 p-1 mobile-settings-dropdown">
+ <select
+ id="settingsSectionSelect"
+ class="form-select form-select-sm"
+ bind:value={activeSettingsSection}
+ onchange={() => scrollToSection(activeSettingsSection)}
+ >
+ {#each settingsSectionsMeta as sec}
+ <option value={sec.id}>{$t(sec.labelKey)}</option>
+ {/each}
+ </select>
+ </div>
+ <div id="appearance">
+ <h3 class="text-primary">🎨 {$t('settings.appearance')}</h3>
+ <div id="lightdark">
+ {#if $tempSettings.darkModeAutoDetect !== $settings.darkModeAutoDetect || $tempSettings.useDarkMode !== $settings.useDarkMode}
+ {@render unsavedChanges()}
+ {/if}
+ <h5>{$t('settings.light_dark_mode')}</h5>
+ <div class="form-check mt-2">
+ <input
+ class="form-check-input"
+ type="radio"
+ name="darkMode"
+ id="darkModeAutoTrue"
+ value={true}
+ bind:group={$tempSettings.darkModeAutoDetect}
+ />
+ <label class="form-check-label" for="darkModeAutoTrue">
+ {$t('settings.light_dark_auto_detect')}
+ {#if window.matchMedia('(prefers-color-scheme: dark)').matches}
+ <b><u>{$t('settings.dark_mode')} <Fa icon={faMoon} /></u></b>
+ {:else}
+ <b><u>{$t('settings.light_mode')} <Fa icon={faSun} /></u></b>
+ {/if}
+ </label>
+ </div>
+ <div class="form-check mt-2">
+ <input
+ class="form-check-input"
+ type="radio"
+ name="darkMode"
+ id="darkModeAutoFalse"
+ value={false}
+ bind:group={$tempSettings.darkModeAutoDetect}
+ />
+ <label class="form-check-label" for="darkModeAutoFalse">
+ {$t('settings.light_dark_manual')}
+ </label>
+ {#if $tempSettings.darkModeAutoDetect === false}
+ <div class="form-check form-switch d-flex flex-row ps-0" transition:slide>
+ <label class="form-check-label me-5" for="darkModeSwitch">
+ <Fa icon={faSun} />
+ </label>
+ <input
+ class="form-check-input"
+ bind:checked={$tempSettings.useDarkMode}
+ type="checkbox"
+ role="switch"
+ id="darkModeSwitch"
+ />
+ <label class="form-check-label ms-2" for="darkModeSwitch">
+ <Fa icon={faMoon} />
+ </label>
+ </div>
{/if}
- </label>
+ </div>
</div>
- <div class="form-check mt-2">
- <input
- class="form-check-input"
- type="radio"
- name="darkMode"
- id="darkModeAutoFalse"
- value={false}
- bind:group={$tempSettings.darkModeAutoDetect}
- />
- <label class="form-check-label" for="darkModeAutoFalse">
- {$t('settings.light_dark_manual')}
- </label>
- {#if $tempSettings.darkModeAutoDetect === false}
- <div class="form-check form-switch d-flex flex-row ps-0" transition:slide>
- <label class="form-check-label me-5" for="darkModeSwitch">
- <Fa icon={faSun} />
- </label>
- <input
- class="form-check-input"
- bind:checked={$tempSettings.useDarkMode}
- type="checkbox"
- role="switch"
- id="darkModeSwitch"
- />
- <label class="form-check-label ms-2" for="darkModeSwitch">
- <Fa icon={faMoon} />
- </label>
- </div>
+ <div id="background">
+ {#if $tempSettings.background !== $settings.background || $tempSettings.monochromeBackgroundColor !== $settings.monochromeBackgroundColor}
+ {@render unsavedChanges()}
{/if}
- </div>
- </div>
- <div id="background">
- {#if $tempSettings.background !== $settings.background || $tempSettings.monochromeBackgroundColor !== $settings.monochromeBackgroundColor}
- {@render unsavedChanges()}
- {/if}
-
- <h5>{$t('settings.background')}</h5>
- <div class="form-check mt-2">
- <input
- class="form-check-input"
- type="radio"
- name="background"
- id="background_gradient"
- value={'gradient'}
- bind:group={$tempSettings.background}
- />
- <label class="form-check-label" for="background_gradient">
- {$t('settings.background_gradient')}
- </label>
- </div>
- <div class="form-check mt-2">
- <input
- class="form-check-input"
- type="radio"
- name="background"
- id="background_monochrome"
- value={'monochrome'}
- bind:group={$tempSettings.background}
- />
- <label class="form-check-label" for="background_monochrome">
- {$t('settings.background_monochrome')}
- </label>
- {#if $tempSettings.background === 'monochrome'}
+
+ <h5>{$t('settings.background')}</h5>
+ <div class="form-check mt-2">
<input
- transition:slide
- class="form-control form-control-color"
- bind:value={$tempSettings.monochromeBackgroundColor}
- type="color"
+ class="form-check-input"
+ type="radio"
+ name="background"
+ id="background_gradient"
+ value={'gradient'}
+ bind:group={$tempSettings.background}
/>
- {/if}
+ <label class="form-check-label" for="background_gradient">
+ {$t('settings.background_gradient')}
+ </label>
+ </div>
+ <div class="form-check mt-2">
+ <input
+ class="form-check-input"
+ type="radio"
+ name="background"
+ id="background_monochrome"
+ value={'monochrome'}
+ bind:group={$tempSettings.background}
+ />
+ <label class="form-check-label" for="background_monochrome">
+ {$t('settings.background_monochrome')}
+ </label>
+ {#if $tempSettings.background === 'monochrome'}
+ <input
+ transition:slide
+ class="form-control form-control-color"
+ bind:value={$tempSettings.monochromeBackgroundColor}
+ type="color"
+ />
+ {/if}
+ </div>
</div>
</div>
- </div>
- <div id="functions">
- <h3 class="text-primary">🛠️ {$t('settings.functions')}</h3>
-
- <div id="autoLoadImages">
- {#if $tempSettings.setAutoloadImagesPerDevice !== $settings.setAutoloadImagesPerDevice || $tempSettings.autoloadImagesByDefault !== $settings.autoloadImagesByDefault}
- {@render unsavedChanges()}
- {/if}
-
- <h5>{$t('settings.images_title')}</h5>
- {@html $t('settings.images_description')}
-
- <div class="form-check form-switch">
- <input
- class="form-check-input"
- bind:checked={$tempSettings.setAutoloadImagesPerDevice}
- type="checkbox"
- role="switch"
- id="setImageLoadingPerDeviceSwitch"
- />
- <label class="form-check-label" for="setImageLoadingPerDeviceSwitch">
- {$t('settings.images_loading_per_device')}
- </label>
- </div>
+ <div id="functions">
+ <h3 class="text-primary">🛠️ {$t('settings.functions')}</h3>
- <div class="form-check form-switch ms-3">
- <input
- class="form-check-input"
- bind:checked={$autoLoadImagesThisDevice}
- type="checkbox"
- role="switch"
- id="loadImagesThisDeviceSwitch"
- disabled={!$tempSettings.setAutoloadImagesPerDevice}
- />
- <label class="form-check-label" for="loadImagesThisDeviceSwitch">
- {@html $t('settings.images_loading_this_device')}
- </label>
- </div>
+ <div id="autoLoadImages">
+ {#if $tempSettings.setAutoloadImagesPerDevice !== $settings.setAutoloadImagesPerDevice || $tempSettings.autoloadImagesByDefault !== $settings.autoloadImagesByDefault}
+ {@render unsavedChanges()}
+ {/if}
- <div class="form-check form-switch mt-3">
- <input
- class="form-check-input"
- bind:checked={$tempSettings.autoloadImagesByDefault}
- type="checkbox"
- role="switch"
- id="autoLoadImagesSwitch"
- disabled={$tempSettings.setAutoloadImagesPerDevice}
- />
- <label class="form-check-label" for="autoLoadImagesSwitch">
- {$t('settings.images_loading_default')}
- </label>
+ <h5>{$t('settings.images_title')}</h5>
+ {@html $t('settings.images_description')}
+
+ <div class="form-check form-switch">
+ <input
+ class="form-check-input"
+ bind:checked={$tempSettings.setAutoloadImagesPerDevice}
+ type="checkbox"
+ role="switch"
+ id="setImageLoadingPerDeviceSwitch"
+ />
+ <label class="form-check-label" for="setImageLoadingPerDeviceSwitch">
+ {$t('settings.images_loading_per_device')}
+ </label>
+ </div>
+
+ <div class="form-check form-switch ms-3">
+ <input
+ class="form-check-input"
+ bind:checked={$autoLoadImagesThisDevice}
+ type="checkbox"
+ role="switch"
+ id="loadImagesThisDeviceSwitch"
+ disabled={!$tempSettings.setAutoloadImagesPerDevice}
+ />
+ <label class="form-check-label" for="loadImagesThisDeviceSwitch">
+ {@html $t('settings.images_loading_this_device')}
+ </label>
+ </div>
+
+ <div class="form-check form-switch mt-3">
+ <input
+ class="form-check-input"
+ bind:checked={$tempSettings.autoloadImagesByDefault}
+ type="checkbox"
+ role="switch"
+ id="autoLoadImagesSwitch"
+ disabled={$tempSettings.setAutoloadImagesPerDevice}
+ />
+ <label class="form-check-label" for="autoLoadImagesSwitch">
+ {$t('settings.images_loading_default')}
+ </label>
+ </div>
</div>
- </div>
- <div id="language">
- {#if $tempSettings.useBrowserLanguage !== $settings.useBrowserLanguage || $tempSettings.language !== $settings.language}
- {@render unsavedChanges()}
- {/if}
- <h5>🌐 {$t('settings.language')}</h5>
- <div class="form-check mt-2">
- <input
- class="form-check-input"
- type="radio"
- name="language"
- id="language_auto"
- value={true}
- bind:group={$tempSettings.useBrowserLanguage}
- />
- <label class="form-check-label" for="language_auto">
- {$t('settings.language_auto_detect')}
- <code>{window.navigator.language}</code>
- {#if tolgeesMatchForBrowserLanguage() !== '' && tolgeesMatchForBrowserLanguage() !== window.navigator.language}
- ➔ <code>{tolgeesMatchForBrowserLanguage()}</code>
- {$t('settings.language_X_used')}
- {/if}
- </label>
- {#if $tempSettings.useBrowserLanguage && tolgeesMatchForBrowserLanguage() === ''}
- <div
- transition:slide
- disabled={!$settings.useBrowserLanguage}
- class="alert alert-danger"
- role="alert"
- >
- {@html $t('settings.language_not_available', {
- browserLanguage: window.navigator.language,
- defaultLanguage: $tolgee.getInitialOptions().defaultLanguage
- })}
- </div>
+ <div id="language">
+ {#if $tempSettings.useBrowserLanguage !== $settings.useBrowserLanguage || $tempSettings.language !== $settings.language}
+ {@render unsavedChanges()}
{/if}
- </div>
- <div class="form-check mt-2">
- <input
- class="form-check-input"
- type="radio"
- name="language"
- id="language_manual"
- value={false}
- bind:group={$tempSettings.useBrowserLanguage}
- />
- <label class="form-check-label" for="language_manual">
- {$t('settings.set_language_manually')}
- {#if !$tempSettings.useBrowserLanguage}
- <select
+ <h5>🌐 {$t('settings.language')}</h5>
+ <div class="form-check mt-2">
+ <input
+ class="form-check-input"
+ type="radio"
+ name="language"
+ id="language_auto"
+ value={true}
+ bind:group={$tempSettings.useBrowserLanguage}
+ />
+ <label class="form-check-label" for="language_auto">
+ {$t('settings.language_auto_detect')}
+ <code>{window.navigator.language}</code>
+ {#if tolgeesMatchForBrowserLanguage() !== '' && tolgeesMatchForBrowserLanguage() !== window.navigator.language}
+ ➔ <code>{tolgeesMatchForBrowserLanguage()}</code>
+ {$t('settings.language_X_used')}
+ {/if}
+ </label>
+ {#if $tempSettings.useBrowserLanguage && tolgeesMatchForBrowserLanguage() === ''}
+ <div
transition:slide
- class="form-select"
- bind:value={$tempSettings.language}
- disabled={$tempSettings.useBrowserLanguage}
+ disabled={!$settings.useBrowserLanguage}
+ class="alert alert-danger"
+ role="alert"
>
- {#each $tolgee.getInitialOptions().availableLanguages as lang}
- <option value={lang}>{loadFlagEmoji(lang)} {lang}</option>
- {/each}
- </select>
+ {@html $t('settings.language_not_available', {
+ browserLanguage: window.navigator.language,
+ defaultLanguage: $tolgee.getInitialOptions().defaultLanguage
+ })}
+ </div>
{/if}
- </label>
- </div>
- <div class="form-text">
- {$t('settings.language.reload_info')}
- </div>
- </div>
- <div id="timezone">
- {#if $tempSettings.useBrowserTimezone !== $settings.useBrowserTimezone || ($tempSettings.timezone !== undefined && $tempSettings.timezone?.value !== $settings.timezone?.value)}
- {@render unsavedChanges()}
- {/if}
- <h5>{$t('settings.timezone')}</h5>
- {$t('settings.timezone.description', { written_on: $t('log.written_on') })}
-
- <div class="form-check mt-2">
- <input
- class="form-check-input"
- type="radio"
- name="timezone"
- id="timezone1"
- value={true}
- bind:group={$tempSettings.useBrowserTimezone}
- />
- <label class="form-check-label" for="timezone1">
- {@html $t('settings.timezone.auto_detect')}
- <code>{new Intl.DateTimeFormat().resolvedOptions().timeZone}</code>
- </label>
+ </div>
+ <div class="form-check mt-2">
+ <input
+ class="form-check-input"
+ type="radio"
+ name="language"
+ id="language_manual"
+ value={false}
+ bind:group={$tempSettings.useBrowserLanguage}
+ />
+ <label class="form-check-label" for="language_manual">
+ {$t('settings.set_language_manually')}
+ {#if !$tempSettings.useBrowserLanguage}
+ <select
+ transition:slide
+ class="form-select"
+ bind:value={$tempSettings.language}
+ disabled={$tempSettings.useBrowserLanguage}
+ >
+ {#each $tolgee.getInitialOptions().availableLanguages as lang}
+ <option value={lang}>{loadFlagEmoji(lang)} {lang}</option>
+ {/each}
+ </select>
+ {/if}
+ </label>
+ </div>
+ <div class="form-text">
+ {$t('settings.language.reload_info')}
+ </div>
</div>
- <div class="form-check">
- <input
- class="form-check-input"
- type="radio"
- name="timezone"
- id="timezone2"
- value={false}
- bind:group={$tempSettings.useBrowserTimezone}
- />
- <label class="form-check-label" for="timezone2">
- {$t('settings.timezone.manual')}
- </label>
- <br />
- <SelectTimezone />
- {#if !$tempSettings.useBrowserTimezone}
- <span transition:fade>
- {$t('settings.timezone.selected')} <code>{$tempSettings.timezone}</code>
- </span>
+ <div id="timezone">
+ {#if $tempSettings.useBrowserTimezone !== $settings.useBrowserTimezone || ($tempSettings.timezone !== undefined && $tempSettings.timezone?.value !== $settings.timezone?.value)}
+ {@render unsavedChanges()}
{/if}
- </div>
-
- <div class="form-text mt-3">
- {@html $t('settings.timezone.help_text')}
- </div>
- </div>
+ <h5>{$t('settings.timezone')}</h5>
+ {$t('settings.timezone.description', { written_on: $t('log.written_on') })}
- <div id="aLookBack">
- {#if $tempSettings.useALookBack !== $settings.useALookBack || JSON.stringify(aLookBackYears
- .trim()
- .split(',')
- .map( (year) => parseInt(year.trim()) )) !== JSON.stringify($settings.aLookBackYears)}
- {@render unsavedChanges()}
- {/if}
-
- <h5>{$t('settings.aLookBack')}</h5>
- <ul>
- {@html $t('settings.aLookBack.description')}
- </ul>
- <div class="form-check form-switch">
- <input
- class="form-check-input"
- bind:checked={$tempSettings.useALookBack}
- type="checkbox"
- role="switch"
- id="useALookBackSwitch"
- />
- <label class="form-check-label" for="useALookBackSwitch">
- {$t('settings.ALookBack.useIt')}
- </label>
- </div>
+ <div class="form-check mt-2">
+ <input
+ class="form-check-input"
+ type="radio"
+ name="timezone"
+ id="timezone1"
+ value={true}
+ bind:group={$tempSettings.useBrowserTimezone}
+ />
+ <label class="form-check-label" for="timezone1">
+ {@html $t('settings.timezone.auto_detect')}
+ <code>{new Intl.DateTimeFormat().resolvedOptions().timeZone}</code>
+ </label>
+ </div>
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ type="radio"
+ name="timezone"
+ id="timezone2"
+ value={false}
+ bind:group={$tempSettings.useBrowserTimezone}
+ />
+ <label class="form-check-label" for="timezone2">
+ {$t('settings.timezone.manual')}
+ </label>
+ <br />
+ <SelectTimezone />
+ {#if !$tempSettings.useBrowserTimezone}
+ <span transition:fade>
+ {$t('settings.timezone.selected')} <code>{$tempSettings.timezone}</code>
+ </span>
+ {/if}
+ </div>
- <div>
- <input
- type="text"
- id="useALookBackYears"
- class="form-control {aLookBackYearsInvalid ? 'is-invalid' : ''}"
- aria-describedby="useALookBackHelpBlock"
- disabled={!$tempSettings.useALookBack}
- placeholder={$t('settings.ALookBack.input_placeholder')}
- bind:value={aLookBackYears}
- invalid
- />
- {#if aLookBackYearsInvalid}
- <div class="alert alert-danger mt-2" role="alert" transition:slide>
- {$t('settings.ALookBack.invalid_input')}
- </div>
- {/if}
- <div id="useALookBackHelpBlock" class="form-text">
- {@html $t('settings.ALookBack.help_text')}
+ <div class="form-text mt-3">
+ {@html $t('settings.timezone.help_text')}
</div>
</div>
- </div>
- <div id="loginonreload">
- <h5>Login bei Reload</h5>
- Bla<br />
- blub <br />
- bla <br />
- blub <br />
- </div>
- </div>
- <div id="tags">
- <h3 class="text-primary">#️⃣ {$t('settings.tags')}</h3>
- <div>
- {$t('settings.tags.description')}
+ <div id="aLookBack">
+ {#if $tempSettings.useALookBack !== $settings.useALookBack || JSON.stringify(aLookBackYears
+ .trim()
+ .split(',')
+ .map( (year) => parseInt(year.trim()) )) !== JSON.stringify($settings.aLookBackYears)}
+ {@render unsavedChanges()}
+ {/if}
- {#if $tags.length === 0}
- <div class="alert alert-info my-2" role="alert">
- {$t('settings.tags.no_tags')}
+ <h5>{$t('settings.aLookBack')}</h5>
+ <ul>
+ {@html $t('settings.aLookBack.description')}
+ </ul>
+ <div class="form-check form-switch">
+ <input
+ class="form-check-input"
+ bind:checked={$tempSettings.useALookBack}
+ type="checkbox"
+ role="switch"
+ id="useALookBackSwitch"
+ />
+ <label class="form-check-label" for="useALookBackSwitch">
+ {$t('settings.ALookBack.useIt')}
+ </label>
</div>
- {/if}
- <div class="d-flex flex-column tagColumn mt-1">
- {#each $tags as tag}
- <Tag
- {tag}
- isEditable
- editTag={openTagModal}
- isDeletable
- deleteTag={askDeleteTag}
+
+ <div>
+ <input
+ type="text"
+ id="useALookBackYears"
+ class="form-control {aLookBackYearsInvalid ? 'is-invalid' : ''}"
+ aria-describedby="useALookBackHelpBlock"
+ disabled={!$tempSettings.useALookBack}
+ placeholder={$t('settings.ALookBack.input_placeholder')}
+ bind:value={aLookBackYears}
+ invalid
/>
- {#if deleteTagId === tag.id}
- <div
- class="alert alert-danger align-items-center"
- role="alert"
- transition:slide
- >
- <div>
- <Fa icon={faTriangleExclamation} fw />
- {@html $t('settings.tags.delete_confirmation')}
- </div>
- <!-- svelte-ignore a11y_consider_explicit_label -->
- <div class="d-flex flex-row mt-2">
- <button class="btn btn-secondary" onclick={() => (deleteTagId = null)}
- >{$t('settings.abort')}
- </button>
- <button
- disabled={isDeletingTag}
- class="btn btn-danger ms-3"
- onclick={() => deleteTag(tag.id)}
- >{$t('settings.delete')}
- {#if isDeletingTag}
- <span
- class="spinner-border spinner-border-sm ms-2"
- role="status"
- aria-hidden="true"
- ></span>
- {/if}
- </button>
- </div>
+ {#if aLookBackYearsInvalid}
+ <div class="alert alert-danger mt-2" role="alert" transition:slide>
+ {$t('settings.ALookBack.invalid_input')}
</div>
{/if}
- {/each}
+ <div id="useALookBackHelpBlock" class="form-text">
+ {@html $t('settings.ALookBack.help_text')}
+ </div>
+ </div>
+ </div>
+ <div id="loginonreload">
+ <h5>Login bei Reload</h5>
+ Bla<br />
+ blub <br />
+ bla <br />
+ blub <br />
</div>
</div>
- </div>
- <div id="templates">
- <h3 class="text-primary">📝 {$t('settings.templates')}</h3>
- <div>
- {#if oldTemplateName !== templateName || oldTemplateText !== templateText}
- {@render unsavedChanges()}
- {/if}
-
- <div class="d-flex flex-column">
- <select
- bind:value={selectedTemplate}
- class="form-select"
- aria-label="Select template"
- onchange={updateSelectedTemplate}
- >
- <option value="-1" selected={selectedTemplate === '-1'}>
- {$t('settings.templates.create_new')}
- </option>
- {#each $templates as template, index}
- <option value={index} selected={index === selectedTemplate}>
- {template.name}
- </option>
+ <div id="tags">
+ <h3 class="text-primary">#️⃣ {$t('settings.tags')}</h3>
+ <div>
+ {$t('settings.tags.description')}
+
+ {#if $tags.length === 0}
+ <div class="alert alert-info my-2" role="alert">
+ {$t('settings.tags.no_tags')}
+ </div>
+ {/if}
+ <div class="d-flex flex-column tagColumn mt-1">
+ {#each $tags as tag}
+ <Tag
+ {tag}
+ isEditable
+ editTag={openTagModal}
+ isDeletable
+ deleteTag={askDeleteTag}
+ />
+ {#if deleteTagId === tag.id}
+ <div
+ class="alert alert-danger align-items-center"
+ role="alert"
+ transition:slide
+ >
+ <div>
+ <Fa icon={faTriangleExclamation} fw />
+ {@html $t('settings.tags.delete_confirmation')}
+ </div>
+ <!-- svelte-ignore a11y_consider_explicit_label -->
+ <div class="d-flex flex-row mt-2">
+ <button class="btn btn-secondary" onclick={() => (deleteTagId = null)}
+ >{$t('settings.abort')}
+ </button>
+ <button
+ disabled={isDeletingTag}
+ class="btn btn-danger ms-3"
+ onclick={() => deleteTag(tag.id)}
+ >{$t('settings.delete')}
+ {#if isDeletingTag}
+ <span
+ class="spinner-border spinner-border-sm ms-2"
+ role="status"
+ aria-hidden="true"
+ ></span>
+ {/if}
+ </button>
+ </div>
+ </div>
+ {/if}
{/each}
- </select>
+ </div>
</div>
+ </div>
+
+ <div id="templates">
+ <h3 class="text-primary">📝 {$t('settings.templates')}</h3>
+ <div>
+ {#if oldTemplateName !== templateName || oldTemplateText !== templateText}
+ {@render unsavedChanges()}
+ {/if}
+
+ <div class="d-flex flex-column">
+ <select
+ bind:value={selectedTemplate}
+ class="form-select"
+ aria-label="Select template"
+ onchange={updateSelectedTemplate}
+ >
+ <option value="-1" selected={selectedTemplate === '-1'}>
+ {$t('settings.templates.create_new')}
+ </option>
+ {#each $templates as template, index}
+ <option value={index} selected={index === selectedTemplate}>
+ {template.name}
+ </option>
+ {/each}
+ </select>
+ </div>
- <hr />
+ <hr />
- {#if confirmDeleteTemplate}
- <div transition:slide class="d-flex flex-row align-items-center mb-2">
- <span>
- {@html $t('settings.templates.delete_confirmation', {
- template: $templates[selectedTemplate]?.name
- })}
- </span>
+ {#if confirmDeleteTemplate}
+ <div transition:slide class="d-flex flex-row align-items-center mb-2">
+ <span>
+ {@html $t('settings.templates.delete_confirmation', {
+ template: $templates[selectedTemplate]?.name
+ })}
+ </span>
+ <button
+ type="button"
+ class="btn btn-secondary ms-2"
+ onclick={() => (confirmDeleteTemplate = false)}
+ >{$t('settings.abort')}</button
+ >
+ <button
+ type="button"
+ class="btn btn-danger ms-2"
+ onclick={() => {
+ deleteTemplate();
+ }}
+ disabled={isDeletingTemplate}
+ >{$t('settings.delete')}
+ {#if isDeletingTemplate}
+ <span
+ class="spinner-border spinner-border-sm ms-2"
+ role="status"
+ aria-hidden="true"
+ ></span>
+ {/if}
+ </button>
+ </div>
+ {/if}
+ <div class="d-flex flex-row">
+ <input
+ disabled={selectedTemplate === null}
+ type="text"
+ bind:value={templateName}
+ class="form-control"
+ placeholder={$t('settings.template.name_of_template')}
+ />
<button
+ disabled={selectedTemplate === '-1' || selectedTemplate === null}
type="button"
- class="btn btn-secondary ms-2"
- onclick={() => (confirmDeleteTemplate = false)}
- >{$t('settings.abort')}</button
+ class="btn btn-outline-danger ms-5"
+ onclick={() => {
+ confirmDeleteTemplate = !confirmDeleteTemplate;
+ }}><Fa fw icon={faTrash} /></button
>
+ </div>
+ <textarea
+ disabled={selectedTemplate === null}
+ bind:value={templateText}
+ class="form-control mt-2"
+ rows="10"
+ placeholder={$t('settings.template.content_of_template')}
+ >
+ </textarea>
+ <div class="d-flex justify-content-end">
<button
+ disabled={(oldTemplateName === templateName &&
+ oldTemplateText === templateText) ||
+ isSavingTemplate}
type="button"
- class="btn btn-danger ms-2"
- onclick={() => {
- deleteTemplate();
- }}
- disabled={isDeletingTemplate}
- >{$t('settings.delete')}
- {#if isDeletingTemplate}
+ class="btn btn-primary mt-2"
+ onclick={saveTemplate}
+ >
+ {$t('settings.template.save_template')}
+ {#if isSavingTemplate}
<span
class="spinner-border spinner-border-sm ms-2"
role="status"
{/if}
</button>
</div>
- {/if}
- <div class="d-flex flex-row">
- <input
- disabled={selectedTemplate === null}
- type="text"
- bind:value={templateName}
- class="form-control"
- placeholder={$t('settings.template.name_of_template')}
- />
- <button
- disabled={selectedTemplate === '-1' || selectedTemplate === null}
- type="button"
- class="btn btn-outline-danger ms-5"
- onclick={() => {
- confirmDeleteTemplate = !confirmDeleteTemplate;
- }}><Fa fw icon={faTrash} /></button
- >
- </div>
- <textarea
- disabled={selectedTemplate === null}
- bind:value={templateText}
- class="form-control mt-2"
- rows="10"
- placeholder={$t('settings.template.content_of_template')}
- >
- </textarea>
- <div class="d-flex justify-content-end">
- <button
- disabled={(oldTemplateName === templateName &&
- oldTemplateText === templateText) ||
- isSavingTemplate}
- type="button"
- class="btn btn-primary mt-2"
- onclick={saveTemplate}
- >
- {$t('settings.template.save_template')}
- {#if isSavingTemplate}
- <span
- class="spinner-border spinner-border-sm ms-2"
- role="status"
- aria-hidden="true"
- ></span>
- {/if}
- </button>
</div>
</div>
- </div>
-
- <div id="data">
- <h3 class="text-primary">📁 {$t('settings.data')}</h3>
- <div>
- <h5>{$t('settings.export')}</h5>
- {$t('settings.export.description')}
-
- <h6>{$t('settings.export.period')}</h6>
- <div class="form-check">
- <input
- class="form-check-input"
- type="radio"
- name="period"
- value="periodAll"
- id="periodAll"
- bind:group={exportPeriod}
- />
- <label class="form-check-label" for="periodAll"
- >{$t('settings.export.period_all')}</label
- >
- </div>
- <div class="form-check">
- <input
- class="form-check-input"
- type="radio"
- name="period"
- value="periodVariable"
- id="periodVariable"
- bind:group={exportPeriod}
- />
- <label class="form-check-label" for="periodVariable">
- {$t('settings.export.period_variable')}</label
- >
- {#if exportPeriod === 'periodVariable'}
- <div class="d-flex flex-row" transition:slide>
- <div class="me-2">
- <label for="exportStartDate">{$t('settings.export.start_date')}</label>
- <input
- type="date"
- class="form-control me-2"
- id="exportStartDate"
- bind:value={exportStartDate}
- />
- </div>
- <div>
- <label for="exportEndDate">{$t('settings.export.end_date')}</label>
- <input
- type="date"
- class="form-control"
- id="exportEndDate"
- bind:value={exportEndDate}
- />
- </div>
- </div>
- {#if exportStartDate !== '' && exportEndDate !== '' && exportStartDate > exportEndDate}
- <div class="alert alert-danger mt-2" role="alert" transition:slide>
- {$t('settings.export.period_invalid')}
- </div>
- {/if}
- {/if}
- </div>
-
- <h6>{$t('settings.export.split')}</h6>
- <div class="form-check">
- <input
- class="form-check-input"
- type="radio"
- name="split"
- value="aio"
- id="splitAIO"
- bind:group={exportSplit}
- />
- <label class="form-check-label" for="splitAIO"
- >{$t('settings.export.split_aio')}
- </label>
- </div>
- <div class="form-check">
- <input
- class="form-check-input"
- type="radio"
- name="split"
- value="year"
- id="splitYear"
- bind:group={exportSplit}
- />
- <label class="form-check-label" for="splitYear"
- >{$t('settings.export.split_year')}
- </label>
- </div>
- <div class="form-check">
- <input
- class="form-check-input"
- type="radio"
- name="split"
- value="month"
- id="splitMonth"
- bind:group={exportSplit}
- />
- <label class="form-check-label" for="splitMonth"
- >{$t('settings.export.split_month')}
- </label>
- </div>
- <h6>{$t('settings.export.show_images')}</h6>
- <div class="form-check">
- <input
- class="form-check-input"
- type="checkbox"
- name="images"
- id="exportImagesInHTML"
- bind:checked={exportImagesInHTML}
- />
- <label class="form-check-label" for="exportImagesInHTML">
- {@html $t('settings.export.show_images_description')}
- </label>
- </div>
-
- <h6>{$t('settings.export.show_tags')}</h6>
- <div class="form-check">
- <input
- class="form-check-input"
- type="checkbox"
- id="exportTagsInHTML"
- bind:checked={exportTagsInHTML}
- />
- <label class="form-check-label" for="exportTagsInHTML">
- {$t('settings.export.show_tags_description')}
- </label>
- </div>
-
- <div class="form-text">
- {@html $t('settings.export.help_text')}
- </div>
- <button
- class="btn btn-primary mt-3"
- onclick={exportData}
- data-sveltekit-noscroll
- disabled={isExporting ||
- (exportPeriod === 'periodVariable' &&
- (exportStartDate === '' || exportEndDate === ''))}
- >
- {$t('settings.export.export_button')}
- {#if isExporting}
- <div class="spinner-border spinner-border-sm ms-2" role="status">
- <span class="visually-hidden">Loading...</span>
- </div>
- {/if}
- </button>
- </div>
- <div><h5>Import</h5></div>
- </div>
+ <div id="data">
+ <h3 class="text-primary">📁 {$t('settings.data')}</h3>
+ <div>
+ <h5>{$t('settings.export')}</h5>
+ {$t('settings.export.description')}
- <div id="security">
- <h3 class="text-primary">🔒 {$t('settings.security')}</h3>
- <div>
- <h5>{$t('settings.security.change_password')}</h5>
- <form onsubmit={changePassword}>
- <div class="form-floating mb-3">
+ <h6>{$t('settings.export.period')}</h6>
+ <div class="form-check">
<input
- type="password"
- class="form-control"
- id="currentPassword"
- placeholder={$t('settings.password.current_password')}
- bind:value={currentPassword}
+ class="form-check-input"
+ type="radio"
+ name="period"
+ value="periodAll"
+ id="periodAll"
+ bind:group={exportPeriod}
/>
- <label for="currentPassword">{$t('settings.password.current_password')}</label
+ <label class="form-check-label" for="periodAll"
+ >{$t('settings.export.period_all')}</label
>
</div>
- <div class="form-floating mb-3">
- <input
- type="password"
- class="form-control"
- id="newPassword"
- placeholder={$t('settings.password.new_password')}
- bind:value={newPassword}
- />
- <label for="newPassword">{$t('settings.password.new_password')}</label>
- </div>
- <div class="form-floating mb-3">
+ <div class="form-check">
<input
- type="password"
- class="form-control"
- id="confirmNewPassword"
- placeholder={$t('settings.password.confirm_new_password')}
- bind:value={confirmNewPassword}
+ class="form-check-input"
+ type="radio"
+ name="period"
+ value="periodVariable"
+ id="periodVariable"
+ bind:group={exportPeriod}
/>
- <label for="confirmNewPassword"
- >{$t('settings.password.confirm_new_password')}</label
+ <label class="form-check-label" for="periodVariable">
+ {$t('settings.export.period_variable')}</label
>
- </div>
- <button class="btn btn-primary" onclick={changePassword}>
- {#if isChangingPassword}
- <!-- svelte-ignore a11y_no_static_element_interactions -->
- <div class="spinner-border" role="status">
- <span class="visually-hidden">Loading...</span>
+ {#if exportPeriod === 'periodVariable'}
+ <div class="d-flex flex-row" transition:slide>
+ <div class="me-2">
+ <label for="exportStartDate">{$t('settings.export.start_date')}</label>
+ <input
+ type="date"
+ class="form-control me-2"
+ id="exportStartDate"
+ bind:value={exportStartDate}
+ />
+ </div>
+ <div>
+ <label for="exportEndDate">{$t('settings.export.end_date')}</label>
+ <input
+ type="date"
+ class="form-control"
+ id="exportEndDate"
+ bind:value={exportEndDate}
+ />
+ </div>
</div>
+ {#if exportStartDate !== '' && exportEndDate !== '' && exportStartDate > exportEndDate}
+ <div class="alert alert-danger mt-2" role="alert" transition:slide>
+ {$t('settings.export.period_invalid')}
+ </div>
+ {/if}
{/if}
- {$t('settings.password.change_password_button')}
- </button>
- </form>
- {#if changePasswordNotEqual}
- <div class="alert alert-danger mt-2" role="alert" transition:slide>
- {$t('settings.password.passwords_dont_match')}
</div>
- {/if}
- {#if changingPasswordSuccess}
- <div class="alert alert-success mt-2" role="alert" transition:slide>
- {@html $t('settings.password.success')}
- </div>
- {/if}
- {#if changingPasswordIncorrect}
- <div class="alert alert-danger mt-2" role="alert" transition:slide>
- {$t('settings.password.current_password_incorrect')}
- </div>
- {:else if changingPasswordError}
- <div class="alert alert-danger mt-2" role="alert" transition:slide>
- {$t('settings.password.change_error')}
+
+ <h6>{$t('settings.export.split')}</h6>
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ type="radio"
+ name="split"
+ value="aio"
+ id="splitAIO"
+ bind:group={exportSplit}
+ />
+ <label class="form-check-label" for="splitAIO"
+ >{$t('settings.export.split_aio')}
+ </label>
</div>
- {/if}
- </div>
- <div>
- <h5>{$t('settings.backup_codes')}</h5>
- <ul>
- {@html $t('settings.backup_codes.description')}
- </ul>
-
- <form onsubmit={createBackupCodes}>
- <div class="form-floating mb-3">
+ <div class="form-check">
<input
- type="password"
- class="form-control"
- id="currentPassword"
- placeholder={$t('settings.password.current_password')}
- bind:value={backupCodesPassword}
+ class="form-check-input"
+ type="radio"
+ name="split"
+ value="year"
+ id="splitYear"
+ bind:group={exportSplit}
/>
- <label for="currentPassword">{$t('settings.password.confirm_password')}</label
- >
+ <label class="form-check-label" for="splitYear"
+ >{$t('settings.export.split_year')}
+ </label>
</div>
- <button
- class="btn btn-primary"
- onclick={createBackupCodes}
- data-sveltekit-noscroll
- >
- {$t('settings.backup_codes.generate_button')}
- {#if isGeneratingBackupCodes}
- <div class="spinner-border spinner-border-sm" role="status">
- <span class="visually-hidden">Loading...</span>
- </div>
- {/if}
- </button>
- </form>
- {#if backupCodes.length > 0}
- <div class="alert alert-success alert-dismissible mt-3" transition:slide>
- {@html $t('settings.backup_codes.success')}
-
- <button class="btn btn-secondary my-2" onclick={copyBackupCodes}>
- <Fa icon={codesCopiedSuccess ? faCheck : faCopy} />
- {$t('settings.backup_codes.copy_button')}
- </button>
- <ul class="list-group">
- {#each backupCodes as code}
- <li class="list-group-item backupCode">
- <code>{code}</code>
- </li>
- {/each}
- </ul>
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ type="radio"
+ name="split"
+ value="month"
+ id="splitMonth"
+ bind:group={exportSplit}
+ />
+ <label class="form-check-label" for="splitMonth"
+ >{$t('settings.export.split_month')}
+ </label>
</div>
- {/if}
- {#if showBackupCodesError}
- <div class="alert alert-danger mt-2" role="alert" transition:slide>
- {$t('settings.backup_codes.error')}
+
+ <h6>{$t('settings.export.show_images')}</h6>
+ <div class="form-check">
+ <input
+ class="form-check-input"
+ type="checkbox"
+ name="images"
+ id="exportImagesInHTML"
+ bind:checked={exportImagesInHTML}
+ />
+ <label class="form-check-label" for="exportImagesInHTML">
+ {@html $t('settings.export.show_images_description')}
+ </label>
</div>
- {/if}
- </div>
- <div><h5>Username ändern</h5></div>
- <div>
- <h5>{$t('settings.delete_account')}</h5>
- <p>
- {$t('settings.delete_account.description')}
- </p>
- <form
- onsubmit={() => {
- showConfirmDeleteAccount = true;
- }}
- >
- <div class="form-floating mb-3">
+
+ <h6>{$t('settings.export.show_tags')}</h6>
+ <div class="form-check">
<input
- type="password"
- class="form-control"
- id="currentPassword"
- placeholder={$t('settings.password.current_password')}
- bind:value={deleteAccountPassword}
+ class="form-check-input"
+ type="checkbox"
+ id="exportTagsInHTML"
+ bind:checked={exportTagsInHTML}
/>
- <label for="currentPassword">{$t('settings.password.confirm_password')}</label
- >
+ <label class="form-check-label" for="exportTagsInHTML">
+ {$t('settings.export.show_tags_description')}
+ </label>
+ </div>
+
+ <div class="form-text">
+ {@html $t('settings.export.help_text')}
</div>
<button
- class="btn btn-danger"
- onclick={() => {
- showConfirmDeleteAccount = true;
- }}
+ class="btn btn-primary mt-3"
+ onclick={exportData}
data-sveltekit-noscroll
+ disabled={isExporting ||
+ (exportPeriod === 'periodVariable' &&
+ (exportStartDate === '' || exportEndDate === ''))}
>
- {$t('settings.delete_account.delete_button')}
- {#if isDeletingAccount}
- <!-- svelte-ignore a11y_no_static_element_interactions -->
- <div class="spinner-border" role="status">
+ {$t('settings.export.export_button')}
+ {#if isExporting}
+ <div class="spinner-border spinner-border-sm ms-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
{/if}
</button>
- </form>
- {#if showDeleteAccountSuccess}
- <div class="alert alert-success mt-2" role="alert" transition:slide>
- {@html $t('settings.delete_account.success')}
- </div>
- {/if}
- {#if deleteAccountPasswordIncorrect}
- <div class="alert alert-danger mt-2" role="alert" transition:slide>
- {$t('settings.delete_account.password_incorrect')}
- </div>
- {/if}
- {#if showConfirmDeleteAccount}
- <div class="alert alert-danger mt-2" role="alert" transition:slide>
- {$t('settings.delete_account.confirm')}
+ </div>
+ <div><h5>Import</h5></div>
+ </div>
- <div class="d-flex flex-row mt-2">
- <button
- class="btn btn-secondary"
- onclick={() => {
- showConfirmDeleteAccount = false;
- deleteAccountPassword = '';
- }}>{$t('settings.abort')}</button
+ <div id="security">
+ <h3 class="text-primary">🔒 {$t('settings.security')}</h3>
+ <div>
+ <h5>{$t('settings.security.change_password')}</h5>
+ <form onsubmit={changePassword}>
+ <div class="form-floating mb-3">
+ <input
+ type="password"
+ class="form-control"
+ id="currentPassword"
+ placeholder={$t('settings.password.current_password')}
+ bind:value={currentPassword}
+ />
+ <label for="currentPassword"
+ >{$t('settings.password.current_password')}</label
>
- <button
- class="btn btn-danger ms-3"
- onclick={deleteAccount}
- disabled={isDeletingAccount}
- >{$t('settings.delete_account.confirm_button')}
- {#if isDeletingAccount}
- <span
- class="spinner-border spinner-border-sm ms-2"
- role="status"
- aria-hidden="true"
- ></span>
- {/if}
+ </div>
+ <div class="form-floating mb-3">
+ <input
+ type="password"
+ class="form-control"
+ id="newPassword"
+ placeholder={$t('settings.password.new_password')}
+ bind:value={newPassword}
+ />
+ <label for="newPassword">{$t('settings.password.new_password')}</label>
+ </div>
+ <div class="form-floating mb-3">
+ <input
+ type="password"
+ class="form-control"
+ id="confirmNewPassword"
+ placeholder={$t('settings.password.confirm_new_password')}
+ bind:value={confirmNewPassword}
+ />
+ <label for="confirmNewPassword"
+ >{$t('settings.password.confirm_new_password')}</label
+ >
+ </div>
+ <button class="btn btn-primary" onclick={changePassword}>
+ {#if isChangingPassword}
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
+ <div class="spinner-border" role="status">
+ <span class="visually-hidden">Loading...</span>
+ </div>
+ {/if}
+ {$t('settings.password.change_password_button')}
+ </button>
+ </form>
+ {#if changePasswordNotEqual}
+ <div class="alert alert-danger mt-2" role="alert" transition:slide>
+ {$t('settings.password.passwords_dont_match')}
+ </div>
+ {/if}
+ {#if changingPasswordSuccess}
+ <div class="alert alert-success mt-2" role="alert" transition:slide>
+ {@html $t('settings.password.success')}
+ </div>
+ {/if}
+ {#if changingPasswordIncorrect}
+ <div class="alert alert-danger mt-2" role="alert" transition:slide>
+ {$t('settings.password.current_password_incorrect')}
+ </div>
+ {:else if changingPasswordError}
+ <div class="alert alert-danger mt-2" role="alert" transition:slide>
+ {$t('settings.password.change_error')}
+ </div>
+ {/if}
+ </div>
+ <div>
+ <h5>{$t('settings.backup_codes')}</h5>
+ <ul>
+ {@html $t('settings.backup_codes.description')}
+ </ul>
+
+ <form onsubmit={createBackupCodes}>
+ <div class="form-floating mb-3">
+ <input
+ type="password"
+ class="form-control"
+ id="currentPassword"
+ placeholder={$t('settings.password.current_password')}
+ bind:value={backupCodesPassword}
+ />
+ <label for="currentPassword"
+ >{$t('settings.password.confirm_password')}</label
+ >
+ </div>
+ <button
+ class="btn btn-primary"
+ onclick={createBackupCodes}
+ data-sveltekit-noscroll
+ >
+ {$t('settings.backup_codes.generate_button')}
+ {#if isGeneratingBackupCodes}
+ <div class="spinner-border spinner-border-sm" role="status">
+ <span class="visually-hidden">Loading...</span>
+ </div>
+ {/if}
+ </button>
+ </form>
+ {#if backupCodes.length > 0}
+ <div class="alert alert-success alert-dismissible mt-3" transition:slide>
+ {@html $t('settings.backup_codes.success')}
+
+ <button class="btn btn-secondary my-2" onclick={copyBackupCodes}>
+ <Fa icon={codesCopiedSuccess ? faCheck : faCopy} />
+ {$t('settings.backup_codes.copy_button')}
</button>
+ <ul class="list-group">
+ {#each backupCodes as code}
+ <li class="list-group-item backupCode">
+ <code>{code}</code>
+ </li>
+ {/each}
+ </ul>
</div>
- </div>
- {/if}
+ {/if}
+ {#if showBackupCodesError}
+ <div class="alert alert-danger mt-2" role="alert" transition:slide>
+ {$t('settings.backup_codes.error')}
+ </div>
+ {/if}
+ </div>
+ <div><h5>Username ändern</h5></div>
+ <div>
+ <h5>{$t('settings.delete_account')}</h5>
+ <p>
+ {$t('settings.delete_account.description')}
+ </p>
+ <form
+ onsubmit={() => {
+ showConfirmDeleteAccount = true;
+ }}
+ >
+ <div class="form-floating mb-3">
+ <input
+ type="password"
+ class="form-control"
+ id="currentPassword"
+ placeholder={$t('settings.password.current_password')}
+ bind:value={deleteAccountPassword}
+ />
+ <label for="currentPassword"
+ >{$t('settings.password.confirm_password')}</label
+ >
+ </div>
+ <button
+ class="btn btn-danger"
+ onclick={() => {
+ showConfirmDeleteAccount = true;
+ }}
+ data-sveltekit-noscroll
+ >
+ {$t('settings.delete_account.delete_button')}
+ {#if isDeletingAccount}
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
+ <div class="spinner-border" role="status">
+ <span class="visually-hidden">Loading...</span>
+ </div>
+ {/if}
+ </button>
+ </form>
+ {#if showDeleteAccountSuccess}
+ <div class="alert alert-success mt-2" role="alert" transition:slide>
+ {@html $t('settings.delete_account.success')}
+ </div>
+ {/if}
+ {#if deleteAccountPasswordIncorrect}
+ <div class="alert alert-danger mt-2" role="alert" transition:slide>
+ {$t('settings.delete_account.password_incorrect')}
+ </div>
+ {/if}
+ {#if showConfirmDeleteAccount}
+ <div class="alert alert-danger mt-2" role="alert" transition:slide>
+ {$t('settings.delete_account.confirm')}
+
+ <div class="d-flex flex-row mt-2">
+ <button
+ class="btn btn-secondary"
+ onclick={() => {
+ showConfirmDeleteAccount = false;
+ deleteAccountPassword = '';
+ }}>{$t('settings.abort')}</button
+ >
+ <button
+ class="btn btn-danger ms-3"
+ onclick={deleteAccount}
+ disabled={isDeletingAccount}
+ >{$t('settings.delete_account.confirm_button')}
+ {#if isDeletingAccount}
+ <span
+ class="spinner-border spinner-border-sm ms-2"
+ role="status"
+ aria-hidden="true"
+ ></span>
+ {/if}
+ </button>
+ </div>
+ </div>
+ {/if}
+ </div>
</div>
- </div>
- <div id="about">
- <h3 class="text-primary">💡 {$t('settings.about')}</h3>
- Version:<br />
- Changelog: <br />
- Link zu github
+ <div id="about">
+ <h3 class="text-primary">💡 {$t('settings.about')}</h3>
+ Version:<br />
+ Changelog: <br />
+ Link zu github
+ </div>
</div>
</div>
- </div>
+ {/if}
+ {#if activeSettingsView === 'stats'}
+ <div class="col-12">
+ <Statistics />
+ </div>
+ {/if}
+ {#if activeSettingsView === 'admin'}
+ <div class="col-12">
+ <Admin />
+ </div>
+ {/if}
</div>
</div>
<div class="modal-footer">
- {#if settingsHaveChanged}
- <div class="footer-unsaved-changes" transition:fade={{ duration: 100 }}>
- {$t('settings.unsaved_changes')}
- </div>
- {/if}
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
- >{$t('settings.abort')}</button
- >
- <button
- type="button"
- class="btn btn-primary"
- onclick={saveUserSettings}
- disabled={isSaving || !settingsHaveChanged}
- >{$t('settings.save')}
- {#if isSaving}
- <span class="spinner-border spinner-border-sm ms-2" role="status" aria-hidden="true"
- ></span>
+ {#if activeSettingsView === 'settings'}
+ {#if settingsHaveChanged}
+ <div class="footer-unsaved-changes" transition:fade={{ duration: 100 }}>
+ {$t('settings.unsaved_changes')}
+ </div>
{/if}
- </button>
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
+ >{$t('settings.abort')}</button
+ >
+ <button
+ type="button"
+ class="btn btn-primary"
+ onclick={saveUserSettings}
+ disabled={isSaving || !settingsHaveChanged}
+ >{$t('settings.save')}
+ {#if isSaving}
+ <span class="spinner-border spinner-border-sm ms-2" role="status" aria-hidden="true"
+ ></span>
+ {/if}
+ </button>
+ {:else}
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
+ {$t('settings.close') || 'Close'}
+ </button>
+ {/if}
</div>
</div>
</div>