Skip to content

Commit acb5bd7

Browse files
Merge pull request #254 from TheAngryRaven/BETA
Release 2.7.1
2 parents 8b8ed6a + 251bfef commit acb5bd7

13 files changed

Lines changed: 78 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
> from git history and grouped by theme rather than exhaustive per-commit
1212
> detail.
1313
14+
## [2.7.1] - 2026-06-19
15+
16+
### Changed
17+
- **Course editor sector toggles.** Once all three major sectors are assigned
18+
(start/finish counts as one), the **Major** toggle is hidden on the remaining
19+
sub-sector rows — the datalogger only ever uses three majors, so there's
20+
nothing to promote them to. Un-flag an existing major and the toggles
21+
immediately reappear.
22+
- **Video panel hints.** The "no video loaded" panel now pads its hint text so
23+
the GoPro tip no longer clips against the panel edges on narrow screens, and
24+
adds a second tip: you can drop in **all** your files at once — logs and
25+
videos together — and the app sorts out which videos go where.
26+
1427
## [2.7.0] - 2026-06-19
1528

1629
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "doves-dataviewer",
33
"private": true,
4-
"version": "2.7.0",
4+
"version": "2.7.1",
55
"description": "Open-source, offline-first motorsport telemetry viewer (Dove's DataViewer / HackTheTrack).",
66
"license": "GPL-3.0-or-later",
77
"author": "TheAngryRaven",

