20-02-2026
This commit is contained in:
parent
c62234e1ca
commit
98084de7d0
80 changed files with 9804 additions and 1771 deletions
293
frontend/src/layouts/LifeWaveLayout.vue
Normal file
293
frontend/src/layouts/LifeWaveLayout.vue
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
<template>
|
||||
<div ref="layoutRef" class="lifewave-layout" :class="{ 'lifewave-layout--dark': isDark }">
|
||||
<!-- FloatingLines Fullscreen Background (always visible) -->
|
||||
<FloatingLines
|
||||
class="lifewave-layout__background"
|
||||
:enabled-waves="['middle']"
|
||||
:line-count="[fl.lineCount]"
|
||||
:animation-speed="fl.speed"
|
||||
:num-points="shaderNumPoints"
|
||||
:point-x-values="shaderPointX"
|
||||
:point-y-values="shaderPointY"
|
||||
:point-colors="shaderPointColors"
|
||||
:line-spread="fl.spread"
|
||||
:fan-spread="fl.fanSpread"
|
||||
:line-sharpness="fl.lineSharpness"
|
||||
:wave-frequency="fl.waveFrequency"
|
||||
:bezier-curvature="fl.bezierCurvature"
|
||||
:circle-radius-px="fl.circleRadius"
|
||||
:circle-glow-size="fl.glowSize"
|
||||
:circle-glow-strength="fl.glowStrength"
|
||||
:lines-gradient="parsedGradient"
|
||||
:bg-color-center="fl.bgCenter"
|
||||
:bg-color-edge="fl.bgEdge"
|
||||
:background-image="fl.backgroundImage"
|
||||
:mix-blend-mode="'screen'"
|
||||
/>
|
||||
|
||||
<!-- Scrollable Timeline -->
|
||||
<TimelineView
|
||||
class="lifewave-layout__timeline"
|
||||
@dot-select="onDotSelect"
|
||||
@view-update="onViewUpdate"
|
||||
/>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="lifewave-header">
|
||||
<span class="lifewave-header__logo" @click="toggleDarkMode">ThatsMe</span>
|
||||
<button class="lifewave-header__user glass--button" @click="settingsOpen = !settingsOpen">
|
||||
<q-icon name="tune" size="22px" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="lifewave-content">
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<!-- Add Event Button (Bottom-Center) — hidden when panel is open -->
|
||||
<AddEventButton v-if="!eventsStore.panelOpen" @click="onAddEvent" />
|
||||
|
||||
<!-- Backdrop blur when panel is open -->
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="eventsStore.panelOpen"
|
||||
class="lifewave-backdrop"
|
||||
@click="eventsStore.closePanel()"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<!-- Event Panel (Slide-Up) -->
|
||||
<EventPanel />
|
||||
|
||||
<!-- Wave Settings Backdrop -->
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="settingsOpen && !eventsStore.panelOpen"
|
||||
class="lifewave-backdrop"
|
||||
@click="settingsOpen = false"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<!-- Wave Settings Panel (Slide-Up) -->
|
||||
<LifeWaveSettings
|
||||
:open="settingsOpen && !eventsStore.panelOpen"
|
||||
@close="settingsOpen = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import AddEventButton from 'components/AddEventButton.vue'
|
||||
import EventPanel from 'components/EventPanel.vue'
|
||||
import FloatingLines from 'components/FloatingLines.vue'
|
||||
import LifeWaveSettings from 'components/LifeWaveSettings.vue'
|
||||
import TimelineView from 'components/TimelineView.vue'
|
||||
import { useEventsStore } from 'stores/events'
|
||||
import { useSettingsStore } from 'stores/settings'
|
||||
|
||||
const $q = useQuasar()
|
||||
const eventsStore = useEventsStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const isDark = computed(() => $q.dark.isActive)
|
||||
const settingsOpen = ref(false)
|
||||
const fl = computed(() => settingsStore.floatingLines)
|
||||
|
||||
// Layout dimensions (for screen→UV conversion)
|
||||
const layoutRef = ref(null)
|
||||
const layoutWidth = ref(window.innerWidth)
|
||||
const layoutHeight = ref(window.innerHeight)
|
||||
let layoutResizeObserver = null
|
||||
|
||||
onMounted(() => {
|
||||
if (layoutRef.value) {
|
||||
layoutWidth.value = layoutRef.value.clientWidth
|
||||
layoutHeight.value = layoutRef.value.clientHeight
|
||||
layoutResizeObserver = new ResizeObserver(() => {
|
||||
layoutWidth.value = layoutRef.value.clientWidth
|
||||
layoutHeight.value = layoutRef.value.clientHeight
|
||||
})
|
||||
layoutResizeObserver.observe(layoutRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
layoutResizeObserver?.disconnect()
|
||||
})
|
||||
|
||||
// Timeline state (received from TimelineView via emit)
|
||||
const timelineState = ref(null)
|
||||
|
||||
function onViewUpdate(state) {
|
||||
timelineState.value = state
|
||||
}
|
||||
|
||||
// Convert screen pixel coordinates → shader UV space
|
||||
// Shader: baseUv = (2*fragCoord - iResolution) / iResolution.y; baseUv.y *= -1;
|
||||
// For CSS pixel (sx, sy) from top-left:
|
||||
// uvX = (2*sx - cssWidth) / cssHeight
|
||||
// uvY = (2*sy - cssHeight) / cssHeight
|
||||
function screenToUV(sx, sy) {
|
||||
const w = layoutWidth.value
|
||||
const h = layoutHeight.value
|
||||
return {
|
||||
x: (2 * sx - w) / h,
|
||||
y: (2 * sy - h) / h
|
||||
}
|
||||
}
|
||||
|
||||
// Compute shader point positions from event positions
|
||||
const TIMELINE_TOP = 60 // CSS: .timeline { top: 60px }
|
||||
|
||||
const shaderNumPoints = computed(() => {
|
||||
if (!timelineState.value) return 0
|
||||
return Math.min(timelineState.value.events.length, 8)
|
||||
})
|
||||
|
||||
const shaderPointX = computed(() => {
|
||||
const xs = Array(8).fill(0)
|
||||
if (!timelineState.value) return xs
|
||||
const { scrollLeft, events } = timelineState.value
|
||||
const count = Math.min(events.length, 8)
|
||||
for (let i = 0; i < count; i++) {
|
||||
const screenX = events[i].x - scrollLeft
|
||||
xs[i] = screenToUV(screenX, 0).x
|
||||
}
|
||||
return xs
|
||||
})
|
||||
|
||||
const shaderPointY = computed(() => {
|
||||
const ys = Array(8).fill(0)
|
||||
if (!timelineState.value) return ys
|
||||
const { containerHeight: tlHeight, events } = timelineState.value
|
||||
const count = Math.min(events.length, 8)
|
||||
for (let i = 0; i < count; i++) {
|
||||
// GlowDot: top = (50 - emotion*35)% of timeline container
|
||||
const yPercent = 50 - events[i].emotion * 35
|
||||
const screenY = TIMELINE_TOP + (yPercent / 100) * tlHeight
|
||||
ys[i] = screenToUV(0, screenY).y
|
||||
}
|
||||
return ys
|
||||
})
|
||||
|
||||
const shaderPointColors = computed(() => {
|
||||
if (!timelineState.value) return []
|
||||
const { events } = timelineState.value
|
||||
const count = Math.min(events.length, 8)
|
||||
const colors = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
colors.push(events[i].color || '#ffffff')
|
||||
}
|
||||
return colors
|
||||
})
|
||||
|
||||
// Parse gradient stops from textarea string
|
||||
const parsedGradient = computed(() => {
|
||||
return fl.value.gradientStops
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0 && s.startsWith('#'))
|
||||
})
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
$q.dark.toggle()
|
||||
}
|
||||
|
||||
const onAddEvent = () => {
|
||||
eventsStore.openPanel()
|
||||
}
|
||||
|
||||
const onDotSelect = (id) => {
|
||||
eventsStore.selectEvent(id)
|
||||
eventsStore.openPanel(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lifewave-layout {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
color: #F5F5F5;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.lifewave-layout--dark {
|
||||
background: #000;
|
||||
color: #F5F5F5;
|
||||
}
|
||||
|
||||
/* FloatingLines Background */
|
||||
.lifewave-layout__background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Timeline — positioning comes from TimelineView's own .timeline class */
|
||||
.lifewave-layout__timeline {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.lifewave-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.lifewave-header__logo {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.3px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.lifewave-header__user {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.lifewave-content {
|
||||
padding-top: 72px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Backdrop blur overlay */
|
||||
.lifewave-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 15;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue