This commit is contained in:
2025-02-27 18:08:08 +02:00
commit 499020323e
48 changed files with 3036 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
library astromic_helpers;
export 'src/form/form_helper.astromic.dart';
export 'src/loading/loading_helper.astromic.dart';
export 'src/sheet/sheet_helper.astromic.dart';
export 'src/presenting/presenting_helper.astromic.dart';

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class VsyncProvider extends StatefulWidget {
//SECTION - Widget Arguments
final Widget child;
//!SECTION
//
const VsyncProvider({
super.key,
required this.child,
});
//--
static VsyncProviderState of(BuildContext context) {
final VsyncProviderState? result = context.findAncestorStateOfType<VsyncProviderState>();
if (result != null) {
return result;
}
throw FlutterError('No VsyncProvider ancestor found in the widget tree!');
}
//--
@override
State<VsyncProvider> createState() => VsyncProviderState();
}
class VsyncProviderState extends State<VsyncProvider> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
//SECTION - Build Return
return widget.child;
//!SECTION
}
}

View File

@@ -0,0 +1,3 @@
export 'package:form_controller/form_controller.dart';
export 'src/controller.dart';
export 'src/form_field.dart';

View File

@@ -0,0 +1,146 @@
//s1 Imports
//s2 Core Package Imports
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter/scheduler.dart';
//s2 1st-party Package Imports
import 'package:form_controller/form_controller.dart';
//s2 3rd-party Package Imports
//s2 Dependancies Imports
//s3 Routes
//s3 Services
//s3 Models & Widgets
import 'enums/enums.exports.dart';
//s1 Exports
/// A specialized form controller to handle form states,
class AstromicFormController extends FormController {
// State Variables
final Map<String, AstromicFieldState> fieldStates = <String, AstromicFieldState>{};
final Map<String, String?> fieldMessages = <String, String?>{};
final Map<String, dynamic> _hostedValues = <String, dynamic>{};
final Map<String, Map<String, String>>? streamedErrorMaps; // fieldId: {errorCode: errorMessage}
final Stream<List<String>>? errorStream;
// State Stream Variables
static final StreamController<(String, AstromicFieldState)> _stateStreamController = StreamController<(String id, AstromicFieldState)>.broadcast();
final Stream<(String id, AstromicFieldState)> stateStream = _stateStreamController.stream;
AstromicFormController({
Map<String, (String initialText, bool initialObscurity)>? extraControllers,
this.streamedErrorMaps,
this.errorStream,
}) : super(controllers: extraControllers) {
// Add states and messages based on initial controller.
_addInitialControllers();
// Listen on the error stream for values and push to the corresponding field state.
if (errorStream != null) {
_handleErrorStream();
}
}
/// Get the field state and message of a specific field using it's ID.
(AstromicFieldState, String? message)? getState(String fieldId) {
if (fieldStates.containsKey(fieldId)) {
return (fieldStates[fieldId]!, fieldMessages[fieldId]);
} else {
return null;
}
}
/// Set the field state and message of a specific field using it's ID.
void setState(String fieldId, AstromicFieldState state, {String? message}) {
if (!fieldStates.containsKey(fieldId)) {
throw FlutterError('Field ID $fieldId does not exist.');
}
fieldStates[fieldId] = state;
fieldMessages[fieldId] = message;
_stateStreamController.add((fieldId, state));
}
/// Reset the state of a specific field using it's ID.
resetState(String fieldId) {
setState(fieldId, AstromicFieldState.idle);
}
/// Set the value of a hosted state variable using it's ID.
void setValue<T>(String id, T data) {
if (_hostedValues.keys.toList().contains(id)) {
_hostedValues[id] = data;
} else {
_hostedValues.addEntries(<MapEntry<String, dynamic>>[MapEntry<String, dynamic>(id, data)]);
}
}
/// Get the value of a hosted state variable using it's ID.
T? getValue<T>(String id) {
if (_hostedValues.keys.toList().contains(id)) {
if (_hostedValues[id] is T) {
return _hostedValues[id];
} else {
throw FlutterError('Value found but is not of the type $T');
}
} else {
return null;
}
}
@override
TextEditingController controller(String id, {String? initialText, bool isObscure = false}) {
if (super.controllers == null || !super.controllers!.keys.toList().contains(id)) {
fieldStates.addEntries(<MapEntry<String, AstromicFieldState>>[MapEntry<String, AstromicFieldState>(id, AstromicFieldState.idle)]);
fieldMessages.addEntries(<MapEntry<String, String?>>[MapEntry<String, String?>(id, null)]);
}
TextEditingController ret = super.controller(id, initialText: initialText, isObscure: isObscure);
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
if (ret.text.isEmpty) {
ret.text = initialText ?? '';
}
});
return ret;
}
@override
Future<void> dispose() async {
_stateStreamController.close();
super.dispose();
}
//SECTION - Helper Methods
_addInitialControllers() {
// Add in the initial field states...
fieldStates.addEntries(super.controllers?.entries.toList().map((MapEntry<String, (String, bool)> e) => MapEntry<String, AstromicFieldState>(
e.key, // controller ID
AstromicFieldState.idle, // Initial state of any new controller is Idle
)) ??
<MapEntry<String, AstromicFieldState>>{});
// Add in the initial field messages...
fieldMessages.addEntries(super.controllers?.entries.toList().map((MapEntry<String, (String, bool)> e) => MapEntry<String, String?>(
e.key, // Controller ID
null, // The initial message it has which is Null
)) ??
<MapEntry<String, String?>>{});
}
_handleErrorStream() {
errorStream!.distinct().listen((List<String> errorCodes) {
if (streamedErrorMaps != null && streamedErrorMaps!.isNotEmpty) {
for (String errorMapId in streamedErrorMaps!.keys.toList()) {
if (super.controllers != null && super.controllers!.containsKey(errorMapId)) {
if (streamedErrorMaps![errorMapId] != null &&
streamedErrorMaps![errorMapId]!.isNotEmpty &&
streamedErrorMaps![errorMapId]!.keys.toList().where((String k) => errorCodes.contains(k)).toList().isNotEmpty) {
for (String eC in streamedErrorMaps![errorMapId]!.keys.toList().where((String k) => errorCodes.contains(k)).toList()) {
String? m = streamedErrorMaps![errorMapId]![eC];
setState(errorMapId, AstromicFieldState.withError, message: m ?? 'Error Message was not set!');
}
}
}
}
}
});
}
//!SECTION
}

View File

@@ -0,0 +1 @@
export 'state.enum.dart';

View File

@@ -0,0 +1,30 @@
enum AstromicFieldState {
idle,
withInfo,
withWarning,
withError,
withSuccess,
}
extension FieldStateExtension on AstromicFieldState {
T valuefyer<T>(
T idle, {
T? withInfo,
T? withWarning,
T? withError,
T? withSuccess,
}) {
switch (this) {
case AstromicFieldState.idle:
return idle;
case AstromicFieldState.withInfo:
return withInfo ?? idle;
case AstromicFieldState.withWarning:
return withWarning ?? idle;
case AstromicFieldState.withError:
return withError ?? idle;
case AstromicFieldState.withSuccess:
return withSuccess ?? idle;
}
}
}

View File

@@ -0,0 +1,315 @@
//s1 Imports
//s2 Core Package Imports
import 'package:flutter/widgets.dart';
//s2 1st-party Package Imports
import 'package:astromic_elements/astromic_elements.dart';
import 'package:form_controller/form_controller.dart';
//s2 3rd-party Package Imports
//s2 Dependancies Imports
//s3 Routes
//s3 Services
//s3 Models & Widgets
import 'controller.dart';
import 'enums/enums.exports.dart';
//s1 Exports
// ignore: must_be_immutable
class AstromicFormField<T> extends StatefulWidget {
//SECTION - Widget Arguments
//S1 -- Shared
final AstromicFormController formController;
final String formID;
final bool? initialObscurity;
//
final AstromicFieldConfiguration? configuration;
final List<FormControllerValidator>? validators;
final bool? resetMessageOnChange;
//
final AstromicFieldStyle Function(bool isEnabled, bool isFocused, AstromicFieldState state)? style;
//
final String? hint;
final Widget? Function(bool isEnabled, bool isFocused, VoidCallback stateSetter, AstromicFieldState state)? prefixWidget;
final Widget? Function(bool isEnabled, bool isFocused, VoidCallback stateSetter, AstromicFieldState state)? suffixWidget;
final Widget? Function(bool isEnabled, bool isFocused, AstromicFieldState state, String? message)? messageBuilder;
//S1 -- Text Specific
String? initialText;
void Function(String v, AstromicFieldState state)? onChanged;
void Function(String v, AstromicFieldState state)? onSubmited;
Iterable<ContextMenuButtonItem>? contextButtons;
//S1 -- Action Specific
(T item, String label)? initialValue;
Future<(T item, String label)?> Function((T item, String label)? currentValue)? onTap;
Future<(T item, String label)?> Function((T item, String label)? currentValue)? onHold;
String Function(String? oldValue, String newValue)? onValueChangedMapper;
//!SECTION
AstromicFormField.text({
required this.formController,
required this.formID,
this.initialObscurity,
this.configuration,
this.validators,
this.resetMessageOnChange,
this.style,
this.hint,
this.prefixWidget,
this.suffixWidget,
this.messageBuilder,
//
this.initialText,
this.onChanged,
this.onSubmited,
this.contextButtons,
});
AstromicFormField.action({
required this.formController,
required this.formID,
this.initialObscurity,
this.configuration,
this.validators,
this.resetMessageOnChange,
this.style,
this.hint,
this.prefixWidget,
this.suffixWidget,
this.messageBuilder,
//
this.initialValue,
this.onTap,
this.onHold,
this.onValueChangedMapper,
});
@override
State<AstromicFormField<T>> createState() => _AstromicFormFieldState<T>();
}
class _AstromicFormFieldState<T> extends State<AstromicFormField<T>> {
//
//SECTION - State Variables
//s1 --State
late AstromicFieldState _currentState;
//s1 --State
//
//s1 --Controllers
late TextEditingController _controller;
//s1 --Controllers
//
//s1 --Constants
//s1 --Constants
//!SECTION
@override
void initState() {
super.initState();
//
//SECTION - State Variables initializations & Listeners
//s1 --State
_currentState = AstromicFieldState.idle;
//s1 --State
//
//s1 --Controllers & Listeners
// Set the textEditingController for this field
_controller = widget.formController.controller(widget.formID, initialText: widget.initialText, isObscure: widget.initialObscurity ?? false);
// Listen to the state stream for updated form states...
widget.formController.stateStream.listen(((String, AstromicFieldState) newState) {
if (mounted && widget.formID == newState.$1) {
setState(() {
_currentState = newState.$2;
});
}
});
// Listen to the error stream for updated errors and set the field's state accordingly...
if (widget.formController.errorStream != null) {
widget.formController.errorStream!.listen((List<String> errorCodes) {
if (widget.formController.streamedErrorMaps != null) {
// fieldId: {errorCode: errorMessage}
for (String cID in widget.formController.streamedErrorMaps!.keys.toList()) {
if (cID == widget.formID && widget.formController.streamedErrorMaps![cID] != null) {
for (MapEntry<String, String> errMapItem in widget.formController.streamedErrorMaps![cID]!.entries) {
if (errorCodes.map((String x) => x.toString()).contains(errMapItem.key.toString())) {
if (mounted) {
setState(() {
_setFieldErrorState(widget.formID, errMapItem.value);
});
}
}
}
}
}
}
//
});
}
//s1 --Controllers & Listeners
//
//s1 --Late & Async Initializers
//s1 --Late & Async Initializers
//!SECTION
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
//
//SECTION - State Variables initializations & Listeners
//s1 --State
//s1 --State
//
//s1 --Controllers & Listeners
//s1 --Controllers & Listeners
//
//!SECTION
}
//SECTION - Dumb Widgets
//!SECTION
//SECTION - Stateless functions
_setFieldErrorState(String id, String? message) {
widget.formController.setState(id, AstromicFieldState.withError, message: message);
}
//!SECTION
//SECTION - Action Callbacks
//!SECTION
@override
Widget build(BuildContext context) {
//SECTION - Build Setup
//s1 --Values
//double w = MediaQuery.of(context).size.width;
//double h = MediaQuery.of(context).size.height;
//s1 --Values
//
//s1 --Contexted Widgets
Widget buildText() {
return AstromicFields.text(
stateKey: widget.formController.fieldStateKey(widget.formID),
controller: _controller,
onChanged: (String s) {
setState(() {
if (widget.resetMessageOnChange ?? false) {
widget.formController.resetState(widget.formID);
}
if (widget.onChanged != null) {
widget.onChanged!(s, _currentState);
}
});
},
onSubmited: (String s) {
if (widget.onSubmited != null) {
widget.onSubmited!(s, _currentState);
}
},
configuration: widget.configuration?.copyWith(
isTextObscured: widget.formController.isObscure(widget.formID),
validator: (widget.validators != null && widget.validators!.isNotEmpty)
? (bool enabled, bool focused, String? s) {
widget.formController.resetState(widget.formID);
//
List<FormControllerValidator> validators = <FormControllerValidator>[];
//
validators.addAll(widget.validators!);
//
Map<FormControllerValidator, bool> checks = <FormControllerValidator, bool>{};
//
for (FormControllerValidator validator in validators) {
bool res = validator.checker(s);
checks.addEntries(<MapEntry<FormControllerValidator, bool>>[MapEntry<FormControllerValidator, bool>(validator, res)]);
}
//
if (checks.containsValue(false)) {
// It has an Error!
_setFieldErrorState(widget.formID, checks.entries.where((MapEntry<FormControllerValidator, bool> e) => e.value == false).toList().first.key.message);
return '';
} else {
// It has no Errors!
return null;
}
}
: null,
),
style: (bool enabled, bool focused) => widget.style != null ? widget.style!(enabled, focused, _currentState) : const AstromicFieldStyle(),
//
contextButtons: widget.contextButtons,
//
hint: widget.hint,
prefixWidget: widget.prefixWidget != null ? (bool enabled, bool focused, void Function() setter) => widget.prefixWidget!(enabled, focused, setter, _currentState) : null,
suffixWidget: widget.suffixWidget != null ? (bool enabled, bool focused, void Function() setter) => widget.suffixWidget!(enabled, focused, setter, _currentState) : null,
messageBuilder:
widget.messageBuilder != null ? (bool enabled, bool focused) => widget.messageBuilder!(enabled, focused, _currentState, widget.formController.getState(widget.formID)?.$2) : null,
);
}
Widget buildAction() {
return AstromicFields.action(
stateKey: widget.formController.fieldStateKey(widget.formID),
controller: _controller,
initialValue: widget.initialValue,
onTap: widget.onTap,
onHold: widget.onHold,
onValueChangedMapper: widget.onValueChangedMapper!,
//
style: (bool enabled) => widget.style != null ? widget.style!(enabled, false, _currentState) : const AstromicFieldStyle(),
configuration: widget.configuration?.copyWith(
isTextObscured: widget.formController.isObscure(widget.formID),
validator: (widget.validators != null && widget.validators!.isNotEmpty)
? (bool enabled, bool focused, String? s) {
widget.formController.resetState(widget.formID);
//
List<FormControllerValidator> validators = <FormControllerValidator>[];
//
validators.addAll(widget.validators!);
//
Map<FormControllerValidator, bool> checks = <FormControllerValidator, bool>{};
//
for (FormControllerValidator validator in validators) {
bool res = validator.checker(s);
checks.addEntries(<MapEntry<FormControllerValidator, bool>>[MapEntry<FormControllerValidator, bool>(validator, res)]);
}
//
if (checks.containsValue(false)) {
// It has an Error!
_setFieldErrorState(widget.formID, checks.entries.where((MapEntry<FormControllerValidator, bool> e) => e.value == false).toList().first.key.message);
return '';
} else {
// It has no Errors!
return null;
}
}
: null,
),
//
hint: widget.hint,
prefixWidget: widget.prefixWidget != null ? (bool enabled, void Function() setter) => widget.prefixWidget!(enabled, false, setter, _currentState) : null,
suffixWidget: widget.suffixWidget != null ? (bool enabled, void Function() setter) => widget.suffixWidget!(enabled, false, setter, _currentState) : null,
messageBuilder: widget.messageBuilder != null ? (bool enabled) => widget.messageBuilder!(enabled, false, _currentState, widget.formController.getState(widget.formID)?.$2) : null,
//
);
}
//s1 --Contexted Widgets
//!SECTION
//SECTION - Build Return
return widget.onValueChangedMapper != null
?
// Is Action Field
buildAction()
:
// Is Text Field
buildText();
//!SECTION
}
@override
void dispose() {
//SECTION - Disposable variables
_controller.dispose();
//!SECTION
super.dispose();
}
}

View File

@@ -0,0 +1,84 @@
//s1 Imports
//s2 Core Package Imports
import 'dart:async';
import 'package:flutter/material.dart';
//s2 1st-party Package Imports
//s2 3rd-party Package Imports
import 'package:loader_overlay/loader_overlay.dart';
//s2 Dependancies Imports
//s3 Routes
//s3 Services
//s3 Models
import 'src/models/models.exports.dart';
//s1 Exports
class AstromicLoadingHelper {
static AstromicLoadingOverlayStyle _style = AstromicLoadingOverlayStyle(loadingWidget: (_) => Container());
/// Initialize the global builder method
static Widget initialize({
required Widget child,
required AstromicLoadingOverlayStyle style,
}) {
_style = style;
return GlobalLoaderOverlay(
child: child,
disableBackButton: !style.canDismess,
closeOnBackButton: style.canDismess,
//
duration: style.inDuration,
reverseDuration: style.outDuration,
switchInCurve: style.inCurve,
switchOutCurve: style.outCurve,
transitionBuilder: (Widget w, Animation<double> a) => style.animationBuilder?.call(w, a),
//
overlayColor: Colors.transparent,
overlayWidgetBuilder: (dynamic progress) => style.loadingWidget(progress),
);
}
/// Get the current visiblity state of the loader.
static bool isShowing(BuildContext context) => context.loaderOverlay.visible;
/// Show the loader with an optional progress parameter.
static Future<void> start(BuildContext context, {dynamic startProgress}) async {
context.loaderOverlay.show(progress: startProgress);
return await Future<dynamic>.delayed(_style.inDuration);
}
/// Hide the loader.
static Future<void> stop(BuildContext context, {dynamic startProgress}) async {
context.loaderOverlay.hide();
return await Future<dynamic>.delayed(_style.outDuration);
}
/// Update the progres in the builder.
static Future<void> updateProgress(BuildContext context, dynamic progress) async {
context.loaderOverlay.progress(progress);
return await Future<dynamic>.delayed(Duration(milliseconds: (_style.inDuration.inMilliseconds / 2).floor()));
}
/// Quickly add in a loading segment for an Async function.
static Future<T?> load<T>(
BuildContext context,
Future<T?> Function(Future<void> Function(dynamic p) updateProgress) process, {
dynamic startingProgress,
Function(dynamic e, StackTrace trace)? onFail,
}) async {
Completer<T> c = Completer<T>();
//
await start(context, startProgress: startingProgress);
try {
T? p = await process((dynamic p) async {
return await updateProgress(context, p);
});
c.complete(p);
} catch (e, trace) {
c.complete(null);
if (onFail != null) onFail(e, trace);
} finally {
await stop(context);
}
return c.future;
}
}

