mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-30 11:23:49 +08:00
fix recording and revert nav tab
This commit is contained in:
@@ -98,6 +98,7 @@
|
||||
|
||||
--appbar-height: 64px;
|
||||
--bottomnav-height: 72px;
|
||||
--bottomnav-clearance: calc(var(--bottomnav-height) + env(safe-area-inset-bottom, 0px) + var(--sp-5));
|
||||
--touch-target: 44px;
|
||||
--z-appbar: 100;
|
||||
--z-drawer: 200;
|
||||
@@ -1387,17 +1388,29 @@ button.gx-chip:hover {
|
||||
}
|
||||
}
|
||||
|
||||
.gx-content:has(.gx-navigation-view) {
|
||||
margin-top: calc(-1 * (var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-1)));
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.gx-navigation-view {
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gx-navigation-stage {
|
||||
background: var(--surface-container-low);
|
||||
min-height: calc(100dvh - var(--appbar-height));
|
||||
height: 100dvh;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gx-navigation-map {
|
||||
height: calc(100dvh - var(--appbar-height));
|
||||
min-height: 520px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1422,7 +1435,10 @@ button.gx-chip:hover {
|
||||
|
||||
.gx-navigation-overlay {
|
||||
inset: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: var(--sp-4);
|
||||
padding-top: calc(var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-4));
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
@@ -1432,6 +1448,19 @@ button.gx-chip:hover {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.gx-navigation-tabs {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: var(--sp-4);
|
||||
top: calc(var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-4));
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.gx-navigation-tabs .gx-tabs {
|
||||
margin-bottom: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.gx-navigation-search,
|
||||
.gx-navigation-summary,
|
||||
.gx-navigation-recent {
|
||||
@@ -1527,87 +1556,98 @@ button.gx-chip:hover {
|
||||
}
|
||||
|
||||
.gx-navigation-summary {
|
||||
margin: var(--sp-4) 0 0;
|
||||
padding: var(--sp-3);
|
||||
margin: var(--sp-3) 0 0;
|
||||
padding: var(--sp-2);
|
||||
}
|
||||
|
||||
.gx-navigation-summary__title {
|
||||
background: var(--surface-container-high);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--fs-lg);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--sp-1);
|
||||
padding: 0 var(--sp-1);
|
||||
}
|
||||
|
||||
.gx-navigation-summary__name {
|
||||
font-size: var(--fs-base);
|
||||
font-weight: var(--fw-bold);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gx-navigation-summary__rows {
|
||||
display: grid;
|
||||
gap: var(--sp-1);
|
||||
padding: var(--sp-3) var(--sp-2);
|
||||
.gx-navigation-summary__fav {
|
||||
color: var(--text-muted);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.gx-navigation-summary__rows > div {
|
||||
align-items: center;
|
||||
.gx-navigation-summary__fav.active { color: var(--accent-rose); }
|
||||
|
||||
.gx-navigation-metrics {
|
||||
display: grid;
|
||||
gap: var(--sp-2);
|
||||
grid-template-columns: 28px 90px 1fr;
|
||||
min-height: 28px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
padding: var(--sp-2) 0;
|
||||
}
|
||||
|
||||
.gx-navigation-summary__rows strong {
|
||||
font-weight: var(--fw-bold);
|
||||
}
|
||||
|
||||
.gx-navigation-summary__icon {
|
||||
font-size: 1.15rem;
|
||||
.gx-navigation-metric {
|
||||
align-items: center;
|
||||
background: var(--surface-container-high);
|
||||
border-radius: var(--radius-sm);
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
justify-items: center;
|
||||
min-width: 0;
|
||||
padding: var(--sp-2) var(--sp-1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gx-navigation-route-picker {
|
||||
border-top: 1px solid var(--outline);
|
||||
display: grid;
|
||||
gap: var(--sp-2);
|
||||
margin-top: var(--sp-2);
|
||||
padding: var(--sp-3) var(--sp-2) 0;
|
||||
.gx-navigation-metric i {
|
||||
color: var(--primary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.gx-navigation-route-picker__title {
|
||||
.gx-navigation-metric span {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--fs-xs);
|
||||
}
|
||||
|
||||
.gx-navigation-metric strong {
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: var(--fw-bold);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.gx-navigation-route-picker {
|
||||
display: flex;
|
||||
gap: var(--sp-2);
|
||||
overflow-x: auto;
|
||||
padding: var(--sp-1) 0 var(--sp-2);
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.gx-navigation-route-picker::-webkit-scrollbar { display: none; }
|
||||
|
||||
.gx-navigation-route-option {
|
||||
align-items: center;
|
||||
background: var(--surface-container);
|
||||
border: 1px solid var(--outline);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: var(--radius-full);
|
||||
color: var(--on-surface);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: var(--sp-3);
|
||||
justify-content: space-between;
|
||||
min-height: 52px;
|
||||
padding: var(--sp-2) var(--sp-3);
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gx-navigation-route-option > span {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 6px;
|
||||
min-height: 36px;
|
||||
padding: 0 var(--sp-3);
|
||||
}
|
||||
|
||||
.gx-navigation-route-option small {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--fs-xs);
|
||||
}
|
||||
|
||||
.gx-navigation-route-option > strong:last-child {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1618,19 +1658,15 @@ button.gx-chip:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.gx-navigation-route-option.selected > strong:first-child {
|
||||
color: var(--primary);
|
||||
}
|
||||
.gx-navigation-route-option.selected { color: var(--primary); }
|
||||
|
||||
.gx-navigation-summary__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-2);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gx-navigation-summary__actions .gx-btn {
|
||||
flex: 1 1 190px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -1638,9 +1674,6 @@ button.gx-chip:hover {
|
||||
.gx-btn--success:hover { background: #6df3e9; }
|
||||
.gx-btn--danger { background: var(--error); color: var(--on-error); }
|
||||
.gx-btn--danger:hover { background: #ff7492; }
|
||||
.gx-btn--favorite { background: var(--accent-rose); color: #fff; }
|
||||
.gx-btn--favorite:hover,
|
||||
.gx-btn--favorite.active { background: #ed4f94; }
|
||||
|
||||
.gx-navigation-recent {
|
||||
margin: var(--sp-4) 0 0;
|
||||
@@ -1658,7 +1691,7 @@ button.gx-chip:hover {
|
||||
gap: var(--sp-3);
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
min-height: calc(100dvh - var(--appbar-height));
|
||||
min-height: 100%;
|
||||
padding: var(--sp-6);
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1671,11 +1704,29 @@ button.gx-chip:hover {
|
||||
@media (max-width: 767px) {
|
||||
.gx-navigation-overlay {
|
||||
padding: var(--sp-3);
|
||||
padding-top: calc(var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-3));
|
||||
padding-bottom: calc(var(--bottomnav-clearance) + 56px);
|
||||
}
|
||||
|
||||
.gx-navigation-summary__rows > div {
|
||||
grid-template-columns: 28px 82px 1fr;
|
||||
.gx-navigation-tabs {
|
||||
bottom: var(--bottomnav-clearance);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
left: var(--sp-3);
|
||||
right: var(--sp-3);
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.gx-navigation-tabs .gx-tabs {
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
padding: 2px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.gx-navigation-tabs .gx-tabs::-webkit-scrollbar { display: none; }
|
||||
}
|
||||
|
||||
.gx-nav-section { margin-bottom: var(--sp-3); }
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Tools } from "./views/Tools.js"
|
||||
import { Recordings } from "./views/Recordings.js"
|
||||
import { Logs } from "./views/Logs.js"
|
||||
import { Tuning } from "./views/Tuning.js"
|
||||
import { Navigation } from "./views/Navigation.js?v=nav-destination-4"
|
||||
import { Navigation } from "./views/Navigation.js?v=nav-destination-6"
|
||||
import { Vehicle } from "./views/Vehicle.js"
|
||||
import { Bluetooth } from "./views/Bluetooth.js"
|
||||
import { SystemTools } from "./views/SystemTools.js"
|
||||
|
||||
+12
-9
@@ -447,26 +447,29 @@ export const NavigationDestinationPanel = {
|
||||
</section>
|
||||
|
||||
<section v-if="destination" class="gx-navigation-summary gx-card">
|
||||
<div class="gx-navigation-summary__title">{{ destination.name || query || 'Destination' }}</div>
|
||||
<div v-if="routeSummary" class="gx-navigation-summary__rows">
|
||||
<div><span class="gx-navigation-summary__icon">🛣️</span><span>Distance:</span><strong>{{ formatDistance(routeSummary.distance) }}</strong></div>
|
||||
<div><span class="gx-navigation-summary__icon">⌛</span><span>Duration:</span><strong>{{ formatDuration(routeSummary.duration) }}</strong></div>
|
||||
<div><span class="gx-navigation-summary__icon">🕗</span><span>ETA:</span><strong>{{ formatEta(routeSummary.duration) }}</strong></div>
|
||||
<div class="gx-navigation-summary__title">
|
||||
<span class="gx-navigation-summary__name">{{ destination.name || query || 'Destination' }}</span>
|
||||
<button type="button" class="gx-icon-btn gx-navigation-summary__fav" :class="{ active: isFavorite }" :aria-pressed="isFavorite" :title="isFavorite ? 'Remove from favorites' : 'Add to favorites'" @click="toggleFavorite">
|
||||
<i class="bi" :class="isFavorite ? 'bi-heart-fill' : 'bi-heart'"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="routeSummary" class="gx-navigation-metrics">
|
||||
<div class="gx-navigation-metric"><i class="bi bi-signpost-2" aria-hidden="true"></i><span>Distance</span><strong>{{ formatDistance(routeSummary.distance) }}</strong></div>
|
||||
<div class="gx-navigation-metric"><i class="bi bi-clock" aria-hidden="true"></i><span>Duration</span><strong>{{ formatDuration(routeSummary.duration) }}</strong></div>
|
||||
<div class="gx-navigation-metric"><i class="bi bi-clock-history" aria-hidden="true"></i><span>ETA</span><strong>{{ formatEta(routeSummary.duration) }}</strong></div>
|
||||
</div>
|
||||
<div v-if="routes.length > 1" class="gx-navigation-route-picker" aria-label="Choose a route">
|
||||
<div class="gx-navigation-route-picker__title">Routes</div>
|
||||
<button v-for="(route, index) in routes" :key="routeId(index)" type="button"
|
||||
class="gx-navigation-route-option" :class="{ selected: selectedRouteId === routeId(index) }"
|
||||
:aria-pressed="selectedRouteId === routeId(index)" :aria-label="'Select route ' + (index + 1)"
|
||||
@click="selectRoute(route, routeId(index))">
|
||||
<span><strong>Route {{ index + 1 }}</strong><small>{{ selectedRouteId === routeId(index) ? 'Selected' : (index === 0 ? 'Recommended' : 'Alternative') }}</small></span>
|
||||
<strong>{{ formatDistance(route.distance) }} · {{ formatDuration(route.duration) }}</strong>
|
||||
<strong>Route {{ index + 1 }}</strong>
|
||||
<small>{{ formatDistance(route.distance) }} · {{ formatDuration(route.duration) }}</small>
|
||||
</button>
|
||||
</div>
|
||||
<div class="gx-navigation-summary__actions">
|
||||
<button v-if="navigationStarted" type="button" class="gx-btn gx-btn--danger" @click="cancelNavigation"><i class="bi bi-x-lg"></i> Cancel Navigation</button>
|
||||
<button v-else type="button" class="gx-btn gx-btn--success" :disabled="loadingRoute || !hasRoutingKey" :title="hasRoutingKey ? 'Start Navigation' : 'A Mapbox secret key is required to start navigation'" @click="setDestination(destination)"><i class="bi bi-sign-turn-right"></i> {{ loadingRoute ? 'Calculating...' : 'Start Navigation' }}</button>
|
||||
<button type="button" class="gx-btn gx-btn--favorite" :class="{ active: isFavorite }" @click="toggleFavorite"><i class="bi" :class="isFavorite ? 'bi-heart-fill' : 'bi-heart'"></i> {{ isFavorite ? 'Unfavorite' : 'Favorite' }}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NavigationDestinationPanel } from "../components/NavigationDestinationPanel.js?v=nav-destination-4"
|
||||
import { NavigationDestinationPanel } from "../components/NavigationDestinationPanel.js?v=nav-destination-6"
|
||||
import { MapsPanel } from "../components/MapsPanel.js"
|
||||
import { NavigationKeysPanel } from "../components/NavigationKeysPanel.js"
|
||||
import { SpeedLimitsPanel } from "../components/SpeedLimitsPanel.js"
|
||||
@@ -22,10 +22,15 @@ export const Navigation = {
|
||||
})
|
||||
},
|
||||
template: `
|
||||
<div class="gx-view">
|
||||
<template v-if="tab === 'nav'">
|
||||
<div class="gx-navigation-view">
|
||||
<NavigationDestinationPanel />
|
||||
<div class="gx-navigation-tabs"><GalaxyTabs :items="TABS" :active="tab" @select="selectTab" /></div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="gx-view">
|
||||
<h2 style="margin-top:0;">Navigation & Maps</h2>
|
||||
<GalaxyTabs :items="TABS" :active="tab" @select="selectTab" />
|
||||
<template v-if="tab === 'nav'"><NavigationDestinationPanel /></template>
|
||||
<template v-if="tab === 'maps'"><MapsPanel /></template>
|
||||
<template v-if="tab === 'keys'"><NavigationKeysPanel /></template>
|
||||
<template v-if="tab === 'speeds'"><SpeedLimitsPanel /></template>
|
||||
|
||||
@@ -99,6 +99,7 @@ export const Recordings = {
|
||||
if (this.sub === "screen" && !this.recordings.length && !this.screenLoading) this.loadScreenRecordings()
|
||||
},
|
||||
screenDisplayName(rec) {
|
||||
if (!rec) return ""
|
||||
return rec.is_custom_name ? rec.filename.replace(/\.mp4$/i, "").replace(/_/g, " ") : formatScreenDate(rec.timestamp)
|
||||
},
|
||||
async loadRoutes() {
|
||||
|
||||
@@ -502,8 +502,8 @@ def test_ui_all_remaining_classic_tools_native_no_embed():
|
||||
assert "GalaxyEmbed" not in tuning and "LateralTuningPanel" in tuning
|
||||
assert _read("js/components/MapsPanel.js") and _read("js/components/NavigationKeysPanel.js")
|
||||
destination = _read("js/components/NavigationDestinationPanel.js")
|
||||
assert '"./views/Navigation.js?v=nav-destination-4"' in _read("js/app.js")
|
||||
assert '"../components/NavigationDestinationPanel.js?v=nav-destination-4"' in _read("js/views/Navigation.js")
|
||||
assert '"./views/Navigation.js?v=nav-destination-6"' in _read("js/app.js")
|
||||
assert '"../components/NavigationDestinationPanel.js?v=nav-destination-6"' in _read("js/views/Navigation.js")
|
||||
assert "mapboxSuggest" in destination and "mapboxRetrieve" in destination
|
||||
assert "mapboxGeocode" in destination and "mapboxDirections" in destination
|
||||
assert "ref=\"map\"" in destination and "setNavigation(this.destination)" in destination
|
||||
@@ -618,6 +618,7 @@ def test_ui_mobile_polish_regressions():
|
||||
assert 'localDeviceUrl(status?.lanIp, "/recordings")' in recordings
|
||||
assert 'localDeviceUrl(status?.lanIp, "/galaxy")' in galaxy
|
||||
assert "gx-btn gx-btn--tonal" in recordings and "Open Recordings Locally" in recordings
|
||||
assert "screenDisplayName(rec) {\n if (!rec) return" in recordings
|
||||
assert "gx-btn gx-btn--tonal" in galaxy and "Open Galaxy Locally" in galaxy
|
||||
|
||||
home = _read("js/views/Home.js")
|
||||
@@ -856,3 +857,33 @@ console.log("hierarchy logic OK")
|
||||
|
||||
result = subprocess.run([node, str(script)], capture_output=True, text=True)
|
||||
assert result.returncode == 0, f"node failed:\n{result.stdout}\n{result.stderr}"
|
||||
|
||||
|
||||
def test_ui_navigation_map_first_layout_regressions():
|
||||
nav = _read("js/views/Navigation.js")
|
||||
css = _read("css/material.css")
|
||||
destination = _read("js/components/NavigationDestinationPanel.js")
|
||||
|
||||
# Destination tab is a map-first screen: the map fills the content area and
|
||||
# runs behind the top appbar and the tab switcher floats above the bottom nav.
|
||||
assert 'class="gx-navigation-view"' in nav and "gx-navigation-tabs" in nav
|
||||
assert ".gx-content:has(.gx-navigation-view)" in css
|
||||
assert "--bottomnav-clearance:" in css
|
||||
assert "margin-top: calc(-1 * (var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-1)))" in css
|
||||
assert "height: 100dvh" in css
|
||||
assert "padding-top: calc(var(--appbar-height) + env(safe-area-inset-top, 0px) + var(--sp-4))" in css
|
||||
assert ".gx-navigation-tabs {" in css
|
||||
assert "bottom: var(--bottomnav-clearance)" in css
|
||||
assert "padding-bottom: calc(var(--bottomnav-clearance) + 56px)" in css
|
||||
|
||||
# Route metrics use Bootstrap Icons consistently, not emoji.
|
||||
assert "bi-signpost-2" in destination and "bi-clock-history" in destination
|
||||
for emoji in ("🛣️", "⌛", "🕗"):
|
||||
assert emoji not in destination
|
||||
|
||||
# The route summary stays compact so the map remains visible: metrics are a
|
||||
# horizontal strip and route options are compact chips, not full-width rows.
|
||||
assert 'class="gx-navigation-metrics"' in destination
|
||||
assert destination.count('class="gx-navigation-metric"') == 3
|
||||
assert "gx-navigation-summary__rows" not in destination and "gx-navigation-summary__rows" not in css
|
||||
assert "grid-template-columns: repeat(3, minmax(0, 1fr))" in css
|
||||
|
||||
Reference in New Issue
Block a user