first commit for mih package toolkit

This commit is contained in:
2026-03-06 15:38:41 +02:00
parent 3a90e777a8
commit 76999db7c5
168 changed files with 10542 additions and 71 deletions

220
lib/src/mih_package.dart Normal file
View File

@@ -0,0 +1,220 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mih_package_toolkit/src/mih_package_tools.dart';
/// The core container for MIH application modules.
///
/// [MihPackage] acts as a high-level scaffold that manages a multi-page
/// interface. It synchronizes a set of toolbar icons ([packageTools]) with
/// a scrollable/swipeable body ([packageToolBodies]).
///
/// Features:
/// * **PageView Integration**: Seamlessly swipes between different tools.
/// * **Toolbar Sync**: Automatically updates the [MihPackageTools] state
/// when the page changes.
/// * **Double-Back Exit**: Built-in logic to prevent accidental app closure
/// on mobile devices.
/// * **Peak Animation**: A subtle visual hint that more pages exist to the right.
///
/// ### Example:
/// ```dart
/// MihPackage(
/// selectedBodyIndex: 0,
/// packageToolTitles: ['Home', 'Settings'],
/// packageTools: MihPackageTools(...),
/// packageToolBodies: [HomeWidget(), SettingsWidget()],
/// onIndexChange: (index) => print("Navigated to $index"),
/// packageActionButton: FloatingActionButton(...),
/// )
/// ```
class MihPackage extends StatefulWidget {
/// The floating action button or primary action trigger for this package.
final Widget packageActionButton;
/// The toolbar widget containing icons that correspond to the [packageToolBodies] pages.
final MihPackageTools packageTools;
/// The list of main content widgets for each "page" of the package.
final List<Widget> packageToolBodies;
/// The titles displayed in the header for each corresponding page.
final List<String> packageToolTitles;
/// An optional drawer for secondary actions.
final Drawer? actionDrawer;
/// The initial page index to display.
final int selectedBodyIndex;
/// Callback triggered whenever the user swipes or navigates to a new page.
final Function(int) onIndexChange;
const MihPackage({
super.key,
required this.packageActionButton,
required this.packageTools,
required this.packageToolBodies,
required this.packageToolTitles,
this.actionDrawer,
required this.selectedBodyIndex,
required this.onIndexChange,
});
@override
State<MihPackage> createState() => _MihPackageState();
}
class _MihPackageState extends State<MihPackage>
with SingleTickerProviderStateMixin {
/// The current active page index.
late int _currentIndex;
/// Controls the horizontal scrolling between [packageToolBodies] items.
late PageController _pageController;
late AnimationController _animationController;
DateTime? lastPressedAt;
void unfocusAll() {
FocusScope.of(context).unfocus();
}
/// Triggers a "peek" animation on startup to show users that
/// horizontal navigation is available.
Future<void> _peakAnimation() async {
int currentPage = _currentIndex;
double peakOffset = _pageController.position.viewportDimension * 0.075;
double currentOffset =
_pageController.page! * _pageController.position.viewportDimension;
int nextPage = currentPage + 1 < widget.packageToolBodies.length
? currentPage + 1
: currentPage;
if (nextPage != currentPage) {
await Future.delayed(const Duration(milliseconds: 100));
await _pageController.animateTo(
currentOffset + peakOffset,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
// await Future.delayed(const Duration(milliseconds: 100));
await _pageController.animateTo(
currentPage * _pageController.position.viewportDimension,
duration: const Duration(milliseconds: 300),
curve: Curves.easeIn,
);
}
}
@override
void dispose() {
_pageController.dispose();
_animationController.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant MihPackage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.selectedBodyIndex != widget.selectedBodyIndex &&
_currentIndex != widget.selectedBodyIndex) {
_currentIndex = widget.selectedBodyIndex;
_pageController.animateToPage(
widget.selectedBodyIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
@override
void initState() {
super.initState();
_currentIndex = widget.selectedBodyIndex;
_pageController = PageController(initialPage: widget.selectedBodyIndex);
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final ModalRoute? currentRoute = ModalRoute.of(context);
if (currentRoute != null) {
currentRoute.animation?.addStatusListener((status) {
if (status == AnimationStatus.completed && mounted) {
_peakAnimation();
}
});
}
});
}
}
@override
Widget build(BuildContext context) {
Size screenSize = MediaQuery.of(context).size;
return GestureDetector(
onTap: unfocusAll,
child: Scaffold(
drawer: widget.actionDrawer,
body: SafeArea(
bottom: false,
minimum: EdgeInsets.only(bottom: 0),
child: Container(
width: screenSize.width,
height: screenSize.height,
//color: Colors.black,
padding: const EdgeInsets.only(top: 5),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
widget.packageActionButton,
const SizedBox(width: 10),
Expanded(
child: Container(
// alignment: Alignment.center,
// alignment: Alignment.centerRight,
alignment: Alignment.centerLeft,
// color: Colors.black,
child: FittedBox(
child: Text(
widget.packageToolTitles[_currentIndex],
style: const TextStyle(
fontSize: 23,
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(width: 5),
widget.packageTools,
const SizedBox(width: 5),
],
),
const SizedBox(height: 5),
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: widget.packageToolBodies.length,
itemBuilder: (context, index) {
return widget.packageToolBodies[index];
},
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
widget.onIndexChange(index);
},
),
),
],
),
),
),
),
);
}
}