View File

@@ -0,0 +1 @@
export 'style.model.dart';

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
class AstromicLoadingOverlayStyle {
final Widget Function(dynamic progress) loadingWidget;
//
final Color? backgroundColor;
final (int sigmaX, int sigmaY)? backgroundBlur;
//
final bool canDismess;
//
final Duration inDuration;
final Duration outDuration;
final Curve inCurve;
final Curve outCurve;
final Function(Widget child, Animation<double> animation)? animationBuilder;
AstromicLoadingOverlayStyle({
required this.loadingWidget,
this.backgroundColor,
this.backgroundBlur,
this.canDismess = false,
this.inCurve = Curves.easeOut,
this.inDuration = const Duration(milliseconds: 250),
this.outCurve = Curves.easeIn,
this.outDuration = const Duration(milliseconds: 250),
this.animationBuilder,
});
AstromicLoadingOverlayStyle copyWith({
Widget Function(dynamic progress)? loadingWidget,
Color? backgroundColor,
(int sigmaX, int sigmaY)? backgroundBlur,
bool? canDismess,
Duration? inDuration,
Duration? outDuration,
Curve? inCurve,
Curve? outCurve,
Function(Widget child, Animation<double> animation)? animationBuilder,
}) {
return AstromicLoadingOverlayStyle(
loadingWidget: loadingWidget ?? this.loadingWidget,
backgroundColor: backgroundColor ?? this.backgroundColor,
backgroundBlur: backgroundBlur ?? this.backgroundBlur,
canDismess: canDismess ?? this.canDismess,
inDuration: inDuration ?? this.inDuration,
outDuration: outDuration ?? this.outDuration,
inCurve: inCurve ?? this.inCurve,
outCurve: outCurve ?? this.outCurve,
animationBuilder: animationBuilder ?? this.animationBuilder,
);
}
@override
String toString() {
return 'AstromicLoadingOverlayStyle(loadingWidget: $loadingWidget, backgroundColor: $backgroundColor, backgroundBlur: $backgroundBlur, canDismess: $canDismess, inDuration: $inDuration, outDuration: $outDuration, inCurve: $inCurve, outCurve: $outCurve, animationBuilder: $animationBuilder)';
}
}

View File

@@ -0,0 +1 @@
export 'models/models.exports.dart';

View File

@@ -0,0 +1,3 @@
export './src/enums/enums.exports.dart';
export './src/models/models.exports.dart';
export './src/widgets/widgets.exports.dart';

View File

@@ -0,0 +1 @@
export './presenter_state.enum.dart';

View File

@@ -0,0 +1,9 @@
enum AstromicPresenterState {
initialLoad, // The first load in the presenter.
//
loading, // all subsequent loadings to the presenter
loaded, // Loaded the data
//
empty, // Data is empty (for list types)
error, // Errors arrising in the presenter
}

View File

@@ -0,0 +1 @@
export './snapshot_helper.dart';

View File

@@ -0,0 +1,92 @@
import 'package:flutter/widgets.dart';
import '../enums/presenter_state.enum.dart';
import '../models/presenter_configuration.model.dart';
import '../models/presenter_return.model.dart';
class SnapshotHelper {
/// Get the state of the snapshot & it's return model.
static (AstromicPresenterState state, PresenterReturnModel<T>? returnModel) stateGetter<T>(
AsyncSnapshot<T> snapshot, T? lastFetchedData, dynamic lastError, AstromicPresenterConfiguration configurations, int? oldBatchId, int currentBatchId) {
ConnectionState state = snapshot.connectionState;
T? data = snapshot.data;
T? previousData = lastFetchedData;
dynamic error = snapshot.error;
dynamic previousError = lastError;
//
bool hasError = error != null && snapshot.hasError;
bool hasPreviousError = previousError != null;
//
bool hasData = data != null && snapshot.hasData;
bool hasPreviousData = previousData != null;
//
bool isDataEmpty = hasData && ((data is List && data.isEmpty) || (data is Map && data.isEmpty));
bool isPreviousDataEmpty = hasPreviousData && ((previousData is List && previousData.isEmpty) || (previousData is Map && previousData.isEmpty));
//
bool disbaleLoadingWithData = configurations.disableLoadingWithPrevData;
bool disbaleLoadingWithError = configurations.disableLoadingWithPrevError;
//
AstromicPresenterState rState;
PresenterReturnModel<T>? rModel;
if (state == ConnectionState.waiting) {
//S1 -- Is Wating/Loading
if (hasPreviousData) {
//S2 -- I have previous data while it's loading
if (!disbaleLoadingWithData) {
//S3 -- Show the loading state even if i have data!
rState = AstromicPresenterState.loading;
rModel = null;
} else {
//S3 -- hide the loading state when i have previous data!
if (isPreviousDataEmpty) {
//S4 -- Previous data is empty and i'm not showing the loading state, show the last empty state!
rState = AstromicPresenterState.empty;
rModel = PresenterSuccessReturnModel<T>(oldBatchId: oldBatchId, batchId: currentBatchId);
} else {
//S4 -- Previous data is NOT empty and i'm not showing the loading state, show the last data state!
rState = AstromicPresenterState.loaded;
rModel = PresenterSuccessReturnModel<T>(data: previousData, oldBatchId: oldBatchId, batchId: currentBatchId);
}
}
} else {
//S2 -- I DON'T have previous data while it's loading (either first load or had an error.)
if (hasPreviousError) {
//S3 -- The previous snap was errored
if (!disbaleLoadingWithError) {
//S4 -- The previous snap was errored, but i will show loading.
rState = AstromicPresenterState.loading;
rModel = null;
} else {
//S4 -- The previous snap was errored, i will keep it.
rState = AstromicPresenterState.error;
rModel = PresenterFailureReturnModel<T>(error: previousError, oldBatchId: oldBatchId, batchId: currentBatchId);
}
} else {
//S3 -- The previous snap was NOT errored, So it's the initial load!
rState = AstromicPresenterState.initialLoad;
rModel = null;
}
}
} else {
//S1 -- Is NOT Waiting/Loading
if (hasData && !hasError) {
//S2 -- Done loading and i have data!
if (isDataEmpty) {
//S3 -- The loaded data is Empty
rState = AstromicPresenterState.empty;
rModel = PresenterSuccessReturnModel<T>(initialData: previousData, oldBatchId: oldBatchId, batchId: currentBatchId);
} else {
//S3 -- The loaded data is NOT Empty
rState = AstromicPresenterState.loaded;
rModel = PresenterSuccessReturnModel<T>(data: data, initialData: previousData, oldBatchId: oldBatchId, batchId: currentBatchId);
}
} else {
//S2 -- Done loading but there is NO data! (errored)
rState = AstromicPresenterState.error;
rModel = PresenterFailureReturnModel<T>(error: error, initialData: previousData, oldBatchId: oldBatchId, batchId: currentBatchId);
}
}
//
return (rState, rModel);
}
}

View File

@@ -0,0 +1,2 @@
export './presenter_return.model.dart';
export './presenter_configuration.model.dart';

View File

@@ -0,0 +1,26 @@
import 'package:flutter/widgets.dart';
class AstromicPresenterConfiguration {
final Duration? timeoutDuration;
final bool disableLoadingWithPrevData;
final bool disableLoadingWithPrevError;
AstromicPresenterConfiguration({
this.timeoutDuration,
this.disableLoadingWithPrevData = false,
this.disableLoadingWithPrevError = false,
});
AstromicPresenterConfiguration copyWith({
int? initialCycle,
Duration? timeoutDuration,
bool? disableLoadingWithPrevData,
bool? disableLoadingWithPrevError,
VoidCallback? stateSetter,
}) {
return AstromicPresenterConfiguration(
timeoutDuration: timeoutDuration ?? this.timeoutDuration,
disableLoadingWithPrevData: disableLoadingWithPrevData ?? this.disableLoadingWithPrevData,
disableLoadingWithPrevError: disableLoadingWithPrevError ?? this.disableLoadingWithPrevError,
);
}
}

View File

@@ -0,0 +1,22 @@
class PresenterReturnModel<T> {
final int? oldBatchId; // The batch of the old data
final int batchId; // The batch of the new data
final T? initialData; // The old data if exists
final T? data; // New data if exists
final dynamic error; // Errors if exists
PresenterReturnModel({
required this.batchId,
this.oldBatchId,
this.error,
this.data,
this.initialData,
});
}
class PresenterSuccessReturnModel<T> extends PresenterReturnModel<T> {
PresenterSuccessReturnModel({super.initialData, super.data, super.error, required super.batchId, super.oldBatchId});
}
class PresenterFailureReturnModel<T> extends PresenterReturnModel<T> {
PresenterFailureReturnModel({super.initialData, super.data, super.error, required super.batchId, super.oldBatchId});
}

View File

