feat(settings): sliders taille texte/menu/icônes/miniatures + CSS vars globales

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 20:12:22 +01:00
parent 0d3bf205b1
commit 155de270dc
2 changed files with 199 additions and 10 deletions

View File

@@ -28,8 +28,8 @@
class="flex items-center gap-3 text-text-muted hover:text-text py-2 px-3 rounded-lg text-sm transition-colors group"
active-class="bg-bg-soft text-green font-medium"
>
<span class="text-base leading-none">{{ l.icon }}</span>
<span>{{ l.label }}</span>
<span :style="`font-size: var(--ui-menu-icon-size, 18px); line-height: 1`">{{ l.icon }}</span>
<span :style="`font-size: var(--ui-menu-font-size, 13px)`">{{ l.label }}</span>
</RouterLink>
</nav>
<div class="px-4 py-4 border-t border-bg-soft text-text-muted text-xs">
@@ -38,7 +38,7 @@
</aside>
<!-- Main content -->
<main class="pt-14 lg:pt-0 lg:pl-60 min-h-screen w-full">
<main class="pt-14 lg:pt-0 lg:pl-60 min-h-screen w-full" style="font-size: var(--ui-font-size, 14px)">
<RouterView />
</main>
</div>
@@ -123,11 +123,22 @@ function startDebugPolling() {
}, 10000)
}
function applyUiSizesFromSettings(data: Record<string, string>) {
const defaults: Record<string, number> = { ui_font_size: 14, ui_menu_font_size: 13, ui_menu_icon_size: 18, ui_thumb_size: 96 }
const root = document.documentElement
for (const [key, def] of Object.entries(defaults)) {
const val = Number(data[key]) || def
const prop = '--' + key.replace(/_/g, '-')
root.style.setProperty(prop, `${val}px`)
}
}
async function loadDebugModeFromApi() {
try {
const data = await settingsApi.get()
debugMode.value = toBool(data.debug_mode)
localStorage.setItem('debug_mode', debugMode.value ? '1' : '0')
applyUiSizesFromSettings(data)
} catch {
// On garde la valeur locale.
}

View File

@@ -2,6 +2,38 @@
<div class="p-4 max-w-3xl mx-auto">
<h1 class="text-2xl font-bold text-green mb-4">Réglages</h1>
<section class="bg-bg-soft border border-bg-hard rounded-xl p-4 mb-4">
<h2 class="text-text font-semibold mb-2">Interface</h2>
<p class="text-text-muted text-sm mb-4">Ajustez les tailles d'affichage. Les changements sont appliqués instantanément.</p>
<div class="grid grid-cols-1 gap-4">
<div v-for="s in uiSizeSettings" :key="s.key" class="flex items-center gap-3">
<label class="text-sm text-text w-44 shrink-0">{{ s.label }}</label>
<input
type="range"
:min="s.min" :max="s.max" :step="s.step"
v-model.number="uiSizes[s.key]"
class="flex-1 accent-green"
@input="applyUiSizes"
/>
<span class="text-text-muted text-xs w-12 text-right">{{ uiSizes[s.key] }}{{ s.unit }}</span>
</div>
</div>
<div class="mt-4 flex items-center gap-2">
<button
class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
:disabled="savingUi"
@click="saveUiSettings"
>{{ savingUi ? 'Enregistrement...' : 'Enregistrer' }}</button>
<button
class="text-text-muted text-xs hover:text-text px-2"
@click="resetUiSettings"
>Réinitialiser</button>
<span v-if="uiSavedMsg" class="text-xs text-aqua">{{ uiSavedMsg }}</span>
</div>
</section>
<section class="bg-bg-soft border border-bg-hard rounded-xl p-4 mb-4">
<h2 class="text-text font-semibold mb-2">Général</h2>
<p class="text-text-muted text-sm mb-3">Options globales de l'application.</p>
@@ -35,14 +67,49 @@
</button>
</section>
<section class="bg-bg-soft border border-bg-hard rounded-xl p-4 mb-4">
<h2 class="text-text font-semibold mb-2">Test API backend</h2>
<p class="text-text-muted text-sm mb-2">
Ouvre la documentation interactive de l'API et un test rapide de santé.
</p>
<p class="text-text-muted text-xs mb-3">Base API détectée: <span class="text-text">{{ apiBaseUrl }}</span></p>
<div class="flex flex-wrap items-center gap-2">
<button
class="bg-blue text-bg px-3 py-2 rounded-lg text-xs font-semibold hover:opacity-90"
@click="openApiDocs"
>
Ouvrir Swagger (/docs)
</button>
<button
class="bg-aqua text-bg px-3 py-2 rounded-lg text-xs font-semibold hover:opacity-90"
@click="openApiRedoc"
>
Ouvrir ReDoc (/redoc)
</button>
<button
class="bg-bg border border-bg-hard text-text px-3 py-2 rounded-lg text-xs font-semibold hover:border-text-muted"
@click="openApiHealth"
>
Tester /api/health
</button>
</div>
</section>
<section class="bg-bg-soft border border-bg-hard rounded-xl p-4">
<h2 class="text-text font-semibold mb-2">Idées utiles (prochaine étape)</h2>
<ul class="text-text-muted text-sm space-y-1">
<li>• Sauvegarde/restauration JSON de la base métier</li>
<li>• Rotation/nettoyage des médias anciens</li>
<li>• Choix des unités météo (°C, mm, km/h)</li>
<li>• Paramètres de seuils alertes (gel, pluie, vent)</li>
</ul>
<h2 class="text-text font-semibold mb-2">Sauvegarde des données</h2>
<p class="text-text-muted text-sm mb-3">
Exporte un ZIP téléchargeable contenant la base SQLite, les images/vidéos uploadées et les fichiers texte utiles.
</p>
<div class="flex items-center gap-2">
<button
class="bg-aqua text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
:disabled="downloadingBackup"
@click="downloadBackup"
>
{{ downloadingBackup ? 'Préparation du ZIP...' : 'Télécharger la sauvegarde (.zip)' }}
</button>
<span v-if="backupMsg" class="text-xs text-aqua">{{ backupMsg }}</span>
</div>
</section>
</div>
</template>
@@ -56,6 +123,90 @@ const debugMode = ref(false)
const saving = ref(false)
const savedMsg = ref('')
const refreshingMeteo = ref(false)
const downloadingBackup = ref(false)
const backupMsg = ref('')
const apiBaseUrl = detectApiBaseUrl()
// --- UI Size settings ---
const UI_DEFAULTS: Record<string, number> = {
ui_font_size: 14,
ui_menu_font_size: 13,
ui_menu_icon_size: 18,
ui_thumb_size: 96,
}
const uiSizeSettings = [
{ key: 'ui_font_size', label: 'Taille texte', min: 12, max: 20, step: 1, unit: 'px' },
{ key: 'ui_menu_font_size', label: 'Texte menu latéral', min: 11, max: 18, step: 1, unit: 'px' },
{ key: 'ui_menu_icon_size', label: 'Icônes menu', min: 14, max: 28, step: 1, unit: 'px' },
{ key: 'ui_thumb_size', label: 'Miniatures images/vidéo', min: 60, max: 200, step: 4, unit: 'px' },
]
const uiSizes = ref<Record<string, number>>({ ...UI_DEFAULTS })
const savingUi = ref(false)
const uiSavedMsg = ref('')
function applyUiSizes() {
const root = document.documentElement
root.style.setProperty('--ui-font-size', `${uiSizes.value.ui_font_size}px`)
root.style.setProperty('--ui-menu-font-size', `${uiSizes.value.ui_menu_font_size}px`)
root.style.setProperty('--ui-menu-icon-size', `${uiSizes.value.ui_menu_icon_size}px`)
root.style.setProperty('--ui-thumb-size', `${uiSizes.value.ui_thumb_size}px`)
window.dispatchEvent(new CustomEvent('ui-sizes-updated', { detail: { ...uiSizes.value } }))
}
async function saveUiSettings() {
savingUi.value = true
uiSavedMsg.value = ''
try {
const payload: Record<string, string> = {}
for (const [k, v] of Object.entries(uiSizes.value)) payload[k] = String(v)
await settingsApi.update(payload)
applyUiSizes()
uiSavedMsg.value = 'Enregistré'
setTimeout(() => { uiSavedMsg.value = '' }, 1800)
} finally {
savingUi.value = false
}
}
function resetUiSettings() {
uiSizes.value = { ...UI_DEFAULTS }
applyUiSizes()
}
function detectApiBaseUrl(): string {
const envBase = String((import.meta as any).env?.VITE_API_URL || '').trim()
if (envBase) {
if (envBase.startsWith('http://') || envBase.startsWith('https://')) {
return envBase.replace(/\/$/, '')
}
if (envBase.startsWith('/')) {
return window.location.origin
}
}
if (window.location.port === '8060') {
return window.location.origin
}
return `${window.location.protocol}//${window.location.hostname}:8060`
}
function openInNewTab(path: string) {
const url = `${apiBaseUrl}${path}`
window.open(url, '_blank', 'noopener,noreferrer')
}
function openApiDocs() {
openInNewTab('/docs')
}
function openApiRedoc() {
openInNewTab('/redoc')
}
function openApiHealth() {
openInNewTab('/api/health')
}
function toBool(value: unknown): boolean {
if (typeof value === 'boolean') return value
@@ -73,6 +224,11 @@ async function loadSettings() {
const data = await settingsApi.get()
debugMode.value = toBool(data.debug_mode)
notifyDebugChanged(debugMode.value)
for (const s of uiSizeSettings) {
const v = data[s.key]
if (v != null) uiSizes.value[s.key] = Number(v) || UI_DEFAULTS[s.key]
}
applyUiSizes()
} catch {
// Laisse la valeur locale si l'API n'est pas disponible.
}
@@ -100,6 +256,28 @@ async function refreshMeteo() {
}
}
async function downloadBackup() {
downloadingBackup.value = true
backupMsg.value = ''
try {
const { blob, filename } = await settingsApi.downloadBackup()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
backupMsg.value = 'Téléchargement lancé.'
} catch {
backupMsg.value = 'Erreur lors de la sauvegarde.'
} finally {
downloadingBackup.value = false
window.setTimeout(() => { backupMsg.value = '' }, 2200)
}
}
onMounted(() => {
void loadSettings()
})