482 lines
13 KiB
Vue
482 lines
13 KiB
Vue
<template>
|
|
<Transition name="slide-up">
|
|
<div
|
|
v-if="open"
|
|
class="lw-settings glass--panel"
|
|
:style="panelHeight != null ? { height: panelHeight + 'dvh' } : {}"
|
|
:class="{ 'lw-settings--dragging': isDragging }"
|
|
>
|
|
<!-- Handle — drag to resize, tap to close -->
|
|
<div
|
|
class="lw-settings__handle"
|
|
v-on="handleListeners"
|
|
>
|
|
<div class="lw-settings__handle-bar"></div>
|
|
</div>
|
|
|
|
<div class="lw-settings__scroll">
|
|
<div class="lw-settings__title">Einstellungen</div>
|
|
|
|
<!-- Linien -->
|
|
<div class="lw-settings__card" :class="{ 'lw-settings__card--dark': isDark }">
|
|
<span class="lw-settings__card-label">Linien</span>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Speed</span>
|
|
<span class="lw-settings__value">{{ fl.speed.toFixed(2) }}</span>
|
|
</div>
|
|
<q-slider
|
|
:model-value="fl.speed"
|
|
@update:model-value="v => update({ speed: v })"
|
|
:min="0.1" :max="3" :step="0.05"
|
|
/>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Anzahl</span>
|
|
<span class="lw-settings__value">{{ fl.lineCount }}</span>
|
|
</div>
|
|
<q-slider
|
|
:model-value="fl.lineCount"
|
|
@update:model-value="v => update({ lineCount: v })"
|
|
:min="1" :max="40" :step="1"
|
|
/>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Wellen-Amp</span>
|
|
<span class="lw-settings__value">{{ fl.spread.toFixed(2) }}</span>
|
|
</div>
|
|
<q-slider
|
|
:model-value="fl.spread"
|
|
@update:model-value="v => update({ spread: v })"
|
|
:min="0.01" :max="1" :step="0.01"
|
|
/>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Fächerbreite</span>
|
|
<span class="lw-settings__value">{{ fl.fanSpread.toFixed(3) }}</span>
|
|
</div>
|
|
<q-slider
|
|
:model-value="fl.fanSpread"
|
|
@update:model-value="v => update({ fanSpread: v })"
|
|
:min="0.01" :max="0.5" :step="0.005"
|
|
/>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Feinheit</span>
|
|
<span class="lw-settings__value">{{ fl.lineSharpness.toFixed(1) }}</span>
|
|
</div>
|
|
<q-slider
|
|
:model-value="fl.lineSharpness"
|
|
@update:model-value="v => update({ lineSharpness: v })"
|
|
:min="0.3" :max="10" :step="0.1"
|
|
/>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Welligkeit</span>
|
|
<span class="lw-settings__value">{{ fl.waveFrequency.toFixed(1) }}</span>
|
|
</div>
|
|
<q-slider
|
|
:model-value="fl.waveFrequency"
|
|
@update:model-value="v => update({ waveFrequency: v })"
|
|
:min="1" :max="30" :step="0.5"
|
|
/>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Kurve</span>
|
|
<span class="lw-settings__value">{{ fl.bezierCurvature.toFixed(2) }}</span>
|
|
</div>
|
|
<q-slider
|
|
:model-value="fl.bezierCurvature"
|
|
@update:model-value="v => update({ bezierCurvature: v })"
|
|
:min="-1" :max="1" :step="0.05"
|
|
/>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Kreis</span>
|
|
<span class="lw-settings__value">{{ fl.circleRadius }}px</span>
|
|
</div>
|
|
<q-slider
|
|
:model-value="fl.circleRadius"
|
|
@update:model-value="v => update({ circleRadius: v })"
|
|
:min="10" :max="200" :step="5"
|
|
/>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Glow Größe</span>
|
|
<span class="lw-settings__value">{{ fl.glowSize }}px</span>
|
|
</div>
|
|
<q-slider
|
|
:model-value="fl.glowSize"
|
|
@update:model-value="v => update({ glowSize: v })"
|
|
:min="5" :max="100" :step="1"
|
|
/>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Glow Stärke</span>
|
|
<span class="lw-settings__value">{{ fl.glowStrength.toFixed(1) }}</span>
|
|
</div>
|
|
<q-slider
|
|
:model-value="fl.glowStrength"
|
|
@update:model-value="v => update({ glowStrength: v })"
|
|
:min="0.5" :max="12" :step="0.5"
|
|
/>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Helligkeit</span>
|
|
<span class="lw-settings__value">{{ (fl.lineBrightness ?? 1).toFixed(2) }}</span>
|
|
</div>
|
|
<q-slider
|
|
:model-value="fl.lineBrightness ?? 1"
|
|
@update:model-value="v => update({ lineBrightness: v })"
|
|
:min="0.05" :max="2" :step="0.05"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Labels -->
|
|
<div class="lw-settings__card" :class="{ 'lw-settings__card--dark': isDark }">
|
|
<span class="lw-settings__card-label">Labels</span>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Schriftgröße</span>
|
|
<div class="lw-settings__segmented">
|
|
<button
|
|
v-for="size in LABEL_SIZES"
|
|
:key="size.value"
|
|
class="lw-settings__seg-btn"
|
|
:class="{ 'lw-settings__seg-btn--active': fl.labelSize === size.value }"
|
|
@click="update({ labelSize: size.value })"
|
|
>
|
|
{{ size.label }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>Schriftfarbe</span>
|
|
<input
|
|
type="color"
|
|
:value="fl.labelColor ?? '#ffffff'"
|
|
@input="e => update({ labelColor: e.target.value })"
|
|
class="lw-settings__color-input"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hintergrundbild -->
|
|
<div class="lw-settings__card" :class="{ 'lw-settings__card--dark': isDark }">
|
|
<span class="lw-settings__card-label">Hintergrundbild</span>
|
|
|
|
<div class="lw-settings__img-grid">
|
|
<button
|
|
class="lw-settings__img-btn"
|
|
:class="{ 'lw-settings__img-btn--active': fl.backgroundImage === '' }"
|
|
@click="update({ backgroundImage: '' })"
|
|
>
|
|
Keins
|
|
</button>
|
|
<button
|
|
v-for="n in 10"
|
|
:key="'bg' + n"
|
|
class="lw-settings__img-btn"
|
|
:class="{ 'lw-settings__img-btn--active': fl.backgroundImage === `/images/bg-image-${n}.jpg` }"
|
|
@click="update({ backgroundImage: `/images/bg-image-${n}.jpg` })"
|
|
>
|
|
{{ n }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hintergrundfarbe -->
|
|
<div class="lw-settings__card" :class="{ 'lw-settings__card--dark': isDark }">
|
|
<span class="lw-settings__card-label">Hintergrundfarbe</span>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>BG Mitte</span>
|
|
<input
|
|
type="color"
|
|
:value="fl.bgCenter"
|
|
@input="e => update({ bgCenter: e.target.value })"
|
|
class="lw-settings__color-input"
|
|
/>
|
|
</div>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>BG Rand</span>
|
|
<input
|
|
type="color"
|
|
:value="fl.bgEdge"
|
|
@input="e => update({ bgEdge: e.target.value })"
|
|
class="lw-settings__color-input"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Farbstopps -->
|
|
<div class="lw-settings__card" :class="{ 'lw-settings__card--dark': isDark }">
|
|
<span class="lw-settings__card-label">Farbverlauf (je Zeile ein Hex)</span>
|
|
|
|
<textarea
|
|
:value="fl.gradientStops"
|
|
@input="e => update({ gradientStops: e.target.value })"
|
|
class="lw-settings__gradient-input"
|
|
rows="4"
|
|
spellcheck="false"
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Extras -->
|
|
<div class="lw-settings__card" :class="{ 'lw-settings__card--dark': isDark }">
|
|
<span class="lw-settings__card-label">Extras</span>
|
|
|
|
<div class="lw-settings__row">
|
|
<span>{{ isDark ? 'Hell-Modus' : 'Dunkel-Modus' }}</span>
|
|
<q-toggle
|
|
:model-value="isDark"
|
|
@update:model-value="toggleDark"
|
|
dense
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reset -->
|
|
<div class="lw-settings__reset">
|
|
<q-btn
|
|
flat dense no-caps
|
|
label="Zurücksetzen"
|
|
icon="restart_alt"
|
|
size="sm"
|
|
@click="settingsStore.resetFloatingLines()"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, watch } from 'vue'
|
|
import { useQuasar } from 'quasar'
|
|
import { useSettingsStore } from 'stores/settings'
|
|
import { usePanelDrag } from 'composables/usePanelDrag'
|
|
|
|
const props = defineProps({ open: { type: Boolean, default: false } })
|
|
const emit = defineEmits(['close'])
|
|
const { panelHeight, isDragging, handleListeners, resetHeight } = usePanelDrag(() => emit('close'))
|
|
|
|
watch(() => props.open, (open) => { if (open) resetHeight() })
|
|
|
|
const $q = useQuasar()
|
|
const settingsStore = useSettingsStore()
|
|
const isDark = computed(() => $q.dark.isActive)
|
|
const fl = computed(() => settingsStore.floatingLines)
|
|
|
|
const LABEL_SIZES = [
|
|
{ label: 'Klein', value: 'small' },
|
|
{ label: 'Mittel', value: 'medium' },
|
|
{ label: 'Groß', value: 'large' }
|
|
]
|
|
|
|
function update(changes) {
|
|
settingsStore.updateFloatingLines(changes)
|
|
}
|
|
|
|
function toggleDark() {
|
|
$q.dark.toggle()
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.lw-settings {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 20;
|
|
height: 75dvh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-radius: 20px 20px 0 0;
|
|
transition: height 0.25s ease;
|
|
}
|
|
|
|
.lw-settings--dragging {
|
|
transition: none;
|
|
}
|
|
|
|
.lw-settings__handle {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 10px 0 4px;
|
|
flex-shrink: 0;
|
|
cursor: grab;
|
|
touch-action: none;
|
|
}
|
|
|
|
.lw-settings__handle:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.lw-settings__handle-bar {
|
|
width: 36px;
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
background: rgba(128, 128, 128, 0.3);
|
|
}
|
|
|
|
.lw-settings__scroll {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0 20px 32px;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
.lw-settings__title {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.lw-settings__card {
|
|
background: rgba(128, 128, 128, 0.06);
|
|
border: 1px solid rgba(128, 128, 128, 0.1);
|
|
border-radius: 14px;
|
|
padding: 16px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.lw-settings__card--dark {
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border-color: rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.lw-settings__card-label {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
opacity: 0.7;
|
|
display: block;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.lw-settings__row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
font-size: 13px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.lw-settings__row:first-of-type {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.lw-settings__value {
|
|
font-weight: 600;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
/* Segmented control */
|
|
.lw-settings__segmented {
|
|
display: flex;
|
|
background: rgba(128, 128, 128, 0.1);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(128, 128, 128, 0.15);
|
|
}
|
|
|
|
.lw-settings__seg-btn {
|
|
flex: 1;
|
|
padding: 5px 12px;
|
|
border: none;
|
|
background: none;
|
|
color: inherit;
|
|
font-family: inherit;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
opacity: 0.5;
|
|
transition: all 0.15s ease;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.lw-settings__seg-btn--active {
|
|
background: rgba(168, 85, 247, 0.25);
|
|
opacity: 1;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.lw-settings__img-grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
|
|
.lw-settings__img-btn {
|
|
background: rgba(128, 128, 128, 0.1);
|
|
border: 1px solid rgba(128, 128, 128, 0.2);
|
|
color: inherit;
|
|
font-family: inherit;
|
|
font-size: 12px;
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.lw-settings__img-btn:hover {
|
|
opacity: 1;
|
|
border-color: #a855f7;
|
|
}
|
|
|
|
.lw-settings__img-btn--active {
|
|
border-color: #a855f7;
|
|
color: #a855f7;
|
|
opacity: 1;
|
|
}
|
|
|
|
.lw-settings__color-input {
|
|
width: 36px;
|
|
height: 22px;
|
|
border: none;
|
|
padding: 0;
|
|
background: none;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.lw-settings__gradient-input {
|
|
width: 100%;
|
|
background: rgba(0, 0, 0, 0.15);
|
|
border: 1px solid rgba(128, 128, 128, 0.2);
|
|
color: inherit;
|
|
font-family: ui-monospace, 'Cascadia Code', monospace;
|
|
font-size: 12px;
|
|
padding: 8px 10px;
|
|
resize: none;
|
|
border-radius: 8px;
|
|
outline: none;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.body--dark .lw-settings__gradient-input {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.lw-settings__reset {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-top: 20px;
|
|
padding-bottom: 8px;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
/* Slide-up transition */
|
|
.slide-up-enter-active,
|
|
.slide-up-leave-active {
|
|
transition: transform 0.35s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
}
|
|
|
|
.slide-up-enter-from,
|
|
.slide-up-leave-to {
|
|
transform: translateY(100%);
|
|
}
|
|
</style>
|