@@ -0,0 +1,179 @@
// ignore_for_file: always_specify_types
//s1 Imports
//s2 Core Package Imports
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
//s2 1st-party Package Imports
//s2 3rd-party Package Imports
//s2 Dependancies Imports
//s3 Routes
//s3 Services
//s3 Models & Widgets
import './presenter_controller.widget.dart';
import '../enums/enums.exports.dart';
import '../models/models.exports.dart';
import '../helpers/helpers.exports.dart';
//s1 Exports
class AstromicFuturePresenter<T> extends StatefulWidget {
//SECTION - Widget Arguments
final AstromicPresenterController futureController;
final String futureId;
//
final Map<AstromicPresenterState, Widget Function(PresenterReturnModel<T?>? r)> stateBuilder;
final AstromicPresenterConfiguration? configuration;
//!SECTION
const AstromicFuturePresenter({
super.key,
required this.futureController,
required this.futureId,
required this.stateBuilder,
this.configuration,
});
@override
State<AstromicFuturePresenter<T>> createState() => _AstromicFuturePresenterState<T>();
}
class _AstromicFuturePresenterState<T> extends State<AstromicFuturePresenter<T>> {
//SECTION - State Variables
//s1 --State
Future<T?>? _future;
late AstromicPresenterConfiguration _configs;
//
T? previousData;
dynamic previousError;
int? previousBatchId;
late int _refreshKey; // Key to force Future uniqueness on refresh
//s1 --State
//!SECTION
@override
void initState() {
super.initState();
//
//SECTION - State Variables initializations & Listeners
//s1 --State
_configs = widget.configuration ?? AstromicPresenterConfiguration();
_refreshKey = 0;
//s1 --State
//
//s1 --Controllers & Listeners
widget.futureController.getRefreshStream(widget.futureId).listen((_) {
_refreshFuture(); // Force future recreation on refresh
});
//s1 --Controllers & Listeners
//
//s1 --Late & Async Initializers
_initializeFuture();
//s1 --Late & Async Initializers
//!SECTION
}
@override
void didUpdateWidget(covariant AstromicFuturePresenter<T> oldWidget) {
super.didUpdateWidget(oldWidget);
//
if (oldWidget.futureId != widget.futureId) {
_initializeFuture();
}
//
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
//
//SECTION - State Variables initializations & Listeners
//s1 --State
//s1 --State
//
//s1 --Controllers & Listeners
//s1 --Controllers & Listeners
//
//!SECTION
}
//SECTION - Dumb Widgets
final Map<AstromicPresenterState, Widget> _defaultstateBuilders = {
AstromicPresenterState.initialLoad: const Center(child: CircularProgressIndicator()),
AstromicPresenterState.loading: const Center(child: CircularProgressIndicator()),
AstromicPresenterState.empty: const Center(child: Text('No data at the moment!')),
AstromicPresenterState.error: const Center(child: Text('an error has occured, check the log!')),
AstromicPresenterState.loaded: const Center(child: Text('I have the data, replace this with a widget!')),
};
//!SECTION
//SECTION - Stateless functions
//S1 -- Set Data
void _setPrevious(T? data, int batchId, dynamic error) async {
//
SchedulerBinding.instance.addPostFrameCallback((_) {
previousData = data ?? previousData;
previousBatchId = data != null ? batchId : previousBatchId;
previousError = error ?? previousError;
});
}
//S1 -- Method to reinitialize or update `_future` with a new instance
void _refreshFuture() {
// Increment the refresh key to ensure a unique future instance
_refreshKey++;
setState(() {
_initializeFuture();
});
}
//S1 -- Method to reinitialize or update `_future` with a new instance
void _initializeFuture() {
_future = widget.futureController.getFuture(widget.futureId)?.then((result) => result) as Future<T?>?;
}
//!SECTION
//SECTION - Action Callbacks
//!SECTION
@override
Widget build(BuildContext context) {
//SECTION - Build Setup
//s1 --Values
//double w = MediaQuery.of(context).size.width;
//double h = MediaQuery.of(context).size.height;
//s1 --Values
//
//s1 --Contexted Widgets
//s1 --Contexted Widgets
//!SECTION
//SECTION - Build Return
return FutureBuilder<T?>(
key: ValueKey(_refreshKey), // This ensures FutureBuilder re-runs
future: _future,
builder: (context, snapshot) {
int? oldBatchId = previousBatchId;
int currentBatchId = snapshot.hashCode;
(AstromicPresenterState state, PresenterReturnModel<T?>? returnModel) ret = SnapshotHelper.stateGetter<T?>(snapshot, previousData, previousError, _configs, oldBatchId, currentBatchId);
AstromicPresenterState returnedState = ret.$1;
PresenterReturnModel<T?>? returnedModel = ret.$2;
Widget defaultBuilder = _defaultstateBuilders[returnedState]!;
if (returnedState == AstromicPresenterState.loaded || returnedState == AstromicPresenterState.empty || returnedState == AstromicPresenterState.error) {
_setPrevious(returnedModel?.data, currentBatchId, returnedModel?.error);
}
return widget.stateBuilder.containsKey(returnedState) ? widget.stateBuilder[returnedState]?.call(returnedModel) ?? defaultBuilder : defaultBuilder;
});
//!SECTION
}
@override
void dispose() async {
super.dispose();
//SECTION - Disposable variables
await widget.futureController.disposeFuture(widget.futureId);
//!SECTION
}
}

View File

@@ -0,0 +1,65 @@
// ignore_for_file: always_specify_types
import 'dart:async';
/// A contrller used to control Futures/Streams to present them effeciantly.
class AstromicPresenterController {
late final Map<String, (Future Function()? fetch, int c)> _futures;
late final Map<String, Stream?> _streams;
late final Map<String, StreamController<void>> _futureRefreshers;
AstromicPresenterController({
Map<String, (Future Function()? fetch, int startCycle)> futures = const {},
Map<String, Stream?> streams = const {},
}) : _futures = futures.map((k, v) => MapEntry(k, (v.$1, v.$2))),
_futureRefreshers = futures.map((k, v) => MapEntry(k, StreamController<void>.broadcast())),
_streams = streams;
/// Get the current cycle of this future ID.
int getFutureCycle(String id) {
assert(_futures.containsKey(id), 'did you add a future with this id?');
return _futures[id]!.$2;
}
/// Get the future using it's ID.
Future<T?>? getFuture<T>(String id) {
assert(_futures.containsKey(id), 'did you add a future with this id?');
return _futures[id]!.$1!() as Future<T?>?;
}
/// Get the stream using it's ID.
Stream<T?>? getStream<T>(String id) {
assert(_streams.containsKey(id), 'did you add a stream with this id?');
return _streams[id]! as Stream<T?>?;
}
/// Get the refresh stream of a future using it's ID.
Stream<void> getRefreshStream<T>(String id) {
assert(_futures.containsKey(id), 'did you add a future with this id?');
return _futureRefreshers[id]!.stream;
}
/// Set the fetching cycle of a future using it's ID.
void setFutureCycle(String id, int cycle) {
assert(_futures.containsKey(id), 'did you add a future with this id?');
_futures[id] = (_futures[id]!.$1, cycle);
}
/// Refresh a future using it's ID.
void refreshFuture(String id) {
assert(_futures.containsKey(id), 'did you add a future with this id?');
_futureRefreshers[id]!.add(null);
}
/// Dispose of a future using it's ID.
Future<void> disposeFuture(String id) async {
assert(_futures.containsKey(id), 'did you add a future with this id?');
return await _futureRefreshers[id]!.close();
}
}

View File

