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

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