diff --git a/app/pages/copilot.vue b/app/pages/copilot.vue index 243561f..d355f59 100644 --- a/app/pages/copilot.vue +++ b/app/pages/copilot.vue @@ -56,7 +56,7 @@ useHead({ link: [{rel: 'manifest', href: '/copilot.webmanifest'}], }) -const STORAGE_KEY = 'opensquawk.copilot.v3' +const STORAGE_KEY = 'opensquawk.copilot.v4' const activeProfile = ref(a320Profile) const phases = computed(() => activeProfile.value.phases) @@ -68,36 +68,8 @@ const activeStepId = ref(null) const simbriefUser = ref('') const simbriefLoading = ref(false) const simbriefError = ref('') -const showAside = ref(false) -const rightTab = ref<'scratch' | 'canvas'>('scratch') - -// Quick-Jot -interface QuickNote { - text: string - time: string -} - -const quickDraft = ref('') -const quickNotes = ref([]) - -function feedQuick() { - const t = quickDraft.value.trim() - if (!t) return - const time = new Date().toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'}) - quickNotes.value.push({text: t, time}) - quickDraft.value = '' - persist() - nextTick(() => { - const f = document.querySelector('.quickjot-feed') as HTMLElement | null - if (f) f.scrollTop = f.scrollHeight - }) -} - -function clearJot() { - if (!confirm('Alle Notizen löschen?')) return - quickNotes.value = [] - persist() -} +const showCanvas = ref(true) +const showSimbrief = ref(false) // Glossary const glossaryByTerm = new Map(glossary.map(g => [g.term.toUpperCase(), g])) @@ -294,7 +266,45 @@ function scrollToStep(id: string) { } function toggleWhy(id: string) { + scrollLock = true expanded.value[id] = !expanded.value[id] + nextTick(() => { + scrollToStep(id) + }) +} + +function selectVariant(stepId: string, variantId: string) { + scrollLock = true + variantSel.value[stepId] = variantId + nextTick(() => scrollToStep(stepId)) +} + +function cycleVariant(dir: 1 | -1) { + const cur = activeStep.value + if (!cur) return + let owner: SopStep | null = null + for (const p of phases.value) { + for (const s of p.steps) { + if (!s.variants?.length) continue + if (s.id === cur.step.id) { + owner = s + break + } + const sel = variantSel.value[s.id] || s.variants[0].id + const v = s.variants.find(x => x.id === sel) + if (v?.steps.some(cs => cs.id === cur.step.id)) { + owner = s + break + } + } + if (owner) break + } + if (!owner || !owner.variants?.length) return + const ids = owner.variants.map(v => v.id) + const cursel = variantSel.value[owner.id] || ids[0] + const idx = ids.indexOf(cursel) + const next = ids[(idx + dir + ids.length) % ids.length] + selectVariant(owner.id, next) } function jumpToPhase(id: string) { @@ -304,27 +314,32 @@ function jumpToPhase(id: string) { } function resetAll() { - if (!confirm('Alle Eingaben & Fortschritt löschen?')) return + if (!confirm('Alle Eingaben, Fortschritt & Canvas löschen?')) return scratch.value = {} variantSel.value = {} expanded.value = {} activeStepId.value = phases.value[0]?.steps[0]?.id || null + canvasImage.value = '' + clearCanvas() persist() } // Persistence +const canvasImage = ref('') + function persist() { if (typeof localStorage === 'undefined') return localStorage.setItem(STORAGE_KEY, JSON.stringify({ scratch: scratch.value, variantSel: variantSel.value, activeStepId: activeStepId.value, - quickNotes: quickNotes.value, + simbriefUser: simbriefUser.value, + showCanvas: showCanvas.value, + canvasImage: canvasImage.value, })) } -watch([scratch, variantSel], persist, {deep: true}) -watch(quickNotes, persist, {deep: true}) +watch([scratch, variantSel, activeStepId, showCanvas, simbriefUser], persist, {deep: true}) // Keyboard function onKey(e: KeyboardEvent) { @@ -339,6 +354,12 @@ function onKey(e: KeyboardEvent) { } else if (e.key === 'Enter') { e.preventDefault() nextStep() + } else if (e.key === 'ArrowLeft') { + e.preventDefault() + cycleVariant(-1) + } else if (e.key === 'ArrowRight') { + e.preventDefault() + cycleVariant(1) } else if (e.key === 'i' || e.key === 'I') { if (activeStepId.value) toggleWhy(activeStepId.value) } @@ -378,7 +399,9 @@ onMounted(() => { if (v.scratch) scratch.value = v.scratch if (v.variantSel) variantSel.value = v.variantSel if (v.activeStepId) activeStepId.value = v.activeStepId - if (Array.isArray(v.quickNotes)) quickNotes.value = v.quickNotes + if (typeof v.simbriefUser === 'string') simbriefUser.value = v.simbriefUser + if (typeof v.showCanvas === 'boolean') showCanvas.value = v.showCanvas + if (typeof v.canvasImage === 'string') canvasImage.value = v.canvasImage } catch { } } @@ -397,30 +420,34 @@ onUnmounted(() => { window.removeEventListener('keydown', onKey) }) -// Scratchpad Felder -const groupedFields = computed(() => { - const groups: Record = {} - for (const f of scratchFields) { - if (!groups[f.group]) groups[f.group] = [] - groups[f.group].push(f) - } - return groups -}) - -const groupTitles: Record = { - flight: 'Flight', - env: 'ATIS / Environment', - atc: 'ATC', - perf: 'Performance', - fuel: 'Fuel & Weights', -} - // Canvas const canvasRef = ref(null) -const drawColor = ref('#fde68a') -const lineWidth = ref(2.6) +const drawColor = '#fde68a' +const lineWidth = 2.6 let canvasInited = false +function loadCanvasImage() { + const cv = canvasRef.value + if (!cv || !canvasImage.value) return + const img = new Image() + img.onload = () => { + const ctx = cv.getContext('2d')! + const r = cv.getBoundingClientRect() + ctx.drawImage(img, 0, 0, r.width, r.height) + } + img.src = canvasImage.value +} + +function saveCanvasImage() { + const cv = canvasRef.value + if (!cv) return + try { + canvasImage.value = cv.toDataURL('image/png') + persist() + } catch { + } +} + function setupCanvas() { const cv = canvasRef.value if (!cv || canvasInited) return @@ -429,6 +456,7 @@ function setupCanvas() { const resize = () => { const rect = cv.getBoundingClientRect() if (rect.width <= 0 || rect.height <= 0) return + const prev = canvasImage.value cv.width = rect.width * dpr cv.height = rect.height * dpr const ctx = cv.getContext('2d')! @@ -436,9 +464,15 @@ function setupCanvas() { ctx.scale(dpr, dpr) ctx.lineCap = 'round' ctx.lineJoin = 'round' + if (prev) { + const img = new Image() + img.onload = () => ctx.drawImage(img, 0, 0, rect.width, rect.height) + img.src = prev + } } resize() new ResizeObserver(resize).observe(cv) + if (canvasImage.value) nextTick(loadCanvasImage) let drawing = false let last: { x: number; y: number; p: number } | null = null @@ -457,8 +491,8 @@ function setupCanvas() { e.preventDefault() const ctx = cv.getContext('2d')! const cur = pos(e) - ctx.strokeStyle = drawColor.value - ctx.lineWidth = lineWidth.value * (0.5 + cur.p) + ctx.strokeStyle = drawColor + ctx.lineWidth = lineWidth * (0.5 + cur.p) ctx.beginPath() ctx.moveTo(last.x, last.y) ctx.lineTo(cur.x, cur.y) @@ -466,17 +500,19 @@ function setupCanvas() { last = cur }) const stop = (e: PointerEvent) => { + if (!drawing) return drawing = false last = null if (cv.hasPointerCapture(e.pointerId)) cv.releasePointerCapture(e.pointerId) + saveCanvasImage() } cv.addEventListener('pointerup', stop) cv.addEventListener('pointercancel', stop) cv.addEventListener('pointerleave', stop) } -watch(rightTab, (v) => { - if (v === 'canvas') nextTick(setupCanvas) +watch(showCanvas, (v) => { + if (v) nextTick(setupCanvas) }) function clearCanvas() { @@ -489,6 +525,25 @@ function clearCanvas() { ctx.clearRect(0, 0, cv.width, cv.height) ctx.restore() ctx.scale(dpr, dpr) + canvasImage.value = '' + persist() +} + +function cleanfeedCanvas() { + const cv = canvasRef.value + if (!cv) return + const ctx = cv.getContext('2d')! + const dpr = window.devicePixelRatio || 1 + const rect = cv.getBoundingClientRect() + const shift = Math.round(rect.height * 0.4) + const img = ctx.getImageData(0, 0, cv.width, cv.height) + ctx.save() + ctx.setTransform(1, 0, 0, 1, 0, 0) + ctx.clearRect(0, 0, cv.width, cv.height) + ctx.putImageData(img, 0, -shift * dpr) + ctx.restore() + ctx.scale(dpr, dpr) + saveCanvasImage() } // Actor-Meta @@ -528,48 +583,62 @@ const actorLabel: Record = {
-
- mdi-cloud-download-outline - +
+ +
+
SimBrief Import
+ + +
{{ simbriefError }}
+
+
-
-
+
-
{{ simbriefError }}
@@ -668,7 +737,7 @@ const actorLabel: Record = { :key="v.id" class="variant-tab" :class="{active: (variantSel[step.id] || step.variants[0].id) === v.id}" - @click="variantSel[step.id] = v.id" + @click="selectVariant(step.id, v.id)" > {{ v.title }} @@ -700,78 +769,23 @@ const actorLabel: Record = {
- -