@@ -0,0 +1,166 @@
//s1 Imports
//s2 Core Package Imports
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
//s2 1st-party Package Imports
//s2 3rd-party Package Imports
//s2 Dependancies Imports
//s3 Routes
//s3 Services
//s3 Models
import '../enums/enums.exports.dart';
import '../models/models.exports.dart';
import '../helpers/helpers.exports.dart';
//s3 Widgets
import './presenter_controller.widget.dart';
//s1 Exports
class AstromicStreamPresenter<T> extends StatefulWidget {
//SECTION - Widget Arguments
final AstromicPresenterController controller;
final String streamId;
//
final Map<AstromicPresenterState, Widget Function(PresenterReturnModel<T?>? r)> stateBuilder;
final AstromicPresenterConfiguration? configuration;
//!SECTION
//
const AstromicStreamPresenter({
super.key,
required this.controller,
required this.streamId,
required this.stateBuilder,
this.configuration,
});
@override
State<AstromicStreamPresenter<T>> createState() => _AstromicStreamPresenterState<T>();
}
class _AstromicStreamPresenterState<T> extends State<AstromicStreamPresenter<T>> {
//
//SECTION - State Variables
//s1 --State
Stream<T?>? _stream;
late AstromicPresenterConfiguration _configs;
//
T? previousData;
dynamic previousError;
int? previousBatchId;
//s1 --State
//
//s1 --Controllers
//late AstromicFormController _formController;
//s1 --Controllers
//
//s1 --Constants
//s1 --Constants
//!SECTION
@override
void initState() {
super.initState();
//
//SECTION - State Variables initializations & Listeners
//s1 --State
_configs = widget.configuration ?? AstromicPresenterConfiguration();
//s1 --State
//
//s1 --Controllers & Listeners
//s1 --Controllers & Listeners
//
//s1 --Late & Async Initializers
_initializeStream();
//s1 --Late & Async Initializers
//!SECTION
}
@override
void didUpdateWidget(covariant AstromicStreamPresenter<T> oldWidget) {
super.didUpdateWidget(oldWidget);
//
if (oldWidget.streamId != widget.streamId) {
_initializeStream();
}
//
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
//
//SECTION - State Variables initializations & Listeners
//s1 --State
//s1 --State
//
//s1 --Controllers & Listeners
//s1 --Controllers & Listeners
//
//!SECTION
}
//SECTION - Dumb Widgets
final Map<AstromicPresenterState, Widget> _defaultstateBuilders = <AstromicPresenterState, Widget>{
AstromicPresenterState.initialLoad: const Center(child: CircularProgressIndicator()),
AstromicPresenterState.loading: const Center(child: CircularProgressIndicator()),
AstromicPresenterState.empty: const Center(child: Text('No data at the moment!')),
AstromicPresenterState.error: const Center(child: Text('an error has occured, check the log!')),
AstromicPresenterState.loaded: const Center(child: Text('I have the data, replace this with a widget!')),
};
//!SECTION
//SECTION - Stateless functions
//S1 -- Set Data
void _setPrevious(T? data, int batchId, dynamic error) async {
//
SchedulerBinding.instance.addPostFrameCallback((_) {
previousData = data ?? previousData;
previousBatchId = data != null ? batchId : previousBatchId;
previousError = error ?? previousError;
});
}
//S1 -- Method to reinitialize or update `_future` with a new instance
void _initializeStream() {
_stream = widget.controller.getStream(widget.streamId)?.asyncMap((dynamic result) => result) as Stream<T?>?;
}
//!SECTION
//SECTION - Action Callbacks
//!SECTION
@override
Widget build(BuildContext context) {
//SECTION - Build Setup
//s1 --Values
//double w = MediaQuery.of(context).size.width;
//double h = MediaQuery.of(context).size.height;
//s1 --Values
//
//s1 --Contexted Widgets
//s1 --Contexted Widgets
//!SECTION
//SECTION - Build Return
return StreamBuilder<T?>(
stream: _stream,
builder: (BuildContext context, AsyncSnapshot<T?> snapshot) {
//
int? oldBatchId = previousBatchId;
int currentBatchId = snapshot.hashCode;
//
(AstromicPresenterState state, PresenterReturnModel<T?>? returnModel) ret = SnapshotHelper.stateGetter<T?>(snapshot, previousData, previousError, _configs, oldBatchId, currentBatchId);
AstromicPresenterState returnedState = ret.$1;
PresenterReturnModel<T?>? returnedModel = ret.$2;
Widget defaultBuilder = _defaultstateBuilders[returnedState]!;
//
if (returnedState == AstromicPresenterState.loaded || returnedState == AstromicPresenterState.empty || returnedState == AstromicPresenterState.error) {
_setPrevious(returnedModel?.data, currentBatchId, returnedModel?.error);
}
//
return widget.stateBuilder.containsKey(returnedState) ? widget.stateBuilder[returnedState]?.call(returnedModel) ?? defaultBuilder : defaultBuilder;
//
});
//!SECTION
}
}

View File

@@ -0,0 +1,3 @@
export 'presenter_controller.widget.dart';
export 'future_presenter.widget.dart';
export 'stream_presenter.widget.dart';

View File

@@ -0,0 +1,219 @@
//s1 Imports
//s2 Core Package Imports
import 'package:flutter/material.dart';
//s2 1st-party Package Imports
//s2 3rd-party Package Imports
//s2 Dependancies Imports
//s3 Routes
//s3 Services
import '../../dependencies/vsync-provider/vsync_provider.dart';
//s3 Models
import '../form/form_helper.astromic.dart';
import '../sheet/src/models/models.exports.dart';
import '../sheet/src/enums/enums.exports.dart';
import '../sheet/src/widgets/widgets.exports.dart';
//s1 Exports
export '../sheet/src/models/models.exports.dart';
export '../form/form_helper.astromic.dart' show AstromicFormController;
class AstromicSheetHelper {
static VsyncProviderState? _vsyncState;
/// A wrapper to initialize the vSync plugin for the sheets' animations.
static Widget vSyncBuilder(Widget child) {
return VsyncProvider(child: child);
}
/// An initializer method to initialize the vSync state.
static _initializevSync(BuildContext context) {
_vsyncState = VsyncProvider.of(context);
}
/// Show FLEX sheet, a flexible sheet that hugs the height of it's content.
static Future<T?> flex<T extends Object?>(
BuildContext context, {
//
Widget? headSection,
required Widget contentSection,
Widget? footerSection,
//
AstromicSheetConfiguration? configuration,
AstromicSheetStyle? style,
//
}) async {
if (_vsyncState == null) {
_initializevSync(context);
}
AstromicSheetConfiguration sheetConfigs = configuration ?? const AstromicSheetConfiguration();
AstromicSheetStyle sheetStyle = style ?? const AstromicSheetStyle();
return await BasicSheet.show<T>(
context: context,
vsyncState: _vsyncState!,
//
useRootNavigator: sheetConfigs.useRootNavigator,
enableOutsideInteraction: sheetConfigs.enableOutsideInteraction,
enableSheetInteraction: sheetConfigs.enableSheetInteraction,
//
forwardAnimationDuration: sheetConfigs.forwardAnimationDuration ?? const Duration(milliseconds: 250),
reverseAnimationDuration: sheetConfigs.reverseAnimationDuration ?? const Duration(milliseconds: 250),
animationCurve: sheetConfigs.animationCurve ?? Curves.easeOut,
//
barrierColor: sheetStyle.maskColor ,
radius: sheetStyle.radius,
//
child: BaseSheetWidget<T?>(
sheetConfiguration: sheetConfigs,
sheetStyle: sheetStyle,
//
headSection: headSection,
contentSection: contentSection,
footerSection: footerSection,
),
);
}
/// Show FLEX sheet using a pre-set FlexSheetTemplate.
static Future<T?> flexTemplate<T extends Object?>(BuildContext context, AstromicFlexSheetTemplate<T> template) async => await flex<T>(
context,
headSection: template.headSection,
contentSection: template.contentSection,
footerSection: template.footerSection,
//
configuration: template.configuration,
style: template.style,
);
/// Show Form sheet, a FLEX sheet but with an integrated form controller.
static Future<T?> form<T extends Object?>(
BuildContext context, {
//
Widget Function(AstromicFormController)? headSectionBuilder,
required Widget Function(AstromicFormController) contentSectionBuilder,
Widget Function(AstromicFormController)? footerSectionBuilder,
//
AstromicSheetConfiguration? configuration,
AstromicSheetStyle? style,
}) async {
if (_vsyncState == null) {
_initializevSync(context);
}
AstromicSheetConfiguration sheetConfigs = configuration ?? const AstromicSheetConfiguration();
AstromicSheetStyle sheetStyle = style ?? const AstromicSheetStyle();
return await BasicSheet.show<T>(
context: context,
vsyncState: _vsyncState!,
//
useRootNavigator: sheetConfigs.useRootNavigator,
enableOutsideInteraction: sheetConfigs.enableOutsideInteraction,
enableSheetInteraction: sheetConfigs.enableSheetInteraction,
//
forwardAnimationDuration: sheetConfigs.forwardAnimationDuration ?? const Duration(milliseconds: 250),
reverseAnimationDuration: sheetConfigs.reverseAnimationDuration ?? const Duration(milliseconds: 250),
animationCurve: sheetConfigs.animationCurve ?? Curves.easeOut,
//
barrierColor: sheetStyle.maskColor,
radius: sheetStyle.radius,
//
child: BaseSheetWidget<T?>(
sheetType: SheetType.form,
//
sheetConfiguration: sheetConfigs,
sheetStyle: sheetStyle,
//
headSectionFormBuilder: headSectionBuilder,
contentSectionFormBuilder: contentSectionBuilder,
footerSectionFormBuilder: footerSectionBuilder,
),
);
}
/// Show Form sheet using a pre-set FormSheetTemplate.
static Future<T?> formTemplate<T extends Object?>(BuildContext c, AstromicFormSheetTemplate<T> template) async => await form<T>(
c,
headSectionBuilder: template.headSectionBuilder,
contentSectionBuilder: template.contentSectionBuilder,
footerSectionBuilder: template.footerSectionBuilder,
//
configuration: template.configuration,
style: template.style,
//
);
/// Show Scroller sheet, a sheet that contains a vertically scrollable elements with some tweeks for animation.
static Future<T?> scroller<T extends Object?>(
BuildContext context, {
//
Widget Function(ScrollController)? headSectionBuilder,
required Widget Function(ScrollController, ScrollPhysics) contentSectionBuilder,
Widget Function(ScrollController)? footerSectionBuilder,
//
AstromicSheetConfiguration? configuration,
AstromicSheetStyle? style,
}) async {
if (_vsyncState == null) {
_initializevSync(context);
}
AstromicSheetConfiguration sheetConfigs = configuration ?? const AstromicSheetConfiguration();
AstromicSheetStyle sheetStyle = style ?? const AstromicSheetStyle();
return await ScrollerSheet.show<T>(
context: context,
vsyncState: _vsyncState!,
//
useRootNavigator: sheetConfigs.useRootNavigator,
enableOutsideInteraction: sheetConfigs.enableOutsideInteraction,
enableSheetInteraction: sheetConfigs.enableSheetInteraction,
respectKeyboardInset: sheetConfigs.respectKeyboardInset,
//
forwardAnimationDuration: sheetConfigs.forwardAnimationDuration ?? const Duration(milliseconds: 250),
reverseAnimationDuration: sheetConfigs.reverseAnimationDuration ?? const Duration(milliseconds: 250),
animationCurve: sheetConfigs.animationCurve ?? Curves.easeOut,
dragThreshold: sheetConfigs.dragThreshold ?? 25,
//
physics: sheetConfigs.physics,
stops: sheetConfigs.expandedHeightFactor != null && (sheetConfigs.expandedHeightFactor! > sheetConfigs.initialHeightFactor)
? <double?>[sheetConfigs.initialHeightFactor, sheetConfigs.expandedHeightFactor].map((double? s) {
double r = sheetConfigs.safeAreaAware ? ((s! * MediaQuery.of(context).size.height) - MediaQuery.of(context).viewPadding.top) : (s! * MediaQuery.of(context).size.height);
return r;
}).toList()
: <double>[
sheetConfigs.initialHeightFactor,
].map((double s) {
double r = sheetConfigs.safeAreaAware ? ((s * MediaQuery.of(context).size.height) - MediaQuery.of(context).viewPadding.top) : (s * MediaQuery.of(context).size.height);
return r;
}).toList(),
// s2 -- Styling
barrierColor: sheetStyle.maskColor,
topInset: sheetStyle.topInset,
// s2 -- Child
builder: (BuildContext context, ScrollController scrollController, ScrollPhysics scrollPhysics, int stop) {
return BaseSheetWidget<T?>(
sheetType: SheetType.scroller,
sheetConfiguration: sheetConfigs,
sheetStyle: sheetStyle,
headSectionScrollerBuilder: headSectionBuilder,
contentSectionScrollBuilder: contentSectionBuilder,
footerSectionScrollerBuilder: footerSectionBuilder,
scrollController: scrollController,
scrollPhysics: scrollPhysics,
);
},
);
}
/// Show Scroller sheet using a pre-set ScrollerSheetTemplate.
static Future<T?> scrollerTemplate<T extends Object?>(BuildContext context, AstromicScrollerSheetTemplate<T> template) async => await scroller<T>(
context,
headSectionBuilder: template.headSectionBuilder,
contentSectionBuilder: template.contentSectionBuilder,
footerSectionBuilder: template.footerSectionBuilder,
//
configuration: template.configuration,
style: template.style,
);
}

