Feature: Add Settings module with PanelNavigator, AppSettingsView, and related components for nested navigation + theming
This commit is contained in:
183
finlog_app/app/lib/core/ui/panel.dart
Normal file
183
finlog_app/app/lib/core/ui/panel.dart
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user