src/components/VideoPlayer.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -570,16 +570,17 @@ export const VideoPlayer = memo(function VideoPlayer({
570570
// No video loaded
571571
if (!state.videoUrl) {
572572
return (
573-
<div className="h-full flex flex-col items-center justify-center bg-muted/20 gap-4">
573+
<div className="h-full flex flex-col items-center justify-center bg-muted/20 gap-4 px-6 text-center">
574574
<Video className="w-12 h-12 text-muted-foreground/50" />
575575
<p className="text-muted-foreground text-sm">{t("player.noVideo")}</p>
576576
{state.videoFileName && (
577-
<p className="text-xs text-muted-foreground">{t("player.lastUsed", { name: state.videoFileName })}</p>
577+
<p className="text-xs text-muted-foreground max-w-xs break-words">{t("player.lastUsed", { name: state.videoFileName })}</p>
578578
)}
579579
<Button variant="outline" size="sm" onClick={actions.loadVideo} className="gap-2">
580580
<Video className="w-4 h-4" /> {t("player.loadVideo")}
581581
</Button>
582-
<p className="text-xs text-muted-foreground/70">{t("player.goproHint")}</p>
582+
<p className="text-xs text-muted-foreground/70 max-w-xs">{t("player.goproHint")}</p>
583+
<p className="text-xs text-muted-foreground/70 max-w-xs">{t("player.bulkSelectHint")}</p>
583584
<RecordingPicker state={state} actions={actions} />
584585
</div>
585586
);

src/components/track-editor/SectorListEditor.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { Switch } from '@/components/ui/switch';
1515
import { cn } from '@/lib/utils';
1616
import { CourseSector, Course } from '@/types/racing';
1717
import {
18-
sectorLabels, validateCourseSectors, isAtSectorLimit, MAX_SECTOR_LINES,
18+
sectorLabels, validateCourseSectors, isAtSectorLimit, isAtMajorLimit, MAX_SECTOR_LINES,
1919
} from '@/lib/courseSectors';
2020
import type { SelectedLine } from '@/hooks/useTrackEditorForm';
2121

@@ -49,6 +49,9 @@ export function SectorListEditor({
4949
const labels = useMemo(() => sectorLabels(course), [course]);
5050
const validation = useMemo(() => validateCourseSectors(course), [course]);
5151
const atLimit = isAtSectorLimit(course);
52+
// Once all three majors are taken, only existing majors keep their toggle (so
53+
// they can be un-flagged); non-major rows hide it since none can be promoted.
54+
const atMajorLimit = isAtMajorLimit(course);
5255

5356
const sensors = useSensors(
5457
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
@@ -124,6 +127,9 @@ export function SectorListEditor({
124127
label={labels[i + 1] ?? ''}
125128
sector={sec}
126129
selected={selectedLine === i}
130+
// Hide the major toggle on non-major rows once the 3-major cap
131+
// is hit; major rows always keep it so they can be un-flagged.
132+
showMajorToggle={sec.major || !atMajorLimit}
127133
onSelect={() => onSelectLine(i)}
128134
onToggleMajor={() => onToggleMajor(i)}
129135
onRemove={() => onRemoveSector(i)}
@@ -158,12 +164,15 @@ interface SectorRowProps {
158164
label: string;
159165
sector: CourseSector;
160166
selected: boolean;
167+
/** Whether the "major" toggle is shown — hidden on non-major rows once the
168+
* 3-major cap is reached (the logger only ever uses three). */
169+
showMajorToggle: boolean;
161170
onSelect: () => void;
162171
onToggleMajor: () => void;
163172
onRemove: () => void;
164173
}
165174

166-
function SectorRow({ index, label, sector, selected, onSelect, onToggleMajor, onRemove }: SectorRowProps) {
175+
function SectorRow({ index, label, sector, selected, showMajorToggle, onSelect, onToggleMajor, onRemove }: SectorRowProps) {
167176
const { t } = useTranslation('tracks');
168177
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: sortableId(index) });
169178
const style = { transform: CSS.Transform.toString(transform), transition };
@@ -193,10 +202,12 @@ function SectorRow({ index, label, sector, selected, onSelect, onToggleMajor, on
193202
<button type="button" onClick={onSelect} className="flex-1 text-left text-sm">
194203
{t('sectors.sectorRow', { label })}
195204
</button>
196-
<label className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-muted-foreground">
197-
{t('sectors.major')}
198-
<Switch checked={sector.major} onCheckedChange={onToggleMajor} aria-label={t('sectors.markMajor')} />
199-
</label>
205+
{showMajorToggle && (
206+
<label className="flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-muted-foreground">
207+
{t('sectors.major')}
208+
<Switch checked={sector.major} onCheckedChange={onToggleMajor} aria-label={t('sectors.markMajor')} />
209+
</label>
210+
)}
200211
<Button
201212
variant="ghost"
202213
size="icon"

src/lib/courseSectors.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
22
import { Course, SectorLine } from '@/types/racing';
33
import {
44
normalizeCourseSectors, majorSectorLines, legacyMirror, sectorLabels,
5-
validateCourseSectors, isAtSectorLimit, rollupMajorSectors, centeredSectorLine,
5+
validateCourseSectors, isAtSectorLimit, isAtMajorLimit, rollupMajorSectors, centeredSectorLine,
66
MAX_SECTOR_LINES, MAX_MAJOR_SECTORS, DEFAULT_SECTOR_HALF_LENGTH_DEG,
77
} from './courseSectors';
88

@@ -120,6 +120,30 @@ describe('isAtSectorLimit', () => {
120120
});
121121
});
122122

123+
describe('isAtMajorLimit', () => {
124+
it('counts start/finish toward the major cap', () => {
125+
// No sub-sectors: start/finish alone is 1 of 3 majors — not at the cap.
126+
expect(isAtMajorLimit(baseCourse())).toBe(false);
127+
expect(isAtMajorLimit(baseCourse({ sectors: [] }))).toBe(false);
128+
});
129+
130+
it('is false while fewer than MAX_MAJOR_SECTORS majors are flagged', () => {
131+
// One flagged major + start/finish = 2 of 3.
132+
const sectors = [{ line: line(0), major: true }, { line: line(1), major: false }];
133+
expect(isAtMajorLimit(baseCourse({ sectors }))).toBe(false);
134+
});
135+
136+
it('is true once start/finish + flagged majors reach the cap', () => {
137+
// Two flagged majors + start/finish = 3 of 3.
138+
const sectors = [
139+
{ line: line(0), major: true },
140+
{ line: line(1), major: false },
141+
{ line: line(2), major: true },
142+
];
143+
expect(isAtMajorLimit(baseCourse({ sectors }))).toBe(true);
144+
});
145+
});
146+
123147
describe('centeredSectorLine', () => {
124148
it('builds a horizontal line centered on the point with the default span', () => {
125149
const l = centeredSectorLine({ lat: 28.41, lon: -81.38 });

src/lib/courseSectors.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,17 @@ export function isAtSectorLimit(course: Course): boolean {
131131
return (course.sectors ?? []).length >= MAX_COURSE_SECTORS;
132132
}
133133

134+
/**
135+
* True once all `MAX_MAJOR_SECTORS` majors are spoken for (start/finish + the
136+
* flagged sub-sectors). The logger only ever sees three majors, so the editor
137+
* hides the "major" toggle on non-major rows at this point; un-flagging an
138+
* existing major drops back below the cap and the toggles return.
139+
*/
140+
export function isAtMajorLimit(course: Course): boolean {
141+
const flaggedMajors = (course.sectors ?? []).filter((s) => s.major).length;
142+
return flaggedMajors + 1 >= MAX_MAJOR_SECTORS; // + start/finish
143+
}
144+
134145
/**
135146
* Roll the fine-grained per-segment times up into the classic S1/S2/S3, where
136147
* each major sector spans from its major line to the next major line. A major

src/locales/de/video.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"lastUsed": "Zuletzt verwendet: {{name}}",
66
"loadVideo": "Video laden",
77
"goproHint": "Mit einer GoPro aufgenommen? Wählen Sie alle Kapiteldateien auf einmal aus – sie werden als ein durchgehendes Video abgespielt.",
8+
"bulkSelectHint": "Tipp: Sie können alle Ihre Dateien auf einmal auswählen – Logs und Videos zusammen – und wir ordnen die Videos automatisch zu.",
89
"chapterShort": "{{current}}/{{total}}",
910
"chapterOf": "Kapitel {{current}} von {{total}}",
1011
"noVideoForPortion": "Kein Video für diesen Abschnitt",

src/locales/en/video.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"lastUsed": "Last used: {{name}}",
55
"loadVideo": "Load Video",
66
"goproHint": "Recorded on a GoPro? Select all the chapter files at once — they'll play as one continuous video.",
7+
"bulkSelectHint": "Tip: you can select all your files at once — logs and videos together — and we'll sort out which videos go where.",
78
"chapterShort": "{{current}}/{{total}}",
89
"chapterOf": "Chapter {{current}} of {{total}}",
910
"noVideoForPortion": "No video for this portion",

src/locales/es/video.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"lastUsed": "Último usado: {{name}}",
66
"loadVideo": "Cargar vídeo",
77
"goproHint": "¿Grabaste con una GoPro? Selecciona todos los archivos de capítulos a la vez: se reproducirán como un único vídeo continuo.",
8+
"bulkSelectHint": "Consejo: puedes seleccionar todos tus archivos a la vez —registros y vídeos juntos— y nosotros nos encargamos de asignar cada vídeo.",
89
"chapterShort": "{{current}}/{{total}}",
910
"chapterOf": "Capítulo {{current}} de {{total}}",
1011
"noVideoForPortion": "No hay vídeo para esta parte",

src/locales/fr/video.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"lastUsed": "Dernière utilisée : {{name}}",
66
"loadVideo": "Charger une vidéo",
77
"goproHint": "Filmé avec une GoPro ? Sélectionnez tous les fichiers de chapitres à la fois : ils seront lus comme une seule vidéo continue.",
8+
"bulkSelectHint": "Astuce : vous pouvez sélectionner tous vos fichiers d'un coup — journaux et vidéos ensemble — et nous nous occupons d'attribuer chaque vidéo.",
89
"chapterShort": "{{current}}/{{total}}",
910
"chapterOf": "Chapitre {{current}} sur {{total}}",
1011
"noVideoForPortion": "Aucune vidéo pour cette portion",

0 commit comments

Comments
 (0)