View File

@@ -0,0 +1 @@
export 'type.enum.dart';

View File

@@ -0,0 +1,5 @@
enum SheetType {
flex,
form,
scroller,
}

View File

@@ -0,0 +1,45 @@
//s1 Imports
//s2 Core Package Imports
import 'package:flutter/widgets.dart';
//s2 1st-party Package Imports
//s2 3rd-party Package Imports
//s2 Dependancies Imports
//s3 Routes
//s3 Services
//s3 Models & Widgets
import 'template_base.dart';
import '../models.exports.dart';
//s1 Exports
class AstromicFlexSheetTemplate<T> extends AstromicSheetTemplate<T> {
final Widget? headSection;
final Widget contentSection;
final Widget? footerSection;
//
final AstromicSheetConfiguration? configuration;
final AstromicSheetStyle? style;
AstromicFlexSheetTemplate({
this.headSection,
required this.contentSection,
this.footerSection,
this.configuration,
this.style,
});
AstromicFlexSheetTemplate<T> copyWith({
Widget? headSection,
Widget? contentSection,
Widget? footerSection,
AstromicSheetConfiguration? configuration,
AstromicSheetStyle? style,
}) {
return AstromicFlexSheetTemplate<T>(
headSection: headSection ?? this.headSection,
contentSection: contentSection ?? this.contentSection,
footerSection: footerSection ?? this.footerSection,
configuration: configuration ?? this.configuration,
style: style ?? this.style,
);
}
}

View File

@@ -0,0 +1,46 @@
//s1 Imports
//s2 Core Package Imports
import 'package:flutter/widgets.dart';
//s2 1st-party Package Imports
//s2 3rd-party Package Imports
//s2 Dependancies Imports
//s3 Routes
//s3 Services
//s3 Models
import 'template_base.dart';
import '../models.exports.dart';
import '../../../../form/form_helper.astromic.dart';
//s1 Exports
class AstromicFormSheetTemplate<T> extends AstromicSheetTemplate<T> {
final Widget Function(AstromicFormController)? headSectionBuilder;
final Widget Function(AstromicFormController) contentSectionBuilder;
final Widget Function(AstromicFormController)? footerSectionBuilder;
//
final AstromicSheetConfiguration? configuration;
final AstromicSheetStyle? style;
AstromicFormSheetTemplate({
this.headSectionBuilder,
required this.contentSectionBuilder,
this.footerSectionBuilder,
this.configuration,
this.style,
});
AstromicFormSheetTemplate<T> copyWith({
Widget Function(AstromicFormController)? headSectionBuilder,
Widget Function(AstromicFormController)? contentSectionBuilder,
Widget Function(AstromicFormController)? footerSectionBuilder,
AstromicSheetConfiguration? configuration,
AstromicSheetStyle? style,
}) {
return AstromicFormSheetTemplate<T>(
headSectionBuilder: headSectionBuilder ?? this.headSectionBuilder,
contentSectionBuilder: contentSectionBuilder ?? this.contentSectionBuilder,
footerSectionBuilder: footerSectionBuilder ?? this.footerSectionBuilder,
configuration: configuration ?? this.configuration,
style: style ?? this.style,
);
}
}

View File

@@ -0,0 +1,45 @@
//s1 Imports
//s2 Core Package Imports
import 'package:flutter/widgets.dart';
//s2 1st-party Package Imports
//s2 3rd-party Package Imports
//s2 Dependancies Imports
//s3 Routes
//s3 Services
//s3 Models
import 'template_base.dart';
import '../models.exports.dart';
//s1 Exports
class AstromicScrollerSheetTemplate<T> extends AstromicSheetTemplate<T> {
final Widget Function(ScrollController)? headSectionBuilder;
final Widget Function(ScrollController, ScrollPhysics) contentSectionBuilder;
final Widget Function(ScrollController)? footerSectionBuilder;
//
final AstromicSheetConfiguration? configuration;
final AstromicSheetStyle? style;
AstromicScrollerSheetTemplate({
this.headSectionBuilder,
required this.contentSectionBuilder,
this.footerSectionBuilder,
this.configuration,
this.style,
});
AstromicScrollerSheetTemplate<T> copyWith({
Widget Function(ScrollController)? headSection,
Widget Function(ScrollController, ScrollPhysics)? contentSectionBuilder,
Widget Function(ScrollController)? footerSection,
AstromicSheetConfiguration? configuration,
AstromicSheetStyle? style,
}) {
return AstromicScrollerSheetTemplate<T>(
headSectionBuilder: headSection ?? this.headSectionBuilder,
contentSectionBuilder: contentSectionBuilder ?? this.contentSectionBuilder,
footerSectionBuilder: footerSection ?? this.footerSectionBuilder,
configuration: configuration ?? this.configuration,
style: style ?? this.style,
);
}
}

View File

@@ -0,0 +1 @@
abstract class AstromicSheetTemplate<T> {}

View File

@@ -0,0 +1,3 @@
export 'flex_sheet.template.dart';
export 'form_sheet.template.dart';
export 'scroller_sheet.template.dart';

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
class AstromicSheetConfiguration {
//s1 -- Shared
final bool useRootNavigator;
//
final Duration? closeDelay;
final Duration? forwardAnimationDuration;
final Duration? reverseAnimationDuration;
final Curve? animationCurve;
//
final bool enableOutsideInteraction;
final bool enableSheetInteraction;
final bool dismissOnBack;
//s1 -- Scroller Specific
final double initialHeightFactor;
final double? expandedHeightFactor;
final bool respectKeyboardInset;
final bool safeAreaAware;
final ScrollPhysics? physics;
final double? dragThreshold;
const AstromicSheetConfiguration({
this.useRootNavigator = true,
this.closeDelay,
this.forwardAnimationDuration = const Duration(milliseconds: 300),
this.reverseAnimationDuration = const Duration(milliseconds: 300),
this.animationCurve = Curves.ease,
this.enableOutsideInteraction = true,
this.enableSheetInteraction = true,
this.dismissOnBack = false,
this.respectKeyboardInset = true,
this.safeAreaAware = false,
this.expandedHeightFactor,
this.initialHeightFactor = 0.5,
this.physics,
this.dragThreshold,
});
AstromicSheetConfiguration copyWith({
bool? useRootNavigator,
Duration? closeDelay,
Duration? forwardAnimationDuration,
Duration? reverseAnimationDuration,
Curve? animationCurve,
bool? enableOutsideInteraction,
bool? enableSheetInteraction,
bool? dismissOnBack,
double? initialHeightFactor,
double? expandedHeightFactor,
bool? respectKeyboardInset,
bool? safeAreaAware,
ScrollPhysics? physics,
double? dragThreshold,
}) {
return AstromicSheetConfiguration(
useRootNavigator: useRootNavigator ?? this.useRootNavigator,
closeDelay: closeDelay ?? this.closeDelay,
forwardAnimationDuration: forwardAnimationDuration ?? this.forwardAnimationDuration,
reverseAnimationDuration: reverseAnimationDuration ?? this.reverseAnimationDuration,
animationCurve: animationCurve ?? this.animationCurve,
enableOutsideInteraction: enableOutsideInteraction ?? this.enableOutsideInteraction,
enableSheetInteraction: enableSheetInteraction ?? this.enableSheetInteraction,
dismissOnBack: dismissOnBack ?? this.dismissOnBack,
initialHeightFactor: initialHeightFactor ?? this.initialHeightFactor,
expandedHeightFactor: expandedHeightFactor ?? this.expandedHeightFactor,
respectKeyboardInset: respectKeyboardInset ?? this.respectKeyboardInset,
safeAreaAware: safeAreaAware ?? this.safeAreaAware,
physics: physics ?? this.physics,
dragThreshold: dragThreshold ?? this.dragThreshold,
);
}
}

