Feature: Add Settings module with PanelNavigator, AppSettingsView, and related components for nested navigation + theming

This commit is contained in:
2025-09-25 21:21:01 +02:00
parent cfa5ceb393
commit bf5dc6b69c
7 changed files with 786 additions and 1 deletions

View File

@@ -0,0 +1,183 @@
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
/// Ermöglicht verschachtelte "Panels" ähnlich einem StackNavigator.
/// Beispiel:
/// ```dart
/// PanelNavigator(
/// rootBuilder: (_) => MyRootPanel(),
/// )
/// ```
class PanelNavigator extends StatefulWidget {
final WidgetBuilder rootBuilder;
const PanelNavigator({super.key, required this.rootBuilder});
@override
State<PanelNavigator> createState() => _PanelNavigatorState();
/// Zugriff per InheritedWidget / Extension
static PanelController of(BuildContext context) {
final inherited = context
.dependOnInheritedWidgetOfExactType<_PanelInherited>();
assert(inherited != null, 'Kein PanelNavigator im Widget-Tree gefunden');
return inherited!.controller;
}
}
class _PanelNavigatorState extends State<PanelNavigator> {
late final PanelController _controller;
@override
void initState() {
super.initState();
_controller = PanelController(
rootBuilder: widget.rootBuilder,
onUpdate: () => setState(() {}),
);
}
@override
Widget build(BuildContext context) {
return _PanelInherited(
controller: _controller,
child: PageTransitionSwitcher(
duration: const Duration(milliseconds: 300),
reverse: _controller.navDirection == _NavDirection.back,
transitionBuilder: (child, primary, secondary) {
return SharedAxisTransition(
animation: primary,
secondaryAnimation: secondary,
transitionType: SharedAxisTransitionType.horizontal,
fillColor: Colors.transparent,
child: child,
);
},
child: _controller.getCurrentPanel(context),
),
);
}
}
class PanelController {
final WidgetBuilder rootBuilder;
final VoidCallback onUpdate;
final List<_PanelEntry> _stack = [];
_NavDirection _navDirection = _NavDirection.forward;
int _version = 0;
PanelController({required this.rootBuilder, required this.onUpdate}) {
_stack.add(_PanelEntry(title: null, builder: rootBuilder));
}
_NavDirection get navDirection => _navDirection;
Widget getCurrentPanel(BuildContext context) {
final entry = _stack.last;
return KeyedSubtree(
key: ValueKey('panel_v${_version}_${_stack.length}'),
child: _PanelScaffold(
title: entry.title,
body: entry.builder(context),
onBack: canPop ? pop : null,
),
);
}
bool get canPop => _stack.length > 1;
void push(WidgetBuilder builder, {String? title}) {
_navDirection = _NavDirection.forward;
_stack.add(_PanelEntry(title: title, builder: builder));
_version++;
onUpdate();
}
void pop() {
if (canPop) {
_navDirection = _NavDirection.back;
_stack.removeLast();
_version++;
onUpdate();
}
}
}
enum _NavDirection { forward, back }
class _PanelEntry {
final String? title;
final WidgetBuilder builder;
_PanelEntry({this.title, required this.builder});
}
class _PanelScaffold extends StatelessWidget {
final String? title;
final Widget body;
final VoidCallback? onBack;
const _PanelScaffold({required this.title, required this.body, this.onBack});
@override
Widget build(BuildContext context) {
return Column(
children: [
if (title != null) PanelHeader(title: title!, onBack: onBack),
Expanded(
child: Padding(padding: const EdgeInsets.all(15), child: body),
),
],
);
}
}
class _PanelInherited extends InheritedWidget {
final PanelController controller;
const _PanelInherited({required super.child, required this.controller});
@override
bool updateShouldNotify(_PanelInherited oldWidget) =>
controller != oldWidget.controller;
}
/// ---------- PUBLIC UI COMPONENTS ----------
class PanelHeader extends StatelessWidget {
final String title;
final VoidCallback? onBack;
const PanelHeader({super.key, required this.title, this.onBack});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Material(
color: scheme.surface,
elevation: 1,
child: SizedBox(
height: 56,
child: Row(
children: [
if (onBack != null)
IconButton(icon: const Icon(Icons.arrow_back), onPressed: onBack),
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
),
);
}
}
/// ---------- EXTENSIONS ----------
extension PanelContext on BuildContext {
PanelController get panels => PanelNavigator.of(this);
}