546 lines
14 KiB
Vue
546 lines
14 KiB
Vue
<template>
|
|
<Transition name="slide-up">
|
|
<div
|
|
v-if="eventsStore.panelOpen"
|
|
class="event-panel glass--panel"
|
|
:style="panelHeight != null ? { height: panelHeight + 'dvh' } : {}"
|
|
:class="{ 'event-panel--dragging': isDragging }"
|
|
>
|
|
<!-- Drag Handle — drag to resize, tap to close -->
|
|
<div
|
|
class="event-panel__handle"
|
|
v-on="handleListeners"
|
|
>
|
|
<div class="event-panel__handle-bar"></div>
|
|
</div>
|
|
|
|
<!-- Scrollable content -->
|
|
<div class="event-panel__scroll">
|
|
<!-- Key Image -->
|
|
<div class="event-panel__image-section">
|
|
<div v-if="eventsStore.ghostImage" class="event-panel__image-wrap">
|
|
<img :src="keyImageSrc || eventsStore.ghostImage" class="event-panel__image" alt="" />
|
|
<span class="event-panel__image-badge">Key Image</span>
|
|
</div>
|
|
<div v-else class="event-panel__image-placeholder" @click="onAddImage">
|
|
<q-icon name="add_photo_alternate" size="32px" color="grey-5" />
|
|
<span>Key Image hinzufügen</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Title — large, editable inline -->
|
|
<q-input
|
|
v-model="eventsStore.ghostTitle"
|
|
placeholder="Was ist passiert?"
|
|
borderless
|
|
class="event-panel__title"
|
|
input-class="event-panel__title-input"
|
|
:dark="isDark"
|
|
/>
|
|
|
|
<!-- Date row — tap to open QDate picker -->
|
|
<div class="event-panel__date-row">
|
|
<q-icon name="event" size="18px" class="event-panel__date-icon" />
|
|
<span class="event-panel__date-label">{{ formattedDate }}</span>
|
|
<q-popup-proxy transition-show="scale" transition-hide="scale">
|
|
<q-date
|
|
v-model="ghostDateSlash"
|
|
mask="YYYY/MM/DD"
|
|
:dark="isDark"
|
|
:locale="dateLocale"
|
|
minimal
|
|
/>
|
|
</q-popup-proxy>
|
|
</div>
|
|
|
|
<!-- Emotional Level — card style with gradient track -->
|
|
<div class="event-panel__card" :class="{ 'event-panel__card--dark': isDark }">
|
|
<div class="event-panel__card-header">
|
|
<span class="event-panel__card-label">Emotional Level</span>
|
|
<span class="event-panel__emotion-value" :style="{ color: emotionColor }">
|
|
{{ emotionLabel }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Gradient slider -->
|
|
<div class="event-panel__slider-wrap">
|
|
<q-slider
|
|
v-model="eventsStore.ghostEmotion"
|
|
:min="-1"
|
|
:max="1"
|
|
:step="0.05"
|
|
track-size="8px"
|
|
thumb-size="22px"
|
|
class="event-panel__slider"
|
|
:style="{
|
|
'--gradient-bg': sliderGradientCSS,
|
|
'--thumb-color': emotionColor
|
|
}"
|
|
/>
|
|
</div>
|
|
|
|
<div class="event-panel__slider-hints">
|
|
<span>Sehr negativ</span>
|
|
<span>Neutral</span>
|
|
<span>Sehr positiv</span>
|
|
</div>
|
|
|
|
<!-- Gradient Preset Selector -->
|
|
<div class="event-panel__presets">
|
|
<span class="event-panel__presets-label">Farbverlauf</span>
|
|
<div class="event-panel__presets-grid">
|
|
<div
|
|
v-for="(preset, index) in gradientPresets"
|
|
:key="index"
|
|
class="event-panel__preset"
|
|
:class="{ 'event-panel__preset--active': eventsStore.ghostGradientPreset === index }"
|
|
:style="{ background: presetGradientCSS(preset.colors) }"
|
|
:title="preset.name"
|
|
@click="selectPreset(index)"
|
|
></div>
|
|
<!-- "None" option to clear preset -->
|
|
<div
|
|
class="event-panel__preset event-panel__preset--none"
|
|
:class="{ 'event-panel__preset--active': eventsStore.ghostGradientPreset === null }"
|
|
title="Standard"
|
|
@click="selectPreset(null)"
|
|
>
|
|
<q-icon name="auto_awesome" size="12px" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Beschreibung — card style -->
|
|
<div class="event-panel__card" :class="{ 'event-panel__card--dark': isDark }">
|
|
<span class="event-panel__card-label">Beschreibung</span>
|
|
<q-input
|
|
v-model="eventsStore.ghostNote"
|
|
type="textarea"
|
|
placeholder="Ein neutraler, aber wichtiger Tag"
|
|
borderless
|
|
autogrow
|
|
class="event-panel__note"
|
|
:dark="isDark"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Weitere Medien -->
|
|
<div class="event-panel__card" :class="{ 'event-panel__card--dark': isDark }">
|
|
<span class="event-panel__card-label">Weitere Medien</span>
|
|
<div class="event-panel__media-grid">
|
|
<div class="event-panel__media-add" @click="onAddMedia">
|
|
<q-icon name="add_photo_alternate" size="24px" color="grey-5" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete (edit mode only) -->
|
|
<div v-if="eventsStore.editingEventId" class="event-panel__delete">
|
|
<q-btn
|
|
flat
|
|
dense
|
|
no-caps
|
|
label="Event löschen"
|
|
icon="delete_outline"
|
|
color="negative"
|
|
size="sm"
|
|
@click="eventsStore.deleteEvent(eventsStore.editingEventId)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, watch, ref } from 'vue'
|
|
import { useQuasar } from 'quasar'
|
|
import { useEventsStore, emotionToColor, GRADIENT_PRESETS } from 'stores/events'
|
|
import { usePanelDrag } from 'composables/usePanelDrag'
|
|
import { resolveFullRes } from 'composables/useImageCache'
|
|
|
|
const $q = useQuasar()
|
|
const eventsStore = useEventsStore()
|
|
const { panelHeight, isDragging, handleListeners, resetHeight } = usePanelDrag(() => eventsStore.closePanel())
|
|
|
|
// Resolve key image: full-res when online, cached thumbnail when offline
|
|
const keyImageSrc = ref(null)
|
|
watch(
|
|
() => eventsStore.ghostImage,
|
|
async (img) => {
|
|
keyImageSrc.value = await resolveFullRes(img)
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
// Reset height when panel opens
|
|
watch(() => eventsStore.panelOpen, (open) => { if (open) resetHeight() })
|
|
const isDark = computed(() => $q.dark.isActive)
|
|
const gradientPresets = GRADIENT_PRESETS
|
|
|
|
// Current glow color based on emotion + gradient
|
|
const emotionColor = computed(() => {
|
|
if (eventsStore.ghostCustomColor) return eventsStore.ghostCustomColor
|
|
return emotionToColor(eventsStore.ghostEmotion, eventsStore.ghostGradientPreset)
|
|
})
|
|
|
|
// Date: store uses YYYY-MM-DD, QDate uses YYYY/MM/DD
|
|
const ghostDateSlash = computed({
|
|
get: () => eventsStore.ghostDate.replace(/-/g, '/'),
|
|
set: (val) => { eventsStore.ghostDate = val.replace(/\//g, '-') }
|
|
})
|
|
|
|
const formattedDate = computed(() => {
|
|
const d = new Date(eventsStore.ghostDate)
|
|
if (isNaN(d.getTime())) return eventsStore.ghostDate
|
|
return d.toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' })
|
|
})
|
|
|
|
const dateLocale = {
|
|
days: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
|
|
daysShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
|
|
months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
|
|
monthsShort: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
|
|
}
|
|
|
|
const emotionLabel = computed(() => {
|
|
const e = eventsStore.ghostEmotion
|
|
if (e > 0.5) return 'Sehr positiv'
|
|
if (e > 0.15) return 'Positiv'
|
|
if (e > -0.15) return 'Neutral'
|
|
if (e > -0.5) return 'Negativ'
|
|
return 'Sehr negativ'
|
|
})
|
|
|
|
// CSS gradient for the slider track
|
|
const sliderGradientCSS = computed(() => {
|
|
const idx = eventsStore.ghostGradientPreset
|
|
if (idx !== null && GRADIENT_PRESETS[idx]) {
|
|
const [neg, mid, pos] = GRADIENT_PRESETS[idx].colors
|
|
return `linear-gradient(90deg, ${neg} 0%, ${mid} 50%, ${pos} 100%)`
|
|
}
|
|
// Default gradient matching the default emotionToColor
|
|
return 'linear-gradient(90deg, #E91E63 0%, #9C27B0 20%, #2196F3 35%, #FFD700 50%, #FF6B35 65%, #FFD700 80%, #4CAF50 100%)'
|
|
})
|
|
|
|
// CSS gradient for a preset swatch
|
|
function presetGradientCSS(colors) {
|
|
return `linear-gradient(90deg, ${colors[0]}, ${colors[1]}, ${colors[2]})`
|
|
}
|
|
|
|
function selectPreset(index) {
|
|
eventsStore.ghostGradientPreset = index
|
|
// Clear custom color when selecting a gradient
|
|
eventsStore.ghostCustomColor = null
|
|
}
|
|
|
|
function onAddImage() {
|
|
// TODO: File picker for key image
|
|
}
|
|
|
|
function onAddMedia() {
|
|
// TODO: File picker for additional media
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.event-panel {
|
|
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;
|
|
}
|
|
|
|
.event-panel--dragging {
|
|
transition: none;
|
|
}
|
|
|
|
.event-panel__handle {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 10px 0 4px;
|
|
flex-shrink: 0;
|
|
cursor: grab;
|
|
touch-action: none;
|
|
}
|
|
|
|
.event-panel__handle:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.event-panel__handle-bar {
|
|
width: 36px;
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
background: rgba(128, 128, 128, 0.3);
|
|
}
|
|
|
|
.event-panel__scroll {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0 20px 32px;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
/* Key Image */
|
|
.event-panel__image-section {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.event-panel__image-wrap {
|
|
position: relative;
|
|
border-radius: 16px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.event-panel__image {
|
|
width: 100%;
|
|
height: 200px;
|
|
object-fit: cover;
|
|
display: block;
|
|
}
|
|
|
|
.event-panel__image-badge {
|
|
position: absolute;
|
|
top: 12px;
|
|
left: 12px;
|
|
background: rgba(0, 0, 0, 0.55);
|
|
color: #fff;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.event-panel__image-placeholder {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
height: 120px;
|
|
border-radius: 16px;
|
|
border: 2px dashed rgba(128, 128, 128, 0.2);
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
opacity: 0.5;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.event-panel__image-placeholder:hover {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
/* Title */
|
|
.event-panel__title {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.event-panel__title :deep(.event-panel__title-input) {
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
/* Date row */
|
|
.event-panel__date-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
margin-bottom: 20px;
|
|
opacity: 0.6;
|
|
cursor: pointer;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.event-panel__date-row:hover {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.event-panel__date-icon {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.event-panel__date-label {
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* Card sections */
|
|
.event-panel__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;
|
|
}
|
|
|
|
.event-panel__card--dark {
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border-color: rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.event-panel__card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.event-panel__card-label {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.event-panel__emotion-value {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Slider with gradient track via Quasar deep styling */
|
|
.event-panel__slider-wrap {
|
|
padding: 8px 0;
|
|
}
|
|
|
|
/* The actual visible track bar — apply gradient here */
|
|
.event-panel__slider :deep(.q-slider__track) {
|
|
background: var(--gradient-bg) !important;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Hide the default grey inner bar */
|
|
.event-panel__slider :deep(.q-slider__inner) {
|
|
background: transparent !important;
|
|
}
|
|
|
|
/* Hide the blue selection bar */
|
|
.event-panel__slider :deep(.q-slider__selection) {
|
|
background: transparent !important;
|
|
}
|
|
|
|
/* Thumb — SVG-based, use color for fill/stroke */
|
|
.event-panel__slider :deep(.q-slider__thumb) {
|
|
color: var(--thumb-color, #fff);
|
|
}
|
|
|
|
.event-panel__slider :deep(.q-slider__thumb-shape) {
|
|
filter: drop-shadow(0 0 6px var(--thumb-color, #fff));
|
|
}
|
|
|
|
.event-panel__slider :deep(.q-slider__focus-ring) {
|
|
display: none;
|
|
}
|
|
|
|
.event-panel__slider-hints {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 10px;
|
|
opacity: 0.4;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* Gradient Preset Selector */
|
|
.event-panel__presets {
|
|
margin-top: 16px;
|
|
border-top: 1px solid rgba(128, 128, 128, 0.1);
|
|
padding-top: 12px;
|
|
}
|
|
|
|
.event-panel__presets-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
opacity: 0.5;
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.event-panel__presets-grid {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.event-panel__preset {
|
|
width: 45px;
|
|
height: 25px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
border: 2px solid #eee;
|
|
transition: border-color 0.2s, transform 0.15s;
|
|
}
|
|
|
|
.event-panel__preset:hover {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.event-panel__preset--active {
|
|
border-color: currentColor;
|
|
transform: scale(1.1);
|
|
box-shadow: 0 0 0 1px rgba(128, 128, 128, 0.3);
|
|
}
|
|
|
|
.event-panel__preset--none {
|
|
background: rgba(128, 128, 128, 0.15);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* Note */
|
|
.event-panel__note {
|
|
margin-top: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* Media grid */
|
|
.event-panel__media-grid {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.event-panel__media-add {
|
|
width: 64px;
|
|
height: 64px;
|
|
border-radius: 10px;
|
|
border: 2px dashed rgba(128, 128, 128, 0.2);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.event-panel__media-add:hover {
|
|
border-color: rgba(128, 128, 128, 0.4);
|
|
}
|
|
|
|
/* Delete */
|
|
.event-panel__delete {
|
|
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>
|