View File

@@ -0,0 +1,3 @@
export 'configuration.model.dart';
export 'style.model.dart';
export 'templates/templates.exports.dart';

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
class AstromicSheetStyle {
final Color maskColor;
final Color headSectionColor;
final Color contentSectionColor;
final Color footerSectionColor;
//
final EdgeInsets headSectionPadding;
final EdgeInsets contentSectionPadding;
final EdgeInsets footerSectionPadding;
//
final double radius;
final double topInset;
//
const AstromicSheetStyle({
this.maskColor = Colors.black38,
this.headSectionColor = Colors.white,
this.contentSectionColor = Colors.white,
this.footerSectionColor = Colors.white,
this.headSectionPadding = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
this.contentSectionPadding = const EdgeInsets.symmetric(horizontal: 16.0, vertical: 18.0),
this.footerSectionPadding = const EdgeInsets.symmetric(horizontal: 16.0, vertical: 18.0),
this.radius = 24,
this.topInset = 0,
});
AstromicSheetStyle copyWith({
Color? maskColor,
Color? headSectionColor,
Color? contentSectionColor,
Color? footerSectionColor,
EdgeInsets? headSectionPadding,
EdgeInsets? contentSectionPadding,
EdgeInsets? footerSectionPadding,
double? radius,
double? topInset,
}) {
return AstromicSheetStyle(
maskColor: maskColor ?? this.maskColor,
headSectionColor: headSectionColor ?? this.headSectionColor,
contentSectionColor: contentSectionColor ?? this.contentSectionColor,
footerSectionColor: footerSectionColor ?? this.footerSectionColor,
headSectionPadding: headSectionPadding ?? this.headSectionPadding,
contentSectionPadding: contentSectionPadding ?? this.contentSectionPadding,
footerSectionPadding: footerSectionPadding ?? this.footerSectionPadding,
radius: radius ?? this.radius,
topInset: topInset ?? this.topInset,
);
}
}

View File

@@ -0,0 +1,263 @@
//s1 Imports
//s2 Core Package Imports
import 'package:flutter/material.dart';
//s2 1st-party Package Imports
//s2 3rd-party Package Imports
//s2 Dependancies Imports
//s3 Routes
//s3 Services
//s3 Models & Widgets
import '../../../form/form_helper.astromic.dart';
import '../enums/enums.exports.dart';
import '../models/models.exports.dart';
//s1 Exports
class BaseSheetWidget<T> extends StatefulWidget {
//SECTION - Widget Arguments
final SheetType sheetType;
//
final AstromicSheetConfiguration? sheetConfiguration;
final AstromicSheetStyle? sheetStyle;
//
final Widget? headSection;
final Widget? contentSection;
final Widget? footerSection;
//
final Widget Function(AstromicFormController)? headSectionFormBuilder;
final Widget Function(AstromicFormController)? contentSectionFormBuilder;
final Widget Function(AstromicFormController)? footerSectionFormBuilder;
//
final ScrollController? scrollController;
final ScrollPhysics? scrollPhysics;
final Widget Function(ScrollController)? headSectionScrollerBuilder;
final Widget Function(ScrollController, ScrollPhysics)? contentSectionScrollBuilder;
final Widget Function(ScrollController)? footerSectionScrollerBuilder;
//!SECTION
const BaseSheetWidget({
super.key,
this.sheetType = SheetType.flex,
this.sheetConfiguration,
this.sheetStyle,
this.headSection,
this.contentSection,
this.footerSection,
this.contentSectionFormBuilder,
this.footerSectionFormBuilder,
this.headSectionFormBuilder,
this.scrollController,
this.scrollPhysics,
this.headSectionScrollerBuilder,
this.contentSectionScrollBuilder,
this.footerSectionScrollerBuilder,
//
}) : assert(sheetType != SheetType.flex || (sheetType == SheetType.flex && contentSection != null)),
assert(sheetType != SheetType.form || (sheetType == SheetType.form && contentSectionFormBuilder != null)),
assert(sheetType != SheetType.scroller || (sheetType == SheetType.scroller && contentSectionScrollBuilder != null && scrollController != null && scrollPhysics != null));
@override
State<BaseSheetWidget<T>> createState() => _BaseSheetWidgetState<T>();
}
class _BaseSheetWidgetState<T> extends State<BaseSheetWidget<T>> {
//
//SECTION - State Variables
//s1 --Controllers
late AstromicFormController _formController;
late AstromicSheetConfiguration _config;
late AstromicSheetStyle _style;
//s1 --Controllers
//
//s1 --State
//s1 --State
//
//s1 --Constants
//s1 --Constants
//!SECTION
@override
void initState() {
super.initState();
//
//SECTION - State Variables initializations & Listeners
//s1 --Controllers & Listeners
_formController = AstromicFormController();
//s1 --Controllers & Listeners
//
//s1 --State
//s1 --State
//!SECTION
}
//SECTION - Stateless functions
//!SECTION
//SECTION - Action Callbacks
//!SECTION
@override
Widget build(BuildContext context) {
//SECTION - Build Setup
//s1 -Values
_config = widget.sheetConfiguration ?? const AstromicSheetConfiguration();
_style = widget.sheetStyle ?? const AstromicSheetStyle();
//s1 -Values
//s1 -Widgets
List<Widget> flexChildren = <Widget>[
widget.headSection != null
? ClipRRect(
borderRadius: BorderRadius.vertical(
top: Radius.circular(_style.radius),
),
child: Container(
padding: _style.headSectionPadding,
child: widget.headSection,
),
)
: Container(),
Container(
color: _style.contentSectionColor,
padding: _style.contentSectionPadding,
child: widget.contentSection != null ? widget.contentSection! : Container(),
),
widget.footerSection != null
? Container(
color: _style.footerSectionColor,
padding: _style.footerSectionPadding,
child: widget.footerSection,
)
: Container()
];
List<Widget> formChildren = <Widget>[
widget.headSectionFormBuilder != null
? ClipRRect(
borderRadius: BorderRadius.vertical(
top: Radius.circular(_style.radius),
),
child: Container(
padding: _style.headSectionPadding,
child: widget.headSectionFormBuilder!(_formController),
),
)
: Container(),
Container(
color: _style.contentSectionColor,
padding: _style.contentSectionPadding,
child: widget.contentSectionFormBuilder != null ? widget.contentSectionFormBuilder!(_formController) : Container(),
),
widget.footerSectionFormBuilder != null
? Container(
color: _style.footerSectionColor,
padding: _style.footerSectionPadding,
child: widget.footerSectionFormBuilder != null ? widget.footerSectionFormBuilder!(_formController) : Container(),
)
: Container()
];
List<Widget> scrollerChildren = <Widget>[
widget.headSectionScrollerBuilder != null
? ClipRRect(
borderRadius: BorderRadius.vertical(
top: Radius.circular(_style.radius),
),
child: Container(
decoration: BoxDecoration(
color: _style.headSectionColor,
borderRadius: BorderRadius.vertical(
top: Radius.circular(_style.radius),
),
),
padding: _style.headSectionPadding,
width: double.infinity,
child: widget.headSectionScrollerBuilder!(widget.scrollController!),
),
)
: Container(),
Expanded(
child: Container(
padding: _style.contentSectionPadding,
width: double.infinity,
color: _style.contentSectionColor,
child: widget.contentSectionScrollBuilder != null ? widget.contentSectionScrollBuilder!(widget.scrollController!, widget.scrollPhysics!) : Container(),
),
),
widget.footerSectionScrollerBuilder != null
? Container(
color: _style.footerSectionColor,
padding: _style.footerSectionPadding,
child: widget.footerSectionScrollerBuilder!(widget.scrollController!),
)
: Container()
];
//s1 -Widgets
//!SECTION
//SECTION - Build Return
return PopScope(
canPop: _config.dismissOnBack || _config.enableOutsideInteraction || _config.enableSheetInteraction,
child: Container(
child:
//FLEX
widget.sheetType == SheetType.flex
? Container(
padding: EdgeInsets.only(bottom: _config.respectKeyboardInset ? MediaQuery.of(context).viewInsets.bottom : 0),
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: _style.headSectionColor,
borderRadius: BorderRadius.vertical(
top: Radius.circular(_style.radius),
),
),
child: Column(
children: flexChildren,
),
),
)
:
//FORM
widget.sheetType == SheetType.form
? Container(
padding: EdgeInsets.only(bottom: _config.respectKeyboardInset ? MediaQuery.of(context).viewInsets.bottom : 0),
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: _style.headSectionColor,
borderRadius: BorderRadius.vertical(
top: Radius.circular(_style.radius),
),
),
child: Form(
key: _formController.key,
child: Column(
children: formChildren,
),
),
))
:
//Scroller
widget.sheetType == SheetType.scroller
? ClipRRect(
borderRadius: BorderRadius.vertical(
top: Radius.circular(_style.radius),
),
child: Column(
children: scrollerChildren,
),
)
: Container(),
),
);
//!SECTION
}
@override
void dispose() {
//SECTION - Disposable variables
_formController.dispose();
//!SECTION
super.dispose();
}
}

View File

