mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-09 19:16:02 +08:00
intercom rausgefactored und ui improvements
This commit is contained in:
161
app/pages/pm.vue
161
app/pages/pm.vue
@@ -125,7 +125,7 @@
|
|||||||
<p class="text-base font-semibold leading-tight truncate">{{ flightContext.callsign || 'N/A' }}</p>
|
<p class="text-base font-semibold leading-tight truncate">{{ flightContext.callsign || 'N/A' }}</p>
|
||||||
<p class="text-[11px] text-white/55 leading-tight truncate">{{ flightContext.dep }} → {{ flightContext.dest }}</p>
|
<p class="text-[11px] text-white/55 leading-tight truncate">{{ flightContext.dep }} → {{ flightContext.dest }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden xs:flex flex-col gap-1 shrink-0">
|
<div class="hidden sm:flex flex-col gap-1 shrink-0">
|
||||||
<v-chip :color="flags.in_air ? 'green' : 'grey'" size="x-small" variant="flat">
|
<v-chip :color="flags.in_air ? 'green' : 'grey'" size="x-small" variant="flat">
|
||||||
{{ flags.in_air ? 'IN-AIR' : 'GROUND' }}
|
{{ flags.in_air ? 'IN-AIR' : 'GROUND' }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
@@ -162,14 +162,25 @@
|
|||||||
<v-icon>mdi-swap-vertical</v-icon>
|
<v-icon>mdi-swap-vertical</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<div class="hidden sm:flex items-end gap-1 pl-1">
|
<HoldSelect
|
||||||
<div
|
:options="readabilityOptions"
|
||||||
v-for="i in 5"
|
placement="down"
|
||||||
:key="i"
|
title="Verständlichkeit"
|
||||||
class="signal-bar"
|
@select="onReadabilitySelect"
|
||||||
:class="{ 'signal-active': i <= signalStrength }"
|
>
|
||||||
/>
|
<template #default="{ open }">
|
||||||
</div>
|
<button type="button" class="signal-chip" :class="{ 'is-open': open }" aria-label="Verständlichkeit einstellen">
|
||||||
|
<span class="signal-bars">
|
||||||
|
<span
|
||||||
|
v-for="i in 5"
|
||||||
|
:key="i"
|
||||||
|
class="signal-bar"
|
||||||
|
:class="{ 'signal-active': i <= signalStrength }"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</HoldSelect>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -239,25 +250,31 @@
|
|||||||
|
|
||||||
<!-- Input mode switch: voice / text -->
|
<!-- Input mode switch: voice / text -->
|
||||||
<div v-show="activeTab === 'funk'" class="pm-block">
|
<div v-show="activeTab === 'funk'" class="pm-block">
|
||||||
<v-btn-toggle v-model="inputMode" color="cyan" mandatory class="flex w-full">
|
<div class="pm-seg">
|
||||||
<v-btn value="voice" prepend-icon="mdi-microphone" class="flex-1">Sprechen</v-btn>
|
<button
|
||||||
<v-btn value="text" prepend-icon="mdi-keyboard" class="flex-1">Tippen</v-btn>
|
type="button"
|
||||||
</v-btn-toggle>
|
class="pm-seg-btn"
|
||||||
|
:class="{ 'is-active': inputMode === 'voice' }"
|
||||||
|
@click="inputMode = 'voice'"
|
||||||
|
>
|
||||||
|
<v-icon size="18">mdi-microphone</v-icon>
|
||||||
|
<span>Sprechen</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pm-seg-btn"
|
||||||
|
:class="{ 'is-active': inputMode === 'text' }"
|
||||||
|
@click="inputMode = 'text'"
|
||||||
|
>
|
||||||
|
<v-icon size="18">mdi-keyboard</v-icon>
|
||||||
|
<span>Tippen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Push to Talk (voice mode) -->
|
<!-- Push to Talk (voice mode) -->
|
||||||
<div v-show="activeTab === 'funk' && inputMode === 'voice'" class="pm-block">
|
<div v-show="activeTab === 'funk' && inputMode === 'voice'" class="pm-block">
|
||||||
<v-sheet class="rounded-3xl border border-white/10 bg-gradient-to-br from-white/5 via-white/10 to-transparent p-4 shadow-lg">
|
<v-sheet class="rounded-3xl border border-white/10 bg-gradient-to-br from-white/5 via-white/10 to-transparent p-4 shadow-lg">
|
||||||
<v-btn-toggle
|
|
||||||
v-model="radioMode"
|
|
||||||
color="cyan"
|
|
||||||
class="mb-4 flex w-full"
|
|
||||||
mandatory
|
|
||||||
>
|
|
||||||
<v-btn value="atc" prepend-icon="mdi-radio-handheld" class="flex-1">ATC</v-btn>
|
|
||||||
<v-btn value="intercom" prepend-icon="mdi-account-voice" class="flex-1">Intercom</v-btn>
|
|
||||||
</v-btn-toggle>
|
|
||||||
|
|
||||||
<v-alert
|
<v-alert
|
||||||
v-if="!micPermission"
|
v-if="!micPermission"
|
||||||
type="info"
|
type="info"
|
||||||
@@ -273,30 +290,29 @@
|
|||||||
|
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<div
|
<div
|
||||||
class="ptt-pad flex h-48 lg:h-56 items-center justify-center rounded-2xl border border-white/10 bg-black/40 text-center transition cursor-pointer"
|
class="ptt-pad flex h-52 lg:h-60 items-center justify-center rounded-2xl border text-center transition cursor-pointer"
|
||||||
:class="isRecording ? 'ring-4 ring-cyan-400/70 shadow-2xl bg-cyan-900/20' : 'ring-1 ring-white/5'"
|
:class="isRecording ? 'border-red-400/40 ring-4 ring-red-400/40 bg-red-500/10' : 'border-white/10 ring-1 ring-white/5 bg-black/40'"
|
||||||
@touchstart.prevent="startRecording(radioMode === 'intercom')"
|
@touchstart.prevent="startRecording(false)"
|
||||||
@touchend.prevent="stopRecording"
|
@touchend.prevent="stopRecording"
|
||||||
@touchcancel.prevent="stopRecording"
|
@touchcancel.prevent="stopRecording"
|
||||||
@mousedown.prevent="startRecording(radioMode === 'intercom')"
|
@mousedown.prevent="startRecording(false)"
|
||||||
@mouseup.prevent="stopRecording"
|
@mouseup.prevent="stopRecording"
|
||||||
@mouseleave="stopRecording"
|
@mouseleave="stopRecording"
|
||||||
>
|
>
|
||||||
<div>
|
<div class="space-y-1">
|
||||||
<v-icon
|
<v-icon
|
||||||
:icon="isRecording ? 'mdi-record' : (radioMode === 'atc' ? 'mdi-radio-handheld' : 'mdi-headphones')"
|
:icon="isRecording ? 'mdi-access-point' : 'mdi-radio-handheld'"
|
||||||
size="48"
|
size="44"
|
||||||
:class="isRecording ? 'text-red-400 animate-pulse' : 'text-cyan-300'"
|
:class="isRecording ? 'text-red-400 animate-pulse' : 'text-cyan-300'"
|
||||||
/>
|
/>
|
||||||
<p class="text-xs uppercase tracking-[0.35em] text-white/40 mt-2">
|
<p
|
||||||
{{ isRecording ? 'TRANSMITTING' : 'Hold to transmit' }}
|
class="text-[11px] uppercase tracking-[0.35em] mt-1"
|
||||||
</p>
|
:class="isRecording ? 'text-red-300' : 'text-white/40'"
|
||||||
<p class="pt-3 text-3xl font-semibold">
|
>
|
||||||
{{ radioMode === 'atc' ? 'ATC' : 'INTERCOM' }}
|
{{ isRecording ? 'Auf Sendung' : 'Halten zum Senden' }}
|
||||||
</p>
|
|
||||||
<p class="mt-2 text-sm text-white/60">
|
|
||||||
{{ radioMode === 'atc' ? `Transmitting on ${frequencies.active}` : 'Crew Intercom • Checklist' }}
|
|
||||||
</p>
|
</p>
|
||||||
|
<p class="pt-2 text-4xl font-bold font-mono tracking-tight">{{ frequencies.active || '---' }}</p>
|
||||||
|
<p class="text-xs text-white/45">Active frequency</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
@@ -309,7 +325,7 @@
|
|||||||
<v-card-text class="space-y-3">
|
<v-card-text class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h3 class="text-lg font-semibold">Text Input</h3>
|
<h3 class="text-lg font-semibold">Text Input</h3>
|
||||||
<v-chip size="small" color="cyan" variant="outlined">{{ radioMode === 'atc' ? 'ATC' : 'Intercom' }}</v-chip>
|
<v-chip size="small" color="cyan" variant="outlined" class="font-mono">{{ frequencies.active || '---' }}</v-chip>
|
||||||
</div>
|
</div>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="pilotInput"
|
v-model="pilotInput"
|
||||||
@@ -1557,7 +1573,6 @@ const currentScreen = ref<'login' | 'flightselect' | 'monitor'>('login')
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const pilotInput = ref('')
|
const pilotInput = ref('')
|
||||||
const radioMode = ref<'atc' | 'intercom'>('atc')
|
|
||||||
const isRecording = ref(false)
|
const isRecording = ref(false)
|
||||||
const micPermission = ref(false)
|
const micPermission = ref(false)
|
||||||
const swapAnimation = ref(false)
|
const swapAnimation = ref(false)
|
||||||
@@ -2782,6 +2797,18 @@ const onPresetSelectStandby = (opt: { value: string | number }) => {
|
|||||||
if (entry) setStandbyFrequencyFromList(entry)
|
if (entry) setStandbyFrequencyFromList(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Readability / signal strength quick-select (top bar)
|
||||||
|
const readabilityOptions = [
|
||||||
|
{ value: 5, label: 'Excellent', sublabel: 'Readability 5', color: '#22c55e' },
|
||||||
|
{ value: 4, label: 'Good', sublabel: 'Readability 4', color: '#22c55e' },
|
||||||
|
{ value: 3, label: 'Fair', sublabel: 'Readability 3', color: '#eab308' },
|
||||||
|
{ value: 2, label: 'Poor', sublabel: 'Readability 2', color: '#f97316' },
|
||||||
|
{ value: 1, label: 'Weak', sublabel: 'Readability 1', color: '#ef4444' },
|
||||||
|
]
|
||||||
|
const onReadabilitySelect = (opt: { value: string | number }) => {
|
||||||
|
signalStrength.value = Number(opt.value)
|
||||||
|
}
|
||||||
|
|
||||||
const formatTime = (date: Date): string => {
|
const formatTime = (date: Date): string => {
|
||||||
return date.toLocaleTimeString('en-US', {
|
return date.toLocaleTimeString('en-US', {
|
||||||
hour12: false,
|
hour12: false,
|
||||||
@@ -3236,6 +3263,60 @@ watch(() => activeFrequency.value, (newFreq) => {
|
|||||||
transform: scale(0.97);
|
transform: scale(0.97);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Unified segmented control (voice / text, etc.) */
|
||||||
|
.pm-seg {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
.pm-seg-btn {
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 11px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
transition: color 130ms ease, background 130ms ease;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.pm-seg-btn:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
.pm-seg-btn.is-active {
|
||||||
|
color: #050910;
|
||||||
|
background: #22d3ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Readability / signal trigger in the top bar */
|
||||||
|
.signal-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
transition: border-color 120ms ease, background 120ms ease, transform 80ms ease;
|
||||||
|
}
|
||||||
|
.signal-chip.is-open {
|
||||||
|
border-color: rgba(34, 211, 238, 0.7);
|
||||||
|
background: rgba(34, 211, 238, 0.14);
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
.signal-chip .signal-bars {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Tablet / desktop: use the extra space ----------------------------------- */
|
/* Tablet / desktop: use the extra space ----------------------------------- */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.pm-sidenav {
|
.pm-sidenav {
|
||||||
|
|||||||
Reference in New Issue
Block a user