Add top bar for switching between Plan and Fly mode
- TopModeBar widget: persistent pill-shaped header (HTML prototype: #topBarWrap/#editModeBtn/#flyModeBtn) tinted in the active mode's color, with the inactive mode offered as a solid button in the corner to switch into it. - AppShell now provides a single shared Scaffold + Stack, overlaying TopModeBar on top of whichever screen (Plan/Fly) is active, instead of each screen owning its own Scaffold. - Wired the Fly button to AppModeCubit.toFly(), which enforces the Ready-to-Fly gate (doc 4.5). Since upload/verify isn't wired up yet, the gate is never satisfied yet, so tapping Fly now shows a SnackBar instead of throwing an uncaught StateError. - Extended the widget test to cover both the bar's presence and the gate-rejection path (was the source of a real bug: an initial negative-margin Container hack for the "bleed into the corner" look violated Container's margin.isNonNegative assertion and crashed the whole tree - fixed with padding instead). Verified with flutter analyze, flutter test, and a real run on the Pixel_10a emulator (visually matches the prototype screenshots in design/, Fly-tap correctly shows the gate message without crashing or switching mode). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
02dd1bc11a
commit
fc7d014fda
@@ -0,0 +1,116 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../app_mode/app_mode_cubit.dart';
|
||||
import '../dmc_colors.dart';
|
||||
|
||||
/// Persistente Kopfleiste zum Umschalten zwischen Plan- und Fly-Modus
|
||||
/// (HTML-Demonstrator: #topBarWrap/#editModeBtn/#flyModeBtn). Die Leiste
|
||||
/// faerbt sich in der Farbe des aktiven Modus; der jeweils andere Modus wird
|
||||
/// als solider Button am Rand angeboten, um in ihn zu wechseln.
|
||||
class TopModeBar extends StatelessWidget {
|
||||
const TopModeBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isFly = context.select((AppModeCubit c) => c.state.mode == AppMode.fly);
|
||||
final activeTint = isFly ? DmcColors.flyModeRed : DmcColors.editModeGreen;
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.64,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: activeTint.withValues(alpha: 0.55),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(26),
|
||||
bottomRight: Radius.circular(26),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_ModeButton(
|
||||
label: 'Plan',
|
||||
isActive: !isFly,
|
||||
inactiveColor: DmcColors.editModeGreen,
|
||||
cornerRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(26),
|
||||
),
|
||||
onTap: () => context.read<AppModeCubit>().toPlan(),
|
||||
),
|
||||
const Spacer(),
|
||||
_ModeButton(
|
||||
label: 'Fly',
|
||||
isActive: isFly,
|
||||
inactiveColor: DmcColors.flyModeRed,
|
||||
cornerRadius: const BorderRadius.only(
|
||||
bottomRight: Radius.circular(26),
|
||||
),
|
||||
onTap: () => _tryEnterFly(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _tryEnterFly(BuildContext context) {
|
||||
try {
|
||||
context.read<AppModeCubit>().toFly();
|
||||
} on StateError {
|
||||
// Ready-to-Fly-Gate (Doku 2.2/4.5) nicht erfuellt - Upload/Verify-Flow
|
||||
// ist noch nicht verdrahtet, daher aktuell immer dieser Fall.
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Mission muss zuerst hochgeladen und verifiziert werden.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ModeButton extends StatelessWidget {
|
||||
const _ModeButton({
|
||||
required this.label,
|
||||
required this.isActive,
|
||||
required this.inactiveColor,
|
||||
required this.cornerRadius,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool isActive;
|
||||
final Color inactiveColor;
|
||||
final BorderRadius cornerRadius;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: isActive ? null : onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: isActive ? 6 : 11,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? null : inactiveColor.withValues(alpha: 0.85),
|
||||
borderRadius: isActive ? null : cornerRadius,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFF141414),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user