@@ -0,0 +1,63 @@
//s1 Imports
//s2 Core Package Imports
import 'package:flutter/material.dart';
//s2 1st-party Package Imports
//s2 3rd-party Package Imports
//s2 Dependancies Imports
//s3 Routes
//s3 Services
import '../../../../dependencies/vsync-provider/vsync_provider.dart';
//s3 Models
//s1 Exports
@immutable
class BasicSheet {
//
static Future<T?> show<T extends Object?>({
required BuildContext context,
required VsyncProviderState vsyncState,
required Widget child,
//
bool useRootNavigator = false,
bool enableSheetInteraction = true,
bool enableOutsideInteraction = true,
//
Color barrierColor = const Color(0xb3212121),
double radius = 24,
//
Duration forwardAnimationDuration = const Duration(milliseconds: 250),
Duration reverseAnimationDuration = const Duration(milliseconds: 250),
Curve? animationCurve,
//
}) async {
AnimationController controller = AnimationController(
vsync: vsyncState,
);
// Animation duration for displaying the BottomSheet
controller.duration = forwardAnimationDuration;
// Animation duration for retracting the BottomSheet
controller.reverseDuration = reverseAnimationDuration;
// Set animation curve duration for the BottomSheet
controller.drive(CurveTween(curve: animationCurve ?? Curves.easeOut));
return await showModalBottomSheet<T?>(
backgroundColor: Colors.transparent,
barrierColor: barrierColor,
//
enableDrag: enableSheetInteraction,
isDismissible: enableOutsideInteraction,
useRootNavigator: useRootNavigator,
//
isScrollControlled: true,
transitionAnimationController: controller,
//
context: context,
builder: (BuildContext c) {
return Wrap(children: <Widget>[ClipRRect(borderRadius: BorderRadius.vertical(top: Radius.circular(radius)), child: child)]);
},
);
}
}

View File

@@ -0,0 +1,286 @@
//s1 Imports
//s2 Core Package Imports
import 'dart:math';
import 'package:flutter/material.dart';
//s2 1st-party Package Imports
//s2 3rd-party Package Imports
//s2 Dependancies Imports
//s3 Routes
//s3 Services
import '../../../../dependencies/vsync-provider/vsync_provider.dart';
//s3 Models
//s1 Exports
typedef StopperBuilder = Widget Function(
BuildContext context,
ScrollController controller,
ScrollPhysics physics,
int stop,
);
class ScrollerSheet<T extends Object?> extends StatefulWidget {
//
final List<double>? stops;
final StopperBuilder? builder;
final int initialStop;
final double dragThreshold;
final Color? barrierColor;
final double? topInset;
//
final ScrollPhysics? physics;
final bool? enableSheetInteraction;
//
ScrollerSheet({
super.key,
required this.builder,
required this.stops,
this.initialStop = 0,
this.barrierColor = Colors.black38,
this.dragThreshold = 25,
this.topInset,
this.physics,
this.enableSheetInteraction = true,
//
}) : assert(initialStop < stops!.length);
@override
ScrollerSheetState<T> createState() => ScrollerSheetState<T>();
static Future<T?> show<T extends Object?>({
Key? key,
required BuildContext context,
required VsyncProviderState vsyncState,
required StopperBuilder? builder,
//
bool useRootNavigator = true,
bool enableSheetInteraction = true,
bool enableOutsideInteraction = true,
bool respectKeyboardInset = true,
ScrollPhysics? physics = const BouncingScrollPhysics(),
//
required List<double>? stops,
int initialStop = 0,
double dragThreshold = 25,
//
Duration? forwardAnimationDuration = const Duration(milliseconds: 250),
Duration? reverseAnimationDuration = const Duration(milliseconds: 250),
Curve? animationCurve,
//
Color barrierColor = const Color(0xb3212121),
double? topInset,
}) async {
Future<T?>? ret;
AnimationController controller = AnimationController(
vsync: vsyncState,
);
controller.duration = forwardAnimationDuration;
controller.reverseDuration = reverseAnimationDuration;
controller.drive(CurveTween(curve: animationCurve ?? Curves.easeOut));
ret = showModalBottomSheet<T?>(
backgroundColor: Colors.transparent,
barrierColor: barrierColor,
//
enableDrag: enableSheetInteraction,
isDismissible: enableOutsideInteraction,
//
isScrollControlled: true,
useRootNavigator: useRootNavigator,
transitionAnimationController: controller,
//
context: context,
builder: (BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: respectKeyboardInset ? MediaQuery.of(context).viewInsets.bottom : 0,
),
child: ScrollerSheet<T>(
key: key,
builder: builder,
stops: stops,
initialStop: initialStop,
dragThreshold: dragThreshold,
physics: physics,
topInset: topInset,
enableSheetInteraction: enableSheetInteraction,
),
);
},
);
return ret;
}
}
class ScrollerSheetState<T> extends State<ScrollerSheet<T>> with SingleTickerProviderStateMixin {
List<double>? _stops;
int? _currentStop;
int? _targetStop;
bool _dragging = false;
bool _closing = false;
double? _dragOffset;
double? _closingHeight;
ScrollController? _scrollController;
ScrollPhysics? _scrollPhysics;
Animation<double>? _animation;
AnimationController? _animationController;
Tween<double>? _tween;
ScrollPhysics _getScrollPhysicsForStop(dynamic s) {
if (s == _stops!.length - 1) {
return widget.physics ?? const BouncingScrollPhysics();
} else {
return const NeverScrollableScrollPhysics();
}
}
@override
void initState() {
super.initState();
_stops = widget.stops;
_currentStop = widget.initialStop;
_targetStop = _currentStop;
_scrollController = ScrollController();
_scrollPhysics = _getScrollPhysicsForStop(_currentStop);
_animationController = AnimationController(vsync: this, duration: const Duration(seconds: 2));
final CurvedAnimation curveAnimation = CurvedAnimation(parent: _animationController!, curve: Curves.linear);
_tween = Tween<double>(begin: _stops?[_currentStop!], end: _stops?[_targetStop!]);
_animation = _tween?.animate(curveAnimation);
_scrollController?.addListener(() {
if (_scrollController!.offset < -widget.dragThreshold) {
if (_currentStop != _targetStop || _dragging) return;
if (_currentStop! > 0) {
double h0 = height;
_targetStop = _currentStop! - 1;
_animate(h0, _stops![_targetStop!]);
} else if (!_closing) {
close();
}
}
});
}
@override
void didUpdateWidget(ScrollerSheet<T> oldWidget) {
super.didUpdateWidget(oldWidget);
_stops = widget.stops;
_currentStop = min(_currentStop!, _stops!.length - 1);
_targetStop = min(_currentStop!, _stops!.length - 1);
}
@override
void dispose() {
super.dispose();
_animationController!.dispose();
_scrollController!.dispose();
}
get stop => _currentStop;
set stop(dynamic nextStop) {
_targetStop = max(0, min(_stops!.length - 1, nextStop));
_animate(height, nextStop);
}
bool get canClose {
return widget.enableSheetInteraction!;
}
void close({T? result}) {
if (!_closing && canClose) {
_closingHeight = height;
_animationController!.stop();
_dragging = false;
_closing = true;
Navigator.pop(context);
}
}
void _animate(double from, double to) {
_tween!.begin = from;
_tween!.end = to;
_animationController!.value = 0;
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) {
if (_scrollController != null && _scrollController!.hasClients) {
if (_scrollController!.offset < 0) {
_scrollController!.animateTo(0, duration: const Duration(seconds: 200), curve: Curves.linear);
}
}
});
_animationController!.fling().then((_) {
_currentStop = _targetStop;
setState(() {
_scrollPhysics = _getScrollPhysicsForStop(_currentStop);
});
});
}
get height {
if (_closing) {
return _closingHeight;
} else if (_dragging) {
return _stops![_currentStop!] + _dragOffset!;
} else if (_animationController!.isAnimating) {
return _animation!.value;
} else {
return _stops![_currentStop!];
}
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(
top: widget.topInset ?? 0,
),
child: AnimatedBuilder(
animation: _animation!,
child: GestureDetector(
onVerticalDragStart: (DragStartDetails details) {
if (_currentStop != _targetStop) return;
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) {
if (_scrollController != null && _scrollController!.hasClients) {
_scrollController!.jumpTo(0);
}
});
_dragging = true;
_dragOffset = 0;
setState(() {});
},
onVerticalDragUpdate: (DragUpdateDetails details) {
if (_dragging) {
WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) {
if (_scrollController != null && _scrollController!.hasClients) {
_scrollController!.jumpTo(0);
}
});
_dragOffset = _dragOffset! - details.delta.dy;
setState(() {});
}
},
onVerticalDragEnd: (DragEndDetails details) {
if (!_dragging || _closing) return;
if (_dragOffset! > widget.dragThreshold) {
_targetStop = min(_currentStop! + 1, _stops!.length - 1);
} else if (_dragOffset! < -widget.dragThreshold) {
_targetStop = max(canClose ? -1 : 0, _currentStop! - 1);
}
if (_targetStop! < 0) {
close();
} else {
_dragging = false;
_animate(_stops![_currentStop!] + _dragOffset!, _stops![_targetStop!]);
}
},
child: widget.builder!(context, _scrollController!, _scrollPhysics!, _currentStop!),
),
builder: (BuildContext context, Widget? child) {
return SizedBox(
height: min(_stops![_stops!.length - 1], max(0, height)),
child: ClipRRect(child: Container(color: Colors.transparent, child: child)),
);
}),
);
}
}

View File

@@ -0,0 +1,3 @@
export 'base.dart';
export 'basic.sheet.dart';
export 'scroller.sheet.dart';