thats-me/frontend/_src/components/AppSettingsModal.vue
2026-04-22 12:57:10 +02:00

322 lines
8.5 KiB
Vue

<template>
<ModalCard
:open="open"
title="Allgemein"
:tabs="tabs"
v-model="activeTab"
@close="$emit('close')"
>
<!-- Allgemein -->
<div v-if="activeTab === 'general'">
<div class="settings-section__title">Allgemein</div>
<div class="settings-section__divider" />
<!-- Aussehen -->
<div class="settings-row">
<span class="settings-row__label">Aussehen</span>
<div class="settings-row__control">
<select
:value="settingsStore.appearance"
@change="onAppearanceChange"
class="settings-select"
:class="{ 'settings-select--dark': isDark }"
>
<option value="system">System</option>
<option value="light">Hell</option>
<option value="dark">Dunkel</option>
</select>
</div>
</div>
<div class="settings-section__divider" />
<!-- Akzentfarbe -->
<div class="settings-row">
<span class="settings-row__label">Akzentfarbe</span>
<div class="settings-row__control">
<button
class="settings-accent-btn"
:class="{ 'settings-accent-btn--dark': isDark }"
@click="accentDropdownOpen = !accentDropdownOpen"
>
<span
class="settings-accent-dot"
:style="{ background: currentAccentHex }"
/>
<span>{{ currentAccentLabel }}</span>
<q-icon name="expand_more" size="16px" />
</button>
<!-- Accent dropdown -->
<Transition name="dropdown">
<div
v-if="accentDropdownOpen"
class="settings-dropdown"
:class="{ 'settings-dropdown--dark': isDark }"
>
<button
v-for="color in ACCENT_COLORS"
:key="color.value"
class="settings-dropdown__item"
@click="selectAccent(color.value)"
>
<span class="settings-accent-dot" :style="{ background: color.hex }" />
<span>{{ color.label }}</span>
<q-icon
v-if="settingsStore.accentColor === color.value"
name="check"
size="18px"
class="settings-dropdown__check"
/>
</button>
</div>
</Transition>
</div>
</div>
<div class="settings-section__divider" />
<!-- Sprache -->
<div class="settings-row">
<span class="settings-row__label">Sprache</span>
<div class="settings-row__control">
<select
:value="settingsStore.language"
@change="e => { settingsStore.language = e.target.value }"
class="settings-select"
:class="{ 'settings-select--dark': isDark }"
>
<option v-for="lang in LANGUAGES" :key="lang.value" :value="lang.value">
{{ lang.label }}
</option>
</select>
</div>
</div>
<div class="settings-section__divider" />
<!-- FPS-Anzeige -->
<div class="settings-row">
<span class="settings-row__label">FPS-Anzeige</span>
<div class="settings-row__control">
<q-toggle
:model-value="settingsStore.showFps"
@update:model-value="v => { settingsStore.showFps = v }"
dense
color="green"
/>
</div>
</div>
</div>
<!-- Benachrichtigungen (placeholder) -->
<div v-else-if="activeTab === 'notifications'">
<div class="settings-section__title">Benachrichtigungen</div>
<div class="settings-section__divider" />
<p class="settings-placeholder">Kommt bald.</p>
</div>
<!-- Personalisierung (placeholder) -->
<div v-else-if="activeTab === 'personalize'">
<div class="settings-section__title">Personalisierung</div>
<div class="settings-section__divider" />
<p class="settings-placeholder">Kommt bald.</p>
</div>
</ModalCard>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useQuasar } from 'quasar'
import ModalCard from 'components/ModalCard.vue'
import { useSettingsStore, ACCENT_COLORS, LANGUAGES } from 'stores/settings'
defineProps({ open: { type: Boolean, default: false } })
defineEmits(['close'])
const $q = useQuasar()
const settingsStore = useSettingsStore()
const isDark = computed(() => $q.dark.isActive)
const activeTab = ref('general')
const accentDropdownOpen = ref(false)
const tabs = [
{ value: 'general', label: 'Allgemein', icon: 'settings' },
{ value: 'notifications', label: 'Benachrichtigungen', icon: 'notifications' },
{ value: 'personalize', label: 'Personalisierung', icon: 'history' }
]
const currentAccentHex = computed(() => {
const found = ACCENT_COLORS.find(c => c.value === settingsStore.accentColor)
return found?.hex ?? '#9e9e9e'
})
const currentAccentLabel = computed(() => {
const found = ACCENT_COLORS.find(c => c.value === settingsStore.accentColor)
return found?.label ?? 'Standard'
})
function selectAccent(value) {
settingsStore.accentColor = value
accentDropdownOpen.value = false
}
function onAppearanceChange(e) {
const value = e.target.value
settingsStore.appearance = value
applyAppearance(value)
}
function applyAppearance(mode) {
if (mode === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
$q.dark.set(prefersDark)
} else {
$q.dark.set(mode === 'dark')
}
}
// Apply appearance on mount and when it changes externally
watch(() => settingsStore.appearance, applyAppearance, { immediate: true })
</script>
<style scoped>
.settings-section__title {
font-size: 18px;
font-weight: 700;
margin-bottom: 4px;
}
.settings-section__divider {
height: 1px;
background: rgba(128, 128, 128, 0.12);
margin: 16px 0;
}
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 40px;
}
.settings-row__label {
font-size: 15px;
}
.settings-row__control {
position: relative;
}
/* Native select styled */
.settings-select {
appearance: none;
-webkit-appearance: none;
background: rgba(128, 128, 128, 0.08);
border: 1px solid rgba(128, 128, 128, 0.15);
color: inherit;
font-family: inherit;
font-size: 14px;
padding: 6px 28px 6px 12px;
border-radius: 8px;
cursor: pointer;
outline: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%23999' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
}
.settings-select--dark {
background-color: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.1);
}
/* Accent button */
.settings-accent-btn {
display: flex;
align-items: center;
gap: 8px;
background: rgba(128, 128, 128, 0.08);
border: 1px solid rgba(128, 128, 128, 0.15);
color: inherit;
font-family: inherit;
font-size: 14px;
padding: 6px 10px;
border-radius: 8px;
cursor: pointer;
}
.settings-accent-btn--dark {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.1);
}
.settings-accent-dot {
width: 14px;
height: 14px;
border-radius: 50%;
flex-shrink: 0;
}
/* Accent Dropdown */
.settings-dropdown {
position: absolute;
right: 0;
top: calc(100% + 6px);
min-width: 180px;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(128, 128, 128, 0.15);
border-radius: 12px;
padding: 6px;
z-index: 10;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
.settings-dropdown--dark {
background: rgba(40, 40, 40, 0.92);
border-color: rgba(255, 255, 255, 0.1);
}
.settings-dropdown__item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 12px;
border: none;
background: none;
color: inherit;
font-family: inherit;
font-size: 14px;
cursor: pointer;
border-radius: 8px;
transition: background 0.12s;
}
.settings-dropdown__item:hover {
background: rgba(128, 128, 128, 0.1);
}
.settings-dropdown__check {
margin-left: auto;
opacity: 0.7;
}
/* Placeholder text */
.settings-placeholder {
font-size: 14px;
opacity: 0.5;
}
/* Dropdown transition */
.dropdown-enter-active,
.dropdown-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-4px);
}
</style>