10-04-2026
This commit is contained in:
parent
70a7776da5
commit
761b1156c1
50 changed files with 11997 additions and 150 deletions
355
frontend/_src/stores/events.js
Normal file
355
frontend/_src/stores/events.js
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { db } from 'src/db'
|
||||
import { startAutoSync, getToken } from 'src/services/syncService'
|
||||
|
||||
// Color interpolation
|
||||
function lerpColor(a, b, t) {
|
||||
const ar = parseInt(a.slice(1, 3), 16)
|
||||
const ag = parseInt(a.slice(3, 5), 16)
|
||||
const ab = parseInt(a.slice(5, 7), 16)
|
||||
const br = parseInt(b.slice(1, 3), 16)
|
||||
const bg = parseInt(b.slice(3, 5), 16)
|
||||
const bb = parseInt(b.slice(5, 7), 16)
|
||||
const r = Math.round(ar + (br - ar) * t)
|
||||
const g = Math.round(ag + (bg - ag) * t)
|
||||
const blue = Math.round(ab + (bb - ab) * t)
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// Gradient presets: [negative, neutral, positive]
|
||||
const GRADIENT_PRESETS = [
|
||||
{ name: 'Standard', colors: ['#E91E63', '#FFD700', '#4CAF50'] },
|
||||
{ name: 'Sunset', colors: ['#FD1D1D', '#FCB045', '#833AB4'] },
|
||||
{ name: 'Earth', colors: ['#ED8153', '#ED8153', '#217B9E'] },
|
||||
{ name: 'Ocean', colors: ['#00D4FF', '#164173', '#440559'] },
|
||||
{ name: 'Spring', colors: ['#FDBB2D', '#96BE74', '#22C1C3'] },
|
||||
{ name: 'Neon', colors: ['#FC466B', '#9A52B6', '#3F5EFB'] },
|
||||
{ name: 'Pastel', colors: ['#EEAECA', '#C2B4D9', '#94BBE9'] },
|
||||
{ name: 'Aurora', colors: ['#FF6B6B', '#C084FC', '#67E8F9'] },
|
||||
{ name: 'Forest', colors: ['#DC2626', '#A3A830', '#059669'] },
|
||||
{ name: 'Berry', colors: ['#F472B6', '#FB923C', '#A78BFA'] }
|
||||
]
|
||||
|
||||
// Glow color logic: emotion value → color, with optional gradient preset
|
||||
function emotionToColor(emotion, gradientIdx = null) {
|
||||
const preset = gradientIdx !== null ? GRADIENT_PRESETS[gradientIdx] : null
|
||||
if (preset) {
|
||||
const [neg, mid, pos] = preset.colors
|
||||
if (emotion >= 0) {
|
||||
return lerpColor(mid, pos, emotion)
|
||||
} else {
|
||||
return lerpColor(mid, neg, Math.abs(emotion))
|
||||
}
|
||||
}
|
||||
if (emotion >= 0) {
|
||||
if (emotion < 0.5) {
|
||||
return lerpColor('#FF6B35', '#FFD700', emotion / 0.5)
|
||||
}
|
||||
return lerpColor('#FFD700', '#4CAF50', (emotion - 0.5) / 0.5)
|
||||
} else {
|
||||
const abs = Math.abs(emotion)
|
||||
if (abs < 0.5) {
|
||||
return lerpColor('#2196F3', '#9C27B0', abs / 0.5)
|
||||
}
|
||||
return lerpColor('#9C27B0', '#E91E63', (abs - 0.5) / 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
// Demo seed data
|
||||
const demoEvents = [
|
||||
{ id: crypto.randomUUID(), title: 'Erster Schultag', date: '1995-09-01', emotion: 0.6, customColor: null, gradientPreset: null, image: null, note: '', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() },
|
||||
{ id: crypto.randomUUID(), title: 'Abiball', date: '2004-06-25', emotion: 0.85, customColor: null, gradientPreset: 1, image: 'demo/photo-1530103862676-de8c9debad1d.jpeg', note: 'Was für eine Party!', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() },
|
||||
{ id: crypto.randomUUID(), title: 'Trennung', date: '2010-03-15', emotion: -0.7, customColor: null, gradientPreset: null, image: null, note: '', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() },
|
||||
{ id: crypto.randomUUID(), title: 'Bergwanderung', date: '2014-08-12', emotion: 0.75, customColor: null, gradientPreset: 4, image: 'demo/photo-1534067783941-51c9c23ecefd.jpeg', note: 'Unvergesslicher Ausblick', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() },
|
||||
{ id: crypto.randomUUID(), title: 'Jobverlust', date: '2016-11-03', emotion: -0.6, customColor: null, gradientPreset: null, image: null, note: '', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() },
|
||||
{ id: crypto.randomUUID(), title: 'Hochzeit', date: '2018-07-20', emotion: 0.95, customColor: null, gradientPreset: 5, image: 'demo/photo-1506905925346-21bda4d32df4.jpeg', note: 'Der schönste Tag', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() },
|
||||
{ id: crypto.randomUUID(), title: 'Umzug', date: '2021-04-01', emotion: -0.3, customColor: null, gradientPreset: null, image: null, note: '', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() },
|
||||
{ id: crypto.randomUUID(), title: 'Neuer Job', date: '2023-01-10', emotion: 0.5, customColor: null, gradientPreset: null, image: 'demo/photo-1530103862676-de8c9debad1d.jpeg', note: 'Neues Kapitel', syncStatus: 'local', createdAt: Date.now(), updatedAt: Date.now() }
|
||||
]
|
||||
|
||||
// Generate realistic demo events for testing at scale
|
||||
function generateManyEvents(count = 500) {
|
||||
// Realistic life event categories with emotion ranges
|
||||
const categories = [
|
||||
// Positive events
|
||||
{ titles: ['Geburtstag', 'Geburtstagsfeier', 'Überraschungsparty'], emotionRange: [0.3, 0.8], noteChance: 0.4, notes: ['Tolles Fest!', 'Viele Geschenke', 'Schöner Tag mit Freunden', 'Alles Gute!'] },
|
||||
{ titles: ['Urlaub', 'Strandurlaub', 'Städtereise', 'Roadtrip', 'Backpacking'], emotionRange: [0.4, 0.95], noteChance: 0.6, notes: ['Unvergesslich', 'Wunderschöne Landschaft', 'Endlich Erholung', 'Muss ich wiederholen'] },
|
||||
{ titles: ['Hochzeit', 'Verlobung', 'Jahrestag'], emotionRange: [0.7, 1.0], noteChance: 0.8, notes: ['Der schönste Tag', 'Für immer', 'Tränen der Freude', 'Unbeschreiblich'] },
|
||||
{ titles: ['Beförderung', 'Neuer Job', 'Gehaltserhöhung', 'Jobangebot'], emotionRange: [0.5, 0.9], noteChance: 0.5, notes: ['Endlich!', 'Harte Arbeit zahlt sich aus', 'Neues Kapitel', 'Verdient'] },
|
||||
{ titles: ['Konzert', 'Festival', 'Theaterbesuch', 'Oper'], emotionRange: [0.3, 0.85], noteChance: 0.5, notes: ['Gänsehaut', 'Beste Band ever', 'Geniale Atmosphäre', 'Nächstes Jahr wieder'] },
|
||||
{ titles: ['Geburt', 'Baby da!', 'Nachwuchs'], emotionRange: [0.85, 1.0], noteChance: 0.9, notes: ['Das größte Wunder', 'Willkommen auf der Welt', 'Unbeschreibliches Glück'] },
|
||||
{ titles: ['Abschluss', 'Prüfung bestanden', 'Diplom', 'Master geschafft'], emotionRange: [0.6, 0.95], noteChance: 0.6, notes: ['Geschafft!', 'Jahre harter Arbeit', 'Stolz', 'Endlich vorbei'] },
|
||||
{ titles: ['Bergwanderung', 'Gipfel erreicht', 'Marathon geschafft', 'Triathlon'], emotionRange: [0.5, 0.9], noteChance: 0.5, notes: ['Was für ein Ausblick!', 'Körperliche Grenzen überwunden', 'Nie aufgeben'] },
|
||||
{ titles: ['Hauskauf', 'Wohnungseinweihung', 'Renovierung fertig'], emotionRange: [0.4, 0.8], noteChance: 0.5, notes: ['Endlich eigene vier Wände', 'Traum wird wahr', 'Viel Arbeit, aber es lohnt sich'] },
|
||||
{ titles: ['Erstes Date', 'Zusammengekommen', 'Liebeserklärung'], emotionRange: [0.5, 0.95], noteChance: 0.6, notes: ['Schmetterlinge', 'Liebe auf den ersten Blick', 'Endlich getraut'] },
|
||||
// Neutral events
|
||||
{ titles: ['Umzug', 'Neue Stadt', 'Wohnungswechsel'], emotionRange: [-0.2, 0.3], noteChance: 0.4, notes: ['Neuanfang', 'Alles anders', 'Spannend und stressig zugleich'] },
|
||||
{ titles: ['Arztbesuch', 'Vorsorge', 'Check-up'], emotionRange: [-0.1, 0.1], noteChance: 0.2, notes: ['Alles okay', 'Routine'] },
|
||||
{ titles: ['Meeting', 'Präsentation', 'Workshop'], emotionRange: [-0.1, 0.4], noteChance: 0.3, notes: ['Gut gelaufen', 'Viel gelernt', 'Anstrengend'] },
|
||||
{ titles: ['Friseur', 'Shopping', 'Einkauf'], emotionRange: [0.0, 0.3], noteChance: 0.1, notes: ['Neuer Look', 'Guter Fund'] },
|
||||
// Negative events
|
||||
{ titles: ['Trennung', 'Beziehungsende', 'Scheidung'], emotionRange: [-1.0, -0.5], noteChance: 0.5, notes: ['Schmerzhaft', 'Warum?', 'Es ist besser so', 'Brauche Zeit'] },
|
||||
{ titles: ['Jobverlust', 'Kündigung', 'Firma pleite'], emotionRange: [-0.9, -0.4], noteChance: 0.5, notes: ['Schock', 'Wie geht es weiter?', 'Unverdient'] },
|
||||
{ titles: ['Krankheit', 'OP', 'Krankenhaus'], emotionRange: [-0.8, -0.3], noteChance: 0.6, notes: ['Wird schon', 'Hauptsache gesund werden', 'Lange Genesung'] },
|
||||
{ titles: ['Abschied', 'Verlust', 'Trauer'], emotionRange: [-1.0, -0.6], noteChance: 0.7, notes: ['Ruhe in Frieden', 'Fehlt mir', 'Unvergessen', 'Schwerer Tag'] },
|
||||
{ titles: ['Streit', 'Konflikt', 'Auseinandersetzung'], emotionRange: [-0.7, -0.2], noteChance: 0.3, notes: ['Muss nicht sein', 'Hoffe auf Klärung'] },
|
||||
{ titles: ['Unfall', 'Panne', 'Autopanne'], emotionRange: [-0.6, -0.2], noteChance: 0.4, notes: ['Zum Glück nichts Schlimmes', 'Ärgerlich', 'Hätte schlimmer sein können'] },
|
||||
{ titles: ['Prüfung nicht bestanden', 'Absage', 'Ablehnung'], emotionRange: [-0.7, -0.3], noteChance: 0.4, notes: ['Nächstes Mal', 'Nicht aufgeben', 'Enttäuschend'] },
|
||||
]
|
||||
|
||||
const demoImages = [
|
||||
'demo/photo-1530103862676-de8c9debad1d.jpeg',
|
||||
'demo/photo-1534067783941-51c9c23ecefd.jpeg',
|
||||
'demo/photo-1506905925346-21bda4d32df4.jpeg'
|
||||
]
|
||||
|
||||
// Seeded random for reproducibility
|
||||
let seed = 42
|
||||
function rand() {
|
||||
seed = (seed * 16807 + 0) % 2147483647
|
||||
return (seed - 1) / 2147483646
|
||||
}
|
||||
|
||||
function randInt(min, max) {
|
||||
return Math.floor(rand() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
function pick(arr) {
|
||||
return arr[Math.floor(rand() * arr.length)]
|
||||
}
|
||||
|
||||
function randFloat(min, max) {
|
||||
return Math.round((min + rand() * (max - min)) * 100) / 100
|
||||
}
|
||||
|
||||
const evts = []
|
||||
const startYear = 1985
|
||||
const endYear = 2026
|
||||
|
||||
// Generate events with realistic distribution (more events in recent years)
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Weight towards recent years: cube root distribution
|
||||
const t = rand()
|
||||
const yearFloat = startYear + (endYear - startYear) * (t * t * 0.4 + t * 0.6)
|
||||
const year = Math.floor(yearFloat)
|
||||
const month = randInt(1, 12)
|
||||
const day = randInt(1, 28) // safe for all months
|
||||
const date = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
||||
|
||||
const cat = pick(categories)
|
||||
const title = pick(cat.titles)
|
||||
const emotion = randFloat(cat.emotionRange[0], cat.emotionRange[1])
|
||||
const hasNote = rand() < cat.noteChance
|
||||
const note = hasNote ? pick(cat.notes) : ''
|
||||
const hasImage = rand() < 0.15 // 15% chance
|
||||
const image = hasImage ? pick(demoImages) : null
|
||||
const hasPreset = rand() < 0.25 // 25% chance
|
||||
const gradientPreset = hasPreset ? randInt(0, 9) : null
|
||||
|
||||
evts.push({
|
||||
id: crypto.randomUUID(),
|
||||
title,
|
||||
date,
|
||||
emotion,
|
||||
customColor: null,
|
||||
gradientPreset,
|
||||
image,
|
||||
note,
|
||||
syncStatus: 'local',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by date
|
||||
evts.sort((a, b) => a.date.localeCompare(b.date))
|
||||
return evts
|
||||
}
|
||||
|
||||
export { emotionToColor, GRADIENT_PRESETS, demoEvents, generateManyEvents }
|
||||
|
||||
export const useEventsStore = defineStore('events', () => {
|
||||
const events = ref([])
|
||||
const isLoaded = ref(false)
|
||||
const selectedEventId = ref(null)
|
||||
const panelOpen = ref(false)
|
||||
const editingEventId = ref(null)
|
||||
|
||||
// Load events from IndexedDB; seed demo data on first launch
|
||||
async function init() {
|
||||
try {
|
||||
let stored = await db.events.orderBy('date').toArray()
|
||||
if (stored.length === 0) {
|
||||
const seed = generateManyEvents(500)
|
||||
await db.events.bulkPut(seed)
|
||||
stored = seed
|
||||
}
|
||||
events.value = stored
|
||||
} catch (e) {
|
||||
console.warn('Dexie load failed, using demo data:', e)
|
||||
events.value = [...demoEvents]
|
||||
}
|
||||
isLoaded.value = true
|
||||
|
||||
// Start auto-sync if authenticated
|
||||
getToken().then((token) => {
|
||||
if (token) startAutoSync()
|
||||
})
|
||||
}
|
||||
|
||||
// Fire-and-forget DB write (UI already updated via ref)
|
||||
function dbPut(event) {
|
||||
db.events.put(event).catch(e => console.warn('Dexie put failed:', e))
|
||||
}
|
||||
|
||||
function dbDelete(id) {
|
||||
db.events.delete(id).catch(e => console.warn('Dexie delete failed:', e))
|
||||
}
|
||||
|
||||
function dbQueueSync(eventId, action, payload) {
|
||||
db.syncQueue.add({ eventId, action, payload, createdAt: Date.now() })
|
||||
.catch(e => console.warn('Dexie sync queue failed:', e))
|
||||
}
|
||||
|
||||
// Ghost event for live preview while creating/editing
|
||||
const ghostEmotion = ref(0)
|
||||
const ghostCustomColor = ref(null)
|
||||
const ghostGradientPreset = ref(null)
|
||||
const ghostTitle = ref('')
|
||||
const ghostDate = ref(new Date().toISOString().slice(0, 10))
|
||||
const ghostNote = ref('')
|
||||
const ghostImage = ref(null)
|
||||
|
||||
const ghostEvent = computed(() => ({
|
||||
id: '__ghost__',
|
||||
title: ghostTitle.value || 'New Event',
|
||||
date: ghostDate.value,
|
||||
emotion: ghostEmotion.value,
|
||||
customColor: ghostCustomColor.value,
|
||||
gradientPreset: ghostGradientPreset.value,
|
||||
image: ghostImage.value,
|
||||
note: ghostNote.value
|
||||
}))
|
||||
|
||||
const sortedEvents = computed(() => {
|
||||
return [...events.value].sort((a, b) => new Date(a.date) - new Date(b.date))
|
||||
})
|
||||
|
||||
function selectEvent(id) {
|
||||
selectedEventId.value = id
|
||||
}
|
||||
|
||||
function openPanel(eventId = null) {
|
||||
if (eventId) {
|
||||
editingEventId.value = eventId
|
||||
const event = events.value.find((e) => e.id === eventId)
|
||||
if (event) {
|
||||
ghostTitle.value = event.title
|
||||
ghostDate.value = event.date
|
||||
ghostEmotion.value = event.emotion
|
||||
ghostCustomColor.value = event.customColor
|
||||
ghostGradientPreset.value = event.gradientPreset ?? null
|
||||
ghostImage.value = event.image || null
|
||||
ghostNote.value = event.note
|
||||
}
|
||||
} else {
|
||||
editingEventId.value = null
|
||||
ghostTitle.value = ''
|
||||
ghostDate.value = new Date().toISOString().slice(0, 10)
|
||||
ghostEmotion.value = 0
|
||||
ghostCustomColor.value = null
|
||||
ghostGradientPreset.value = null
|
||||
ghostImage.value = null
|
||||
ghostNote.value = ''
|
||||
}
|
||||
panelOpen.value = true
|
||||
}
|
||||
|
||||
// Auto-save: persist ghost → event in edit mode
|
||||
function persistToEvent() {
|
||||
if (!editingEventId.value) return
|
||||
const idx = events.value.findIndex((e) => e.id === editingEventId.value)
|
||||
if (idx === -1) return
|
||||
const updated = {
|
||||
...events.value[idx],
|
||||
title: ghostTitle.value,
|
||||
date: ghostDate.value,
|
||||
emotion: ghostEmotion.value,
|
||||
customColor: ghostCustomColor.value,
|
||||
gradientPreset: ghostGradientPreset.value,
|
||||
image: ghostImage.value,
|
||||
note: ghostNote.value,
|
||||
syncStatus: 'modified',
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
events.value[idx] = updated
|
||||
dbPut(updated)
|
||||
}
|
||||
|
||||
watch(
|
||||
[ghostTitle, ghostDate, ghostEmotion, ghostCustomColor, ghostGradientPreset, ghostImage, ghostNote],
|
||||
() => { persistToEvent() }
|
||||
)
|
||||
|
||||
function closePanel() {
|
||||
if (!editingEventId.value && ghostTitle.value.trim()) {
|
||||
const newEvent = {
|
||||
id: crypto.randomUUID(),
|
||||
title: ghostTitle.value,
|
||||
date: ghostDate.value,
|
||||
emotion: ghostEmotion.value,
|
||||
customColor: ghostCustomColor.value,
|
||||
gradientPreset: ghostGradientPreset.value,
|
||||
image: ghostImage.value,
|
||||
note: ghostNote.value,
|
||||
syncStatus: 'local',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
events.value.push(newEvent)
|
||||
dbPut(newEvent)
|
||||
dbQueueSync(newEvent.id, 'create', { ...newEvent })
|
||||
}
|
||||
panelOpen.value = false
|
||||
editingEventId.value = null
|
||||
selectedEventId.value = null
|
||||
}
|
||||
|
||||
function deleteEvent(id) {
|
||||
events.value = events.value.filter((e) => e.id !== id)
|
||||
dbDelete(id)
|
||||
dbQueueSync(id, 'delete', null)
|
||||
closePanel()
|
||||
}
|
||||
|
||||
function getGlowColor(event) {
|
||||
if (event.customColor) return event.customColor
|
||||
return emotionToColor(event.emotion, event.gradientPreset ?? null)
|
||||
}
|
||||
|
||||
// Auto-init on store creation
|
||||
init()
|
||||
|
||||
return {
|
||||
events,
|
||||
isLoaded,
|
||||
selectedEventId,
|
||||
panelOpen,
|
||||
editingEventId,
|
||||
ghostEmotion,
|
||||
ghostCustomColor,
|
||||
ghostGradientPreset,
|
||||
ghostTitle,
|
||||
ghostDate,
|
||||
ghostNote,
|
||||
ghostImage,
|
||||
ghostEvent,
|
||||
sortedEvents,
|
||||
selectEvent,
|
||||
openPanel,
|
||||
closePanel,
|
||||
deleteEvent,
|
||||
getGlowColor
|
||||
}
|
||||
})
|
||||
21
frontend/_src/stores/example-store.js
Normal file
21
frontend/_src/stores/example-store.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', {
|
||||
state: () => ({
|
||||
counter: 0
|
||||
}),
|
||||
|
||||
getters: {
|
||||
doubleCount: (state) => state.counter * 2
|
||||
},
|
||||
|
||||
actions: {
|
||||
increment() {
|
||||
this.counter++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot))
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
20
frontend/_src/stores/index.js
Normal file
20
frontend/_src/stores/index.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { defineStore } from '#q-app/wrappers'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
/*
|
||||
* If not building with SSR mode, you can
|
||||
* directly export the Store instantiation;
|
||||
*
|
||||
* The function below can be async too; either use
|
||||
* async/await or return a Promise which resolves
|
||||
* with the Store instance.
|
||||
*/
|
||||
|
||||
export default defineStore((/* { ssrContext } */) => {
|
||||
const pinia = createPinia()
|
||||
|
||||
// You can add Pinia plugins here
|
||||
// pinia.use(SomePiniaPlugin)
|
||||
|
||||
return pinia
|
||||
})
|
||||
107
frontend/_src/stores/settings.js
Normal file
107
frontend/_src/stores/settings.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const STORAGE_KEY = 'thatsme-settings'
|
||||
|
||||
export const ACCENT_COLORS = [
|
||||
{ label: 'Standard', value: 'default', hex: '#9e9e9e' },
|
||||
{ label: 'Blau', value: 'blue', hex: '#2196F3' },
|
||||
{ label: 'Grün', value: 'green', hex: '#4CAF50' },
|
||||
{ label: 'Gelb', value: 'yellow', hex: '#FFC107' },
|
||||
{ label: 'Rosa', value: 'pink', hex: '#E91E63' },
|
||||
{ label: 'Orange', value: 'orange', hex: '#FF9800' }
|
||||
]
|
||||
|
||||
export const LANGUAGES = [
|
||||
{ label: 'Deutsch', value: 'de' },
|
||||
{ label: 'English', value: 'en' }
|
||||
]
|
||||
|
||||
const FLOATING_LINES_DEFAULTS = {
|
||||
// Linien
|
||||
speed: 1.0,
|
||||
lineCount: 10,
|
||||
spread: 0.05,
|
||||
fanSpread: 0.05,
|
||||
lineSharpness: 8.0,
|
||||
waveFrequency: 7.0,
|
||||
bezierCurvature: 0.2,
|
||||
circleRadius: 75,
|
||||
glowSize: 18,
|
||||
glowStrength: 1.5,
|
||||
lineBrightness: 1.0,
|
||||
// Hintergrund
|
||||
bgCenter: '#0a0514',
|
||||
bgEdge: '#000000',
|
||||
gradientStops: '#e947f5\n#2f4ba2\n#0a0a12',
|
||||
backgroundImage: '',
|
||||
// Labels
|
||||
labelSize: 'small', // 'small' | 'medium' | 'large'
|
||||
labelColor: '#ffffff'
|
||||
}
|
||||
|
||||
function loadFromStorage() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
return stored ? JSON.parse(stored) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export { FLOATING_LINES_DEFAULTS }
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
const stored = loadFromStorage()
|
||||
|
||||
const theme = ref(stored?.theme ?? 'light')
|
||||
const floatingLines = ref(stored?.floatingLines ?? { ...FLOATING_LINES_DEFAULTS })
|
||||
|
||||
// App preferences
|
||||
const appearance = ref(stored?.appearance ?? 'system') // 'system' | 'light' | 'dark'
|
||||
const accentColor = ref(stored?.accentColor ?? 'default')
|
||||
const language = ref(stored?.language ?? 'de')
|
||||
|
||||
// Developer / debug
|
||||
const showFps = ref(stored?.showFps ?? false)
|
||||
|
||||
function persist() {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
theme: theme.value,
|
||||
floatingLines: floatingLines.value,
|
||||
appearance: appearance.value,
|
||||
accentColor: accentColor.value,
|
||||
language: language.value,
|
||||
showFps: showFps.value
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
watch([theme, floatingLines, appearance, accentColor, language, showFps], persist, { deep: true })
|
||||
|
||||
function toggleTheme() {
|
||||
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
function updateFloatingLines(updates) {
|
||||
floatingLines.value = { ...floatingLines.value, ...updates }
|
||||
}
|
||||
|
||||
function resetFloatingLines() {
|
||||
floatingLines.value = { ...FLOATING_LINES_DEFAULTS }
|
||||
}
|
||||
|
||||
return {
|
||||
theme,
|
||||
floatingLines,
|
||||
appearance,
|
||||
accentColor,
|
||||
language,
|
||||
showFps,
|
||||
toggleTheme,
|
||||
updateFloatingLines,
|
||||
resetFloatingLines
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue